From eba89fb1d16b33fa65b49bbf7cc0109f4d4a6a65 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 28 Jan 2013 16:46:44 -0200 Subject: Add CouchServerState for serving Soledad as WSGI app. --- src/leap/soledad/__init__.py | 5 ++- src/leap/soledad/backends/couch.py | 53 ++++++++++++++++++++++++++++++++ src/leap/soledad/backends/objectstore.py | 9 ++++-- 3 files changed, 62 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py index c83627f0..2ed1a369 100644 --- a/src/leap/soledad/__init__.py +++ b/src/leap/soledad/__init__.py @@ -8,7 +8,6 @@ import random import hmac from leap.soledad.backends import sqlcipher from leap.soledad.util import GPGWrapper -import util class Soledad(object): @@ -28,7 +27,7 @@ class Soledad(object): os.makedirs(self.PREFIX) if not gpghome: gpghome = self.GNUPG_HOME - self._gpg = util.GPGWrapper(gpghome=gpghome) + self._gpg = GPGWrapper(gpghome=gpghome) # load/generate OpenPGP keypair if not self._has_openpgp_keypair(): self._gen_openpgp_keypair() @@ -64,7 +63,7 @@ class Soledad(object): try: with open(self.SECRET_PATH) as f: self._secret = str(self._gpg.decrypt(f.read())) - except IOError as e: + except IOError: raise IOError('Failed to open secret file %s.' % self.SECRET_PATH) def _gen_secret(self): diff --git a/src/leap/soledad/backends/couch.py b/src/leap/soledad/backends/couch.py index c8dadfa8..56b12574 100644 --- a/src/leap/soledad/backends/couch.py +++ b/src/leap/soledad/backends/couch.py @@ -1,10 +1,16 @@ +# general imports import uuid from base64 import b64encode, b64decode +import re +# u1db from u1db import errors from u1db.sync import LocalSyncTarget from u1db.backends.inmemory import InMemoryIndex +from u1db.remote.server_state import ServerState +# couchdb from couchdb.client import Server, Document as CouchDocument from couchdb.http import ResourceNotFound +# leap from leap.soledad.backends.objectstore import ObjectStore from leap.soledad.backends.leap_backend import LeapDocument @@ -14,9 +20,29 @@ except ImportError: import json # noqa +class InvalidURLError(Exception): + pass + + class CouchDatabase(ObjectStore): """A U1DB implementation that uses Couch as its persistence layer.""" + @classmethod + def open_database(cls, url, create): + # get database from url + m = re.match('(.*)/([^/]+)$', url) + if not m: + raise InvalidURLError + url = m.group(1) + dbname = m.group(2) + server = Server(url=url) + try: + server[dbname] + except ResourceNotFound: + if not create: + raise + return cls(url, dbname) + def __init__(self, url, database, replica_uid=None, full_commit=True, session=None): """Create a new Couch data container.""" @@ -215,3 +241,30 @@ class CouchSyncTarget(LocalSyncTarget): self._db._set_replica_gen_and_trans_id( source_replica_uid, source_replica_generation, source_replica_transaction_id) + + +class CouchServerState(ServerState): + + def open_database(self, path): + """ + Open a database at the given location. + """ + return CouchDatabase.open_database(path, create=False) + + def check_database(self, path): + """ + Check if the database at the given location exists. + """ + db = self.open_database(path) + db.close() + + def ensure_database(self, path): + """Ensure database at the given location.""" + db = CouchDatabase.open_database(path, + create=True) + return db, db._replica_uid + + def delete_database(self, path): + """Delete database at the given location.""" + db = CouchDatabase.open_database(path) + db.delete_database() diff --git a/src/leap/soledad/backends/objectstore.py b/src/leap/soledad/backends/objectstore.py index 588fc7a1..199107af 100644 --- a/src/leap/soledad/backends/objectstore.py +++ b/src/leap/soledad/backends/objectstore.py @@ -7,8 +7,13 @@ class ObjectStore(InMemoryDatabase): A backend for storing u1db data in an object store. """ - def __init__(self, replica_uid=None): - super(ObjectStore, self).__init__(replica_uid) + @classmethod + def open_database(cls, url, create, document_factory=None): + raise NotImplementedError(cls.open_database) + + def __init__(self, replica_uid=None, document_factory=None): + super(ObjectStore, self).__init__(replica_uid, + document_factory=document_factory) # sync data in memory with data in object store if not self._get_doc(self.U1DB_DATA_DOC_ID): self._init_u1db_data() -- cgit v1.2.3 From 4b0dc38d6eaa970b49cadbf5c0174eb7c34197fc Mon Sep 17 00:00:00 2001 From: antialias Date: Wed, 30 Jan 2013 13:26:18 -0500 Subject: pep8. --- src/leap/email/smtp/smtprelay.py | 4 ++-- src/leap/email/smtp/tests/test_smtprelay.py | 2 +- src/leap/soledad/tests/test_couch.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/leap/email/smtp/smtprelay.py b/src/leap/email/smtp/smtprelay.py index 7a647717..fdb8eb91 100644 --- a/src/leap/email/smtp/smtprelay.py +++ b/src/leap/email/smtp/smtprelay.py @@ -106,7 +106,7 @@ class EncryptedMessage(): """Separate message headers from body.""" sep = self.lines.index('') self.headers = self.lines[:sep] - self.body = self.lines[sep+1:] + self.body = self.lines[sep + 1:] def connectionLost(self): log.msg("Connection lost unexpectedly!") @@ -128,7 +128,7 @@ class EncryptedMessage(): def sendMessage(self): self.prepareHeader() - msg = '\n'.join(self.headers+[self.cyphertext]) + msg = '\n'.join(self.headers + [self.cyphertext]) d = defer.Deferred() factory = smtp.ESMTPSenderFactory(self.smtp_username, self.smtp_password, diff --git a/src/leap/email/smtp/tests/test_smtprelay.py b/src/leap/email/smtp/tests/test_smtprelay.py index 109e253b..1c549eb3 100644 --- a/src/leap/email/smtp/tests/test_smtprelay.py +++ b/src/leap/email/smtp/tests/test_smtprelay.py @@ -60,7 +60,7 @@ class TestSmtpRelay(tests.OpenPGPTestCase): for i, line in enumerate(self.EMAIL_DATA): proto.lineReceived(line + '\r\n') self.assertMatch(transport.value(), - '\r\n'.join(SMTP_ANSWERS[0:i+1])) + '\r\n'.join(SMTP_ANSWERS[0:i + 1])) proto.setTimeout(None) def test_message_encrypt(self): diff --git a/src/leap/soledad/tests/test_couch.py b/src/leap/soledad/tests/test_couch.py index b5d6378c..02399e4c 100644 --- a/src/leap/soledad/tests/test_couch.py +++ b/src/leap/soledad/tests/test_couch.py @@ -110,7 +110,7 @@ class CouchDBTestCase(unittest.TestCase): class TestCouchBackendImpl(CouchDBTestCase): def test__allocate_doc_id(self): - db = couch.CouchDatabase('http://localhost:'+str(self.wrapper.port), + db = couch.CouchDatabase('http://localhost:' + str(self.wrapper.port), 'u1db_tests') doc_id1 = db._allocate_doc_id() self.assertTrue(doc_id1.startswith('D-')) @@ -125,13 +125,13 @@ class TestCouchBackendImpl(CouchDBTestCase): def make_couch_database_for_test(test, replica_uid): port = str(test.wrapper.port) - return couch.CouchDatabase('http://localhost:'+port, replica_uid, + return couch.CouchDatabase('http://localhost:' + port, replica_uid, replica_uid=replica_uid or 'test') def copy_couch_database_for_test(test, db): port = str(test.wrapper.port) - new_db = couch.CouchDatabase('http://localhost:'+port, + new_db = couch.CouchDatabase('http://localhost:' + port, db._replica_uid + '_copy', replica_uid=db._replica_uid or 'test') gen, docs = db.get_all_docs(include_deleted=True) -- cgit v1.2.3 From 8e5c1a50b4d6e57860a63fe5b52d56a4c205345d Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 31 Jan 2013 05:47:03 +0900 Subject: add underscore to the $HOME pattern exception --- src/leap/base/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/base/config.py b/src/leap/base/config.py index 6a13db7d..d796bcf1 100644 --- a/src/leap/base/config.py +++ b/src/leap/base/config.py @@ -282,7 +282,7 @@ def get_config_dir(): @rtype: string """ home = os.path.expanduser("~") - if re.findall("leap_tests-[a-zA-Z0-9]{6}", home): + if re.findall("leap_tests-[_a-zA-Z0-9]{6}", home): # we're inside a test! :) return os.path.join(home, ".config/leap") else: -- cgit v1.2.3 From 35c77f1e762acde8e5f635a99f9fad9e706d7d4f Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 31 Jan 2013 05:39:47 +0900 Subject: fix tests (resources hash + argparse) --- src/leap/gui/tests/test_mainwindow_rc.py | 2 +- src/leap/util/tests/test_leap_argparse.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/gui/tests/test_mainwindow_rc.py b/src/leap/gui/tests/test_mainwindow_rc.py index 67b9fae0..9f5172f7 100644 --- a/src/leap/gui/tests/test_mainwindow_rc.py +++ b/src/leap/gui/tests/test_mainwindow_rc.py @@ -26,7 +26,7 @@ class MainWindowResourcesTest(unittest.TestCase): def test_mainwindow_resources_hash(self): self.assertEqual( hashlib.md5(mainwindow_rc.qt_resource_data).hexdigest(), - '53e196f29061d8f08f112e5a2e64eb53') + 'e04cb467985ba38b9eb91e7689f9458f') if __name__ == "__main__": unittest.main() diff --git a/src/leap/util/tests/test_leap_argparse.py b/src/leap/util/tests/test_leap_argparse.py index 082919b7..4e2b811f 100644 --- a/src/leap/util/tests/test_leap_argparse.py +++ b/src/leap/util/tests/test_leap_argparse.py @@ -24,11 +24,11 @@ class LeapArgParseTest(unittest.TestCase): self.assertEqual( opts, Namespace( - config_file=None, debug=True, log_file=None, - no_provider_checks=False, - no_ca_verify=False, + #config_file=None, + #no_provider_checks=False, + #no_ca_verify=False, openvpn_verb=None)) if __name__ == "__main__": -- cgit v1.2.3 From d936fbf05756b40aeb94c595ff48f5e74b07d49c Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 31 Jan 2013 14:18:09 -0200 Subject: Add ability to close Soledad instance. --- src/leap/soledad/__init__.py | 4 ++++ src/leap/soledad/tests/__init__.py | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py index 2ed1a369..c9c981c2 100644 --- a/src/leap/soledad/__init__.py +++ b/src/leap/soledad/__init__.py @@ -41,6 +41,9 @@ class Soledad(object): # for symmetric encryption. self._db = sqlcipher.open(self.LOCAL_DB_PATH, True, self._secret) + def close(self): + self._db.close() + #------------------------------------------------------------------------- # Management of secret for symmetric encryption #------------------------------------------------------------------------- @@ -63,6 +66,7 @@ class Soledad(object): try: with open(self.SECRET_PATH) as f: self._secret = str(self._gpg.decrypt(f.read())) + f.close() except IOError: raise IOError('Failed to open secret file %s.' % self.SECRET_PATH) diff --git a/src/leap/soledad/tests/__init__.py b/src/leap/soledad/tests/__init__.py index 890c4d2a..3bb66680 100644 --- a/src/leap/soledad/tests/__init__.py +++ b/src/leap/soledad/tests/__init__.py @@ -28,7 +28,9 @@ class BaseSoledadTest(BaseLeapTest): self._soledad._gpg.import_keys(PRIVATE_KEY) def tearDown(self): - pass + self._db1.close() + self._db2.close() + self._soledad.close() # Key material for testing -- cgit v1.2.3 From ec0afdd1cf01f19eb80f8275bfef6ad4f61413c8 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 31 Jan 2013 15:51:54 -0200 Subject: Add support to test_scenarios to test_sqlcipher. --- src/leap/soledad/tests/test_sqlcipher.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/leap/soledad/tests/test_sqlcipher.py b/src/leap/soledad/tests/test_sqlcipher.py index a3ab35b6..0d5d00ee 100644 --- a/src/leap/soledad/tests/test_sqlcipher.py +++ b/src/leap/soledad/tests/test_sqlcipher.py @@ -372,3 +372,5 @@ class SQLCipherEncryptionTest(unittest.TestCase): "non-encrypted dbs.") except DatabaseIsNotEncrypted: pass + +load_tests = tests.load_with_scenarios -- cgit v1.2.3 From 43eae005265333802ff1fa7180f7b5ae0833a79e Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 31 Jan 2013 15:53:07 -0200 Subject: Add LeapServerState for interfacing with couch. --- src/leap/soledad/backends/leap_backend.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'src') diff --git a/src/leap/soledad/backends/leap_backend.py b/src/leap/soledad/backends/leap_backend.py index ec26dca4..e1acabec 100644 --- a/src/leap/soledad/backends/leap_backend.py +++ b/src/leap/soledad/backends/leap_backend.py @@ -7,6 +7,7 @@ from u1db import Document from u1db.remote import utils from u1db.remote.http_target import HTTPSyncTarget from u1db.remote.http_database import HTTPDatabase +from u1db.remote.server_state import ServerState from u1db.errors import BrokenSyncStream import uuid @@ -108,6 +109,10 @@ class LeapDatabase(HTTPDatabase): class LeapSyncTarget(HTTPSyncTarget): + """ + A SyncTarget that encrypts data before sending and decrypts data after + receiving. + """ def __init__(self, url, creds=None, soledad=None): super(LeapSyncTarget, self).__init__(url, creds) @@ -196,3 +201,26 @@ class LeapSyncTarget(HTTPSyncTarget): res = self._parse_sync_stream(data, return_doc_cb, ensure_callback) data = None return res['new_generation'], res['new_transaction_id'] + + +class LeapServerState(ServerState): + """ + Inteface of the WSGI server with the CouchDB backend. + """ + + def __init__(self): + pass + + def open_database(self, url): + # TODO: open couch + from leap.soledad.backends.couch import CouchDatabase + return CouchDatabase(url, create=False) + + def ensure_database(self, url): + from leap.soledad.backends.couch import CouchDatabase + db = CouchDatabase(url, create=True) + return db, db._replica_uid + + def delete_database(self, url): + from leap.soledad.backends.couch import CouchDatabase + CouchDatabase.delete_database(url) -- cgit v1.2.3 From 123c035b421dcb62a9cd36884f1f0761938899a0 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 31 Jan 2013 15:53:50 -0200 Subject: Add basic Soledad server with no support for new backends yet. --- src/leap/soledad/server.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/leap/soledad/server.py (limited to 'src') diff --git a/src/leap/soledad/server.py b/src/leap/soledad/server.py new file mode 100644 index 00000000..708e2e8c --- /dev/null +++ b/src/leap/soledad/server.py @@ -0,0 +1,22 @@ +""" +An u1db server that stores data using couchdb. + +This should be run with: + twistd -n web --wsgi=leap.soledad.server.application +""" + +from twisted.web.wsgi import WSGIResource +from twisted.internet import reactor + +from u1db.remote import ( + http_app, + server_state, +) + +state = server_state.ServerState() +# TODO: change working dir to something meaningful +state.set_workingdir('/tmp') +# TODO: write a LeapHTTPApp that will use Couch as backend instead of SQLite +application = http_app.HTTPApp(state) + +resource = WSGIResource(reactor, reactor.getThreadPool(), application) -- cgit v1.2.3 From 5505f1397f7130b9731e51cb52abd820a8564cb8 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 31 Jan 2013 18:15:02 -0200 Subject: Soledad server can store using CouchDB. --- src/leap/soledad/backends/couch.py | 48 +++++++++++++++---------------- src/leap/soledad/backends/leap_backend.py | 23 --------------- src/leap/soledad/server.py | 10 +++---- 3 files changed, 27 insertions(+), 54 deletions(-) (limited to 'src') diff --git a/src/leap/soledad/backends/couch.py b/src/leap/soledad/backends/couch.py index 56b12574..11122aa8 100644 --- a/src/leap/soledad/backends/couch.py +++ b/src/leap/soledad/backends/couch.py @@ -7,6 +7,7 @@ from u1db import errors from u1db.sync import LocalSyncTarget from u1db.backends.inmemory import InMemoryIndex from u1db.remote.server_state import ServerState +from u1db.errors import DatabaseDoesNotExist # couchdb from couchdb.client import Server, Document as CouchDocument from couchdb.http import ResourceNotFound @@ -30,7 +31,7 @@ class CouchDatabase(ObjectStore): @classmethod def open_database(cls, url, create): # get database from url - m = re.match('(.*)/([^/]+)$', url) + m = re.match('(^https?://[^/]+)/(.+)$', url) if not m: raise InvalidURLError url = m.group(1) @@ -40,8 +41,8 @@ class CouchDatabase(ObjectStore): server[dbname] except ResourceNotFound: if not create: - raise - return cls(url, dbname) + raise DatabaseDoesNotExist() + return cls(url, dbname) def __init__(self, url, database, replica_uid=None, full_commit=True, session=None): @@ -242,29 +243,26 @@ class CouchSyncTarget(LocalSyncTarget): source_replica_uid, source_replica_generation, source_replica_transaction_id) - class CouchServerState(ServerState): - - def open_database(self, path): - """ - Open a database at the given location. - """ - return CouchDatabase.open_database(path, create=False) - - def check_database(self, path): - """ - Check if the database at the given location exists. - """ - db = self.open_database(path) - db.close() - - def ensure_database(self, path): - """Ensure database at the given location.""" - db = CouchDatabase.open_database(path, + """ + Inteface of the WSGI server with the CouchDB backend. + """ + + def __init__(self, couch_url): + self.couch_url = couch_url + + def open_database(self, dbname): + # TODO: open couch + from leap.soledad.backends.couch import CouchDatabase + return CouchDatabase.open_database(self.couch_url + '/' + dbname, + create=False) + + def ensure_database(self, dbname): + from leap.soledad.backends.couch import CouchDatabase + db = CouchDatabase.open_database(self.couch_url + '/' + dbname, create=True) return db, db._replica_uid - def delete_database(self, path): - """Delete database at the given location.""" - db = CouchDatabase.open_database(path) - db.delete_database() + def delete_database(self, dbname): + from leap.soledad.backends.couch import CouchDatabase + CouchDatabase.delete_database(self.couch_url + '/' + dbname) diff --git a/src/leap/soledad/backends/leap_backend.py b/src/leap/soledad/backends/leap_backend.py index e1acabec..f9d37e19 100644 --- a/src/leap/soledad/backends/leap_backend.py +++ b/src/leap/soledad/backends/leap_backend.py @@ -7,7 +7,6 @@ from u1db import Document from u1db.remote import utils from u1db.remote.http_target import HTTPSyncTarget from u1db.remote.http_database import HTTPDatabase -from u1db.remote.server_state import ServerState from u1db.errors import BrokenSyncStream import uuid @@ -202,25 +201,3 @@ class LeapSyncTarget(HTTPSyncTarget): data = None return res['new_generation'], res['new_transaction_id'] - -class LeapServerState(ServerState): - """ - Inteface of the WSGI server with the CouchDB backend. - """ - - def __init__(self): - pass - - def open_database(self, url): - # TODO: open couch - from leap.soledad.backends.couch import CouchDatabase - return CouchDatabase(url, create=False) - - def ensure_database(self, url): - from leap.soledad.backends.couch import CouchDatabase - db = CouchDatabase(url, create=True) - return db, db._replica_uid - - def delete_database(self, url): - from leap.soledad.backends.couch import CouchDatabase - CouchDatabase.delete_database(url) diff --git a/src/leap/soledad/server.py b/src/leap/soledad/server.py index 708e2e8c..4fc97be5 100644 --- a/src/leap/soledad/server.py +++ b/src/leap/soledad/server.py @@ -7,13 +7,11 @@ This should be run with: from twisted.web.wsgi import WSGIResource from twisted.internet import reactor +from u1db.remote import http_app +from leap.soledad.backends.couch import CouchServerState -from u1db.remote import ( - http_app, - server_state, -) - -state = server_state.ServerState() +couch_url = 'http://localhost:5984' +state = CouchServerState(couch_url) # TODO: change working dir to something meaningful state.set_workingdir('/tmp') # TODO: write a LeapHTTPApp that will use Couch as backend instead of SQLite -- cgit v1.2.3 From fb13d4d6ea63c92cc97e81d61a556de8001b4ba1 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 4 Feb 2013 19:34:23 -0200 Subject: Close file handles. --- src/leap/soledad/__init__.py | 1 - src/leap/soledad/tests/test_couch.py | 8 +++++--- src/leap/soledad/tests/test_sqlcipher.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py index c9c981c2..688ee160 100644 --- a/src/leap/soledad/__init__.py +++ b/src/leap/soledad/__init__.py @@ -66,7 +66,6 @@ class Soledad(object): try: with open(self.SECRET_PATH) as f: self._secret = str(self._gpg.decrypt(f.read())) - f.close() except IOError: raise IOError('Failed to open secret file %s.' % self.SECRET_PATH) diff --git a/src/leap/soledad/tests/test_couch.py b/src/leap/soledad/tests/test_couch.py index b5d6378c..6eb5501c 100644 --- a/src/leap/soledad/tests/test_couch.py +++ b/src/leap/soledad/tests/test_couch.py @@ -42,6 +42,7 @@ class CouchDBWrapper(object): conf = handle.read() % { 'tempdir': self.tempdir, } + handle.close() confPath = os.path.join(self.tempdir, 'test.ini') handle = open(confPath, 'w') @@ -51,10 +52,10 @@ class CouchDBWrapper(object): # create the dirs from the template os.mkdir(os.path.join(self.tempdir, 'lib')) os.mkdir(os.path.join(self.tempdir, 'log')) - argus = ['couchdb', '-n' '-a', confPath] - null = open('/dev/null', 'w') + args = ['couchdb', '-n' '-a', confPath] + #null = open('/dev/null', 'w') self.process = subprocess.Popen( - argus, env=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + args, env=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # find port logPath = os.path.join(self.tempdir, 'log', 'couch.log') while not os.path.exists(logPath): @@ -75,6 +76,7 @@ stderr: handle = open(logPath) line = handle.read() + handle.close() m = PORT_RE.search(line) if not m: self.stop() diff --git a/src/leap/soledad/tests/test_sqlcipher.py b/src/leap/soledad/tests/test_sqlcipher.py index 0d5d00ee..18b9173e 100644 --- a/src/leap/soledad/tests/test_sqlcipher.py +++ b/src/leap/soledad/tests/test_sqlcipher.py @@ -3,7 +3,7 @@ import os import time from sqlite3 import dbapi2, DatabaseError -import unittest2 as unittest +import unittest from StringIO import StringIO import threading -- cgit v1.2.3 From 6328511e86429bcd23f6ca179ef10114aa69a4e9 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 6 Feb 2013 16:52:53 -0200 Subject: Remove OpenStack backend from soledad. --- src/leap/soledad/README | 14 +++-- src/leap/soledad/backends/openstack.py | 98 ---------------------------------- 2 files changed, 6 insertions(+), 106 deletions(-) delete mode 100644 src/leap/soledad/backends/openstack.py (limited to 'src') diff --git a/src/leap/soledad/README b/src/leap/soledad/README index 3bf62494..b14d5932 100644 --- a/src/leap/soledad/README +++ b/src/leap/soledad/README @@ -9,16 +9,14 @@ Dependencies Soledad depends on the following python libraries: * u1db 0.1.4 [1] - * python-swiftclient 1.2.0 [2] - * python-gnupg 0.3.1 [3] - * CouchDB 0.8 [4] - * hmac 20101005 [5] + * python-gnupg 0.3.1 [2] + * CouchDB 0.8 [3] + * hmac 20101005 [4] [1] http://pypi.python.org/pypi/u1db/0.1.4 -[2] http://pypi.python.org/pypi/python-swiftclient/1.2.0 -[3] http://pypi.python.org/pypi/python-gnupg/0.3.1 -[4] http://pypi.python.org/pypi/CouchDB/0.8 -[5] http://pypi.python.org/pypi/hmac/20101005 +[2] http://pypi.python.org/pypi/python-gnupg/0.3.1 +[3] http://pypi.python.org/pypi/CouchDB/0.8 +[4] http://pypi.python.org/pypi/hmac/20101005 Tests diff --git a/src/leap/soledad/backends/openstack.py b/src/leap/soledad/backends/openstack.py deleted file mode 100644 index a9615736..00000000 --- a/src/leap/soledad/backends/openstack.py +++ /dev/null @@ -1,98 +0,0 @@ -# TODO: this backend is not tested yet. -from u1db.remote.http_target import HTTPSyncTarget -import swiftclient -from soledad.backends.objectstore import ObjectStore - - -class OpenStackDatabase(ObjectStore): - """A U1DB implementation that uses OpenStack as its persistence layer.""" - - def __init__(self, auth_url, user, auth_key, container): - """Create a new OpenStack data container.""" - self._auth_url = auth_url - self._user = user - self._auth_key = auth_key - self._container = container - self._connection = swiftclient.Connection(self._auth_url, self._user, - self._auth_key) - self._get_auth() - # this will ensure transaction and sync logs exist and are up-to-date. - super(OpenStackDatabase, self).__init__() - - #------------------------------------------------------------------------- - # implemented methods from Database - #------------------------------------------------------------------------- - - def _get_doc(self, doc_id, check_for_conflicts=False): - """Get just the document content, without fancy handling. - - Conflicts do not happen on server side, so there's no need to check - for them. - """ - try: - response, contents = self._connection.get_object(self._container, - doc_id) - # TODO: change revision to be a dictionary element? - rev = response['x-object-meta-rev'] - return self._factory(doc_id, rev, contents) - except swiftclient.ClientException: - return None - - def get_all_docs(self, include_deleted=False): - """Get all documents from the database.""" - generation = self._get_generation() - results = [] - _, doc_ids = self._connection.get_container(self._container, - full_listing=True) - for doc_id in doc_ids: - doc = self._get_doc(doc_id) - if doc.content is None and not include_deleted: - continue - results.append(doc) - return (generation, results) - - def _put_doc(self, doc, new_rev): - new_rev = self._allocate_doc_rev(doc.rev) - # TODO: change revision to be a dictionary element? - headers = {'X-Object-Meta-Rev': new_rev} - self._connection.put_object(self._container, doc_id, doc.get_json(), - headers=headers) - - def get_sync_target(self): - return OpenStackSyncTarget(self) - - def close(self): - raise NotImplementedError(self.close) - - def sync(self, url, creds=None, autocreate=True): - from u1db.sync import Synchronizer - from u1db.remote.http_target import OpenStackSyncTarget - return Synchronizer(self, OpenStackSyncTarget(url, creds=creds)).sync( - autocreate=autocreate) - - #------------------------------------------------------------------------- - # OpenStack specific methods - #------------------------------------------------------------------------- - - def _get_auth(self): - self._url, self._auth_token = self._connection.get_auth() - return self._url, self.auth_token - - -class OpenStackSyncTarget(HTTPSyncTarget): - - def get_sync_info(self, source_replica_uid): - source_gen, source_trans_id = self._db._get_replica_gen_and_trans_id( - source_replica_uid) - my_gen, my_trans_id = self._db._get_generation_info() - return ( - self._db._replica_uid, my_gen, my_trans_id, source_gen, - source_trans_id) - - def record_sync_info(self, source_replica_uid, source_replica_generation, - source_replica_transaction_id): - if self._trace_hook: - self._trace_hook('record_sync_info') - self._db._set_replica_gen_and_trans_id( - source_replica_uid, source_replica_generation, - source_replica_transaction_id) -- cgit v1.2.3 From 4a0ecb6832f28453976757fe4a7c4a5f1f811105 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 6 Feb 2013 18:01:58 -0200 Subject: SQLCipher syncs using LeapSyncTarget. --- src/leap/soledad/backends/sqlcipher.py | 7 +++++-- src/leap/soledad/tests/test_couch.py | 2 ++ src/leap/soledad/tests/test_sqlcipher.py | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/soledad/backends/sqlcipher.py b/src/leap/soledad/backends/sqlcipher.py index 6cebcf7d..354fcd31 100644 --- a/src/leap/soledad/backends/sqlcipher.py +++ b/src/leap/soledad/backends/sqlcipher.py @@ -131,8 +131,11 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase): """ from u1db.sync import Synchronizer from leap.soledad.backends.leap_backend import LeapSyncTarget - return Synchronizer(self, LeapSyncTarget(url, creds=creds), - soledad=self._soledad).sync(autocreate=autocreate) + return Synchronizer( + self, + LeapSyncTarget(url, + creds=creds, + soledad=self._soledad)).sync(autocreate=autocreate) def _extra_schema_init(self, c): c.execute( diff --git a/src/leap/soledad/tests/test_couch.py b/src/leap/soledad/tests/test_couch.py index b5d6378c..a9b2f39e 100644 --- a/src/leap/soledad/tests/test_couch.py +++ b/src/leap/soledad/tests/test_couch.py @@ -27,6 +27,8 @@ import time import unittest +# from: https://github.com/smcq/paisley/blob/master/paisley/test/util.py +# TODO: include license of above project. class CouchDBWrapper(object): """ Wrapper for external CouchDB instance which is started and stopped for diff --git a/src/leap/soledad/tests/test_sqlcipher.py b/src/leap/soledad/tests/test_sqlcipher.py index 0d5d00ee..b964f51e 100644 --- a/src/leap/soledad/tests/test_sqlcipher.py +++ b/src/leap/soledad/tests/test_sqlcipher.py @@ -27,6 +27,7 @@ from leap.soledad.tests import u1db_tests as tests from leap.soledad.tests.u1db_tests import test_sqlite_backend from leap.soledad.tests.u1db_tests import test_backends from leap.soledad.tests.u1db_tests import test_open +from leap.soledad.tests.u1db_tests import test_sync PASSWORD = '123456' @@ -321,6 +322,23 @@ class SQLCipherOpen(test_open.TestU1DBOpen): self.assertIsInstance(db2, SQLCipherDatabase) +#----------------------------------------------------------------------------- +# The following tests come from `u1db.tests.test_sync`. +#----------------------------------------------------------------------------- + +sync_scenarios = [] +for name, scenario in SQLCIPHER_SCENARIOS: + scenario = dict(scenario) + scenario['do_sync'] = test_sync.sync_via_synchronizer + sync_scenarios.append((name, scenario)) + scenario = dict(scenario) + + +class SQLCipherDatabaseSyncTests(test_sync.DatabaseSyncTests): + + scenarios = sync_scenarios + + #----------------------------------------------------------------------------- # Tests for actual encryption of the database #----------------------------------------------------------------------------- @@ -373,4 +391,5 @@ class SQLCipherEncryptionTest(unittest.TestCase): except DatabaseIsNotEncrypted: pass + load_tests = tests.load_with_scenarios -- cgit v1.2.3 From c1289d25f25814e1b34bb1ccf3e55b809d0c5f96 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 6 Feb 2013 19:14:50 -0200 Subject: Add test scenario for SQLCipher backend using LeapSyncTarget. --- src/leap/soledad/backends/couch.py | 1 + src/leap/soledad/backends/leap_backend.py | 1 - src/leap/soledad/tests/test_sqlcipher.py | 22 ++++++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/soledad/backends/couch.py b/src/leap/soledad/backends/couch.py index 11122aa8..8757f5af 100644 --- a/src/leap/soledad/backends/couch.py +++ b/src/leap/soledad/backends/couch.py @@ -243,6 +243,7 @@ class CouchSyncTarget(LocalSyncTarget): source_replica_uid, source_replica_generation, source_replica_transaction_id) + class CouchServerState(ServerState): """ Inteface of the WSGI server with the CouchDB backend. diff --git a/src/leap/soledad/backends/leap_backend.py b/src/leap/soledad/backends/leap_backend.py index f9d37e19..41027e50 100644 --- a/src/leap/soledad/backends/leap_backend.py +++ b/src/leap/soledad/backends/leap_backend.py @@ -200,4 +200,3 @@ class LeapSyncTarget(HTTPSyncTarget): res = self._parse_sync_stream(data, return_doc_cb, ensure_callback) data = None return res['new_generation'], res['new_transaction_id'] - diff --git a/src/leap/soledad/tests/test_sqlcipher.py b/src/leap/soledad/tests/test_sqlcipher.py index b964f51e..85b9e486 100644 --- a/src/leap/soledad/tests/test_sqlcipher.py +++ b/src/leap/soledad/tests/test_sqlcipher.py @@ -11,6 +11,7 @@ import threading from u1db import ( errors, query_parser, + sync, ) from u1db.backends.sqlite_backend import SQLitePartialExpandDatabase @@ -28,6 +29,7 @@ from leap.soledad.tests.u1db_tests import test_sqlite_backend from leap.soledad.tests.u1db_tests import test_backends from leap.soledad.tests.u1db_tests import test_open from leap.soledad.tests.u1db_tests import test_sync +from leap.soledad.backends.leap_backend import LeapSyncTarget PASSWORD = '123456' @@ -334,6 +336,26 @@ for name, scenario in SQLCIPHER_SCENARIOS: scenario = dict(scenario) +def sync_via_synchronizer_and_leap(test, db_source, db_target, + trace_hook=None, trace_hook_shallow=None): + if trace_hook: + test.skipTest("full trace hook unsupported over http") + path = test._http_at[db_target] + target = LeapSyncTarget.connect(test.getURL(path)) + if trace_hook_shallow: + target._set_trace_hook_shallow(trace_hook_shallow) + return sync.Synchronizer(db_source, target).sync() + + +sync_scenarios.append(('pyleap', { + 'make_database_for_test': test_sync.make_database_for_http_test, + 'copy_database_for_test': test_sync.copy_database_for_http_test, + 'make_document_for_test': tests.make_document_for_test, + 'make_app_with_state': tests.test_remote_sync_target.make_http_app, + 'do_sync': sync_via_synchronizer_and_leap, +})) + + class SQLCipherDatabaseSyncTests(test_sync.DatabaseSyncTests): scenarios = sync_scenarios -- cgit v1.2.3 From 634f82d95984b4bb78cbfdd52e6e75116c8df53d Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 6 Feb 2013 20:01:31 -0200 Subject: Add DatabaseSyncTargetTests to SQLCipher tests. --- src/leap/soledad/tests/test_sqlcipher.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'src') diff --git a/src/leap/soledad/tests/test_sqlcipher.py b/src/leap/soledad/tests/test_sqlcipher.py index 85b9e486..042ee4ff 100644 --- a/src/leap/soledad/tests/test_sqlcipher.py +++ b/src/leap/soledad/tests/test_sqlcipher.py @@ -361,6 +361,26 @@ class SQLCipherDatabaseSyncTests(test_sync.DatabaseSyncTests): scenarios = sync_scenarios +def _make_local_db_and_leap_target(test, path='test'): + test.startServer() + db = test.request_state._create_database(os.path.basename(path)) + st = LeapSyncTarget.connect(test.getURL(path)) + return db, st + + +target_scenarios = [ + ('leap', { + 'create_db_and_target': _make_local_db_and_leap_target, + 'make_app_with_state': tests.test_remote_sync_target.make_http_app}), +] + + +class SQLCipherSyncTargetTests(test_sync.DatabaseSyncTargetTests): + + scenarios = (tests.multiply_scenarios(SQLCIPHER_SCENARIOS, + target_scenarios)) + + #----------------------------------------------------------------------------- # Tests for actual encryption of the database #----------------------------------------------------------------------------- -- cgit v1.2.3 From a0f36427f9473c7f2d759b9d0605b72d2c401731 Mon Sep 17 00:00:00 2001 From: drebs Date: Sat, 9 Feb 2013 19:25:44 -0200 Subject: Enforce doc.rev as bytes to match Twisted expectations. --- src/leap/soledad/backends/couch.py | 4 ++-- src/leap/soledad/backends/leap_backend.py | 20 +++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/soledad/backends/couch.py b/src/leap/soledad/backends/couch.py index 8757f5af..d349efaf 100644 --- a/src/leap/soledad/backends/couch.py +++ b/src/leap/soledad/backends/couch.py @@ -56,13 +56,13 @@ class CouchDatabase(ObjectStore): self._dbname = database # this will ensure that transaction and sync logs exist and are # up-to-date. - self.set_document_factory(LeapDocument) try: self._database = self._server[database] except ResourceNotFound: self._server.create(database) self._database = self._server[database] - super(CouchDatabase, self).__init__(replica_uid=replica_uid) + super(CouchDatabase, self).__init__(replica_uid=replica_uid, + document_factory=LeapDocument) #------------------------------------------------------------------------- # methods from Database diff --git a/src/leap/soledad/backends/leap_backend.py b/src/leap/soledad/backends/leap_backend.py index 41027e50..d3ae6db6 100644 --- a/src/leap/soledad/backends/leap_backend.py +++ b/src/leap/soledad/backends/leap_backend.py @@ -22,6 +22,8 @@ class NoSoledadInstance(Exception): class LeapDocument(Document): """ + Encryptable and syncable document. + LEAP Documents are standard u1db documents with cabability of returning an encrypted version of the document json string as well as setting document content based on an encrypted version of json string. @@ -37,7 +39,7 @@ class LeapDocument(Document): def get_encrypted_json(self): """ - Returns document's json serialization encrypted with user's public key. + Return document's json serialization encrypted with user's public key. """ if not self._soledad: raise NoSoledadInstance() @@ -67,6 +69,22 @@ class LeapDocument(Document): doc="Determine if document should be synced with server." ) + # Returning the revision as string solves the following exception in + # Twisted web: + # exceptions.TypeError: Can only pass-through bytes on Python 2 + def _get_rev(self): + if self._rev is None: + return None + return str(self._rev) + + def _set_rev(self, rev): + self._rev = rev + + rev = property( + _get_rev, + _set_rev, + doc="Wrapper to ensure `doc.rev` is always returned as bytes.") + class LeapDatabase(HTTPDatabase): """Implement the HTTP remote database API to a Leap server.""" -- cgit v1.2.3 From a4a46d4a9cfc22324cb8a2037a5c4825f14b4b1b Mon Sep 17 00:00:00 2001 From: drebs Date: Sat, 9 Feb 2013 20:22:25 -0200 Subject: Soledad may be created without gpg/secret initialization. --- src/leap/soledad/__init__.py | 6 +++++- src/leap/soledad/tests/__init__.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py index 688ee160..6893c7a8 100644 --- a/src/leap/soledad/__init__.py +++ b/src/leap/soledad/__init__.py @@ -21,13 +21,17 @@ class Soledad(object): # other configs SECRET_LENGTH = 50 - def __init__(self, user_email, gpghome=None): + def __init__(self, user_email, gpghome=None, initialize=True): self._user_email = user_email if not os.path.isdir(self.PREFIX): os.makedirs(self.PREFIX) if not gpghome: gpghome = self.GNUPG_HOME self._gpg = GPGWrapper(gpghome=gpghome) + if initialize: + self._initialize() + + def _initialize(self): # load/generate OpenPGP keypair if not self._has_openpgp_keypair(): self._gen_openpgp_keypair() diff --git a/src/leap/soledad/tests/__init__.py b/src/leap/soledad/tests/__init__.py index 3bb66680..6135e648 100644 --- a/src/leap/soledad/tests/__init__.py +++ b/src/leap/soledad/tests/__init__.py @@ -23,9 +23,11 @@ class BaseSoledadTest(BaseLeapTest): self._db2 = u1db.open(self.db2_file, create=True, document_factory=LeapDocument) # open a soledad instance - self._soledad = Soledad(self.email, gpghome=self.gnupg_home) + self._soledad = Soledad(self.email, gpghome=self.gnupg_home, + initialize=False) self._soledad._gpg.import_keys(PUBLIC_KEY) self._soledad._gpg.import_keys(PRIVATE_KEY) + self._soledad._initialize() def tearDown(self): self._db1.close() -- cgit v1.2.3 From be3a837c912d3dc90386334e5e389813aa77773a Mon Sep 17 00:00:00 2001 From: drebs Date: Sat, 9 Feb 2013 20:38:55 -0200 Subject: Couch server subprocess for tests now waits for returncode to avoid zombie childs. --- src/leap/soledad/tests/test_couch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/soledad/tests/test_couch.py b/src/leap/soledad/tests/test_couch.py index 55bcf442..09c7269e 100644 --- a/src/leap/soledad/tests/test_couch.py +++ b/src/leap/soledad/tests/test_couch.py @@ -57,7 +57,8 @@ class CouchDBWrapper(object): args = ['couchdb', '-n' '-a', confPath] #null = open('/dev/null', 'w') self.process = subprocess.Popen( - args, env=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + args, env=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + close_fds=True) # find port logPath = os.path.join(self.tempdir, 'log', 'couch.log') while not os.path.exists(logPath): @@ -87,7 +88,7 @@ stderr: def stop(self): self.process.terminate() - + self.process.wait() os.system("rm -rf %s" % self.tempdir) -- cgit v1.2.3 From 8a887bb41c56da368fb1410c63da117566374858 Mon Sep 17 00:00:00 2001 From: drebs Date: Sat, 9 Feb 2013 22:09:03 -0200 Subject: SQLCipher backend can sync with remote Leap HTTP target. --- src/leap/soledad/__init__.py | 5 +++-- src/leap/soledad/backends/leap_backend.py | 19 ++++++++--------- src/leap/soledad/backends/sqlcipher.py | 34 ++++++++++++++++++++++--------- src/leap/soledad/tests/test_sqlcipher.py | 9 +++++--- 4 files changed, 42 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py index 6893c7a8..e11b8319 100644 --- a/src/leap/soledad/__init__.py +++ b/src/leap/soledad/__init__.py @@ -43,7 +43,8 @@ class Soledad(object): # instantiate u1db # TODO: verify if secret for sqlcipher should be the same as the one # for symmetric encryption. - self._db = sqlcipher.open(self.LOCAL_DB_PATH, True, self._secret) + self._db = sqlcipher.open(self.LOCAL_DB_PATH, True, self._secret, + soledad=self) def close(self): self._db.close() @@ -213,6 +214,6 @@ class Soledad(object): Synchronize the local encrypted database with LEAP server. """ # TODO: create authentication scheme for sync with server. - return self._db.sync(url, creds=None, autocreate=True, soledad=self) + return self._db.sync(url, creds=None, autocreate=True) __all__ = ['util'] diff --git a/src/leap/soledad/backends/leap_backend.py b/src/leap/soledad/backends/leap_backend.py index d3ae6db6..a2208404 100644 --- a/src/leap/soledad/backends/leap_backend.py +++ b/src/leap/soledad/backends/leap_backend.py @@ -92,7 +92,15 @@ class LeapDatabase(HTTPDatabase): def __init__(self, url, document_factory=None, creds=None, soledad=None): super(LeapDatabase, self).__init__(url, creds=creds) self._soledad = soledad - self._factory = LeapDocument + + # wrap soledad in factory + def factory(doc_id=None, rev=None, json='{}', has_conflicts=False, + encrypted_json=None, syncable=True): + return LeapDocument(doc_id=doc_id, rev=rev, json=json, + has_conflicts=has_conflicts, + encrypted_json=encrypted_json, + syncable=syncable, soledad=self._soledad) + self.set_document_factory(factory) @staticmethod def open_database(url, create): @@ -115,15 +123,6 @@ class LeapDatabase(HTTPDatabase): st._creds = self._creds return st - def create_doc_from_json(self, content, doc_id=None): - if doc_id is None: - doc_id = self._allocate_doc_id() - res, headers = self._request_json('PUT', ['doc', doc_id], {}, - content, 'application/json') - new_doc = self._factory(doc_id, res['rev'], content, - soledad=self._soledad) - return new_doc - class LeapSyncTarget(HTTPSyncTarget): """ diff --git a/src/leap/soledad/backends/sqlcipher.py b/src/leap/soledad/backends/sqlcipher.py index 354fcd31..9108f73a 100644 --- a/src/leap/soledad/backends/sqlcipher.py +++ b/src/leap/soledad/backends/sqlcipher.py @@ -31,7 +31,7 @@ from u1db import ( from leap.soledad.backends.leap_backend import LeapDocument -def open(path, password, create=True, document_factory=None): +def open(path, password, create=True, document_factory=None, soledad=None): """Open a database at the given location. Will raise u1db.errors.DatabaseDoesNotExist if create=False and the @@ -45,7 +45,8 @@ def open(path, password, create=True, document_factory=None): :return: An instance of Database. """ return SQLCipherDatabase.open_database( - path, password, create=create, document_factory=document_factory) + path, password, create=create, document_factory=document_factory, + soledad=soledad) class DatabaseIsNotEncrypted(Exception): @@ -64,14 +65,23 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase): def set_pragma_key(cls, db_handle, key): db_handle.cursor().execute("PRAGMA key = '%s'" % key) - def __init__(self, sqlite_file, password, document_factory=None): + def __init__(self, sqlite_file, password, document_factory=None, + soledad=None): """Create a new sqlcipher file.""" self._check_if_db_is_encrypted(sqlite_file) self._db_handle = dbapi2.connect(sqlite_file) SQLCipherDatabase.set_pragma_key(self._db_handle, password) self._real_replica_uid = None self._ensure_schema() - self._factory = document_factory or LeapDocument + self._soledad = soledad + + def factory(doc_id=None, rev=None, json='{}', has_conflicts=False, + encrypted_json=None, syncable=True): + return LeapDocument(doc_id=doc_id, rev=rev, json=json, + has_conflicts=has_conflicts, + encrypted_json=encrypted_json, + syncable=syncable, soledad=self._soledad) + self.set_document_factory(factory) def _check_if_db_is_encrypted(self, sqlite_file): if not os.path.exists(sqlite_file): @@ -86,7 +96,8 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase): pass @classmethod - def _open_database(cls, sqlite_file, password, document_factory=None): + def _open_database(cls, sqlite_file, password, document_factory=None, + soledad=None): if not os.path.isfile(sqlite_file): raise errors.DatabaseDoesNotExist() tries = 2 @@ -108,14 +119,16 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase): tries -= 1 time.sleep(cls.WAIT_FOR_PARALLEL_INIT_HALF_INTERVAL) return SQLCipherDatabase._sqlite_registry[v]( - sqlite_file, password, document_factory=document_factory) + sqlite_file, password, document_factory=document_factory, + soledad=soledad) @classmethod def open_database(cls, sqlite_file, password, create, backend_cls=None, - document_factory=None): + document_factory=None, soledad=None): try: return cls._open_database(sqlite_file, password, - document_factory=document_factory) + document_factory=document_factory, + soledad=soledad) except errors.DatabaseDoesNotExist: if not create: raise @@ -123,9 +136,10 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase): # default is SQLCipherPartialExpandDatabase backend_cls = SQLCipherDatabase return backend_cls(sqlite_file, password, - document_factory=document_factory) + document_factory=document_factory, + soledad=soledad) - def sync(self, url, creds=None, autocreate=True, soledad=None): + def sync(self, url, creds=None, autocreate=True): """ Synchronize encrypted documents with remote replica exposed at url. """ diff --git a/src/leap/soledad/tests/test_sqlcipher.py b/src/leap/soledad/tests/test_sqlcipher.py index 38ad09ed..09b76b82 100644 --- a/src/leap/soledad/tests/test_sqlcipher.py +++ b/src/leap/soledad/tests/test_sqlcipher.py @@ -236,7 +236,8 @@ class TestSQLCipherPartialExpandDatabase( db2 = SQLCipherDatabase._open_database( path, PASSWORD, document_factory=TestAlternativeDocument) - self.assertEqual(TestAlternativeDocument, db2._factory) + doc = db2.create_doc({}) + self.assertTrue(isinstance(doc, LeapDocument)) def test_open_database_existing(self): temp_dir = self.createTempDir(prefix='u1db-test-') @@ -252,7 +253,8 @@ class TestSQLCipherPartialExpandDatabase( db2 = SQLCipherDatabase.open_database( path, PASSWORD, create=False, document_factory=TestAlternativeDocument) - self.assertEqual(TestAlternativeDocument, db2._factory) + doc = db2.create_doc({}) + self.assertTrue(isinstance(doc, LeapDocument)) def test_create_database_initializes_schema(self): # This test had to be cloned because our implementation of SQLCipher @@ -304,7 +306,8 @@ class SQLCipherOpen(test_open.TestU1DBOpen): db = u1db_open(self.db_path, password=PASSWORD, create=True, document_factory=TestAlternativeDocument) self.addCleanup(db.close) - self.assertEqual(TestAlternativeDocument, db._factory) + doc = db.create_doc({}) + self.assertTrue(isinstance(doc, LeapDocument)) def test_open_existing(self): db = SQLCipherDatabase(self.db_path, PASSWORD) -- cgit v1.2.3 From e726f3df7c7efbe4069846468ffdbed6829ccfc4 Mon Sep 17 00:00:00 2001 From: drebs Date: Sat, 9 Feb 2013 22:23:10 -0200 Subject: Remove LeapDatabase because we don't need it (LeapSyncTarget does the job). --- src/leap/soledad/backends/leap_backend.py | 38 -------- src/leap/soledad/tests/test_leap_backend.py | 146 +--------------------------- 2 files changed, 5 insertions(+), 179 deletions(-) (limited to 'src') diff --git a/src/leap/soledad/backends/leap_backend.py b/src/leap/soledad/backends/leap_backend.py index a2208404..c0a22feb 100644 --- a/src/leap/soledad/backends/leap_backend.py +++ b/src/leap/soledad/backends/leap_backend.py @@ -86,44 +86,6 @@ class LeapDocument(Document): doc="Wrapper to ensure `doc.rev` is always returned as bytes.") -class LeapDatabase(HTTPDatabase): - """Implement the HTTP remote database API to a Leap server.""" - - def __init__(self, url, document_factory=None, creds=None, soledad=None): - super(LeapDatabase, self).__init__(url, creds=creds) - self._soledad = soledad - - # wrap soledad in factory - def factory(doc_id=None, rev=None, json='{}', has_conflicts=False, - encrypted_json=None, syncable=True): - return LeapDocument(doc_id=doc_id, rev=rev, json=json, - has_conflicts=has_conflicts, - encrypted_json=encrypted_json, - syncable=syncable, soledad=self._soledad) - self.set_document_factory(factory) - - @staticmethod - def open_database(url, create): - db = LeapDatabase(url) - db.open(create) - return db - - @staticmethod - def delete_database(url): - db = LeapDatabase(url) - db._delete() - db.close() - - def _allocate_doc_id(self): - """Generate a unique identifier for this document.""" - return 'D-' + uuid.uuid4().hex # 'D-' stands for document - - def get_sync_target(self): - st = LeapSyncTarget(self._url.geturl()) - st._creds = self._creds - return st - - class LeapSyncTarget(HTTPSyncTarget): """ A SyncTarget that encrypts data before sending and decrypts data after diff --git a/src/leap/soledad/tests/test_leap_backend.py b/src/leap/soledad/tests/test_leap_backend.py index cdd60b10..a061533c 100644 --- a/src/leap/soledad/tests/test_leap_backend.py +++ b/src/leap/soledad/tests/test_leap_backend.py @@ -20,52 +20,10 @@ from leap.soledad.tests.u1db_tests import test_remote_sync_target from leap.soledad.tests.u1db_tests import test_https -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_common_backend`. -#----------------------------------------------------------------------------- - -class TestLeapBackendImpl(tests.TestCase): - - def test__allocate_doc_id(self): - db = leap_backend.LeapDatabase('test') - doc_id1 = db._allocate_doc_id() - self.assertTrue(doc_id1.startswith('D-')) - self.assertEqual(34, len(doc_id1)) - int(doc_id1[len('D-'):], 16) - self.assertNotEqual(doc_id1, db._allocate_doc_id()) - - #----------------------------------------------------------------------------- # The following tests come from `u1db.tests.test_backends`. #----------------------------------------------------------------------------- -def make_leap_database_for_test(test, replica_uid, path='test'): - test.startServer() - test.request_state._create_database(replica_uid) - return leap_backend.LeapDatabase(test.getURL(path)) - - -def copy_leap_database_for_test(test, db): - # DO NOT COPY OR REUSE THIS CODE OUTSIDE TESTS: COPYING U1DB DATABASES IS - # THE WRONG THING TO DO, THE ONLY REASON WE DO SO HERE IS TO TEST THAT WE - # CORRECTLY DETECT IT HAPPENING SO THAT WE CAN RAISE ERRORS RATHER THAN - # CORRUPT USER DATA. USE SYNC INSTEAD, OR WE WILL SEND NINJA TO YOUR - # HOUSE. - return test.request_state._copy_database(db) - - -def make_oauth_leap_database_for_test(test, replica_uid): - http_db = make_leap_database_for_test(test, replica_uid, '~/test') - http_db.set_oauth_credentials(tests.consumer1.key, tests.consumer1.secret, - tests.token1.key, tests.token1.secret) - return http_db - - -def make_document_for_test(test, doc_id, rev, content, has_conflicts=False): - return leap_backend.LeapDocument( - doc_id, rev, content, has_conflicts=has_conflicts) - - def make_leap_document_for_test(test, doc_id, rev, content, has_conflicts=False): return leap_backend.LeapDocument( @@ -82,10 +40,11 @@ def make_leap_encrypted_document_for_test(test, doc_id, rev, encrypted_content, LEAP_SCENARIOS = [ - ('http', {'make_database_for_test': make_leap_database_for_test, - 'copy_database_for_test': copy_leap_database_for_test, - 'make_document_for_test': make_leap_document_for_test, - 'make_app_with_state': make_http_app}), + ('http', { + 'make_database_for_test': test_backends.make_http_database_for_test, + 'copy_database_for_test': test_backends.copy_http_database_for_test, + 'make_document_for_test': make_leap_document_for_test, + 'make_app_with_state': make_http_app}), ] @@ -94,101 +53,6 @@ class LeapTests(test_backends.AllDatabaseTests, BaseSoledadTest): scenarios = LEAP_SCENARIOS -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_http_database`. -#----------------------------------------------------------------------------- - -class TestLeapDatabaseSimpleOperations( - test_http_database.TestHTTPDatabaseSimpleOperations): - - def setUp(self): - super(test_http_database.TestHTTPDatabaseSimpleOperations, - self).setUp() - self.db = leap_backend.LeapDatabase('dbase') - self.db._conn = object() # crash if used - self.got = None - self.response_val = None - - def _request(method, url_parts, params=None, body=None, - content_type=None): - self.got = method, url_parts, params, body, content_type - if isinstance(self.response_val, Exception): - raise self.response_val - return self.response_val - - def _request_json(method, url_parts, params=None, body=None, - content_type=None): - self.got = method, url_parts, params, body, content_type - if isinstance(self.response_val, Exception): - raise self.response_val - return self.response_val - - self.db._request = _request - self.db._request_json = _request_json - - def test_get_sync_target(self): - st = self.db.get_sync_target() - self.assertIsInstance(st, leap_backend.LeapSyncTarget) - self.assertEqual(st._url, self.db._url) - - -class TestLeapDatabaseCtrWithCreds( - test_http_database.TestHTTPDatabaseCtrWithCreds): - pass - - -class TestLeapDatabaseIntegration( - test_http_database.TestHTTPDatabaseIntegration): - - def test_non_existing_db(self): - db = leap_backend.LeapDatabase(self.getURL('not-there')) - self.assertRaises(u1db.errors.DatabaseDoesNotExist, db.get_doc, 'doc1') - - def test__ensure(self): - db = leap_backend.LeapDatabase(self.getURL('new')) - db._ensure() - self.assertIs(None, db.get_doc('doc1')) - - def test__delete(self): - self.request_state._create_database('db0') - db = leap_backend.LeapDatabase(self.getURL('db0')) - db._delete() - self.assertRaises(u1db.errors.DatabaseDoesNotExist, - self.request_state.check_database, 'db0') - - def test_open_database_existing(self): - self.request_state._create_database('db0') - db = leap_backend.LeapDatabase.open_database(self.getURL('db0'), - create=False) - self.assertIs(None, db.get_doc('doc1')) - - def test_open_database_non_existing(self): - self.assertRaises(u1db.errors.DatabaseDoesNotExist, - leap_backend.LeapDatabase.open_database, - self.getURL('not-there'), - create=False) - - def test_open_database_create(self): - db = leap_backend.LeapDatabase.open_database(self.getURL('new'), - create=True) - self.assertIs(None, db.get_doc('doc1')) - - def test_delete_database_existing(self): - self.request_state._create_database('db0') - leap_backend.LeapDatabase.delete_database(self.getURL('db0')) - self.assertRaises(u1db.errors.DatabaseDoesNotExist, - self.request_state.check_database, 'db0') - - def test_doc_ids_needing_quoting(self): - db0 = self.request_state._create_database('db0') - db = leap_backend.LeapDatabase.open_database(self.getURL('db0'), - create=False) - doc = leap_backend.LeapDocument('%fff', None, '{}') - db.put_doc(doc) - self.assertGetDoc(db0, '%fff', doc.rev, '{}', False) - self.assertGetDoc(db, '%fff', doc.rev, '{}', False) - - #----------------------------------------------------------------------------- # The following tests come from `u1db.tests.test_http_client`. #----------------------------------------------------------------------------- -- cgit v1.2.3 From 3f8460cd2506ff78971f2ddb0edf4d0b0f009d12 Mon Sep 17 00:00:00 2001 From: drebs Date: Sun, 10 Feb 2013 00:29:07 -0200 Subject: Make couch wrapper terminate subprocess properly. --- src/leap/soledad/tests/test_couch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/soledad/tests/test_couch.py b/src/leap/soledad/tests/test_couch.py index 3482b035..bbdf44b4 100644 --- a/src/leap/soledad/tests/test_couch.py +++ b/src/leap/soledad/tests/test_couch.py @@ -88,7 +88,7 @@ stderr: def stop(self): self.process.terminate() - self.process.wait() + self.process.communicate() os.system("rm -rf %s" % self.tempdir) -- cgit v1.2.3 From 86b8f78e7d4ac3e3e47535604713c497a8b68936 Mon Sep 17 00:00:00 2001 From: drebs Date: Sun, 10 Feb 2013 00:46:36 -0200 Subject: Soledad can be instantiated in different directories. --- src/leap/soledad/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py index e11b8319..4b7a12df 100644 --- a/src/leap/soledad/__init__.py +++ b/src/leap/soledad/__init__.py @@ -21,13 +21,15 @@ class Soledad(object): # other configs SECRET_LENGTH = 50 - def __init__(self, user_email, gpghome=None, initialize=True): + def __init__(self, user_email, gpghome=None, initialize=True, + prefix=None, secret_path=None, local_db_path=None): self._user_email = user_email + self.PREFIX = prefix or self.PREFIX + self.SECRET_PATH = secret_path or self.SECRET_PATH + self.LOCAL_DB_PATH = local_db_path or self.LOCAL_DB_PATH if not os.path.isdir(self.PREFIX): os.makedirs(self.PREFIX) - if not gpghome: - gpghome = self.GNUPG_HOME - self._gpg = GPGWrapper(gpghome=gpghome) + self._gpg = GPGWrapper(gpghome=(gpghome or self.GNUPG_HOME)) if initialize: self._initialize() -- cgit v1.2.3 From d8bb7d00c79c4e843ca15778c06991275c0181cb Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 12 Feb 2013 16:41:26 -0200 Subject: Adapt code and tests to use pysqlcipher. --- src/leap/soledad/backends/sqlcipher.py | 4 +- src/leap/soledad/tests/test_sqlcipher.py | 81 +++++++++++++++++++--- src/leap/soledad/tests/u1db_tests/__init__.py | 2 +- .../tests/u1db_tests/test_sqlite_backend.py | 2 +- 4 files changed, 75 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/leap/soledad/backends/sqlcipher.py b/src/leap/soledad/backends/sqlcipher.py index 9108f73a..c902b466 100644 --- a/src/leap/soledad/backends/sqlcipher.py +++ b/src/leap/soledad/backends/sqlcipher.py @@ -17,7 +17,7 @@ """A U1DB implementation that uses SQLCipher as its persistence layer.""" import os -from sqlite3 import dbapi2, DatabaseError +from pysqlcipher import dbapi2 import time from u1db.backends.sqlite_backend import ( @@ -92,7 +92,7 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase): # backend should raise a DatabaseError exception. SQLitePartialExpandDatabase(sqlite_file) raise DatabaseIsNotEncrypted() - except DatabaseError: + except dbapi2.DatabaseError: pass @classmethod diff --git a/src/leap/soledad/tests/test_sqlcipher.py b/src/leap/soledad/tests/test_sqlcipher.py index 09b76b82..ee9b38dd 100644 --- a/src/leap/soledad/tests/test_sqlcipher.py +++ b/src/leap/soledad/tests/test_sqlcipher.py @@ -2,7 +2,7 @@ import os import time -from sqlite3 import dbapi2, DatabaseError +from pysqlcipher import dbapi2 import unittest from StringIO import StringIO import threading @@ -30,6 +30,7 @@ from leap.soledad.tests.u1db_tests import test_backends from leap.soledad.tests.u1db_tests import test_open from leap.soledad.tests.u1db_tests import test_sync from leap.soledad.backends.leap_backend import LeapSyncTarget +from leap.testing.basetest import BaseLeapTest PASSWORD = '123456' @@ -125,20 +126,20 @@ load_tests = tests.load_with_scenarios #----------------------------------------------------------------------------- class TestSQLCipherDatabase(test_sqlite_backend.TestSQLiteDatabase): - + def test_atomic_initialize(self): tmpdir = self.createTempDir() dbname = os.path.join(tmpdir, 'atomic.db') t2 = None # will be a thread - class SQLCipherDatabaseTesting(SQLitePartialExpandDatabase): + class SQLCipherDatabaseTesting(SQLCipherDatabase): _index_storage_value = "testing" def __init__(self, dbname, ntry): self._try = ntry self._is_initialized_invocations = 0 - super(SQLCipherDatabaseTesting, self).__init__(dbname) + super(SQLCipherDatabaseTesting, self).__init__(dbname, PASSWORD) def _is_initialized(self, c): res = super(SQLCipherDatabaseTesting, self)._is_initialized(c) @@ -238,6 +239,59 @@ class TestSQLCipherPartialExpandDatabase( document_factory=TestAlternativeDocument) doc = db2.create_doc({}) self.assertTrue(isinstance(doc, LeapDocument)) + + def test__open_database_non_existent(self): + temp_dir = self.createTempDir(prefix='u1db-test-') + path = temp_dir + '/non-existent.sqlite' + self.assertRaises(errors.DatabaseDoesNotExist, + SQLCipherDatabase._open_database, + path, PASSWORD) + + def test__open_database_during_init(self): + temp_dir = self.createTempDir(prefix='u1db-test-') + path = temp_dir + '/initialised.db' + db = SQLCipherDatabase.__new__( + SQLCipherDatabase) + db._db_handle = dbapi2.connect(path) # db is there but not yet init-ed + c = db._db_handle.cursor() + c.execute('PRAGMA key="%s"' % PASSWORD) + self.addCleanup(db.close) + observed = [] + + class SQLiteDatabaseTesting(SQLCipherDatabase): + WAIT_FOR_PARALLEL_INIT_HALF_INTERVAL = 0.1 + + @classmethod + def _which_index_storage(cls, c): + res = super(SQLiteDatabaseTesting, cls)._which_index_storage(c) + db._ensure_schema() # init db + observed.append(res[0]) + return res + + db2 = SQLiteDatabaseTesting._open_database(path, PASSWORD) + self.addCleanup(db2.close) + self.assertIsInstance(db2, SQLCipherDatabase) + self.assertEqual( + [None, + SQLCipherDatabase._index_storage_value], + observed) + + def test__open_database_invalid(self): + class SQLiteDatabaseTesting(SQLCipherDatabase): + WAIT_FOR_PARALLEL_INIT_HALF_INTERVAL = 0.1 + temp_dir = self.createTempDir(prefix='u1db-test-') + path1 = temp_dir + '/invalid1.db' + with open(path1, 'wb') as f: + f.write("") + self.assertRaises(dbapi2.OperationalError, + SQLiteDatabaseTesting._open_database, path1, + PASSWORD) + with open(path1, 'wb') as f: + f.write("invalid") + self.assertRaises(dbapi2.DatabaseError, + SQLiteDatabaseTesting._open_database, path1, + PASSWORD) + def test_open_database_existing(self): temp_dir = self.createTempDir(prefix='u1db-test-') @@ -256,6 +310,13 @@ class TestSQLCipherPartialExpandDatabase( doc = db2.create_doc({}) self.assertTrue(isinstance(doc, LeapDocument)) + def test_open_database_create(self): + temp_dir = self.createTempDir(prefix='u1db-test-') + path = temp_dir + '/new.sqlite' + SQLCipherDatabase.open_database(path, PASSWORD, create=True) + db2 = SQLCipherDatabase.open_database(path, PASSWORD, create=False) + self.assertIsInstance(db2, SQLCipherDatabase) + def test_create_database_initializes_schema(self): # This test had to be cloned because our implementation of SQLCipher # backend is referenced with an index_storage_value that includes the @@ -388,9 +449,7 @@ class SQLCipherSyncTargetTests(test_sync.DatabaseSyncTargetTests): # Tests for actual encryption of the database #----------------------------------------------------------------------------- -class SQLCipherEncryptionTest(unittest.TestCase): - - DB_FILE = '/tmp/test.db' +class SQLCipherEncryptionTest(BaseLeapTest): def delete_dbfiles(self): for dbfile in [self.DB_FILE]: @@ -398,6 +457,7 @@ class SQLCipherEncryptionTest(unittest.TestCase): os.unlink(dbfile) def setUp(self): + self.DB_FILE = self.tempdir + '/test.db' self.delete_dbfiles() def tearDown(self): @@ -413,7 +473,7 @@ class SQLCipherEncryptionTest(unittest.TestCase): SQLitePartialExpandDatabase(self.DB_FILE, document_factory=LeapDocument) raise DatabaseIsNotEncrypted() - except DatabaseError: + except dbapi2.DatabaseError: # at this point we know that the regular U1DB sqlcipher backend # did not succeed on opening the database, so it was indeed # encrypted. @@ -431,8 +491,9 @@ class SQLCipherEncryptionTest(unittest.TestCase): # trying to open the a non-encrypted database with sqlcipher # backend should raise a DatabaseIsNotEncrypted exception. SQLCipherDatabase(self.DB_FILE, PASSWORD) - raise DatabaseError("SQLCipher backend should not be able to open " - "non-encrypted dbs.") + raise db1pi2.DatabaseError( + "SQLCipher backend should not be able to open non-encrypted " + "dbs.") except DatabaseIsNotEncrypted: pass diff --git a/src/leap/soledad/tests/u1db_tests/__init__.py b/src/leap/soledad/tests/u1db_tests/__init__.py index 27aa4d79..43304b43 100644 --- a/src/leap/soledad/tests/u1db_tests/__init__.py +++ b/src/leap/soledad/tests/u1db_tests/__init__.py @@ -30,7 +30,7 @@ except ImportError: from wsgiref import simple_server from oauth import oauth -from sqlite3 import dbapi2 +from pysqlcipher import dbapi2 from StringIO import StringIO import testscenarios diff --git a/src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py b/src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py index 2003da03..1380e4b1 100644 --- a/src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py +++ b/src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py @@ -20,7 +20,7 @@ import os import time import threading -from sqlite3 import dbapi2 +from pysqlcipher import dbapi2 from u1db import ( errors, -- cgit v1.2.3 From 5ff29dc57e2877a14e705d09b7042cddf4165d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 6 Mar 2013 15:27:23 -0300 Subject: Remove everything to start from scratch --- src/leap/__init__.py | 36 - src/leap/_version.py | 197 -- src/leap/app.py | 120 -- src/leap/base/__init__.py | 0 src/leap/base/auth.py | 355 ---- src/leap/base/authentication.py | 11 - src/leap/base/checks.py | 213 --- src/leap/base/config.py | 348 ---- src/leap/base/connection.py | 115 -- src/leap/base/constants.py | 42 - src/leap/base/exceptions.py | 97 - src/leap/base/network.py | 107 -- src/leap/base/pluggableconfig.py | 455 ----- src/leap/base/providers.py | 29 - src/leap/base/specs.py | 67 - src/leap/base/tests/__init__.py | 0 src/leap/base/tests/test_auth.py | 58 - src/leap/base/tests/test_checks.py | 177 -- src/leap/base/tests/test_config.py | 247 --- src/leap/base/tests/test_providers.py | 150 -- src/leap/base/tests/test_validation.py | 92 - src/leap/baseapp/__init__.py | 0 src/leap/baseapp/constants.py | 6 - src/leap/baseapp/dialogs.py | 61 - src/leap/baseapp/eip.py | 243 --- src/leap/baseapp/leap_app.py | 153 -- src/leap/baseapp/log.py | 69 - src/leap/baseapp/mainwindow.py | 191 -- src/leap/baseapp/network.py | 63 - src/leap/baseapp/permcheck.py | 17 - src/leap/baseapp/systray.py | 268 --- src/leap/certs/__init__.py | 7 - src/leap/crypto/__init__.py | 0 src/leap/crypto/certs.py | 112 -- src/leap/crypto/certs_gnutls.py | 112 -- src/leap/crypto/leapkeyring.py | 70 - src/leap/crypto/tests/__init__.py | 0 src/leap/crypto/tests/test_certs.py | 22 - src/leap/eip/__init__.py | 0 src/leap/eip/checks.py | 537 ------ src/leap/eip/config.py | 398 ---- src/leap/eip/constants.py | 3 - src/leap/eip/eipconnection.py | 405 ----- src/leap/eip/exceptions.py | 175 -- src/leap/eip/openvpnconnection.py | 410 ----- src/leap/eip/specs.py | 136 -- src/leap/eip/tests/__init__.py | 0 src/leap/eip/tests/data.py | 51 - src/leap/eip/tests/test_checks.py | 373 ---- src/leap/eip/tests/test_config.py | 298 --- src/leap/eip/tests/test_eipconnection.py | 216 --- src/leap/eip/tests/test_openvpnconnection.py | 161 -- src/leap/eip/udstelnet.py | 38 - src/leap/email/__init__.py | 0 src/leap/email/smtp/README | 43 - src/leap/email/smtp/__init__.py | 0 src/leap/email/smtp/smtprelay.py | 207 --- src/leap/email/smtp/tests/185CA770.key | 79 - src/leap/email/smtp/tests/185CA770.pub | 52 - src/leap/email/smtp/tests/__init__.py | 215 --- src/leap/email/smtp/tests/mail.txt | 10 - src/leap/email/smtp/tests/test_smtprelay.py | 75 - src/leap/gui/__init__.py | 11 - src/leap/gui/constants.py | 13 - src/leap/gui/firstrun/__init__.py | 28 - src/leap/gui/firstrun/connect.py | 214 --- src/leap/gui/firstrun/constants.py | 0 src/leap/gui/firstrun/intro.py | 68 - src/leap/gui/firstrun/last.py | 119 -- src/leap/gui/firstrun/login.py | 332 ---- src/leap/gui/firstrun/mixins.py | 18 - src/leap/gui/firstrun/providerinfo.py | 106 -- src/leap/gui/firstrun/providerselect.py | 471 ----- src/leap/gui/firstrun/providersetup.py | 157 -- src/leap/gui/firstrun/register.py | 387 ---- .../firstrun/tests/integration/fake_provider.py | 302 ---- src/leap/gui/firstrun/wizard.py | 309 ---- src/leap/gui/locale_rc.py | 813 --------- src/leap/gui/mainwindow_rc.py | 1130 ------------ src/leap/gui/progress.py | 488 ----- src/leap/gui/styles.py | 16 - src/leap/gui/tests/__init__.py | 0 src/leap/gui/tests/integration/fake_user_signup.py | 84 - src/leap/gui/tests/test_firstrun_login.py | 212 --- src/leap/gui/tests/test_firstrun_providerselect.py | 203 --- src/leap/gui/tests/test_firstrun_register.py | 244 --- src/leap/gui/tests/test_firstrun_wizard.py | 137 -- src/leap/gui/tests/test_mainwindow_rc.py | 32 - src/leap/gui/tests/test_progress.py | 449 ----- src/leap/gui/tests/test_threads.py | 27 - src/leap/gui/threads.py | 21 - src/leap/gui/utils.py | 34 - src/leap/soledad/README | 35 - src/leap/soledad/__init__.py | 221 --- src/leap/soledad/backends/__init__.py | 5 - src/leap/soledad/backends/couch.py | 269 --- src/leap/soledad/backends/leap_backend.py | 193 -- src/leap/soledad/backends/objectstore.py | 114 -- src/leap/soledad/backends/sqlcipher.py | 176 -- src/leap/soledad/server.py | 20 - src/leap/soledad/tests/__init__.py | 199 -- src/leap/soledad/tests/couchdb.ini.template | 222 --- src/leap/soledad/tests/test_couch.py | 298 --- src/leap/soledad/tests/test_encrypted.py | 15 - src/leap/soledad/tests/test_leap_backend.py | 207 --- src/leap/soledad/tests/test_sqlcipher.py | 501 ----- src/leap/soledad/tests/u1db_tests/README | 34 - src/leap/soledad/tests/u1db_tests/__init__.py | 421 ----- src/leap/soledad/tests/u1db_tests/test_backends.py | 1907 -------------------- src/leap/soledad/tests/u1db_tests/test_document.py | 150 -- src/leap/soledad/tests/u1db_tests/test_http_app.py | 1135 ------------ .../soledad/tests/u1db_tests/test_http_client.py | 363 ---- .../soledad/tests/u1db_tests/test_http_database.py | 260 --- src/leap/soledad/tests/u1db_tests/test_https.py | 117 -- src/leap/soledad/tests/u1db_tests/test_open.py | 69 - .../tests/u1db_tests/test_remote_sync_target.py | 317 ---- .../tests/u1db_tests/test_sqlite_backend.py | 494 ----- src/leap/soledad/tests/u1db_tests/test_sync.py | 1242 ------------- .../tests/u1db_tests/testing-certs/Makefile | 35 - .../tests/u1db_tests/testing-certs/cacert.pem | 58 - .../tests/u1db_tests/testing-certs/testing.cert | 61 - .../tests/u1db_tests/testing-certs/testing.key | 16 - src/leap/soledad/util.py | 55 - src/leap/testing/__init__.py | 0 src/leap/testing/basetest.py | 85 - src/leap/testing/cacert.pem | 23 - src/leap/testing/https_server.py | 68 - src/leap/testing/leaptestscert.pem | 84 - src/leap/testing/leaptestskey.pem | 27 - src/leap/testing/pyqt.py | 52 - src/leap/testing/qunittest.py | 302 ---- src/leap/testing/test_basetest.py | 91 - src/leap/util/__init__.py | 9 - src/leap/util/certs.py | 18 - src/leap/util/coroutines.py | 109 -- src/leap/util/dicts.py | 268 --- src/leap/util/fileutil.py | 120 -- src/leap/util/geo.py | 32 - src/leap/util/leap_argparse.py | 44 - src/leap/util/misc.py | 37 - src/leap/util/tests/__init__.py | 0 src/leap/util/tests/test_fileutil.py | 100 - src/leap/util/tests/test_leap_argparse.py | 35 - src/leap/util/tests/test_translations.py | 22 - src/leap/util/translations.py | 82 - src/leap/util/web.py | 40 - 146 files changed, 25474 deletions(-) delete mode 100644 src/leap/__init__.py delete mode 100644 src/leap/_version.py delete mode 100644 src/leap/app.py delete mode 100644 src/leap/base/__init__.py delete mode 100644 src/leap/base/auth.py delete mode 100644 src/leap/base/authentication.py delete mode 100644 src/leap/base/checks.py delete mode 100644 src/leap/base/config.py delete mode 100644 src/leap/base/connection.py delete mode 100644 src/leap/base/constants.py delete mode 100644 src/leap/base/exceptions.py delete mode 100644 src/leap/base/network.py delete mode 100644 src/leap/base/pluggableconfig.py delete mode 100644 src/leap/base/providers.py delete mode 100644 src/leap/base/specs.py delete mode 100644 src/leap/base/tests/__init__.py delete mode 100644 src/leap/base/tests/test_auth.py delete mode 100644 src/leap/base/tests/test_checks.py delete mode 100644 src/leap/base/tests/test_config.py delete mode 100644 src/leap/base/tests/test_providers.py delete mode 100644 src/leap/base/tests/test_validation.py delete mode 100644 src/leap/baseapp/__init__.py delete mode 100644 src/leap/baseapp/constants.py delete mode 100644 src/leap/baseapp/dialogs.py delete mode 100644 src/leap/baseapp/eip.py delete mode 100644 src/leap/baseapp/leap_app.py delete mode 100644 src/leap/baseapp/log.py delete mode 100644 src/leap/baseapp/mainwindow.py delete mode 100644 src/leap/baseapp/network.py delete mode 100644 src/leap/baseapp/permcheck.py delete mode 100644 src/leap/baseapp/systray.py delete mode 100644 src/leap/certs/__init__.py delete mode 100644 src/leap/crypto/__init__.py delete mode 100644 src/leap/crypto/certs.py delete mode 100644 src/leap/crypto/certs_gnutls.py delete mode 100644 src/leap/crypto/leapkeyring.py delete mode 100644 src/leap/crypto/tests/__init__.py delete mode 100644 src/leap/crypto/tests/test_certs.py delete mode 100644 src/leap/eip/__init__.py delete mode 100644 src/leap/eip/checks.py delete mode 100644 src/leap/eip/config.py delete mode 100644 src/leap/eip/constants.py delete mode 100644 src/leap/eip/eipconnection.py delete mode 100644 src/leap/eip/exceptions.py delete mode 100644 src/leap/eip/openvpnconnection.py delete mode 100644 src/leap/eip/specs.py delete mode 100644 src/leap/eip/tests/__init__.py delete mode 100644 src/leap/eip/tests/data.py delete mode 100644 src/leap/eip/tests/test_checks.py delete mode 100644 src/leap/eip/tests/test_config.py delete mode 100644 src/leap/eip/tests/test_eipconnection.py delete mode 100644 src/leap/eip/tests/test_openvpnconnection.py delete mode 100644 src/leap/eip/udstelnet.py delete mode 100644 src/leap/email/__init__.py delete mode 100644 src/leap/email/smtp/README delete mode 100644 src/leap/email/smtp/__init__.py delete mode 100644 src/leap/email/smtp/smtprelay.py delete mode 100644 src/leap/email/smtp/tests/185CA770.key delete mode 100644 src/leap/email/smtp/tests/185CA770.pub delete mode 100644 src/leap/email/smtp/tests/__init__.py delete mode 100644 src/leap/email/smtp/tests/mail.txt delete mode 100644 src/leap/email/smtp/tests/test_smtprelay.py delete mode 100644 src/leap/gui/__init__.py delete mode 100644 src/leap/gui/constants.py delete mode 100644 src/leap/gui/firstrun/__init__.py delete mode 100644 src/leap/gui/firstrun/connect.py delete mode 100644 src/leap/gui/firstrun/constants.py delete mode 100644 src/leap/gui/firstrun/intro.py delete mode 100644 src/leap/gui/firstrun/last.py delete mode 100644 src/leap/gui/firstrun/login.py delete mode 100644 src/leap/gui/firstrun/mixins.py delete mode 100644 src/leap/gui/firstrun/providerinfo.py delete mode 100644 src/leap/gui/firstrun/providerselect.py delete mode 100644 src/leap/gui/firstrun/providersetup.py delete mode 100644 src/leap/gui/firstrun/register.py delete mode 100755 src/leap/gui/firstrun/tests/integration/fake_provider.py delete mode 100755 src/leap/gui/firstrun/wizard.py delete mode 100644 src/leap/gui/locale_rc.py delete mode 100644 src/leap/gui/mainwindow_rc.py delete mode 100644 src/leap/gui/progress.py delete mode 100644 src/leap/gui/styles.py delete mode 100644 src/leap/gui/tests/__init__.py delete mode 100644 src/leap/gui/tests/integration/fake_user_signup.py delete mode 100644 src/leap/gui/tests/test_firstrun_login.py delete mode 100644 src/leap/gui/tests/test_firstrun_providerselect.py delete mode 100644 src/leap/gui/tests/test_firstrun_register.py delete mode 100644 src/leap/gui/tests/test_firstrun_wizard.py delete mode 100644 src/leap/gui/tests/test_mainwindow_rc.py delete mode 100644 src/leap/gui/tests/test_progress.py delete mode 100644 src/leap/gui/tests/test_threads.py delete mode 100644 src/leap/gui/threads.py delete mode 100644 src/leap/gui/utils.py delete mode 100644 src/leap/soledad/README delete mode 100644 src/leap/soledad/__init__.py delete mode 100644 src/leap/soledad/backends/__init__.py delete mode 100644 src/leap/soledad/backends/couch.py delete mode 100644 src/leap/soledad/backends/leap_backend.py delete mode 100644 src/leap/soledad/backends/objectstore.py delete mode 100644 src/leap/soledad/backends/sqlcipher.py delete mode 100644 src/leap/soledad/server.py delete mode 100644 src/leap/soledad/tests/__init__.py delete mode 100644 src/leap/soledad/tests/couchdb.ini.template delete mode 100644 src/leap/soledad/tests/test_couch.py delete mode 100644 src/leap/soledad/tests/test_encrypted.py delete mode 100644 src/leap/soledad/tests/test_leap_backend.py delete mode 100644 src/leap/soledad/tests/test_sqlcipher.py delete mode 100644 src/leap/soledad/tests/u1db_tests/README delete mode 100644 src/leap/soledad/tests/u1db_tests/__init__.py delete mode 100644 src/leap/soledad/tests/u1db_tests/test_backends.py delete mode 100644 src/leap/soledad/tests/u1db_tests/test_document.py delete mode 100644 src/leap/soledad/tests/u1db_tests/test_http_app.py delete mode 100644 src/leap/soledad/tests/u1db_tests/test_http_client.py delete mode 100644 src/leap/soledad/tests/u1db_tests/test_http_database.py delete mode 100644 src/leap/soledad/tests/u1db_tests/test_https.py delete mode 100644 src/leap/soledad/tests/u1db_tests/test_open.py delete mode 100644 src/leap/soledad/tests/u1db_tests/test_remote_sync_target.py delete mode 100644 src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py delete mode 100644 src/leap/soledad/tests/u1db_tests/test_sync.py delete mode 100644 src/leap/soledad/tests/u1db_tests/testing-certs/Makefile delete mode 100644 src/leap/soledad/tests/u1db_tests/testing-certs/cacert.pem delete mode 100644 src/leap/soledad/tests/u1db_tests/testing-certs/testing.cert delete mode 100644 src/leap/soledad/tests/u1db_tests/testing-certs/testing.key delete mode 100644 src/leap/soledad/util.py delete mode 100644 src/leap/testing/__init__.py delete mode 100644 src/leap/testing/basetest.py delete mode 100644 src/leap/testing/cacert.pem delete mode 100644 src/leap/testing/https_server.py delete mode 100644 src/leap/testing/leaptestscert.pem delete mode 100644 src/leap/testing/leaptestskey.pem delete mode 100644 src/leap/testing/pyqt.py delete mode 100644 src/leap/testing/qunittest.py delete mode 100644 src/leap/testing/test_basetest.py delete mode 100644 src/leap/util/__init__.py delete mode 100644 src/leap/util/certs.py delete mode 100644 src/leap/util/coroutines.py delete mode 100644 src/leap/util/dicts.py delete mode 100644 src/leap/util/fileutil.py delete mode 100644 src/leap/util/geo.py delete mode 100644 src/leap/util/leap_argparse.py delete mode 100644 src/leap/util/misc.py delete mode 100644 src/leap/util/tests/__init__.py delete mode 100644 src/leap/util/tests/test_fileutil.py delete mode 100644 src/leap/util/tests/test_leap_argparse.py delete mode 100644 src/leap/util/tests/test_translations.py delete mode 100644 src/leap/util/translations.py delete mode 100644 src/leap/util/web.py (limited to 'src') diff --git a/src/leap/__init__.py b/src/leap/__init__.py deleted file mode 100644 index 2adbb34a..00000000 --- a/src/leap/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -LEAP Encryption Access Project -website: U{https://leap.se/} -""" - -from leap import eip -from leap import baseapp -from leap import util -from leap import soledad - -__all__ = [eip, baseapp, util, soledad] - -__version__ = "unknown" -try: - from ._version import get_versions - __version__ = get_versions()['version'] - del get_versions -except ImportError: - #running on a tree that has not run - #the setup.py setver - pass - -__appname__ = "unknown" -try: - from leap._appname import __appname__ -except ImportError: - #running on a tree that has not run - #the setup.py setver - pass - -__full_version__ = __appname__ + '/' + str(__version__) - -try: - from leap._branding import BRANDING as __branding -except ImportError: - __branding = {} diff --git a/src/leap/_version.py b/src/leap/_version.py deleted file mode 100644 index c33430ea..00000000 --- a/src/leap/_version.py +++ /dev/null @@ -1,197 +0,0 @@ - -IN_LONG_VERSION_PY = True -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (build by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. Generated by -# versioneer-0.7+ (https://github.com/warner/python-versioneer) - -# these strings will be replaced by git during git-archive -git_refnames = "$Format:%d$" -git_full = "$Format:%H$" - - -import subprocess -import sys - -def run_command(args, cwd=None, verbose=False): - try: - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) - except EnvironmentError: - e = sys.exc_info()[1] - if verbose: - print("unable to run %s" % args[0]) - print(e) - return None - stdout = p.communicate()[0].strip() - if sys.version >= '3': - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %s (error)" % args[0]) - return None - return stdout - - -import sys -import re -import os.path - -def get_expanded_variables(versionfile_source): - # the code embedded in _version.py can just fetch the value of these - # variables. When used from setup.py, we don't want to import - # _version.py, so we do it with a regexp instead. This function is not - # used from _version.py. - variables = {} - try: - for line in open(versionfile_source,"r").readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - variables["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - variables["full"] = mo.group(1) - except EnvironmentError: - pass - return variables - -def versions_from_expanded_variables(variables, tag_prefix, verbose=False): - refnames = variables["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("variables are unexpanded, not using") - return {} # unexpanded, so not in an unpacked git-archive tarball - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - for ref in list(refs): - if not re.search(r'\d', ref): - if verbose: - print("discarding '%s', no digits" % ref) - refs.discard(ref) - # Assume all version tags have a digit. git's %d expansion - # behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us - # distinguish between branches and tags. By ignoring refnames - # without digits, we filter out many common branch names like - # "release" and "stabilization", as well as "HEAD" and "master". - if verbose: - print("remaining refs: %s" % ",".join(sorted(refs))) - for ref in sorted(refs): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %s" % r) - return { "version": r, - "full": variables["full"].strip() } - # no suitable tags, so we use the full revision id - if verbose: - print("no suitable tags, using full revision id") - return { "version": variables["full"].strip(), - "full": variables["full"].strip() } - -def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): - # this runs 'git' from the root of the source tree. That either means - # someone ran a setup.py command (and this code is in versioneer.py, so - # IN_LONG_VERSION_PY=False, thus the containing directory is the root of - # the source tree), or someone ran a project-specific entry point (and - # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the - # containing directory is somewhere deeper in the source tree). This only - # gets called if the git-archive 'subst' variables were *not* expanded, - # and _version.py hasn't already been rewritten with a short version - # string, meaning we're inside a checked out source tree. - - try: - here = os.path.abspath(__file__) - except NameError: - # some py2exe/bbfreeze/non-CPython implementations don't do __file__ - return {} # not always correct - - # versionfile_source is the relative path from the top of the source tree - # (where the .git directory might live) to this file. Invert this to find - # the root from __file__. - root = here - if IN_LONG_VERSION_PY: - for i in range(len(versionfile_source.split("/"))): - root = os.path.dirname(root) - else: - root = os.path.dirname(here) - if not os.path.exists(os.path.join(root, ".git")): - if verbose: - print("no .git in %s" % root) - return {} - - GIT = "git" - if sys.platform == "win32": - GIT = "git.cmd" - stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], - cwd=root) - if stdout is None: - return {} - if not stdout.startswith(tag_prefix): - if verbose: - print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix)) - return {} - tag = stdout[len(tag_prefix):] - stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) - if stdout is None: - return {} - full = stdout.strip() - if tag.endswith("-dirty"): - full += "-dirty" - return {"version": tag, "full": full} - - -def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False): - if IN_LONG_VERSION_PY: - # We're running from _version.py. If it's from a source tree - # (execute-in-place), we can work upwards to find the root of the - # tree, and then check the parent directory for a version string. If - # it's in an installed application, there's no hope. - try: - here = os.path.abspath(__file__) - except NameError: - # py2exe/bbfreeze/non-CPython don't have __file__ - return {} # without __file__, we have no hope - # versionfile_source is the relative path from the top of the source - # tree to _version.py. Invert this to find the root from __file__. - root = here - for i in range(len(versionfile_source.split("/"))): - root = os.path.dirname(root) - else: - # we're running from versioneer.py, which means we're running from - # the setup.py in a source tree. sys.argv[0] is setup.py in the root. - here = os.path.abspath(sys.argv[0]) - root = os.path.dirname(here) - - # Source tarballs conventionally unpack into a directory that includes - # both the project name and a version string. - dirname = os.path.basename(root) - if not dirname.startswith(parentdir_prefix): - if verbose: - print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % - (root, dirname, parentdir_prefix)) - return None - return {"version": dirname[len(parentdir_prefix):], "full": ""} - -tag_prefix = "" -parentdir_prefix = "leap_client-" -versionfile_source = "src/leap/_version.py" - -def get_versions(default={"version": "unknown", "full": ""}, verbose=False): - variables = { "refnames": git_refnames, "full": git_full } - ver = versions_from_expanded_variables(variables, tag_prefix, verbose) - if not ver: - ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) - if not ver: - ver = versions_from_parentdir(parentdir_prefix, versionfile_source, - verbose) - if not ver: - ver = default - return ver - diff --git a/src/leap/app.py b/src/leap/app.py deleted file mode 100644 index eb38751c..00000000 --- a/src/leap/app.py +++ /dev/null @@ -1,120 +0,0 @@ -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -from functools import partial -import logging -import signal - -# This is only needed for Python v2 but is harmless for Python v3. -import sip -sip.setapi('QVariant', 2) -sip.setapi('QString', 2) -from PyQt4.QtGui import (QApplication, QSystemTrayIcon, QMessageBox) -from PyQt4 import QtCore - -from leap import __version__ as VERSION -from leap.baseapp.mainwindow import LeapWindow -from leap.gui import locale_rc - - -def sigint_handler(*args, **kwargs): - logger = kwargs.get('logger', None) - logger.debug('SIGINT catched. shutting down...') - mainwindow = args[0] - mainwindow.shutdownSignal.emit() - - -def main(): - """ - launches the main event loop - long live to the (hidden) leap window! - """ - import sys - from leap.util import leap_argparse - parser, opts = leap_argparse.init_leapc_args() - debug = getattr(opts, 'debug', False) - - # XXX get severity from command line args - if debug: - level = logging.DEBUG - else: - level = logging.WARNING - - logger = logging.getLogger(name='leap') - logger.setLevel(level) - console = logging.StreamHandler() - console.setLevel(level) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') - logger.info('LEAP client version %s', VERSION) - logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') - logfile = getattr(opts, 'log_file', False) - if logfile: - logger.debug('setting logfile to %s ', logfile) - fileh = logging.FileHandler(logfile) - fileh.setLevel(logging.DEBUG) - fileh.setFormatter(formatter) - logger.addHandler(fileh) - - logger.info('Starting app') - app = QApplication(sys.argv) - - # To test: - # $ LANG=es ./app.py - locale = QtCore.QLocale.system().name() - qtTranslator = QtCore.QTranslator() - if qtTranslator.load("qt_%s" % locale, ":/translations"): - app.installTranslator(qtTranslator) - appTranslator = QtCore.QTranslator() - if appTranslator.load("leap_client_%s" % locale, ":/translations"): - app.installTranslator(appTranslator) - - # needed for initializing qsettings - # it will write .config/leap/leap.conf - # top level app settings - # in a platform independent way - app.setOrganizationName("leap") - app.setApplicationName("leap") - app.setOrganizationDomain("leap.se") - - # XXX we could check here - # if leap-client is already running, and abort - # gracefully in that case. - - if not QSystemTrayIcon.isSystemTrayAvailable(): - QMessageBox.critical(None, "Systray", - "I couldn't detect" - "any system tray on this system.") - sys.exit(1) - if not debug: - QApplication.setQuitOnLastWindowClosed(False) - - window = LeapWindow(opts) - - # this dummy timer ensures that - # control is given to the outside loop, so we - # can hook our sigint handler. - timer = QtCore.QTimer() - timer.start(500) - timer.timeout.connect(lambda: None) - - sigint_window = partial(sigint_handler, window, logger=logger) - signal.signal(signal.SIGINT, sigint_window) - - if debug: - # we only show the main window - # if debug mode active. - # if not, it will be set visible - # from the systray menu. - window.show() - if sys.platform == "darwin": - window.raise_() - - # run main loop - sys.exit(app.exec_()) - -if __name__ == "__main__": - main() diff --git a/src/leap/base/__init__.py b/src/leap/base/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/base/auth.py b/src/leap/base/auth.py deleted file mode 100644 index c2d3f424..00000000 --- a/src/leap/base/auth.py +++ /dev/null @@ -1,355 +0,0 @@ -import binascii -import json -import logging -#import urlparse - -import requests -import srp - -from PyQt4 import QtCore - -from leap.base import constants as baseconstants -from leap.crypto import leapkeyring -from leap.util.misc import null_check -from leap.util.web import get_https_domain_and_port - -logger = logging.getLogger(__name__) - -SIGNUP_TIMEOUT = getattr(baseconstants, 'SIGNUP_TIMEOUT', 5) - -""" -Registration and authentication classes for the -SRP auth mechanism used in the leap platform. - -We're using the srp library which uses a c-based implementation -of the protocol if the c extension is available, and a python-based -one if not. -""" - - -class SRPAuthenticationError(Exception): - """ - exception raised - for authentication errors - """ - - -safe_unhexlify = lambda x: binascii.unhexlify(x) \ - if (len(x) % 2 == 0) else binascii.unhexlify('0' + x) - - -class LeapSRPRegister(object): - - def __init__(self, - schema="https", - provider=None, - verify=True, - register_path="1/users", - method="POST", - fetcher=requests, - srp=srp, - hashfun=srp.SHA256, - ng_constant=srp.NG_1024): - - null_check(provider, "provider") - - self.schema = schema - - domain, port = get_https_domain_and_port(provider) - self.provider = domain - self.port = port - - self.verify = verify - self.register_path = register_path - self.method = method - self.fetcher = fetcher - self.srp = srp - self.HASHFUN = hashfun - self.NG = ng_constant - - self.init_session() - - def init_session(self): - self.session = self.fetcher.session() - - def get_registration_uri(self): - # XXX assert is https! - # use urlparse - if self.port: - uri = "%s://%s:%s/%s" % ( - self.schema, - self.provider, - self.port, - self.register_path) - else: - uri = "%s://%s/%s" % ( - self.schema, - self.provider, - self.register_path) - - return uri - - def register_user(self, username, password, keep=False): - """ - @rtype: tuple - @rparam: (ok, request) - """ - salt, vkey = self.srp.create_salted_verification_key( - username, - password, - self.HASHFUN, - self.NG) - - user_data = { - 'user[login]': username, - 'user[password_verifier]': binascii.hexlify(vkey), - 'user[password_salt]': binascii.hexlify(salt)} - - uri = self.get_registration_uri() - logger.debug('post to uri: %s' % uri) - - # XXX get self.method - req = self.session.post( - uri, data=user_data, - timeout=SIGNUP_TIMEOUT, - verify=self.verify) - # we catch it in the form - #req.raise_for_status() - return (req.ok, req) - - -class SRPAuth(requests.auth.AuthBase): - - def __init__(self, username, password, server=None, verify=None): - # sanity check - null_check(server, 'server') - self.username = username - self.password = password - self.server = server - self.verify = verify - - logger.debug('SRPAuth. verify=%s' % verify) - logger.debug('server: %s. username=%s' % (server, username)) - - self.init_data = None - self.session = requests.session() - - self.init_srp() - - def init_srp(self): - usr = srp.User( - self.username, - self.password, - srp.SHA256, - srp.NG_1024) - uname, A = usr.start_authentication() - - self.srp_usr = usr - self.A = A - - def get_auth_data(self): - return { - 'login': self.username, - 'A': binascii.hexlify(self.A) - } - - def get_init_data(self): - try: - init_session = self.session.post( - self.server + '/1/sessions/', - data=self.get_auth_data(), - verify=self.verify) - except requests.exceptions.ConnectionError: - raise SRPAuthenticationError( - "No connection made (salt).") - except: - raise SRPAuthenticationError( - "Unknown error (salt).") - if init_session.status_code not in (200, ): - raise SRPAuthenticationError( - "No valid response (salt).") - - self.init_data = init_session.json - return self.init_data - - def get_server_proof_data(self): - try: - auth_result = self.session.put( - #self.server + '/1/sessions.json/' + self.username, - self.server + '/1/sessions/' + self.username, - data={'client_auth': binascii.hexlify(self.M)}, - verify=self.verify) - except requests.exceptions.ConnectionError: - raise SRPAuthenticationError( - "No connection made (HAMK).") - - if auth_result.status_code not in (200, ): - raise SRPAuthenticationError( - "No valid response (HAMK).") - - self.auth_data = auth_result.json - return self.auth_data - - def authenticate(self): - logger.debug('start authentication...') - - init_data = self.get_init_data() - salt = init_data.get('salt', None) - B = init_data.get('B', None) - - # XXX refactor this function - # move checks and un-hex - # to routines - - if not salt or not B: - raise SRPAuthenticationError( - "Server did not send initial data.") - - try: - unhex_salt = safe_unhexlify(salt) - except TypeError: - raise SRPAuthenticationError( - "Bad data from server (salt)") - try: - unhex_B = safe_unhexlify(B) - except TypeError: - raise SRPAuthenticationError( - "Bad data from server (B)") - - self.M = self.srp_usr.process_challenge( - unhex_salt, - unhex_B - ) - - proof_data = self.get_server_proof_data() - - HAMK = proof_data.get("M2", None) - if not HAMK: - errors = proof_data.get('errors', None) - if errors: - logger.error(errors) - raise SRPAuthenticationError("Server did not send HAMK.") - - try: - unhex_HAMK = safe_unhexlify(HAMK) - except TypeError: - raise SRPAuthenticationError( - "Bad data from server (HAMK)") - - self.srp_usr.verify_session( - unhex_HAMK) - - try: - assert self.srp_usr.authenticated() - logger.debug('user is authenticated!') - except (AssertionError): - raise SRPAuthenticationError( - "Auth verification failed.") - - def __call__(self, req): - self.authenticate() - req.cookies = self.session.cookies - return req - - -def srpauth_protected(user=None, passwd=None, server=None, verify=True): - """ - decorator factory that accepts - user and password keyword arguments - and add those to the decorated request - """ - def srpauth(fn): - def wrapper(*args, **kwargs): - if user and passwd: - auth = SRPAuth(user, passwd, server, verify) - kwargs['auth'] = auth - kwargs['verify'] = verify - if not args: - logger.warning('attempting to get from empty uri!') - return fn(*args, **kwargs) - return wrapper - return srpauth - - -def get_leap_credentials(): - settings = QtCore.QSettings() - full_username = settings.value('username') - username, domain = full_username.split('@') - seed = settings.value('%s_seed' % domain, None) - password = leapkeyring.leap_get_password(full_username, seed=seed) - return (username, password) - - -# XXX TODO -# Pass verify as single argument, -# in srpauth_protected style - -def magick_srpauth(fn): - """ - decorator that gets user and password - from the config file and adds those to - the decorated request - """ - logger.debug('magick srp auth decorator called') - - def wrapper(*args, **kwargs): - #uri = args[0] - # XXX Ugh! - # Problem with this approach. - # This won't work when we're using - # api.foo.bar - # Unless we keep a table with the - # equivalencies... - user, passwd = get_leap_credentials() - - # XXX pass verify and server too - # (pop) - auth = SRPAuth(user, passwd) - kwargs['auth'] = auth - return fn(*args, **kwargs) - return wrapper - - -if __name__ == "__main__": - """ - To test against test_provider (twisted version) - Register an user: (will be valid during the session) - >>> python auth.py add test password - - Test login with that user: - >>> python auth.py login test password - """ - - import sys - - if len(sys.argv) not in (4, 5): - print 'Usage: auth [server]' - sys.exit(0) - - action = sys.argv[1] - user = sys.argv[2] - passwd = sys.argv[3] - - if len(sys.argv) == 5: - SERVER = sys.argv[4] - else: - SERVER = "https://localhost:8443" - - if action == "login": - - @srpauth_protected( - user=user, passwd=passwd, server=SERVER, verify=False) - def test_srp_protected_get(*args, **kwargs): - req = requests.get(*args, **kwargs) - req.raise_for_status - return req - - #req = test_srp_protected_get('https://localhost:8443/1/cert') - req = test_srp_protected_get('%s/1/cert' % SERVER) - #print 'cert :', req.content[:200] + "..." - print req.content - sys.exit(0) - - if action == "add": - auth = LeapSRPRegister(provider=SERVER, verify=False) - auth.register_user(user, passwd) diff --git a/src/leap/base/authentication.py b/src/leap/base/authentication.py deleted file mode 100644 index 09ff1d07..00000000 --- a/src/leap/base/authentication.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Authentication Base Class -""" - - -class Authentication(object): - """ - I have no idea how Authentication (certs,?) - will be done, but stub it here. - """ - pass diff --git a/src/leap/base/checks.py b/src/leap/base/checks.py deleted file mode 100644 index 0bf44f59..00000000 --- a/src/leap/base/checks.py +++ /dev/null @@ -1,213 +0,0 @@ -# -*- coding: utf-8 -*- -import logging -import platform -import re -import socket - -import netifaces -import sh - -from leap.base import constants -from leap.base import exceptions - -logger = logging.getLogger(name=__name__) -_platform = platform.system() - -#EVENTS OF NOTE -EVENT_CONNECT_REFUSED = "[ECONNREFUSED]: Connection refused (code=111)" - -ICMP_TARGET = "8.8.8.8" - - -class LeapNetworkChecker(object): - """ - all network related checks - """ - def __init__(self, *args, **kwargs): - provider_gw = kwargs.pop('provider_gw', None) - self.provider_gateway = provider_gw - - def run_all(self, checker=None): - if not checker: - checker = self - #self.error = None # ? - - # for MVS - checker.check_tunnel_default_interface() - checker.check_internet_connection() - checker.is_internet_up() - - if self.provider_gateway: - checker.ping_gateway(self.provider_gateway) - - checker.parse_log_and_react([], ()) - - def check_internet_connection(self): - if _platform == "Linux": - try: - output = sh.ping("-c", "5", "-w", "5", ICMP_TARGET) - # XXX should redirect this to netcheck logger. - # and don't clutter main log. - logger.debug('Network appears to be up.') - except sh.ErrorReturnCode_1 as e: - packet_loss = re.findall("\d+% packet loss", e.message)[0] - logger.debug("Unidentified Connection Error: " + packet_loss) - if not self.is_internet_up(): - error = "No valid internet connection found." - else: - error = "Provider server appears to be down." - - logger.error(error) - raise exceptions.NoInternetConnection(error) - - else: - raise NotImplementedError - - def is_internet_up(self): - iface, gateway = self.get_default_interface_gateway() - try: - self.ping_gateway(self.provider_gateway) - except exceptions.NoConnectionToGateway: - return False - return True - - def _get_route_table_linux(self): - # do not use context manager, tests pass a StringIO - f = open("/proc/net/route") - route_table = f.readlines() - f.close() - #toss out header - route_table.pop(0) - if not route_table: - raise exceptions.NoDefaultInterfaceFoundError - return route_table - - def _get_def_iface_osx(self): - default_iface = None - #gateway = None - routes = list(sh.route('-n', 'get', ICMP_TARGET, _iter=True)) - iface = filter(lambda l: "interface" in l, routes) - if not iface: - return None, None - def_ifacel = re.findall('\w+\d', iface[0]) - default_iface = def_ifacel[0] if def_ifacel else None - if not default_iface: - return None, None - _gw = filter(lambda l: "gateway" in l, routes) - gw = re.findall('\d+\.\d+\.\d+\.\d+', _gw[0])[0] - return default_iface, gw - - def _get_tunnel_iface_linux(self): - # XXX review. - # valid also when local router has a default entry? - route_table = self._get_route_table_linux() - line = route_table.pop(0) - iface, destination = line.split('\t')[0:2] - if not destination == '00000000' or not iface == 'tun0': - raise exceptions.TunnelNotDefaultRouteError() - return True - - def check_tunnel_default_interface(self): - """ - Raises an TunnelNotDefaultRouteError - if tun0 is not the chosen default route - (including when no routes are present) - """ - #logger.debug('checking tunnel default interface...') - - if _platform == "Linux": - valid = self._get_tunnel_iface_linux() - return valid - elif _platform == "Darwin": - default_iface, gw = self._get_def_iface_osx() - #logger.debug('iface: %s', default_iface) - if default_iface != "tun0": - logger.debug('tunnel not default route! gw: %s', default_iface) - # XXX should catch this and act accordingly... - # but rather, this test should only be launched - # when we have successfully completed a connection - # ... TRIGGER: Connection stablished (or whatever it is) - # in the logs - raise exceptions.TunnelNotDefaultRouteError - else: - #logger.debug('PLATFORM !!! %s', _platform) - raise NotImplementedError - - def _get_def_iface_linux(self): - default_iface = None - gateway = None - - route_table = self._get_route_table_linux() - while route_table: - line = route_table.pop(0) - iface, destination, gateway = line.split('\t')[0:3] - if destination == '00000000': - default_iface = iface - break - return default_iface, gateway - - def get_default_interface_gateway(self): - """ - gets the interface we are going thru. - (this should be merged with check tunnel default interface, - imo...) - """ - if _platform == "Linux": - default_iface, gw = self._get_def_iface_linux() - elif _platform == "Darwin": - default_iface, gw = self._get_def_iface_osx() - else: - raise NotImplementedError - - if not default_iface: - raise exceptions.NoDefaultInterfaceFoundError - - if default_iface not in netifaces.interfaces(): - raise exceptions.InterfaceNotFoundError - logger.debug('-- default iface %s', default_iface) - return default_iface, gw - - def ping_gateway(self, gateway): - # TODO: Discuss how much packet loss (%) is acceptable. - - # XXX -- validate gateway - # -- is it a valid ip? (there's something in util) - # -- is it a domain? - # -- can we resolve? -- raise NoDNSError if not. - - # XXX -- sh.ping implemtation needs review! - try: - output = sh.ping("-c", "10", gateway).stdout - except sh.ErrorReturnCode_1 as e: - output = e.message - finally: - packet_loss = int(re.findall("(\d+)% packet loss", output)[0]) - - logger.debug('packet loss %s%%' % packet_loss) - if packet_loss > constants.MAX_ICMP_PACKET_LOSS: - raise exceptions.NoConnectionToGateway - - def check_name_resolution(self, domain_name): - try: - socket.gethostbyname(domain_name) - return True - except socket.gaierror: - raise exceptions.CannotResolveDomainError - - def parse_log_and_react(self, log, error_matrix=None): - """ - compares the recent openvpn status log to - strings passed in and executes the callbacks passed in. - @param log: openvpn log - @type log: list of strings - @param error_matrix: tuples of strings and tuples of callbacks - @type error_matrix: tuples strings and call backs - """ - for line in log: - # we could compile a regex here to save some cycles up -- kali - for each in error_matrix: - error, callbacks = each - if error in line: - for cb in callbacks: - if callable(cb): - cb() diff --git a/src/leap/base/config.py b/src/leap/base/config.py deleted file mode 100644 index d796bcf1..00000000 --- a/src/leap/base/config.py +++ /dev/null @@ -1,348 +0,0 @@ -""" -Configuration Base Class -""" -import grp -import json -import logging -import re -import socket -import time -import os - -logger = logging.getLogger(name=__name__) - -from dateutil import parser as dateparser -from xdg import BaseDirectory -import requests - -from leap.base import exceptions -from leap.base import constants -from leap.base.pluggableconfig import PluggableConfig -from leap.util.fileutil import (mkdir_p) - -# move to base! -from leap.eip import exceptions as eipexceptions - - -class BaseLeapConfig(object): - slug = None - - # XXX we have to enforce that every derived class - # has a slug (via interface) - # get property getter that raises NI.. - - def save(self): - raise NotImplementedError("abstract base class") - - def load(self): - raise NotImplementedError("abstract base class") - - def get_config(self, *kwargs): - raise NotImplementedError("abstract base class") - - @property - def config(self): - return self.get_config() - - def get_value(self, *kwargs): - raise NotImplementedError("abstract base class") - - -class MetaConfigWithSpec(type): - """ - metaclass for JSONLeapConfig classes. - It creates a configuration spec out of - the `spec` dictionary. The `properties` attribute - of the spec dict is turn into the `schema` attribute - of the new class (which will be used to validate against). - """ - # XXX in the near future, this is the - # place where we want to enforce - # singletons, read-only and similar stuff. - - def __new__(meta, classname, bases, classDict): - schema_obj = classDict.get('spec', None) - - # not quite happy with this workaround. - # I want to raise if missing spec dict, but only - # for grand-children of this metaclass. - # maybe should use abc module for this. - abcderived = ("JSONLeapConfig",) - if schema_obj is None and classname not in abcderived: - raise exceptions.ImproperlyConfigured( - "missing spec dict on your derived class (%s)" % classname) - - # we create a configuration spec attribute - # from the spec dict - config_class = type( - classname + "Spec", - (PluggableConfig, object), - {'options': schema_obj}) - classDict['spec'] = config_class - - return type.__new__(meta, classname, bases, classDict) - -########################################################## -# some hacking still in progress: - -# Configs have: - -# - a slug (from where a filename/folder is derived) -# - a spec (for validation and defaults). -# this spec is conformant to the json-schema. -# basically a dict that will be used -# for type casting and validation, and defaults settings. - -# all config objects, since they are derived from BaseConfig, implement basic -# useful methods: -# - save -# - load - -########################################################## - - -class JSONLeapConfig(BaseLeapConfig): - - __metaclass__ = MetaConfigWithSpec - - def __init__(self, *args, **kwargs): - # sanity check - try: - assert self.slug is not None - except AssertionError: - raise exceptions.ImproperlyConfigured( - "missing slug on JSONLeapConfig" - " derived class") - try: - assert self.spec is not None - except AssertionError: - raise exceptions.ImproperlyConfigured( - "missing spec on JSONLeapConfig" - " derived class") - assert issubclass(self.spec, PluggableConfig) - - self.domain = kwargs.pop('domain', None) - self._config = self.spec(format="json") - self._config.load() - self.fetcher = kwargs.pop('fetcher', requests) - - # mandatory baseconfig interface - - def save(self, to=None, force=False): - """ - force param will skip the dirty check. - :type force: bool - """ - # XXX this force=True does not feel to right - # but still have to look for a better way - # of dealing with dirtiness and the - # trick of loading remote config only - # when newer. - - if force: - do_save = True - else: - do_save = self._config.is_dirty() - - if do_save: - if to is None: - to = self.filename - folder, filename = os.path.split(to) - if folder and not os.path.isdir(folder): - mkdir_p(folder) - self._config.serialize(to) - return True - - else: - return False - - def load(self, fromfile=None, from_uri=None, fetcher=None, - force_download=False, verify=True): - - if from_uri is not None: - fetched = self.fetch( - from_uri, - fetcher=fetcher, - verify=verify, - force_dl=force_download) - if fetched: - return - if fromfile is None: - fromfile = self.filename - if os.path.isfile(fromfile): - self._config.load(fromfile=fromfile) - else: - logger.error('tried to load config from non-existent path') - logger.error('Not Found: %s', fromfile) - - def fetch(self, uri, fetcher=None, verify=True, force_dl=False): - if not fetcher: - fetcher = self.fetcher - - logger.debug('uri: %s (verify: %s)' % (uri, verify)) - - rargs = (uri, ) - rkwargs = {'verify': verify} - headers = {} - - curmtime = self.get_mtime() if not force_dl else None - if curmtime: - logger.debug('requesting with if-modified-since %s' % curmtime) - headers['if-modified-since'] = curmtime - rkwargs['headers'] = headers - - #request = fetcher.get(uri, verify=verify) - request = fetcher.get(*rargs, **rkwargs) - request.raise_for_status() - - if request.status_code == 304: - logger.debug('...304 Not Changed') - # On this point, we have to assume that - # we HAD the filename. If that filename is corruct, - # we should enforce a force_download in the load - # method above. - self._config.load(fromfile=self.filename) - return True - - if request.json: - mtime = None - last_modified = request.headers.get('last-modified', None) - if last_modified: - _mtime = dateparser.parse(last_modified) - mtime = int(_mtime.strftime("%s")) - if callable(request.json): - _json = request.json() - else: - # back-compat - _json = request.json - self._config.load(json.dumps(_json), mtime=mtime) - self._config.set_dirty() - else: - # not request.json - # might be server did not announce content properly, - # let's try deserializing all the same. - try: - self._config.load(request.content) - self._config.set_dirty() - except ValueError: - raise eipexceptions.LeapBadConfigFetchedError - - return True - - def get_mtime(self): - try: - _mtime = os.stat(self.filename)[8] - mtime = time.strftime("%c GMT", time.gmtime(_mtime)) - return mtime - except OSError: - return None - - def get_config(self): - return self._config.config - - # public methods - - def get_filename(self): - return self._slug_to_filename() - - @property - def filename(self): - return self.get_filename() - - def validate(self, data): - logger.debug('validating schema') - self._config.validate(data) - return True - - # private - - def _slug_to_filename(self): - # is this going to work in winland if slug is "foo/bar" ? - folder, filename = os.path.split(self.slug) - config_file = get_config_file(filename, folder) - return config_file - - def exists(self): - return os.path.isfile(self.filename) - - -# -# utility functions -# -# (might be moved to some class as we see fit, but -# let's remain functional for a while) -# maybe base.config.util ?? -# - - -def get_config_dir(): - """ - get the base dir for all leap config - @rparam: config path - @rtype: string - """ - home = os.path.expanduser("~") - if re.findall("leap_tests-[_a-zA-Z0-9]{6}", home): - # we're inside a test! :) - return os.path.join(home, ".config/leap") - else: - # XXX dirspec is cross-platform, - # we should borrow some of those - # routines for osx/win and wrap this call. - return os.path.join(BaseDirectory.xdg_config_home, - 'leap') - - -def get_config_file(filename, folder=None): - """ - concatenates the given filename - with leap config dir. - @param filename: name of the file - @type filename: string - @rparam: full path to config file - """ - path = [] - path.append(get_config_dir()) - if folder is not None: - path.append(folder) - path.append(filename) - return os.path.join(*path) - - -def get_default_provider_path(): - default_subpath = os.path.join("providers", - constants.DEFAULT_PROVIDER) - default_provider_path = get_config_file( - '', - folder=default_subpath) - return default_provider_path - - -def get_provider_path(domain): - # XXX if not domain, return get_default_provider_path - default_subpath = os.path.join("providers", domain) - provider_path = get_config_file( - '', - folder=default_subpath) - return provider_path - - -def validate_ip(ip_str): - """ - raises exception if the ip_str is - not a valid representation of an ip - """ - socket.inet_aton(ip_str) - - -def get_username(): - try: - return os.getlogin() - except OSError as e: - import pwd - return pwd.getpwuid(os.getuid())[0] - - -def get_groupname(): - gid = os.getgroups()[-1] - return grp.getgrgid(gid).gr_name diff --git a/src/leap/base/connection.py b/src/leap/base/connection.py deleted file mode 100644 index 41d13935..00000000 --- a/src/leap/base/connection.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Base Connection Classs -""" -from __future__ import (division, unicode_literals, print_function) - -import logging - -from leap.base.authentication import Authentication - -logger = logging.getLogger(name=__name__) - - -class Connection(Authentication): - # JSONLeapConfig - #spec = {} - - def __init__(self, *args, **kwargs): - self.connection_state = None - self.desired_connection_state = None - #XXX FIXME diamond inheritance gotcha.. - #If you inherit from >1 class, - #super is only initializing one - #of the bases..!! - # I think we better pass config as a constructor - # parameter -- kali 2012-08-30 04:33 - super(Connection, self).__init__(*args, **kwargs) - - def connect(self): - """ - entry point for connection process - """ - pass - - def disconnect(self): - """ - disconnects client - """ - pass - - #def shutdown(self): - #""" - #shutdown and quit - #""" - #self.desired_con_state = self.status.DISCONNECTED - - def connection_state(self): - """ - returns the current connection state - """ - return self.status.current - - def desired_connection_state(self): - """ - returns the desired_connection state - """ - return self.desired_connection_state - - def get_icon_name(self): - """ - get icon name from status object - """ - return self.status.get_state_icon() - - # - # private methods - # - - def _disconnect(self): - """ - private method for disconnecting - """ - if self.subp is not None: - self.subp.terminate() - self.subp = None - # XXX signal state changes! :) - - def _is_alive(self): - """ - don't know yet - """ - pass - - def _connect(self): - """ - entry point for connection cascade methods. - """ - #conn_result = ConState.DISCONNECTED - try: - conn_result = self._try_connection() - except UnrecoverableError as except_msg: - logger.error("FATAL: %s" % unicode(except_msg)) - conn_result = self.status.UNRECOVERABLE - except Exception as except_msg: - self.error_queue.append(except_msg) - logger.error("Failed Connection: %s" % - unicode(except_msg)) - return conn_result - - -class ConnectionError(Exception): - """ - generic connection error - """ - def __str__(self): - if len(self.args) >= 1: - return repr(self.args[0]) - else: - raise self() - - -class UnrecoverableError(ConnectionError): - """ - we cannot do anything about it, sorry - """ - pass diff --git a/src/leap/base/constants.py b/src/leap/base/constants.py deleted file mode 100644 index f5665e5f..00000000 --- a/src/leap/base/constants.py +++ /dev/null @@ -1,42 +0,0 @@ -"""constants to be used in base module""" -from leap import __branding -APP_NAME = __branding.get("short_name", "leap-client") -OPENVPN_BIN = "openvpn" - -# default provider placeholder -# using `example.org` we make sure that this -# is not going to be resolved during the tests phases -# (we expect testers to add it to their /etc/hosts - -DEFAULT_PROVIDER = __branding.get( - "provider_domain", - "testprovider.example.org") - -DEFINITION_EXPECTED_PATH = "provider.json" - -DEFAULT_PROVIDER_DEFINITION = { - u"api_uri": "https://api.%s/" % DEFAULT_PROVIDER, - u"api_version": u"1", - u"ca_cert_fingerprint": "SHA256: fff", - u"ca_cert_uri": u"https://%s/ca.crt" % DEFAULT_PROVIDER, - u"default_language": u"en", - u"description": { - u"en": u"A demonstration service provider using the LEAP platform" - }, - u"domain": "%s" % DEFAULT_PROVIDER, - u"enrollment_policy": u"open", - u"languages": [ - u"en" - ], - u"name": { - u"en": u"Test Provider" - }, - u"services": [ - "openvpn" - ] -} - - -MAX_ICMP_PACKET_LOSS = 10 - -ROUTE_CHECK_INTERVAL = 10 diff --git a/src/leap/base/exceptions.py b/src/leap/base/exceptions.py deleted file mode 100644 index 2e31b33b..00000000 --- a/src/leap/base/exceptions.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Exception attributes and their meaning/uses -------------------------------------------- - -* critical: if True, will abort execution prematurely, - after attempting any cleaning - action. - -* failfirst: breaks any error_check loop that is examining - the error queue. - -* message: the message that will be used in the __repr__ of the exception. - -* usermessage: the message that will be passed to user in ErrorDialogs - in Qt-land. -""" -from leap.util.translations import translate - - -class LeapException(Exception): - """ - base LeapClient exception - sets some parameters that we will check - during error checking routines - """ - - critical = False - failfirst = False - warning = False - - -class CriticalError(LeapException): - """ - we cannot do anything about it - """ - critical = True - failfirst = True - - -# In use ??? -# don't thing so. purge if not... - -class MissingConfigFileError(Exception): - pass - - -class ImproperlyConfigured(Exception): - pass - - -# NOTE: "Errors" (context) has to be a explicit string! - - -class InterfaceNotFoundError(LeapException): - # XXX should take iface arg on init maybe? - message = "interface not found" - usermessage = translate( - "Errors", - "Interface not found") - - -class NoDefaultInterfaceFoundError(LeapException): - message = "no default interface found" - usermessage = translate( - "Errors", - "Looks like your computer " - "is not connected to the internet") - - -class NoConnectionToGateway(CriticalError): - message = "no connection to gateway" - usermessage = translate( - "Errors", - "Looks like there are problems " - "with your internet connection") - - -class NoInternetConnection(CriticalError): - message = "No Internet connection found" - usermessage = translate( - "Errors", - "It looks like there is no internet connection.") - # and now we try to connect to our web to troubleshoot LOL :P - - -class CannotResolveDomainError(LeapException): - message = "Cannot resolve domain" - usermessage = translate( - "Errors", - "Domain cannot be found") - - -class TunnelNotDefaultRouteError(LeapException): - message = "Tunnel connection dissapeared. VPN down?" - usermessage = translate( - "Errors", - "The Encrypted Connection was lost.") diff --git a/src/leap/base/network.py b/src/leap/base/network.py deleted file mode 100644 index d841e692..00000000 --- a/src/leap/base/network.py +++ /dev/null @@ -1,107 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import (print_function) -import logging -import threading - -from leap.eip import config as eipconfig -from leap.base.checks import LeapNetworkChecker -from leap.base.constants import ROUTE_CHECK_INTERVAL -from leap.base.exceptions import TunnelNotDefaultRouteError -from leap.util.misc import null_check -from leap.util.coroutines import (launch_thread, process_events) - -from time import sleep - -logger = logging.getLogger(name=__name__) - - -class NetworkCheckerThread(object): - """ - Manages network checking thread that makes sure we have a working network - connection. - """ - def __init__(self, *args, **kwargs): - - self.status_signals = kwargs.pop('status_signals', None) - self.error_cb = kwargs.pop( - 'error_cb', - lambda exc: logger.error("%s", exc.message)) - self.shutdown = threading.Event() - - # XXX get provider passed here - provider = kwargs.pop('provider', None) - null_check(provider, 'provider') - - eipconf = eipconfig.EIPConfig(domain=provider) - eipconf.load() - eipserviceconf = eipconfig.EIPServiceConfig(domain=provider) - eipserviceconf.load() - - gw = eipconfig.get_eip_gateway( - eipconfig=eipconf, - eipserviceconfig=eipserviceconf) - self.checker = LeapNetworkChecker( - provider_gw=gw) - - def start(self): - self.process_handle = self._launch_recurrent_network_checks( - (self.error_cb,)) - - def stop(self): - self.process_handle.join(timeout=0.1) - self.shutdown.set() - logger.debug("network checked stopped.") - - def run_checks(self): - pass - - #private methods - - #here all the observers in fail_callbacks expect one positional argument, - #which is exception so we can try by passing a lambda with logger to - #check it works. - - def _network_checks_thread(self, fail_callbacks): - #TODO: replace this with waiting for a signal from openvpn - while True: - try: - self.checker.check_tunnel_default_interface() - break - except TunnelNotDefaultRouteError: - # XXX ??? why do we sleep here??? - # aa: If the openvpn isn't up and running yet, - # let's give it a moment to breath. - #logger.error('NOT DEFAULT ROUTE!----') - # Instead of this, we should flag when the - # iface IS SUPPOSED to be up imo. -- kali - sleep(1) - - fail_observer_dict = dict((( - observer, - process_events(observer)) for observer in fail_callbacks)) - - while not self.shutdown.is_set(): - try: - self.checker.check_tunnel_default_interface() - self.checker.check_internet_connection() - sleep(ROUTE_CHECK_INTERVAL) - except Exception as exc: - for obs in fail_observer_dict: - fail_observer_dict[obs].send(exc) - sleep(ROUTE_CHECK_INTERVAL) - - #reset event - # I see a problem with this. You cannot stop it, it - # resets itself forever. -- kali - - # XXX use QTimer for the recurrent triggers, - # and ditch the sleeps. - logger.debug('resetting event') - self.shutdown.clear() - - def _launch_recurrent_network_checks(self, fail_callbacks): - # XXX reimplement using QTimer -- kali - watcher = launch_thread( - self._network_checks_thread, - (fail_callbacks,)) - return watcher diff --git a/src/leap/base/pluggableconfig.py b/src/leap/base/pluggableconfig.py deleted file mode 100644 index 3517db6b..00000000 --- a/src/leap/base/pluggableconfig.py +++ /dev/null @@ -1,455 +0,0 @@ -""" -generic configuration handlers -""" -import copy -import json -import logging -import os -import time -import urlparse - -import jsonschema - -from leap.util.translations import LEAPTranslatable - -logger = logging.getLogger(__name__) - - -__all__ = ['PluggableConfig', - 'adaptors', - 'types', - 'UnknownOptionException', - 'MissingValueException', - 'ConfigurationProviderException', - 'TypeCastException'] - -# exceptions - - -class UnknownOptionException(Exception): - """exception raised when a non-configuration - value is present in the configuration""" - - -class MissingValueException(Exception): - """exception raised when a required value is missing""" - - -class ConfigurationProviderException(Exception): - """exception raised when a configuration provider is missing, etc""" - - -class TypeCastException(Exception): - """exception raised when a - configuration item cannot be coerced to a type""" - - -class ConfigAdaptor(object): - """ - abstract base class for config adaotors for - serialization/deserialization and custom validation - and type casting. - """ - def read(self, filename): - raise NotImplementedError("abstract base class") - - def write(self, config, filename): - with open(filename, 'w') as f: - self._write(f, config) - - def _write(self, fp, config): - raise NotImplementedError("abstract base class") - - def validate(self, config, schema): - raise NotImplementedError("abstract base class") - - -adaptors = {} - - -class JSONSchemaEncoder(json.JSONEncoder): - """ - custom default encoder that - casts python objects to json objects for - the schema validation - """ - def default(self, obj): - if obj is str: - return 'string' - if obj is unicode: - return 'string' - if obj is int: - return 'integer' - if obj is list: - return 'array' - if obj is dict: - return 'object' - if obj is bool: - return 'boolean' - - -class JSONAdaptor(ConfigAdaptor): - indent = 2 - extensions = ['json'] - - def read(self, _from): - if isinstance(_from, file): - _from_string = _from.read() - if isinstance(_from, str): - _from_string = _from - return json.loads(_from_string) - - def _write(self, fp, config): - fp.write(json.dumps(config, - indent=self.indent, - sort_keys=True)) - - def validate(self, config, schema_obj): - schema_json = JSONSchemaEncoder().encode(schema_obj) - schema = json.loads(schema_json) - jsonschema.validate(config, schema) - - -adaptors['json'] = JSONAdaptor() - -# -# Adaptors -# -# Allow to apply a predefined set of types to the -# specs, so it checks the validity of formats and cast it -# to proper python types. - -# TODO: -# - HTTPS uri - - -class DateType(object): - fmt = '%Y-%m-%d' - - def to_python(self, data): - return time.strptime(data, self.fmt) - - def get_prep_value(self, data): - return time.strftime(self.fmt, data) - - -class TranslatableType(object): - """ - a type that casts to LEAPTranslatable objects. - Used for labels we get from providers and stuff. - """ - - def to_python(self, data): - return LEAPTranslatable(data) - - # needed? we already have an extended dict... - #def get_prep_value(self, data): - #return dict(data) - - -class URIType(object): - - def to_python(self, data): - parsed = urlparse.urlparse(data) - if not parsed.scheme: - raise TypeCastException("uri %s has no schema" % data) - return parsed - - def get_prep_value(self, data): - return data.geturl() - - -class HTTPSURIType(object): - - def to_python(self, data): - parsed = urlparse.urlparse(data) - if not parsed.scheme: - raise TypeCastException("uri %s has no schema" % data) - if parsed.scheme != "https": - raise TypeCastException( - "uri %s does not has " - "https schema" % data) - return parsed - - def get_prep_value(self, data): - return data.geturl() - - -types = { - 'date': DateType(), - 'uri': URIType(), - 'https-uri': HTTPSURIType(), - 'translatable': TranslatableType(), -} - - -class PluggableConfig(object): - - options = {} - - def __init__(self, - adaptors=adaptors, - types=types, - format=None): - - self.config = {} - self.adaptors = adaptors - self.types = types - self._format = format - self.mtime = None - self.dirty = False - - @property - def option_dict(self): - if hasattr(self, 'options') and isinstance(self.options, dict): - return self.options.get('properties', None) - - def items(self): - """ - act like an iterator - """ - if isinstance(self.option_dict, dict): - return self.option_dict.items() - return self.options - - def validate(self, config, format=None): - """ - validate config - """ - schema = self.options - if format is None: - format = self._format - - if format: - adaptor = self.get_adaptor(self._format) - adaptor.validate(config, schema) - else: - # we really should make format mandatory... - logger.error('no format passed to validate') - - # first round of validation is ok. - # now we proceed to cast types if any specified. - self.to_python(config) - - def to_python(self, config): - """ - cast types following first type and then format indications. - """ - unseen_options = [i for i in config if i not in self.option_dict] - if unseen_options: - raise UnknownOptionException( - "Unknown options: %s" % ', '.join(unseen_options)) - - for key, value in config.items(): - _type = self.option_dict[key].get('type') - if _type is None and 'default' in self.option_dict[key]: - _type = type(self.option_dict[key]['default']) - if _type is not None: - tocast = True - if not callable(_type) and isinstance(value, _type): - tocast = False - if tocast: - try: - config[key] = _type(value) - except BaseException, e: - raise TypeCastException( - "Could not coerce %s, %s, " - "to type %s: %s" % (key, value, _type.__name__, e)) - _format = self.option_dict[key].get('format', None) - _ftype = self.types.get(_format, None) - if _ftype: - try: - config[key] = _ftype.to_python(value) - except BaseException, e: - raise TypeCastException( - "Could not coerce %s, %s, " - "to format %s: %s" % (key, value, - _ftype.__class__.__name__, - e)) - - return config - - def prep_value(self, config): - """ - the inverse of to_python method, - called just before serialization - """ - for key, value in config.items(): - _format = self.option_dict[key].get('format', None) - _ftype = self.types.get(_format, None) - if _ftype and hasattr(_ftype, 'get_prep_value'): - try: - config[key] = _ftype.get_prep_value(value) - except BaseException, e: - raise TypeCastException( - "Could not serialize %s, %s, " - "by format %s: %s" % (key, value, - _ftype.__class__.__name__, - e)) - else: - config[key] = value - return config - - # methods for adding configuration - - def get_default_values(self): - """ - return a config options from configuration defaults - """ - defaults = {} - for key, value in self.items(): - if 'default' in value: - defaults[key] = value['default'] - return copy.deepcopy(defaults) - - def get_adaptor(self, format): - """ - get specified format adaptor or - guess for a given filename - """ - adaptor = self.adaptors.get(format, None) - if adaptor: - return adaptor - - # not registered in adaptors dict, let's try all - for adaptor in self.adaptors.values(): - if format in adaptor.extensions: - return adaptor - - def filename2format(self, filename): - extension = os.path.splitext(filename)[-1] - return extension.lstrip('.') or None - - def serialize(self, filename, format=None, full=False): - if not format: - format = self._format - if not format: - format = self.filename2format(filename) - if not format: - raise Exception('Please specify a format') - # TODO: more specific exception type - - adaptor = self.get_adaptor(format) - if not adaptor: - raise Exception("Adaptor not found for format: %s" % format) - - config = copy.deepcopy(self.config) - serializable = self.prep_value(config) - adaptor.write(serializable, filename) - - if self.mtime: - self.touch_mtime(filename) - - def touch_mtime(self, filename): - mtime = self.mtime - os.utime(filename, (mtime, mtime)) - - def deserialize(self, string=None, fromfile=None, format=None): - """ - load configuration from a file or string - """ - - def _try_deserialize(): - if fromfile: - with open(fromfile, 'r') as f: - content = adaptor.read(f) - elif string: - content = adaptor.read(string) - return content - - # XXX cleanup this! - - if fromfile: - assert os.path.exists(fromfile) - if not format: - format = self.filename2format(fromfile) - - if not format: - format = self._format - if format: - adaptor = self.get_adaptor(format) - else: - adaptor = None - - if adaptor: - content = _try_deserialize() - return content - - # no adaptor, let's try rest of adaptors - - adaptors = self.adaptors[:] - - if format: - adaptors.sort( - key=lambda x: int( - format in x.extensions), - reverse=True) - - for adaptor in adaptors: - content = _try_deserialize() - return content - - def set_dirty(self): - self.dirty = True - - def is_dirty(self): - return self.dirty - - def load(self, *args, **kwargs): - """ - load from string or file - if no string of fromfile option is given, - it will attempt to load from defaults - defined in the schema. - """ - string = args[0] if args else None - fromfile = kwargs.get("fromfile", None) - mtime = kwargs.pop("mtime", None) - self.mtime = mtime - content = None - - # start with defaults, so we can - # have partial values applied. - content = self.get_default_values() - if string and isinstance(string, str): - content = self.deserialize(string) - - if not string and fromfile is not None: - #import ipdb;ipdb.set_trace() - content = self.deserialize(fromfile=fromfile) - - if not content: - logger.error('no content could be loaded') - # XXX raise! - return - - # lazy evaluation until first level of nesting - # to allow lambdas with context-dependant info - # like os.path.expanduser - for k, v in content.iteritems(): - if callable(v): - content[k] = v() - - self.validate(content) - self.config = content - return True - - -def testmain(): # pragma: no cover - - from tests import test_validation as t - import pprint - - config = PluggableConfig(_format="json") - properties = copy.deepcopy(t.sample_spec) - - config.options = properties - config.load(fromfile='data.json') - - print 'config' - pprint.pprint(config.config) - - config.serialize('/tmp/testserial.json') - -if __name__ == "__main__": - testmain() diff --git a/src/leap/base/providers.py b/src/leap/base/providers.py deleted file mode 100644 index d41f3695..00000000 --- a/src/leap/base/providers.py +++ /dev/null @@ -1,29 +0,0 @@ -"""all dealing with leap-providers: definition files, updating""" -from leap.base import config as baseconfig -from leap.base import specs - - -class LeapProviderDefinition(baseconfig.JSONLeapConfig): - spec = specs.leap_provider_spec - - def _get_slug(self): - domain = getattr(self, 'domain', None) - if domain: - path = baseconfig.get_provider_path(domain) - else: - path = baseconfig.get_default_provider_path() - - return baseconfig.get_config_file( - 'provider.json', folder=path) - - def _set_slug(self, *args, **kwargs): - raise AttributeError("you cannot set slug") - - slug = property(_get_slug, _set_slug) - - -class LeapProviderSet(object): - # we gather them from the filesystem - # TODO: (MVS+) - def __init__(self): - self.count = 0 diff --git a/src/leap/base/specs.py b/src/leap/base/specs.py deleted file mode 100644 index f57d7e9c..00000000 --- a/src/leap/base/specs.py +++ /dev/null @@ -1,67 +0,0 @@ -leap_provider_spec = { - 'description': 'provider definition', - 'type': 'object', - 'properties': { - #'serial': { - #'type': int, - #'default': 1, - #'required': True, - #}, - 'version': { - 'type': unicode, - 'default': '0.1.0' - #'required': True - }, - "default_language": { - 'type': unicode, - 'default': 'en' - }, - 'domain': { - 'type': unicode, # XXX define uri type - 'default': 'testprovider.example.org' - #'required': True, - }, - 'name': { - #'type': LEAPTranslatable, - 'type': dict, - 'format': 'translatable', - 'default': {u'en': u'Test Provider'} - #'required': True - }, - 'description': { - #'type': LEAPTranslatable, - 'type': dict, - 'format': 'translatable', - 'default': {u'en': u'Test provider'} - }, - 'enrollment_policy': { - 'type': unicode, # oneof ?? - 'default': 'open' - }, - 'services': { - 'type': list, # oneof ?? - 'default': ['eip'] - }, - 'api_version': { - 'type': unicode, - 'default': '0.1.0' # version regexp - }, - 'api_uri': { - 'type': unicode # uri - }, - 'public_key': { - 'type': unicode # fingerprint - }, - 'ca_cert_fingerprint': { - 'type': unicode, - }, - 'ca_cert_uri': { - 'type': unicode, - 'format': 'https-uri' - }, - 'languages': { - 'type': list, - 'default': ['en'] - } - } -} diff --git a/src/leap/base/tests/__init__.py b/src/leap/base/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/base/tests/test_auth.py b/src/leap/base/tests/test_auth.py deleted file mode 100644 index b3009a9b..00000000 --- a/src/leap/base/tests/test_auth.py +++ /dev/null @@ -1,58 +0,0 @@ -from BaseHTTPServer import BaseHTTPRequestHandler -import urlparse -try: - import unittest2 as unittest -except ImportError: - import unittest - -import requests -#from mock import Mock - -from leap.base import auth -#from leap.base import exceptions -from leap.eip.tests.test_checks import NoLogRequestHandler -from leap.testing.basetest import BaseLeapTest -from leap.testing.https_server import BaseHTTPSServerTestCase - - -class LeapSRPRegisterTests(BaseHTTPSServerTestCase, BaseLeapTest): - __name__ = "leap_srp_register_test" - provider = "testprovider.example.org" - - class request_handler(NoLogRequestHandler, BaseHTTPRequestHandler): - responses = { - '/': ['OK', '']} - - def do_GET(self): - path = urlparse.urlparse(self.path) - message = '\n'.join(self.responses.get( - path.path, None)) - self.send_response(200) - self.end_headers() - self.wfile.write(message) - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_srp_auth_should_implement_check_methods(self): - SERVER = "https://localhost:8443" - srp_auth = auth.LeapSRPRegister(provider=SERVER, verify=False) - - self.assertTrue(hasattr(srp_auth, "init_session"), - "missing meth") - self.assertTrue(hasattr(srp_auth, "get_registration_uri"), - "missing meth") - self.assertTrue(hasattr(srp_auth, "register_user"), - "missing meth") - - def test_srp_auth_basic_functionality(self): - SERVER = "https://localhost:8443" - srp_auth = auth.LeapSRPRegister(provider=SERVER, verify=False) - - self.assertIsInstance(srp_auth.session, requests.sessions.Session) - self.assertEqual( - srp_auth.get_registration_uri(), - "https://localhost:8443/1/users") diff --git a/src/leap/base/tests/test_checks.py b/src/leap/base/tests/test_checks.py deleted file mode 100644 index 8126755b..00000000 --- a/src/leap/base/tests/test_checks.py +++ /dev/null @@ -1,177 +0,0 @@ -try: - import unittest2 as unittest -except ImportError: - import unittest -import os -import sh - -from mock import (patch, Mock) -from StringIO import StringIO - -from leap.base import checks -from leap.base import exceptions -from leap.testing.basetest import BaseLeapTest - -_uid = os.getuid() - - -class LeapNetworkCheckTest(BaseLeapTest): - __name__ = "leap_network_check_tests" - - def setUp(self): - os.environ['PATH'] += ':/bin' - pass - - def tearDown(self): - pass - - def test_checker_should_implement_check_methods(self): - checker = checks.LeapNetworkChecker() - - self.assertTrue(hasattr(checker, "check_internet_connection"), - "missing meth") - self.assertTrue(hasattr(checker, "check_tunnel_default_interface"), - "missing meth") - self.assertTrue(hasattr(checker, "is_internet_up"), - "missing meth") - self.assertTrue(hasattr(checker, "ping_gateway"), - "missing meth") - self.assertTrue(hasattr(checker, "parse_log_and_react"), - "missing meth") - - def test_checker_should_actually_call_all_tests(self): - checker = checks.LeapNetworkChecker() - mc = Mock() - checker.run_all(checker=mc) - self.assertTrue(mc.check_internet_connection.called, "not called") - self.assertTrue(mc.check_tunnel_default_interface.called, "not called") - self.assertTrue(mc.is_internet_up.called, "not called") - self.assertTrue(mc.parse_log_and_react.called, "not called") - - # ping gateway only called if we pass provider_gw - checker = checks.LeapNetworkChecker(provider_gw="0.0.0.0") - mc = Mock() - checker.run_all(checker=mc) - self.assertTrue(mc.check_internet_connection.called, "not called") - self.assertTrue(mc.check_tunnel_default_interface.called, "not called") - self.assertTrue(mc.ping_gateway.called, "not called") - self.assertTrue(mc.is_internet_up.called, "not called") - self.assertTrue(mc.parse_log_and_react.called, "not called") - - def test_get_default_interface_no_interface(self): - checker = checks.LeapNetworkChecker() - with patch('leap.base.checks.open', create=True) as mock_open: - with self.assertRaises(exceptions.NoDefaultInterfaceFoundError): - mock_open.return_value = StringIO( - "Iface\tDestination Gateway\t" - "Flags\tRefCntd\tUse\tMetric\t" - "Mask\tMTU\tWindow\tIRTT") - checker.get_default_interface_gateway() - - def test_check_tunnel_default_interface(self): - checker = checks.LeapNetworkChecker() - with patch('leap.base.checks.open', create=True) as mock_open: - with self.assertRaises(exceptions.TunnelNotDefaultRouteError): - mock_open.return_value = StringIO( - "Iface\tDestination Gateway\t" - "Flags\tRefCntd\tUse\tMetric\t" - "Mask\tMTU\tWindow\tIRTT\n" - "wlan0\t00000000\t0102A8C0\t" - "0003\t0\t0\t0\t00000000\t0\t0\t0") - checker.check_tunnel_default_interface() - - with patch('leap.base.checks.open', create=True) as mock_open: - mock_open.return_value = StringIO( - "Iface\tDestination Gateway\t" - "Flags\tRefCntd\tUse\tMetric\t" - "Mask\tMTU\tWindow\tIRTT\n" - "tun0\t00000000\t01002A0A\t0003\t0\t0\t0\t00000080\t0\t0\t0") - checker.check_tunnel_default_interface() - - def test_ping_gateway_fail(self): - checker = checks.LeapNetworkChecker() - with patch.object(sh, "ping") as mocked_ping: - with self.assertRaises(exceptions.NoConnectionToGateway): - mocked_ping.return_value = Mock - mocked_ping.return_value.stdout = "11% packet loss" - checker.ping_gateway("4.2.2.2") - - def test_ping_gateway(self): - checker = checks.LeapNetworkChecker() - with patch.object(sh, "ping") as mocked_ping: - mocked_ping.return_value = Mock - mocked_ping.return_value.stdout = """ -PING 4.2.2.2 (4.2.2.2) 56(84) bytes of data. -64 bytes from 4.2.2.2: icmp_req=1 ttl=54 time=33.8 ms -64 bytes from 4.2.2.2: icmp_req=2 ttl=54 time=30.6 ms -64 bytes from 4.2.2.2: icmp_req=3 ttl=54 time=31.4 ms -64 bytes from 4.2.2.2: icmp_req=4 ttl=54 time=36.1 ms -64 bytes from 4.2.2.2: icmp_req=5 ttl=54 time=30.8 ms -64 bytes from 4.2.2.2: icmp_req=6 ttl=54 time=30.4 ms -64 bytes from 4.2.2.2: icmp_req=7 ttl=54 time=30.7 ms -64 bytes from 4.2.2.2: icmp_req=8 ttl=54 time=32.7 ms -64 bytes from 4.2.2.2: icmp_req=9 ttl=54 time=31.4 ms -64 bytes from 4.2.2.2: icmp_req=10 ttl=54 time=33.3 ms - ---- 4.2.2.2 ping statistics --- -10 packets transmitted, 10 received, 0% packet loss, time 9016ms -rtt min/avg/max/mdev = 30.497/32.172/36.161/1.755 ms""" - checker.ping_gateway("4.2.2.2") - - def test_check_internet_connection_failures(self): - checker = checks.LeapNetworkChecker() - TimeoutError = get_ping_timeout_error() - with patch.object(sh, "ping") as mocked_ping: - mocked_ping.side_effect = TimeoutError - with self.assertRaises(exceptions.NoInternetConnection): - with patch.object(checker, "ping_gateway") as mock_gateway: - mock_gateway.side_effect = exceptions.NoConnectionToGateway - checker.check_internet_connection() - - with patch.object(sh, "ping") as mocked_ping: - mocked_ping.side_effect = TimeoutError - with self.assertRaises(exceptions.NoInternetConnection): - with patch.object(checker, "ping_gateway") as mock_gateway: - mock_gateway.return_value = True - checker.check_internet_connection() - - def test_parse_log_and_react(self): - checker = checks.LeapNetworkChecker() - to_call = Mock() - log = [("leap.openvpn - INFO - Mon Nov 19 13:36:24 2012 " - "read UDPv4 [ECONNREFUSED]: Connection refused (code=111)")] - err_matrix = [(checks.EVENT_CONNECT_REFUSED, (to_call, ))] - checker.parse_log_and_react(log, err_matrix) - self.assertTrue(to_call.called) - - log = [("2012-11-19 13:36:26,177 - leap.openvpn - INFO - " - "Mon Nov 19 13:36:24 2012 ERROR: Linux route delete command " - "failed: external program exited"), - ("2012-11-19 13:36:26,178 - leap.openvpn - INFO - " - "Mon Nov 19 13:36:24 2012 ERROR: Linux route delete command " - "failed: external program exited"), - ("2012-11-19 13:36:26,180 - leap.openvpn - INFO - " - "Mon Nov 19 13:36:24 2012 ERROR: Linux route delete command " - "failed: external program exited"), - ("2012-11-19 13:36:26,181 - leap.openvpn - INFO - " - "Mon Nov 19 13:36:24 2012 /sbin/ifconfig tun0 0.0.0.0"), - ("2012-11-19 13:36:26,182 - leap.openvpn - INFO - " - "Mon Nov 19 13:36:24 2012 Linux ip addr del failed: external " - "program exited with error stat"), - ("2012-11-19 13:36:26,183 - leap.openvpn - INFO - " - "Mon Nov 19 13:36:26 2012 SIGTERM[hard,] received, process" - "exiting"), ] - to_call.reset_mock() - checker.parse_log_and_react(log, err_matrix) - self.assertFalse(to_call.called) - - to_call.reset_mock() - checker.parse_log_and_react([], err_matrix) - self.assertFalse(to_call.called) - - -def get_ping_timeout_error(): - try: - sh.ping("-c", "1", "-w", "1", "8.8.7.7") - except Exception as e: - return e diff --git a/src/leap/base/tests/test_config.py b/src/leap/base/tests/test_config.py deleted file mode 100644 index d03149b2..00000000 --- a/src/leap/base/tests/test_config.py +++ /dev/null @@ -1,247 +0,0 @@ -import json -import os -import platform -import socket -#import tempfile - -import mock -import requests - -from leap.base import config -from leap.base import constants -from leap.base import exceptions -from leap.eip import constants as eipconstants -from leap.util.fileutil import mkdir_p -from leap.testing.basetest import BaseLeapTest - - -try: - import unittest2 as unittest -except ImportError: - import unittest - -_system = platform.system() - - -class JSONLeapConfigTest(BaseLeapTest): - def setUp(self): - pass - - def tearDown(self): - pass - - def test_metaclass(self): - with self.assertRaises(exceptions.ImproperlyConfigured) as exc: - class DummyTestConfig(config.JSONLeapConfig): - __metaclass__ = config.MetaConfigWithSpec - exc.startswith("missing spec dict") - - class DummyTestConfig(config.JSONLeapConfig): - __metaclass__ = config.MetaConfigWithSpec - spec = {'properties': {}} - with self.assertRaises(exceptions.ImproperlyConfigured) as exc: - DummyTestConfig() - exc.startswith("missing slug") - - class DummyTestConfig(config.JSONLeapConfig): - __metaclass__ = config.MetaConfigWithSpec - spec = {'properties': {}} - slug = "foo" - DummyTestConfig() - -######################################3 -# -# provider fetch tests block -# - - -class ProviderTest(BaseLeapTest): - # override per test fixtures - - def setUp(self): - pass - - def tearDown(self): - pass - - -# XXX depreacated. similar test in eip.checks - -#class BareHomeTestCase(ProviderTest): -# - #__name__ = "provider_config_tests_bare_home" -# - #def test_should_raise_if_missing_eip_json(self): - #with self.assertRaises(exceptions.MissingConfigFileError): - #config.get_config_json(os.path.join(self.home, 'eip.json')) - - -class ProviderDefinitionTestCase(ProviderTest): - # XXX MOVE TO eip.test_checks - # -- kali 2012-08-24 00:38 - - __name__ = "provider_config_tests" - - def setUp(self): - # dump a sample eip file - # XXX Move to Use EIP Spec Instead!!! - # XXX tests to be moved to eip.checks and eip.providers - # XXX can use eipconfig.dump_default_eipconfig - - path = os.path.join(self.home, '.config', 'leap') - mkdir_p(path) - with open(os.path.join(path, 'eip.json'), 'w') as fp: - json.dump(eipconstants.EIP_SAMPLE_JSON, fp) - - -# these tests below should move to -# eip.checks -# config.Configuration has been deprecated - -# TODO: -# - We're instantiating a ProviderTest because we're doing the home wipeoff -# on setUpClass instead of the setUp (for speedup of the general cases). - -# We really should be testing all of them in the same testCase, and -# doing an extra wipe of the tempdir... but be careful!!!! do not mess with -# os.environ home more than needed... that could potentially bite! - -# XXX actually, another thing to fix here is separating tests: -# - test that requests has been called. -# - check deeper for error types/msgs - -# we SHOULD inject requests dep in the constructor -# (so we can pass mock easily). - - -#class ProviderFetchConError(ProviderTest): - #def test_connection_error(self): - #with mock.patch.object(requests, "get") as mock_method: - #mock_method.side_effect = requests.ConnectionError - #cf = config.Configuration() - #self.assertIsInstance(cf.error, str) -# -# -#class ProviderFetchHttpError(ProviderTest): - #def test_file_not_found(self): - #with mock.patch.object(requests, "get") as mock_method: - #mock_method.side_effect = requests.HTTPError - #cf = config.Configuration() - #self.assertIsInstance(cf.error, str) -# -# -#class ProviderFetchInvalidUrl(ProviderTest): - #def test_invalid_url(self): - #cf = config.Configuration("ht") - #self.assertTrue(cf.error) - - -# end provider fetch tests -########################################### - - -class ConfigHelperFunctions(BaseLeapTest): - - __name__ = "config_helper_tests" - - def setUp(self): - pass - - def tearDown(self): - pass - - # tests - - @unittest.skipUnless(_system == "Linux", "linux only") - def test_lin_get_config_file(self): - """ - config file path where expected? (linux) - """ - self.assertEqual( - config.get_config_file( - 'test', folder="foo/bar"), - os.path.expanduser( - '~/.config/leap/foo/bar/test') - ) - - @unittest.skipUnless(_system == "Darwin", "mac only") - def test_mac_get_config_file(self): - """ - config file path where expected? (mac) - """ - self._missing_test_for_plat(do_raise=True) - - @unittest.skipUnless(_system == "Windows", "win only") - def test_win_get_config_file(self): - """ - config file path where expected? - """ - self._missing_test_for_plat(do_raise=True) - - # - # XXX hey, I'm raising exceptions here - # on purpose. just wanted to make sure - # that the skip stuff is doing it right. - # If you're working on win/macos tests, - # feel free to remove tests that you see - # are too redundant. - - @unittest.skipUnless(_system == "Linux", "linux only") - def test_lin_get_config_dir(self): - """ - nice config dir? (linux) - """ - self.assertEqual( - config.get_config_dir(), - os.path.expanduser('~/.config/leap')) - - @unittest.skipUnless(_system == "Darwin", "mac only") - def test_mac_get_config_dir(self): - """ - nice config dir? (mac) - """ - self._missing_test_for_plat(do_raise=True) - - @unittest.skipUnless(_system == "Windows", "win only") - def test_win_get_config_dir(self): - """ - nice config dir? (win) - """ - self._missing_test_for_plat(do_raise=True) - - # provider paths - - @unittest.skipUnless(_system == "Linux", "linux only") - def test_get_default_provider_path(self): - """ - is default provider path ok? - """ - self.assertEqual( - config.get_default_provider_path(), - os.path.expanduser( - '~/.config/leap/providers/%s/' % - constants.DEFAULT_PROVIDER) - ) - - # validate ip - - def test_validate_ip(self): - """ - check our ip validation - """ - config.validate_ip('3.3.3.3') - with self.assertRaises(socket.error): - config.validate_ip('255.255.255.256') - with self.assertRaises(socket.error): - config.validate_ip('foobar') - - @unittest.skip - def test_validate_domain(self): - """ - code to be written yet - """ - raise NotImplementedError - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/base/tests/test_providers.py b/src/leap/base/tests/test_providers.py deleted file mode 100644 index f257f54d..00000000 --- a/src/leap/base/tests/test_providers.py +++ /dev/null @@ -1,150 +0,0 @@ -import copy -import json -try: - import unittest2 as unittest -except ImportError: - import unittest -import os - -import jsonschema - -#from leap import __branding as BRANDING -from leap.testing.basetest import BaseLeapTest -from leap.base import providers - - -EXPECTED_DEFAULT_CONFIG = { - u"api_version": u"0.1.0", - #u"description": "LEAPTranslatable<{u'en': u'Test provider'}>", - u"description": {u'en': u'Test provider'}, - u"default_language": u"en", - #u"display_name": {u'en': u"Test Provider"}, - u"domain": u"testprovider.example.org", - #u'name': "LEAPTranslatable<{u'en': u'Test Provider'}>", - u'name': {u'en': u'Test Provider'}, - u"enrollment_policy": u"open", - #u"serial": 1, - u"services": [ - u"eip" - ], - u"languages": [u"en"], - u"version": u"0.1.0" -} - - -class TestLeapProviderDefinition(BaseLeapTest): - def setUp(self): - self.domain = "testprovider.example.org" - self.definition = providers.LeapProviderDefinition( - domain=self.domain) - self.definition.save(force=True) - self.definition.load() # why have to load after save?? - self.config = self.definition.config - - def tearDown(self): - if hasattr(self, 'testfile') and os.path.isfile(self.testfile): - os.remove(self.testfile) - - # tests - - # XXX most of these tests can be made more abstract - # and moved to test_baseconfig *triangulate!* - - def test_provider_slug_property(self): - slug = self.definition.slug - self.assertEquals( - slug, - os.path.join( - self.home, - '.config', 'leap', 'providers', - '%s' % self.domain, - 'provider.json')) - with self.assertRaises(AttributeError): - self.definition.slug = 23 - - def test_provider_dump(self): - # check a good provider definition is dumped to disk - self.testfile = self.get_tempfile('test.json') - self.definition.save(to=self.testfile, force=True) - deserialized = json.load(open(self.testfile, 'rb')) - self.maxDiff = None - #import ipdb;ipdb.set_trace() - self.assertEqual(deserialized, EXPECTED_DEFAULT_CONFIG) - - def test_provider_dump_to_slug(self): - # same as above, but we test the ability to save to a - # file generated from the slug. - # XXX THIS TEST SHOULD MOVE TO test_baseconfig - self.definition.save() - filename = self.definition.filename - self.assertTrue(os.path.isfile(filename)) - deserialized = json.load(open(filename, 'rb')) - self.assertEqual(deserialized, EXPECTED_DEFAULT_CONFIG) - - def test_provider_load(self): - # check loading provider from disk file - self.testfile = self.get_tempfile('test_load.json') - with open(self.testfile, 'w') as wf: - wf.write(json.dumps(EXPECTED_DEFAULT_CONFIG)) - self.definition.load(fromfile=self.testfile) - #self.assertDictEqual(self.config, - #EXPECTED_DEFAULT_CONFIG) - self.assertItemsEqual(self.config, EXPECTED_DEFAULT_CONFIG) - - def test_provider_validation(self): - self.definition.validate(self.config) - _config = copy.deepcopy(self.config) - # bad type, raise validation error - _config['domain'] = 111 - with self.assertRaises(jsonschema.ValidationError): - self.definition.validate(_config) - - @unittest.skip - def test_load_malformed_json_definition(self): - raise NotImplementedError - - @unittest.skip - def test_type_validation(self): - # check various type validation - # type cast - raise NotImplementedError - - -class TestLeapProviderSet(BaseLeapTest): - - def setUp(self): - self.providers = providers.LeapProviderSet() - - def tearDown(self): - pass - ### - - def test_get_zero_count(self): - self.assertEqual(self.providers.count, 0) - - @unittest.skip - def test_count_defined_providers(self): - # check the method used for making - # the list of providers - raise NotImplementedError - - @unittest.skip - def test_get_default_provider(self): - raise NotImplementedError - - @unittest.skip - def test_should_be_at_least_one_provider_after_init(self): - # when we init an empty environment, - # there should be at least one provider, - # that will be a dump of the default provider definition - # somehow a high level test - raise NotImplementedError - - @unittest.skip - def test_get_eip_remote_from_default_provider(self): - # from: default provider - # expect: remote eip domain - raise NotImplementedError - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/base/tests/test_validation.py b/src/leap/base/tests/test_validation.py deleted file mode 100644 index 87e99648..00000000 --- a/src/leap/base/tests/test_validation.py +++ /dev/null @@ -1,92 +0,0 @@ -import copy -import datetime -#import json -try: - import unittest2 as unittest -except ImportError: - import unittest -import os - -import jsonschema - -from leap.base.config import JSONLeapConfig -from leap.base import pluggableconfig -from leap.testing.basetest import BaseLeapTest - -SAMPLE_CONFIG_DICT = { - 'prop_one': 1, - 'prop_uri': "http://example.org", - 'prop_date': '2012-12-12', -} - -EXPECTED_CONFIG = { - 'prop_one': 1, - 'prop_uri': "http://example.org", - 'prop_date': datetime.datetime(2012, 12, 12) -} - -sample_spec = { - 'description': 'sample schema definition', - 'type': 'object', - 'properties': { - 'prop_one': { - 'type': int, - 'default': 1, - 'required': True - }, - 'prop_uri': { - 'type': str, - 'default': 'http://example.org', - 'required': True, - 'format': 'uri' - }, - 'prop_date': { - 'type': str, - 'default': '2012-12-12', - 'format': 'date' - } - } -} - - -class SampleConfig(JSONLeapConfig): - spec = sample_spec - - @property - def slug(self): - return os.path.expanduser('~/sampleconfig.json') - - -class TestJSONLeapConfigValidation(BaseLeapTest): - def setUp(self): - self.sampleconfig = SampleConfig() - self.sampleconfig.save() - self.sampleconfig.load() - self.config = self.sampleconfig.config - - def tearDown(self): - if hasattr(self, 'testfile') and os.path.isfile(self.testfile): - os.remove(self.testfile) - - # tests - - def test_good_validation(self): - self.sampleconfig.validate(SAMPLE_CONFIG_DICT) - - def test_broken_int(self): - _config = copy.deepcopy(SAMPLE_CONFIG_DICT) - _config['prop_one'] = '1' - with self.assertRaises(jsonschema.ValidationError): - self.sampleconfig.validate(_config) - - def test_format_property(self): - # JsonSchema Validator does not check the format property. - # We should have to extend the Configuration class - blah = copy.deepcopy(SAMPLE_CONFIG_DICT) - blah['prop_uri'] = 'xxx' - with self.assertRaises(pluggableconfig.TypeCastException): - self.sampleconfig.validate(blah) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/baseapp/__init__.py b/src/leap/baseapp/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/baseapp/constants.py b/src/leap/baseapp/constants.py deleted file mode 100644 index e312be21..00000000 --- a/src/leap/baseapp/constants.py +++ /dev/null @@ -1,6 +0,0 @@ -# This timer used for polling vpn manager state. - -# XXX what is an optimum polling interval? -# too little will be overkill, too much will -# miss transition states. -TIMER_MILLISECONDS = 250.0 diff --git a/src/leap/baseapp/dialogs.py b/src/leap/baseapp/dialogs.py deleted file mode 100644 index d256fc99..00000000 --- a/src/leap/baseapp/dialogs.py +++ /dev/null @@ -1,61 +0,0 @@ -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -import logging - -from PyQt4.QtGui import (QDialog, QFrame, QPushButton, QLabel, QMessageBox) - -logger = logging.getLogger(name=__name__) - - -class ErrorDialog(QDialog): - def __init__(self, parent=None, errtype=None, msg=None, label=None): - super(ErrorDialog, self).__init__(parent) - frameStyle = QFrame.Sunken | QFrame.Panel - self.warningLabel = QLabel() - self.warningLabel.setFrameStyle(frameStyle) - self.warningButton = QPushButton("QMessageBox.&warning()") - - if msg is not None: - self.msg = msg - if label is not None: - self.label = label - if errtype == "critical": - self.criticalMessage(self.msg, self.label) - - def warningMessage(self, msg, label): - msgBox = QMessageBox(QMessageBox.Warning, - "LEAP Client Error", - msg, - QMessageBox.NoButton, self) - msgBox.addButton("&Ok", QMessageBox.AcceptRole) - if msgBox.exec_() == QMessageBox.AcceptRole: - pass - # do whatever we want to do after - # closing the dialog. we can pass that - # in the constructor - - def criticalMessage(self, msg, label): - msgBox = QMessageBox(QMessageBox.Critical, - "LEAP Client Error", - msg, - QMessageBox.NoButton, self) - msgBox.addButton("&Ok", QMessageBox.AcceptRole) - msgBox.exec_() - - # It's critical, so we exit. - # We should better emit a signal and connect it - # with the proper shutdownAndQuit method, but - # this suffices for now. - logger.info('Quitting') - import sys - sys.exit() - - def confirmMessage(self, msg, label, action): - msgBox = QMessageBox(QMessageBox.Critical, - self.tr("LEAP Client Error"), - msg, - QMessageBox.NoButton, self) - msgBox.addButton("&Ok", QMessageBox.AcceptRole) - msgBox.addButton("&Cancel", QMessageBox.RejectRole) - - if msgBox.exec_() == QMessageBox.AcceptRole: - action() diff --git a/src/leap/baseapp/eip.py b/src/leap/baseapp/eip.py deleted file mode 100644 index b34cc82e..00000000 --- a/src/leap/baseapp/eip.py +++ /dev/null @@ -1,243 +0,0 @@ -from __future__ import print_function -import logging -import time -#import sys - -from PyQt4 import QtCore - -from leap.baseapp.dialogs import ErrorDialog -from leap.baseapp import constants -from leap.eip import exceptions as eip_exceptions -from leap.eip.eipconnection import EIPConnection -from leap.base.checks import EVENT_CONNECT_REFUSED -from leap.util import geo - -logger = logging.getLogger(name=__name__) - - -class EIPConductorAppMixin(object): - """ - initializes an instance of EIPConnection, - gathers errors, and passes status-change signals - from Qt land along to the conductor. - Connects the eip connect/disconnect logic - to the switches in the app (buttons/menu items). - """ - ERR_DIALOG = False - - def __init__(self, *args, **kwargs): - opts = kwargs.pop('opts') - config_file = getattr(opts, 'config_file', None) - provider = kwargs.pop('provider') - - self.eip_service_started = False - - # conductor (eip connection) is in charge of all - # vpn-related configuration / monitoring. - # we pass a tuple of signals that will be - # triggered when status changes. - - self.conductor = EIPConnection( - watcher_cb=self.newLogLine.emit, - config_file=config_file, - checker_signals=(self.eipStatusChange.emit, ), - status_signals=(self.openvpnStatusChange.emit, ), - debug=self.debugmode, - ovpn_verbosity=opts.openvpn_verb, - provider=provider) - - # Do we want to enable the skip checks w/o being - # in debug mode?? - #self.skip_download = opts.no_provider_checks - #self.skip_verify = opts.no_ca_verify - self.skip_download = False - self.skip_verify = False - - def run_eip_checks(self): - """ - runs eip checks and - the error checking loop - """ - logger.debug('running EIP CHECKS') - self.conductor.run_checks( - skip_download=self.skip_download, - skip_verify=self.skip_verify) - self.error_check() - - self.start_eipconnection.emit() - - def error_check(self): - """ - consumes the conductor error queue. - pops errors, and acts accordingly (launching user dialogs). - """ - logger.debug('error check') - - errq = self.conductor.error_queue - while errq.qsize() != 0: - logger.debug('%s errors left in conductor queue', errq.qsize()) - # we get exception and original traceback from queue - error, tb = errq.get() - - # redundant log, debugging the loop. - logger.error('%s: %s', error.__class__.__name__, error.message) - - if issubclass(error.__class__, eip_exceptions.EIPClientError): - self.triggerEIPError.emit(error) - - else: - # deprecated form of raising exception. - raise error, None, tb - - if error.failfirst is True: - break - - @QtCore.pyqtSlot(object) - def onEIPError(self, error): - """ - check severity and launches - dialogs informing user about the errors. - in the future we plan to derive errors to - our log viewer. - """ - if self.ERR_DIALOG: - logger.warning('another error dialog suppressed') - return - - # XXX this is actually a one-shot. - # On the dialog there should be - # a reset signal binded to the ok button - # or something like that. - self.ERR_DIALOG = True - - if getattr(error, 'usermessage', None): - message = error.usermessage - else: - message = error.message - - # XXX - # check headless = False before - # launching dialog. - # (so Qt tests can assert stuff) - - if error.critical: - logger.critical(error.message) - #critical error (non recoverable), - #we give user some info and quit. - #(critical error dialog will exit app) - ErrorDialog(errtype="critical", - msg=message, - label="critical error") - - elif error.warning: - logger.warning(error.message) - - else: - dialog = ErrorDialog() - dialog.warningMessage(message, 'error') - - @QtCore.pyqtSlot() - def statusUpdate(self): - """ - polls status and updates ui with real time - info about transferred bytes / connection state. - right now is triggered by a timer tick - (timer controlled by StatusAwareTrayIcon class) - """ - # TODO I guess it's too expensive to poll - # continously. move to signal events instead. - # (i.e., subscribe to connection status changes - # from openvpn manager) - - if not self.eip_service_started: - # there is a race condition - # going on here. Depending on how long we take - # to init the qt app, the management socket - # is not ready yet. - return - - #if self.conductor.with_errors: - #XXX how to wait on pkexec??? - #something better that this workaround, plz!! - #I removed the pkexec pass authentication at all. - #time.sleep(5) - #logger.debug('timeout') - #logger.error('errors. disconnect') - #self.start_or_stopVPN() # is stop - - state = self.conductor.poll_connection_state() - if not state: - return - - ts, con_status, ok, ip, remote = state - self.set_statusbarMessage(con_status) - self.setIconToolTip() - - ts = time.strftime("%a %b %d %X", ts) - if self.debugmode: - self.updateTS.setText(ts) - self.status_label.setText(con_status) - self.ip_label.setText(ip) - self.remote_label.setText(remote) - self.remote_country.setText( - geo.get_country_name(remote)) - - # status i/o - - status = self.conductor.get_status_io() - if status and self.debugmode: - #XXX move this to systray menu indicators - ts, (tun_read, tun_write, tcp_read, tcp_write, auth_read) = status - ts = time.strftime("%a %b %d %X", ts) - self.updateTS.setText(ts) - self.tun_read_bytes.setText(tun_read) - self.tun_write_bytes.setText(tun_write) - - # connection information via management interface - log = self.conductor.get_log() - error_matrix = [(EVENT_CONNECT_REFUSED, (self.start_or_stopVPN, ))] - if hasattr(self.network_checker, 'checker'): - self.network_checker.checker.parse_log_and_react(log, error_matrix) - - @QtCore.pyqtSlot() - def start_or_stopVPN(self, **kwargs): - """ - stub for running child process with vpn - """ - if self.conductor.has_errors(): - logger.debug('not starting vpn; conductor has errors') - return - - if self.eip_service_started is False: - try: - self.conductor.connect() - - except eip_exceptions.EIPNoCommandError as exc: - logger.error('tried to run openvpn but no command is set') - self.triggerEIPError.emit(exc) - - except Exception as err: - # raise generic exception (Bad Thing Happened?) - logger.exception(err) - else: - # no errors, so go on. - if self.debugmode: - self.startStopButton.setText(self.tr('&Disconnect')) - self.eip_service_started = True - self.toggleEIPAct() - - # XXX decouple! (timer is init by icons class). - # we could bring Timer Init to this Mixin - # or to its own Mixin. - self.timer.start(constants.TIMER_MILLISECONDS) - return - - if self.eip_service_started is True: - self.network_checker.stop() - self.conductor.disconnect() - if self.debugmode: - self.startStopButton.setText(self.tr('&Connect')) - self.eip_service_started = False - self.toggleEIPAct() - self.timer.stop() - return diff --git a/src/leap/baseapp/leap_app.py b/src/leap/baseapp/leap_app.py deleted file mode 100644 index 4d3aebd6..00000000 --- a/src/leap/baseapp/leap_app.py +++ /dev/null @@ -1,153 +0,0 @@ -import logging - -import sip -sip.setapi('QVariant', 2) - -from PyQt4 import QtCore -from PyQt4 import QtGui - -from leap.gui import mainwindow_rc - -logger = logging.getLogger(name=__name__) - - -APP_LOGO = ':/images/leap-color-small.png' - - -class MainWindowMixin(object): - """ - create the main window - for leap app - """ - - def __init__(self, *args, **kwargs): - # XXX set initial visibility - # debug = no visible - - widget = QtGui.QWidget() - self.setCentralWidget(widget) - - mainLayout = QtGui.QVBoxLayout() - # add widgets to layout - #self.createWindowHeader() - #mainLayout.addWidget(self.headerBox) - - # created in systray - mainLayout.addWidget(self.statusIconBox) - if self.debugmode: - mainLayout.addWidget(self.statusBox) - mainLayout.addWidget(self.loggerBox) - widget.setLayout(mainLayout) - - self.createMainActions() - self.createMainMenus() - - self.setWindowTitle("LEAP Client") - self.set_app_icon() - self.set_statusbarMessage('ready') - - def createMainActions(self): - #self.openAct = QtGui.QAction("&Open...", self, shortcut="Ctrl+O", - #triggered=self.open) - - self.firstRunWizardAct = QtGui.QAction( - "&First run wizard...", self, - triggered=self.stop_connection_and_launch_first_run_wizard) - self.aboutAct = QtGui.QAction("&About", self, triggered=self.about) - - #self.aboutQtAct = QtGui.QAction("About &Qt", self, - #triggered=QtGui.qApp.aboutQt) - - def createMainMenus(self): - self.connMenu = QtGui.QMenu("&Connections", self) - #self.viewMenu.addSeparator() - self.connMenu.addAction(self.quitAction) - - self.settingsMenu = QtGui.QMenu("&Settings", self) - self.settingsMenu.addAction(self.firstRunWizardAct) - - self.helpMenu = QtGui.QMenu("&Help", self) - self.helpMenu.addAction(self.aboutAct) - #self.helpMenu.addAction(self.aboutQtAct) - - self.menuBar().addMenu(self.connMenu) - self.menuBar().addMenu(self.settingsMenu) - self.menuBar().addMenu(self.helpMenu) - - def stop_connection_and_launch_first_run_wizard(self): - settings = QtCore.QSettings() - settings.setValue('FirstRunWizardDone', False) - logger.debug('should run first run wizard again...') - - status = self.conductor.get_icon_name() - if status != "disconnected": - self.start_or_stopVPN() - - self.launch_first_run_wizard() - #from leap.gui.firstrunwizard import FirstRunWizard - #wizard = FirstRunWizard( - #parent=self, - #success_cb=self.initReady.emit) - #wizard.show() - - def set_app_icon(self): - icon = QtGui.QIcon(APP_LOGO) - self.setWindowIcon(icon) - - #def createWindowHeader(self): - #""" - #description lines for main window - #""" - #self.headerBox = QtGui.QGroupBox() - #self.headerLabel = QtGui.QLabel( - #"LEAP Encryption Access Project") - #self.headerLabelSub = QtGui.QLabel( - #"
your internet encryption toolkit") -# - #pixmap = QtGui.QPixmap(APP_LOGO) - #leap_lbl = QtGui.QLabel() - #leap_lbl.setPixmap(pixmap) -# - #headerLayout = QtGui.QHBoxLayout() - #headerLayout.addWidget(leap_lbl) - #headerLayout.addWidget(self.headerLabel) - #headerLayout.addWidget(self.headerLabelSub) - #headerLayout.addStretch() - #self.headerBox.setLayout(headerLayout) - - def set_statusbarMessage(self, msg): - self.statusBar().showMessage(msg) - - def closeEvent(self, event): - """ - redefines close event (persistent window behaviour) - """ - if self.trayIcon.isVisible() and not self.debugmode: - QtGui.QMessageBox.information( - self, "Systray", - "The program will keep running " - "in the system tray. To " - "terminate the program, choose " - "Quit in the " - "context menu of the system tray entry.") - self.hide() - event.ignore() - return - self.cleanupAndQuit() - - def cleanupAndQuit(self): - """ - cleans state before shutting down app. - """ - # save geometry for restoring - settings = QtCore.QSettings() - geom_key = "DebugGeometry" if self.debugmode else "Geometry" - settings.setValue(geom_key, self.saveGeometry()) - - # TODO:make sure to shutdown all child process / threads - # in conductor - # XXX send signal instead? - logger.info('Shutting down') - self.conductor.disconnect(shutdown=True) - logger.info('Exiting. Bye.') - QtGui.qApp.quit() diff --git a/src/leap/baseapp/log.py b/src/leap/baseapp/log.py deleted file mode 100644 index 636e5bae..00000000 --- a/src/leap/baseapp/log.py +++ /dev/null @@ -1,69 +0,0 @@ -import logging - -from PyQt4 import QtGui -from PyQt4 import QtCore - -vpnlogger = logging.getLogger('leap.openvpn') - - -class LogPaneMixin(object): - """ - a simple log pane - that writes new lines as they come - """ - EXCLUDES = ('MANAGEMENT',) - - def createLogBrowser(self): - """ - creates Browser widget for displaying logs - (in debug mode only). - """ - self.loggerBox = QtGui.QGroupBox() - logging_layout = QtGui.QVBoxLayout() - self.logbrowser = QtGui.QTextBrowser() - - startStopButton = QtGui.QPushButton(self.tr("&Connect")) - self.startStopButton = startStopButton - - logging_layout.addWidget(self.logbrowser) - logging_layout.addWidget(self.startStopButton) - self.loggerBox.setLayout(logging_layout) - - # status box - - self.statusBox = QtGui.QGroupBox() - grid = QtGui.QGridLayout() - - self.updateTS = QtGui.QLabel('') - self.status_label = QtGui.QLabel(self.tr('Disconnected')) - self.ip_label = QtGui.QLabel('') - self.remote_label = QtGui.QLabel('') - self.remote_country = QtGui.QLabel('') - - tun_read_label = QtGui.QLabel("tun read") - self.tun_read_bytes = QtGui.QLabel("0") - tun_write_label = QtGui.QLabel("tun write") - self.tun_write_bytes = QtGui.QLabel("0") - - grid.addWidget(self.updateTS, 0, 0) - grid.addWidget(self.status_label, 0, 1) - grid.addWidget(self.ip_label, 1, 0) - grid.addWidget(self.remote_label, 1, 1) - grid.addWidget(self.remote_country, 2, 1) - grid.addWidget(tun_read_label, 3, 0) - grid.addWidget(self.tun_read_bytes, 3, 1) - grid.addWidget(tun_write_label, 4, 0) - grid.addWidget(self.tun_write_bytes, 4, 1) - - self.statusBox.setLayout(grid) - - @QtCore.pyqtSlot(str) - def onLoggerNewLine(self, line): - """ - simple slot: writes new line to logger Pane. - """ - msg = line[:-1] - if self.debugmode and all(map(lambda w: w not in msg, - LogPaneMixin.EXCLUDES)): - self.logbrowser.append(msg) - vpnlogger.info(msg) diff --git a/src/leap/baseapp/mainwindow.py b/src/leap/baseapp/mainwindow.py deleted file mode 100644 index 91b0dc61..00000000 --- a/src/leap/baseapp/mainwindow.py +++ /dev/null @@ -1,191 +0,0 @@ -# vim: set fileencoding=utf-8 : -#!/usr/bin/env python -import logging - -import sip -sip.setapi('QString', 2) -sip.setapi('QVariant', 2) - -from PyQt4 import QtCore -from PyQt4 import QtGui - -from leap.baseapp.eip import EIPConductorAppMixin -from leap.baseapp.log import LogPaneMixin -from leap.baseapp.systray import StatusAwareTrayIconMixin -from leap.baseapp.network import NetworkCheckerAppMixin -from leap.baseapp.leap_app import MainWindowMixin -from leap.eip.checks import ProviderCertChecker -from leap.gui.threads import FunThread - -logger = logging.getLogger(name=__name__) - - -class LeapWindow(QtGui.QMainWindow, - MainWindowMixin, EIPConductorAppMixin, - StatusAwareTrayIconMixin, - NetworkCheckerAppMixin, - LogPaneMixin): - """ - main window for the leap app. - Initializes all of its base classes - We keep here some signal initialization - that gets tricky otherwise. - """ - - # signals - - newLogLine = QtCore.pyqtSignal([str]) - mainappReady = QtCore.pyqtSignal([]) - initReady = QtCore.pyqtSignal([]) - networkError = QtCore.pyqtSignal([object]) - triggerEIPError = QtCore.pyqtSignal([object]) - start_eipconnection = QtCore.pyqtSignal([]) - shutdownSignal = QtCore.pyqtSignal([]) - initNetworkChecker = QtCore.pyqtSignal([]) - - # this is status change got from openvpn management - openvpnStatusChange = QtCore.pyqtSignal([object]) - # this is global eip status - eipStatusChange = QtCore.pyqtSignal([str]) - - def __init__(self, opts): - logger.debug('init leap window') - self.debugmode = getattr(opts, 'debug', False) - super(LeapWindow, self).__init__() - if self.debugmode: - self.createLogBrowser() - - settings = QtCore.QSettings() - self.provider_domain = settings.value("provider_domain", None) - self.username = settings.value("username", None) - - logger.debug('provider: %s', self.provider_domain) - logger.debug('username: %s', self.username) - - provider = self.provider_domain - EIPConductorAppMixin.__init__( - self, opts=opts, provider=provider) - StatusAwareTrayIconMixin.__init__(self) - - # XXX network checker should probably not - # trigger run_checks on init... but wait - # for ready signal instead... - NetworkCheckerAppMixin.__init__(self, provider=provider) - MainWindowMixin.__init__(self) - - geom_key = "DebugGeometry" if self.debugmode else "Geometry" - geom = settings.value(geom_key) - if geom: - self.restoreGeometry(geom) - - # XXX check for wizard - self.wizard_done = settings.value("FirstRunWizardDone") - - self.initchecks = FunThread(self.run_eip_checks) - - # bind signals - self.initchecks.finished.connect( - lambda: logger.debug('Initial checks thread finished')) - self.trayIcon.activated.connect(self.iconActivated) - self.newLogLine.connect( - lambda line: self.onLoggerNewLine(line)) - self.timer.timeout.connect( - lambda: self.onTimerTick()) - self.networkError.connect( - lambda exc: self.onNetworkError(exc)) - self.triggerEIPError.connect( - lambda exc: self.onEIPError(exc)) - - if self.debugmode: - self.startStopButton.clicked.connect( - lambda: self.start_or_stopVPN()) - self.start_eipconnection.connect( - self.do_start_eipconnection) - self.shutdownSignal.connect( - self.cleanupAndQuit) - self.initNetworkChecker.connect( - lambda: self.init_network_checker(self.conductor.provider)) - - # status change. - # TODO unify - self.openvpnStatusChange.connect( - lambda status: self.onOpenVPNStatusChange(status)) - self.eipStatusChange.connect( - lambda newstatus: self.onEIPConnStatusChange(newstatus)) - self.eipStatusChange.connect( - lambda newstatus: self.toggleEIPAct()) - - # do first run wizard and init signals - self.mainappReady.connect(self.do_first_run_wizard_check) - self.initReady.connect(self.runchecks_and_eipconnect) - - # ... all ready. go! - # connected to do_first_run_wizard_check - self.mainappReady.emit() - - def do_first_run_wizard_check(self): - """ - checks whether first run wizard needs to be run - launches it if needed - and emits initReady signal if not. - """ - - logger.debug('first run wizard check...') - need_wizard = False - - # do checks (can overlap if wizard was interrupted) - if not self.wizard_done: - need_wizard = True - - if not self.provider_domain: - need_wizard = True - else: - pcertchecker = ProviderCertChecker(domain=self.provider_domain) - if not pcertchecker.is_cert_valid(do_raise=False): - logger.warning('missing valid client cert. need wizard') - need_wizard = True - - # launch wizard if needed - if need_wizard: - logger.debug('running first run wizard') - self.launch_first_run_wizard() - else: # no wizard needed - self.initReady.emit() - - def launch_first_run_wizard(self): - """ - launches wizard and blocks - """ - from leap.gui.firstrun.wizard import FirstRunWizard - wizard = FirstRunWizard( - self.conductor, - parent=self, - username=self.username, - start_eipconnection_signal=self.start_eipconnection, - eip_statuschange_signal=self.eipStatusChange, - quitcallback=self.onWizardCancel) - wizard.show() - - def onWizardCancel(self): - if not self.wizard_done: - logger.debug( - 'clicked on Cancel during first ' - 'run wizard. shutting down') - self.cleanupAndQuit() - - def runchecks_and_eipconnect(self): - """ - shows icon and run init checks - """ - self.show_systray_icon() - self.initchecks.begin() - - def do_start_eipconnection(self): - """ - shows icon and init eip connection - called from the end of wizard - """ - self.show_systray_icon() - # this will setup the command - self.conductor.run_openvpn_checks() - self.start_or_stopVPN() diff --git a/src/leap/baseapp/network.py b/src/leap/baseapp/network.py deleted file mode 100644 index dc5182a4..00000000 --- a/src/leap/baseapp/network.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import print_function - -import logging - -logger = logging.getLogger(name=__name__) - -from PyQt4 import QtCore - -from leap.baseapp.dialogs import ErrorDialog -from leap.base.network import NetworkCheckerThread - -from leap.util.misc import null_check - - -class NetworkCheckerAppMixin(object): - """ - initialize an instance of the Network Checker, - which gathers error and passes them on. - """ - ERR_NETERR = False - - def __init__(self, *args, **kwargs): - provider = kwargs.pop('provider', None) - self.network_checker = None - if provider: - self.init_network_checker(provider) - - def init_network_checker(self, provider): - null_check(provider, "provider_domain") - if not self.network_checker: - self.network_checker = NetworkCheckerThread( - error_cb=self.networkError.emit, - debug=self.debugmode, - provider=provider) - self.network_checker.start() - - @QtCore.pyqtSlot(object) - def runNetworkChecks(self): - logger.debug('running checks (from NetworkChecker Mixin slot)') - self.network_checker.run_checks() - - @QtCore.pyqtSlot(object) - def onNetworkError(self, exc): - """ - slot that receives a network exceptions - and raises a user error message - """ - # FIXME this should not HANDLE anything after - # the network check thread has been stopped. - - logger.debug('handling network exception') - if not self.ERR_NETERR: - self.ERR_NETERR = True - - logger.error(exc.message) - dialog = ErrorDialog(parent=self) - if exc.critical: - dialog.criticalMessage(exc.usermessage, "network error") - else: - dialog.warningMessage(exc.usermessage, "network error") - - self.start_or_stopVPN() - self.network_checker.stop() diff --git a/src/leap/baseapp/permcheck.py b/src/leap/baseapp/permcheck.py deleted file mode 100644 index 6b74cb6e..00000000 --- a/src/leap/baseapp/permcheck.py +++ /dev/null @@ -1,17 +0,0 @@ -import commands -import os - -from leap.util.fileutil import which - - -def is_pkexec_in_system(): - pkexec_path = which('pkexec') - if not pkexec_path: - return False - return os.access(pkexec_path, os.X_OK) - - -def is_auth_agent_running(): - return bool( - commands.getoutput( - 'ps aux | grep polkit-[g]nome-authentication-agent-1')) diff --git a/src/leap/baseapp/systray.py b/src/leap/baseapp/systray.py deleted file mode 100644 index 77eb3fe9..00000000 --- a/src/leap/baseapp/systray.py +++ /dev/null @@ -1,268 +0,0 @@ -import logging -import sys - -import sip -sip.setapi('QString', 2) -sip.setapi('QVariant', 2) - -from PyQt4 import QtCore -from PyQt4 import QtGui - -from leap import __branding as BRANDING -from leap import __version__ as VERSION - -from leap.gui import mainwindow_rc - -logger = logging.getLogger(__name__) - - -class StatusAwareTrayIconMixin(object): - """ - a mix of several functions needed - to create a systray and make it - get updated from conductor status - polling. - """ - states = { - "disconnected": 0, - "connecting": 1, - "connected": 2} - - iconpath = { - "disconnected": ':/images/conn_error.png', - "connecting": ':/images/conn_connecting.png', - "connected": ':/images/conn_connected.png'} - - Icons = { - 'disconnected': lambda self: QtGui.QIcon( - self.iconpath['disconnected']), - 'connecting': lambda self: QtGui.QIcon( - self.iconpath['connecting']), - 'connected': lambda self: QtGui.QIcon( - self.iconpath['connected']) - } - - def __init__(self, *args, **kwargs): - self.createIconGroupBox() - self.createActions() - self.createTrayIcon() - - # not sure if this really belongs here, but... - self.timer = QtCore.QTimer() - - def show_systray_icon(self): - #logger.debug('showing tray icon................') - self.trayIcon.show() - - def createIconGroupBox(self): - """ - dummy icongroupbox - (to be removed from here -- reference only) - """ - con_widgets = { - 'disconnected': QtGui.QLabel(), - 'connecting': QtGui.QLabel(), - 'connected': QtGui.QLabel(), - } - con_widgets['disconnected'].setPixmap( - QtGui.QPixmap( - self.iconpath['disconnected'])) - con_widgets['connecting'].setPixmap( - QtGui.QPixmap( - self.iconpath['connecting'])) - con_widgets['connected'].setPixmap( - QtGui.QPixmap( - self.iconpath['connected'])), - self.ConnectionWidgets = con_widgets - - self.statusIconBox = QtGui.QGroupBox( - self.tr("EIP Connection Status")) - statusIconLayout = QtGui.QHBoxLayout() - statusIconLayout.addWidget(self.ConnectionWidgets['disconnected']) - statusIconLayout.addWidget(self.ConnectionWidgets['connecting']) - statusIconLayout.addWidget(self.ConnectionWidgets['connected']) - statusIconLayout.itemAt(1).widget().hide() - statusIconLayout.itemAt(2).widget().hide() - - self.leapConnStatus = QtGui.QLabel( - self.tr("disconnected")) - statusIconLayout.addWidget(self.leapConnStatus) - - self.statusIconBox.setLayout(statusIconLayout) - - def createTrayIcon(self): - """ - creates the tray icon - """ - self.trayIconMenu = QtGui.QMenu(self) - - self.trayIconMenu.addAction(self.connAct) - self.trayIconMenu.addSeparator() - self.trayIconMenu.addAction(self.detailsAct) - self.trayIconMenu.addSeparator() - self.trayIconMenu.addAction(self.aboutAct) - # we should get this hidden inside the "about" dialog - # (as a little button maybe) - #self.trayIconMenu.addAction(self.aboutQtAct) - self.trayIconMenu.addSeparator() - self.trayIconMenu.addAction(self.quitAction) - - self.trayIcon = QtGui.QSystemTrayIcon(self) - self.setIcon('disconnected') - self.trayIcon.setContextMenu(self.trayIconMenu) - - #self.trayIconMenu.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - #self.trayIconMenu.customContextMenuRequested.connect( - #self.on_context_menu) - - #def bad(self): - #logger.error('this should not be called') - - def createActions(self): - """ - creates actions to be binded to tray icon - """ - # XXX change action name on (dis)connect - self.connAct = QtGui.QAction( - self.tr("Encryption ON turn &off"), - self, - triggered=lambda: self.start_or_stopVPN()) - - self.detailsAct = QtGui.QAction( - self.tr("&Details..."), - self, - triggered=self.detailsWin) - self.aboutAct = QtGui.QAction( - self.tr("&About"), self, - triggered=self.about) - self.aboutQtAct = QtGui.QAction( - self.tr("About Q&t"), self, - triggered=QtGui.qApp.aboutQt) - self.quitAction = QtGui.QAction( - self.tr("&Quit"), self, - triggered=self.cleanupAndQuit) - - def toggleEIPAct(self): - # this is too simple by now. - # XXX get STATUS CONSTANTS INSTEAD - - icon_status = self.conductor.get_icon_name() - if icon_status == "connected": - self.connAct.setEnabled(True) - self.connAct.setText( - self.tr('Encryption ON turn o&ff')) - return - if icon_status == "disconnected": - self.connAct.setEnabled(True) - self.connAct.setText( - self.tr('Encryption OFF turn &on')) - return - if icon_status == "connecting": - self.connAct.setDisabled(True) - self.connAct.setText(self.tr('connecting...')) - return - - def detailsWin(self): - visible = self.isVisible() - if visible: - self.hide() - else: - self.show() - if sys.platform == "darwin": - self.raise_() - - def about(self): - # move to widget - flavor = BRANDING.get('short_name', None) - content = self.tr( - ("LEAP client
" - "(version %s)
" % VERSION)) - if flavor: - content = content + ('
Flavor: %s
' % flavor) - content = content + ( - "
" - "https://leap.se") - QtGui.QMessageBox.about(self, self.tr("About"), content) - - def setConnWidget(self, icon_name): - oldlayout = self.statusIconBox.layout() - - for i in range(3): - oldlayout.itemAt(i).widget().hide() - new = self.states[icon_name] - oldlayout.itemAt(new).widget().show() - - def setIcon(self, name): - icon_fun = self.Icons.get(name) - if icon_fun and callable(icon_fun): - icon = icon_fun(self) - self.trayIcon.setIcon(icon) - - def getIcon(self, icon_name): - return self.states.get(icon_name, None) - - def setIconToolTip(self): - """ - get readable status and place it on systray tooltip - """ - status = self.conductor.status.get_readable_status() - self.trayIcon.setToolTip(status) - - def iconActivated(self, reason): - """ - handles left click, left double click - showing the trayicon menu - """ - if reason in (QtGui.QSystemTrayIcon.Trigger, - QtGui.QSystemTrayIcon.DoubleClick): - context_menu = self.trayIcon.contextMenu() - # for some reason, context_menu.show() - # is failing in a way beyond my understanding. - # (not working the first time it's clicked). - # this works however. - # XXX in osx it shows some glitches. - context_menu.exec_(self.trayIcon.geometry().center()) - - @QtCore.pyqtSlot() - def onTimerTick(self): - self.statusUpdate() - - @QtCore.pyqtSlot(object) - def onOpenVPNStatusChange(self, status): - """ - updates icon, according to the openvpn status change. - """ - icon_name = self.conductor.get_icon_name() - if not icon_name: - return - - # XXX refactor. Use QStateMachine - - if icon_name in ("disconnected", "connected"): - self.eipStatusChange.emit(icon_name) - - if icon_name in ("connecting"): - # let's see how it matches - leap_status_name = self.conductor.get_leap_status() - self.eipStatusChange.emit(leap_status_name) - - if icon_name == "connected": - # When we change to "connected', we launch - # the network checker. - self.initNetworkChecker.emit() - - self.setIcon(icon_name) - # change connection pixmap widget - self.setConnWidget(icon_name) - - @QtCore.pyqtSlot(str) - def onEIPConnStatusChange(self, newstatus): - """ - slot for EIP status changes - not to be confused with onOpenVPNStatusChange. - this only updates the non-debug LEAP Status line - next to the connection icon. - """ - # XXX move bold to style sheet - self.leapConnStatus.setText( - "%s" % newstatus) diff --git a/src/leap/certs/__init__.py b/src/leap/certs/__init__.py deleted file mode 100644 index c4d009b1..00000000 --- a/src/leap/certs/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -_where = os.path.split(__file__)[0] - - -def where(filename): - return os.path.join(_where, filename) diff --git a/src/leap/crypto/__init__.py b/src/leap/crypto/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/crypto/certs.py b/src/leap/crypto/certs.py deleted file mode 100644 index cbb5725a..00000000 --- a/src/leap/crypto/certs.py +++ /dev/null @@ -1,112 +0,0 @@ -import logging -import os -from StringIO import StringIO -import ssl -import time - -from dateutil.parser import parse -from OpenSSL import crypto - -from leap.util.misc import null_check - -logger = logging.getLogger(__name__) - - -class BadCertError(Exception): - """ - raised for malformed certs - """ - - -class NoCertError(Exception): - """ - raised for cert not found in given path - """ - - -def get_https_cert_from_domain(domain, port=443): - """ - @param domain: a domain name to get a certificate from. - """ - cert = ssl.get_server_certificate((domain, port)) - x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert) - return x509 - - -def get_cert_from_file(_file): - null_check(_file, "pem file") - if isinstance(_file, (str, unicode)): - if not os.path.isfile(_file): - raise NoCertError - with open(_file) as f: - cert = f.read() - else: - cert = _file.read() - x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert) - return x509 - - -def get_pkey_from_file(_file): - getkey = lambda f: crypto.load_privatekey( - crypto.FILETYPE_PEM, f.read()) - - if isinstance(_file, str): - with open(_file) as f: - key = getkey(f) - else: - key = getkey(_file) - return key - - -def can_load_cert_and_pkey(string): - """ - loads certificate and private key from - a buffer - """ - try: - f = StringIO(string) - cert = get_cert_from_file(f) - - f = StringIO(string) - key = get_pkey_from_file(f) - - null_check(cert, 'certificate') - null_check(key, 'private key') - except Exception as exc: - logger.error(type(exc), exc.message) - raise BadCertError - else: - return True - - -def get_cert_fingerprint(domain=None, port=443, filepath=None, - hash_type="SHA256", sep=":"): - """ - @param domain: a domain name to get a fingerprint from - @type domain: str - @param filepath: path to a file containing a PEM file - @type filepath: str - @param hash_type: the hash function to be used in the fingerprint. - must be one of SHA1, SHA224, SHA256, SHA384, SHA512 - @type hash_type: str - @rparam: hex_fpr, a hexadecimal representation of a bytestring - containing the fingerprint. - @rtype: string - """ - if domain: - cert = get_https_cert_from_domain(domain, port=port) - if filepath: - cert = get_cert_from_file(filepath) - hex_fpr = cert.digest(hash_type) - return hex_fpr - - -def get_time_boundaries(certfile): - cert = get_cert_from_file(certfile) - null_check(cert, 'certificate') - - fromts, tots = (cert.get_notBefore(), cert.get_notAfter()) - from_, to_ = map( - lambda ts: time.gmtime(time.mktime(parse(ts).timetuple())), - (fromts, tots)) - return from_, to_ diff --git a/src/leap/crypto/certs_gnutls.py b/src/leap/crypto/certs_gnutls.py deleted file mode 100644 index 20c0e043..00000000 --- a/src/leap/crypto/certs_gnutls.py +++ /dev/null @@ -1,112 +0,0 @@ -''' -We're using PyOpenSSL now - -import ctypes -from StringIO import StringIO -import socket - -import gnutls.connection -import gnutls.crypto -import gnutls.library - -from leap.util.misc import null_check - - -class BadCertError(Exception): - """raised for malformed certs""" - - -def get_https_cert_from_domain(domain): - """ - @param domain: a domain name to get a certificate from. - """ - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - cred = gnutls.connection.X509Credentials() - - session = gnutls.connection.ClientSession(sock, cred) - session.connect((domain, 443)) - session.handshake() - cert = session.peer_certificate - return cert - - -def get_cert_from_file(_file): - getcert = lambda f: gnutls.crypto.X509Certificate(f.read()) - if isinstance(_file, str): - with open(_file) as f: - cert = getcert(f) - else: - cert = getcert(_file) - return cert - - -def get_pkey_from_file(_file): - getkey = lambda f: gnutls.crypto.X509PrivateKey(f.read()) - if isinstance(_file, str): - with open(_file) as f: - key = getkey(f) - else: - key = getkey(_file) - return key - - -def can_load_cert_and_pkey(string): - try: - f = StringIO(string) - cert = get_cert_from_file(f) - - f = StringIO(string) - key = get_pkey_from_file(f) - - null_check(cert, 'certificate') - null_check(key, 'private key') - except: - # XXX catch GNUTLSError? - raise BadCertError - else: - return True - -def get_cert_fingerprint(domain=None, filepath=None, - hash_type="SHA256", sep=":"): - """ - @param domain: a domain name to get a fingerprint from - @type domain: str - @param filepath: path to a file containing a PEM file - @type filepath: str - @param hash_type: the hash function to be used in the fingerprint. - must be one of SHA1, SHA224, SHA256, SHA384, SHA512 - @type hash_type: str - @rparam: hex_fpr, a hexadecimal representation of a bytestring - containing the fingerprint. - @rtype: string - """ - if domain: - cert = get_https_cert_from_domain(domain) - if filepath: - cert = get_cert_from_file(filepath) - - _buffer = ctypes.create_string_buffer(64) - buffer_length = ctypes.c_size_t(64) - - SUPPORTED_DIGEST_FUN = ("SHA1", "SHA224", "SHA256", "SHA384", "SHA512") - if hash_type in SUPPORTED_DIGEST_FUN: - digestfunction = getattr( - gnutls.library.constants, - "GNUTLS_DIG_%s" % hash_type) - else: - # XXX improperlyconfigured or something - raise Exception("digest function not supported") - - gnutls.library.functions.gnutls_x509_crt_get_fingerprint( - cert._c_object, digestfunction, - ctypes.byref(_buffer), ctypes.byref(buffer_length)) - - # deinit - #server_cert._X509Certificate__deinit(server_cert._c_object) - # needed? is segfaulting - - fpr = ctypes.string_at(_buffer, buffer_length.value) - hex_fpr = sep.join(u"%02X" % ord(char) for char in fpr) - - return hex_fpr -''' diff --git a/src/leap/crypto/leapkeyring.py b/src/leap/crypto/leapkeyring.py deleted file mode 100644 index c241d0bc..00000000 --- a/src/leap/crypto/leapkeyring.py +++ /dev/null @@ -1,70 +0,0 @@ -import keyring - -from leap.base.config import get_config_file - -############# -# Disclaimer -############# -# This currently is not a keyring, it's more like a joke. -# No, seriously. -# We're affected by this **bug** - -# https://bitbucket.org/kang/python-keyring-lib/ -# issue/65/dbusexception-method-opensession-with - -# so using the gnome keyring does not seem feasible right now. -# I thought this was the next best option to store secrets in plain sight. - -# in the future we should move to use the gnome/kde/macosx/win keyrings. - - -class LeapCryptedFileKeyring(keyring.backend.CryptedFileKeyring): - - filename = ".secrets" - - @property - def file_path(self): - return get_config_file(self.filename) - - def __init__(self, seed=None): - self.seed = seed - - def _get_new_password(self): - # XXX every time this method is called, - # $deity kills a kitten. - return "secret%s" % self.seed - - def _init_file(self): - self.keyring_key = self._get_new_password() - self.set_password('keyring_setting', 'pass_ref', 'pass_ref_value') - - def _unlock(self): - self.keyring_key = self._get_new_password() - print 'keyring key ', self.keyring_key - try: - ref_pw = self.get_password( - 'keyring_setting', - 'pass_ref') - print 'ref pw ', ref_pw - assert ref_pw == "pass_ref_value" - except AssertionError: - self._lock() - raise ValueError('Incorrect password') - - -def leap_set_password(key, value, seed="xxx"): - key, value = map(unicode, (key, value)) - keyring.set_keyring(LeapCryptedFileKeyring(seed=seed)) - keyring.set_password('leap', key, value) - - -def leap_get_password(key, seed="xxx"): - keyring.set_keyring(LeapCryptedFileKeyring(seed=seed)) - #import ipdb;ipdb.set_trace() - return keyring.get_password('leap', key) - - -if __name__ == "__main__": - leap_set_password('test', 'bar') - passwd = leap_get_password('test') - assert passwd == 'bar' diff --git a/src/leap/crypto/tests/__init__.py b/src/leap/crypto/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/crypto/tests/test_certs.py b/src/leap/crypto/tests/test_certs.py deleted file mode 100644 index e476b630..00000000 --- a/src/leap/crypto/tests/test_certs.py +++ /dev/null @@ -1,22 +0,0 @@ -import unittest - -from leap.testing.https_server import where -from leap.crypto import certs - - -class CertTestCase(unittest.TestCase): - - def test_can_load_client_and_pkey(self): - with open(where('leaptestscert.pem')) as cf: - cs = cf.read() - with open(where('leaptestskey.pem')) as kf: - ks = kf.read() - certs.can_load_cert_and_pkey(cs + ks) - - with self.assertRaises(certs.BadCertError): - # screw header - certs.can_load_cert_and_pkey(cs.replace("BEGIN", "BEGINN") + ks) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/eip/__init__.py b/src/leap/eip/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/eip/checks.py b/src/leap/eip/checks.py deleted file mode 100644 index 9a34a428..00000000 --- a/src/leap/eip/checks.py +++ /dev/null @@ -1,537 +0,0 @@ -import logging -import time -import os -import sys - -import requests - -from leap import __branding as BRANDING -from leap import certs as leapcerts -from leap.base.auth import srpauth_protected, magick_srpauth -from leap.base import config as baseconfig -from leap.base import constants as baseconstants -from leap.base import providers -from leap.crypto import certs -from leap.eip import config as eipconfig -from leap.eip import constants as eipconstants -from leap.eip import exceptions as eipexceptions -from leap.eip import specs as eipspecs -from leap.util.certs import get_mac_cabundle -from leap.util.fileutil import mkdir_p -from leap.util.web import get_https_domain_and_port - -logger = logging.getLogger(name=__name__) - -""" -ProviderCertChecker -------------------- -Checks on certificates. To be moved to base. -docs TBD - -EIPConfigChecker ----------- -It is used from the eip conductor (a instance of EIPConnection that is -managed from the QtApp), running `run_all` method before trying to call -`connect` or any other of the state-changing methods. - -It checks that the needed files are provided or can be discovered over the -net. Much of these tests are not specific to EIP module, and can be splitted -into base.tests to be invoked by the base leap init routines. -However, I'm testing them alltogether for the sake of having the whole unit -reachable and testable as a whole. - -""" - - -def get_branding_ca_cert(domain): - # deprecated - ca_file = BRANDING.get('provider_ca_file') - if ca_file: - return leapcerts.where(ca_file) - - -class ProviderCertChecker(object): - """ - Several checks needed for getting - client certs and checking tls connection - with provider. - """ - def __init__(self, fetcher=requests, - domain=None): - - self.fetcher = fetcher - self.domain = domain - #XXX needs some kind of autoinit - #right now we set by hand - #by loading and reading provider config - self.apidomain = None - self.cacert = eipspecs.provider_ca_path(domain) - - def run_all( - self, checker=None, - skip_download=False, skip_verify=False): - - if not checker: - checker = self - - do_verify = not skip_verify - logger.debug('do_verify: %s', do_verify) - # checker.download_ca_cert() - - # For MVS+ - # checker.download_ca_signature() - # checker.get_ca_signatures() - # checker.is_there_trust_path() - - # For MVS - checker.is_there_provider_ca() - - checker.is_https_working(verify=do_verify, autocacert=False) - checker.check_new_cert_needed(verify=do_verify) - - def download_ca_cert(self, uri=None, verify=True): - req = self.fetcher.get(uri, verify=verify) - req.raise_for_status() - - # should check domain exists - capath = self._get_ca_cert_path(self.domain) - with open(capath, 'w') as f: - f.write(req.content) - - def check_ca_cert_fingerprint( - self, hash_type="SHA256", - fingerprint=None): - """ - compares the fingerprint in - the ca cert with a string - we are passed - returns True if they are equal, False if not. - @param hash_type: digest function - @type hash_type: str - @param fingerprint: the fingerprint to compare with. - @type fingerprint: str (with : separator) - @rtype bool - """ - ca_cert_path = self.ca_cert_path - ca_cert_fpr = certs.get_cert_fingerprint( - filepath=ca_cert_path) - return ca_cert_fpr == fingerprint - - def verify_api_https(self, uri): - assert uri.startswith('https://') - cacert = self.ca_cert_path - verify = cacert and cacert or True - req = self.fetcher.get(uri, verify=verify) - req.raise_for_status() - return True - - def download_ca_signature(self): - # MVS+ - raise NotImplementedError - - def get_ca_signatures(self): - # MVS+ - raise NotImplementedError - - def is_there_trust_path(self): - # MVS+ - raise NotImplementedError - - def is_there_provider_ca(self): - if not self.cacert: - return False - cacert_exists = os.path.isfile(self.cacert) - if cacert_exists: - logger.debug('True') - return True - logger.debug('False!') - return False - - def is_https_working( - self, uri=None, verify=True, - autocacert=False): - if uri is None: - uri = self._get_root_uri() - # XXX raise InsecureURI or something better - try: - assert uri.startswith('https') - except AssertionError: - raise AssertionError( - "uri passed should start with https") - if autocacert and verify is True and self.cacert is not None: - logger.debug('verify cert: %s', self.cacert) - verify = self.cacert - if sys.platform == "darwin": - verify = get_mac_cabundle() - logger.debug('checking https connection') - logger.debug('uri: %s (verify:%s)', uri, verify) - - try: - self.fetcher.get(uri, verify=verify) - - except requests.exceptions.SSLError as exc: - raise eipexceptions.HttpsBadCertError - - except requests.exceptions.ConnectionError: - logger.error('ConnectionError') - raise eipexceptions.HttpsNotSupported - - else: - return True - - def check_new_cert_needed(self, skip_download=False, verify=True): - # XXX add autocacert - if not self.is_cert_valid(do_raise=False): - logger.debug('cert needed: true') - self.download_new_client_cert( - skip_download=skip_download, - verify=verify) - return True - logger.debug('cert needed: false') - return False - - def download_new_client_cert(self, uri=None, verify=True, - skip_download=False, - credentials=None): - logger.debug('download new client cert') - if skip_download: - return True - if uri is None: - uri = self._get_client_cert_uri() - # XXX raise InsecureURI or something better - #assert uri.startswith('https') - - if verify is True and self.cacert is not None: - verify = self.cacert - logger.debug('verify = %s', verify) - - fgetfn = self.fetcher.get - - if credentials: - user, passwd = credentials - logger.debug('apidomain = %s', self.apidomain) - - @srpauth_protected(user, passwd, - server="https://%s" % self.apidomain, - verify=verify) - def getfn(*args, **kwargs): - return fgetfn(*args, **kwargs) - - else: - # XXX FIXME fix decorated args - @magick_srpauth(verify) - def getfn(*args, **kwargs): - return fgetfn(*args, **kwargs) - try: - - req = getfn(uri, verify=verify) - req.raise_for_status() - - except requests.exceptions.SSLError: - logger.warning('SSLError while fetching cert. ' - 'Look below for stack trace.') - # XXX raise better exception - return self.fail("SSLError") - except Exception as exc: - return self.fail(exc.message) - - try: - logger.debug('validating cert...') - pemfile_content = req.content - valid = self.is_valid_pemfile(pemfile_content) - if not valid: - logger.warning('invalid cert') - return False - cert_path = self._get_client_cert_path() - self.write_cert(pemfile_content, to=cert_path) - except: - logger.warning('Error while validating cert') - raise - return True - - def is_cert_valid(self, cert_path=None, do_raise=True): - exists = lambda: self.is_certificate_exists() - valid_pemfile = lambda: self.is_valid_pemfile() - not_expired = lambda: self.is_cert_not_expired() - - valid = exists() and valid_pemfile() and not_expired() - if not valid: - if do_raise: - raise Exception('missing valid cert') - else: - return False - return True - - def is_certificate_exists(self, certfile=None): - if certfile is None: - certfile = self._get_client_cert_path() - return os.path.isfile(certfile) - - def is_cert_not_expired(self, certfile=None, now=time.gmtime): - if certfile is None: - certfile = self._get_client_cert_path() - from_, to_ = certs.get_time_boundaries(certfile) - - return from_ < now() < to_ - - def is_valid_pemfile(self, cert_s=None): - """ - checks that the passed string - is a valid pem certificate - @param cert_s: string containing pem content - @type cert_s: string - @rtype: bool - """ - if cert_s is None: - certfile = self._get_client_cert_path() - with open(certfile) as cf: - cert_s = cf.read() - try: - valid = certs.can_load_cert_and_pkey(cert_s) - except certs.BadCertError: - logger.warning("Not valid pemfile") - valid = False - return valid - - @property - def ca_cert_path(self): - return self._get_ca_cert_path(self.domain) - - def _get_root_uri(self): - return u"https://%s/" % self.domain - - def _get_client_cert_uri(self): - return "https://%s/1/cert" % self.apidomain - - def _get_client_cert_path(self): - return eipspecs.client_cert_path(domain=self.domain) - - def _get_ca_cert_path(self, domain): - # XXX this folder path will be broken for win - # and this should be moved to eipspecs.ca_path - - # XXX use baseconfig.get_provider_path(folder=Foo) - # !!! - - capath = baseconfig.get_config_file( - 'cacert.pem', - folder='providers/%s/keys/ca' % domain) - folder, fname = os.path.split(capath) - if not os.path.isdir(folder): - mkdir_p(folder) - return capath - - def write_cert(self, pemfile_content, to=None): - folder, filename = os.path.split(to) - if not os.path.isdir(folder): - mkdir_p(folder) - with open(to, 'w') as cert_f: - cert_f.write(pemfile_content) - - def set_api_domain(self, domain): - self.apidomain = domain - - -class EIPConfigChecker(object): - """ - Several checks needed - to ensure a EIPConnection - can be sucessfully established. - use run_all to run all checks. - """ - - def __init__(self, fetcher=requests, domain=None): - # we do not want to accept too many - # argument on init. - # we want tests - # to be explicitely run. - - self.fetcher = fetcher - - # if not domain, get from config - self.domain = domain - self.apidomain = None - self.cacert = eipspecs.provider_ca_path(domain) - - self.defaultprovider = providers.LeapProviderDefinition(domain=domain) - self.defaultprovider.load() - self.eipconfig = eipconfig.EIPConfig(domain=domain) - self.set_api_domain() - self.eipserviceconfig = eipconfig.EIPServiceConfig(domain=domain) - self.eipserviceconfig.load() - - def run_all(self, checker=None, skip_download=False): - """ - runs all checks in a row. - will raise if some error encountered. - catching those exceptions is not - our responsibility at this moment - """ - if not checker: - checker = self - - # let's call all tests - # needed for a sane eip session. - - # TODO: get rid of check_default. - # check_complete should - # be enough. but here to make early tests easier. - checker.check_default_eipconfig() - - checker.check_is_there_default_provider() - checker.fetch_definition(skip_download=skip_download) - checker.fetch_eip_service_config(skip_download=skip_download) - checker.check_complete_eip_config() - #checker.ping_gateway() - - # public checks - - def check_default_eipconfig(self): - """ - checks if default eipconfig exists, - and dumps a default file if not - """ - # XXX ONLY a transient check - # because some old function still checks - # for eip config at the beginning. - - # it *really* does not make sense to - # dump it right now, we can get an in-memory - # config object and dump it to disk in a - # later moment - logger.debug('checking default eip config') - if not self._is_there_default_eipconfig(): - self._dump_default_eipconfig() - - def check_is_there_default_provider(self, config=None): - """ - raises EIPMissingDefaultProvider if no - default provider found on eip config. - This is catched by ui and runs FirstRunWizard (MVS+) - """ - if config is None: - config = self.eipconfig.config - logger.debug('checking default provider') - provider = config.get('provider', None) - if provider is None: - raise eipexceptions.EIPMissingDefaultProvider - # XXX raise also if malformed ProviderDefinition? - return True - - def fetch_definition(self, skip_download=False, - force_download=False, - config=None, uri=None, - domain=None): - """ - fetches a definition file from server - """ - # TODO: - # - Implement diff - # - overwrite only if different. - # (attend to serial field different, for instance) - - logger.debug('fetching definition') - - if skip_download: - logger.debug('(fetching def skipped)') - return True - if config is None: - config = self.defaultprovider.config - if uri is None: - if not domain: - domain = config.get('provider', None) - uri = self._get_provider_definition_uri(domain=domain) - - if sys.platform == "darwin": - verify = get_mac_cabundle() - else: - verify = True - - self.defaultprovider.load( - from_uri=uri, - fetcher=self.fetcher, - verify=verify) - self.defaultprovider.save() - - def fetch_eip_service_config(self, skip_download=False, - force_download=False, - config=None, uri=None, # domain=None, - autocacert=True, verify=True): - if skip_download: - return True - if config is None: - self.eipserviceconfig.load() - config = self.eipserviceconfig.config - if uri is None: - #XXX - #if not domain: - #domain = self.domain or config.get('provider', None) - uri = self._get_eip_service_uri( - domain=self.apidomain) - - if autocacert and self.cacert is not None: - verify = self.cacert - - self.eipserviceconfig.load( - from_uri=uri, - fetcher=self.fetcher, - force_download=force_download, - verify=verify) - self.eipserviceconfig.save() - - def check_complete_eip_config(self, config=None): - # TODO check for gateway - if config is None: - config = self.eipconfig.config - try: - assert 'provider' in config - assert config['provider'] is not None - # XXX assert there is gateway !! - except AssertionError: - raise eipexceptions.EIPConfigurationError - - # XXX TODO: - # We should WRITE eip config if missing or - # incomplete at this point - #self.eipconfig.save() - - # - # private helpers - # - - def _is_there_default_eipconfig(self): - return self.eipconfig.exists() - - def _dump_default_eipconfig(self): - self.eipconfig.save(force=True) - - def _get_provider_definition_uri(self, domain=None, path=None): - if domain is None: - domain = self.domain or baseconstants.DEFAULT_PROVIDER - if path is None: - path = baseconstants.DEFINITION_EXPECTED_PATH - uri = u"https://%s/%s" % (domain, path) - logger.debug('getting provider definition from %s' % uri) - return uri - - def _get_eip_service_uri(self, domain=None, path=None): - if domain is None: - domain = self.domain or baseconstants.DEFAULT_PROVIDER - if path is None: - path = eipconstants.EIP_SERVICE_EXPECTED_PATH - uri = "https://%s/%s" % (domain, path) - logger.debug('getting eip service file from %s', uri) - return uri - - def set_api_domain(self): - """sets api domain from defaultprovider config object""" - api = self.defaultprovider.config.get('api_uri', None) - # the caller is responsible for having loaded the config - # object at this point - if api: - api_dom = get_https_domain_and_port(api) - self.apidomain = "%s:%s" % api_dom - - def get_api_domain(self): - """gets api domain""" - return self.apidomain diff --git a/src/leap/eip/config.py b/src/leap/eip/config.py deleted file mode 100644 index 917871da..00000000 --- a/src/leap/eip/config.py +++ /dev/null @@ -1,398 +0,0 @@ -import logging -import os -import platform -import re -import tempfile - -from leap import __branding as BRANDING -from leap import certs -from leap.util.misc import null_check -from leap.util.fileutil import (which, mkdir_p, check_and_fix_urw_only) - -from leap.base import config as baseconfig -from leap.baseapp.permcheck import (is_pkexec_in_system, - is_auth_agent_running) -from leap.eip import exceptions as eip_exceptions -from leap.eip import specs as eipspecs - -logger = logging.getLogger(name=__name__) -provider_ca_file = BRANDING.get('provider_ca_file', None) - -_platform = platform.system() - - -class EIPConfig(baseconfig.JSONLeapConfig): - spec = eipspecs.eipconfig_spec - - def _get_slug(self): - eipjsonpath = baseconfig.get_config_file( - 'eip.json') - return eipjsonpath - - def _set_slug(self, *args, **kwargs): - raise AttributeError("you cannot set slug") - - slug = property(_get_slug, _set_slug) - - -class EIPServiceConfig(baseconfig.JSONLeapConfig): - spec = eipspecs.eipservice_config_spec - - def _get_slug(self): - domain = getattr(self, 'domain', None) - if domain: - path = baseconfig.get_provider_path(domain) - else: - path = baseconfig.get_default_provider_path() - return baseconfig.get_config_file( - 'eip-service.json', folder=path) - - def _set_slug(self): - raise AttributeError("you cannot set slug") - - slug = property(_get_slug, _set_slug) - - -def get_socket_path(): - socket_path = os.path.join( - tempfile.mkdtemp(prefix="leap-tmp"), - 'openvpn.socket') - #logger.debug('socket path: %s', socket_path) - return socket_path - - -def get_eip_gateway(eipconfig=None, eipserviceconfig=None): - """ - return the first host in eip service config - that matches the name defined in the eip.json config - file. - """ - # XXX eventually we should move to a more clever - # gateway selection. maybe we could return - # all gateways that match our cluster. - - null_check(eipconfig, "eipconfig") - null_check(eipserviceconfig, "eipserviceconfig") - PLACEHOLDER = "testprovider.example.org" - - conf = eipconfig.config - eipsconf = eipserviceconfig.config - - primary_gateway = conf.get('primary_gateway', None) - if not primary_gateway: - return PLACEHOLDER - - gateways = eipsconf.get('gateways', None) - if not gateways: - logger.error('missing gateways in eip service config') - return PLACEHOLDER - - if len(gateways) > 0: - for gw in gateways: - clustername = gw.get('cluster', None) - if not clustername: - logger.error('no cluster name') - return - - if clustername == primary_gateway: - # XXX at some moment, we must - # make this a more generic function, - # and return ports, protocols... - ipaddress = gw.get('ip_address', None) - if not ipaddress: - logger.error('no ip_address') - return - return ipaddress - logger.error('could not find primary gateway in provider' - 'gateway list') - - -def get_cipher_options(eipserviceconfig=None): - """ - gathers optional cipher options from eip-service config. - :param eipserviceconfig: EIPServiceConfig instance - """ - null_check(eipserviceconfig, 'eipserviceconfig') - eipsconf = eipserviceconfig.get_config() - - ALLOWED_KEYS = ("auth", "cipher", "tls-cipher") - CIPHERS_REGEX = re.compile("[A-Z0-9\-]+") - opts = [] - if 'openvpn_configuration' in eipsconf: - config = eipserviceconfig.config.get( - "openvpn_configuration", {}) - for key, value in config.items(): - if key in ALLOWED_KEYS and value is not None: - sanitized_val = CIPHERS_REGEX.findall(value) - if len(sanitized_val) != 0: - _val = sanitized_val[0] - opts.append('--%s' % key) - opts.append('%s' % _val) - return opts - -LINUX_UP_DOWN_SCRIPT = "/etc/leap/resolv-update" -OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" - - -def has_updown_scripts(): - """ - checks the existence of the up/down scripts - """ - # XXX should check permissions too - is_file = os.path.isfile(LINUX_UP_DOWN_SCRIPT) - if not is_file: - logger.warning( - "Could not find up/down scripts at %s! " - "Risk of DNS Leaks!!!") - return is_file - - -def build_ovpn_options(daemon=False, socket_path=None, **kwargs): - """ - build a list of options - to be passed in the - openvpn invocation - @rtype: list - @rparam: options - """ - # XXX review which of the - # options we don't need. - - # TODO pass also the config file, - # since we will need to take some - # things from there if present. - - provider = kwargs.pop('provider', None) - eipconfig = EIPConfig(domain=provider) - eipconfig.load() - eipserviceconfig = EIPServiceConfig(domain=provider) - eipserviceconfig.load() - - # get user/group name - # also from config. - user = baseconfig.get_username() - group = baseconfig.get_groupname() - - opts = [] - - opts.append('--client') - - opts.append('--dev') - # XXX same in win? - opts.append('tun') - opts.append('--persist-tun') - opts.append('--persist-key') - - verbosity = kwargs.get('ovpn_verbosity', None) - if verbosity and 1 <= verbosity <= 6: - opts.append('--verb') - opts.append("%s" % verbosity) - - # remote ############################## - # (server, port, protocol) - - opts.append('--remote') - - gw = get_eip_gateway(eipconfig=eipconfig, - eipserviceconfig=eipserviceconfig) - logger.debug('setting eip gateway to %s', gw) - opts.append(str(gw)) - - # get port/protocol from eipservice too - opts.append('1194') - #opts.append('80') - opts.append('udp') - - opts.append('--tls-client') - opts.append('--remote-cert-tls') - opts.append('server') - - # get ciphers ####################### - - ciphers = get_cipher_options( - eipserviceconfig=eipserviceconfig) - for cipheropt in ciphers: - opts.append(str(cipheropt)) - - # set user and group - opts.append('--user') - opts.append('%s' % user) - opts.append('--group') - opts.append('%s' % group) - - opts.append('--management-client-user') - opts.append('%s' % user) - opts.append('--management-signal') - - # set default options for management - # interface. unix sockets or telnet interface for win. - # XXX take them from the config object. - - if _platform == "Windows": - opts.append('--management') - opts.append('localhost') - # XXX which is a good choice? - opts.append('7777') - - if _platform in ("Linux", "Darwin"): - opts.append('--management') - - if socket_path is None: - socket_path = get_socket_path() - opts.append(socket_path) - opts.append('unix') - - opts.append('--script-security') - opts.append('2') - - if _platform == "Linux": - if has_updown_scripts(): - opts.append("--up") - opts.append(LINUX_UP_DOWN_SCRIPT) - opts.append("--down") - opts.append(LINUX_UP_DOWN_SCRIPT) - opts.append("--plugin") - opts.append(OPENVPN_DOWN_ROOT) - opts.append("'script_type=down %s'" % LINUX_UP_DOWN_SCRIPT) - - # certs - client_cert_path = eipspecs.client_cert_path(provider) - ca_cert_path = eipspecs.provider_ca_path(provider) - - # XXX FIX paths for MAC - opts.append('--cert') - opts.append(client_cert_path) - opts.append('--key') - opts.append(client_cert_path) - opts.append('--ca') - opts.append(ca_cert_path) - - # we cannot run in daemon mode - # with the current subp setting. - # see: https://leap.se/code/issues/383 - #if daemon is True: - #opts.append('--daemon') - - logger.debug('vpn options: %s', ' '.join(opts)) - return opts - - -def build_ovpn_command(debug=False, do_pkexec_check=True, vpnbin=None, - socket_path=None, **kwargs): - """ - build a string with the - complete openvpn invocation - - @rtype [string, [list of strings]] - @rparam: a list containing the command string - and a list of options. - """ - command = [] - use_pkexec = True - ovpn = None - - # XXX get use_pkexec from config instead. - - if _platform == "Linux" and use_pkexec and do_pkexec_check: - - # check for both pkexec - # AND a suitable authentication - # agent running. - logger.info('use_pkexec set to True') - - if not is_pkexec_in_system(): - logger.error('no pkexec in system') - raise eip_exceptions.EIPNoPkexecAvailable - - if not is_auth_agent_running(): - logger.warning( - "no polkit auth agent found. " - "pkexec will use its own text " - "based authentication agent. " - "that's probably a bad idea") - raise eip_exceptions.EIPNoPolkitAuthAgentAvailable - - command.append('pkexec') - - if vpnbin is None: - if _platform == "Darwin": - # XXX Should hardcode our installed path - # /Applications/LEAPClient.app/Contents/Resources/openvpn.leap - openvpn_bin = "openvpn.leap" - else: - openvpn_bin = "openvpn" - #XXX hardcode for darwin - ovpn = which(openvpn_bin) - else: - ovpn = vpnbin - if ovpn: - vpn_command = ovpn - else: - vpn_command = "openvpn" - command.append(vpn_command) - daemon_mode = not debug - - for opt in build_ovpn_options(daemon=daemon_mode, socket_path=socket_path, - **kwargs): - command.append(opt) - - # XXX check len and raise proper error - - if _platform == "Darwin": - OSX_ASADMIN = 'do shell script "%s" with administrator privileges' - # XXX fix workaround for Nones - _command = [x if x else " " for x in command] - # XXX debugging! - # XXX get openvpn log path from debug flags - _command.append('--log') - _command.append('/tmp/leap_openvpn.log') - return ["osascript", ["-e", OSX_ASADMIN % ' '.join(_command)]] - else: - return [command[0], command[1:]] - - -def check_vpn_keys(provider=None): - """ - performs an existance and permission check - over the openvpn keys file. - Currently we're expecting a single file - per provider, containing the CA cert, - the provider key, and our client certificate - """ - assert provider is not None - provider_ca = eipspecs.provider_ca_path(provider) - client_cert = eipspecs.client_cert_path(provider) - - logger.debug('provider ca = %s', provider_ca) - logger.debug('client cert = %s', client_cert) - - # if no keys, raise error. - # it's catched by the ui and signal user. - - if not os.path.isfile(provider_ca): - # not there. let's try to copy. - folder, filename = os.path.split(provider_ca) - if not os.path.isdir(folder): - mkdir_p(folder) - if provider_ca_file: - cacert = certs.where(provider_ca_file) - with open(provider_ca, 'w') as pca: - with open(cacert, 'r') as cac: - pca.write(cac.read()) - - if not os.path.isfile(provider_ca): - logger.error('key file %s not found. aborting.', - provider_ca) - raise eip_exceptions.EIPInitNoKeyFileError - - if not os.path.isfile(client_cert): - logger.error('key file %s not found. aborting.', - client_cert) - raise eip_exceptions.EIPInitNoKeyFileError - - for keyfile in (provider_ca, client_cert): - # bad perms? try to fix them - try: - check_and_fix_urw_only(keyfile) - except OSError: - raise eip_exceptions.EIPInitBadKeyFilePermError diff --git a/src/leap/eip/constants.py b/src/leap/eip/constants.py deleted file mode 100644 index 9af5a947..00000000 --- a/src/leap/eip/constants.py +++ /dev/null @@ -1,3 +0,0 @@ -# not used anymore with the new JSONConfig.slug -EIP_CONFIG = "eip.json" -EIP_SERVICE_EXPECTED_PATH = "1/config/eip-service.json" diff --git a/src/leap/eip/eipconnection.py b/src/leap/eip/eipconnection.py deleted file mode 100644 index d012c567..00000000 --- a/src/leap/eip/eipconnection.py +++ /dev/null @@ -1,405 +0,0 @@ -""" -EIP Connection Class -""" -from __future__ import (absolute_import,) -import logging -import Queue -import sys -import time - -from dateutil.parser import parse as dateparse - -from leap.eip.checks import ProviderCertChecker -from leap.eip.checks import EIPConfigChecker -from leap.eip import config as eipconfig -from leap.eip import exceptions as eip_exceptions -from leap.eip.openvpnconnection import OpenVPNConnection - -logger = logging.getLogger(name=__name__) - - -class StatusMixIn(object): - - # a bunch of methods related with querying the connection - # state/status and displaying useful info. - # Needs to get clear on what is what, and - # separate functions. - # Should separate EIPConnectionStatus (self.status) - # from the OpenVPN state/status command and parsing. - - ERR_CONNREFUSED = False - - def connection_state(self): - """ - returns the current connection state - """ - return self.status.current - - def get_icon_name(self): - """ - get icon name from status object - """ - return self.status.get_state_icon() - - def get_leap_status(self): - return self.status.get_leap_status() - - def poll_connection_state(self): - """ - """ - try: - state = self.get_connection_state() - except eip_exceptions.ConnectionRefusedError: - # connection refused. might be not ready yet. - if not self.ERR_CONNREFUSED: - logger.warning('connection refused') - self.ERR_CONNREFUSED = True - return - if not state: - #logger.debug('no state') - return - (ts, status_step, - ok, ip, remote) = state - self.status.set_vpn_state(status_step) - status_step = self.status.get_readable_status() - return (ts, status_step, ok, ip, remote) - - def make_error(self): - """ - capture error and wrap it in an - understandable format - """ - # mostly a hack to display errors in the debug UI - # w/o breaking the polling. - #XXX get helpful error codes - self.with_errors = True - now = int(time.time()) - return '%s,LAUNCHER ERROR,ERROR,-,-' % now - - def state(self): - """ - Sends OpenVPN command: state - """ - state = self._send_command("state") - if not state: - return None - if isinstance(state, str): - return state - if isinstance(state, list): - if len(state) == 1: - return state[0] - else: - return state[-1] - - def vpn_status(self): - """ - OpenVPN command: status - """ - status = self._send_command("status") - return status - - def vpn_status2(self): - """ - OpenVPN command: last 2 statuses - """ - return self._send_command("status 2") - - # - # parse info as the UI expects - # - - def get_status_io(self): - status = self.vpn_status() - if isinstance(status, str): - lines = status.split('\n') - if isinstance(status, list): - lines = status - try: - (header, when, tun_read, tun_write, - tcp_read, tcp_write, auth_read) = tuple(lines) - except ValueError: - return None - - when_ts = dateparse(when.split(',')[1]).timetuple() - sep = ',' - # XXX clean up this! - tun_read = tun_read.split(sep)[1] - tun_write = tun_write.split(sep)[1] - tcp_read = tcp_read.split(sep)[1] - tcp_write = tcp_write.split(sep)[1] - auth_read = auth_read.split(sep)[1] - - # XXX this could be a named tuple. prettier. - return when_ts, (tun_read, tun_write, tcp_read, tcp_write, auth_read) - - def get_connection_state(self): - state = self.state() - if state is not None: - ts, status_step, ok, ip, remote = state.split(',') - ts = time.gmtime(float(ts)) - # XXX this could be a named tuple. prettier. - return ts, status_step, ok, ip, remote - - -class EIPConnection(OpenVPNConnection, StatusMixIn): - """ - Aka conductor. - Manages the execution of the OpenVPN process, auto starts, monitors the - network connection, handles configuration, fixes leaky hosts, handles - errors, etc. - Status updates (connected, bandwidth, etc) are signaled to the GUI. - """ - - # XXX change name to EIPConductor ?? - - def __init__(self, - provider_cert_checker=ProviderCertChecker, - config_checker=EIPConfigChecker, - *args, **kwargs): - #self.settingsfile = kwargs.get('settingsfile', None) - #self.logfile = kwargs.get('logfile', None) - self.provider = kwargs.pop('provider', None) - self._providercertchecker = provider_cert_checker - self._configchecker = config_checker - - self.error_queue = Queue.Queue() - - status_signals = kwargs.pop('status_signals', None) - self.status = EIPConnectionStatus(callbacks=status_signals) - - checker_signals = kwargs.pop('checker_signals', None) - self.checker_signals = checker_signals - - self.init_checkers() - - host = eipconfig.get_socket_path() - kwargs['host'] = host - - super(EIPConnection, self).__init__(*args, **kwargs) - - def connect(self, **kwargs): - """ - entry point for connection process - """ - # in OpenVPNConnection - self.try_openvpn_connection() - - def disconnect(self, shutdown=False): - """ - disconnects client - """ - self.terminate_openvpn_connection(shutdown=shutdown) - self.status.change_to(self.status.DISCONNECTED) - - def has_errors(self): - return True if self.error_queue.qsize() != 0 else False - - def init_checkers(self): - """ - initialize checkers - """ - self.provider_cert_checker = self._providercertchecker( - domain=self.provider) - self.config_checker = self._configchecker(domain=self.provider) - - def set_provider_domain(self, domain): - """ - sets the provider domain. - used from the first run wizard when we launch the run_checks - and connect process after having initialized the conductor. - """ - # This looks convoluted, right. - # We have to reinstantiate checkers cause we're passing - # the domain param that we did not know at the beginning - # (only for the firstrunwizard case) - self.provider = domain - self.init_checkers() - - def run_checks(self, skip_download=False, skip_verify=False): - """ - run all eip checks previous to attempting a connection - """ - logger.debug('running conductor checks') - - def push_err(exc): - # keep the original traceback! - exc_traceback = sys.exc_info()[2] - self.error_queue.put((exc, exc_traceback)) - - try: - # network (1) - if self.checker_signals: - for signal in self.checker_signals: - signal('checking encryption keys') - self.provider_cert_checker.run_all(skip_verify=skip_verify) - except Exception as exc: - push_err(exc) - try: - if self.checker_signals: - for signal in self.checker_signals: - signal('checking provider config') - self.config_checker.run_all(skip_download=skip_download) - except Exception as exc: - push_err(exc) - try: - self.run_openvpn_checks() - except Exception as exc: - push_err(exc) - - -class EIPConnectionStatus(object): - """ - Keep track of client (gui) and openvpn - states. - - These are the OpenVPN states: - CONNECTING -- OpenVPN's initial state. - WAIT -- (Client only) Waiting for initial response - from server. - AUTH -- (Client only) Authenticating with server. - GET_CONFIG -- (Client only) Downloading configuration options - from server. - ASSIGN_IP -- Assigning IP address to virtual network - interface. - ADD_ROUTES -- Adding routes to system. - CONNECTED -- Initialization Sequence Completed. - RECONNECTING -- A restart has occurred. - EXITING -- A graceful exit is in progress. - - We add some extra states: - - DISCONNECTED -- GUI initial state. - UNRECOVERABLE -- An unrecoverable error has been raised - while invoking openvpn service. - """ - CONNECTING = 1 - WAIT = 2 - AUTH = 3 - GET_CONFIG = 4 - ASSIGN_IP = 5 - ADD_ROUTES = 6 - CONNECTED = 7 - RECONNECTING = 8 - EXITING = 9 - - # gui specific states: - UNRECOVERABLE = 11 - DISCONNECTED = 0 - - def __init__(self, callbacks=None): - """ - EIPConnectionStatus is initialized with a tuple - of signals to be triggered. - :param callbacks: a tuple of (callable) observers - :type callbacks: tuple - """ - self.current = self.DISCONNECTED - self.previous = None - # (callbacks to connect to signals in Qt-land) - self.callbacks = callbacks - - def get_readable_status(self): - # XXX DRY status / labels a little bit. - # think we'll want to i18n this. - human_status = { - 0: 'disconnected', - 1: 'connecting', - 2: 'waiting', - 3: 'authenticating', - 4: 'getting config', - 5: 'assigning ip', - 6: 'adding routes', - 7: 'connected', - 8: 'reconnecting', - 9: 'exiting', - 11: 'unrecoverable error', - } - return human_status[self.current] - - def get_leap_status(self): - # XXX improve nomenclature - leap_status = { - 0: 'disconnected', - 1: 'connecting to gateway', - 2: 'connecting to gateway', - 3: 'authenticating', - 4: 'establishing network encryption', - 5: 'establishing network encryption', - 6: 'establishing network encryption', - 7: 'connected', - 8: 'reconnecting', - 9: 'exiting', - 11: 'unrecoverable error', - } - return leap_status[self.current] - - def get_state_icon(self): - """ - returns the high level icon - for each fine-grain openvpn state - """ - connecting = (self.CONNECTING, - self.WAIT, - self.AUTH, - self.GET_CONFIG, - self.ASSIGN_IP, - self.ADD_ROUTES) - connected = (self.CONNECTED,) - disconnected = (self.DISCONNECTED, - self.UNRECOVERABLE) - - # this can be made smarter, - # but it's like it'll change, - # so +readability. - - if self.current in connecting: - return "connecting" - if self.current in connected: - return "connected" - if self.current in disconnected: - return "disconnected" - - def set_vpn_state(self, status): - """ - accepts a state string from the management - interface, and sets the internal state. - :param status: openvpn STATE (uppercase). - :type status: str - """ - if hasattr(self, status): - self.change_to(getattr(self, status)) - - def set_current(self, to): - """ - setter for the 'current' property - :param to: destination state - :type to: int - """ - self.current = to - - def change_to(self, to): - """ - :param to: destination state - :type to: int - """ - if to == self.current: - return - changed = False - from_ = self.current - self.current = to - - # We can add transition restrictions - # here to ensure no transitions are - # allowed outside the fsm. - - self.set_current(to) - changed = True - - #trigger signals (as callbacks) - #print('current state: %s' % self.current) - if changed: - self.previous = from_ - if self.callbacks: - for cb in self.callbacks: - if callable(cb): - cb(self) diff --git a/src/leap/eip/exceptions.py b/src/leap/eip/exceptions.py deleted file mode 100644 index b7d398c3..00000000 --- a/src/leap/eip/exceptions.py +++ /dev/null @@ -1,175 +0,0 @@ -""" -Generic error hierarchy -Leap/EIP exceptions used for exception handling, -logging, and notifying user of errors -during leap operation. - -Exception hierarchy -------------------- -All EIP Errors must inherit from EIPClientError (note: move that to -a more generic LEAPClientBaseError). - -Exception attributes and their meaning/uses -------------------------------------------- - -* critical: if True, will abort execution prematurely, - after attempting any cleaning - action. - -* failfirst: breaks any error_check loop that is examining - the error queue. - -* message: the message that will be used in the __repr__ of the exception. - -* usermessage: the message that will be passed to user in ErrorDialogs - in Qt-land. - -TODO: - -* EIPClientError: - Should inherit from LeapException - -* gettext / i18n for user messages. - -""" -from leap.base.exceptions import LeapException -from leap.util.translations import translate - - -# This should inherit from LeapException -class EIPClientError(Exception): - """ - base EIPClient exception - """ - critical = False - failfirst = False - warning = False - - -class CriticalError(EIPClientError): - """ - we cannot do anything about it, sorry - """ - critical = True - failfirst = True - - -class Warning(EIPClientError): - """ - just that, warnings - """ - warning = True - - -class EIPNoPolkitAuthAgentAvailable(CriticalError): - message = "No polkit authentication agent could be found" - usermessage = translate( - "EIPErrors", - "We could not find any authentication " - "agent in your system.
" - "Make sure you have " - "polkit-gnome-authentication-agent-1 " - "running and try again.") - - -class EIPNoPkexecAvailable(Warning): - message = "No pkexec binary found" - usermessage = translate( - "EIPErrors", - "We could not find pkexec in your " - "system.
Do you want to try " - "setuid workaround? " - "(DOES NOTHING YET)") - failfirst = True - - -class EIPNoCommandError(EIPClientError): - message = "no suitable openvpn command found" - usermessage = translate( - "EIPErrors", - "No suitable openvpn command found. " - "
(Might be a permissions problem)") - - -class EIPBadCertError(Warning): - # XXX this should be critical and fail close - message = "cert verification failed" - usermessage = translate( - "EIPErrors", - "there is a problem with provider certificate") - - -class LeapBadConfigFetchedError(Warning): - message = "provider sent a malformed json file" - usermessage = translate( - "EIPErrors", - "an error occurred during configuratio of leap services") - - -class OpenVPNAlreadyRunning(CriticalError): - message = "Another OpenVPN Process is already running." - usermessage = translate( - "EIPErrors", - "Another OpenVPN Process has been detected. " - "Please close it before starting leap-client") - - -class HttpsNotSupported(LeapException): - message = "connection refused while accessing via https" - usermessage = translate( - "EIPErrors", - "Server does not allow secure connections") - - -class HttpsBadCertError(LeapException): - message = "verification error on cert" - usermessage = translate( - "EIPErrors", - "Server certificate could not be verified") - -# -# errors still needing some love -# - - -class EIPInitNoKeyFileError(CriticalError): - message = "No vpn keys found in the expected path" - usermessage = translate( - "EIPErrors", - "We could not find your eip certs in the expected path") - - -class EIPInitBadKeyFilePermError(Warning): - # I don't know if we should be telling user or not, - # we try to fix permissions and should only re-raise - # if permission check failed. - pass - - -class EIPInitNoProviderError(EIPClientError): - pass - - -class EIPInitBadProviderError(EIPClientError): - pass - - -class EIPConfigurationError(EIPClientError): - pass - -# -# Errors that probably we don't need anymore -# chase down for them and check. -# - - -class MissingSocketError(Exception): - pass - - -class ConnectionRefusedError(Exception): - pass - - -class EIPMissingDefaultProvider(Exception): - pass diff --git a/src/leap/eip/openvpnconnection.py b/src/leap/eip/openvpnconnection.py deleted file mode 100644 index 455735c8..00000000 --- a/src/leap/eip/openvpnconnection.py +++ /dev/null @@ -1,410 +0,0 @@ -""" -OpenVPN Connection -""" -from __future__ import (print_function) -from functools import partial -import logging -import os -import psutil -import shutil -import select -import socket -from time import sleep - -logger = logging.getLogger(name=__name__) - -from leap.base.connection import Connection -from leap.base.constants import OPENVPN_BIN -from leap.util.coroutines import spawn_and_watch_process -from leap.util.misc import get_openvpn_pids - -from leap.eip.udstelnet import UDSTelnet -from leap.eip import config as eip_config -from leap.eip import exceptions as eip_exceptions - - -class OpenVPNManagement(object): - - # TODO explain a little bit how management interface works - # and our telnet interface with support for unix sockets. - - """ - for more information, read openvpn management notes. - zcat `dpkg -L openvpn | grep management` - """ - - def _connect_to_management(self): - """ - Connect to openvpn management interface - """ - if hasattr(self, 'tn'): - self._close_management_socket() - self.tn = UDSTelnet(self.host, self.port) - - # XXX make password optional - # specially for win. we should generate - # the pass on the fly when invoking manager - # from conductor - - #self.tn.read_until('ENTER PASSWORD:', 2) - #self.tn.write(self.password + '\n') - #self.tn.read_until('SUCCESS:', 2) - if self.tn: - self._seek_to_eof() - return True - - def _close_management_socket(self, announce=True): - """ - Close connection to openvpn management interface - """ - logger.debug('closing socket') - if announce: - self.tn.write("quit\n") - self.tn.read_all() - self.tn.get_socket().close() - del self.tn - - def _seek_to_eof(self): - """ - Read as much as available. Position seek pointer to end of stream - """ - try: - b = self.tn.read_eager() - except EOFError: - logger.debug("Could not read from socket. Assuming it died.") - return - while b: - try: - b = self.tn.read_eager() - except EOFError: - logger.debug("Could not read from socket. Assuming it died.") - - def _send_command(self, cmd): - """ - Send a command to openvpn and return response as list - """ - if not self.connected(): - try: - self._connect_to_management() - except eip_exceptions.MissingSocketError: - #logger.warning('missing management socket') - return [] - try: - if hasattr(self, 'tn'): - self.tn.write(cmd + "\n") - except socket.error: - logger.error('socket error') - self._close_management_socket(announce=False) - return [] - try: - buf = self.tn.read_until(b"END", 2) - self._seek_to_eof() - blist = buf.split('\r\n') - if blist[-1].startswith('END'): - del blist[-1] - return blist - else: - return [] - except socket.error as exc: - logger.debug('socket error: %s' % exc.message) - except select.error as exc: - logger.debug('select error: %s' % exc.message) - - def _send_short_command(self, cmd): - """ - parse output from commands that are - delimited by "success" instead - """ - if not self.connected(): - self.connect() - self.tn.write(cmd + "\n") - # XXX not working? - buf = self.tn.read_until(b"SUCCESS", 2) - self._seek_to_eof() - blist = buf.split('\r\n') - return blist - - # - # random maybe useful vpn commands - # - - def pid(self): - #XXX broken - return self._send_short_command("pid") - - -class OpenVPNConnection(Connection, OpenVPNManagement): - """ - All related to invocation - of the openvpn binary. - It's extended by EIPConnection. - """ - - # XXX Inheriting from Connection was an early design idea - # but currently that's an empty class. - # We can get rid of that if we don't use it for sharing - # state with other leap modules. - - def __init__(self, - watcher_cb=None, - debug=False, - host=None, - port="unix", - password=None, - *args, **kwargs): - """ - :param watcher_cb: callback to be \ -called for each line in watched stdout - :param signal_map: dictionary of signal names and callables \ -to be triggered for each one of them. - :type watcher_cb: function - :type signal_map: dict - """ - #XXX FIXME - #change watcher_cb to line_observer - # XXX if not host: raise ImproperlyConfigured - - logger.debug('init openvpn connection') - self.debug = debug - self.ovpn_verbosity = kwargs.get('ovpn_verbosity', None) - - self.watcher_cb = watcher_cb - #self.signal_maps = signal_maps - - self.subp = None - self.watcher = None - - self.server = None - self.port = None - self.proto = None - - self.command = None - self.args = None - - # XXX get autostart from config - self.autostart = True - - # management interface init - self.host = host - if isinstance(port, str) and port.isdigit(): - port = int(port) - elif port == "unix": - port = "unix" - else: - port = None - self.port = port - self.password = password - - def run_openvpn_checks(self): - """ - runs check needed before launching - openvpn subprocess. will raise if errors found. - """ - logger.debug('running openvpn checks') - # XXX I think that "check_if_running" should be called - # from try openvpn connection instead. -- kali. - # let's prepare tests for that before changing it... - self._check_if_running_instance() - self._set_ovpn_command() - self._check_vpn_keys() - - def try_openvpn_connection(self): - """ - attempts to connect - """ - # XXX should make public method - if self.command is None: - raise eip_exceptions.EIPNoCommandError - if self.subp is not None: - logger.debug('cowardly refusing to launch subprocess again') - # XXX this is not returning ???!! - # FIXME -- so it's calling it all the same!! - - self._launch_openvpn() - - def connected(self): - """ - Returns True if connected - rtype: bool - """ - # XXX make a property - return hasattr(self, 'tn') - - def terminate_openvpn_connection(self, shutdown=False): - """ - terminates openvpn child subprocess - """ - if self.subp: - try: - self._stop_openvpn() - except eip_exceptions.ConnectionRefusedError: - logger.warning( - 'unable to send sigterm signal to openvpn: ' - 'connection refused.') - - # XXX kali -- - # XXX review-me - # I think this will block if child process - # does not return. - # Maybe we can .poll() for a given - # interval and exit in any case. - - RETCODE = self.subp.wait() - if RETCODE: - logger.error( - 'cannot terminate subprocess! Retcode %s' - '(We might have left openvpn running)' % RETCODE) - - if shutdown: - self._cleanup_tempfiles() - - def _cleanup_tempfiles(self): - """ - remove all temporal files - we might have left behind - """ - # if self.port is 'unix', we have - # created a temporal socket path that, under - # normal circumstances, we should be able to - # delete - - if self.port == "unix": - logger.debug('cleaning socket file temp folder') - - tempfolder = os.path.split(self.host)[0] - if os.path.isdir(tempfolder): - try: - shutil.rmtree(tempfolder) - except OSError: - logger.error('could not delete tmpfolder %s' % tempfolder) - - # checks - - def _check_if_running_instance(self): - """ - check if openvpn is already running - """ - openvpn_pids = get_openvpn_pids() - if openvpn_pids: - logger.debug('an openvpn instance is already running.') - logger.debug('attempting to stop openvpn instance.') - if not self._stop_openvpn(): - raise eip_exceptions.OpenVPNAlreadyRunning - return - else: - logger.debug('no openvpn instance found.') - - def _set_ovpn_command(self): - try: - command, args = eip_config.build_ovpn_command( - provider=self.provider, - debug=self.debug, - socket_path=self.host, - ovpn_verbosity=self.ovpn_verbosity) - except eip_exceptions.EIPNoPolkitAuthAgentAvailable: - command = args = None - raise - except eip_exceptions.EIPNoPkexecAvailable: - command = args = None - raise - - # XXX if not command, signal error. - self.command = command - self.args = args - - def _check_vpn_keys(self): - """ - checks for correct permissions on vpn keys - """ - try: - eip_config.check_vpn_keys(provider=self.provider) - except eip_exceptions.EIPInitBadKeyFilePermError: - logger.error('Bad VPN Keys permission!') - # do nothing now - # and raise the rest ... - - # starting and stopping openvpn subprocess - - def _launch_openvpn(self): - """ - invocation of openvpn binaries in a subprocess. - """ - #XXX TODO: - #deprecate watcher_cb, - #use _only_ signal_maps instead - - #logger.debug('_launch_openvpn called') - if self.watcher_cb is not None: - linewrite_callback = self.watcher_cb - else: - #XXX get logger instead - linewrite_callback = lambda line: logger.debug( - 'watcher: %s' % line) - - # the partial is not - # being applied now because we're not observing the process - # stdout like we did in the early stages. but I leave it - # here since it will be handy for observing patterns in the - # thru-the-manager updates (with regex) - observers = (linewrite_callback, - partial(lambda con_status, - line: linewrite_callback, self.status)) - subp, watcher = spawn_and_watch_process( - self.command, - self.args, - observers=observers) - self.subp = subp - self.watcher = watcher - - def _stop_openvpn(self): - """ - stop openvpn process - by sending SIGTERM to the management - interface - """ - # XXX method a bit too long, split - logger.debug("atempting to terminate openvpn process...") - if self.connected(): - try: - self._send_command("signal SIGTERM\n") - sleep(1) - if not self.subp: # XXX ??? - return True - except socket.error: - logger.warning('management socket died') - return - - #shutting openvpn failured - #try patching in old openvpn host and trying again - # XXX could be more than one! - process = self._get_openvpn_process() - if process: - logger.debug('process: %s' % process.name) - cmdline = process.cmdline - - manag_flag = "--management" - if isinstance(cmdline, list) and manag_flag in cmdline: - _index = cmdline.index(manag_flag) - self.host = cmdline[_index + 1] - self._send_command("signal SIGTERM\n") - - #make sure the process was terminated - process = self._get_openvpn_process() - if not process: - logger.debug("Existing OpenVPN Process Terminated") - return True - else: - logger.error("Unable to terminate existing OpenVPN Process.") - return False - - return True - - def _get_openvpn_process(self): - for process in psutil.process_iter(): - if OPENVPN_BIN in process.name: - return process - return None - - def get_log(self, lines=1): - log = self._send_command("log %s" % lines) - return log diff --git a/src/leap/eip/specs.py b/src/leap/eip/specs.py deleted file mode 100644 index c41fd29b..00000000 --- a/src/leap/eip/specs.py +++ /dev/null @@ -1,136 +0,0 @@ -from __future__ import (unicode_literals) -import os - -from leap import __branding -from leap.base import config as baseconfig - -# XXX move provider stuff to base config - -PROVIDER_CA_CERT = __branding.get( - 'provider_ca_file', - 'cacert.pem') - -provider_ca_path = lambda domain: str(os.path.join( - #baseconfig.get_default_provider_path(), - baseconfig.get_provider_path(domain), - 'keys', 'ca', - 'cacert.pem' -)) if domain else None - -default_provider_ca_path = lambda: str(os.path.join( - baseconfig.get_default_provider_path(), - 'keys', 'ca', - PROVIDER_CA_CERT -)) - -PROVIDER_DOMAIN = __branding.get('provider_domain', 'testprovider.example.org') - - -client_cert_path = lambda domain: unicode(os.path.join( - baseconfig.get_provider_path(domain), - 'keys', 'client', - 'openvpn.pem' -)) if domain else None - -default_client_cert_path = lambda: unicode(os.path.join( - baseconfig.get_default_provider_path(), - 'keys', 'client', - 'openvpn.pem' -)) - -eipconfig_spec = { - 'description': 'sample eipconfig', - 'type': 'object', - 'properties': { - 'provider': { - 'type': unicode, - 'default': u"%s" % PROVIDER_DOMAIN, - 'required': True, - }, - 'transport': { - 'type': unicode, - 'default': u"openvpn", - }, - 'openvpn_protocol': { - 'type': unicode, - 'default': u"tcp" - }, - 'openvpn_port': { - 'type': int, - 'default': 80 - }, - 'openvpn_ca_certificate': { - 'type': unicode, # path - 'default': default_provider_ca_path - }, - 'openvpn_client_certificate': { - 'type': unicode, # path - 'default': default_client_cert_path - }, - 'connect_on_login': { - 'type': bool, - 'default': True - }, - 'block_cleartext_traffic': { - 'type': bool, - 'default': True - }, - 'primary_gateway': { - 'type': unicode, - 'default': u"location_unknown", - #'required': True - }, - 'secondary_gateway': { - 'type': unicode, - 'default': u"location_unknown2" - }, - 'management_password': { - 'type': unicode - } - } -} - -eipservice_config_spec = { - 'description': 'sample eip service config', - 'type': 'object', - 'properties': { - 'serial': { - 'type': int, - 'required': True, - 'default': 1 - }, - 'version': { - 'type': int, - 'required': True, - 'default': 1 - }, - 'clusters': { - 'type': list, - 'default': [ - {"label": { - "en": "Location Unknown"}, - "name": "location_unknown"}] - }, - 'gateways': { - 'type': list, - 'default': [ - {"capabilities": { - "adblock": True, - "filter_dns": True, - "ports": ["80", "53", "443", "1194"], - "protocols": ["udp", "tcp"], - "transport": ["openvpn"], - "user_ips": False}, - "cluster": "location_unknown", - "host": "location.example.org", - "ip_address": "127.0.0.1"}] - }, - 'openvpn_configuration': { - 'type': dict, - 'default': { - "auth": None, - "cipher": None, - "tls-cipher": None} - } - } -} diff --git a/src/leap/eip/tests/__init__.py b/src/leap/eip/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/eip/tests/data.py b/src/leap/eip/tests/data.py deleted file mode 100644 index a7fe1853..00000000 --- a/src/leap/eip/tests/data.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import unicode_literals -import os - -#from leap import __branding - -# sample data used in tests - -#PROVIDER = __branding.get('provider_domain') -PROVIDER = "testprovider.example.org" - -EIP_SAMPLE_CONFIG = { - "provider": "%s" % PROVIDER, - "transport": "openvpn", - "openvpn_protocol": "tcp", - "openvpn_port": 80, - "openvpn_ca_certificate": os.path.expanduser( - "~/.config/leap/providers/" - "%s/" - "keys/ca/cacert.pem" % PROVIDER), - "openvpn_client_certificate": os.path.expanduser( - "~/.config/leap/providers/" - "%s/" - "keys/client/openvpn.pem" % PROVIDER), - "connect_on_login": True, - "block_cleartext_traffic": True, - "primary_gateway": "location_unknown", - "secondary_gateway": "location_unknown2", - #"management_password": "oph7Que1othahwiech6J" -} - -EIP_SAMPLE_SERVICE = { - "serial": 1, - "version": 1, - "clusters": [ - {"label": { - "en": "Location Unknown"}, - "name": "location_unknown"} - ], - "gateways": [ - {"capabilities": { - "adblock": True, - "filter_dns": True, - "ports": ["80", "53", "443", "1194"], - "protocols": ["udp", "tcp"], - "transport": ["openvpn"], - "user_ips": False}, - "cluster": "location_unknown", - "host": "location.example.org", - "ip_address": "192.0.43.10"} - ] -} diff --git a/src/leap/eip/tests/test_checks.py b/src/leap/eip/tests/test_checks.py deleted file mode 100644 index ab11037a..00000000 --- a/src/leap/eip/tests/test_checks.py +++ /dev/null @@ -1,373 +0,0 @@ -from BaseHTTPServer import BaseHTTPRequestHandler -import copy -import json -try: - import unittest2 as unittest -except ImportError: - import unittest -import os -import time -import urlparse - -from mock import (patch, Mock) - -import jsonschema -#import ping -import requests - -from leap.base import config as baseconfig -from leap.base.constants import (DEFAULT_PROVIDER_DEFINITION, - DEFINITION_EXPECTED_PATH) -from leap.eip import checks as eipchecks -from leap.eip import specs as eipspecs -from leap.eip import exceptions as eipexceptions -from leap.eip.tests import data as testdata -from leap.testing.basetest import BaseLeapTest -from leap.testing.https_server import BaseHTTPSServerTestCase -from leap.testing.https_server import where as where_cert -from leap.util.fileutil import mkdir_f - - -class NoLogRequestHandler: - def log_message(self, *args): - # don't write log msg to stderr - pass - - def read(self, n=None): - return '' - - -class EIPCheckTest(BaseLeapTest): - - __name__ = "eip_check_tests" - provider = "testprovider.example.org" - maxDiff = None - - def setUp(self): - pass - - def tearDown(self): - pass - - # test methods are there, and can be called from run_all - - def test_checker_should_implement_check_methods(self): - checker = eipchecks.EIPConfigChecker(domain=self.provider) - - self.assertTrue(hasattr(checker, "check_default_eipconfig"), - "missing meth") - self.assertTrue(hasattr(checker, "check_is_there_default_provider"), - "missing meth") - self.assertTrue(hasattr(checker, "fetch_definition"), "missing meth") - self.assertTrue(hasattr(checker, "fetch_eip_service_config"), - "missing meth") - self.assertTrue(hasattr(checker, "check_complete_eip_config"), - "missing meth") - - def test_checker_should_actually_call_all_tests(self): - checker = eipchecks.EIPConfigChecker(domain=self.provider) - - mc = Mock() - checker.run_all(checker=mc) - self.assertTrue(mc.check_default_eipconfig.called, "not called") - self.assertTrue(mc.check_is_there_default_provider.called, - "not called") - self.assertTrue(mc.fetch_definition.called, - "not called") - self.assertTrue(mc.fetch_eip_service_config.called, - "not called") - self.assertTrue(mc.check_complete_eip_config.called, - "not called") - - # test individual check methods - - def test_check_default_eipconfig(self): - checker = eipchecks.EIPConfigChecker(domain=self.provider) - # no eip config (empty home) - eipconfig_path = checker.eipconfig.filename - self.assertFalse(os.path.isfile(eipconfig_path)) - checker.check_default_eipconfig() - # we've written one, so it should be there. - self.assertTrue(os.path.isfile(eipconfig_path)) - with open(eipconfig_path, 'rb') as fp: - deserialized = json.load(fp) - - # force re-evaluation of the paths - # small workaround for evaluating home dirs correctly - EIP_SAMPLE_CONFIG = copy.copy(testdata.EIP_SAMPLE_CONFIG) - EIP_SAMPLE_CONFIG['openvpn_client_certificate'] = \ - eipspecs.client_cert_path(self.provider) - EIP_SAMPLE_CONFIG['openvpn_ca_certificate'] = \ - eipspecs.provider_ca_path(self.provider) - self.assertEqual(deserialized, EIP_SAMPLE_CONFIG) - - # TODO: shold ALSO run validation methods. - - def test_check_is_there_default_provider(self): - checker = eipchecks.EIPConfigChecker(domain=self.provider) - # we do dump a sample eip config, but lacking a - # default provider entry. - # This error will be possible catched in a different - # place, when JSONConfig does validation of required fields. - - # passing direct config - with self.assertRaises(eipexceptions.EIPMissingDefaultProvider): - checker.check_is_there_default_provider(config={}) - - # ok. now, messing with real files... - # blank out default_provider - sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG) - sampleconfig['provider'] = None - eipcfg_path = checker.eipconfig.filename - mkdir_f(eipcfg_path) - with open(eipcfg_path, 'w') as fp: - json.dump(sampleconfig, fp) - #with self.assertRaises(eipexceptions.EIPMissingDefaultProvider): - # XXX we should catch this as one of our errors, but do not - # see how to do it quickly. - with self.assertRaises(jsonschema.ValidationError): - #import ipdb;ipdb.set_trace() - checker.eipconfig.load(fromfile=eipcfg_path) - checker.check_is_there_default_provider() - - sampleconfig = testdata.EIP_SAMPLE_CONFIG - #eipcfg_path = checker._get_default_eipconfig_path() - with open(eipcfg_path, 'w') as fp: - json.dump(sampleconfig, fp) - checker.eipconfig.load() - self.assertTrue(checker.check_is_there_default_provider()) - - def test_fetch_definition(self): - with patch.object(requests, "get") as mocked_get: - mocked_get.return_value.status_code = 200 - mocked_get.return_value.headers = { - 'last-modified': "Wed Dec 12 12:12:12 GMT 2012"} - mocked_get.return_value.json = DEFAULT_PROVIDER_DEFINITION - checker = eipchecks.EIPConfigChecker(fetcher=requests) - sampleconfig = testdata.EIP_SAMPLE_CONFIG - checker.fetch_definition(config=sampleconfig) - - fn = os.path.join(baseconfig.get_default_provider_path(), - DEFINITION_EXPECTED_PATH) - with open(fn, 'r') as fp: - deserialized = json.load(fp) - self.assertEqual(DEFAULT_PROVIDER_DEFINITION, deserialized) - - # XXX TODO check for ConnectionError, HTTPError, InvalidUrl - # (and proper EIPExceptions are raised). - # Look at base.test_config. - - def test_fetch_eip_service_config(self): - with patch.object(requests, "get") as mocked_get: - mocked_get.return_value.status_code = 200 - mocked_get.return_value.headers = { - 'last-modified': "Wed Dec 12 12:12:12 GMT 2012"} - mocked_get.return_value.json = testdata.EIP_SAMPLE_SERVICE - checker = eipchecks.EIPConfigChecker(fetcher=requests) - sampleconfig = testdata.EIP_SAMPLE_CONFIG - checker.fetch_eip_service_config(config=sampleconfig) - - def test_check_complete_eip_config(self): - checker = eipchecks.EIPConfigChecker() - with self.assertRaises(eipexceptions.EIPConfigurationError): - sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG) - sampleconfig['provider'] = None - checker.check_complete_eip_config(config=sampleconfig) - with self.assertRaises(eipexceptions.EIPConfigurationError): - sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG) - del sampleconfig['provider'] - checker.check_complete_eip_config(config=sampleconfig) - - # normal case - sampleconfig = copy.copy(testdata.EIP_SAMPLE_CONFIG) - checker.check_complete_eip_config(config=sampleconfig) - - -class ProviderCertCheckerTest(BaseLeapTest): - - __name__ = "provider_cert_checker_tests" - provider = "testprovider.example.org" - - def setUp(self): - pass - - def tearDown(self): - pass - - # test methods are there, and can be called from run_all - - def test_checker_should_implement_check_methods(self): - checker = eipchecks.ProviderCertChecker() - - # For MVS+ - self.assertTrue(hasattr(checker, "download_ca_cert"), - "missing meth") - self.assertTrue(hasattr(checker, "download_ca_signature"), - "missing meth") - self.assertTrue(hasattr(checker, "get_ca_signatures"), "missing meth") - self.assertTrue(hasattr(checker, "is_there_trust_path"), - "missing meth") - - # For MVS - self.assertTrue(hasattr(checker, "is_there_provider_ca"), - "missing meth") - self.assertTrue(hasattr(checker, "is_https_working"), "missing meth") - self.assertTrue(hasattr(checker, "check_new_cert_needed"), - "missing meth") - - def test_checker_should_actually_call_all_tests(self): - checker = eipchecks.ProviderCertChecker() - - mc = Mock() - checker.run_all(checker=mc) - # XXX MVS+ - #self.assertTrue(mc.download_ca_cert.called, "not called") - #self.assertTrue(mc.download_ca_signature.called, "not called") - #self.assertTrue(mc.get_ca_signatures.called, "not called") - #self.assertTrue(mc.is_there_trust_path.called, "not called") - - # For MVS - self.assertTrue(mc.is_there_provider_ca.called, "not called") - self.assertTrue(mc.is_https_working.called, - "not called") - self.assertTrue(mc.check_new_cert_needed.called, - "not called") - - # test individual check methods - - @unittest.skip - def test_is_there_provider_ca(self): - # XXX commenting out this test. - # With the generic client this does not make sense, - # we should dump one there. - # or test conductor logic. - checker = eipchecks.ProviderCertChecker() - self.assertTrue( - checker.is_there_provider_ca()) - - -class ProviderCertCheckerHTTPSTests(BaseHTTPSServerTestCase, BaseLeapTest): - provider = "testprovider.example.org" - - class request_handler(NoLogRequestHandler, BaseHTTPRequestHandler): - responses = { - '/': ['OK', ''], - '/client.cert': [ - # XXX get sample cert - '-----BEGIN CERTIFICATE-----', - '-----END CERTIFICATE-----'], - '/badclient.cert': [ - 'BADCERT']} - - def do_GET(self): - path = urlparse.urlparse(self.path) - message = '\n'.join(self.responses.get( - path.path, None)) - self.send_response(200) - self.end_headers() - self.wfile.write(message) - - def test_is_https_working(self): - fetcher = requests - uri = "https://%s/" % (self.get_server()) - # bare requests call. this should just pass (if there is - # an https service there). - fetcher.get(uri, verify=False) - checker = eipchecks.ProviderCertChecker(fetcher=fetcher) - self.assertTrue(checker.is_https_working(uri=uri, verify=False)) - - # for local debugs, when in doubt - #self.assertTrue(checker.is_https_working(uri="https://github.com", - #verify=True)) - - # for the two checks below, I know they fail because no ca - # cert is passed to them, and I know that's the error that - # requests return with our implementation. - # We're receiving this because our - # server is dying prematurely when the handshake is interrupted on the - # client side. - # Since we have access to the server, we could check that - # the error raised has been: - # SSL23_READ_BYTES: alert bad certificate - with self.assertRaises(requests.exceptions.SSLError) as exc: - fetcher.get(uri, verify=True) - self.assertTrue( - "SSL23_GET_SERVER_HELLO:unknown protocol" in exc.message) - - # XXX FIXME! Uncomment after #638 is done - #with self.assertRaises(eipexceptions.EIPBadCertError) as exc: - #checker.is_https_working(uri=uri, verify=True) - #self.assertTrue( - #"cert verification failed" in exc.message) - - # get cacert from testing.https_server - cacert = where_cert('cacert.pem') - fetcher.get(uri, verify=cacert) - self.assertTrue(checker.is_https_working(uri=uri, verify=cacert)) - - # same, but get cacert from leap.custom - # XXX TODO! - - @unittest.skip - def test_download_new_client_cert(self): - # FIXME - # Magick srp decorator broken right now... - # Have to mock the decorator and inject something that - # can bypass the authentication - - uri = "https://%s/client.cert" % (self.get_server()) - cacert = where_cert('cacert.pem') - checker = eipchecks.ProviderCertChecker(domain=self.provider) - credentials = "testuser", "testpassword" - self.assertTrue(checker.download_new_client_cert( - credentials=credentials, uri=uri, verify=cacert)) - - # now download a malformed cert - uri = "https://%s/badclient.cert" % (self.get_server()) - cacert = where_cert('cacert.pem') - checker = eipchecks.ProviderCertChecker() - with self.assertRaises(ValueError): - self.assertTrue(checker.download_new_client_cert( - credentials=credentials, uri=uri, verify=cacert)) - - # did we write cert to its path? - clientcertfile = eipspecs.client_cert_path() - self.assertTrue(os.path.isfile(clientcertfile)) - certfile = eipspecs.client_cert_path() - with open(certfile, 'r') as cf: - certcontent = cf.read() - self.assertEqual(certcontent, - '\n'.join( - self.request_handler.responses['/client.cert'])) - os.remove(clientcertfile) - - def test_is_cert_valid(self): - checker = eipchecks.ProviderCertChecker() - # TODO: better exception catching - # should raise eipexceptions.BadClientCertificate, and give reasons - # on msg. - with self.assertRaises(Exception) as exc: - self.assertFalse(checker.is_cert_valid()) - exc.message = "missing cert" - - def test_bad_validity_certs(self): - checker = eipchecks.ProviderCertChecker() - certfile = where_cert('leaptestscert.pem') - self.assertFalse(checker.is_cert_not_expired( - certfile=certfile, - now=lambda: time.mktime((2038, 1, 1, 1, 1, 1, 1, 1, 1)))) - self.assertFalse(checker.is_cert_not_expired( - certfile=certfile, - now=lambda: time.mktime((1970, 1, 1, 1, 1, 1, 1, 1, 1)))) - - def test_check_new_cert_needed(self): - # check: missing cert - checker = eipchecks.ProviderCertChecker(domain=self.provider) - self.assertTrue(checker.check_new_cert_needed(skip_download=True)) - # TODO check: malformed cert - # TODO check: expired cert - # TODO check: pass test server uri instead of skip - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/eip/tests/test_config.py b/src/leap/eip/tests/test_config.py deleted file mode 100644 index 72ab3c8e..00000000 --- a/src/leap/eip/tests/test_config.py +++ /dev/null @@ -1,298 +0,0 @@ -from collections import OrderedDict -import json -import os -import platform -import stat - -try: - import unittest2 as unittest -except ImportError: - import unittest - -#from leap.base import constants -#from leap.eip import config as eip_config -#from leap import __branding as BRANDING -from leap.eip import config as eipconfig -from leap.eip.tests.data import EIP_SAMPLE_CONFIG, EIP_SAMPLE_SERVICE -from leap.testing.basetest import BaseLeapTest -from leap.util.fileutil import mkdir_p, mkdir_f - -_system = platform.system() - -#PROVIDER = BRANDING.get('provider_domain') -#PROVIDER_SHORTNAME = BRANDING.get('short_name') - - -class EIPConfigTest(BaseLeapTest): - - __name__ = "eip_config_tests" - provider = "testprovider.example.org" - - maxDiff = None - - def setUp(self): - pass - - def tearDown(self): - pass - - # - # helpers - # - - def touch_exec(self): - path = os.path.join( - self.tempdir, 'bin') - mkdir_p(path) - tfile = os.path.join( - path, - 'openvpn') - open(tfile, 'wb').close() - os.chmod(tfile, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) - - def write_sample_eipservice(self, vpnciphers=False, extra_vpnopts=None, - gateways=None): - conf = eipconfig.EIPServiceConfig() - mkdir_f(conf.filename) - if gateways: - EIP_SAMPLE_SERVICE['gateways'] = gateways - if vpnciphers: - openvpnconfig = OrderedDict({ - "auth": "SHA1", - "cipher": "AES-128-CBC", - "tls-cipher": "DHE-RSA-AES128-SHA"}) - if extra_vpnopts: - for k, v in extra_vpnopts.items(): - openvpnconfig[k] = v - EIP_SAMPLE_SERVICE['openvpn_configuration'] = openvpnconfig - - with open(conf.filename, 'w') as fd: - fd.write(json.dumps(EIP_SAMPLE_SERVICE)) - - def write_sample_eipconfig(self): - conf = eipconfig.EIPConfig() - folder, f = os.path.split(conf.filename) - if not os.path.isdir(folder): - mkdir_p(folder) - with open(conf.filename, 'w') as fd: - fd.write(json.dumps(EIP_SAMPLE_CONFIG)) - - def get_expected_openvpn_args(self, with_openvpn_ciphers=False): - """ - yeah, this is almost as duplicating the - code for building the command - """ - args = [] - eipconf = eipconfig.EIPConfig(domain=self.provider) - eipconf.load() - eipsconf = eipconfig.EIPServiceConfig(domain=self.provider) - eipsconf.load() - - username = self.get_username() - groupname = self.get_groupname() - - args.append('--client') - args.append('--dev') - #does this have to be tap for win?? - args.append('tun') - args.append('--persist-tun') - args.append('--persist-key') - args.append('--remote') - - args.append('%s' % eipconfig.get_eip_gateway( - eipconfig=eipconf, - eipserviceconfig=eipsconf)) - # XXX get port!? - args.append('1194') - # XXX get proto - args.append('udp') - args.append('--tls-client') - args.append('--remote-cert-tls') - args.append('server') - - if with_openvpn_ciphers: - CIPHERS = [ - "--tls-cipher", "DHE-RSA-AES128-SHA", - "--cipher", "AES-128-CBC", - "--auth", "SHA1"] - for opt in CIPHERS: - args.append(opt) - - args.append('--user') - args.append(username) - args.append('--group') - args.append(groupname) - args.append('--management-client-user') - args.append(username) - args.append('--management-signal') - - args.append('--management') - #XXX hey! - #get platform switches here! - args.append('/tmp/test.socket') - args.append('unix') - - args.append('--script-security') - args.append('2') - - if _system == "Linux": - UPDOWN_SCRIPT = "/etc/leap/resolv-update" - if os.path.isfile(UPDOWN_SCRIPT): - args.append('--up') - args.append('/etc/leap/resolv-update') - args.append('--down') - args.append('/etc/leap/resolv-update') - args.append('--plugin') - args.append('/usr/lib/openvpn/openvpn-down-root.so') - args.append("'script_type=down /etc/leap/resolv-update'") - - # certs - # XXX get values from specs? - args.append('--cert') - args.append(os.path.join( - self.home, - '.config', 'leap', 'providers', - '%s' % self.provider, - 'keys', 'client', - 'openvpn.pem')) - args.append('--key') - args.append(os.path.join( - self.home, - '.config', 'leap', 'providers', - '%s' % self.provider, - 'keys', 'client', - 'openvpn.pem')) - args.append('--ca') - args.append(os.path.join( - self.home, - '.config', 'leap', 'providers', - '%s' % self.provider, - 'keys', 'ca', - 'cacert.pem')) - return args - - # build command string - # these tests are going to have to check - # many combinations. we should inject some - # params in the function call, to disable - # some checks. - - def test_get_eip_gateway(self): - self.write_sample_eipconfig() - eipconf = eipconfig.EIPConfig(domain=self.provider) - - # default eipservice - self.write_sample_eipservice() - eipsconf = eipconfig.EIPServiceConfig(domain=self.provider) - - gateway = eipconfig.get_eip_gateway( - eipconfig=eipconf, - eipserviceconfig=eipsconf) - - # in spec is local gateway by default - self.assertEqual(gateway, '127.0.0.1') - - # change eipservice - # right now we only check that cluster == selected primary gw in - # eip.json, and pick first matching ip - eipconf._config.config['primary_gateway'] = "foo_provider" - newgateways = [{"cluster": "foo_provider", - "ip_address": "127.0.0.99"}] - self.write_sample_eipservice(gateways=newgateways) - eipsconf = eipconfig.EIPServiceConfig(domain=self.provider) - # load from disk file - eipsconf.load() - - gateway = eipconfig.get_eip_gateway( - eipconfig=eipconf, - eipserviceconfig=eipsconf) - self.assertEqual(gateway, '127.0.0.99') - - # change eipservice, several gateways - # right now we only check that cluster == selected primary gw in - # eip.json, and pick first matching ip - eipconf._config.config['primary_gateway'] = "bar_provider" - newgateways = [{"cluster": "foo_provider", - "ip_address": "127.0.0.99"}, - {'cluster': "bar_provider", - "ip_address": "127.0.0.88"}] - self.write_sample_eipservice(gateways=newgateways) - eipsconf = eipconfig.EIPServiceConfig(domain=self.provider) - # load from disk file - eipsconf.load() - - gateway = eipconfig.get_eip_gateway( - eipconfig=eipconf, - eipserviceconfig=eipsconf) - self.assertEqual(gateway, '127.0.0.88') - - def test_build_ovpn_command_empty_config(self): - self.touch_exec() - self.write_sample_eipservice() - self.write_sample_eipconfig() - - from leap.eip import config as eipconfig - from leap.util.fileutil import which - path = os.environ['PATH'] - vpnbin = which('openvpn', path=path) - #print 'path =', path - #print 'vpnbin = ', vpnbin - vpncommand, vpnargs = eipconfig.build_ovpn_command( - do_pkexec_check=False, vpnbin=vpnbin, - socket_path="/tmp/test.socket", - provider=self.provider) - self.assertEqual(vpncommand, self.home + '/bin/openvpn') - self.assertEqual(vpnargs, self.get_expected_openvpn_args()) - - def test_build_ovpn_command_openvpnoptions(self): - self.touch_exec() - - from leap.eip import config as eipconfig - from leap.util.fileutil import which - path = os.environ['PATH'] - vpnbin = which('openvpn', path=path) - - self.write_sample_eipconfig() - - # regular run, everything normal - self.write_sample_eipservice(vpnciphers=True) - vpncommand, vpnargs = eipconfig.build_ovpn_command( - do_pkexec_check=False, vpnbin=vpnbin, - socket_path="/tmp/test.socket", - provider=self.provider) - self.assertEqual(vpncommand, self.home + '/bin/openvpn') - expected = self.get_expected_openvpn_args( - with_openvpn_ciphers=True) - self.assertEqual(vpnargs, expected) - - # bad options -- illegal options - self.write_sample_eipservice( - vpnciphers=True, - # WE ONLY ALLOW vpn options in auth, cipher, tls-cipher - extra_vpnopts={"notallowedconfig": "badvalue"}) - vpncommand, vpnargs = eipconfig.build_ovpn_command( - do_pkexec_check=False, vpnbin=vpnbin, - socket_path="/tmp/test.socket", - provider=self.provider) - self.assertEqual(vpncommand, self.home + '/bin/openvpn') - expected = self.get_expected_openvpn_args( - with_openvpn_ciphers=True) - self.assertEqual(vpnargs, expected) - - # bad options -- illegal chars - self.write_sample_eipservice( - vpnciphers=True, - # WE ONLY ALLOW A-Z09\- - extra_vpnopts={"cipher": "AES-128-CBC;FOOTHING"}) - vpncommand, vpnargs = eipconfig.build_ovpn_command( - do_pkexec_check=False, vpnbin=vpnbin, - socket_path="/tmp/test.socket", - provider=self.provider) - self.assertEqual(vpncommand, self.home + '/bin/openvpn') - expected = self.get_expected_openvpn_args( - with_openvpn_ciphers=True) - self.assertEqual(vpnargs, expected) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/eip/tests/test_eipconnection.py b/src/leap/eip/tests/test_eipconnection.py deleted file mode 100644 index 163f8d45..00000000 --- a/src/leap/eip/tests/test_eipconnection.py +++ /dev/null @@ -1,216 +0,0 @@ -import glob -import logging -import platform -#import os -import shutil - -logging.basicConfig() -logger = logging.getLogger(name=__name__) - -try: - import unittest2 as unittest -except ImportError: - import unittest - -from mock import Mock, patch # MagicMock - -from leap.eip.eipconnection import EIPConnection -from leap.eip.exceptions import ConnectionRefusedError -from leap.eip import specs as eipspecs -from leap.testing.basetest import BaseLeapTest - -_system = platform.system() - -PROVIDER = "testprovider.example.org" - - -class NotImplementedError(Exception): - pass - - -@patch('OpenVPNConnection._get_or_create_config') -@patch('OpenVPNConnection._set_ovpn_command') -class MockedEIPConnection(EIPConnection): - - def _set_ovpn_command(self): - self.command = "mock_command" - self.args = [1, 2, 3] - - -class EIPConductorTest(BaseLeapTest): - - __name__ = "eip_conductor_tests" - provider = PROVIDER - - def setUp(self): - # XXX there's a conceptual/design - # mistake here. - # If we're testing just attrs after init, - # init shold not be doing so much side effects. - - # for instance: - # We have to TOUCH a keys file because - # we're triggerig the key checks FROM - # the constructor. me not like that, - # key checker should better be called explicitelly. - - # XXX change to keys_checker invocation - # (see config_checker) - - keyfiles = (eipspecs.provider_ca_path(domain=self.provider), - eipspecs.client_cert_path(domain=self.provider)) - for filepath in keyfiles: - self.touch(filepath) - self.chmod600(filepath) - - # we init the manager with only - # some methods mocked - self.manager = Mock(name="openvpnmanager_mock") - self.con = MockedEIPConnection() - self.con.provider = self.provider - - # XXX watch out. This sometimes is throwing the following error: - # NoSuchProcess: process no longer exists (pid=6571) - # because of a bad implementation of _check_if_running_instance - - self.con.run_openvpn_checks() - - def tearDown(self): - pass - - def doCleanups(self): - super(BaseLeapTest, self).doCleanups() - self.cleanupSocketDir() - del self.con - - def cleanupSocketDir(self): - ptt = ('/tmp/leap-tmp*') - for tmpdir in glob.glob(ptt): - shutil.rmtree(tmpdir) - - # - # tests - # - - def test_vpnconnection_defaults(self): - """ - default attrs as expected - """ - con = self.con - self.assertEqual(con.autostart, True) - # XXX moar! - - def test_ovpn_command(self): - """ - set_ovpn_command called - """ - self.assertEqual(self.con.command, - "mock_command") - self.assertEqual(self.con.args, - [1, 2, 3]) - - # config checks - - def test_config_checked_called(self): - # XXX this single test is taking half of the time - # needed to run tests. (roughly 3 secs for this only) - # We should modularize and inject Mocks on more places. - - oldcon = self.con - del(self.con) - config_checker = Mock() - self.con = MockedEIPConnection(config_checker=config_checker) - self.assertTrue(config_checker.called) - self.con.run_checks() - self.con.config_checker.run_all.assert_called_with( - skip_download=False) - - # XXX test for cert_checker also - self.con = oldcon - - # connect/disconnect calls - - def test_disconnect(self): - """ - disconnect method calls private and changes status - """ - self.con._disconnect = Mock( - name="_disconnect") - - # first we set status to connected - self.con.status.set_current(self.con.status.CONNECTED) - self.assertEqual(self.con.status.current, - self.con.status.CONNECTED) - - # disconnect - self.con.terminate_openvpn_connection = Mock() - self.con.disconnect() - self.con.terminate_openvpn_connection.assert_called_once_with( - shutdown=False) - self.con.terminate_openvpn_connection = Mock() - self.con.disconnect(shutdown=True) - self.con.terminate_openvpn_connection.assert_called_once_with( - shutdown=True) - - # new status should be disconnected - # XXX this should evolve and check no errors - # during disconnection - self.assertEqual(self.con.status.current, - self.con.status.DISCONNECTED) - - def test_connect(self): - """ - connect calls _launch_openvpn private - """ - self.con._launch_openvpn = Mock() - self.con.connect() - self.con._launch_openvpn.assert_called_once_with() - - # XXX tests breaking here ... - - def test_good_poll_connection_state(self): - """ - """ - #@patch -- - # self.manager.get_connection_state - - #XXX review this set of poll_state tests - #they SHOULD NOT NEED TO MOCK ANYTHING IN THE - #lower layers!! -- status, vpn_manager.. - #right now we're testing implementation, not - #behavior!!! - good_state = ["1345466946", "unknown_state", "ok", - "192.168.1.1", "192.168.1.100"] - self.con.get_connection_state = Mock(return_value=good_state) - self.con.status.set_vpn_state = Mock() - - state = self.con.poll_connection_state() - good_state[1] = "disconnected" - final_state = tuple(good_state) - self.con.status.set_vpn_state.assert_called_with("unknown_state") - self.assertEqual(state, final_state) - - # TODO between "good" and "bad" (exception raised) cases, - # we can still test for malformed states and see that only good - # states do have a change (and from only the expected transition - # states). - - def test_bad_poll_connection_state(self): - """ - get connection state raises ConnectionRefusedError - state is None - """ - self.con.get_connection_state = Mock( - side_effect=ConnectionRefusedError('foo!')) - state = self.con.poll_connection_state() - self.assertEqual(state, None) - - - # XXX more things to test: - # - called config routines during initz. - # - raising proper exceptions with no config - # - called proper checks on config / permissions - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/eip/tests/test_openvpnconnection.py b/src/leap/eip/tests/test_openvpnconnection.py deleted file mode 100644 index 95bfb2f0..00000000 --- a/src/leap/eip/tests/test_openvpnconnection.py +++ /dev/null @@ -1,161 +0,0 @@ -import logging -import os -import platform -import psutil -import shutil -#import socket - -logging.basicConfig() -logger = logging.getLogger(name=__name__) - -try: - import unittest2 as unittest -except ImportError: - import unittest - -from mock import Mock, patch # MagicMock - -from leap.eip import config as eipconfig -from leap.eip import openvpnconnection -from leap.eip import exceptions as eipexceptions -from leap.eip.udstelnet import UDSTelnet -from leap.testing.basetest import BaseLeapTest - -_system = platform.system() - - -class NotImplementedError(Exception): - pass - - -mock_UDSTelnet = Mock(spec=UDSTelnet) -# XXX cautious!!! -# this might be fragile right now (counting a global -# reference of calls I think. -# investigate this other form instead: -# http://www.voidspace.org.uk/python/mock/patch.html#start-and-stop - -# XXX redo after merge-refactor - - -@patch('openvpnconnection.OpenVPNConnection.connect_to_management') -class MockedOpenVPNConnection(openvpnconnection.OpenVPNConnection): - def __init__(self, *args, **kwargs): - self.mock_UDSTelnet = Mock() - super(MockedOpenVPNConnection, self).__init__( - *args, **kwargs) - self.tn = self.mock_UDSTelnet(self.host, self.port) - - def connect_to_management(self): - #print 'patched connect' - self.tn = mock_UDSTelnet(self.host, port=self.port) - - -class OpenVPNConnectionTest(BaseLeapTest): - - __name__ = "vpnconnection_tests" - - def setUp(self): - # XXX this will have to change for win, host=localhost - host = eipconfig.get_socket_path() - self.host = host - self.manager = MockedOpenVPNConnection(host=host) - - def tearDown(self): - pass - - def doCleanups(self): - super(BaseLeapTest, self).doCleanups() - self.cleanupSocketDir() - - def cleanupSocketDir(self): - # remove the socket folder. - # XXX only if posix. in win, host is localhost, so nothing - # has to be done. - if self.host: - folder, fpath = os.path.split(self.host) - try: - assert folder.startswith('/tmp/leap-tmp') # safety check - shutil.rmtree(folder) - except: - self.fail("could not remove temp file") - - del self.manager - - # - # tests - # - - def test_detect_vpn(self): - # XXX review, not sure if captured all the logic - # while fixing. kali. - openvpn_connection = openvpnconnection.OpenVPNConnection() - - with patch.object(psutil, "process_iter") as mocked_psutil: - mocked_process = Mock() - mocked_process.name = "openvpn" - mocked_process.cmdline = ["openvpn", "-foo", "-bar", "-gaaz"] - mocked_psutil.return_value = [mocked_process] - with self.assertRaises(eipexceptions.OpenVPNAlreadyRunning): - openvpn_connection._check_if_running_instance() - - openvpn_connection._check_if_running_instance() - - @unittest.skipIf(_system == "Windows", "lin/mac only") - def test_lin_mac_default_init(self): - """ - check default host for management iface - """ - self.assertTrue(self.manager.host.startswith('/tmp/leap-tmp')) - self.assertEqual(self.manager.port, 'unix') - - @unittest.skipUnless(_system == "Windows", "win only") - def test_win_default_init(self): - """ - check default host for management iface - """ - # XXX should we make the platform specific switch - # here or in the vpn command string building? - self.assertEqual(self.manager.host, 'localhost') - self.assertEqual(self.manager.port, 7777) - - def test_port_types_init(self): - oldmanager = self.manager - self.manager = MockedOpenVPNConnection(port="42") - self.assertEqual(self.manager.port, 42) - self.manager = MockedOpenVPNConnection() - self.assertEqual(self.manager.port, "unix") - self.manager = MockedOpenVPNConnection(port="bad") - self.assertEqual(self.manager.port, None) - self.manager = oldmanager - - def test_uds_telnet_called_on_connect(self): - self.manager.connect_to_management() - mock_UDSTelnet.assert_called_with( - self.manager.host, - port=self.manager.port) - - @unittest.skip - def test_connect(self): - raise NotImplementedError - # XXX calls close - # calls UDSTelnet mock. - - # XXX - # tests to write: - # UDSTelnetTest (for real?) - # HAVE A LOOK AT CORE TESTS FOR TELNETLIB. - # very illustrative instead... - - # - raise MissingSocket - # - raise ConnectionRefusedError - # - test send command - # - tries connect - # - ... tries? - # - ... calls _seek_to_eof - # - ... read_until --> return value - # - ... - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/eip/udstelnet.py b/src/leap/eip/udstelnet.py deleted file mode 100644 index 18e927c2..00000000 --- a/src/leap/eip/udstelnet.py +++ /dev/null @@ -1,38 +0,0 @@ -import os -import socket -import telnetlib - -from leap.eip import exceptions as eip_exceptions - - -class UDSTelnet(telnetlib.Telnet): - """ - a telnet-alike class, that can listen - on unix domain sockets - """ - - def open(self, host, port=23, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): - """Connect to a host. If port is 'unix', it - will open a connection over unix docmain sockets. - - The optional second argument is the port number, which - defaults to the standard telnet port (23). - - Don't try to reopen an already connected instance. - """ - self.eof = 0 - self.host = host - self.port = port - self.timeout = timeout - - if self.port == "unix": - # unix sockets spoken - if not os.path.exists(self.host): - raise eip_exceptions.MissingSocketError - self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - try: - self.sock.connect(self.host) - except socket.error: - raise eip_exceptions.ConnectionRefusedError - else: - self.sock = socket.create_connection((host, port), timeout) diff --git a/src/leap/email/__init__.py b/src/leap/email/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/email/smtp/README b/src/leap/email/smtp/README deleted file mode 100644 index 2b2a1180..00000000 --- a/src/leap/email/smtp/README +++ /dev/null @@ -1,43 +0,0 @@ -Leap SMTP Relay -=============== - -Outgoing mail workflow: - - * LEAP client runs a thin SMTP proxy on the user's device, bound to - localhost. - * User's MUA is configured outgoing SMTP to localhost - * When SMTP proxy receives an email from MUA - * SMTP proxy queries Key Manager for the user's private key and public - keys of all recipients - * Message is signed by sender and encrypted to recipients. - * If recipient's key is missing, email goes out in cleartext (unless - user has configured option to send only encrypted email) - * Finally, message is relayed to provider's SMTP relay - - -Dependencies ------------- - -Leap SMTP Relay depends on the following python libraries: - - * Twisted 12.3.0 [1] - * zope.interface 4.0.3 [2] - -[1] http://pypi.python.org/pypi/Twisted/12.3.0 -[2] http://pypi.python.org/pypi/zope.interface/4.0.3 - - -How to run ----------- - -To launch the SMTP relay, run the following command: - - twistd -y smtprelay.tac - - -Running tests -------------- - -Tests are run using Twisted's Trial API, like this: - - trial leap.email.smtp.tests diff --git a/src/leap/email/smtp/__init__.py b/src/leap/email/smtp/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/email/smtp/smtprelay.py b/src/leap/email/smtp/smtprelay.py deleted file mode 100644 index fdb8eb91..00000000 --- a/src/leap/email/smtp/smtprelay.py +++ /dev/null @@ -1,207 +0,0 @@ -import re -import gnupg -from zope.interface import implements -from StringIO import StringIO -from twisted.mail import smtp -from twisted.internet.protocol import ServerFactory -from twisted.internet import reactor -from twisted.internet import defer -from twisted.application import internet, service -from twisted.python import log -from email.Header import Header - - -class SMTPFactory(ServerFactory): - """ - Factory for an SMTP server with encrypted relaying capabilities. - """ - - def __init__(self, gpg=None): - self._gpg = gpg - - def buildProtocol(self, addr): - "Return a protocol suitable for the job." - # TODO: use ESMTP here. - smtpProtocol = smtp.SMTP(SMTPDelivery(self._gpg)) - smtpProtocol.factory = self - return smtpProtocol - - -class SMTPDelivery(object): - """ - Validate email addresses and handle message delivery. - """ - - implements(smtp.IMessageDelivery) - - def __init__(self, gpg=None): - if gpg: - self._gpg = gpg - else: - self._gpg = GPGWrapper() - - def receivedHeader(self, helo, origin, recipients): - myHostname, clientIP = helo - headerValue = "by %s from %s with ESMTP ; %s" % ( - myHostname, clientIP, smtp.rfc822date()) - # email.Header.Header used for automatic wrapping of long lines - return "Received: %s" % Header(headerValue) - - def validateTo(self, user): - """Assert existence of and trust on recipient's GPG public key.""" - # try to find recipient's public key - try: - # this will raise an exception if key is not found - trust = self._gpg.find_key(user.dest.addrstr)['trust'] - # if key is not ultimatelly trusted, then the message will not - # be encrypted. So, we check for this below - #if trust != 'u': - # raise smtp.SMTPBadRcpt(user) - log.msg("Accepting mail for %s..." % user.dest) - return lambda: EncryptedMessage(user, gpg=self._gpg) - except LookupError: - raise smtp.SMTPBadRcpt(user) - - def validateFrom(self, helo, originAddress): - # accept mail from anywhere. To reject an address, raise - # smtp.SMTPBadSender here. - return originAddress - - -class EncryptedMessage(): - """ - Receive plaintext from client, encrypt it and send message to a - recipient. - """ - implements(smtp.IMessage) - - SMTP_HOSTNAME = "mail.riseup.net" - SMTP_PORT = 25 - - def __init__(self, user, gpg=None): - self.user = user - self.getSMTPInfo() - self.lines = [] - if gpg: - self._gpg = gpg - else: - self._gpg = GPGWrapper() - - def lineReceived(self, line): - """Store email DATA lines as they arrive.""" - self.lines.append(line) - - def eomReceived(self): - """Encrypt and send message.""" - log.msg("Message data complete.") - self.lines.append('') # add a trailing newline - self.parseMessage() - try: - self.encrypt() - return self.sendMessage() - except LookupError: - return None - - def parseMessage(self): - """Separate message headers from body.""" - sep = self.lines.index('') - self.headers = self.lines[:sep] - self.body = self.lines[sep + 1:] - - def connectionLost(self): - log.msg("Connection lost unexpectedly!") - log.err() - # unexpected loss of connection; don't save - self.lines = [] - - def sendSuccess(self, r): - log.msg(r) - - def sendError(self, e): - log.msg(e) - log.err() - - def prepareHeader(self): - self.headers.insert(1, "From: %s" % self.user.orig.addrstr) - self.headers.insert(2, "To: %s" % self.user.dest.addrstr) - self.headers.append('') - - def sendMessage(self): - self.prepareHeader() - msg = '\n'.join(self.headers + [self.cyphertext]) - d = defer.Deferred() - factory = smtp.ESMTPSenderFactory(self.smtp_username, - self.smtp_password, - self.smtp_username, - self.user.dest.addrstr, - StringIO(msg), - d) - # the next call is TSL-powered! - reactor.connectTCP(self.SMTP_HOSTNAME, self.SMTP_PORT, factory) - d.addCallback(self.sendSuccess) - d.addErrback(self.sendError) - return d - - def encrypt(self, always_trust=True): - # TODO: do not "always trust" here. - fp = self._gpg.find_key(self.user.dest.addrstr)['fingerprint'] - log.msg("Encrypting to %s" % fp) - self.cyphertext = str(self._gpg.encrypt('\n'.join(self.body), [fp], - always_trust=always_trust)) - - # this will be replaced by some other mechanism of obtaining credentials - # for SMTP server. - def getSMTPInfo(self): - #f = open('/media/smtp-info.txt', 'r') - #self.smtp_host = f.readline().rstrip() - #self.smtp_port = f.readline().rstrip() - #self.smtp_username = f.readline().rstrip() - #self.smtp_password = f.readline().rstrip() - #f.close() - self.smtp_host = '' - self.smtp_port = '' - self.smtp_username = '' - self.smtp_password = '' - - -class GPGWrapper(): - """ - This is a temporary class for handling GPG requests, and should be - replaced by a more general class used throughout the project. - """ - - GNUPG_HOME = "~/.config/leap/gnupg" - GNUPG_BINARY = "/usr/bin/gpg" # TODO: change this based on OS - - def __init__(self, gpghome=GNUPG_HOME, gpgbinary=GNUPG_BINARY): - self.gpg = gnupg.GPG(gnupghome=gpghome, gpgbinary=gpgbinary) - - def find_key(self, email): - """ - Find user's key based on their email. - """ - for key in self.gpg.list_keys(): - for uid in key['uids']: - if re.search(email, uid): - return key - raise LookupError("GnuPG public key for %s not found!" % email) - - def encrypt(self, data, recipient, always_trust=True): - # TODO: do not 'always_trust'. - return self.gpg.encrypt(data, recipient, always_trust=always_trust) - - def decrypt(self, data): - return self.gpg.decrypt(data) - - def import_keys(self, data): - return self.gpg.import_keys(data) - - -# service configuration -port = 25 -factory = SMTPFactory() - -# these enable the use of this service with twistd -application = service.Application("LEAP SMTP Relay") -service = internet.TCPServer(port, factory) -service.setServiceParent(application) diff --git a/src/leap/email/smtp/tests/185CA770.key b/src/leap/email/smtp/tests/185CA770.key deleted file mode 100644 index 587b4164..00000000 --- a/src/leap/email/smtp/tests/185CA770.key +++ /dev/null @@ -1,79 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -lQIVBFCJNL4BEADFsI1TCD4yq7ZqL7VhdVviTuX6JUps8/mVEhRVOZhojLcTYaqQ -gs6T6WabRxcK7ymOnf4K8NhYdz6HFoJN46BT87etokx7J/Sl2OhpiqBQEY+jW8Rp -+3MSGrGmvFw0s1lGrz/cXzM7UNgWSTOnYZ5nJS1veMhy0jseZOUK7ekp2oEDjGZh -pzgd3zICCR2SvlpLIXB2Nr/CUcuRWTcc5LlKmbjMybu0E/uuY14st3JL+7qI6QX0 -atFm0VhFVpagOl0vWKxakUx4hC7j1wH2ADlCvSZPG0StSLUyHkJx3UPsmYxOZFao -ATED3Okjwga6E7PJEbzyqAkvzw/M973kaZCUSH75ZV0cQnpdgXV3DK1gSa3d3gug -W1lE0V7pwnN2NTOYfBMi+WloCs/bp4iZSr4QP1duZ3IqKraeBDCk7MoFo4A9Wk07 -kvqPwF9IBgatu62WVEZIzwyViN+asFUGfgp+8D7gtnlWAw0V6y/lSTzyl+dnLP98 -Hfr2eLBylFs+Kl3Pivpg2uHw09LLCrjeLEN3dj9SfBbA9jDIo9Zhs1voiIK/7Shx -E0BRJaBgG3C4QaytYEu7RFFOKuvBai9w2Y5OfsKFo8rA7v4dxFFDvzKGujCtNnwf -oyaGlZmMBU5MUmHUNiG8ON21COZBtK5oMScuY1VC9CQonj3OClg3IbU9SQARAQAB -/gNlAkdOVQG0JGRyZWJzIChncGcgdGVzdCBrZXkpIDxkcmVic0BsZWFwLnNlPokC -OAQTAQIAIgUCUIk0vgIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQty9e -xhhcp3Bdhw//bdPUNbp6rgIjRRuwYvGJ6IuiFuFWJQ0m3iAuuAoZo5GHAPqZAuGk -dMVYu0dtCtZ68MJ/QpjBCT9RRL+mgIgfLfUSj2ZknP4nb6baiG5u28l0KId/e5IC -iQKBnIsjxKxhLBVHSzRaS1P+vZeF2C2R9XyNy0eCnAwyCMcD0R8TVROGQ7i4ZQsM -bMj1LPpOwhV/EGp23nD+upWOVbn/wQHOYV2kMiA/8fizmWRIWsV4/68uMA+WDP4L -40AnJ0fcs04f9deM9P6pjlm00VD7qklYEGw6Mpr2g/M73kGh1nlAv+ImQBGlLMle -RXyzHY3WAhzmRKWO4koFuKeR9Q0EMzk2R4/kuagdWEpM+bhwE4xPV1tPZhn9qFTz -pQD4p/VT4qNQKOD0+aTFWre65Rt2cFFMLI7UmEHNLi0NB9JCIAi4+l+b9WQNlmaO -C8EhOGwRzmehUyHmXM3BNW28MnyKFJ7bBFMd7uJz+vAPOrr6OzuNvVCv2I2ICkTs -ihIj/zw5GXxkPO7YbMu9rKG0nKF1N3JB1gUJ78DHmhbjeaGSvHw85sPD0/1dPZK4 -8Gig8i62aCxf8OlJPlt8ZhBBolzs6ITUNa75Rw9fJsj3UWuv2VFaIuR57bFWmY3s -A9KPgdf7jVQlAZKlVyli7IkyaZmxDZNFQoTdIC9uo0aggIDP8zKv0n2dBz4EUIk0 -vgEQAOO8BAR7sBdqj2RRMRNeWSA4S9GuHfV3YQARnqYsbITs1jRgAo7jx9Z5C80c -ZOxOUVK7CJjtTqU0JB9QP/zwV9hk5i6y6aQTysclQyTNN10aXu/3zJla5Duhz+Cs -+5UcVAmNJX9FgTMVvhKDEIY/LNmb9MoBLMut1CkDx+WPCV45WOIBCDdj2HpIjie4 -phs0/65SWjPiVg3WsFZljVxpJCGXP48Eet2bf8afYH1lx3sQMcNbyJACIPtz+YKz -c7jIKwKSWzg1VyYikbk9eWCxcz6VKNJKi94YH9c7U8X3TdZ8G0kGYUldjYDvesyl -nuQlcGCtSGKOAhrN/Bu2R0gpFgYl247u79CmjotefMdv8BGUDW6u9/Sep9xN3dW8 -S87h6M/tvs0ChlkDDpJedzCd7ThdikGvFRJfW/8sT/+qoTKskySQaDIeNJnxZuyK -wELLMBvCZGpamwmnkEGhvuZWq0h/DwyTs4QAE8OVHXJSM3UN7hM4lJIUh+sRKJ1F -AXXTdSY4cUNaS+OKtj2LJ85zFqhfAZ4pFwLCgYbJtU5hej2LnMJNbYcSkjxbk+c5 -IjkoZRF+ExjZlc0VLYNT57ZriwZ/pX42ofjOyMR/dkHQuFik/4K7v1ZemfaTdm07 -SEMBknR6OZsy/5+viEtXiih3ptTMaT9row+g+cFoxdXkisKvABEBAAH+AwMCIlVK -Xs3x0Slgwx03cTNIoWXmishkPCJlEEdcjldz2VyQF9hjdp1VIe+npI26chKwCZqm -U8yYbJh4UBrugUUzKKd4EfnmKfu+/BsJciFRVKwBtiolIiUImzcHPWktYLwo9yzX -W42teShXXVgWmsJN1/6FqJdsLg8dxWesXMKoaNF4n1P7zx6vKBmDHTRz7PToaI/d -5/nKrjED7ZT1h+qR5i9UUgbvF0ySp8mlqk/KNqHUSLDB9kf/JDg4XVtPHGGd9Ik/ -60UJ7aDfohi4Z0VgwWmfLBwcQ3It+ENtnPFufH3WHW8c1UA4wVku9tOTqyrRG6tP -TZGiRfuwsv7Hq3pWT6rntbDkTiVgESM4C1fiZblc98iWUKGXSHqm+te1TwXOUCci -J/gryXcjQFM8A0rwA/m+EvsoWuzoqIl3x++p3/3/mGux6UD4O7OhJNRVRz+8Mhq1 -ksrR9XkQzpq3Yv3ulTHz7l+WCRRXxw5+XWAkRHHF47Vf/na38NJQHcsCBbRIuLYR -wBzS48cYzYkF6VejKThdQmdYJ0/fUrlUBCAJWgrfqCihFLDa1s4jJ16/fqi8a97Y -4raVy2hrF2vFc/wet13hsaddVn4rPRAMDEGdgEmJX7MmU1emT/yaIG9lvjMpI2c5 -ADXGF2yYYa7H8zPIFyHU1RSavlT0S/K9yzIZvv+jA5KbNeGp+WWFT8MLZs0IhoCZ -d1EgLUYAt7LPUSm2lBy1w/IL+VtYuyn/UVFo2xWiHd1ABiNWl1ji3X9Ki5613QqH -bvn4z46voCzdZ02rYkAwrdqDr92fiBR8ctwA0AudaG6nf2ztmFKtM3E/RPMkPgKF -8NHYc7QxS2jruJxXBtjRBMtoIaZ0+AXUO6WuEJrDLDHWaM08WKByQMm808xNCbRr -CpiK8qyR3SwkfaOMCp22mqViirQ2KfuVvBpBT2pBYlgDKs50nE+stDjUMv+FDKAo -5NtiyPfNtaBOYnXAEQb/hjjW5bKq7JxHSxIWAYKbNKIWgftJ3ACZAsBMHfaOCFNH -+XLojAoxOI+0zbN6FtjN+YMU1XrLd6K49v7GEiJQZVQSfLCecVDhDU9paNROA/Xq -/3nDCTKhd3stTPnc8ymLAwhTP0bSoFh/KtU96D9ZMC2cu9XZ+UcSQYES/ncZWcLw -wTKrt+VwBG1z3DbV2O0ruUiXTLcZMsrwbUSDx1RVhmKZ0i42AttMdauFQ9JaX2CS -2ddqFBS1b4X6+VCy44KkpdXsmp0NWMgm/PM3PTisCxrha7bI5/LqfXG0b+GuIFb4 -h/lEA0Ae0gMgkzm3ePAPPVlRj7kFl5Osjxm3YVRW23WWGDRF5ywIROlBjbdozA0a -MyMgXlG9hhJseIpFveoiwqenNE5Wxg0yQbnhMUTKeCQ0xskG82P+c9bvDsevAQUR -uv1JAGGxDd1/4nk0M5m9/Gf4Bn0uLAz29LdMg0FFUvAm2ol3U3uChm7OISU8dqFy -JdCFACKBMzAREiXfgH2TrTxAhpy5uVcUSQV8x5J8qJ/mUoTF1WE3meXEm9CIvIAF -Mz49KKebLS3zGFixMcKLAOKA+s/tUWO7ZZoJyQjvQVerLyDo6UixVb11LQUJQOXb -ZIuSKV7deCgBDQ26C42SpF3rHfEQa7XH7j7tl1IIW/9DfYJYVQHaz1NTq6zcjWS2 -e+cUexBPhxbadGn0zelXr6DLJqQT7kaVeYOHlkYUHkZXdHE4CWoHqOboeB02uM/A -e7nge1rDi57ySrsF4AVl59QJYBPR43AOVbCJAh8EGAECAAkFAlCJNL4CGwwACgkQ -ty9exhhcp3DetA/8D/IscSBlWY3TjCD2P7t3+X34USK8EFD3QJse9dnCWOLcskFQ -IoIfhRM752evFu2W9owEvxSQdG+otQAOqL72k1EH2g7LsADuV8I4LOYOnLyeIE9I -b+CFPBkmzTEzrdYp6ITUU7qqgkhcgnltKGHoektIjxE8gtxCKEdyxkzazum6nCQQ -kSBZOXVU3ezm+A2QHHP6XT1GEbdKbJ0tIuJR8ADu08pBx2c/LDBBreVStrrt1Dbz -uR+U8MJsfLVcYX/Rw3V+KA24oLRzg91y3cfi3sNU/kmd5Cw42Tj00B+FXQny51Mq -s4KyqHobj62II68eL5HRB2pcGsoaedQyxu2cYSeVyarBOiUPNYkoGDJoKdDyZRIB -NNK0W+ASTf0zeHhrY/okt1ybTVtvbt6wkTEbKVePUaYmNmhre1cAj4uNwFzYjkzJ -cm+8XWftD+TV8cE5DyVdnF00SPDuPzodRAPXaGpQUMLkE4RPr1TAwcuoPH9aFHZ/ -se6rw6TQHLd0vMk0U/DocikXpSJ1N6caE3lRwI/+nGfXNiCr8MIdofgkBeO86+G7 -k0UXS4v5FKk1nwTyt4PkFJDvAJX6rZPxIZ9NmtA5ao5vyu1DT5IhoXgDzwurAe8+ -R+y6gtA324hXIweFNt7SzYPfI4SAjunlmm8PIBf3owBrk3j+w6EQoaCreK4= -=6HcJ ------END PGP PRIVATE KEY BLOCK----- diff --git a/src/leap/email/smtp/tests/185CA770.pub b/src/leap/email/smtp/tests/185CA770.pub deleted file mode 100644 index 38af19f8..00000000 --- a/src/leap/email/smtp/tests/185CA770.pub +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -mQINBFCJNL4BEADFsI1TCD4yq7ZqL7VhdVviTuX6JUps8/mVEhRVOZhojLcTYaqQ -gs6T6WabRxcK7ymOnf4K8NhYdz6HFoJN46BT87etokx7J/Sl2OhpiqBQEY+jW8Rp -+3MSGrGmvFw0s1lGrz/cXzM7UNgWSTOnYZ5nJS1veMhy0jseZOUK7ekp2oEDjGZh -pzgd3zICCR2SvlpLIXB2Nr/CUcuRWTcc5LlKmbjMybu0E/uuY14st3JL+7qI6QX0 -atFm0VhFVpagOl0vWKxakUx4hC7j1wH2ADlCvSZPG0StSLUyHkJx3UPsmYxOZFao -ATED3Okjwga6E7PJEbzyqAkvzw/M973kaZCUSH75ZV0cQnpdgXV3DK1gSa3d3gug -W1lE0V7pwnN2NTOYfBMi+WloCs/bp4iZSr4QP1duZ3IqKraeBDCk7MoFo4A9Wk07 -kvqPwF9IBgatu62WVEZIzwyViN+asFUGfgp+8D7gtnlWAw0V6y/lSTzyl+dnLP98 -Hfr2eLBylFs+Kl3Pivpg2uHw09LLCrjeLEN3dj9SfBbA9jDIo9Zhs1voiIK/7Shx -E0BRJaBgG3C4QaytYEu7RFFOKuvBai9w2Y5OfsKFo8rA7v4dxFFDvzKGujCtNnwf -oyaGlZmMBU5MUmHUNiG8ON21COZBtK5oMScuY1VC9CQonj3OClg3IbU9SQARAQAB -tCRkcmVicyAoZ3BnIHRlc3Qga2V5KSA8ZHJlYnNAbGVhcC5zZT6JAjgEEwECACIF -AlCJNL4CGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJELcvXsYYXKdwXYcP -/23T1DW6eq4CI0UbsGLxieiLohbhViUNJt4gLrgKGaORhwD6mQLhpHTFWLtHbQrW -evDCf0KYwQk/UUS/poCIHy31Eo9mZJz+J2+m2ohubtvJdCiHf3uSAokCgZyLI8Ss -YSwVR0s0WktT/r2XhdgtkfV8jctHgpwMMgjHA9EfE1UThkO4uGULDGzI9Sz6TsIV -fxBqdt5w/rqVjlW5/8EBzmFdpDIgP/H4s5lkSFrFeP+vLjAPlgz+C+NAJydH3LNO -H/XXjPT+qY5ZtNFQ+6pJWBBsOjKa9oPzO95BodZ5QL/iJkARpSzJXkV8sx2N1gIc -5kSljuJKBbinkfUNBDM5NkeP5LmoHVhKTPm4cBOMT1dbT2YZ/ahU86UA+Kf1U+Kj -UCjg9PmkxVq3uuUbdnBRTCyO1JhBzS4tDQfSQiAIuPpfm/VkDZZmjgvBIThsEc5n -oVMh5lzNwTVtvDJ8ihSe2wRTHe7ic/rwDzq6+js7jb1Qr9iNiApE7IoSI/88ORl8 -ZDzu2GzLvayhtJyhdTdyQdYFCe/Ax5oW43mhkrx8PObDw9P9XT2SuPBooPIutmgs -X/DpST5bfGYQQaJc7OiE1DWu+UcPXybI91Frr9lRWiLkee2xVpmN7APSj4HX+41U -JQGSpVcpYuyJMmmZsQ2TRUKE3SAvbqNGoICAz/Myr9J9uQINBFCJNL4BEADjvAQE -e7AXao9kUTETXlkgOEvRrh31d2EAEZ6mLGyE7NY0YAKO48fWeQvNHGTsTlFSuwiY -7U6lNCQfUD/88FfYZOYusumkE8rHJUMkzTddGl7v98yZWuQ7oc/grPuVHFQJjSV/ -RYEzFb4SgxCGPyzZm/TKASzLrdQpA8fljwleOVjiAQg3Y9h6SI4nuKYbNP+uUloz -4lYN1rBWZY1caSQhlz+PBHrdm3/Gn2B9Zcd7EDHDW8iQAiD7c/mCs3O4yCsCkls4 -NVcmIpG5PXlgsXM+lSjSSoveGB/XO1PF903WfBtJBmFJXY2A73rMpZ7kJXBgrUhi -jgIazfwbtkdIKRYGJduO7u/Qpo6LXnzHb/ARlA1urvf0nqfcTd3VvEvO4ejP7b7N -AoZZAw6SXncwne04XYpBrxUSX1v/LE//qqEyrJMkkGgyHjSZ8WbsisBCyzAbwmRq -WpsJp5BBob7mVqtIfw8Mk7OEABPDlR1yUjN1De4TOJSSFIfrESidRQF103UmOHFD -WkvjirY9iyfOcxaoXwGeKRcCwoGGybVOYXo9i5zCTW2HEpI8W5PnOSI5KGURfhMY -2ZXNFS2DU+e2a4sGf6V+NqH4zsjEf3ZB0LhYpP+Cu79WXpn2k3ZtO0hDAZJ0ejmb -Mv+fr4hLV4ood6bUzGk/a6MPoPnBaMXV5IrCrwARAQABiQIfBBgBAgAJBQJQiTS+ -AhsMAAoJELcvXsYYXKdw3rQP/A/yLHEgZVmN04wg9j+7d/l9+FEivBBQ90CbHvXZ -wlji3LJBUCKCH4UTO+dnrxbtlvaMBL8UkHRvqLUADqi+9pNRB9oOy7AA7lfCOCzm -Dpy8niBPSG/ghTwZJs0xM63WKeiE1FO6qoJIXIJ5bShh6HpLSI8RPILcQihHcsZM -2s7pupwkEJEgWTl1VN3s5vgNkBxz+l09RhG3SmydLSLiUfAA7tPKQcdnPywwQa3l -Ura67dQ287kflPDCbHy1XGF/0cN1figNuKC0c4Pdct3H4t7DVP5JneQsONk49NAf -hV0J8udTKrOCsqh6G4+tiCOvHi+R0QdqXBrKGnnUMsbtnGEnlcmqwTolDzWJKBgy -aCnQ8mUSATTStFvgEk39M3h4a2P6JLdcm01bb27esJExGylXj1GmJjZoa3tXAI+L -jcBc2I5MyXJvvF1n7Q/k1fHBOQ8lXZxdNEjw7j86HUQD12hqUFDC5BOET69UwMHL -qDx/WhR2f7Huq8Ok0By3dLzJNFPw6HIpF6UidTenGhN5UcCP/pxn1zYgq/DCHaH4 -JAXjvOvhu5NFF0uL+RSpNZ8E8reD5BSQ7wCV+q2T8SGfTZrQOWqOb8rtQ0+SIaF4 -A88LqwHvPkfsuoLQN9uIVyMHhTbe0s2D3yOEgI7p5ZpvDyAX96MAa5N4/sOhEKGg -q3iu -=RChS ------END PGP PUBLIC KEY BLOCK----- diff --git a/src/leap/email/smtp/tests/__init__.py b/src/leap/email/smtp/tests/__init__.py deleted file mode 100644 index 022968e1..00000000 --- a/src/leap/email/smtp/tests/__init__.py +++ /dev/null @@ -1,215 +0,0 @@ -import os -import shutil -import tempfile -from leap.email.smtp.smtprelay import GPGWrapper -from twisted.trial import unittest -from leap.testing.basetest import BaseLeapTest - - -class OpenPGPTestCase(unittest.TestCase, BaseLeapTest): - - def setUp(self): - # mimic LeapBaseTest.setUpClass behaviour, because this is deprecated - # in Twisted: http://twistedmatrix.com/trac/ticket/1870 - self.old_path = os.environ['PATH'] - self.old_home = os.environ['HOME'] - self.tempdir = tempfile.mkdtemp(prefix="leap_tests-") - self.home = self.tempdir - bin_tdir = os.path.join( - self.tempdir, - 'bin') - os.environ["PATH"] = bin_tdir - os.environ["HOME"] = self.tempdir - # setup our own stuff - self.gnupg_home = self.tempdir + '/gnupg' - os.mkdir(self.gnupg_home) - self.email = 'leap@leap.se' - self._gpg = GPGWrapper(gpghome=self.gnupg_home) - - self.assertEqual(self._gpg.import_keys(PUBLIC_KEY).summary(), - '1 imported', "error importing public key") - self.assertEqual(self._gpg.import_keys(PRIVATE_KEY).summary(), - # note that gnupg does not return a successful import - # for private keys. Bug? - '0 imported', "error importing private key") - - def tearDown(self): - # mimic LeapBaseTest.tearDownClass behaviour - os.environ["PATH"] = self.old_path - os.environ["HOME"] = self.old_home - # safety check - assert self.tempdir.startswith('/tmp/leap_tests-') - shutil.rmtree(self.tempdir) - - def test_openpgp_encrypt_decrypt(self): - text = "simple raw text" - encrypted = str(self._gpg.encrypt(text, KEY_FINGERPRINT, - # TODO: handle always trust issue - always_trust=True)) - self.assertNotEqual(text, encrypted, "failed encrypting text") - decrypted = str(self._gpg.decrypt(encrypted)) - self.assertEqual(text, decrypted, "failed decrypting text") - - -# Key material for testing -KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF" -PUBLIC_KEY = """ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -mQINBFC9+dkBEADNRfwV23TWEoGc/x0wWH1P7PlXt8MnC2Z1kKaKKmfnglVrpOiz -iLWoiU58sfZ0L5vHkzXHXCBf6Eiy/EtUIvdiWAn+yASJ1mk5jZTBKO/WMAHD8wTO -zpMsFmWyg3xc4DkmFa9KQ5EVU0o/nqPeyQxNMQN7px5pPwrJtJFmPxnxm+aDkPYx -irDmz/4DeDNqXliazGJKw7efqBdlwTHkl9Akw2gwy178pmsKwHHEMOBOFFvX61AT -huKqHYmlCGSliwbrJppTG7jc1/ls3itrK+CWTg4txREkSpEVmfcASvw/ZqLbjgfs -d/INMwXnR9U81O8+7LT6yw/ca4ppcFoJD7/XJbkRiML6+bJ4Dakiy6i727BzV17g -wI1zqNvm5rAhtALKfACha6YO43aJzairO4II1wxVHvRDHZn2IuKDDephQ3Ii7/vb -hUOf6XCSmchkAcpKXUOvbxm1yfB1LRa64mMc2RcZxf4mW7KQkulBsdV5QG2276lv -U2UUy2IutXcGP5nXC+f6sJJGJeEToKJ57yiO/VWJFjKN8SvP+7AYsQSqINUuEf6H -T5gCPCraGMkTUTPXrREvu7NOohU78q6zZNaL3GW8ai7eSeANSuQ8Vzffx7Wd8Y7i -Pw9sYj0SMFs1UgjbuL6pO5ueHh+qyumbtAq2K0Bci0kqOcU4E9fNtdiovQARAQAB -tBxMZWFwIFRlc3QgS2V5IDxsZWFwQGxlYXAuc2U+iQI3BBMBCAAhBQJQvfnZAhsD -BQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEC9FXigk0Y3fT7EQAKH3IuRniOpb -T/DDIgwwjz3oxB/W0DDMyPXowlhSOuM0rgGfntBpBb3boezEXwL86NPQxNGGruF5 -hkmecSiuPSvOmQlqlS95NGQp6hNG0YaKColh+Q5NTspFXCAkFch9oqUje0LdxfSP -QfV9UpeEvGyPmk1I9EJV/YDmZ4+Djge1d7qhVZInz4Rx1NrSyF/Tc2EC0VpjQFsU -Y9Kb2YBBR7ivG6DBc8ty0jJXi7B4WjkFcUEJviQpMF2dCLdonCehYs1PqsN1N7j+ -eFjQd+hqVMJgYuSGKjvuAEfClM6MQw7+FmFwMyLgK/Ew/DttHEDCri77SPSkOGSI -txCzhTg6798f6mJr7WcXmHX1w1Vcib5FfZ8vTDFVhz/XgAgArdhPo9V6/1dgSSiB -KPQ/spsco6u5imdOhckERE0lnAYvVT6KE81TKuhF/b23u7x+Wdew6kK0EQhYA7wy -7LmlaNXc7rMBQJ9Z60CJ4JDtatBWZ0kNrt2VfdDHVdqBTOpl0CraNUjWE5YMDasr -K2dF5IX8D3uuYtpZnxqg0KzyLg0tzL0tvOL1C2iudgZUISZNPKbS0z0v+afuAAnx -2pTC3uezbh2Jt8SWTLhll4i0P4Ps5kZ6HQUO56O+/Z1cWovX+mQekYFmERySDR9n -3k1uAwLilJmRmepGmvYbB8HloV8HqwgguQINBFC9+dkBEAC0I/xn1uborMgDvBtf -H0sEhwnXBC849/32zic6udB6/3Efk9nzbSpL3FSOuXITZsZgCHPkKarnoQ2ztMcS -sh1ke1C5gQGms75UVmM/nS+2YI4vY8OX/GC/on2vUyncqdH+bR6xH5hx4NbWpfTs -iQHmz5C6zzS/kuabGdZyKRaZHt23WQ7JX/4zpjqbC99DjHcP9BSk7tJ8wI4bkMYD -uFVQdT9O6HwyKGYwUU4sAQRAj7XCTGvVbT0dpgJwH4RmrEtJoHAx4Whg8mJ710E0 -GCmzf2jqkNuOw76ivgk27Kge+Hw00jmJjQhHY0yVbiaoJwcRrPKzaSjEVNgrpgP3 -lXPRGQArgESsIOTeVVHQ8fhK2YtTeCY9rIiO+L0OX2xo9HK7hfHZZWL6rqymXdyS -fhzh/f6IPyHFWnvj7Brl7DR8heMikygcJqv+ed2yx7iLyCUJ10g12I48+aEj1aLe -dP7lna32iY8/Z0SHQLNH6PXO9SlPcq2aFUgKqE75A/0FMk7CunzU1OWr2ZtTLNO1 -WT/13LfOhhuEq9jTyTosn0WxBjJKq18lnhzCXlaw6EAtbA7CUwsD3CTPR56aAXFK -3I7KXOVAqggrvMe5Tpdg5drfYpI8hZovL5aAgb+7Y5ta10TcJdUhS5K3kFAWe/td -U0cmWUMDP1UMSQ5Jg6JIQVWhSwARAQABiQIfBBgBCAAJBQJQvfnZAhsMAAoJEC9F -Xigk0Y3fRwsP/i0ElYCyxeLpWJTwo1iCLkMKz2yX1lFVa9nT1BVTPOQwr/IAc5OX -NdtbJ14fUsKL5pWgW8OmrXtwZm1y4euI1RPWWubG01ouzwnGzv26UcuHeqC5orZj -cOnKtL40y8VGMm8LoicVkRJH8blPORCnaLjdOtmA3rx/v2EXrJpSa3AhOy0ZSRXk -ZSrK68AVNwamHRoBSYyo0AtaXnkPX4+tmO8X8BPfj125IljubvwZPIW9VWR9UqCE -VPfDR1XKegVb6VStIywF7kmrknM1C5qUY28rdZYWgKorw01hBGV4jTW0cqde3N51 -XT1jnIAa+NoXUM9uQoGYMiwrL7vNsLlyyiW5ayDyV92H/rIuiqhFgbJsHTlsm7I8 -oGheR784BagAA1NIKD1qEO9T6Kz9lzlDaeWS5AUKeXrb7ZJLI1TTCIZx5/DxjLqM -Tt/RFBpVo9geZQrvLUqLAMwdaUvDXC2c6DaCPXTh65oCZj/hqzlJHH+RoTWWzKI+ -BjXxgUWF9EmZUBrg68DSmI+9wuDFsjZ51BcqvJwxyfxtTaWhdoYqH/UQS+D1FP3/ -diZHHlzwVwPICzM9ooNTgbrcDzyxRkIVqsVwBq7EtzcvgYUyX53yG25Giy6YQaQ2 -ZtQ/VymwFL3XdUWV6B/hU4PVAFvO3qlOtdJ6TpE+nEWgcWjCv5g7RjXX -=MuOY ------END PGP PUBLIC KEY BLOCK----- -""" -PRIVATE_KEY = """ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -lQcYBFC9+dkBEADNRfwV23TWEoGc/x0wWH1P7PlXt8MnC2Z1kKaKKmfnglVrpOiz -iLWoiU58sfZ0L5vHkzXHXCBf6Eiy/EtUIvdiWAn+yASJ1mk5jZTBKO/WMAHD8wTO -zpMsFmWyg3xc4DkmFa9KQ5EVU0o/nqPeyQxNMQN7px5pPwrJtJFmPxnxm+aDkPYx -irDmz/4DeDNqXliazGJKw7efqBdlwTHkl9Akw2gwy178pmsKwHHEMOBOFFvX61AT -huKqHYmlCGSliwbrJppTG7jc1/ls3itrK+CWTg4txREkSpEVmfcASvw/ZqLbjgfs -d/INMwXnR9U81O8+7LT6yw/ca4ppcFoJD7/XJbkRiML6+bJ4Dakiy6i727BzV17g -wI1zqNvm5rAhtALKfACha6YO43aJzairO4II1wxVHvRDHZn2IuKDDephQ3Ii7/vb -hUOf6XCSmchkAcpKXUOvbxm1yfB1LRa64mMc2RcZxf4mW7KQkulBsdV5QG2276lv -U2UUy2IutXcGP5nXC+f6sJJGJeEToKJ57yiO/VWJFjKN8SvP+7AYsQSqINUuEf6H -T5gCPCraGMkTUTPXrREvu7NOohU78q6zZNaL3GW8ai7eSeANSuQ8Vzffx7Wd8Y7i -Pw9sYj0SMFs1UgjbuL6pO5ueHh+qyumbtAq2K0Bci0kqOcU4E9fNtdiovQARAQAB -AA/+JHtlL39G1wsH9R6UEfUQJGXR9MiIiwZoKcnRB2o8+DS+OLjg0JOh8XehtuCs -E/8oGQKtQqa5bEIstX7IZoYmYFiUQi9LOzIblmp2vxOm+HKkxa4JszWci2/ZmC3t -KtaA4adl9XVnshoQ7pijuCMUKB3naBEOAxd8s9d/JeReGIYkJErdrnVfNk5N71Ds -FmH5Ll3XtEDvgBUQP3nkA6QFjpsaB94FHjL3gDwum/cxzj6pCglcvHOzEhfY0Ddb -J967FozQTaf2JW3O+w3LOqtcKWpq87B7+O61tVidQPSSuzPjCtFF0D2LC9R/Hpky -KTMQ6CaKja4MPhjwywd4QPcHGYSqjMpflvJqi+kYIt8psUK/YswWjnr3r4fbuqVY -VhtiHvnBHQjz135lUqWvEz4hM3Xpnxydx7aRlv5NlevK8+YIO5oFbWbGNTWsPZI5 -jpoFBpSsnR1Q5tnvtNHauvoWV+XN2qAOBTG+/nEbDYH6Ak3aaE9jrpTdYh0CotYF -q7csANsDy3JvkAzeU6WnYpsHHaAjqOGyiZGsLej1UcXPFMosE/aUo4WQhiS8Zx2c -zOVKOi/X5vQ2GdNT9Qolz8AriwzsvFR+bxPzyd8V6ALwDsoXvwEYinYBKK8j0OPv -OOihSR6HVsuP9NUZNU9ewiGzte/+/r6pNXHvR7wTQ8EWLcEIAN6Zyrb0bHZTIlxt -VWur/Ht2mIZrBaO50qmM5RD3T5oXzWXi/pjLrIpBMfeZR9DWfwQwjYzwqi7pxtYx -nJvbMuY505rfnMoYxb4J+cpRXV8MS7Dr1vjjLVUC9KiwSbM3gg6emfd2yuA93ihv -Pe3mffzLIiQa4mRE3wtGcioC43nWuV2K2e1KjxeFg07JhrezA/1Cak505ab/tmvP -4YmjR5c44+yL/YcQ3HdFgs4mV+nVbptRXvRcPpolJsgxPccGNdvHhsoR4gwXMS3F -RRPD2z6x8xeN73Q4KH3bm01swQdwFBZbWVfmUGLxvN7leCdfs9+iFJyqHiCIB6Iv -mQfp8F0IAOwSo8JhWN+V1dwML4EkIrM8wUb4yecNLkyR6TpPH/qXx4PxVMC+vy6x -sCtjeHIwKE+9vqnlhd5zOYh7qYXEJtYwdeDDmDbL8oks1LFfd+FyAuZXY33DLwn0 -cRYsr2OEZmaajqUB3NVmj3H4uJBN9+paFHyFSXrH68K1Fk2o3n+RSf2EiX+eICwI -L6rqoF5sSVUghBWdNegV7qfy4anwTQwrIMGjgU5S6PKW0Dr/3iO5z3qQpGPAj5OW -ATqPWkDICLbObPxD5cJlyyNE2wCA9VVc6/1d6w4EVwSq9h3/WTpATEreXXxTGptd -LNiTA1nmakBYNO2Iyo3djhaqBdWjk+EIAKtVEnJH9FAVwWOvaj1RoZMA5DnDMo7e -SnhrCXl8AL7Z1WInEaybasTJXn1uQ8xY52Ua4b8cbuEKRKzw/70NesFRoMLYoHTO -dyeszvhoDHberpGRTciVmpMu7Hyi33rM31K9epA4ib6QbbCHnxkWOZB+Bhgj1hJ8 -xb4RBYWiWpAYcg0+DAC3w9gfxQhtUlZPIbmbrBmrVkO2GVGUj8kH6k4UV6kUHEGY -HQWQR0HcbKcXW81ZXCCD0l7ROuEWQtTe5Jw7dJ4/QFuqZnPutXVRNOZqpl6eRShw -7X2/a29VXBpmHA95a88rSQsL+qm7Fb3prqRmuMCtrUZgFz7HLSTuUMR867QcTGVh -cCBUZXN0IEtleSA8bGVhcEBsZWFwLnNlPokCNwQTAQgAIQUCUL352QIbAwULCQgH -AwUVCgkICwUWAgMBAAIeAQIXgAAKCRAvRV4oJNGN30+xEACh9yLkZ4jqW0/wwyIM -MI896MQf1tAwzMj16MJYUjrjNK4Bn57QaQW926HsxF8C/OjT0MTRhq7heYZJnnEo -rj0rzpkJapUveTRkKeoTRtGGigqJYfkOTU7KRVwgJBXIfaKlI3tC3cX0j0H1fVKX -hLxsj5pNSPRCVf2A5mePg44HtXe6oVWSJ8+EcdTa0shf03NhAtFaY0BbFGPSm9mA -QUe4rxugwXPLctIyV4uweFo5BXFBCb4kKTBdnQi3aJwnoWLNT6rDdTe4/nhY0Hfo -alTCYGLkhio77gBHwpTOjEMO/hZhcDMi4CvxMPw7bRxAwq4u+0j0pDhkiLcQs4U4 -Ou/fH+pia+1nF5h19cNVXIm+RX2fL0wxVYc/14AIAK3YT6PVev9XYEkogSj0P7Kb -HKOruYpnToXJBERNJZwGL1U+ihPNUyroRf29t7u8flnXsOpCtBEIWAO8Muy5pWjV -3O6zAUCfWetAieCQ7WrQVmdJDa7dlX3Qx1XagUzqZdAq2jVI1hOWDA2rKytnReSF -/A97rmLaWZ8aoNCs8i4NLcy9Lbzi9QtornYGVCEmTTym0tM9L/mn7gAJ8dqUwt7n -s24dibfElky4ZZeItD+D7OZGeh0FDuejvv2dXFqL1/pkHpGBZhEckg0fZ95NbgMC -4pSZkZnqRpr2GwfB5aFfB6sIIJ0HGARQvfnZARAAtCP8Z9bm6KzIA7wbXx9LBIcJ -1wQvOPf99s4nOrnQev9xH5PZ820qS9xUjrlyE2bGYAhz5Cmq56ENs7THErIdZHtQ -uYEBprO+VFZjP50vtmCOL2PDl/xgv6J9r1Mp3KnR/m0esR+YceDW1qX07IkB5s+Q -us80v5LmmxnWcikWmR7dt1kOyV/+M6Y6mwvfQ4x3D/QUpO7SfMCOG5DGA7hVUHU/ -Tuh8MihmMFFOLAEEQI+1wkxr1W09HaYCcB+EZqxLSaBwMeFoYPJie9dBNBgps39o -6pDbjsO+or4JNuyoHvh8NNI5iY0IR2NMlW4mqCcHEazys2koxFTYK6YD95Vz0RkA -K4BErCDk3lVR0PH4StmLU3gmPayIjvi9Dl9saPRyu4Xx2WVi+q6spl3ckn4c4f3+ -iD8hxVp74+wa5ew0fIXjIpMoHCar/nndsse4i8glCddINdiOPPmhI9Wi3nT+5Z2t -9omPP2dEh0CzR+j1zvUpT3KtmhVICqhO+QP9BTJOwrp81NTlq9mbUyzTtVk/9dy3 -zoYbhKvY08k6LJ9FsQYySqtfJZ4cwl5WsOhALWwOwlMLA9wkz0eemgFxStyOylzl -QKoIK7zHuU6XYOXa32KSPIWaLy+WgIG/u2ObWtdE3CXVIUuSt5BQFnv7XVNHJllD -Az9VDEkOSYOiSEFVoUsAEQEAAQAP/1AagnZQZyzHDEgw4QELAspYHCWLXE5aZInX -wTUJhK31IgIXNn9bJ0hFiSpQR2xeMs9oYtRuPOu0P8oOFMn4/z374fkjZy8QVY3e -PlL+3EUeqYtkMwlGNmVw5a/NbNuNfm5Darb7pEfbYd1gPcni4MAYw7R2SG/57GbC -9gucvspHIfOSfBNLBthDzmK8xEKe1yD2eimfc2T7IRYb6hmkYfeds5GsqvGI6mwI -85h4uUHWRc5JOlhVM6yX8hSWx0L60Z3DZLChmc8maWnFXd7C8eQ6P1azJJbW71Ih -7CoK0XW4LE82vlQurSRFgTwfl7wFYszW2bOzCuhHDDtYnwH86Nsu0DC78ZVRnvxn -E8Ke/AJgrdhIOo4UAyR+aZD2+2mKd7/waOUTUrUtTzc7i8N3YXGi/EIaNReBXaq+ -ZNOp24BlFzRp+FCF/pptDW9HjPdiV09x0DgICmeZS4Gq/4vFFIahWctg52NGebT0 -Idxngjj+xDtLaZlLQoOz0n5ByjO/Wi0ANmMv1sMKCHhGvdaSws2/PbMR2r4caj8m -KXpIgdinM/wUzHJ5pZyF2U/qejsRj8Kw8KH/tfX4JCLhiaP/mgeTuWGDHeZQERAT -xPmRFHaLP9/ZhvGNh6okIYtrKjWTLGoXvKLHcrKNisBLSq+P2WeFrlme1vjvJMo/ -jPwLT5o9CADQmcbKZ+QQ1ZM9v99iDZol7SAMZX43JC019sx6GK0u6xouJBcLfeB4 -OXacTgmSYdTa9RM9fbfVpti01tJ84LV2SyL/VJq/enJF4XQPSynT/tFTn1PAor6o -tEAAd8fjKdJ6LnD5wb92SPHfQfXqI84rFEO8rUNIE/1ErT6DYifDzVCbfD2KZdoF -cOSp7TpD77sY1bs74ocBX5ejKtd+aH99D78bJSMM4pSDZsIEwnomkBHTziubPwJb -OwnATy0LmSMAWOw5rKbsh5nfwCiUTM20xp0t5JeXd+wPVWbpWqI2EnkCEN+RJr9i -7dp/ymDQ+Yt5wrsN3NwoyiexPOG91WQVCADdErHsnglVZZq9Z8Wx7KwecGCUurJ2 -H6lKudv5YOxPnAzqZS5HbpZd/nRTMZh2rdXCr5m2YOuewyYjvM757AkmUpM09zJX -MQ1S67/UX2y8/74TcRF97Ncx9HeELs92innBRXoFitnNguvcO6Esx4BTe1OdU6qR -ER3zAmVf22Le9ciXbu24DN4mleOH+OmBx7X2PqJSYW9GAMTsRB081R6EWKH7romQ -waxFrZ4DJzZ9ltyosEJn5F32StyLrFxpcrdLUoEaclZCv2qka7sZvi0EvovDVEBU -e10jOx9AOwf8Gj2ufhquQ6qgVYCzbP+YrodtkFrXRS3IsljIchj1M2ffB/0bfoUs -rtER9pLvYzCjBPg8IfGLw0o754Qbhh/ReplCRTusP/fQMybvCvfxreS3oyEriu/G -GufRomjewZ8EMHDIgUsLcYo2UHZsfF7tcazgxMGmMvazp4r8vpgrvW/8fIN/6Adu -tF+WjWDTvJLFJCe6O+BFJOWrssNrrra1zGtLC1s8s+Wfpe+bGPL5zpHeebGTwH1U -22eqgJArlEKxrfarz7W5+uHZJHSjF/K9ZvunLGD0n9GOPMpji3UO3zeM8IYoWn7E -/EWK1XbjnssNemeeTZ+sDh+qrD7BOi+vCX1IyBxbfqnQfJZvmcPWpruy1UsO+aIC -0GY8Jr3OL69dDQ21jueJAh8EGAEIAAkFAlC9+dkCGwwACgkQL0VeKCTRjd9HCw/+ -LQSVgLLF4ulYlPCjWIIuQwrPbJfWUVVr2dPUFVM85DCv8gBzk5c121snXh9Swovm -laBbw6ate3BmbXLh64jVE9Za5sbTWi7PCcbO/bpRy4d6oLmitmNw6cq0vjTLxUYy -bwuiJxWREkfxuU85EKdouN062YDevH+/YResmlJrcCE7LRlJFeRlKsrrwBU3BqYd -GgFJjKjQC1peeQ9fj62Y7xfwE9+PXbkiWO5u/Bk8hb1VZH1SoIRU98NHVcp6BVvp -VK0jLAXuSauSczULmpRjbyt1lhaAqivDTWEEZXiNNbRyp17c3nVdPWOcgBr42hdQ -z25CgZgyLCsvu82wuXLKJblrIPJX3Yf+si6KqEWBsmwdOWybsjygaF5HvzgFqAAD -U0goPWoQ71PorP2XOUNp5ZLkBQp5etvtkksjVNMIhnHn8PGMuoxO39EUGlWj2B5l -Cu8tSosAzB1pS8NcLZzoNoI9dOHrmgJmP+GrOUkcf5GhNZbMoj4GNfGBRYX0SZlQ -GuDrwNKYj73C4MWyNnnUFyq8nDHJ/G1NpaF2hiof9RBL4PUU/f92JkceXPBXA8gL -Mz2ig1OButwPPLFGQhWqxXAGrsS3Ny+BhTJfnfIbbkaLLphBpDZm1D9XKbAUvdd1 -RZXoH+FTg9UAW87eqU610npOkT6cRaBxaMK/mDtGNdc= -=JTFu ------END PGP PRIVATE KEY BLOCK----- -""" diff --git a/src/leap/email/smtp/tests/mail.txt b/src/leap/email/smtp/tests/mail.txt deleted file mode 100644 index 95420470..00000000 --- a/src/leap/email/smtp/tests/mail.txt +++ /dev/null @@ -1,10 +0,0 @@ -HELO drebs@riseup.net -MAIL FROM: drebs@riseup.net -RCPT TO: drebs@riseup.net -RCPT TO: drebs@leap.se -DATA -Subject: leap test - -Hello world! -. -QUIT diff --git a/src/leap/email/smtp/tests/test_smtprelay.py b/src/leap/email/smtp/tests/test_smtprelay.py deleted file mode 100644 index 1c549eb3..00000000 --- a/src/leap/email/smtp/tests/test_smtprelay.py +++ /dev/null @@ -1,75 +0,0 @@ -from datetime import datetime -import re -from leap.email.smtp.smtprelay import ( - SMTPFactory, - #SMTPDelivery, # an object - EncryptedMessage, -) -from leap.email.smtp import tests -from twisted.test import proto_helpers -from twisted.mail.smtp import User - - -# some regexps -IP_REGEX = "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}" + \ - "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])" -HOSTNAME_REGEX = "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*" + \ - "([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])" -IP_OR_HOST_REGEX = '(' + IP_REGEX + '|' + HOSTNAME_REGEX + ')' - - -class TestSmtpRelay(tests.OpenPGPTestCase): - - EMAIL_DATA = ['HELO relay.leap.se', - 'MAIL FROM: ', - 'RCPT TO: ', - 'DATA', - 'From: User ', - 'To: Leap ', - 'Date: ' + datetime.now().strftime('%c'), - 'Subject: test message', - '', - 'This is a secret message.', - 'Yours,', - 'A.', - '', - '.', - 'QUIT'] - - def assertMatch(self, string, pattern, msg=None): - if not re.match(pattern, string): - msg = self._formatMessage(msg, '"%s" does not match pattern "%s".' - % (string, pattern)) - raise self.failureException(msg) - - def test_relay_accepts_valid_email(self): - """ - Test if SMTP server responds correctly for valid interaction. - """ - - SMTP_ANSWERS = ['220 ' + IP_OR_HOST_REGEX + - ' NO UCE NO UBE NO RELAY PROBES', - '250 ' + IP_OR_HOST_REGEX + ' Hello ' + - IP_OR_HOST_REGEX + ', nice to meet you', - '250 Sender address accepted', - '250 Recipient address accepted', - '354 Continue'] - proto = SMTPFactory(self._gpg).buildProtocol(('127.0.0.1', 0)) - transport = proto_helpers.StringTransport() - proto.makeConnection(transport) - for i, line in enumerate(self.EMAIL_DATA): - proto.lineReceived(line + '\r\n') - self.assertMatch(transport.value(), - '\r\n'.join(SMTP_ANSWERS[0:i + 1])) - proto.setTimeout(None) - - def test_message_encrypt(self): - proto = SMTPFactory(self._gpg).buildProtocol(('127.0.0.1', 0)) - user = User('leap@leap.se', 'relay.leap.se', proto, 'leap@leap.se') - m = EncryptedMessage(user, self._gpg) - for line in self.EMAIL_DATA[4:12]: - m.lineReceived(line) - m.parseMessage() - m.encrypt() - decrypted = str(self._gpg.decrypt(m.cyphertext)) - self.assertEqual('\n'.join(self.EMAIL_DATA[9:12]), decrypted) diff --git a/src/leap/gui/__init__.py b/src/leap/gui/__init__.py deleted file mode 100644 index 804bfbc1..00000000 --- a/src/leap/gui/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -try: - import sip - sip.setapi('QString', 2) - sip.setapi('QVariant', 2) -except ValueError: - pass - -import firstrun -import firstrun.wizard - -__all__ = ['firstrun', 'firstrun.wizard'] diff --git a/src/leap/gui/constants.py b/src/leap/gui/constants.py deleted file mode 100644 index 277f3540..00000000 --- a/src/leap/gui/constants.py +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 2a523d6a..00000000 --- a/src/leap/gui/firstrun/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -try: - import sip - sip.setapi('QString', 2) - sip.setapi('QVariant', 2) -except ValueError: - pass - -import intro -import connect -import last -import login -import mixins -import providerinfo -import providerselect -import providersetup -import register - -__all__ = [ - 'intro', - 'connect', - 'last', - 'login', - 'mixins', - 'providerinfo', - 'providerselect', - 'providersetup', - 'register', - ] # ,'wizard'] diff --git a/src/leap/gui/firstrun/connect.py b/src/leap/gui/firstrun/connect.py deleted file mode 100644 index ad7bb13a..00000000 --- a/src/leap/gui/firstrun/connect.py +++ /dev/null @@ -1,214 +0,0 @@ -""" -Provider Setup Validation Page, -used in First Run Wizard -""" -import logging - -from PyQt4 import QtGui - -#import requests - -from leap.gui.progress import ValidationPage -from leap.util.web import get_https_domain_and_port - -from leap.base import auth -from leap.gui.constants import APP_LOGO - -logger = logging.getLogger(__name__) - - -class ConnectionPage(ValidationPage): - - def __init__(self, parent=None): - super(ConnectionPage, self).__init__(parent) - self.current_page = "connect" - - title = self.tr("Connecting...") - subtitle = self.tr("Setting up a encrypted " - "connection with the provider") - - self.setTitle(title) - self.setSubTitle(subtitle) - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - def _do_checks(self, update_signal=None): - """ - executes actual checks in a separate thread - - we initialize the srp protocol register - and try to register user. - """ - wizard = self.wizard() - full_domain = self.field('provider_domain') - domain, port = get_https_domain_and_port(full_domain) - - pconfig = wizard.eipconfigchecker(domain=domain) - # this should be persisted... - pconfig.defaultprovider.load() - pconfig.set_api_domain() - - pCertChecker = wizard.providercertchecker( - domain=domain) - pCertChecker.set_api_domain(pconfig.apidomain) - - ########################################### - # Set Credentials. - # username and password are in different fields - # if they were stored in log_in or sign_up pages. - from_login = wizard.from_login - - unamek_base = 'userName' - passwk_base = 'userPassword' - unamek = 'login_%s' % unamek_base if from_login else unamek_base - passwk = 'login_%s' % passwk_base if from_login else passwk_base - - username = self.field(unamek) - password = self.field(passwk) - credentials = username, password - - yield(("head_sentinel", 0), lambda: None) - - ################################################## - # 1) fetching eip service config - ################################################## - def fetcheipconf(): - try: - pconfig.fetch_eip_service_config() - - # XXX get specific exception - except Exception as exc: - return self.fail(exc.message) - - yield((self.tr("Getting EIP configuration files"), 40), - fetcheipconf) - - ################################################## - # 2) getting client certificate - ################################################## - - def fetcheipcert(): - try: - downloaded = pCertChecker.download_new_client_cert( - credentials=credentials) - if not downloaded: - logger.error('Could not download client cert') - return False - - except auth.SRPAuthenticationError as exc: - return self.fail(self.tr( - "Authentication error: %s" % exc.message)) - - except Exception as exc: - return self.fail(exc.message) - else: - return True - - yield((self.tr("Getting EIP certificate"), 80), - fetcheipcert) - - ################ - # end ! - ################ - self.set_done() - yield(("end_sentinel", 100), lambda: None) - - def on_checks_validation_ready(self): - """ - called after _do_checks has finished - (connected to checker thread finished signal) - """ - # here we go! :) - if self.is_done(): - nextbutton = self.wizard().button(QtGui.QWizard.NextButton) - nextbutton.setFocus() - - full_domain = self.field('provider_domain') - domain, port = get_https_domain_and_port(full_domain) - _domain = u"%s:%s" % ( - domain, port) if port != 443 else unicode(domain) - self.run_eip_checks_for_provider_and_connect(_domain) - - def run_eip_checks_for_provider_and_connect(self, domain): - wizard = self.wizard() - conductor = wizard.conductor - start_eip_signal = getattr( - wizard, - 'start_eipconnection_signal', None) - - if conductor: - conductor.set_provider_domain(domain) - # we could run some of the checks to be - # sure everything is in order, but - # I see no point in doing it, we assume - # we've gone thru all checks during the wizard. - #conductor.run_checks() - #self.conductor = conductor - #errors = self.eip_error_check() - #if not errors and start_eip_signal: - if start_eip_signal: - start_eip_signal.emit() - - else: - logger.warning( - "No conductor found. This means that " - "probably the wizard has been launched " - "in an stand-alone way.") - - self.set_done() - - #def eip_error_check(self): - #""" - #a version of the main app error checker, - #but integrated within the connecting page of the wizard. - #consumes the conductor error queue. - #pops errors, and add those to the wizard page - #""" - # TODO handle errors. - # We should redirect them to the log viewer - # with a brief message. - # XXX move to LAST PAGE instead. - #logger.debug('eip error check from connecting page') - #errq = self.conductor.error_queue - - #def _do_validation(self): - #""" - #called after _do_checks has finished - #(connected to checker thread finished signal) - #""" - #from_login = self.wizard().from_login - #prevpage = "login" if from_login else "signup" - - #wizard = self.wizard() - #if self.errors: - #logger.debug('going back with errors') - #logger.error(self.errors) - #name, first_error = self.pop_first_error() - #wizard.set_validation_error( - #prevpage, - #first_error) - #self.go_back() - - def nextId(self): - wizard = self.wizard() - return wizard.get_page_index('lastpage') - - def initializePage(self): - super(ConnectionPage, self).initializePage() - self.set_undone() - cancelbutton = self.wizard().button(QtGui.QWizard.CancelButton) - cancelbutton.hide() - self.completeChanged.emit() - - wizard = self.wizard() - eip_statuschange_signal = wizard.eip_statuschange_signal - if eip_statuschange_signal: - eip_statuschange_signal.connect( - lambda status: self.send_status( - status)) - - def send_status(self, status): - wizard = self.wizard() - wizard.openvpn_status.append(status) diff --git a/src/leap/gui/firstrun/constants.py b/src/leap/gui/firstrun/constants.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/gui/firstrun/intro.py b/src/leap/gui/firstrun/intro.py deleted file mode 100644 index b519362f..00000000 --- a/src/leap/gui/firstrun/intro.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Intro page used in first run wizard -""" - -from PyQt4 import QtGui - -from leap.gui.constants import APP_LOGO - - -class IntroPage(QtGui.QWizardPage): - def __init__(self, parent=None): - super(IntroPage, self).__init__(parent) - - self.setTitle(self.tr("First run wizard")) - - #self.setPixmap( - #QtGui.QWizard.WatermarkPixmap, - #QtGui.QPixmap(':/images/watermark1.png')) - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - label = QtGui.QLabel(self.tr( - "Now we will guide you through " - "some configuration that is needed before you " - "can connect for the first time.

" - "If you ever need to modify these options again, " - "you can find the wizard in the 'Settings' menu from the " - "main window.

" - "Do you want to sign up for a new account, or log " - "in with an already existing username?
")) - label.setWordWrap(True) - - radiobuttonGroup = QtGui.QGroupBox() - - self.sign_up = QtGui.QRadioButton( - self.tr("Sign up for a new account")) - self.sign_up.setChecked(True) - self.log_in = QtGui.QRadioButton( - self.tr("Log In with my credentials")) - - radiobLayout = QtGui.QVBoxLayout() - radiobLayout.addWidget(self.sign_up) - radiobLayout.addWidget(self.log_in) - radiobuttonGroup.setLayout(radiobLayout) - - layout = QtGui.QVBoxLayout() - layout.addWidget(label) - layout.addWidget(radiobuttonGroup) - self.setLayout(layout) - - #self.registerField('is_signup', self.sign_up) - - def validatePage(self): - return True - - def nextId(self): - """ - returns next id - in a non-linear wizard - """ - if self.sign_up.isChecked(): - next_ = 'providerselection' - if self.log_in.isChecked(): - next_ = 'login' - wizard = self.wizard() - return wizard.get_page_index(next_) diff --git a/src/leap/gui/firstrun/last.py b/src/leap/gui/firstrun/last.py deleted file mode 100644 index f3e467db..00000000 --- a/src/leap/gui/firstrun/last.py +++ /dev/null @@ -1,119 +0,0 @@ -""" -Last Page, used in First Run Wizard -""" -import logging - -from PyQt4 import QtGui - -from leap.util.coroutines import coroutine -from leap.gui.constants import APP_LOGO - -logger = logging.getLogger(__name__) - - -class LastPage(QtGui.QWizardPage): - def __init__(self, parent=None): - super(LastPage, self).__init__(parent) - - self.setTitle(self.tr( - "Connecting to Encrypted Internet Proxy service...")) - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - #self.setPixmap( - #QtGui.QWizard.WatermarkPixmap, - #QtGui.QPixmap(':/images/watermark2.png')) - - self.label = QtGui.QLabel() - self.label.setWordWrap(True) - - self.wizard_done = False - - # XXX REFACTOR to a Validating Page... - self.status_line_1 = QtGui.QLabel() - self.status_line_2 = QtGui.QLabel() - self.status_line_3 = QtGui.QLabel() - self.status_line_4 = QtGui.QLabel() - self.status_line_5 = QtGui.QLabel() - - layout = QtGui.QVBoxLayout() - layout.addWidget(self.label) - - # make loop - layout.addWidget(self.status_line_1) - layout.addWidget(self.status_line_2) - layout.addWidget(self.status_line_3) - layout.addWidget(self.status_line_4) - layout.addWidget(self.status_line_5) - - self.setLayout(layout) - - def isComplete(self): - return self.wizard_done - - def set_status_line(self, line, status): - statusline = getattr(self, 'status_line_%s' % line) - if statusline: - statusline.setText(status) - - def set_finished_status(self): - self.setTitle(self.tr('You are now using an encrypted connection!')) - finishText = self.wizard().buttonText( - QtGui.QWizard.FinishButton) - finishText = finishText.replace('&', '') - self.label.setText(self.tr( - "Click '%s' to end the wizard and " - "save your settings." % finishText)) - self.wizard_done = True - self.completeChanged.emit() - - @coroutine - def eip_status_handler(self): - # XXX this can be changed to use - # signals. See progress.py - logger.debug('logging status in last page') - self.validation_done = False - status_count = 1 - try: - while True: - status = (yield) - status_count += 1 - # XXX add to line... - logger.debug('status --> %s', status) - self.set_status_line(status_count, status) - if status == "connected": - self.set_finished_status() - self.completeChanged.emit() - break - self.completeChanged.emit() - except GeneratorExit: - pass - except StopIteration: - pass - - def initializePage(self): - super(LastPage, self).initializePage() - wizard = self.wizard() - wizard.button(QtGui.QWizard.FinishButton).setDisabled(True) - - handler = self.eip_status_handler() - - # get statuses done in prev page - for st in wizard.openvpn_status: - self.send_status(handler.send, st) - - # bind signal for events yet to come - eip_statuschange_signal = wizard.eip_statuschange_signal - if eip_statuschange_signal: - eip_statuschange_signal.connect( - lambda status: self.send_status( - handler.send, status)) - self.completeChanged.emit() - - def send_status(self, cb, status): - try: - cb(status) - except StopIteration: - pass diff --git a/src/leap/gui/firstrun/login.py b/src/leap/gui/firstrun/login.py deleted file mode 100644 index 3707d3ff..00000000 --- a/src/leap/gui/firstrun/login.py +++ /dev/null @@ -1,332 +0,0 @@ -""" -LogIn Page, used inf First Run Wizard -""" -from PyQt4 import QtCore -from PyQt4 import QtGui - -import requests - -from leap.base import auth -from leap.gui.firstrun.mixins import UserFormMixIn -from leap.gui.progress import InlineValidationPage -from leap.gui import styles - -from leap.gui.constants import APP_LOGO, FULL_USERNAME_REGEX - - -class LogInPage(InlineValidationPage, UserFormMixIn): # InlineValidationPage - - def __init__(self, parent=None): - - super(LogInPage, self).__init__(parent) - self.current_page = "login" - - self.setTitle(self.tr("Log In")) - self.setSubTitle(self.tr("Log in with your credentials")) - self.current_page = "login" - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - self.setupSteps() - self.setupUI() - - self.do_confirm_next = False - - def setupUI(self): - userNameLabel = QtGui.QLabel(self.tr("User &name:")) - userNameLineEdit = QtGui.QLineEdit() - userNameLineEdit.cursorPositionChanged.connect( - self.reset_validation_status) - userNameLabel.setBuddy(userNameLineEdit) - - # let's add regex validator - usernameRe = QtCore.QRegExp(FULL_USERNAME_REGEX) - userNameLineEdit.setValidator( - QtGui.QRegExpValidator(usernameRe, self)) - - #userNameLineEdit.setPlaceholderText( - #'username@provider.example.org') - self.userNameLineEdit = userNameLineEdit - - userPasswordLabel = QtGui.QLabel(self.tr("&Password:")) - self.userPasswordLineEdit = QtGui.QLineEdit() - self.userPasswordLineEdit.setEchoMode( - QtGui.QLineEdit.Password) - userPasswordLabel.setBuddy(self.userPasswordLineEdit) - - self.registerField('login_userName*', self.userNameLineEdit) - self.registerField('login_userPassword*', self.userPasswordLineEdit) - - layout = QtGui.QGridLayout() - layout.setColumnMinimumWidth(0, 20) - - validationMsg = QtGui.QLabel("") - validationMsg.setStyleSheet(styles.ErrorLabelStyleSheet) - self.validationMsg = validationMsg - - layout.addWidget(validationMsg, 0, 3) - layout.addWidget(userNameLabel, 1, 0) - layout.addWidget(self.userNameLineEdit, 1, 3) - layout.addWidget(userPasswordLabel, 2, 0) - layout.addWidget(self.userPasswordLineEdit, 2, 3) - - # add validation frame - self.setupValidationFrame() - layout.addWidget(self.valFrame, 4, 2, 4, 2) - self.valFrame.hide() - - self.nextText(self.tr("Log in")) - self.setLayout(layout) - - #self.registerField('is_login_wizard') - - # actual checks - - def _do_checks(self): - - full_username = self.userNameLineEdit.text() - ########################### - # 0) check user@domain form - ########################### - - def checkusername(): - if full_username.count('@') != 1: - return self.fail( - self.tr( - "Username must be in the username@provider form.")) - else: - return True - - yield(("head_sentinel", 0), checkusername) - - username, domain = full_username.split('@') - password = self.userPasswordLineEdit.text() - - # We try a call to an authenticated - # page here as a mean to catch - # srp authentication errors while - wizard = self.wizard() - eipconfigchecker = wizard.eipconfigchecker(domain=domain) - - ######################## - # 1) try name resolution - ######################## - # show the frame before going on... - QtCore.QMetaObject.invokeMethod( - self, "showStepsFrame") - - # Able to contact domain? - # can get definition? - # two-by-one - def resolvedomain(): - try: - eipconfigchecker.fetch_definition(domain=domain) - - # we're using requests here for all - # the possible error cases that it catches. - except requests.exceptions.ConnectionError as exc: - return self.fail(exc.message[1]) - except requests.exceptions.HTTPError as exc: - return self.fail(exc.message) - except Exception as exc: - # XXX get catchall error msg - return self.fail( - exc.message) - else: - return True - - yield((self.tr("Resolving domain name"), 20), resolvedomain) - - wizard.set_providerconfig( - eipconfigchecker.defaultprovider.config) - - ######################## - # 2) do authentication - ######################## - credentials = username, password - pCertChecker = wizard.providercertchecker( - domain=domain) - - def validate_credentials(): - ################# - # FIXME #BUG #638 - verify = False - - try: - pCertChecker.download_new_client_cert( - credentials=credentials, - verify=verify) - - except auth.SRPAuthenticationError as exc: - return self.fail( - self.tr("Authentication error: %s" % exc.message)) - - except Exception as exc: - return self.fail(exc.message) - - else: - return True - - yield(('Validating credentials', 60), validate_credentials) - - self.set_done() - yield(("end_sentinel", 100), lambda: None) - - def green_validation_status(self): - val = self.validationMsg - val.setText(self.tr('Credentials validated.')) - val.setStyleSheet(styles.GreenLineEdit) - - def on_checks_validation_ready(self): - """ - after checks - """ - if self.is_done(): - self.disableFields() - self.cleanup_errormsg() - self.clean_wizard_errors(self.current_page) - # make the user confirm the transition - # to next page. - self.nextText('&Next') - self.nextFocus() - self.green_validation_status() - self.do_confirm_next = True - - # ui update - - def nextText(self, text): - self.setButtonText( - QtGui.QWizard.NextButton, text) - - def nextFocus(self): - self.wizard().button( - QtGui.QWizard.NextButton).setFocus() - - def disableNextButton(self): - self.wizard().button( - QtGui.QWizard.NextButton).setDisabled(True) - - def onUserNamePositionChanged(self, *args): - if self.initial_username_sample: - self.userNameLineEdit.setText('') - # XXX set regular color - self.initial_username_sample = None - - def onUserNameTextChanged(self, *args): - if self.initial_username_sample: - k = args[0][-1] - self.initial_username_sample = None - self.userNameLineEdit.setText(k) - - def disableFields(self): - for field in (self.userNameLineEdit, - self.userPasswordLineEdit): - field.setDisabled(True) - - def populateErrors(self): - # XXX could move this to ValidationMixin - # used in providerselect and register too - - errors = self.wizard().get_validation_error( - self.current_page) - showerr = self.validationMsg.setText - - if errors: - bad_str = getattr(self, 'bad_string', None) - cur_str = self.userNameLineEdit.text() - - if bad_str is None: - # first time we fall here. - # save the current bad_string value - self.bad_string = cur_str - showerr(errors) - else: - # not the first time - if cur_str == bad_str: - showerr(errors) - else: - self.focused_field = False - showerr('') - - def cleanup_errormsg(self): - """ - we reset bad_string to None - should be called before leaving the page - """ - self.bad_string = None - - def paintEvent(self, event): - """ - we hook our populate errors - on paintEvent because we need it to catch - when user enters the page coming from next, - and initializePage does not cover that case. - Maybe there's a better event to hook upon. - """ - super(LogInPage, self).paintEvent(event) - self.populateErrors() - - def set_prevalidation_error(self, error): - self.prevalidation_error = error - - # pagewizard methods - - def nextId(self): - wizard = self.wizard() - if not wizard: - return - if wizard.is_provider_setup is False: - next_ = 'providersetupvalidation' - if wizard.is_provider_setup is True: - # XXX bad name, ok, gonna change that - next_ = 'signupvalidation' - return wizard.get_page_index(next_) - - def initializePage(self): - super(LogInPage, self).initializePage() - username = self.userNameLineEdit - username.setText('username@provider.example.org') - username.cursorPositionChanged.connect( - self.onUserNamePositionChanged) - username.textChanged.connect( - self.onUserNameTextChanged) - self.initial_username_sample = True - self.validationMsg.setText('') - self.valFrame.hide() - - def reset_validation_status(self): - """ - empty the validation msg - and clean the inline validation widget. - """ - self.validationMsg.setText('') - self.steps.removeAllSteps() - self.clearTable() - - def validatePage(self): - """ - if not register done, do checks. - if done, wait for click. - """ - self.disableNextButton() - self.cleanup_errormsg() - self.clean_wizard_errors(self.current_page) - - if self.do_confirm_next: - full_username = self.userNameLineEdit.text() - password = self.userPasswordLineEdit.text() - username, domain = full_username.split('@') - self.setField('provider_domain', domain) - self.setField('login_userName', username) - self.setField('login_userPassword', password) - self.wizard().from_login = True - - return True - - if not self.is_done(): - self.reset_validation_status() - self.do_checks() - - return self.is_done() diff --git a/src/leap/gui/firstrun/mixins.py b/src/leap/gui/firstrun/mixins.py deleted file mode 100644 index c4731893..00000000 --- a/src/leap/gui/firstrun/mixins.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -mixins used in First Run Wizard -""" - - -class UserFormMixIn(object): - - def reset_validation_status(self): - """ - empty the validation msg - """ - self.validationMsg.setText('') - - def set_validation_status(self, msg): - """ - set generic validation status - """ - self.validationMsg.setText(msg) diff --git a/src/leap/gui/firstrun/providerinfo.py b/src/leap/gui/firstrun/providerinfo.py deleted file mode 100644 index cff4caca..00000000 --- a/src/leap/gui/firstrun/providerinfo.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -Provider Info Page, used in First run Wizard -""" -import logging - -from PyQt4 import QtGui - -from leap.gui.constants import APP_LOGO -from leap.util.translations import translate - -logger = logging.getLogger(__name__) - - -class ProviderInfoPage(QtGui.QWizardPage): - - def __init__(self, parent=None): - super(ProviderInfoPage, self).__init__(parent) - - self.setTitle(self.tr("Provider Information")) - self.setSubTitle(self.tr( - "Services offered by this provider")) - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - self.create_info_panel() - - def create_info_panel(self): - # Use stacked widget instead - # of reparenting the layout. - - infoWidget = QtGui.QStackedWidget() - - info = QtGui.QWidget() - layout = QtGui.QVBoxLayout() - - displayName = QtGui.QLabel("") - description = QtGui.QLabel("") - enrollment_policy = QtGui.QLabel("") - - # XXX set stylesheet... - # prettify a little bit. - # bigger fonts and so on... - - # We could use a QFrame here - - layout.addWidget(displayName) - layout.addWidget(description) - layout.addWidget(enrollment_policy) - layout.addStretch(1) - - info.setLayout(layout) - infoWidget.addWidget(info) - - pageLayout = QtGui.QVBoxLayout() - pageLayout.addWidget(infoWidget) - self.setLayout(pageLayout) - - # add refs to self to allow for - # updates. - # Watch out! Have to get rid of these references! - # this should be better handled with signals !! - self.displayName = displayName - self.description = description - self.description.setWordWrap(True) - self.enrollment_policy = enrollment_policy - - def show_provider_info(self): - - # XXX get multilingual objects - # directly from the config object - - lang = "en" - pconfig = self.wizard().providerconfig - - dn = pconfig.get('name') - display_name = dn[lang] if dn else '' - domain_name = self.field('provider_domain') - - self.displayName.setText( - "%s https://%s" % (display_name, domain_name)) - - desc = pconfig.get('description') - - #description_text = desc[lang] if desc else '' - description_text = translate(desc) if desc else '' - - self.description.setText( - "%s" % description_text) - - # XXX should translate this... - enroll = pconfig.get('enrollment_policy') - if enroll: - self.enrollment_policy.setText( - '%s: %s' % ( - self.tr('enrollment policy'), - enroll)) - - def nextId(self): - wizard = self.wizard() - next_ = "providersetupvalidation" - return wizard.get_page_index(next_) - - def initializePage(self): - self.show_provider_info() diff --git a/src/leap/gui/firstrun/providerselect.py b/src/leap/gui/firstrun/providerselect.py deleted file mode 100644 index 917b16fd..00000000 --- a/src/leap/gui/firstrun/providerselect.py +++ /dev/null @@ -1,471 +0,0 @@ -""" -Select Provider Page, used in First Run Wizard -""" -import logging - -import requests - -from PyQt4 import QtCore -from PyQt4 import QtGui - -from leap.base import exceptions as baseexceptions -#from leap.crypto import certs -from leap.eip import exceptions as eipexceptions -from leap.gui.progress import InlineValidationPage -from leap.gui import styles -from leap.gui.utils import delay -from leap.util.web import get_https_domain_and_port - -from leap.gui.constants import APP_LOGO - -logger = logging.getLogger(__name__) - - -class SelectProviderPage(InlineValidationPage): - - launchChecks = QtCore.pyqtSignal() - - def __init__(self, parent=None, providers=None): - super(SelectProviderPage, self).__init__(parent) - self.current_page = 'providerselection' - - self.setTitle(self.tr("Enter Provider")) - self.setSubTitle(self.tr( - "Please enter the domain of the provider you want " - "to use for your connection") - ) - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - self.did_cert_check = False - - self.done = False - - self.setupSteps() - self.setupUI() - - self.launchChecks.connect( - self.launch_checks) - - self.providerNameEdit.editingFinished.connect( - lambda: self.providerCheckButton.setFocus(True)) - - def setupUI(self): - """ - initializes the UI - """ - providerNameLabel = QtGui.QLabel("h&ttps://") - # note that we expect the bare domain name - # we will add the scheme later - providerNameEdit = QtGui.QLineEdit() - providerNameEdit.cursorPositionChanged.connect( - self.reset_validation_status) - providerNameLabel.setBuddy(providerNameEdit) - - # add regex validator - providerDomainRe = QtCore.QRegExp(r"^[a-z1-9_\-\.]+$") - providerNameEdit.setValidator( - QtGui.QRegExpValidator(providerDomainRe, self)) - self.providerNameEdit = providerNameEdit - - # Eventually we will seed a list of - # well known providers here. - - #providercombo = QtGui.QComboBox() - #if providers: - #for provider in providers: - #providercombo.addItem(provider) - #providerNameSelect = providercombo - - self.registerField("provider_domain*", self.providerNameEdit) - #self.registerField('provider_name_index', providerNameSelect) - - validationMsg = QtGui.QLabel("") - validationMsg.setStyleSheet(styles.ErrorLabelStyleSheet) - self.validationMsg = validationMsg - providerCheckButton = QtGui.QPushButton(self.tr("chec&k!")) - self.providerCheckButton = providerCheckButton - - # cert info - - # this is used in the callback - # for the checkbox changes. - # tricky, since the first time came - # from the exception message. - # should get string from exception too! - self.bad_cert_status = self.tr( - "Server certificate could not be verified.") - - self.certInfo = QtGui.QLabel("") - self.certInfo.setWordWrap(True) - self.certWarning = QtGui.QLabel("") - self.trustProviderCertCheckBox = QtGui.QCheckBox( - self.tr("&Trust this provider certificate.")) - - self.trustProviderCertCheckBox.stateChanged.connect( - self.onTrustCheckChanged) - self.providerNameEdit.textChanged.connect( - self.onProviderChanged) - self.providerCheckButton.clicked.connect( - self.onCheckButtonClicked) - - layout = QtGui.QGridLayout() - layout.addWidget(validationMsg, 0, 2) - layout.addWidget(providerNameLabel, 1, 1) - layout.addWidget(providerNameEdit, 1, 2) - layout.addWidget(providerCheckButton, 1, 3) - - # add certinfo group - # XXX not shown now. should move to validation box. - #layout.addWidget(certinfoGroup, 4, 1, 4, 2) - #self.certinfoGroup = certinfoGroup - #self.certinfoGroup.hide() - - # add validation frame - self.setupValidationFrame() - layout.addWidget(self.valFrame, 4, 2, 4, 2) - self.valFrame.hide() - - self.setLayout(layout) - - # certinfo - - def setupCertInfoGroup(self): # pragma: no cover - # XXX not used now. - certinfoGroup = QtGui.QGroupBox( - self.tr("Certificate validation")) - certinfoLayout = QtGui.QVBoxLayout() - certinfoLayout.addWidget(self.certInfo) - certinfoLayout.addWidget(self.certWarning) - certinfoLayout.addWidget(self.trustProviderCertCheckBox) - certinfoGroup.setLayout(certinfoLayout) - self.certinfoGroup = self.certinfoGroup - - # progress frame - - def setupValidationFrame(self): - qframe = QtGui.QFrame - valFrame = qframe() - valFrame.setFrameStyle(qframe.NoFrame) - valframeLayout = QtGui.QVBoxLayout() - zeros = (0, 0, 0, 0) - valframeLayout.setContentsMargins(*zeros) - - valframeLayout.addWidget(self.stepsTableWidget) - valFrame.setLayout(valframeLayout) - self.valFrame = valFrame - - @QtCore.pyqtSlot() - def onDisableCheckButton(self): - #print 'CHECK BUTTON DISABLED!!!' - self.providerCheckButton.setDisabled(True) - - @QtCore.pyqtSlot() - def launch_checks(self): - self.do_checks() - - def onCheckButtonClicked(self): - QtCore.QMetaObject.invokeMethod( - self, "onDisableCheckButton") - - QtCore.QMetaObject.invokeMethod( - self, "showStepsFrame") - - delay(self, "launch_checks") - - def _do_checks(self): - """ - generator that yields actual checks - that are executed in a separate thread - """ - - wizard = self.wizard() - full_domain = self.providerNameEdit.text() - - # we check if we have a port in the domain string. - domain, port = get_https_domain_and_port(full_domain) - _domain = u"%s:%s" % (domain, port) if port != 443 else unicode(domain) - - netchecker = wizard.netchecker() - providercertchecker = wizard.providercertchecker() - eipconfigchecker = wizard.eipconfigchecker(domain=_domain) - - yield(("head_sentinel", 0), lambda: None) - - ######################## - # 1) try name resolution - ######################## - - def namecheck(): - """ - in which we check if - we are able to name resolve - this domain - """ - try: - #import ipdb;ipdb.set_trace() - netchecker.check_name_resolution( - domain) - - except baseexceptions.LeapException as exc: - logger.error(exc.message) - return self.fail(exc.usermessage) - - except Exception as exc: - return self.fail(exc.message) - - else: - return True - - logger.debug('checking name resolution') - yield((self.tr("Checking if it is a valid provider"), 20), namecheck) - - ######################### - # 2) try https connection - ######################### - - def httpscheck(): - """ - in which we check - if the provider - is offering service over - https - """ - try: - providercertchecker.is_https_working( - "https://%s" % _domain, - verify=True) - - except eipexceptions.HttpsBadCertError as exc: - logger.debug('exception') - return self.fail(exc.usermessage) - # XXX skipping for now... - ############################################## - # We had this validation logic - # in the provider selection page before - ############################################## - #if self.trustProviderCertCheckBox.isChecked(): - #pass - #else: - #fingerprint = certs.get_cert_fingerprint( - #domain=domain, sep=" ") - - # it's ok if we've trusted this fgprt before - #trustedcrts = wizard.trusted_certs - #if trustedcrts and \ - # fingerprint.replace(' ', '') in trustedcrts: - #pass - #else: - # let your user face panick :P - #self.add_cert_info(fingerprint) - #self.did_cert_check = True - #self.completeChanged.emit() - #return False - - except baseexceptions.LeapException as exc: - return self.fail(exc.usermessage) - - except Exception as exc: - return self.fail(exc.message) - - else: - return True - - logger.debug('checking https connection') - yield((self.tr("Checking for a secure connection"), 40), httpscheck) - - ################################## - # 3) try download provider info... - ################################## - - def fetchinfo(): - try: - # XXX we already set _domain in the initialization - # so it should not be needed here. - eipconfigchecker.fetch_definition(domain=_domain) - wizard.set_providerconfig( - eipconfigchecker.defaultprovider.config) - except requests.exceptions.SSLError: - return self.fail(self.tr( - "Could not get info from provider.")) - except requests.exceptions.ConnectionError: - return self.fail(self.tr( - "Could not download provider info " - "(refused conn.).")) - - except Exception as exc: - return self.fail( - self.tr(exc.message)) - else: - return True - - yield((self.tr("Getting info from the provider"), 80), fetchinfo) - - # done! - - self.done = True - yield(("end_sentinel", 100), lambda: None) - - def on_checks_validation_ready(self): - """ - called after _do_checks has finished. - """ - self.domain_checked = True - self.completeChanged.emit() - # let's set focus... - if self.is_done(): - self.wizard().clean_validation_error(self.current_page) - nextbutton = self.wizard().button(QtGui.QWizard.NextButton) - nextbutton.setFocus() - else: - self.providerNameEdit.setFocus() - - # cert trust verification - # (disabled for now) - - def is_insecure_cert_trusted(self): - return self.trustProviderCertCheckBox.isChecked() - - def onTrustCheckChanged(self, state): # pragma: no cover XXX - checked = False - if state == 2: - checked = True - - if checked: - self.reset_validation_status() - else: - self.set_validation_status(self.bad_cert_status) - - # trigger signal to redraw next button - self.completeChanged.emit() - - def add_cert_info(self, certinfo): # pragma: no cover XXX - self.certWarning.setText( - self.tr("Do you want to trust this provider certificate?")) - # XXX Check if this needs to abstracted to remove certinfo - self.certInfo.setText( - self.tr('SHA-256 fingerprint: %s
' % certinfo)) - self.certInfo.setWordWrap(True) - self.certinfoGroup.show() - - def onProviderChanged(self, text): - self.done = False - provider = self.providerNameEdit.text() - if provider: - self.providerCheckButton.setDisabled(False) - else: - self.providerCheckButton.setDisabled(True) - self.completeChanged.emit() - - def reset_validation_status(self): - """ - empty the validation msg - and clean the inline validation widget. - """ - self.validationMsg.setText('') - self.steps.removeAllSteps() - self.clearTable() - self.domain_checked = False - - # pagewizard methods - - def isComplete(self): - provider = self.providerNameEdit.text() - - if not self.is_done(): - return False - - if not provider: - return False - else: - if self.is_insecure_cert_trusted(): - return True - if not self.did_cert_check: - if self.is_done(): - # XXX sure? - return True - return False - - def populateErrors(self): - # XXX could move this to ValidationMixin - # with some defaults for the validating fields - # (now it only allows one field, manually specified) - - #logger.debug('getting errors') - errors = self.wizard().get_validation_error( - self.current_page) - if errors: - bad_str = getattr(self, 'bad_string', None) - cur_str = self.providerNameEdit.text() - showerr = self.validationMsg.setText - markred = lambda: self.providerNameEdit.setStyleSheet( - styles.ErrorLineEdit) - umarkrd = lambda: self.providerNameEdit.setStyleSheet( - styles.RegularLineEdit) - if bad_str is None: - # first time we fall here. - # save the current bad_string value - self.bad_string = cur_str - showerr(errors) - markred() - else: - # not the first time - # XXX hey, this is getting convoluted. - # roll out this. - # but be careful about all the possibilities - # with going back and forth once you - # enter a domain. - if cur_str == bad_str: - showerr(errors) - markred() - else: - if not getattr(self, 'domain_checked', None): - showerr('') - umarkrd() - else: - self.bad_string = cur_str - showerr(errors) - - def cleanup_errormsg(self): - """ - we reset bad_string to None - should be called before leaving the page - """ - self.bad_string = None - self.domain_checked = False - - def paintEvent(self, event): - """ - we hook our populate errors - on paintEvent because we need it to catch - when user enters the page coming from next, - and initializePage does not cover that case. - Maybe there's a better event to hook upon. - """ - super(SelectProviderPage, self).paintEvent(event) - self.populateErrors() - - def initializePage(self): - self.validationMsg.setText('') - if hasattr(self, 'certinfoGroup'): - # XXX remove ? - self.certinfoGroup.hide() - self.done = False - self.providerCheckButton.setDisabled(True) - self.valFrame.hide() - self.steps.removeAllSteps() - self.clearTable() - - def validatePage(self): - # some cleanup before we leave the page - self.cleanup_errormsg() - - # go - return True - - def nextId(self): - wizard = self.wizard() - if not wizard: - return - return wizard.get_page_index('providerinfo') diff --git a/src/leap/gui/firstrun/providersetup.py b/src/leap/gui/firstrun/providersetup.py deleted file mode 100644 index 47060f6e..00000000 --- a/src/leap/gui/firstrun/providersetup.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -Provider Setup Validation Page, -used if First Run Wizard -""" -import logging - -import requests - -from PyQt4 import QtGui - -from leap.base import exceptions as baseexceptions -from leap.gui.progress import ValidationPage - -from leap.gui.constants import APP_LOGO - -logger = logging.getLogger(__name__) - - -class ProviderSetupValidationPage(ValidationPage): - def __init__(self, parent=None): - super(ProviderSetupValidationPage, self).__init__(parent) - self.current_page = "providersetupvalidation" - - # XXX needed anymore? - #is_signup = self.field("is_signup") - #self.is_signup = is_signup - - self.setTitle(self.tr("Provider setup")) - self.setSubTitle( - self.tr("Gathering configuration options for this provider")) - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - def _do_checks(self): - """ - generator that yields actual checks - that are executed in a separate thread - """ - - full_domain = self.field('provider_domain') - wizard = self.wizard() - pconfig = wizard.providerconfig - - #pCertChecker = wizard.providercertchecker - #certchecker = pCertChecker(domain=full_domain) - pCertChecker = wizard.providercertchecker( - domain=full_domain) - - yield(("head_sentinel", 0), lambda: None) - - ######################## - # 1) fetch ca cert - ######################## - - def fetchcacert(): - if pconfig: - ca_cert_uri = pconfig.get('ca_cert_uri').geturl() - else: - ca_cert_uri = None - - # XXX check scheme == "https" - # XXX passing verify == False because - # we have trusted right before. - # We should check it's the same domain!!! - # (Check with the trusted fingerprints dict - # or something smart) - try: - pCertChecker.download_ca_cert( - uri=ca_cert_uri, - verify=False) - - except baseexceptions.LeapException as exc: - logger.error(exc.message) - # XXX this should be _ method - return self.fail(self.tr(exc.usermessage)) - - except Exception as exc: - return self.fail(exc.message) - - else: - return True - - yield((self.tr('Fetching CA certificate'), 30), - fetchcacert) - - ######################### - # 2) check CA fingerprint - ######################### - - def checkcafingerprint(): - # XXX get the real thing!!! - pass - #ca_cert_fingerprint = pconfig.get('ca_cert_fingerprint', None) - - # XXX get fingerprint dict (types) - #sha256_fpr = ca_cert_fingerprint.split('=')[1] - - #validate_fpr = pCertChecker.check_ca_cert_fingerprint( - #fingerprint=sha256_fpr) - #if not validate_fpr: - # XXX update validationMsg - # should catch exception - #return False - - yield((self.tr("Checking CA fingerprint"), 60), - checkcafingerprint) - - ######################### - # 2) check CA fingerprint - ######################### - - def validatecacert(): - api_uri = pconfig.get('api_uri', None) - try: - pCertChecker.verify_api_https(api_uri) - except requests.exceptions.SSLError as exc: - return self.fail("Validation Error") - except Exception as exc: - return self.fail(exc.msg) - else: - return True - - yield((self.tr('Validating api certificate'), 90), validatecacert) - - self.set_done() - yield(('end_sentinel', 100), lambda: None) - - def on_checks_validation_ready(self): - """ - called after _do_checks has finished - (connected to checker thread finished signal) - """ - wizard = self.wizard() - prevpage = "login" if wizard.from_login else "providerselection" - - if self.errors: - logger.debug('going back with errors') - name, first_error = self.pop_first_error() - wizard.set_validation_error( - prevpage, - first_error) - - def nextId(self): - wizard = self.wizard() - from_login = wizard.from_login - if from_login: - next_ = 'connect' - else: - next_ = 'signup' - return wizard.get_page_index(next_) - - def initializePage(self): - super(ProviderSetupValidationPage, self).initializePage() - self.set_undone() - self.completeChanged.emit() diff --git a/src/leap/gui/firstrun/register.py b/src/leap/gui/firstrun/register.py deleted file mode 100644 index 15278330..00000000 --- a/src/leap/gui/firstrun/register.py +++ /dev/null @@ -1,387 +0,0 @@ -""" -Register User Page, used in First Run Wizard -""" -import json -import logging -import socket - -import requests - -from PyQt4 import QtCore -from PyQt4 import QtGui - -from leap.gui.firstrun.mixins import UserFormMixIn - -logger = logging.getLogger(__name__) - -from leap.base import auth -from leap.gui import styles -from leap.gui.constants import APP_LOGO, BARE_USERNAME_REGEX -from leap.gui.progress import InlineValidationPage -from leap.gui.styles import ErrorLabelStyleSheet - - -class RegisterUserPage(InlineValidationPage, UserFormMixIn): - - def __init__(self, parent=None): - - super(RegisterUserPage, self).__init__(parent) - self.current_page = "signup" - - self.setTitle(self.tr("Sign Up")) - # subtitle is set in the initializePage - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - # commit page means there's no way back after this... - # XXX should change the text on the "commit" button... - self.setCommitPage(True) - - self.setupSteps() - self.setupUI() - self.do_confirm_next = False - self.focused_field = False - - def setupUI(self): - userNameLabel = QtGui.QLabel(self.tr("User &name:")) - userNameLineEdit = QtGui.QLineEdit() - userNameLineEdit.cursorPositionChanged.connect( - self.reset_validation_status) - userNameLabel.setBuddy(userNameLineEdit) - - # let's add regex validator - usernameRe = QtCore.QRegExp(BARE_USERNAME_REGEX) - userNameLineEdit.setValidator( - QtGui.QRegExpValidator(usernameRe, self)) - self.userNameLineEdit = userNameLineEdit - - userPasswordLabel = QtGui.QLabel(self.tr("&Password:")) - self.userPasswordLineEdit = QtGui.QLineEdit() - self.userPasswordLineEdit.setEchoMode( - QtGui.QLineEdit.Password) - userPasswordLabel.setBuddy(self.userPasswordLineEdit) - - userPassword2Label = QtGui.QLabel(self.tr("Password (again):")) - self.userPassword2LineEdit = QtGui.QLineEdit() - self.userPassword2LineEdit.setEchoMode( - QtGui.QLineEdit.Password) - userPassword2Label.setBuddy(self.userPassword2LineEdit) - - rememberPasswordCheckBox = QtGui.QCheckBox( - self.tr("&Remember username and password.")) - rememberPasswordCheckBox.setChecked(True) - - self.registerField('userName*', self.userNameLineEdit) - self.registerField('userPassword*', self.userPasswordLineEdit) - self.registerField('userPassword2*', self.userPassword2LineEdit) - - # XXX missing password confirmation - # XXX validator! - - self.registerField('rememberPassword', rememberPasswordCheckBox) - - layout = QtGui.QGridLayout() - layout.setColumnMinimumWidth(0, 20) - - validationMsg = QtGui.QLabel("") - validationMsg.setStyleSheet(ErrorLabelStyleSheet) - - self.validationMsg = validationMsg - - layout.addWidget(validationMsg, 0, 3) - layout.addWidget(userNameLabel, 1, 0) - layout.addWidget(self.userNameLineEdit, 1, 3) - layout.addWidget(userPasswordLabel, 2, 0) - layout.addWidget(userPassword2Label, 3, 0) - layout.addWidget(self.userPasswordLineEdit, 2, 3) - layout.addWidget(self.userPassword2LineEdit, 3, 3) - layout.addWidget(rememberPasswordCheckBox, 4, 3, 4, 4) - - # add validation frame - self.setupValidationFrame() - layout.addWidget(self.valFrame, 5, 2, 5, 2) - self.valFrame.hide() - - self.setLayout(layout) - self.commitText("Sign up!") - - # commit button - - def commitText(self, text): - # change "commit" button text - self.setButtonText( - QtGui.QWizard.CommitButton, text) - - @property - def commitButton(self): - return self.wizard().button(QtGui.QWizard.CommitButton) - - def commitFocus(self): - self.commitButton.setFocus() - - def disableCommitButton(self): - self.commitButton.setDisabled(True) - - def disableFields(self): - for field in (self.userNameLineEdit, - self.userPasswordLineEdit, - self.userPassword2LineEdit): - field.setDisabled(True) - - # error painting - def paintEvent(self, event): - """ - we hook our populate errors - on paintEvent because we need it to catch - when user enters the page coming from next, - and initializePage does not cover that case. - Maybe there's a better event to hook upon. - """ - super(RegisterUserPage, self).paintEvent(event) - self.populateErrors() - - def markRedAndGetFocus(self, field): - field.setStyleSheet(styles.ErrorLineEdit) - if not self.focused_field: - self.focused_field = True - field.setFocus(QtCore.Qt.OtherFocusReason) - - def markRegular(self, field): - field.setStyleSheet(styles.RegularLineEdit) - - def populateErrors(self): - def showerr(text): - self.validationMsg.setText(text) - err_lower = text.lower() - if "username" in err_lower: - self.markRedAndGetFocus( - self.userNameLineEdit) - if "password" in err_lower: - self.markRedAndGetFocus( - self.userPasswordLineEdit) - - def unmarkred(): - for field in (self.userNameLineEdit, - self.userPasswordLineEdit, - self.userPassword2LineEdit): - self.markRegular(field) - - errors = self.wizard().get_validation_error( - self.current_page) - if errors: - bad_str = getattr(self, 'bad_string', None) - cur_str = self.userNameLineEdit.text() - #prev_er = getattr(self, 'prevalidation_error', None) - - if bad_str is None: - # first time we fall here. - # save the current bad_string value - self.bad_string = cur_str - showerr(errors) - else: - #if prev_er: - #showerr(prev_er) - #return - # not the first time - if cur_str == bad_str: - showerr(errors) - else: - self.focused_field = False - showerr('') - unmarkred() - else: - # no errors - self.focused_field = False - unmarkred() - - def cleanup_errormsg(self): - """ - we reset bad_string to None - should be called before leaving the page - """ - self.bad_string = None - - def green_validation_status(self): - val = self.validationMsg - val.setText(self.tr('Registration succeeded!')) - val.setStyleSheet(styles.GreenLineEdit) - - def reset_validation_status(self): - """ - empty the validation msg - and clean the inline validation widget. - """ - self.validationMsg.setText('') - self.steps.removeAllSteps() - self.clearTable() - - # actual checks - - def _do_checks(self): - """ - generator that yields actual checks - that are executed in a separate thread - """ - wizard = self.wizard() - - provider = self.field('provider_domain') - username = self.userNameLineEdit.text() - password = self.userPasswordLineEdit.text() - password2 = self.userPassword2LineEdit.text() - - pconfig = wizard.eipconfigchecker(domain=provider) - pconfig.defaultprovider.load() - pconfig.set_api_domain() - - def checkpass(): - # we better have here - # some call to a password checker... - # to assess strenght and avoid silly stuff. - - if password != password2: - return self.fail(self.tr('Password does not match..')) - - if len(password) < 6: - #self.set_prevalidation_error('Password too short.') - return self.fail(self.tr('Password too short.')) - - if password == "123456": - # joking, but not too much. - #self.set_prevalidation_error('Password too obvious.') - return self.fail(self.tr('Password too obvious.')) - - # go - return True - - yield(("head_sentinel", 0), checkpass) - - # XXX should emit signal for .show the frame! - # XXX HERE! - - ################################################## - # 1) register user - ################################################## - - # show the frame before going on... - QtCore.QMetaObject.invokeMethod( - self, "showStepsFrame") - - def register(): - - signup = auth.LeapSRPRegister( - schema="https", - provider=pconfig.apidomain, - verify=pconfig.cacert) - try: - ok, req = signup.register_user( - username, password) - - except socket.timeout: - return self.fail( - self.tr("Error connecting to provider (timeout)")) - - except requests.exceptions.ConnectionError as exc: - logger.error(exc.message) - return self.fail( - self.tr('Error Connecting to provider (connerr).')) - except Exception as exc: - return self.fail(exc.message) - - # XXX check for != OK instead??? - - if req.status_code in (404, 500): - return self.fail( - self.tr( - "Error during registration (%s)") % req.status_code) - - try: - validation_msgs = json.loads(req.content) - errors = validation_msgs.get('errors', None) - logger.debug('validation errors: %s' % validation_msgs) - except ValueError: - # probably bad json returned - return self.fail( - self.tr( - "Could not register (bad response)")) - - if errors and errors.get('login', None): - # XXX this sometimes catch the blank username - # but we're not allowing that (soon) - return self.fail( - self.tr('Username not available.')) - - return True - - logger.debug('registering user') - yield(("Registering username", 40), register) - - self.set_done() - yield(("end_sentinel", 100), lambda: None) - - def on_checks_validation_ready(self): - """ - after checks - """ - if self.is_done(): - self.disableFields() - self.cleanup_errormsg() - self.clean_wizard_errors(self.current_page) - # make the user confirm the transition - # to next page. - self.commitText('Connect!') - self.commitFocus() - self.green_validation_status() - self.do_confirm_next = True - - # pagewizard methods - - def validatePage(self): - """ - if not register done, do checks. - if done, wait for click. - """ - self.disableCommitButton() - self.cleanup_errormsg() - self.clean_wizard_errors(self.current_page) - - # After a successful validation - # (ie, success register with server) - # we change the commit button text - # and set this flag to True. - if self.do_confirm_next: - return True - - if not self.is_done(): - # calls checks, which after successful - # execution will call on_checks_validation_ready - self.reset_validation_status() - self.do_checks() - - return self.is_done() - - def initializePage(self): - """ - inits wizard page - """ - provider = unicode(self.field('provider_domain')) - if provider: - # here we should have provider - # but in tests we might not. - - # XXX this error causes a segfault on free() - # that we might want to get fixed ... - #self.setSubTitle( - #self.tr("Register a new user with provider %s.") % - #provider) - self.setSubTitle( - self.tr("Register a new user with provider %s" % - provider)) - self.validationMsg.setText('') - self.userPassword2LineEdit.setText('') - self.valFrame.hide() - - def nextId(self): - wizard = self.wizard() - return wizard.get_page_index('connect') diff --git a/src/leap/gui/firstrun/tests/integration/fake_provider.py b/src/leap/gui/firstrun/tests/integration/fake_provider.py deleted file mode 100755 index 668db5d1..00000000 --- a/src/leap/gui/firstrun/tests/integration/fake_provider.py +++ /dev/null @@ -1,302 +0,0 @@ -#!/usr/bin/env python -"""A server faking some of the provider resources and apis, -used for testing Leap Client requests - -It needs that you create a subfolder named 'certs', -and that you place the following files: - -[ ] certs/leaptestscert.pem -[ ] certs/leaptestskey.pem -[ ] certs/cacert.pem -[ ] certs/openvpn.pem - -[ ] provider.json -[ ] eip-service.json -""" -# XXX NOTE: intended for manual debug. -# I intend to include this as a regular test after 0.2.0 release -# (so we can add twisted as a dep there) -import binascii -import json -import os -import sys - -# python SRP LIB (! important MUST be >=1.0.1 !) -import srp - -# GnuTLS Example -- is not working as expected -#from gnutls import crypto -#from gnutls.constants import COMP_LZO, COMP_DEFLATE, COMP_NULL -#from gnutls.interfaces.twisted import X509Credentials - -# Going with OpenSSL as a workaround instead -# But we DO NOT want to introduce this dependency. -from OpenSSL import SSL - -from zope.interface import Interface, Attribute, implements - -from twisted.web.server import Site -from twisted.web.static import File -from twisted.web.resource import Resource -from twisted.internet import reactor - -from leap.testing.https_server import where - -# See -# http://twistedmatrix.com/documents/current/web/howto/web-in-60/index.htmln -# for more examples - -""" -Testing the FAKE_API: -##################### - - 1) register an user - >> curl -d "user[login]=me" -d "user[password_salt]=foo" \ - -d "user[password_verifier]=beef" http://localhost:8000/1/users.json - << {"errors": null} - - 2) check that if you try to register again, it will fail: - >> curl -d "user[login]=me" -d "user[password_salt]=foo" \ - -d "user[password_verifier]=beef" http://localhost:8000/1/users.json - << {"errors": {"login": "already taken!"}} - -""" - -# Globals to mock user/sessiondb - -USERDB = {} -SESSIONDB = {} - - -safe_unhexlify = lambda x: binascii.unhexlify(x) \ - if (len(x) % 2 == 0) else binascii.unhexlify('0' + x) - - -class IUser(Interface): - login = Attribute("User login.") - salt = Attribute("Password salt.") - verifier = Attribute("Password verifier.") - session = Attribute("Session.") - svr = Attribute("Server verifier.") - - -class User(object): - implements(IUser) - - def __init__(self, login, salt, verifier): - self.login = login - self.salt = salt - self.verifier = verifier - self.session = None - - def set_server_verifier(self, svr): - self.svr = svr - - def set_session(self, session): - SESSIONDB[session] = self - self.session = session - - -class FakeUsers(Resource): - def __init__(self, name): - self.name = name - - def render_POST(self, request): - args = request.args - - login = args['user[login]'][0] - salt = args['user[password_salt]'][0] - verifier = args['user[password_verifier]'][0] - - if login in USERDB: - return "%s\n" % json.dumps( - {'errors': {'login': 'already taken!'}}) - - print login, verifier, salt - user = User(login, salt, verifier) - USERDB[login] = user - return json.dumps({'errors': None}) - - -def get_user(request): - login = request.args.get('login') - if login: - user = USERDB.get(login[0], None) - if user: - return user - - session = request.getSession() - user = SESSIONDB.get(session, None) - return user - - -class FakeSession(Resource): - def __init__(self, name): - self.name = name - - def render_GET(self, request): - return "%s\n" % json.dumps({'errors': None}) - - def render_POST(self, request): - - user = get_user(request) - - if not user: - # XXX get real error from demo provider - return json.dumps({'errors': 'no such user'}) - - A = request.args['A'][0] - - _A = safe_unhexlify(A) - _salt = safe_unhexlify(user.salt) - _verifier = safe_unhexlify(user.verifier) - - svr = srp.Verifier( - user.login, - _salt, - _verifier, - _A, - hash_alg=srp.SHA256, - ng_type=srp.NG_1024) - - s, B = svr.get_challenge() - - _B = binascii.hexlify(B) - - print 'login = %s' % user.login - print 'salt = %s' % user.salt - print 'len(_salt) = %s' % len(_salt) - print 'vkey = %s' % user.verifier - print 'len(vkey) = %s' % len(_verifier) - print 's = %s' % binascii.hexlify(s) - print 'B = %s' % _B - print 'len(B) = %s' % len(_B) - - session = request.getSession() - user.set_session(session) - user.set_server_verifier(svr) - - # yep, this is tricky. - # some things are *already* unhexlified. - data = { - 'salt': user.salt, - 'B': _B, - 'errors': None} - - return json.dumps(data) - - def render_PUT(self, request): - - # XXX check session??? - user = get_user(request) - - if not user: - print 'NO USER' - return json.dumps({'errors': 'no such user'}) - - data = request.content.read() - auth = data.split("client_auth=") - M = auth[1] if len(auth) > 1 else None - # if not H, return - if not M: - return json.dumps({'errors': 'no M proof passed by client'}) - - svr = user.svr - HAMK = svr.verify_session(binascii.unhexlify(M)) - if HAMK is None: - print 'verification failed!!!' - raise Exception("Authentication failed!") - #import ipdb;ipdb.set_trace() - - assert svr.authenticated() - print "***" - print 'server authenticated user SRP!' - print "***" - - return json.dumps( - {'M2': binascii.hexlify(HAMK), 'errors': None}) - - -class API_Sessions(Resource): - def getChild(self, name, request): - return FakeSession(name) - - -def get_certs_path(): - script_path = os.path.realpath(os.path.dirname(sys.argv[0])) - certs_path = os.path.join(script_path, 'certs') - return certs_path - - -def get_TLS_credentials(): - # XXX this is giving errors - # XXX REview! We want to use gnutls! - - cert = crypto.X509Certificate( - open(where('leaptestscert.pem')).read()) - key = crypto.X509PrivateKey( - open(where('leaptestskey.pem')).read()) - ca = crypto.X509Certificate( - open(where('cacert.pem')).read()) - #crl = crypto.X509CRL(open(certs_path + '/crl.pem').read()) - #cred = crypto.X509Credentials(cert, key, [ca], [crl]) - cred = X509Credentials(cert, key, [ca]) - cred.verify_peer = True - cred.session_params.compressions = (COMP_LZO, COMP_DEFLATE, COMP_NULL) - return cred - - -class OpenSSLServerContextFactory: - # XXX workaround for broken TLS interface - # from gnuTLS. - - def getContext(self): - """Create an SSL context. - This is a sample implementation that loads a certificate from a file - called 'server.pem'.""" - - ctx = SSL.Context(SSL.SSLv23_METHOD) - #certs_path = get_certs_path() - #ctx.use_certificate_file(certs_path + '/leaptestscert.pem') - #ctx.use_privatekey_file(certs_path + '/leaptestskey.pem') - ctx.use_certificate_file(where('leaptestscert.pem')) - ctx.use_privatekey_file(where('leaptestskey.pem')) - return ctx - - -def serve_fake_provider(): - root = Resource() - root.putChild("provider.json", File("./provider.json")) - config = Resource() - config.putChild( - "eip-service.json", - File("./eip-service.json")) - apiv1 = Resource() - apiv1.putChild("config", config) - apiv1.putChild("sessions.json", API_Sessions()) - apiv1.putChild("users.json", FakeUsers(None)) - apiv1.putChild("cert", File(get_certs_path() + '/openvpn.pem')) - root.putChild("1", apiv1) - - cred = get_TLS_credentials() - - factory = Site(root) - - # regular http (for debugging with curl) - reactor.listenTCP(8000, factory) - - # TLS with gnutls --- seems broken :( - #reactor.listenTLS(8003, factory, cred) - - # OpenSSL - reactor.listenSSL(8443, factory, OpenSSLServerContextFactory()) - - reactor.run() - - -if __name__ == "__main__": - - from twisted.python import log - log.startLogging(sys.stdout) - - serve_fake_provider() diff --git a/src/leap/gui/firstrun/wizard.py b/src/leap/gui/firstrun/wizard.py deleted file mode 100755 index f198dca0..00000000 --- a/src/leap/gui/firstrun/wizard.py +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env python -import logging - -import sip -try: - sip.setapi('QString', 2) - sip.setapi('QVariant', 2) -except ValueError: - pass - -from PyQt4 import QtCore -from PyQt4 import QtGui - -from leap.base import checks as basechecks -from leap.crypto import leapkeyring -from leap.eip import checks as eipchecks - -from leap.gui import firstrun - -from leap.gui import mainwindow_rc - -try: - from collections import OrderedDict -except ImportError: - # We must be in 2.6 - from leap.util.dicts import OrderedDict - -logger = logging.getLogger(__name__) - -""" -~~~~~~~~~~~~~~~~~~~~~~~~~~ -Work in progress! -~~~~~~~~~~~~~~~~~~~~~~~~~~ -This wizard still needs to be refactored out. - -TODO-ish: - -[X] Break file in wizard / pages files (and its own folder). -[ ] Separate presentation from logic. -[ ] Have a "manager" class for connections, that can be - dep-injected for testing. -[ ] Document signals used / expected. -[ ] Separate style from widgets. -[ ] Fix TOFU Widget for provider cert. -[X] Refactor widgets out. -[ ] Follow more MVC style. -[ ] Maybe separate "first run wizard" into different wizards - that share some of the pages? -""" - - -def get_pages_dict(): - return OrderedDict(( - ('intro', firstrun.intro.IntroPage), - ('providerselection', - firstrun.providerselect.SelectProviderPage), - ('login', firstrun.login.LogInPage), - ('providerinfo', firstrun.providerinfo.ProviderInfoPage), - ('providersetupvalidation', - firstrun.providersetup.ProviderSetupValidationPage), - ('signup', firstrun.register.RegisterUserPage), - ('connect', - firstrun.connect.ConnectionPage), - ('lastpage', firstrun.last.LastPage) - )) - - -class FirstRunWizard(QtGui.QWizard): - - def __init__( - self, - conductor_instance, - parent=None, - pages_dict=None, - username=None, - providers=None, - success_cb=None, is_provider_setup=False, - trusted_certs=None, - netchecker=basechecks.LeapNetworkChecker, - providercertchecker=eipchecks.ProviderCertChecker, - eipconfigchecker=eipchecks.EIPConfigChecker, - start_eipconnection_signal=None, - eip_statuschange_signal=None, - debug_server=None, - quitcallback=None): - super(FirstRunWizard, self).__init__( - parent, - QtCore.Qt.WindowStaysOnTopHint) - - # we keep a reference to the conductor - # to be able to launch eip checks and connection - # in the connection page, before the wizard has ended. - self.conductor = conductor_instance - - self.username = username - self.providers = providers - - # success callback - self.success_cb = success_cb - - # is provider setup? - self.is_provider_setup = is_provider_setup - - # a dict with trusted fingerprints - # in the form {'nospacesfingerprint': ['host1', 'host2']} - self.trusted_certs = trusted_certs - - # Checkers - self.netchecker = netchecker - self.providercertchecker = providercertchecker - self.eipconfigchecker = eipconfigchecker - - # debug server - self.debug_server = debug_server - - # Signals - # will be emitted in connecting page - self.start_eipconnection_signal = start_eipconnection_signal - self.eip_statuschange_signal = eip_statuschange_signal - - if quitcallback is not None: - self.button( - QtGui.QWizard.CancelButton).clicked.connect( - quitcallback) - - self.providerconfig = None - # previously registered - # if True, jumps to LogIn page. - # by setting 1st page?? - #self.is_previously_registered = is_previously_registered - # XXX ??? ^v - self.is_previously_registered = bool(self.username) - self.from_login = False - - pages_dict = pages_dict or get_pages_dict() - self.add_pages_from_dict(pages_dict) - - self.validation_errors = {} - self.openvpn_status = [] - - self.setPixmap( - QtGui.QWizard.BannerPixmap, - QtGui.QPixmap(':/images/banner.png')) - self.setPixmap( - QtGui.QWizard.BackgroundPixmap, - QtGui.QPixmap(':/images/background.png')) - - # set options - self.setOption(QtGui.QWizard.IndependentPages, on=False) - self.setOption(QtGui.QWizard.NoBackButtonOnStartPage, on=True) - - self.setWindowTitle("First Run Wizard") - - # TODO: set style for MAC / windows ... - #self.setWizardStyle() - - # - # setup pages in wizard - # - - def add_pages_from_dict(self, pages_dict): - """ - @param pages_dict: the dictionary with pages, where - values are a tuple of InstanceofWizardPage, kwargs. - @type pages_dict: dict - """ - for name, page in pages_dict.items(): - # XXX check for is_previously registered - # and skip adding the signup branch if so - self.addPage(page()) - self.pages_dict = pages_dict - - def get_page_index(self, page_name): - """ - returns the index of the given page - @param page_name: the name of the desired page - @type page_name: str - @rparam: index of page in wizard - @rtype: int - """ - return self.pages_dict.keys().index(page_name) - - # - # validation errors - # - - def set_validation_error(self, pagename, error): - self.validation_errors[pagename] = error - - def clean_validation_error(self, pagename): - vald = self.validation_errors - if pagename in vald: - del vald[pagename] - - def get_validation_error(self, pagename): - return self.validation_errors.get(pagename, None) - - def accept(self): - """ - final step in the wizard. - gather the info, update settings - and call the success callback if any has been passed. - """ - super(FirstRunWizard, self).accept() - - # username and password are in different fields - # if they were stored in log_in or sign_up pages. - from_login = self.from_login - unamek_base = 'userName' - passwk_base = 'userPassword' - unamek = 'login_%s' % unamek_base if from_login else unamek_base - passwk = 'login_%s' % passwk_base if from_login else passwk_base - - username = self.field(unamek) - password = self.field(passwk) - provider = self.field('provider_domain') - remember_pass = self.field('rememberPassword') - - logger.debug('chosen provider: %s', provider) - logger.debug('username: %s', username) - logger.debug('remember password: %s', remember_pass) - - # we are assuming here that we only remember one username - # in the form username@provider.domain - # We probably could extend this to support some form of - # profiles. - - settings = QtCore.QSettings() - - settings.setValue("FirstRunWizardDone", True) - settings.setValue("provider_domain", provider) - full_username = "%s@%s" % (username, provider) - - settings.setValue("remember_user_and_pass", remember_pass) - - if remember_pass: - settings.setValue("username", full_username) - seed = self.get_random_str(10) - settings.setValue("%s_seed" % provider, seed) - - # XXX #744: comment out for 0.2.0 release - # if we need to have a version of python-keyring < 0.9 - leapkeyring.leap_set_password( - full_username, password, seed=seed) - - logger.debug('First Run Wizard Done.') - cb = self.success_cb - if cb and callable(cb): - self.success_cb() - - # misc helpers - - def get_random_str(self, n): - """ - returns a random string - :param n: the length of the desired string - :rvalue: str - """ - from string import (ascii_uppercase, ascii_lowercase, digits) - from random import choice - return ''.join(choice( - ascii_uppercase + - ascii_lowercase + - digits) for x in range(n)) - - def set_providerconfig(self, providerconfig): - """ - sets a providerconfig attribute - used when we fetch and parse a json configuration - """ - self.providerconfig = providerconfig - - def get_provider_by_index(self): # pragma: no cover - """ - returns the value of a provider given its index. - this was used in the select provider page, - in the case where we were preseeding providers in a combobox - """ - # Leaving it here for the moment when we go back at the - # option of preseeding with known provider values. - provider = self.field('provider_index') - return self.providers[provider] - - -if __name__ == '__main__': - # standalone test - # it can be (somehow) run against - # gui/tests/integration/fake_user_signup.py - - import sys - import logging - logging.basicConfig() - logger = logging.getLogger() - logger.setLevel(logging.DEBUG) - - app = QtGui.QApplication(sys.argv) - server = sys.argv[1] if len(sys.argv) > 1 else None - - trusted_certs = { - "3DF83F316BFA0186" - "0A11A5C9C7FC24B9" - "18C62B941192CC1A" - "49AE62218B2A4B7C": ['springbok']} - - wizard = FirstRunWizard( - None, trusted_certs=trusted_certs, - debug_server=server) - wizard.show() - sys.exit(app.exec_()) diff --git a/src/leap/gui/locale_rc.py b/src/leap/gui/locale_rc.py deleted file mode 100644 index 8c383709..00000000 --- a/src/leap/gui/locale_rc.py +++ /dev/null @@ -1,813 +0,0 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created: Fri Jan 25 18:19:04 2013 -# by: The Resource Compiler for PyQt (Qt v4.8.2) -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore - -qt_resource_data = "\ -\x00\x00\x17\x94\ -\x3c\ -\xb8\x64\x18\xca\xef\x9c\x95\xcd\x21\x1c\xbf\x60\xa1\xbd\xdd\x42\ -\x00\x00\x01\x30\x00\x8f\x9b\xbe\x00\x00\x14\x69\x01\x23\x92\xe5\ -\x00\x00\x10\x2f\x01\x87\x64\x8e\x00\x00\x08\xbe\x01\xa8\xbe\x7e\ -\x00\x00\x0d\xf4\x02\x2c\xac\xe9\x00\x00\x0b\x9c\x02\x3a\xce\xbf\ -\x00\x00\x15\xe2\x02\x6e\x0f\xe5\x00\x00\x09\x2d\x02\x87\x60\x9e\ -\x00\x00\x06\xc6\x02\xaa\x52\x6e\x00\x00\x07\xc9\x02\xf2\xe0\x59\ -\x00\x00\x0a\x6c\x03\xec\x70\x0e\x00\x00\x10\x9c\x04\xd4\x45\xee\ -\x00\x00\x0d\x3c\x05\xb7\x8f\x59\x00\x00\x0c\x35\x06\x3e\x6a\x9e\ -\x00\x00\x06\x01\x06\x40\xa8\x7e\x00\x00\x0b\x02\x06\xee\xff\x6e\ -\x00\x00\x13\x50\x08\x13\xe8\xae\x00\x00\x0c\xc2\x08\x7a\x64\xee\ -\x00\x00\x11\x8b\x08\xe6\x98\x33\x00\x00\x05\x93\x08\xe6\x98\x33\ -\x00\x00\x0f\xb0\x09\x5c\x35\xe1\x00\x00\x0e\x96\x09\x74\x75\x4e\ -\x00\x00\x0d\x9c\x09\x98\x34\x0e\x00\x00\x12\x55\x09\xd8\x1f\x95\ -\x00\x00\x15\x19\x09\xfc\x2c\x8e\x00\x00\x05\x19\x09\xfe\x05\x90\ -\x00\x00\x0f\x06\x0a\x74\xb8\x1e\x00\x00\x00\xe6\x0a\xfd\x99\xfe\ -\x00\x00\x00\x6d\x0b\xd2\x4b\x3f\x00\x00\x07\x7d\x0c\x44\x41\xbe\ -\x00\x00\x00\x00\x0c\xc0\x94\x05\x00\x00\x09\xf2\x0d\x0d\x9d\xc5\ -\x00\x00\x06\x5f\x0d\x15\x34\x70\x00\x00\x09\x98\x0e\x36\x15\x54\ -\x00\x00\x08\x47\x0e\x7e\xf5\xee\x00\x00\x0f\x42\x0e\x91\x50\x3e\ -\x00\x00\x15\x76\x0e\xc0\xbb\x72\x00\x00\x12\xfb\x0f\x27\x0d\x6e\ -\x00\x00\x11\x22\x69\x00\x00\x16\x43\x03\x00\x00\x00\x3e\x00\x41\ -\x00\x73\x00\x73\x00\x69\x00\x73\x00\x74\x00\x65\x00\x6e\x00\x74\ -\x00\x20\x00\x66\x00\xfc\x00\x72\x00\x20\x00\x65\x00\x72\x00\x73\ -\x00\x74\x00\x6d\x00\x61\x00\x6c\x00\x69\x00\x67\x00\x65\x00\x6e\ -\x00\x20\x00\x53\x00\x74\x00\x61\x00\x72\x00\x74\x08\x00\x00\x00\ -\x00\x06\x00\x00\x00\x11\x46\x69\x72\x73\x74\x20\x72\x75\x6e\x20\ -\x77\x69\x7a\x61\x72\x64\x2e\x07\x00\x00\x00\x09\x49\x6e\x74\x72\ -\x6f\x50\x61\x67\x65\x01\x03\x00\x00\x00\x40\x00\x4d\x00\x69\x00\ -\x74\x00\x20\x00\x62\x00\x65\x00\x73\x00\x74\x00\x65\x00\x68\x00\ -\x65\x00\x6e\x00\x64\x00\x65\x00\x6e\x00\x20\x00\x44\x00\x61\x00\ -\x74\x00\x65\x00\x6e\x00\x20\x00\x65\x00\x69\x00\x6e\x00\x6c\x00\ -\x6f\x00\x67\x00\x67\x00\x65\x00\x6e\x00\x2e\x08\x00\x00\x00\x00\ -\x06\x00\x00\x00\x1b\x4c\x6f\x67\x20\x49\x6e\x20\x77\x69\x74\x68\ -\x20\x6d\x79\x20\x63\x72\x65\x64\x65\x6e\x74\x69\x61\x6c\x73\x2e\ -\x07\x00\x00\x00\x09\x49\x6e\x74\x72\x6f\x50\x61\x67\x65\x01\x03\ -\x00\x00\x02\xb8\x00\x57\x00\x69\x00\x72\x00\x20\x00\x77\x00\x65\ -\x00\x72\x00\x64\x00\x65\x00\x6e\x00\x20\x00\x64\x00\x69\x00\x63\ -\x00\x68\x00\x20\x00\x6e\x00\x75\x00\x6e\x00\x20\x00\x64\x00\x75\ -\x00\x72\x00\x63\x00\x68\x00\x20\x00\x65\x00\x69\x00\x6e\x00\x69\ -\x00\x67\x00\x65\x00\x20\x00\x4b\x00\x6f\x00\x6e\x00\x66\x00\x69\ -\x00\x67\x00\x75\x00\x72\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\ -\x00\x65\x00\x6e\x00\x20\x00\x66\x00\xfc\x00\x68\x00\x72\x00\x65\ -\x00\x6e\x00\x2c\x00\x20\x00\x64\x00\x69\x00\x65\x00\x20\x00\x64\ -\x00\x75\x00\x20\x00\x66\x00\xfc\x00\x72\x00\x20\x00\x64\x00\x65\ -\x00\x6e\x00\x20\x00\x65\x00\x72\x00\x73\x00\x74\x00\x65\x00\x6e\ -\x00\x20\x00\x53\x00\x74\x00\x61\x00\x72\x00\x74\x00\x20\x00\x62\ -\x00\x65\x00\x6e\x00\xf6\x00\x74\x00\x69\x00\x67\x00\x73\x00\x74\ -\x00\x2e\x00\x3c\x00\x62\x00\x72\x00\x3e\x00\x3c\x00\x62\x00\x72\ -\x00\x3e\x00\x57\x00\x65\x00\x6e\x00\x6e\x00\x20\x00\x64\x00\x75\ -\x00\x20\x00\x64\x00\x69\x00\x65\x00\x73\x00\x65\x00\x20\x00\x4b\ -\x00\x6f\x00\x6e\x00\x66\x00\x69\x00\x67\x00\x75\x00\x72\x00\x61\ -\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x65\x00\x6e\x00\x20\x00\x6a\ -\x00\x65\x00\x6d\x00\x61\x00\x6c\x00\x73\x00\x20\x00\xe4\x00\x6e\ -\x00\x64\x00\x65\x00\x72\x00\x6e\x00\x20\x00\x6d\x00\x75\x00\x73\ -\x00\x73\x00\x74\x00\x2c\x00\x20\x00\x66\x00\x69\x00\x6e\x00\x64\ -\x00\x65\x00\x73\x00\x74\x00\x20\x00\x64\x00\x75\x00\x20\x00\x64\ -\x00\x65\x00\x6e\x00\x20\x00\x41\x00\x73\x00\x73\x00\x69\x00\x73\ -\x00\x74\x00\x65\x00\x6e\x00\x74\x00\x65\x00\x6e\x00\x20\x00\x69\ -\x00\x6d\x00\x20\x00\x27\x00\x3c\x00\x69\x00\x3e\x00\x45\x00\x69\ -\x00\x6e\x00\x73\x00\x74\x00\x65\x00\x6c\x00\x6c\x00\x75\x00\x6e\ -\x00\x67\x00\x65\x00\x6e\x00\x3c\x00\x2f\x00\x69\x00\x3e\x00\x27\ -\x00\x2d\x00\x4d\x00\x65\x00\x6e\x00\xfc\x00\x20\x00\x64\x00\x65\ -\x00\x73\x00\x20\x00\x48\x00\x61\x00\x75\x00\x70\x00\x66\x00\x65\ -\x00\x6e\x00\x73\x00\x74\x00\x65\x00\x72\x00\x73\x00\x2e\x00\x3c\ -\x00\x62\x00\x72\x00\x3e\x00\x3c\x00\x62\x00\x72\x00\x3e\x00\x4d\ -\x00\xf6\x00\x63\x00\x68\x00\x74\x00\x65\x00\x73\x00\x74\x00\x20\ -\x00\x64\x00\x75\x00\x20\x00\x64\x00\x69\x00\x63\x00\x68\x00\x20\ -\x00\x66\x00\xfc\x00\x72\x00\x20\x00\x65\x00\x69\x00\x6e\x00\x65\ -\x00\x6e\x00\x20\x00\x6e\x00\x65\x00\x75\x00\x65\x00\x6e\x00\x20\ -\x00\x41\x00\x63\x00\x63\x00\x6f\x00\x75\x00\x6e\x00\x74\x00\x20\ -\x00\x3c\x00\x62\x00\x3e\x00\x61\x00\x6e\x00\x6d\x00\x65\x00\x6c\ -\x00\x64\x00\x65\x00\x6e\x00\x3c\x00\x2f\x00\x62\x00\x3e\x00\x20\ -\x00\x6f\x00\x64\x00\x65\x00\x72\x00\x20\x00\x6d\x00\x69\x00\x74\ -\x00\x20\x00\x65\x00\x69\x00\x6e\x00\x65\x00\x6d\x00\x20\x00\x62\ -\x00\x65\x00\x73\x00\x74\x00\x65\x00\x68\x00\x65\x00\x6e\x00\x64\ -\x00\x65\x00\x6e\x00\x20\x00\x55\x00\x73\x00\x65\x00\x72\x00\x6e\ -\x00\x61\x00\x6d\x00\x65\x00\x6e\x00\x20\x00\x3c\x00\x62\x00\x3e\ -\x00\x65\x00\x69\x00\x6e\x00\x6c\x00\x6f\x00\x67\x00\x67\x00\x65\ -\x00\x6e\x00\x3c\x00\x2f\x00\x62\x00\x3e\x00\x3f\x08\x00\x00\x00\ -\x00\x06\x00\x00\x01\x5d\x4e\x6f\x77\x20\x77\x65\x20\x77\x69\x6c\ -\x6c\x20\x67\x75\x69\x64\x65\x20\x79\x6f\x75\x20\x74\x68\x72\x6f\ -\x75\x67\x68\x20\x73\x6f\x6d\x65\x20\x63\x6f\x6e\x66\x69\x67\x75\ -\x72\x61\x74\x69\x6f\x6e\x20\x74\x68\x61\x74\x20\x69\x73\x20\x6e\ -\x65\x65\x64\x65\x64\x20\x62\x65\x66\x6f\x72\x65\x20\x79\x6f\x75\ -\x20\x63\x61\x6e\x20\x63\x6f\x6e\x6e\x65\x63\x74\x20\x66\x6f\x72\ -\x20\x74\x68\x65\x20\x66\x69\x72\x73\x74\x20\x74\x69\x6d\x65\x2e\ -\x3c\x62\x72\x3e\x3c\x62\x72\x3e\x49\x66\x20\x79\x6f\x75\x20\x65\ -\x76\x65\x72\x20\x6e\x65\x65\x64\x20\x74\x6f\x20\x6d\x6f\x64\x69\ -\x66\x79\x20\x74\x68\x65\x73\x65\x20\x6f\x70\x74\x69\x6f\x6e\x73\ -\x20\x61\x67\x61\x69\x6e\x2c\x20\x79\x6f\x75\x20\x63\x61\x6e\x20\ -\x66\x69\x6e\x64\x20\x74\x68\x65\x20\x77\x69\x7a\x61\x72\x64\x20\ -\x69\x6e\x20\x74\x68\x65\x20\x27\x3c\x69\x3e\x53\x65\x74\x74\x69\ -\x6e\x67\x73\x3c\x2f\x69\x3e\x27\x20\x6d\x65\x6e\x75\x20\x66\x72\ -\x6f\x6d\x20\x74\x68\x65\x20\x6d\x61\x69\x6e\x20\x77\x69\x6e\x64\ -\x6f\x77\x2e\x3c\x62\x72\x3e\x3c\x62\x72\x3e\x44\x6f\x20\x79\x6f\ -\x75\x20\x77\x61\x6e\x74\x20\x74\x6f\x20\x3c\x62\x3e\x73\x69\x67\ -\x6e\x20\x75\x70\x3c\x2f\x62\x3e\x20\x66\x6f\x72\x20\x61\x20\x6e\ -\x65\x77\x20\x61\x63\x63\x6f\x75\x6e\x74\x2c\x20\x6f\x72\x20\x3c\ -\x62\x3e\x6c\x6f\x67\x20\x69\x6e\x3c\x2f\x62\x3e\x20\x77\x69\x74\ -\x68\x20\x61\x6e\x20\x61\x6c\x72\x65\x61\x64\x79\x20\x65\x78\x69\ -\x73\x74\x69\x6e\x67\x20\x75\x73\x65\x72\x6e\x61\x6d\x65\x3f\x3c\ -\x62\x72\x3e\x07\x00\x00\x00\x09\x49\x6e\x74\x72\x6f\x50\x61\x67\ -\x65\x01\x03\x00\x00\x00\x42\x00\x46\x00\xfc\x00\x72\x00\x20\x00\ -\x65\x00\x69\x00\x6e\x00\x65\x00\x6e\x00\x20\x00\x6e\x00\x65\x00\ -\x75\x00\x65\x00\x6e\x00\x20\x00\x41\x00\x63\x00\x63\x00\x6f\x00\ -\x75\x00\x6e\x00\x74\x00\x20\x00\x61\x00\x6e\x00\x6d\x00\x65\x00\ -\x6c\x00\x64\x00\x65\x00\x6e\x00\x2e\x08\x00\x00\x00\x00\x06\x00\ -\x00\x00\x1a\x53\x69\x67\x6e\x20\x75\x70\x20\x66\x6f\x72\x20\x61\ -\x20\x6e\x65\x77\x20\x61\x63\x63\x6f\x75\x6e\x74\x2e\x07\x00\x00\ -\x00\x09\x49\x6e\x74\x72\x6f\x50\x61\x67\x65\x01\x03\x00\x00\x00\ -\x38\x00\x41\x00\x75\x00\x74\x00\x68\x00\x65\x00\x6e\x00\x74\x00\ -\x69\x00\x66\x00\x69\x00\x7a\x00\x69\x00\x65\x00\x72\x00\x75\x00\ -\x6e\x00\x67\x00\x73\x00\x66\x00\x65\x00\x68\x00\x6c\x00\x65\x00\ -\x72\x00\x3a\x00\x20\x00\x25\x00\x73\x08\x00\x00\x00\x00\x06\x00\ -\x00\x00\x18\x41\x75\x74\x68\x65\x6e\x74\x69\x63\x61\x74\x69\x6f\ -\x6e\x20\x65\x72\x72\x6f\x72\x3a\x20\x25\x73\x07\x00\x00\x00\x09\ -\x4c\x6f\x67\x49\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x2a\x00\ -\x41\x00\x6e\x00\x6d\x00\x65\x00\x6c\x00\x64\x00\x65\x00\x64\x00\ -\x61\x00\x74\x00\x65\x00\x6e\x00\x20\x00\x6b\x00\x6f\x00\x72\x00\ -\x72\x00\x65\x00\x6b\x00\x74\x00\x2e\x08\x00\x00\x00\x00\x06\x00\ -\x00\x00\x16\x43\x72\x65\x64\x65\x6e\x74\x69\x61\x6c\x73\x20\x76\ -\x61\x6c\x69\x64\x61\x74\x65\x64\x2e\x07\x00\x00\x00\x09\x4c\x6f\ -\x67\x49\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x34\x00\x41\x00\ -\x75\x00\x66\x00\x6c\x00\xf6\x00\x73\x00\x65\x00\x6e\x00\x20\x00\ -\x64\x00\x65\x00\x73\x00\x20\x00\x44\x00\x6f\x00\x6d\x00\x61\x00\ -\x69\x00\x6e\x00\x2d\x00\x4e\x00\x61\x00\x6d\x00\x65\x00\x6e\x00\ -\x73\x08\x00\x00\x00\x00\x06\x00\x00\x00\x15\x52\x65\x73\x6f\x6c\ -\x76\x69\x6e\x67\x20\x64\x6f\x6d\x61\x69\x6e\x20\x6e\x61\x6d\x65\ -\x07\x00\x00\x00\x09\x4c\x6f\x67\x49\x6e\x50\x61\x67\x65\x01\x03\ -\x00\x00\x00\x6a\x00\x44\x00\x65\x00\x72\x00\x20\x00\x55\x00\x73\ -\x00\x65\x00\x72\x00\x6e\x00\x61\x00\x6d\x00\x65\x00\x20\x00\x6d\ -\x00\x75\x00\x73\x00\x73\x00\x20\x00\x69\x00\x6e\x00\x20\x00\x64\ -\x00\x65\x00\x72\x00\x20\x00\x46\x00\x6f\x00\x72\x00\x6d\x00\x20\ -\x00\x75\x00\x73\x00\x65\x00\x72\x00\x6e\x00\x61\x00\x6d\x00\x65\ -\x00\x40\x00\x70\x00\x72\x00\x6f\x00\x76\x00\x69\x00\x64\x00\x65\ -\x00\x72\x00\x20\x00\x73\x00\x65\x00\x69\x00\x6e\x00\x2e\x08\x00\ -\x00\x00\x00\x06\x00\x00\x00\x2f\x55\x73\x65\x72\x6e\x61\x6d\x65\ -\x20\x6d\x75\x73\x74\x20\x62\x65\x20\x69\x6e\x20\x74\x68\x65\x20\ -\x75\x73\x65\x72\x6e\x61\x6d\x65\x40\x70\x72\x6f\x76\x69\x64\x65\ -\x72\x20\x66\x6f\x72\x6d\x2e\x07\x00\x00\x00\x09\x4c\x6f\x67\x49\ -\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x1a\x00\x50\x00\x72\x00\ -\x6f\x00\x76\x00\x69\x00\x64\x00\x65\x00\x72\x00\x2d\x00\x69\x00\ -\x6e\x00\x66\x00\x6f\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0d\x50\ -\x72\x6f\x76\x69\x64\x65\x72\x20\x49\x6e\x66\x6f\x07\x00\x00\x00\ -\x10\x50\x72\x6f\x76\x69\x64\x65\x72\x49\x6e\x66\x6f\x50\x61\x67\ -\x65\x01\x03\x00\x00\x00\x3e\x00\x44\x00\x61\x00\x73\x00\x20\x00\ -\x69\x00\x73\x00\x74\x00\x2c\x00\x20\x00\x77\x00\x61\x00\x73\x00\ -\x20\x00\x64\x00\x65\x00\x72\x00\x20\x00\x50\x00\x72\x00\x6f\x00\ -\x76\x00\x69\x00\x64\x00\x65\x00\x72\x00\x20\x00\x73\x00\x61\x00\ -\x67\x00\x74\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1b\x54\ -\x68\x69\x73\x20\x69\x73\x20\x77\x68\x61\x74\x20\x70\x72\x6f\x76\ -\x69\x64\x65\x72\x20\x73\x61\x79\x73\x2e\x07\x00\x00\x00\x10\x50\ -\x72\x6f\x76\x69\x64\x65\x72\x49\x6e\x66\x6f\x50\x61\x67\x65\x01\ -\x03\x00\x00\x00\x30\x00\xdc\x00\x62\x00\x65\x00\x72\x00\x70\x00\ -\x72\x00\xfc\x00\x66\x00\x65\x00\x20\x00\x43\x00\x41\x00\x2d\x00\ -\x46\x00\x69\x00\x6e\x00\x67\x00\x65\x00\x72\x00\x70\x00\x72\x00\ -\x69\x00\x6e\x00\x74\x08\x00\x00\x00\x00\x06\x00\x00\x00\x17\x43\ -\x68\x65\x63\x6b\x69\x6e\x67\x20\x43\x41\x20\x66\x69\x6e\x67\x65\ -\x72\x70\x72\x69\x6e\x74\x07\x00\x00\x00\x1b\x50\x72\x6f\x76\x69\ -\x64\x65\x72\x53\x65\x74\x75\x70\x56\x61\x6c\x69\x64\x61\x74\x69\ -\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x2e\x00\x46\x00\xfc\ -\x00\x68\x00\x72\x00\x65\x00\x20\x00\x61\x00\x75\x00\x74\x00\x6f\ -\x00\x63\x00\x6f\x00\x6e\x00\x66\x00\x69\x00\x67\x00\x20\x00\x64\ -\x00\x75\x00\x72\x00\x63\x00\x68\x00\x2e\x08\x00\x00\x00\x00\x06\ -\x00\x00\x00\x11\x44\x6f\x69\x6e\x67\x20\x61\x75\x74\x6f\x63\x6f\ -\x6e\x66\x69\x67\x2e\x07\x00\x00\x00\x1b\x50\x72\x6f\x76\x69\x64\ -\x65\x72\x53\x65\x74\x75\x70\x56\x61\x6c\x69\x64\x61\x74\x69\x6f\ -\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x24\x00\x48\x00\x6f\x00\ -\x6c\x00\x65\x00\x20\x00\x43\x00\x41\x00\x2d\x00\x5a\x00\x65\x00\ -\x72\x00\x74\x00\x69\x00\x66\x00\x69\x00\x6b\x00\x61\x00\x74\x08\ -\x00\x00\x00\x00\x06\x00\x00\x00\x17\x46\x65\x74\x63\x68\x69\x6e\ -\x67\x20\x43\x41\x20\x63\x65\x72\x74\x69\x66\x69\x63\x61\x74\x65\ -\x07\x00\x00\x00\x1b\x50\x72\x6f\x76\x69\x64\x65\x72\x53\x65\x74\ -\x75\x70\x56\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\x50\x61\x67\x65\ -\x01\x03\x00\x00\x00\x1c\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x69\ -\x00\x64\x00\x65\x00\x72\x00\x2d\x00\x53\x00\x65\x00\x74\x00\x75\ -\x00\x70\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0e\x50\x72\x6f\x76\ -\x69\x64\x65\x72\x20\x73\x65\x74\x75\x70\x07\x00\x00\x00\x1b\x50\ -\x72\x6f\x76\x69\x64\x65\x72\x53\x65\x74\x75\x70\x56\x61\x6c\x69\ -\x64\x61\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x30\ -\x00\xdc\x00\x62\x00\x65\x00\x72\x00\x70\x00\x72\x00\xfc\x00\x66\ -\x00\x65\x00\x20\x00\x41\x00\x50\x00\x49\x00\x2d\x00\x5a\x00\x65\ -\x00\x72\x00\x74\x00\x69\x00\x66\x00\x69\x00\x6b\x00\x61\x00\x74\ -\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1a\x56\x61\x6c\x69\x64\x61\ -\x74\x69\x6e\x67\x20\x61\x70\x69\x20\x63\x65\x72\x74\x69\x66\x69\ -\x63\x61\x74\x65\x07\x00\x00\x00\x1b\x50\x72\x6f\x76\x69\x64\x65\ -\x72\x53\x65\x74\x75\x70\x56\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\ -\x50\x61\x67\x65\x01\x03\x00\x00\x00\x50\x00\x4b\x00\x6f\x00\x6e\ -\x00\x6e\x00\x74\x00\x65\x00\x20\x00\x6e\x00\x69\x00\x63\x00\x68\ -\x00\x74\x00\x20\x00\x72\x00\x65\x00\x67\x00\x69\x00\x73\x00\x74\ -\x00\x72\x00\x69\x00\x65\x00\x72\x00\x65\x00\x6e\x00\x20\x00\x28\ -\x00\x62\x00\x61\x00\x64\x00\x20\x00\x72\x00\x65\x00\x73\x00\x70\ -\x00\x6f\x00\x6e\x00\x73\x00\x65\x00\x29\x08\x00\x00\x00\x00\x06\ -\x00\x00\x00\x21\x43\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\x72\x65\ -\x67\x69\x73\x74\x65\x72\x20\x28\x62\x61\x64\x20\x72\x65\x73\x70\ -\x6f\x6e\x73\x65\x29\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\ -\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x4e\ -\x00\x56\x00\x65\x00\x72\x00\x62\x00\x69\x00\x6e\x00\x64\x00\x75\ -\x00\x6e\x00\x67\x00\x73\x00\x66\x00\x65\x00\x68\x00\x6c\x00\x65\ -\x00\x72\x00\x20\x00\x7a\x00\x75\x00\x20\x00\x50\x00\x72\x00\x6f\ -\x00\x76\x00\x69\x00\x64\x00\x65\x00\x72\x00\x20\x00\x28\x00\x63\ -\x00\x6f\x00\x6e\x00\x6e\x00\x65\x00\x72\x00\x72\x00\x29\x08\x00\ -\x00\x00\x00\x06\x00\x00\x00\x27\x45\x72\x72\x6f\x72\x20\x43\x6f\ -\x6e\x6e\x65\x63\x74\x69\x6e\x67\x20\x74\x6f\x20\x70\x72\x6f\x76\ -\x69\x64\x65\x72\x20\x28\x63\x6f\x6e\x6e\x65\x72\x72\x29\x2e\x07\ -\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\ -\x50\x61\x67\x65\x01\x03\x00\x00\x00\x4e\x00\x56\x00\x65\x00\x72\ -\x00\x62\x00\x69\x00\x6e\x00\x64\x00\x75\x00\x6e\x00\x67\x00\x73\ -\x00\x66\x00\x65\x00\x68\x00\x6c\x00\x65\x00\x72\x00\x20\x00\x7a\ -\x00\x75\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x69\x00\x64\ -\x00\x65\x00\x72\x00\x20\x00\x28\x00\x74\x00\x69\x00\x6d\x00\x65\ -\x00\x6f\x00\x75\x00\x74\x00\x29\x08\x00\x00\x00\x00\x06\x00\x00\ -\x00\x26\x45\x72\x72\x6f\x72\x20\x63\x6f\x6e\x6e\x65\x63\x74\x69\ -\x6e\x67\x20\x74\x6f\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x20\x28\ -\x74\x69\x6d\x65\x6f\x75\x74\x29\x07\x00\x00\x00\x10\x52\x65\x67\ -\x69\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\ -\x00\x00\x4a\x00\x46\x00\x65\x00\x68\x00\x6c\x00\x65\x00\x72\x00\ -\x20\x00\x77\x00\xe4\x00\x68\x00\x72\x00\x65\x00\x6e\x00\x64\x00\ -\x20\x00\x64\x00\x65\x00\x72\x00\x20\x00\x52\x00\x65\x00\x67\x00\ -\x69\x00\x73\x00\x74\x00\x72\x00\x69\x00\x65\x00\x72\x00\x75\x00\ -\x6e\x00\x67\x00\x20\x00\x28\x00\x25\x00\x73\x00\x29\x08\x00\x00\ -\x00\x00\x06\x00\x00\x00\x1e\x45\x72\x72\x6f\x72\x20\x64\x75\x72\ -\x69\x6e\x67\x20\x72\x65\x67\x69\x73\x74\x72\x61\x74\x69\x6f\x6e\ -\x20\x28\x25\x73\x29\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\ -\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x3c\ -\x00\x50\x00\x61\x00\x73\x00\x73\x00\x77\x00\x6f\x00\x72\x00\x74\ -\x00\x20\x00\x73\x00\x74\x00\x69\x00\x6d\x00\x6d\x00\x74\x00\x20\ -\x00\x6e\x00\x69\x00\x63\x00\x68\x00\x74\x00\x20\x00\xfc\x00\x62\ -\x00\x65\x00\x72\x00\x69\x00\x65\x00\x6e\x00\x2e\x08\x00\x00\x00\ -\x00\x06\x00\x00\x00\x19\x50\x61\x73\x73\x77\x6f\x72\x64\x20\x64\ -\x6f\x65\x73\x20\x6e\x6f\x74\x20\x6d\x61\x74\x63\x68\x2e\x2e\x07\ -\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\ -\x50\x61\x67\x65\x01\x03\x00\x00\x00\x26\x00\x50\x00\x61\x00\x73\ -\x00\x73\x00\x77\x00\x6f\x00\x72\x00\x74\x00\x20\x00\x7a\x00\x75\ -\x00\x20\x00\x73\x00\x69\x00\x6d\x00\x70\x00\x65\x00\x6c\x00\x2e\ -\x08\x00\x00\x00\x00\x06\x00\x00\x00\x15\x50\x61\x73\x73\x77\x6f\ -\x72\x64\x20\x74\x6f\x6f\x20\x6f\x62\x76\x69\x6f\x75\x73\x2e\x07\ -\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\ -\x50\x61\x67\x65\x01\x03\x00\x00\x00\x20\x00\x50\x00\x61\x00\x73\ -\x00\x73\x00\x77\x00\x6f\x00\x72\x00\x74\x00\x20\x00\x7a\x00\x75\ -\x00\x20\x00\x6b\x00\x75\x00\x72\x00\x7a\x08\x00\x00\x00\x00\x06\ -\x00\x00\x00\x13\x50\x61\x73\x73\x77\x6f\x72\x64\x20\x74\x6f\x6f\ -\x20\x73\x68\x6f\x72\x74\x2e\x07\x00\x00\x00\x10\x52\x65\x67\x69\ -\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\ -\x00\x58\x00\x52\x00\x65\x00\x67\x00\x69\x00\x73\x00\x74\x00\x72\ -\x00\x69\x00\x65\x00\x72\x00\x65\x00\x20\x00\x65\x00\x69\x00\x6e\ -\x00\x65\x00\x6e\x00\x20\x00\x6e\x00\x65\x00\x75\x00\x65\x00\x6e\ -\x00\x20\x00\x55\x00\x73\x00\x65\x00\x72\x00\x20\x00\x62\x00\x65\ -\x00\x69\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x69\x00\x64\ -\x00\x65\x00\x72\x00\x20\x00\x25\x00\x73\x08\x00\x00\x00\x00\x06\ -\x00\x00\x00\x25\x52\x65\x67\x69\x73\x74\x65\x72\x20\x61\x20\x6e\ -\x65\x77\x20\x75\x73\x65\x72\x20\x77\x69\x74\x68\x20\x70\x72\x6f\ -\x76\x69\x64\x65\x72\x20\x25\x73\x2e\x07\x00\x00\x00\x10\x52\x65\ -\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\ -\x00\x00\x00\x34\x00\x52\x00\x65\x00\x67\x00\x69\x00\x73\x00\x74\ -\x00\x72\x00\x69\x00\x65\x00\x72\x00\x75\x00\x6e\x00\x67\x00\x20\ -\x00\x65\x00\x72\x00\x66\x00\x6f\x00\x6c\x00\x67\x00\x72\x00\x65\ -\x00\x69\x00\x63\x00\x68\x00\x21\x08\x00\x00\x00\x00\x06\x00\x00\ -\x00\x17\x52\x65\x67\x69\x73\x74\x72\x61\x74\x69\x6f\x6e\x20\x73\ -\x75\x63\x63\x65\x65\x64\x65\x64\x21\x07\x00\x00\x00\x10\x52\x65\ -\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\ -\x00\x00\x00\x10\x00\x41\x00\x6e\x00\x6d\x00\x65\x00\x6c\x00\x64\ -\x00\x65\x00\x6e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x07\x53\x69\ -\x67\x6e\x20\x55\x70\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\ -\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x32\ -\x00\x55\x00\x73\x00\x65\x00\x72\x00\x6e\x00\x61\x00\x6d\x00\x65\ -\x00\x20\x00\x6e\x00\x69\x00\x63\x00\x68\x00\x74\x00\x20\x00\x76\ -\x00\x65\x00\x72\x00\x66\x00\xfc\x00\x67\x00\x62\x00\x61\x00\x72\ -\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x17\x55\x73\x65\x72\ -\x6e\x61\x6d\x65\x20\x6e\x6f\x74\x20\x61\x76\x61\x69\x6c\x61\x62\ -\x6c\x65\x2e\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\ -\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x38\x00\x41\ -\x00\x75\x00\x74\x00\x68\x00\x65\x00\x6e\x00\x74\x00\x69\x00\x66\ -\x00\x69\x00\x7a\x00\x69\x00\x65\x00\x72\x00\x75\x00\x6e\x00\x67\ -\x00\x73\x00\x66\x00\x65\x00\x68\x00\x6c\x00\x65\x00\x72\x00\x3a\ -\x00\x20\x00\x25\x00\x73\x08\x00\x00\x00\x00\x06\x00\x00\x00\x18\ -\x41\x75\x74\x68\x65\x6e\x74\x69\x63\x61\x74\x69\x6f\x6e\x20\x65\ -\x72\x72\x6f\x72\x3a\x20\x25\x73\x07\x00\x00\x00\x1a\x52\x65\x67\ -\x69\x73\x74\x65\x72\x55\x73\x65\x72\x56\x61\x6c\x69\x64\x61\x74\ -\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x26\x00\x48\x00\ -\x6f\x00\x6c\x00\x65\x00\x20\x00\x45\x00\x49\x00\x50\x00\x2d\x00\ -\x5a\x00\x65\x00\x72\x00\x74\x00\x69\x00\x66\x00\x69\x00\x6b\x00\ -\x61\x00\x74\x08\x00\x00\x00\x00\x06\x00\x00\x00\x18\x46\x65\x74\ -\x63\x68\x69\x6e\x67\x20\x65\x69\x70\x20\x63\x65\x72\x74\x69\x66\ -\x69\x63\x61\x74\x65\x07\x00\x00\x00\x1a\x52\x65\x67\x69\x73\x74\ -\x65\x72\x55\x73\x65\x72\x56\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\ -\x50\x61\x67\x65\x01\x03\x00\x00\x00\x3c\x00\x48\x00\x6f\x00\x6c\ -\x00\x65\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x69\x00\x64\ -\x00\x65\x00\x72\x00\x2d\x00\x4b\x00\x6f\x00\x6e\x00\x66\x00\x69\ -\x00\x67\x00\x75\x00\x72\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\ -\x00\x2e\x00\x2e\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1b\ -\x46\x65\x74\x63\x68\x69\x6e\x67\x20\x70\x72\x6f\x76\x69\x64\x65\ -\x72\x20\x63\x6f\x6e\x66\x69\x67\x2e\x2e\x2e\x07\x00\x00\x00\x1a\ -\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x56\x61\x6c\x69\ -\x64\x61\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x2c\ -\x00\x5a\x00\x65\x00\x72\x00\x74\x00\x69\x00\x66\x00\x69\x00\x6b\ -\x00\x61\x00\x74\x00\x73\x00\xfc\x00\x62\x00\x65\x00\x72\x00\x70\ -\x00\x72\x00\xfc\x00\x66\x00\x75\x00\x6e\x00\x67\x08\x00\x00\x00\ -\x00\x06\x00\x00\x00\x16\x43\x65\x72\x74\x69\x66\x69\x63\x61\x74\ -\x65\x20\x76\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\x07\x00\x00\x00\ -\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\ -\x61\x67\x65\x01\x03\x00\x00\x00\x72\x00\x4b\x00\x6f\x00\x6e\x00\ -\x6e\x00\x74\x00\x65\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x76\x00\ -\x69\x00\x64\x00\x65\x00\x72\x00\x2d\x00\x49\x00\x6e\x00\x66\x00\ -\x6f\x00\x20\x00\x6e\x00\x69\x00\x63\x00\x68\x00\x74\x00\x20\x00\ -\x68\x00\x65\x00\x72\x00\x75\x00\x6e\x00\x74\x00\x65\x00\x72\x00\ -\x6c\x00\x61\x00\x64\x00\x65\x00\x6e\x00\x20\x00\x28\x00\x72\x00\ -\x65\x00\x66\x00\x75\x00\x73\x00\x65\x00\x64\x00\x20\x00\x63\x00\ -\x6f\x00\x6e\x00\x6e\x00\x2e\x00\x29\x00\x2e\x08\x00\x00\x00\x00\ -\x06\x00\x00\x00\x31\x43\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\x64\ -\x6f\x77\x6e\x6c\x6f\x61\x64\x20\x70\x72\x6f\x76\x69\x64\x65\x72\ -\x20\x69\x6e\x66\x6f\x20\x28\x72\x65\x66\x75\x73\x65\x64\x20\x63\ -\x6f\x6e\x6e\x2e\x29\x2e\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\ -\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\ -\x00\x00\x5e\x00\x4b\x00\x6f\x00\x6e\x00\x6e\x00\x74\x00\x65\x00\ -\x20\x00\x6b\x00\x65\x00\x69\x00\x6e\x00\x65\x00\x20\x00\x49\x00\ -\x6e\x00\x66\x00\x6f\x00\x72\x00\x6d\x00\x61\x00\x74\x00\x69\x00\ -\x6f\x00\x6e\x00\x20\x00\x76\x00\x6f\x00\x6d\x00\x20\x00\x50\x00\ -\x72\x00\x6f\x00\x76\x00\x69\x00\x64\x00\x65\x00\x72\x00\x20\x00\ -\x62\x00\x65\x00\x6b\x00\x6f\x00\x6d\x00\x6d\x00\x65\x00\x6e\x00\ -\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x21\x43\x6f\x75\x6c\x64\ -\x20\x6e\x6f\x74\x20\x67\x65\x74\x20\x69\x6e\x66\x6f\x20\x66\x72\ -\x6f\x6d\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x2e\x07\x00\x00\x00\ -\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\ -\x61\x67\x65\x01\x03\x00\x00\x00\x20\x00\x47\x00\x69\x00\x62\x00\ -\x20\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x69\x00\x64\x00\x65\x00\ -\x72\x00\x20\x00\x65\x00\x69\x00\x6e\x08\x00\x00\x00\x00\x06\x00\ -\x00\x00\x0e\x45\x6e\x74\x65\x72\x20\x50\x72\x6f\x76\x69\x64\x65\ -\x72\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\ -\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\xa6\x00\x42\ -\x00\x69\x00\x74\x00\x74\x00\x65\x00\x20\x00\x67\x00\x69\x00\x62\ -\x00\x20\x00\x64\x00\x69\x00\x65\x00\x20\x00\x44\x00\x6f\x00\x6d\ -\x00\x61\x00\x69\x00\x6e\x00\x20\x00\x64\x00\x65\x00\x73\x00\x20\ -\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x69\x00\x64\x00\x65\x00\x72\ -\x00\x73\x00\x20\x00\x61\x00\x6e\x00\x2c\x00\x20\x00\x64\x00\x65\ -\x00\x6e\x00\x20\x00\x64\x00\x75\x00\x20\x00\x66\x00\xfc\x00\x72\ -\x00\x20\x00\x64\x00\x65\x00\x69\x00\x6e\x00\x65\x00\x20\x00\x56\ -\x00\x65\x00\x72\x00\x62\x00\x69\x00\x6e\x00\x64\x00\x75\x00\x6e\ -\x00\x67\x00\x20\x00\x6e\x00\x75\x00\x74\x00\x7a\x00\x65\x00\x6e\ -\x00\x20\x00\x6d\x00\xf6\x00\x63\x00\x68\x00\x74\x00\x65\x00\x73\ -\x00\x74\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x4c\x50\x6c\ -\x65\x61\x73\x65\x20\x65\x6e\x74\x65\x72\x20\x74\x68\x65\x20\x64\ -\x6f\x6d\x61\x69\x6e\x20\x6f\x66\x20\x74\x68\x65\x20\x70\x72\x6f\ -\x76\x69\x64\x65\x72\x20\x79\x6f\x75\x20\x77\x61\x6e\x74\x20\x74\ -\x6f\x20\x75\x73\x65\x20\x66\x6f\x72\x20\x79\x6f\x75\x72\x20\x63\ -\x6f\x6e\x6e\x65\x63\x74\x69\x6f\x6e\x2e\x07\x00\x00\x00\x12\x53\ -\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\ -\x65\x01\x03\x00\x00\x00\x60\x00\x53\x00\x65\x00\x72\x00\x76\x00\ -\x65\x00\x72\x00\x2d\x00\x5a\x00\x65\x00\x72\x00\x74\x00\x69\x00\ -\x66\x00\x69\x00\x6b\x00\x61\x00\x74\x00\x20\x00\x6b\x00\x6f\x00\ -\x6e\x00\x6e\x00\x74\x00\x65\x00\x20\x00\x6e\x00\x69\x00\x63\x00\ -\x68\x00\x74\x00\x20\x00\x62\x00\x65\x00\x73\x00\x74\x00\xe4\x00\ -\x74\x00\x69\x00\x67\x00\x74\x00\x20\x00\x77\x00\x65\x00\x72\x00\ -\x64\x00\x65\x00\x6e\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\ -\x29\x53\x65\x72\x76\x65\x72\x20\x63\x65\x72\x74\x69\x66\x69\x63\ -\x61\x74\x65\x20\x63\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\x62\x65\ -\x20\x76\x65\x72\x69\x66\x69\x65\x64\x2e\x07\x00\x00\x00\x12\x53\ -\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\ -\x65\x01\x03\x00\x00\x00\x22\x00\x50\x00\x72\x00\xfc\x00\x66\x00\ -\x65\x00\x20\x00\x44\x00\x6f\x00\x6d\x00\x61\x00\x69\x00\x6e\x00\ -\x2d\x00\x4e\x00\x61\x00\x6d\x00\x65\x08\x00\x00\x00\x00\x06\x00\ -\x00\x00\x14\x63\x68\x65\x63\x6b\x69\x6e\x67\x20\x64\x6f\x6d\x61\ -\x69\x6e\x20\x6e\x61\x6d\x65\x07\x00\x00\x00\x12\x53\x65\x6c\x65\ -\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\ -\x00\x00\x00\x2c\x00\x50\x00\x72\x00\xfc\x00\x66\x00\x65\x00\x20\ -\x00\x48\x00\x54\x00\x54\x00\x50\x00\x53\x00\x2d\x00\x56\x00\x65\ -\x00\x72\x00\x62\x00\x69\x00\x6e\x00\x64\x00\x75\x00\x6e\x00\x67\ -\x08\x00\x00\x00\x00\x06\x00\x00\x00\x19\x63\x68\x65\x63\x6b\x69\ -\x6e\x67\x20\x68\x74\x74\x70\x73\x20\x63\x6f\x6e\x6e\x65\x63\x74\ -\x69\x6f\x6e\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\ -\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x24\ -\x00\x48\x00\x6f\x00\x6c\x00\x65\x00\x20\x00\x50\x00\x72\x00\x6f\ -\x00\x76\x00\x69\x00\x64\x00\x65\x00\x72\x00\x2d\x00\x49\x00\x6e\ -\x00\x66\x00\x6f\x08\x00\x00\x00\x00\x06\x00\x00\x00\x16\x66\x65\ -\x74\x63\x68\x69\x6e\x67\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x20\ -\x69\x6e\x66\x6f\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\ -\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x88\x00\x00\x00\ -\x02\x01\x01\ -\x00\x00\x18\x32\ -\x3c\ -\xb8\x64\x18\xca\xef\x9c\x95\xcd\x21\x1c\xbf\x60\xa1\xbd\xdd\x42\ -\x00\x00\x01\x38\x00\x8f\x9b\xbe\x00\x00\x14\x83\x01\x23\x92\xe5\ -\x00\x00\x10\x3d\x01\x87\x64\x8e\x00\x00\x08\x7a\x01\xa8\xbe\x7e\ -\x00\x00\x0e\x02\x02\x2c\xac\xe9\x00\x00\x0b\x8a\x02\x3a\xce\xbf\ -\x00\x00\x16\x62\x02\x6e\x0f\xe5\x00\x00\x08\xdd\x02\x87\x60\x9e\ -\x00\x00\x06\x6e\x02\xaa\x52\x6e\x00\x00\x07\x6b\x02\xf2\xe0\x59\ -\x00\x00\x0a\x5e\x03\xec\x70\x0e\x00\x00\x10\xb8\x04\xd4\x45\xee\ -\x00\x00\x0d\x24\x05\xb7\x8f\x59\x00\x00\x0c\x27\x06\x3e\x6a\x9e\ -\x00\x00\x05\x9f\x06\x40\xa8\x7e\x00\x00\x0a\xea\x06\xee\xff\x6e\ -\x00\x00\x13\x74\x08\x13\xe8\xae\x00\x00\x0c\xa6\x08\x7a\x64\xee\ -\x00\x00\x11\xc5\x08\xe6\x98\x33\x00\x00\x05\x35\x08\xe6\x98\x33\ -\x00\x00\x0f\xc2\x09\x5c\x35\xe1\x00\x00\x0e\xaa\x09\x74\x75\x4e\ -\x00\x00\x0d\x94\x09\x98\x34\x0e\x00\x00\x12\x89\x09\xd8\x1f\x95\ -\x00\x00\x15\x79\x09\xeb\x5c\xb1\x00\x00\x15\x35\x09\xfc\x2c\x8e\ -\x00\x00\x04\xc7\x09\xfe\x05\x90\x00\x00\x0f\x16\x0a\x74\xb8\x1e\ -\x00\x00\x00\xd6\x0a\xfd\x99\xfe\x00\x00\x00\x51\x0b\xd2\x4b\x3f\ -\x00\x00\x07\x15\x0c\x44\x41\xbe\x00\x00\x00\x00\x0c\xc0\x94\x05\ -\x00\x00\x09\xd6\x0d\x0d\x9d\xc5\x00\x00\x06\x01\x0d\x15\x34\x70\ -\x00\x00\x09\x62\x0e\x36\x15\x54\x00\x00\x07\xed\x0e\x7e\xf5\xee\ -\x00\x00\x0f\x5a\x0e\x91\x50\x3e\x00\x00\x15\xee\x0e\xc0\xbb\x72\ -\x00\x00\x13\x1b\x0f\x27\x0d\x6e\x00\x00\x11\x54\x69\x00\x00\x16\ -\xd9\x03\x00\x00\x00\x22\x00\x50\x00\x72\x00\x69\x00\x6d\x00\x65\ -\x00\x72\x00\x61\x00\x20\x00\x43\x00\x6f\x00\x6e\x00\x65\x00\x78\ -\x00\x69\x00\x6f\x00\x6e\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\ -\x00\x11\x46\x69\x72\x73\x74\x20\x72\x75\x6e\x20\x77\x69\x7a\x61\ -\x72\x64\x2e\x07\x00\x00\x00\x09\x49\x6e\x74\x72\x6f\x50\x61\x67\ -\x65\x01\x03\x00\x00\x00\x4c\x00\x4c\x00\x6f\x00\x67\x00\x75\x00\ -\x65\x00\x61\x00\x72\x00\x6d\x00\x65\x00\x20\x00\x63\x00\x6f\x00\ -\x6e\x00\x20\x00\x75\x00\x6e\x00\x20\x00\x75\x00\x73\x00\x75\x00\ -\x61\x00\x72\x00\x69\x00\x6f\x00\x20\x00\x71\x00\x75\x00\x65\x00\ -\x20\x00\x79\x00\x61\x00\x20\x00\x74\x00\x65\x00\x6e\x00\x67\x00\ -\x6f\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1b\x4c\x6f\x67\ -\x20\x49\x6e\x20\x77\x69\x74\x68\x20\x6d\x79\x20\x63\x72\x65\x64\ -\x65\x6e\x74\x69\x61\x6c\x73\x2e\x07\x00\x00\x00\x09\x49\x6e\x74\ -\x72\x6f\x50\x61\x67\x65\x01\x03\x00\x00\x02\x76\x00\x56\x00\x61\ -\x00\x6d\x00\x6f\x00\x73\x00\x20\x00\x61\x00\x20\x00\x63\x00\x6f\ -\x00\x6e\x00\x66\x00\x69\x00\x67\x00\x75\x00\x72\x00\x61\x00\x72\ -\x00\x20\x00\x61\x00\x6c\x00\x67\x00\x75\x00\x6e\x00\x61\x00\x73\ -\x00\x20\x00\x63\x00\x6f\x00\x73\x00\x61\x00\x73\x00\x20\x00\x61\ -\x00\x6e\x00\x74\x00\x65\x00\x73\x00\x20\x00\x64\x00\x65\x00\x20\ -\x00\x71\x00\x75\x00\x65\x00\x20\x00\x74\x00\x65\x00\x20\x00\x70\ -\x00\x75\x00\x65\x00\x64\x00\x61\x00\x73\x00\x20\x00\x63\x00\x6f\ -\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x61\x00\x72\x00\x20\x00\x70\ -\x00\x6f\x00\x72\x00\x20\x00\x70\x00\x72\x00\x69\x00\x6d\x00\x65\ -\x00\x72\x00\x61\x00\x20\x00\x76\x00\x65\x00\x7a\x00\x2e\x00\x3c\ -\x00\x62\x00\x72\x00\x3e\x00\x3c\x00\x62\x00\x72\x00\x3e\x00\x53\ -\x00\x69\x00\x20\x00\x6e\x00\x65\x00\x63\x00\x65\x00\x73\x00\x69\ -\x00\x74\x00\x61\x00\x73\x00\x20\x00\x6d\x00\x6f\x00\x64\x00\x69\ -\x00\x66\x00\x69\x00\x63\x00\x61\x00\x72\x00\x20\x00\x65\x00\x73\ -\x00\x74\x00\x61\x00\x73\x00\x20\x00\x6f\x00\x70\x00\x63\x00\x69\ -\x00\x6f\x00\x6e\x00\x65\x00\x73\x00\x20\x00\x64\x00\x65\x00\x20\ -\x00\x6e\x00\x75\x00\x65\x00\x76\x00\x6f\x00\x2c\x00\x20\x00\x70\ -\x00\x75\x00\x65\x00\x64\x00\x65\x00\x73\x00\x20\x00\x65\x00\x6e\ -\x00\x63\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x61\x00\x72\x00\x20\ -\x00\x65\x00\x73\x00\x74\x00\x65\x00\x20\x00\x61\x00\x73\x00\x69\ -\x00\x73\x00\x74\x00\x65\x00\x6e\x00\x74\x00\x65\x00\x20\x00\x65\ -\x00\x6e\x00\x20\x00\x65\x00\x6c\x00\x20\x00\x6d\x00\x65\x00\x6e\ -\x00\x75\x00\x20\x00\x64\x00\x65\x00\x20\x00\x27\x00\x3c\x00\x69\ -\x00\x3e\x00\x4f\x00\x70\x00\x63\x00\x69\x00\x6f\x00\x6e\x00\x65\ -\x00\x73\x00\x3c\x00\x2f\x00\x69\x00\x3e\x00\x27\x00\x20\x00\x65\ -\x00\x6e\x00\x20\x00\x6c\x00\x61\x00\x20\x00\x76\x00\x65\x00\x6e\ -\x00\x74\x00\x61\x00\x6e\x00\x61\x00\x20\x00\x70\x00\x72\x00\x69\ -\x00\x6e\x00\x63\x00\x69\x00\x70\x00\x61\x00\x6c\x00\x2e\x00\x3c\ -\x00\x62\x00\x72\x00\x3e\x00\x3c\x00\x62\x00\x72\x00\x3e\x00\x51\ -\x00\x75\x00\x69\x00\x65\x00\x72\x00\x65\x00\x73\x00\x20\x00\x3c\ -\x00\x62\x00\x3e\x00\x72\x00\x65\x00\x67\x00\x69\x00\x73\x00\x74\ -\x00\x72\x00\x61\x00\x72\x00\x3c\x00\x2f\x00\x62\x00\x3e\x00\x20\ -\x00\x75\x00\x6e\x00\x61\x00\x20\x00\x6e\x00\x75\x00\x65\x00\x76\ -\x00\x61\x00\x20\x00\x63\x00\x75\x00\x65\x00\x6e\x00\x74\x00\x61\ -\x00\x2c\x00\x20\x00\x6f\x00\x20\x00\x3c\x00\x62\x00\x3e\x00\x6c\ -\x00\x6f\x00\x67\x00\x75\x00\x65\x00\x61\x00\x72\x00\x74\x00\x65\ -\x00\x3c\x00\x2f\x00\x62\x00\x3e\x00\x20\x00\x63\x00\x6f\x00\x6e\ -\x00\x20\x00\x74\x00\x75\x00\x20\x00\x75\x00\x73\x00\x75\x00\x61\ -\x00\x72\x00\x69\x00\x6f\x00\x3f\x00\x3c\x00\x62\x00\x72\x00\x3e\ -\x00\x20\x08\x00\x00\x00\x00\x06\x00\x00\x01\x5d\x4e\x6f\x77\x20\ -\x77\x65\x20\x77\x69\x6c\x6c\x20\x67\x75\x69\x64\x65\x20\x79\x6f\ -\x75\x20\x74\x68\x72\x6f\x75\x67\x68\x20\x73\x6f\x6d\x65\x20\x63\ -\x6f\x6e\x66\x69\x67\x75\x72\x61\x74\x69\x6f\x6e\x20\x74\x68\x61\ -\x74\x20\x69\x73\x20\x6e\x65\x65\x64\x65\x64\x20\x62\x65\x66\x6f\ -\x72\x65\x20\x79\x6f\x75\x20\x63\x61\x6e\x20\x63\x6f\x6e\x6e\x65\ -\x63\x74\x20\x66\x6f\x72\x20\x74\x68\x65\x20\x66\x69\x72\x73\x74\ -\x20\x74\x69\x6d\x65\x2e\x3c\x62\x72\x3e\x3c\x62\x72\x3e\x49\x66\ -\x20\x79\x6f\x75\x20\x65\x76\x65\x72\x20\x6e\x65\x65\x64\x20\x74\ -\x6f\x20\x6d\x6f\x64\x69\x66\x79\x20\x74\x68\x65\x73\x65\x20\x6f\ -\x70\x74\x69\x6f\x6e\x73\x20\x61\x67\x61\x69\x6e\x2c\x20\x79\x6f\ -\x75\x20\x63\x61\x6e\x20\x66\x69\x6e\x64\x20\x74\x68\x65\x20\x77\ -\x69\x7a\x61\x72\x64\x20\x69\x6e\x20\x74\x68\x65\x20\x27\x3c\x69\ -\x3e\x53\x65\x74\x74\x69\x6e\x67\x73\x3c\x2f\x69\x3e\x27\x20\x6d\ -\x65\x6e\x75\x20\x66\x72\x6f\x6d\x20\x74\x68\x65\x20\x6d\x61\x69\ -\x6e\x20\x77\x69\x6e\x64\x6f\x77\x2e\x3c\x62\x72\x3e\x3c\x62\x72\ -\x3e\x44\x6f\x20\x79\x6f\x75\x20\x77\x61\x6e\x74\x20\x74\x6f\x20\ -\x3c\x62\x3e\x73\x69\x67\x6e\x20\x75\x70\x3c\x2f\x62\x3e\x20\x66\ -\x6f\x72\x20\x61\x20\x6e\x65\x77\x20\x61\x63\x63\x6f\x75\x6e\x74\ -\x2c\x20\x6f\x72\x20\x3c\x62\x3e\x6c\x6f\x67\x20\x69\x6e\x3c\x2f\ -\x62\x3e\x20\x77\x69\x74\x68\x20\x61\x6e\x20\x61\x6c\x72\x65\x61\ -\x64\x79\x20\x65\x78\x69\x73\x74\x69\x6e\x67\x20\x75\x73\x65\x72\ -\x6e\x61\x6d\x65\x3f\x3c\x62\x72\x3e\x07\x00\x00\x00\x09\x49\x6e\ -\x74\x72\x6f\x50\x61\x67\x65\x01\x03\x00\x00\x00\x36\x00\x52\x00\ -\x65\x00\x67\x00\x69\x00\x73\x00\x74\x00\x72\x00\x61\x00\x72\x00\ -\x20\x00\x75\x00\x6e\x00\x61\x00\x20\x00\x63\x00\x75\x00\x65\x00\ -\x6e\x00\x74\x00\x61\x00\x20\x00\x6e\x00\x75\x00\x65\x00\x76\x00\ -\x61\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1a\x53\x69\x67\ -\x6e\x20\x75\x70\x20\x66\x6f\x72\x20\x61\x20\x6e\x65\x77\x20\x61\ -\x63\x63\x6f\x75\x6e\x74\x2e\x07\x00\x00\x00\x09\x49\x6e\x74\x72\ -\x6f\x50\x61\x67\x65\x01\x03\x00\x00\x00\x34\x00\x45\x00\x72\x00\ -\x72\x00\x6f\x00\x72\x00\x20\x00\x64\x00\x65\x00\x20\x00\x61\x00\ -\x75\x00\x74\x00\x65\x00\x6e\x00\x74\x00\x69\x00\x63\x00\x61\x00\ -\x63\x00\x69\x00\x6f\x00\x6e\x00\x3a\x00\x20\x00\x25\x00\x73\x08\ -\x00\x00\x00\x00\x06\x00\x00\x00\x18\x41\x75\x74\x68\x65\x6e\x74\ -\x69\x63\x61\x74\x69\x6f\x6e\x20\x65\x72\x72\x6f\x72\x3a\x20\x25\ -\x73\x07\x00\x00\x00\x09\x4c\x6f\x67\x49\x6e\x50\x61\x67\x65\x01\ -\x03\x00\x00\x00\x2e\x00\x43\x00\x72\x00\x65\x00\x64\x00\x65\x00\ -\x6e\x00\x63\x00\x69\x00\x61\x00\x6c\x00\x65\x00\x73\x00\x20\x00\ -\x76\x00\x61\x00\x6c\x00\x69\x00\x64\x00\x61\x00\x64\x00\x61\x00\ -\x73\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x16\x43\x72\x65\ -\x64\x65\x6e\x74\x69\x61\x6c\x73\x20\x76\x61\x6c\x69\x64\x61\x74\ -\x65\x64\x2e\x07\x00\x00\x00\x09\x4c\x6f\x67\x49\x6e\x50\x61\x67\ -\x65\x01\x03\x00\x00\x00\x3a\x00\x52\x00\x65\x00\x73\x00\x6f\x00\ -\x6c\x00\x76\x00\x69\x00\x65\x00\x6e\x00\x64\x00\x6f\x00\x20\x00\ -\x6e\x00\x6f\x00\x6d\x00\x62\x00\x72\x00\x65\x00\x20\x00\x64\x00\ -\x65\x00\x20\x00\x64\x00\x6f\x00\x6d\x00\x69\x00\x6e\x00\x69\x00\ -\x6f\x08\x00\x00\x00\x00\x06\x00\x00\x00\x15\x52\x65\x73\x6f\x6c\ -\x76\x69\x6e\x67\x20\x64\x6f\x6d\x61\x69\x6e\x20\x6e\x61\x6d\x65\ -\x07\x00\x00\x00\x09\x4c\x6f\x67\x49\x6e\x50\x61\x67\x65\x01\x03\ -\x00\x00\x00\x5a\x00\x45\x00\x6c\x00\x20\x00\x75\x00\x73\x00\x75\ -\x00\x61\x00\x72\x00\x69\x00\x6f\x00\x20\x00\x74\x00\x69\x00\x65\ -\x00\x6e\x00\x65\x00\x20\x00\x71\x00\x75\x00\x65\x00\x20\x00\x73\ -\x00\x65\x00\x72\x00\x20\x00\x75\x00\x73\x00\x75\x00\x61\x00\x72\ -\x00\x69\x00\x6f\x00\x40\x00\x74\x00\x75\x00\x2e\x00\x70\x00\x72\ -\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x08\x00\ -\x00\x00\x00\x06\x00\x00\x00\x2f\x55\x73\x65\x72\x6e\x61\x6d\x65\ -\x20\x6d\x75\x73\x74\x20\x62\x65\x20\x69\x6e\x20\x74\x68\x65\x20\ -\x75\x73\x65\x72\x6e\x61\x6d\x65\x40\x70\x72\x6f\x76\x69\x64\x65\ -\x72\x20\x66\x6f\x72\x6d\x2e\x07\x00\x00\x00\x09\x4c\x6f\x67\x49\ -\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x24\x00\x49\x00\x6e\x00\ -\x66\x00\x6f\x00\x20\x00\x64\x00\x65\x00\x6c\x00\x20\x00\x50\x00\ -\x72\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x08\ -\x00\x00\x00\x00\x06\x00\x00\x00\x0d\x50\x72\x6f\x76\x69\x64\x65\ -\x72\x20\x49\x6e\x66\x6f\x07\x00\x00\x00\x10\x50\x72\x6f\x76\x69\ -\x64\x65\x72\x49\x6e\x66\x6f\x50\x61\x67\x65\x01\x03\x00\x00\x00\ -\x42\x00\x45\x00\x73\x00\x74\x00\x6f\x00\x20\x00\x65\x00\x73\x00\ -\x20\x00\x6c\x00\x6f\x00\x20\x00\x71\x00\x75\x00\x65\x00\x20\x00\ -\x64\x00\x69\x00\x63\x00\x65\x00\x20\x00\x65\x00\x6c\x00\x20\x00\ -\x70\x00\x72\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\ -\x72\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1b\x54\x68\x69\ -\x73\x20\x69\x73\x20\x77\x68\x61\x74\x20\x70\x72\x6f\x76\x69\x64\ -\x65\x72\x20\x73\x61\x79\x73\x2e\x07\x00\x00\x00\x10\x50\x72\x6f\ -\x76\x69\x64\x65\x72\x49\x6e\x66\x6f\x50\x61\x67\x65\x01\x03\x00\ -\x00\x00\x46\x00\x43\x00\x6f\x00\x6d\x00\x70\x00\x72\x00\x6f\x00\ -\x62\x00\x61\x00\x6e\x00\x64\x00\x6f\x00\x20\x00\x65\x00\x6c\x00\ -\x20\x00\x66\x00\x69\x00\x6e\x00\x67\x00\x65\x00\x72\x00\x70\x00\ -\x72\x00\x69\x00\x6e\x00\x74\x00\x20\x00\x64\x00\x65\x00\x20\x00\ -\x6c\x00\x61\x00\x20\x00\x43\x00\x41\x08\x00\x00\x00\x00\x06\x00\ -\x00\x00\x17\x43\x68\x65\x63\x6b\x69\x6e\x67\x20\x43\x41\x20\x66\ -\x69\x6e\x67\x65\x72\x70\x72\x69\x6e\x74\x07\x00\x00\x00\x1b\x50\ -\x72\x6f\x76\x69\x64\x65\x72\x53\x65\x74\x75\x70\x56\x61\x6c\x69\ -\x64\x61\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x22\ -\x00\x41\x00\x75\x00\x74\x00\x6f\x00\x63\x00\x6f\x00\x6e\x00\x66\ -\x00\x69\x00\x67\x00\x75\x00\x72\x00\x61\x00\x6e\x00\x64\x00\x6f\ -\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x11\x44\x6f\x69\x6e\ -\x67\x20\x61\x75\x74\x6f\x63\x6f\x6e\x66\x69\x67\x2e\x07\x00\x00\ -\x00\x1b\x50\x72\x6f\x76\x69\x64\x65\x72\x53\x65\x74\x75\x70\x56\ -\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\ -\x00\x00\x3e\x00\x4f\x00\x62\x00\x74\x00\x65\x00\x6e\x00\x69\x00\ -\x65\x00\x6e\x00\x64\x00\x6f\x00\x20\x00\x63\x00\x65\x00\x72\x00\ -\x74\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x64\x00\x6f\x00\ -\x20\x00\x64\x00\x65\x00\x20\x00\x6c\x00\x61\x00\x20\x00\x43\x00\ -\x41\x08\x00\x00\x00\x00\x06\x00\x00\x00\x17\x46\x65\x74\x63\x68\ -\x69\x6e\x67\x20\x43\x41\x20\x63\x65\x72\x74\x69\x66\x69\x63\x61\ -\x74\x65\x07\x00\x00\x00\x1b\x50\x72\x6f\x76\x69\x64\x65\x72\x53\ -\x65\x74\x75\x70\x56\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\x50\x61\ -\x67\x65\x01\x03\x00\x00\x00\x36\x00\x43\x00\x6f\x00\x6e\x00\x66\ -\x00\x69\x00\x67\x00\x75\x00\x72\x00\x61\x00\x63\x00\x69\x00\x6f\ -\x00\x6e\x00\x20\x00\x64\x00\x65\x00\x6c\x00\x20\x00\x50\x00\x72\ -\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x08\x00\ -\x00\x00\x00\x06\x00\x00\x00\x0e\x50\x72\x6f\x76\x69\x64\x65\x72\ -\x20\x73\x65\x74\x75\x70\x07\x00\x00\x00\x1b\x50\x72\x6f\x76\x69\ -\x64\x65\x72\x53\x65\x74\x75\x70\x56\x61\x6c\x69\x64\x61\x74\x69\ -\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x3e\x00\x56\x00\x61\ -\x00\x6c\x00\x69\x00\x64\x00\x61\x00\x6e\x00\x64\x00\x6f\x00\x20\ -\x00\x63\x00\x65\x00\x72\x00\x74\x00\x69\x00\x66\x00\x69\x00\x63\ -\x00\x61\x00\x64\x00\x6f\x00\x20\x00\x64\x00\x65\x00\x20\x00\x6c\ -\x00\x61\x00\x20\x00\x61\x00\x70\x00\x69\x08\x00\x00\x00\x00\x06\ -\x00\x00\x00\x1a\x56\x61\x6c\x69\x64\x61\x74\x69\x6e\x67\x20\x61\ -\x70\x69\x20\x63\x65\x72\x74\x69\x66\x69\x63\x61\x74\x65\x07\x00\ -\x00\x00\x1b\x50\x72\x6f\x76\x69\x64\x65\x72\x53\x65\x74\x75\x70\ -\x56\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\ -\x00\x00\x00\x46\x00\x4e\x00\x6f\x00\x20\x00\x73\x00\x65\x00\x20\ -\x00\x70\x00\x75\x00\x64\x00\x6f\x00\x20\x00\x72\x00\x65\x00\x67\ -\x00\x69\x00\x73\x00\x74\x00\x72\x00\x61\x00\x72\x00\x20\x00\x28\ -\x00\x62\x00\x61\x00\x64\x00\x20\x00\x72\x00\x65\x00\x73\x00\x70\ -\x00\x6f\x00\x6e\x00\x73\x00\x65\x00\x29\x08\x00\x00\x00\x00\x06\ -\x00\x00\x00\x21\x43\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\x72\x65\ -\x67\x69\x73\x74\x65\x72\x20\x28\x62\x61\x64\x20\x72\x65\x73\x70\ -\x6f\x6e\x73\x65\x29\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\ -\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x54\ -\x00\x45\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x20\x00\x63\x00\x6f\ -\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x61\x00\x6e\x00\x64\x00\x6f\ -\x00\x73\x00\x65\x00\x20\x00\x61\x00\x6c\x00\x20\x00\x70\x00\x72\ -\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x00\x20\ -\x00\x28\x00\x63\x00\x6f\x00\x6e\x00\x6e\x00\x65\x00\x72\x00\x72\ -\x00\x29\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x27\x45\x72\ -\x72\x6f\x72\x20\x43\x6f\x6e\x6e\x65\x63\x74\x69\x6e\x67\x20\x74\ -\x6f\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x20\x28\x63\x6f\x6e\x6e\ -\x65\x72\x72\x29\x2e\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\ -\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x52\ -\x00\x45\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x20\x00\x63\x00\x6f\ -\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x61\x00\x6e\x00\x64\x00\x6f\ -\x00\x73\x00\x65\x00\x20\x00\x61\x00\x6c\x00\x20\x00\x70\x00\x72\ -\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x00\x20\ -\x00\x28\x00\x74\x00\x69\x00\x6d\x00\x65\x00\x6f\x00\x75\x00\x74\ -\x00\x29\x08\x00\x00\x00\x00\x06\x00\x00\x00\x26\x45\x72\x72\x6f\ -\x72\x20\x63\x6f\x6e\x6e\x65\x63\x74\x69\x6e\x67\x20\x74\x6f\x20\ -\x70\x72\x6f\x76\x69\x64\x65\x72\x20\x28\x74\x69\x6d\x65\x6f\x75\ -\x74\x29\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\ -\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x3c\x00\x45\x00\ -\x72\x00\x72\x00\x6f\x00\x72\x00\x20\x00\x64\x00\x75\x00\x72\x00\ -\x61\x00\x6e\x00\x74\x00\x65\x00\x20\x00\x65\x00\x6c\x00\x20\x00\ -\x72\x00\x65\x00\x67\x00\x69\x00\x73\x00\x74\x00\x72\x00\x6f\x00\ -\x20\x00\x28\x00\x25\x00\x73\x00\x29\x08\x00\x00\x00\x00\x06\x00\ -\x00\x00\x1e\x45\x72\x72\x6f\x72\x20\x64\x75\x72\x69\x6e\x67\x20\ -\x72\x65\x67\x69\x73\x74\x72\x61\x74\x69\x6f\x6e\x20\x28\x25\x73\ -\x29\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\ -\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x40\x00\x4c\x00\x61\ -\x00\x73\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x61\ -\x00\x73\x00\x65\x00\x6e\x00\x61\x00\x73\x00\x20\x00\x6e\x00\x6f\ -\x00\x20\x00\x73\x00\x6f\x00\x6e\x00\x20\x00\x69\x00\x67\x00\x75\ -\x00\x61\x00\x6c\x00\x65\x00\x73\x00\x2e\x00\x2e\x08\x00\x00\x00\ -\x00\x06\x00\x00\x00\x19\x50\x61\x73\x73\x77\x6f\x72\x64\x20\x64\ -\x6f\x65\x73\x20\x6e\x6f\x74\x20\x6d\x61\x74\x63\x68\x2e\x2e\x07\ -\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\ -\x50\x61\x67\x65\x01\x03\x00\x00\x00\x36\x00\x43\x00\x6f\x00\x6e\ -\x00\x74\x00\x72\x00\x61\x00\x73\x00\x65\x00\x6e\x00\x61\x00\x20\ -\x00\x64\x00\x65\x00\x6d\x00\x61\x00\x73\x00\x69\x00\x61\x00\x64\ -\x00\x6f\x00\x20\x00\x6f\x00\x62\x00\x76\x00\x69\x00\x61\x00\x2e\ -\x08\x00\x00\x00\x00\x06\x00\x00\x00\x15\x50\x61\x73\x73\x77\x6f\ -\x72\x64\x20\x74\x6f\x6f\x20\x6f\x62\x76\x69\x6f\x75\x73\x2e\x07\ -\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\ -\x50\x61\x67\x65\x01\x03\x00\x00\x00\x36\x00\x43\x00\x6f\x00\x6e\ -\x00\x74\x00\x72\x00\x61\x00\x73\x00\x65\x00\x6e\x00\x61\x00\x20\ -\x00\x64\x00\x65\x00\x6d\x00\x61\x00\x73\x00\x69\x00\x61\x00\x64\ -\x00\x6f\x00\x20\x00\x63\x00\x6f\x00\x72\x00\x74\x00\x61\x00\x2e\ -\x08\x00\x00\x00\x00\x06\x00\x00\x00\x13\x50\x61\x73\x73\x77\x6f\ -\x72\x64\x20\x74\x6f\x6f\x20\x73\x68\x6f\x72\x74\x2e\x07\x00\x00\ -\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\ -\x67\x65\x01\x03\x00\x00\x00\x5e\x00\x52\x00\x65\x00\x67\x00\x69\ -\x00\x73\x00\x74\x00\x72\x00\x61\x00\x72\x00\x20\x00\x75\x00\x6e\ -\x00\x20\x00\x6e\x00\x75\x00\x65\x00\x76\x00\x6f\x00\x20\x00\x75\ -\x00\x73\x00\x75\x00\x61\x00\x72\x00\x69\x00\x6f\x00\x20\x00\x63\ -\x00\x6f\x00\x6e\x00\x20\x00\x65\x00\x6c\x00\x20\x00\x70\x00\x72\ -\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x00\x20\ -\x00\x25\x00\x73\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x25\ -\x52\x65\x67\x69\x73\x74\x65\x72\x20\x61\x20\x6e\x65\x77\x20\x75\ -\x73\x65\x72\x20\x77\x69\x74\x68\x20\x70\x72\x6f\x76\x69\x64\x65\ -\x72\x20\x25\x73\x2e\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\ -\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x30\ -\x00\x43\x00\x75\x00\x65\x00\x6e\x00\x74\x00\x61\x00\x20\x00\x63\ -\x00\x72\x00\x65\x00\x61\x00\x64\x00\x61\x00\x20\x00\x63\x00\x6f\ -\x00\x6e\x00\x20\x00\x65\x00\x78\x00\x69\x00\x74\x00\x6f\x00\x21\ -\x08\x00\x00\x00\x00\x06\x00\x00\x00\x17\x52\x65\x67\x69\x73\x74\ -\x72\x61\x74\x69\x6f\x6e\x20\x73\x75\x63\x63\x65\x65\x64\x65\x64\ -\x21\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\ -\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x18\x00\x4e\x00\x75\ -\x00\x65\x00\x76\x00\x61\x00\x20\x00\x43\x00\x75\x00\x65\x00\x6e\ -\x00\x74\x00\x61\x08\x00\x00\x00\x00\x06\x00\x00\x00\x07\x53\x69\ -\x67\x6e\x20\x55\x70\x07\x00\x00\x00\x10\x52\x65\x67\x69\x73\x74\ -\x65\x72\x55\x73\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x2c\ -\x00\x55\x00\x73\x00\x75\x00\x61\x00\x72\x00\x69\x00\x6f\x00\x20\ -\x00\x6e\x00\x6f\x00\x20\x00\x64\x00\x69\x00\x73\x00\x70\x00\x6f\ -\x00\x6e\x00\x69\x00\x62\x00\x6c\x00\x65\x00\x2e\x08\x00\x00\x00\ -\x00\x06\x00\x00\x00\x17\x55\x73\x65\x72\x6e\x61\x6d\x65\x20\x6e\ -\x6f\x74\x20\x61\x76\x61\x69\x6c\x61\x62\x6c\x65\x2e\x07\x00\x00\ -\x00\x10\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x50\x61\ -\x67\x65\x01\x03\x00\x00\x00\x34\x00\x45\x00\x72\x00\x72\x00\x6f\ -\x00\x72\x00\x20\x00\x64\x00\x65\x00\x20\x00\x61\x00\x75\x00\x74\ -\x00\x65\x00\x6e\x00\x74\x00\x69\x00\x63\x00\x61\x00\x63\x00\x69\ -\x00\x6f\x00\x6e\x00\x3a\x00\x20\x00\x25\x00\x73\x08\x00\x00\x00\ -\x00\x06\x00\x00\x00\x18\x41\x75\x74\x68\x65\x6e\x74\x69\x63\x61\ -\x74\x69\x6f\x6e\x20\x65\x72\x72\x6f\x72\x3a\x20\x25\x73\x07\x00\ -\x00\x00\x1a\x52\x65\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x56\ -\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\ -\x00\x00\x34\x00\x4f\x00\x62\x00\x74\x00\x65\x00\x6e\x00\x69\x00\ -\x65\x00\x6e\x00\x64\x00\x6f\x00\x20\x00\x63\x00\x65\x00\x72\x00\ -\x74\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x64\x00\x6f\x00\ -\x20\x00\x65\x00\x69\x00\x70\x08\x00\x00\x00\x00\x06\x00\x00\x00\ -\x18\x46\x65\x74\x63\x68\x69\x6e\x67\x20\x65\x69\x70\x20\x63\x65\ -\x72\x74\x69\x66\x69\x63\x61\x74\x65\x07\x00\x00\x00\x1a\x52\x65\ -\x67\x69\x73\x74\x65\x72\x55\x73\x65\x72\x56\x61\x6c\x69\x64\x61\ -\x74\x69\x6f\x6e\x50\x61\x67\x65\x01\x03\x00\x00\x00\x52\x00\x4f\ -\x00\x62\x00\x74\x00\x65\x00\x6e\x00\x69\x00\x65\x00\x6e\x00\x64\ -\x00\x6f\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x66\x00\x69\x00\x67\ -\x00\x75\x00\x72\x00\x61\x00\x63\x00\x69\x00\x6f\x00\x6e\x00\x20\ -\x00\x64\x00\x65\x00\x6c\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x76\ -\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x00\x2e\x00\x2e\x00\x2e\ -\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1b\x46\x65\x74\x63\x68\x69\ -\x6e\x67\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x20\x63\x6f\x6e\x66\ -\x69\x67\x2e\x2e\x2e\x07\x00\x00\x00\x1a\x52\x65\x67\x69\x73\x74\ -\x65\x72\x55\x73\x65\x72\x56\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\ -\x50\x61\x67\x65\x01\x03\x00\x00\x00\x34\x00\x56\x00\x61\x00\x6c\ -\x00\x69\x00\x64\x00\x61\x00\x63\x00\x69\x00\x6f\x00\x6e\x00\x20\ -\x00\x64\x00\x65\x00\x6c\x00\x20\x00\x63\x00\x65\x00\x72\x00\x74\ -\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x64\x00\x6f\x08\x00\ -\x00\x00\x00\x06\x00\x00\x00\x16\x43\x65\x72\x74\x69\x66\x69\x63\ -\x61\x74\x65\x20\x76\x61\x6c\x69\x64\x61\x74\x69\x6f\x6e\x07\x00\ -\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\ -\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x6c\x00\x6e\x00\x6f\x00\ -\x20\x00\x73\x00\x65\x00\x20\x00\x70\x00\x75\x00\x64\x00\x6f\x00\ -\x20\x00\x6f\x00\x62\x00\x74\x00\x65\x00\x6e\x00\x65\x00\x72\x00\ -\x20\x00\x69\x00\x6e\x00\x66\x00\x6f\x00\x20\x00\x64\x00\x65\x00\ -\x6c\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x76\x00\x65\x00\x65\x00\ -\x64\x00\x6f\x00\x72\x00\x20\x00\x28\x00\x72\x00\x65\x00\x66\x00\ -\x75\x00\x73\x00\x65\x00\x64\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\ -\x6e\x00\x2e\x00\x29\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\ -\x31\x43\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\x64\x6f\x77\x6e\x6c\ -\x6f\x61\x64\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x20\x69\x6e\x66\ -\x6f\x20\x28\x72\x65\x66\x75\x73\x65\x64\x20\x63\x6f\x6e\x6e\x2e\ -\x29\x2e\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\ -\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x4a\x00\ -\x6e\x00\x6f\x00\x20\x00\x73\x00\x65\x00\x20\x00\x70\x00\x75\x00\ -\x64\x00\x6f\x00\x20\x00\x6f\x00\x62\x00\x74\x00\x65\x00\x6e\x00\ -\x65\x00\x72\x00\x20\x00\x69\x00\x6e\x00\x66\x00\x6f\x00\x20\x00\ -\x64\x00\x65\x00\x6c\x00\x20\x00\x70\x00\x72\x00\x6f\x00\x76\x00\ -\x65\x00\x65\x00\x64\x00\x6f\x00\x72\x08\x00\x00\x00\x00\x06\x00\ -\x00\x00\x21\x43\x6f\x75\x6c\x64\x20\x6e\x6f\x74\x20\x67\x65\x74\ -\x20\x69\x6e\x66\x6f\x20\x66\x72\x6f\x6d\x20\x70\x72\x6f\x76\x69\ -\x64\x65\x72\x2e\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\ -\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\ -\x24\x00\x45\x00\x6e\x00\x74\x00\x72\x00\x61\x00\x20\x00\x74\x00\ -\x75\x00\x20\x00\x50\x00\x72\x00\x6f\x00\x76\x00\x65\x00\x65\x00\ -\x64\x00\x6f\x00\x72\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0e\x45\ -\x6e\x74\x65\x72\x20\x50\x72\x6f\x76\x69\x64\x65\x72\x07\x00\x00\ -\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\ -\x50\x61\x67\x65\x01\x03\x00\x00\x00\x9c\x00\x50\x00\x6f\x00\x72\ -\x00\x20\x00\x66\x00\x61\x00\x76\x00\x6f\x00\x72\x00\x2c\x00\x20\ -\x00\x72\x00\x65\x00\x6c\x00\x6c\x00\x65\x00\x6e\x00\x61\x00\x20\ -\x00\x65\x00\x6c\x00\x20\x00\x64\x00\x6f\x00\x6d\x00\x69\x00\x6e\ -\x00\x69\x00\x6f\x00\x20\x00\x64\x00\x65\x00\x6c\x00\x20\x00\x70\ -\x00\x72\x00\x6f\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\x00\x72\ -\x00\x20\x00\x71\x00\x75\x00\x65\x00\x20\x00\x71\x00\x75\x00\x69\ -\x00\x65\x00\x72\x00\x61\x00\x73\x00\x20\x00\x75\x00\x73\x00\x61\ -\x00\x72\x00\x20\x00\x70\x00\x61\x00\x72\x00\x61\x00\x20\x00\x74\ -\x00\x75\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x65\x00\x78\x00\x69\ -\x00\x6f\x00\x6e\x00\x2e\x08\x00\x00\x00\x00\x06\x00\x00\x00\x4c\ -\x50\x6c\x65\x61\x73\x65\x20\x65\x6e\x74\x65\x72\x20\x74\x68\x65\ -\x20\x64\x6f\x6d\x61\x69\x6e\x20\x6f\x66\x20\x74\x68\x65\x20\x70\ -\x72\x6f\x76\x69\x64\x65\x72\x20\x79\x6f\x75\x20\x77\x61\x6e\x74\ -\x20\x74\x6f\x20\x75\x73\x65\x20\x66\x6f\x72\x20\x79\x6f\x75\x72\ -\x20\x63\x6f\x6e\x6e\x65\x63\x74\x69\x6f\x6e\x2e\x07\x00\x00\x00\ -\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\ -\x61\x67\x65\x01\x03\x00\x00\x00\x62\x00\x4e\x00\x6f\x00\x20\x00\ -\x73\x00\x65\x00\x20\x00\x70\x00\x75\x00\x64\x00\x6f\x00\x20\x00\ -\x76\x00\x65\x00\x72\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\ -\x72\x00\x20\x00\x65\x00\x6c\x00\x20\x00\x63\x00\x65\x00\x72\x00\ -\x74\x00\x69\x00\x66\x00\x69\x00\x63\x00\x61\x00\x64\x00\x6f\x00\ -\x20\x00\x64\x00\x65\x00\x6c\x00\x20\x00\x73\x00\x65\x00\x72\x00\ -\x76\x00\x69\x00\x64\x00\x6f\x00\x72\x00\x2e\x08\x00\x00\x00\x00\ -\x06\x00\x00\x00\x29\x53\x65\x72\x76\x65\x72\x20\x63\x65\x72\x74\ -\x69\x66\x69\x63\x61\x74\x65\x20\x63\x6f\x75\x6c\x64\x20\x6e\x6f\ -\x74\x20\x62\x65\x20\x76\x65\x72\x69\x66\x69\x65\x64\x2e\x07\x00\ -\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\ -\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x16\x00\x63\x00\x6f\x00\ -\x6d\x00\x70\x00\x72\x00\x6f\x00\x26\x00\x62\x00\x61\x00\x72\x00\ -\x21\x08\x00\x00\x00\x00\x06\x00\x00\x00\x07\x63\x68\x65\x63\x26\ -\x6b\x21\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\ -\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\x00\x00\x00\x3a\x00\ -\x63\x00\x6f\x00\x6d\x00\x70\x00\x72\x00\x6f\x00\x62\x00\x61\x00\ -\x6e\x00\x64\x00\x6f\x00\x20\x00\x6e\x00\x6f\x00\x6d\x00\x62\x00\ -\x72\x00\x65\x00\x20\x00\x64\x00\x65\x00\x20\x00\x64\x00\x6f\x00\ -\x6d\x00\x69\x00\x6e\x00\x69\x00\x6f\x08\x00\x00\x00\x00\x06\x00\ -\x00\x00\x14\x63\x68\x65\x63\x6b\x69\x6e\x67\x20\x64\x6f\x6d\x61\ -\x69\x6e\x20\x6e\x61\x6d\x65\x07\x00\x00\x00\x12\x53\x65\x6c\x65\ -\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x03\ -\x00\x00\x00\x34\x00\x63\x00\x6f\x00\x6d\x00\x70\x00\x72\x00\x6f\ -\x00\x62\x00\x61\x00\x6e\x00\x64\x00\x6f\x00\x20\x00\x63\x00\x6f\ -\x00\x6e\x00\x65\x00\x78\x00\x69\x00\x6f\x00\x6e\x00\x20\x00\x68\ -\x00\x74\x00\x74\x00\x70\x00\x73\x08\x00\x00\x00\x00\x06\x00\x00\ -\x00\x19\x63\x68\x65\x63\x6b\x69\x6e\x67\x20\x68\x74\x74\x70\x73\ -\x20\x63\x6f\x6e\x6e\x65\x63\x74\x69\x6f\x6e\x07\x00\x00\x00\x12\ -\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\x76\x69\x64\x65\x72\x50\x61\ -\x67\x65\x01\x03\x00\x00\x00\x3a\x00\x6f\x00\x62\x00\x74\x00\x65\ -\x00\x6e\x00\x69\x00\x65\x00\x6e\x00\x64\x00\x6f\x00\x20\x00\x69\ -\x00\x6e\x00\x66\x00\x6f\x00\x20\x00\x64\x00\x65\x00\x6c\x00\x20\ -\x00\x70\x00\x72\x00\x65\x00\x76\x00\x65\x00\x65\x00\x64\x00\x6f\ -\x00\x72\x08\x00\x00\x00\x00\x06\x00\x00\x00\x16\x66\x65\x74\x63\ -\x68\x69\x6e\x67\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x20\x69\x6e\ -\x66\x6f\x07\x00\x00\x00\x12\x53\x65\x6c\x65\x63\x74\x50\x72\x6f\ -\x76\x69\x64\x65\x72\x50\x61\x67\x65\x01\x88\x00\x00\x00\x02\x01\ -\x01\ -" - -qt_resource_name = "\ -\x00\x0c\ -\x0d\xfc\x11\x13\ -\x00\x74\ -\x00\x72\x00\x61\x00\x6e\x00\x73\x00\x6c\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x73\ -\x00\x05\ -\x00\x6a\x85\x7d\ -\x00\x64\ -\x00\x65\x00\x2e\x00\x71\x00\x6d\ -\x00\x05\ -\x00\x6c\x65\x7d\ -\x00\x65\ -\x00\x73\x00\x2e\x00\x71\x00\x6d\ -" - -qt_resource_struct = "\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\ -\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x00\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x17\x98\ -" - -def qInitResources(): - QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -def qCleanupResources(): - QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() diff --git a/src/leap/gui/mainwindow_rc.py b/src/leap/gui/mainwindow_rc.py deleted file mode 100644 index 9d16a35e..00000000 --- a/src/leap/gui/mainwindow_rc.py +++ /dev/null @@ -1,1130 +0,0 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created: Wed Jan 30 06:06:54 2013 -# by: The Resource Compiler for PyQt (Qt v4.8.2) -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore - -qt_resource_data = "\ -\x00\x00\x05\x95\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x40\x00\x00\x00\x40\x08\x03\x00\x00\x00\x9d\xb7\x81\xec\ -\x00\x00\x00\x03\x73\x42\x49\x54\x08\x08\x08\xdb\xe1\x4f\xe0\x00\ -\x00\x00\x09\x70\x48\x59\x73\x00\x00\x37\x5d\x00\x00\x37\x5d\x01\ -\x19\x80\x46\x5d\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\ -\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x1f\x74\x45\x58\ -\x74\x54\x69\x74\x6c\x65\x00\x47\x6e\x6f\x6d\x65\x20\x53\x79\x6d\ -\x62\x6f\x6c\x69\x63\x20\x49\x63\x6f\x6e\x20\x54\x68\x65\x6d\x65\ -\x8e\xa4\x29\xab\x00\x00\x02\x13\x50\x4c\x54\x45\xff\xff\xff\xff\ -\x00\x00\xff\x00\x00\xaa\x00\x00\xbf\x00\x00\xbf\xbf\xbf\xd5\x00\ -\x00\xc6\x00\x00\xc4\x00\x00\xbb\xbb\xbb\xcc\x00\x00\xcf\x00\x00\ -\xcc\x00\x00\xce\x00\x00\xb9\xb9\xb9\xc2\xc2\xc2\xce\x00\x00\xca\ -\x00\x00\xcc\x00\x00\xcd\x00\x00\xcc\x00\x00\xc1\xc1\xc1\xce\x00\ -\x00\xca\x00\x00\xcb\x00\x00\xcd\x00\x00\xcb\x00\x00\xcd\x00\x00\ -\xce\x00\x00\xbe\xbe\xbe\xcc\x00\x00\xbf\xbf\xbf\xbe\xbe\xbe\xcd\ -\x00\x00\xcb\x00\x00\xcc\x00\x00\xcd\x00\x00\xcc\x00\x00\xcc\x00\ -\x00\xbe\xbe\xbe\xbf\xbf\xbf\xcb\x00\x00\xcb\x00\x00\xcb\x00\x00\ -\xcc\x00\x00\xcc\x00\x00\xbf\xbf\xbf\xcd\x00\x00\xcb\x00\x00\xcc\ -\x00\x00\xcc\x00\x00\xbf\xbf\xbf\xcc\x00\x00\xcc\x00\x00\xcc\x00\ -\x00\xbe\xbe\xbe\xcc\x00\x00\xbe\xbe\xbe\xcc\x00\x00\xcc\x00\x00\ -\xcc\x00\x00\xcc\x00\x00\xbe\xbe\xbe\xbe\xbe\xbe\xcc\x00\x00\xcc\ -\x00\x00\xcc\x00\x00\xcc\x00\x00\xbe\xb7\xb7\xbe\xb8\xb8\xbe\xba\ -\xba\xbe\xbc\xbc\xbe\xbd\xbd\xbe\xbe\xbe\xbf\xaa\xaa\xbf\xab\xab\ -\xbf\xac\xac\xbf\xad\xad\xbf\xae\xae\xbf\xb0\xb0\xbf\xb1\xb1\xbf\ -\xb4\xb4\xbf\xb6\xb6\xbf\xb7\xb7\xc0\x9c\x9c\xc0\x9d\x9d\xc0\xa1\ -\xa1\xc0\xa2\xa2\xc0\xa4\xa4\xc0\xa5\xa5\xc0\xa6\xa6\xc0\xa7\xa7\ -\xc0\xa8\xa8\xc1\x8d\x8d\xc1\x91\x91\xc1\x94\x94\xc1\x95\x95\xc1\ -\x96\x96\xc1\x99\x99\xc1\x9c\x9c\xc2\x82\x82\xc2\x87\x87\xc2\x88\ -\x88\xc2\x8d\x8d\xc2\x8e\x8e\xc3\x73\x73\xc3\x74\x74\xc3\x76\x76\ -\xc3\x79\x79\xc3\x7c\x7c\xc3\x7d\x7d\xc3\x7f\x7f\xc4\x67\x67\xc4\ -\x6c\x6c\xc4\x6d\x6d\xc4\x6e\x6e\xc4\x70\x70\xc5\x59\x59\xc5\x5d\ -\x5d\xc5\x5f\x5f\xc5\x62\x62\xc5\x63\x63\xc6\x4c\x4c\xc6\x4f\x4f\ -\xc6\x50\x50\xc6\x53\x53\xc6\x56\x56\xc6\x58\x58\xc7\x3e\x3e\xc7\ -\x41\x41\xc7\x43\x43\xc7\x45\x45\xc7\x46\x46\xc7\x47\x47\xc7\x4b\ -\x4b\xc8\x31\x31\xc8\x35\x35\xc8\x36\x36\xc8\x38\x38\xc8\x3a\x3a\ -\xc8\x3c\x3c\xc9\x22\x22\xc9\x25\x25\xc9\x26\x26\xc9\x27\x27\xc9\ -\x28\x28\xc9\x2a\x2a\xc9\x2d\x2d\xc9\x2e\x2e\xca\x16\x16\xca\x17\ -\x17\xca\x1a\x1a\xca\x1b\x1b\xca\x1c\x1c\xca\x1d\x1d\xca\x1e\x1e\ -\xca\x20\x20\xca\x21\x21\xcb\x07\x07\xcb\x09\x09\xcb\x0a\x0a\xcb\ -\x0c\x0c\xcb\x0d\x0d\xcb\x0e\x0e\xcb\x0f\x0f\xcb\x10\x10\xcb\x11\ -\x11\xcb\x12\x12\xcb\x13\x13\xcc\x00\x00\xcc\x01\x01\xcc\x02\x02\ -\xcc\x03\x03\xcc\x04\x04\xcc\x05\x05\xcc\x06\x06\xcc\x07\x07\xd4\ -\x0d\x79\xbb\x00\x00\x00\x44\x74\x52\x4e\x53\x00\x01\x02\x03\x04\ -\x04\x06\x09\x0d\x0f\x0f\x10\x14\x15\x16\x19\x1a\x1d\x1e\x24\x28\ -\x29\x2a\x30\x36\x3d\x40\x42\x43\x4b\x55\x58\x5e\x60\x63\x64\x65\ -\x6e\x73\x7d\x7f\x8a\x94\x99\x9a\xaa\xb2\xbb\xbc\xc3\xc9\xca\xd2\ -\xd5\xde\xe0\xe3\xe6\xe8\xed\xef\xf6\xf7\xfa\xfa\xfb\xfc\xfd\xef\ -\xfa\x14\xec\x00\x00\x02\x79\x49\x44\x41\x54\x58\xc3\xed\x96\xd7\ -\x5b\x13\x41\x14\x47\x47\x05\x15\x5b\x2c\xa0\x58\x62\xb0\x26\xb6\ -\xa0\x46\xc5\x28\x12\x6c\x49\xae\x0d\x62\x01\xc5\x82\x58\x10\x7b\ -\x2f\xd8\x1b\x8a\xbd\x00\x62\x41\x45\x14\xf5\x04\x51\xff\x44\x1f\ -\x76\x37\x1f\x09\xc9\x66\x37\x8f\xc8\xef\xed\xee\xf7\x9d\xb3\x33\ -\x73\x67\x66\x57\xa9\xfe\x98\x65\x94\xd3\xe3\xf5\xfb\xbd\x1e\xe7\ -\xa8\x8c\xf0\x6c\x77\x40\xf4\x04\xdc\xd9\xf6\xf9\x3c\x9f\xf4\x88\ -\x2f\xcf\x2e\x5f\x50\x22\x71\x29\x29\xb0\xf9\xfe\x04\x5e\xa4\xc4\ -\xd6\x18\xb2\x7d\xd2\x2b\x3e\x3b\xeb\xe0\x96\x24\x71\xdb\xe8\x5f\ -\x20\x99\x20\x60\xbd\x9b\x4e\x83\x29\x72\x39\x1c\xae\x22\xa3\x72\ -\x5a\x16\x78\x0c\x3e\x47\x29\xa5\x72\x0c\x83\xc7\xb2\xc0\xab\x13\ -\x2e\xad\x74\xe9\xa5\xd7\xb2\xc0\xaf\x13\x0e\xad\x74\xe8\xa5\xdf\ -\x12\x3c\x31\x57\x29\x63\xd2\xc6\xb3\x58\x3d\x60\xda\xa0\x74\x7c\ -\x6e\x71\xf1\x94\x94\x82\x21\x0b\x98\x9b\x86\x1f\xb9\x0c\x56\xcf\ -\x4c\x21\xd8\xb4\x18\x98\x65\x2e\x28\x04\xe0\x66\x38\x99\xe0\x60\ -\x0b\xc0\x9a\x7c\x53\xc1\x84\xe5\x00\x3c\xd9\xda\x7b\x1f\x1d\x6e\ -\x07\x60\xce\x40\xf3\x21\x8c\x7e\x01\xc0\xf3\x0d\x89\xfc\xd9\x2e\ -\x80\xee\xa3\x69\xbb\xb0\xed\x29\x00\x3f\x0f\xc4\xe1\xa1\x6b\x00\ -\x7c\x39\x22\x69\x05\x12\xbe\x05\xc0\xa7\x43\x3d\xf8\x48\x23\x00\ -\xad\x15\x62\x41\x20\x72\xac\x1b\x20\x7a\x26\xc6\x57\xb5\x01\xd0\ -\x54\x26\xd6\x04\x52\xd7\x01\xc0\x83\xa0\xc6\x57\x77\x02\x70\x2f\ -\x28\x56\x05\x52\xf9\x16\x80\x87\xe5\x22\x22\x27\x7f\x03\x44\xaf\ -\xc4\xb7\xd6\x5c\x20\x65\xcd\x00\xbc\xdf\x29\x72\x19\x80\x3f\xa7\ -\xc4\x96\x40\x82\x0d\x00\x7c\xdd\x7b\x17\x80\xce\x6a\xb1\x29\x10\ -\xa9\x8f\x02\x44\x01\x68\xab\x12\xfb\x02\x39\xfd\x17\x3d\x8d\x11\ -\xc9\x44\x20\x35\x5a\x33\xb8\x1a\x92\xcc\x04\x17\x7e\x69\x82\x57\ -\x9b\x33\x12\x84\x6e\x18\x33\xa0\x65\x7b\x06\x82\xc8\x63\x00\x3e\ -\x03\xd0\x5e\x6b\x5b\xb0\xe7\x19\x00\x2f\x37\x9e\xef\x02\xe8\x3a\ -\x67\x53\x50\xf3\x03\x80\xdb\xeb\x44\x6a\xb5\x7b\xe0\x7a\xc8\x8e\ -\x40\xef\xe0\xc5\xb0\x88\xc8\x8e\x37\x00\x3c\x8a\x58\x17\x68\x7b\ -\xa8\xfb\xb8\x3e\xea\x2d\xaf\x01\xf8\xb8\xdb\xa2\x20\x78\x1f\x80\ -\x8e\xba\xd8\xc2\xad\xbf\x03\xc0\xf7\xfd\x96\x04\x65\x4d\x00\xbc\ -\xab\xec\xd1\xfc\xf0\xa5\xd8\x89\x4a\x2b\xa8\x68\x05\xa0\xb9\x3c\ -\xfe\x4a\x3c\x61\x9c\xe9\x74\xfc\x64\x6d\xfb\x36\x04\x13\x2f\xd5\ -\x7d\xdf\xb4\x5b\x65\xb0\x39\x3f\xa3\x14\x20\x5a\x9f\xe4\xf7\x60\ -\xd7\x07\x00\x16\x8d\x30\x15\xcc\x03\x58\x35\x35\xf1\x9b\xa8\x94\ -\x52\x6a\xf8\x42\x80\x95\xe3\x4c\x05\x59\x85\xb0\x22\x5f\x25\x15\ -\xa8\xac\xf9\x50\x3a\x29\xcd\x1a\x0c\x5b\xba\x64\xac\x4a\x21\x50\ -\x6a\xf6\xda\xe9\x69\xbb\x30\x66\xa8\x4a\x2d\x50\xe3\xed\xfd\x2d\ -\x5a\x3e\x40\x7d\x44\x20\x36\xd3\x2f\xe8\x9b\x82\xff\x38\xff\x00\ -\xc1\x36\x30\x95\xf0\x66\xca\x60\x00\x00\x00\x00\x49\x45\x4e\x44\ -\xae\x42\x60\x82\ -\x00\x00\x04\xec\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0\x77\x3d\xf8\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x06\xec\x00\x00\x06\xec\ -\x01\x1e\x75\x38\x35\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ -\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ -\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x13\x74\x45\ -\x58\x74\x41\x75\x74\x68\x6f\x72\x00\x52\x6f\x64\x6e\x65\x79\x20\ -\x44\x61\x77\x65\x73\x0e\xd8\x7e\x1d\x00\x00\x04\x4a\x49\x44\x41\ -\x54\x48\x89\x8d\x96\x5d\x6c\x53\x65\x18\xc7\x7f\xef\x39\x6b\xbb\ -\x7e\x9c\x75\x65\xad\x2b\x9b\xfb\xd0\x31\xdd\x14\xb6\x8c\x19\x44\ -\x90\x44\x63\x82\x42\x88\x5e\x90\x98\xcc\x19\x15\x13\xd4\x18\x76\ -\x61\xd4\x18\xe3\x85\x57\xca\x05\xe1\xc2\x0c\xa3\xa8\x51\xd0\x4c\ -\x12\xe3\x85\x31\x80\x26\x6a\xe2\x85\x23\xb0\x38\xb6\xc1\x1c\xce\ -\xb1\x40\x59\xf6\xe5\xca\xda\xae\xed\xfa\x75\x7a\x5e\x2f\x4e\xd7\ -\x59\xd6\x32\xfe\xc9\x7b\xf3\x9e\xe7\xf9\xff\x9f\xe7\xff\x9e\xf3\ -\x9c\x57\x48\x29\x59\x0f\xbd\x7b\x85\x0d\x17\xed\x1e\xbb\xb2\x07\ -\x20\x94\x30\x7e\x22\xc6\x48\xcf\x59\x99\x5a\x2f\x57\x94\x12\xf8\ -\xec\x55\x61\x71\x65\x6d\x47\xfc\xbe\xda\x47\x9d\x5a\xa5\xbf\xda\ -\x69\xaf\xda\xe0\x28\x2f\x07\x58\x5c\x4e\x26\xe7\xe3\x89\x9b\xf1\ -\x68\x78\x6e\x6e\x61\xfa\x8f\x98\x9a\x7a\xfb\x95\xe3\x32\x73\xc7\ -\x02\x9f\x76\x89\x8e\xba\xda\xda\x2f\xb7\x37\xdf\xdf\xe6\x2a\x13\ -\x8a\x94\x06\x82\xc2\x38\x89\x40\x08\x85\x98\x2e\x8d\xf3\x13\xe3\ -\x97\xa6\xa6\xa7\x5f\x7e\xed\x94\x1c\x5a\x57\xa0\xef\xa0\xfd\x70\ -\x5b\xf3\x96\x03\xcd\xde\x8a\x6a\x61\x64\xd7\x73\xc0\x14\x53\x54\ -\x26\x82\x4b\xf3\x97\x26\x2e\x7f\xd5\xfd\x79\xe2\xdd\x92\x02\x27\ -\x5f\x2a\x7b\xe1\x89\xce\x1d\xc7\xbc\x76\x55\x13\xc5\x98\xac\x4e\ -\x10\x0a\xa4\xa2\x6b\x45\x80\x60\x22\x1b\xfd\x6d\xf0\xdc\xa1\x17\ -\x4f\xe8\x5f\xaf\x11\x38\xfa\x9c\xf0\x6e\xdb\xf4\xc0\xf9\x6d\xf5\ -\xfe\x26\x30\xf2\x89\xca\xc6\x76\xd4\x07\xf7\xa3\xd4\x74\x80\xd5\ -\x65\x6e\xa6\xe3\x64\x03\xfd\x64\x2f\x9e\x40\x46\x67\xff\x27\xa3\ -\x30\x70\x63\x6e\x72\xe0\xea\xd8\xf6\x37\xbf\x95\x41\x73\x27\x87\ -\x06\x8f\xa7\x6f\x6b\x7d\x4d\x01\x39\x80\x52\xff\x08\x4a\xe3\xae\ -\x55\xf2\x5c\x27\x6a\xf3\x6e\x2c\x7b\x8f\x9a\x5d\xe5\x61\xb0\xb5\ -\xbe\xa6\xa9\xc1\xe3\xe9\x5b\x95\x04\x7a\xbb\x44\x47\x5b\x53\xcb\ -\x4e\x15\xbd\x98\x31\xc8\x70\x00\xfd\xfc\xc7\x64\xce\xbc\x81\x7e\ -\xe1\x13\xc8\x75\x2d\xb4\x8d\x28\xb5\x0f\x15\xc4\xaa\xe8\xb4\x35\ -\xb5\xec\xec\xed\x12\x1d\x00\x65\x00\xee\x72\x65\x9f\x5f\x73\x38\ -\x05\x6b\x0f\x35\x3b\xf6\x03\xfa\xc0\xf1\x3c\x29\xb3\xc3\xa8\xf7\ -\x3e\x8e\xf0\xb5\x98\x22\xf6\x0d\x05\xf1\x02\xf0\x6b\x0e\xa7\xbb\ -\x5c\xd9\x07\x0c\x29\x00\x9a\xc3\xd5\x69\x55\xd5\xe2\xd5\x47\xe7\ -\x56\xc9\x01\xe1\xbe\x1b\xe1\xb9\x67\xf5\x79\x70\x7c\x4d\x8e\x55\ -\x55\xd1\x1c\xae\xce\xbc\x45\x15\x6e\x5f\x9d\x90\xc5\xed\x29\xa8\ -\xae\xa2\x06\xcb\x53\x47\xa0\xcc\x66\x76\x37\xfa\x3d\xd9\xa9\x81\ -\xb5\x71\x52\xa7\xc2\xed\xab\x83\x9c\x45\x76\xbb\x56\x25\xa5\xa4\ -\xe8\xab\xb9\x02\x9b\x86\x65\xf7\x87\x08\xcd\x6f\x92\x8f\x9f\x21\ -\xf5\xdd\xf3\xa0\xa7\x10\xe5\x6e\x44\x45\x2d\x38\x7d\x08\x21\x90\ -\xd2\xe4\xcc\x0b\x24\x12\xd1\x9b\x42\xbd\xab\x81\x6c\xba\x28\xb7\ -\x94\x06\x65\xcd\x4f\x22\x2a\x1b\x00\x30\xa6\xff\x24\xd5\xb7\x1f\ -\x74\x73\x14\xc9\x64\x04\x99\x8c\x80\xc5\x8e\xe2\xae\x03\xab\x93\ -\x44\x22\x7a\x33\x6f\xd1\x52\x64\x61\x0a\xb5\xbc\x28\xb1\xb1\x34\ -\x83\x91\xb3\xc1\x98\x1d\xc1\x98\x1d\x41\x3f\xd7\x9b\x27\x2f\x40\ -\x26\x81\x11\xfc\x07\x99\x8a\x99\x9c\x2b\x1d\x44\x97\x63\x83\xc9\ -\xe8\xfc\x33\x36\x23\x05\xaa\x05\xd2\xcb\xc8\x74\xcc\xfc\x88\x72\ -\x5d\xa5\x7f\x3c\x74\x3b\x03\x0b\x90\x52\xed\x44\x97\x63\x83\x79\ -\x81\x48\xd2\x38\x3d\x1b\xcf\xbc\x53\x1f\xb9\xe4\x44\x1a\x45\x93\ -\xac\xcf\x7e\x83\xda\xb8\xcb\x2c\xf4\xd7\xf7\xd1\x2f\x9e\x2c\xce\ -\x2e\x14\xe6\xd2\x65\xf1\x48\xd2\x38\x0d\x39\x8b\x7a\x4e\xc9\xa1\ -\xd1\xc0\xb5\xfe\xac\xb7\xb5\x64\x55\xc2\xe5\x47\x54\x36\x98\xe7\ -\x60\xd3\x4a\xc6\x65\xbd\xad\x8c\x06\xae\xf5\xf7\xe4\x26\x6b\x7e\ -\x54\x04\x42\xa1\xee\xe1\x90\x31\x29\x1c\xde\xd2\xbd\xaf\x03\xe1\ -\xf0\x32\x1c\x32\x26\x03\xa1\x50\x77\x7e\xef\xd6\x69\xfa\x58\x7b\ -\xe7\x31\x5f\x78\x54\x23\xb3\x5c\x90\xac\xf8\xdb\x10\x0e\xf3\xab\ -\x35\x82\x13\xc8\xa5\xe9\x42\x76\x8b\x83\x85\xca\xcd\xd1\xdf\x47\ -\x06\x8b\x4f\xd3\x15\xf4\x1d\xb4\x1f\xde\xd2\xd4\x7a\x60\x93\x1a\ -\xaa\x26\x74\xfd\xce\x4a\xf7\x34\x72\x35\xeb\x99\xbf\x3c\x79\xe5\ -\xf6\xff\x83\x15\x7c\xf0\xb4\xd8\xbe\xb9\xa9\xe6\x8b\x1d\x0d\xd5\ -\xad\xae\xd8\x94\x22\x13\x21\x90\xb7\xcc\x29\xa1\x22\xec\x1e\x62\ -\xae\x3a\xa3\xff\xfa\xfc\xdf\xe7\xc6\x66\x5e\x3f\xf2\x0b\xfd\x52\ -\x16\x8e\x84\x02\x01\x21\x84\x0a\x54\x01\x95\x9a\x1d\xdf\x7b\x7b\ -\xac\x6f\xdd\x57\xb7\xb1\x6d\x83\xbb\xd2\x53\xe3\x10\x2e\x9f\xcd\ -\xb0\x00\xfc\x9b\x54\xf4\x99\x84\x8c\x2d\x86\xc3\xe1\x2b\x81\xd9\ -\xbf\x0e\xff\x9c\xfe\x28\x9e\x22\x08\x84\x80\xb0\x94\x32\x5c\xb2\ -\x03\x21\x84\x13\xf0\x00\xee\xdc\xd2\x5c\x56\x3c\x5b\xeb\x69\x79\ -\xb8\x51\x74\x18\x12\xe5\xc2\x75\x39\x3c\x74\x83\xc9\x78\x86\x10\ -\x10\x03\x96\x80\x48\x6e\x2d\x4a\xb9\x7a\x01\x28\x79\xab\xc8\x89\ -\x59\x00\x2b\x60\xcb\x2d\x0b\xa0\x02\x3a\x90\x02\xd2\x40\x12\xc8\ -\x48\x79\xab\x87\x26\xfe\x03\x26\x93\xd5\x41\x51\x76\x98\xdb\x00\ -\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x01\xaa\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x40\x00\x00\x00\x40\x08\x03\x00\x00\x00\x9d\xb7\x81\xec\ -\x00\x00\x00\x03\x73\x42\x49\x54\x08\x08\x08\xdb\xe1\x4f\xe0\x00\ -\x00\x00\x09\x70\x48\x59\x73\x00\x00\x37\x5d\x00\x00\x37\x5d\x01\ -\x19\x80\x46\x5d\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\ -\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x1f\x74\x45\x58\ -\x74\x54\x69\x74\x6c\x65\x00\x47\x6e\x6f\x6d\x65\x20\x53\x79\x6d\ -\x62\x6f\x6c\x69\x63\x20\x49\x63\x6f\x6e\x20\x54\x68\x65\x6d\x65\ -\x8e\xa4\x29\xab\x00\x00\x00\x36\x50\x4c\x54\x45\xff\xff\xff\xbf\ -\xbf\xbf\xbb\xbb\xbb\xb9\xb9\xb9\xc2\xc2\xc2\xc1\xc1\xc1\xbe\xbe\ -\xbe\xbf\xbf\xbf\xbe\xbe\xbe\xbe\xbe\xbe\xbf\xbf\xbf\xbf\xbf\xbf\ -\xbf\xbf\xbf\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\ -\xbe\xbe\xf1\xb6\xe9\xa5\x00\x00\x00\x11\x74\x52\x4e\x53\x00\x04\ -\x0f\x16\x19\x29\x4b\x58\x5e\x7d\x7f\xb2\xca\xe0\xe6\xf7\xfa\x2a\ -\xb3\x5d\x53\x00\x00\x00\x9e\x49\x44\x41\x54\x58\xc3\xed\x95\xb9\ -\x12\x83\x30\x0c\x05\x31\x18\x1b\x1f\x18\xeb\xff\x7f\x36\xc9\x20\ -\xcd\x24\xe1\x92\xe8\x00\x6d\xf7\x8a\x5d\xc0\x2e\x68\x1a\x65\x8f\ -\xce\xc5\x54\x4a\x8a\xae\x3b\xa5\x9b\x50\x01\xa9\xc1\xc8\xfd\x3e\ -\xc3\x17\xb9\x97\xfa\xc3\x04\x3f\x4c\x83\xf0\xf9\x7f\xfe\xbb\x20\ -\x7a\x07\x93\x61\x41\x96\x9c\x43\x80\x15\x82\xe0\xfe\xea\x5a\xa0\ -\xf2\x6f\xd3\x91\x33\x7a\x6b\xfd\x48\xcb\xb1\x03\x91\xfc\xf6\xb3\ -\x5a\x2a\x44\x76\x20\xa1\xe1\xe7\xe9\x71\x26\x76\xa0\xa0\x61\xe7\ -\x69\x71\x16\x76\x80\x3e\x7a\x6b\xdf\x3d\x00\x07\x68\x40\x03\x1a\ -\xd0\x80\x06\x9e\x15\xd8\xfb\xc1\x88\xd1\xc0\xe5\x02\x20\x44\x03\ -\xf7\x0c\x3c\x98\x17\xb4\xcd\x62\x13\x3b\x4c\x60\xe6\x00\x00\x00\ -\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x02\xc8\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x40\x00\x00\x00\x40\x08\x03\x00\x00\x00\x9d\xb7\x81\xec\ -\x00\x00\x00\x03\x73\x42\x49\x54\x08\x08\x08\xdb\xe1\x4f\xe0\x00\ -\x00\x00\x09\x70\x48\x59\x73\x00\x00\x37\x5d\x00\x00\x37\x5d\x01\ -\x19\x80\x46\x5d\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\ -\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x1f\x74\x45\x58\ -\x74\x54\x69\x74\x6c\x65\x00\x47\x6e\x6f\x6d\x65\x20\x53\x79\x6d\ -\x62\x6f\x6c\x69\x63\x20\x49\x63\x6f\x6e\x20\x54\x68\x65\x6d\x65\ -\x8e\xa4\x29\xab\x00\x00\x00\xb4\x50\x4c\x54\x45\xff\xff\xff\xff\ -\xff\xff\x80\x80\x80\xbf\xbf\xbf\xcc\xcc\xcc\xbf\xbf\xbf\xc6\xc6\ -\xc6\xb3\xb3\xb3\xc8\xc8\xc8\xc3\xc3\xc3\xba\xba\xba\xc4\xc4\xc4\ -\xbd\xbd\xbd\xb9\xb9\xb9\xb9\xb9\xb9\xbf\xbf\xbf\xbc\xbc\xbc\xbd\ -\xbd\xbd\xbe\xbe\xbe\xbd\xbd\xbd\xbc\xbc\xbc\xbe\xbe\xbe\xbe\xbe\ -\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbf\xbf\xbf\xbd\xbd\xbd\xbe\xbe\xbe\ -\xbf\xbf\xbf\xbd\xbd\xbd\xbf\xbf\xbf\xbe\xbe\xbe\xbe\xbe\xbe\xbe\ -\xbe\xbe\xbe\xbe\xbe\xbd\xbd\xbd\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\ -\xbe\xbe\xbe\xbe\xbd\xbd\xbd\xbf\xbf\xbf\xbe\xbe\xbe\xbe\xbe\xbe\ -\xbe\xbe\xbe\xbf\xbf\xbf\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\ -\xbe\xbe\xbe\xbe\xbe\xbd\xbd\xbd\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\ -\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\ -\xe4\x72\x0e\xe3\x00\x00\x00\x3b\x74\x52\x4e\x53\x00\x01\x02\x04\ -\x05\x08\x09\x0a\x0e\x11\x1a\x1a\x1f\x21\x2c\x2c\x35\x36\x3b\x3e\ -\x41\x43\x47\x4b\x4e\x50\x55\x56\x57\x59\x63\x66\x71\x86\x89\x90\ -\x95\x96\x9d\x9e\xa7\xaa\xad\xb5\xb8\xbe\xc0\xc3\xc5\xc9\xcd\xd6\ -\xe0\xe6\xef\xf5\xf7\xfc\xfd\xec\xba\xa4\x27\x00\x00\x01\x14\x49\ -\x44\x41\x54\x58\xc3\xed\x95\x5b\x57\x82\x40\x14\x85\x9d\x44\xb4\ -\x40\x50\x29\x92\x64\xb2\xd4\xee\xf7\xbc\x54\xf2\xff\xff\x97\xb2\ -\x98\x91\xe2\x20\xed\xe3\x4b\x2b\xe4\x7b\xfb\xd6\x39\x7b\x73\x99\ -\x87\xa9\xd5\x2a\x8a\x68\x58\xae\x17\x04\x9e\x6b\x35\x76\x8a\x0b\ -\x27\x94\x8a\xd0\x11\xfc\x7c\xcb\x97\xdf\xf0\x5b\xdc\x7c\x7b\x20\ -\x7f\x30\x68\x33\x9f\x9f\xc9\xaf\x1b\x58\xef\x20\x7c\x49\xf0\x39\ -\xff\xc1\x91\x39\x38\x8c\xf3\x0b\xf3\x0a\x42\xfc\x34\x2d\x9d\xe9\ -\xdb\xa6\x69\xf7\xb5\x59\x70\x81\xab\xf3\x46\x6c\x86\x6e\x70\xe1\ -\x02\x4f\x25\xec\x44\x6d\xa5\x1e\x5c\x10\xa8\x84\x99\xa8\xa9\x34\ -\x80\x0b\xf4\x47\x6f\xf3\xb2\x17\xc8\x5f\xa8\x0a\xfe\x45\xc1\xc9\ -\x9a\x6d\xe1\x78\x56\xdd\x76\xc5\x74\xc7\x6f\xef\x93\x63\xdc\x33\ -\x18\x97\x5f\x51\xcc\x55\x13\x73\xc2\x28\x52\xdc\x60\x9e\xa5\xb7\ -\xd4\x0b\xd1\x29\xe2\x84\xeb\xcd\x3c\xba\x47\x9c\x30\x4d\x17\xe6\ -\x88\x13\x66\xe9\xc2\x42\x00\x4e\xb8\x4d\x17\x1e\x11\x27\x9c\xa5\ -\x0b\x43\xc4\x29\x77\x7a\xfe\x24\x20\x27\x1c\x3e\x24\xf3\x97\x23\ -\xcc\x73\x38\x7f\xfe\xf8\x7c\xbd\x38\x80\x3d\x87\xba\xc1\xf3\x3d\ -\xa0\xa3\xd8\xb9\x80\x7d\xad\xff\x6d\x81\x64\x52\x15\x94\xb3\x60\ -\x8f\x59\x01\x25\xba\xb5\x2a\xd7\xa3\x29\x75\x00\x00\x00\x00\x49\ -\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x27\x74\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x80\x00\x00\x00\x66\x08\x06\x00\x00\x00\x03\x23\x99\x54\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x02\x3a\x00\x00\x02\x3a\ -\x01\xfe\x36\x29\x51\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ -\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ -\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x26\xf1\x49\x44\ -\x41\x54\x78\xda\xed\x9d\x05\x58\x54\x59\xff\xc7\x7f\x77\x86\x14\ -\x90\x50\x14\x41\x94\x10\x51\x09\x45\xb0\x50\xb0\x10\x10\x13\x6b\ -\x2d\x6c\xb0\xdd\xb5\xdb\x35\xd7\x5a\x5b\x09\xdb\xb5\xdb\xb5\x0b\ -\x19\x42\xba\x15\x24\x0c\x14\x29\x15\x29\x25\x06\x66\x7e\xff\x73\ -\x86\xb9\x38\xb0\x60\xbc\xaf\xfe\x5f\x70\x67\x9e\xe7\xf3\xec\xaa\ -\xc0\x1d\xe6\xfb\xb9\xe7\x9e\x3e\x80\x88\x20\xe5\xdf\x8b\xf4\x43\ -\xf8\x9a\x0f\x09\x40\x51\x2a\xc0\xbf\x27\x6c\x5d\x42\x77\xc2\x28\ -\x42\x34\x21\x9f\xc0\x27\xcc\x21\x34\x94\xf8\x3a\x19\xa9\x00\x3f\ -\x4f\xe8\xfa\x84\x49\x84\xab\x84\x62\x02\xd6\x40\x1e\xe1\x00\x21\ -\x96\xb0\x59\x2a\x40\xdd\x0f\xbe\x1d\xe1\x2d\x1b\x70\x23\x79\x79\ -\xec\xa7\xa5\x85\xab\x5b\xb7\x46\x2f\x73\x6b\xf4\x6c\xb0\x12\x97\ -\xa8\x4e\xc6\xd1\x4a\x4e\x68\x2d\xdf\x0e\x75\xb8\x8d\x90\x03\x1c\ -\x56\x86\x9b\x52\x01\xea\x5e\xe0\xc3\x08\xbb\x08\xd3\x09\xf3\x08\ -\x6f\x54\x64\x64\x70\x93\xa9\x29\xbe\x74\x74\x44\x1c\x32\xa4\x82\ -\xe2\xc1\x83\x05\x99\xba\xde\xc2\x77\xba\xfe\x28\x49\x82\xf6\x55\ -\x6c\x29\xdb\x9c\xfe\xb0\xd4\x9f\xe1\x31\xf0\x6f\x13\x60\x11\x7b\ -\xb7\x73\x18\x06\x27\xe9\xe9\x61\x66\xbf\x7e\x95\x82\x97\x24\xdb\ -\xe4\x68\x5c\x55\x01\x28\x8f\xb5\x2f\xa3\x9e\x8c\x0e\xfd\x39\x01\ -\x04\x6d\xa9\x00\x75\x23\x7c\x2f\x1a\xbc\xa5\x66\x7d\xf4\xb0\xb0\ -\xc0\x94\x2a\x77\x7c\x75\x14\x77\x5f\xe9\x53\x9d\x00\x94\xa8\x26\ -\xe7\x45\x8f\x04\xf2\x33\x93\x08\x06\x52\x01\x6a\xbf\x00\x9a\x84\ -\xe7\x2b\x26\x70\x30\xca\xa5\xfd\x17\xc3\xa7\x94\x0d\x18\x17\x56\ -\x93\x00\x94\xb0\x26\xa7\xb1\x31\xb7\x01\xfd\xe1\x45\x84\x38\x82\ -\x8e\x54\x80\xda\x2b\x00\x43\xc8\x98\x3b\x06\x30\xf5\x06\x60\xc9\ -\x1c\x9b\xf8\x2f\x4a\xe0\x3c\x24\xff\x5d\x33\xbf\xb2\xcf\x49\xf0\ -\x50\xeb\x38\x36\xe0\xa8\xb2\x15\xc3\x35\x3f\xf8\xfd\xdb\x12\x8e\ -\x88\x1f\x3d\x43\xe9\xdf\x49\x05\xf8\xb6\x0f\xd1\x63\xe2\x40\xc0\ -\x1c\x1e\xe0\xfb\x07\x90\x57\x3a\xab\xfb\x17\x25\xc8\x6b\x75\xe6\ -\xc9\xe7\x04\xa0\xb8\x6b\xac\x60\x05\x08\xfb\x01\xef\xd9\x98\x70\ -\x81\xca\x4b\xaf\xa1\xa4\xda\x00\xad\xec\x7f\x41\x0e\x87\x4b\xff\ -\xf1\x11\x15\x41\x2a\xc0\xd7\x7f\x98\x9e\x43\x7a\x95\x0b\x20\xc2\ -\x07\xde\x97\xce\xe8\x91\xf0\x39\x01\x8a\xba\xad\xe1\x7d\x49\x80\ -\xe9\x03\xf7\xa3\x4d\xa7\xc9\xac\x04\x7f\x7e\xc7\xf7\x3b\x88\xf6\ -\x3d\x34\x92\xd7\xc2\xfa\xb6\xe3\xb1\xdb\xbe\x68\xec\xeb\x1e\x8e\ -\x93\x4e\x25\x09\xe7\x1c\x8b\xc2\x96\x96\x3d\xd8\x6b\x3e\x24\x58\ -\x48\x05\xf8\xfc\x87\x29\x4f\xc8\xe9\x69\x25\x21\x00\xe5\x01\x64\ -\x97\x4e\xef\x95\x54\x63\x3d\xa0\xdf\xc4\xe0\xcf\x85\xef\x6d\x1b\ -\xf0\x68\xd3\xf2\xe7\xb8\xfc\xd7\x60\x36\x8c\xb4\xef\x54\xdc\xaf\ -\xe6\x32\x5c\xe1\x9e\xd6\x1e\xd8\x65\xf8\x45\x64\xd6\x3d\xc1\x4e\ -\x07\x9e\xa6\x39\xdd\x42\x64\x71\xbc\xf2\x11\x3b\xad\xb9\x86\x5c\ -\x05\x25\x51\x8b\x44\x2a\xc0\xe7\x3f\x54\x2b\x1a\x90\x85\x71\x15\ -\x01\x44\x25\x01\xf3\xb6\x6c\x6a\xaf\xa7\xd5\x09\x20\x1c\x32\x34\ -\xbb\xa6\xf0\xdf\x36\x0b\x28\xfb\x73\xc9\xf3\x64\x2a\xc0\xba\xc5\ -\xf1\x58\xaf\xe5\x50\x7a\xa1\xa7\xff\xed\xb8\x03\xe1\x9c\x22\x47\ -\x11\xcf\xb7\xbb\x82\xa3\x06\xdd\x10\x02\x09\x9f\xc2\xac\x4f\x10\ -\xd8\x9c\xc9\x8e\x1b\x72\xb2\x18\x6d\x3c\x62\xb1\xdd\xa2\x13\xa8\ -\xd0\xb0\x29\xfd\xa6\x77\x84\x66\x75\x52\x00\x71\x30\x46\xdf\xa3\ -\x42\xf3\x85\xeb\xb4\xa1\x02\xe8\x6b\x57\x23\x00\x21\x97\xc7\x64\ -\x09\xa6\xf4\x7e\x5e\x9d\x04\x39\x2d\x2f\x3d\xab\x4e\x80\xdb\xfd\ -\xa3\xfd\x69\xf8\x94\x51\x2e\x97\xb1\xe9\xcc\xb7\xf4\x42\x31\xff\ -\xc5\x7b\xec\x4a\x08\x57\x97\x55\x47\x9f\x8e\x01\xb8\xbc\x9f\x3f\ -\x1f\xd6\xc6\x0b\x68\xf8\xf2\x6b\x13\x0b\x7a\xce\x7b\x19\x36\x7b\ -\xd2\x8b\x0f\x33\xc6\xc4\xb0\xa5\x0d\x5f\xdc\x2d\xdd\xb5\x4e\xd6\ -\x01\xc8\x6b\x3e\x41\x20\xd1\xcf\xbe\x91\x50\xef\x07\x5d\xab\x19\ -\xbd\x8e\x7a\xfd\xea\x05\x10\x49\xf0\x80\xc9\x14\x4c\xb6\x4b\xa9\ -\x2a\x40\x61\x97\x3f\xfc\xaa\x86\x9f\xa5\x17\x50\xb4\x79\xe9\xf3\ -\x74\x1a\xfe\x8a\x25\x2f\x22\xf5\xe6\x95\x60\xb3\x05\x02\x64\x38\ -\x72\xb1\xdf\xf8\xbe\x5a\xd3\xe2\x9e\xf0\x44\x51\x46\x19\xed\x0d\ -\xc7\xe0\x8d\x8e\xf7\xf1\xa8\x53\x5c\x21\x09\xbe\x50\x6f\xd5\xcb\ -\xc7\xe3\xa6\xa7\x3e\x5a\x31\xb3\x50\xb0\xe6\x57\xc4\xd9\xe3\x12\ -\xb0\x6d\x2b\x17\xfa\x8d\xf7\xea\x6c\x33\x90\xbc\x38\x84\xbd\xa2\ -\xe0\x3b\x8f\xc3\xb6\xee\xbe\xa8\xda\x67\x12\x82\xbc\xe8\x59\x96\ -\x46\x58\x49\x68\xfc\x9d\xaf\x59\x4f\xd4\x03\xc8\x21\x2d\x00\x9f\ -\x9a\x25\x20\x8f\x83\x74\xc1\x64\xfb\x57\x92\x02\x94\x3a\x4d\x79\ -\x58\x55\x80\x8b\x23\x1e\xf1\x68\xf8\x6b\x97\xa7\x3c\x36\x58\x50\ -\x56\xd8\x6c\x81\x10\x29\x5c\x65\x6d\x7a\x31\xcb\x2f\xbc\x17\x5a\ -\xe2\xad\xa0\xb5\x78\x86\xe1\xa0\xa5\x76\x2f\x5c\x6a\x73\x08\x6f\ -\x8c\x79\x83\xde\xe3\x3f\xe2\x61\xc7\xa0\xd7\xc3\x17\xbd\x09\x5a\ -\x31\xb7\x38\x83\x86\xbe\x62\x56\x2e\x4e\x1e\xf5\x37\xea\x36\xe9\ -\xc8\xde\xf9\x2f\x69\x93\xb0\x4e\x0a\x40\x5e\xb2\x84\xd3\x20\xab\ -\x88\x30\xee\x10\xca\xac\x7d\x54\x36\x80\x14\x9a\xfd\x22\x05\xa8\ -\xb5\xe8\x14\x32\xdd\x5d\x59\x11\x4a\x08\xe7\x09\xd3\xe8\x5d\xf2\ -\x9d\xae\x5d\x4a\x3f\xc0\xf4\x3b\x50\x52\xa3\x00\x14\x6f\x26\x4d\ -\x30\xc1\xfe\xf5\xa7\xfe\x80\xa1\xe9\x92\xe1\xa7\xb5\x78\xf8\x7e\ -\xd3\xb2\xe7\xb9\x1b\x96\xbd\x78\x66\xbc\xb0\xec\x3d\x1b\x3e\x45\ -\xae\x91\x05\xbd\xd0\xe2\x1a\x46\x1c\x97\x10\xa2\xe8\x7b\xd0\x53\ -\x6b\x8d\x6e\x96\xeb\xf1\xd0\xc0\x20\x3c\xe4\x74\x1d\x8f\xd9\x6d\ -\x29\xe2\x39\x4c\x49\xf5\x1b\xec\x70\x6f\xd1\x8c\x5d\x89\xdb\xb6\ -\x79\xe7\x78\x79\x46\xe0\x86\xf5\x37\xb1\xad\xb9\x2d\x8a\x47\x29\ -\xb7\x10\xfa\x10\xb8\x75\xb2\x23\x48\x3c\x18\x13\x00\x8d\x8d\x11\ -\x56\xc5\x22\x78\x21\xb6\xba\xfc\x21\x82\x0a\xc0\x62\x71\x2c\x86\ -\x2f\x33\xfd\x28\xc2\x5c\x62\xbb\xbd\x0c\x82\x2e\x31\x9e\x11\x59\ -\x9f\x25\x6e\x0f\x53\x21\x9a\xfe\x87\xe3\xfc\x54\x2a\x4c\xbc\x04\ -\x85\x9f\x15\x40\xd4\x3a\x60\x5e\x09\x26\x3a\xa6\xb1\x12\xbc\x37\ -\xba\xf6\x8a\x15\xe0\xf8\xc4\x78\xde\x1f\xcb\x5e\xa4\x9b\x2c\x2c\ -\x49\x93\x0c\x9f\xa2\xa0\xe7\x40\x2f\x36\x5d\x7c\x4d\x6d\xc2\x6f\ -\x84\x60\xd1\x68\xa3\x0a\xe0\xf8\xe6\x4d\x71\x6f\x47\x07\xbc\xd5\ -\xdd\x09\x53\xed\x74\x05\xe8\x20\x87\x94\x8f\x4e\xb2\x61\x27\xc6\ -\x37\xbb\xe3\xe6\xba\x9b\xef\xe6\xe6\x85\xe3\xc7\xff\x89\xfd\xfa\ -\xb9\xa2\xac\xac\x3c\xfd\x41\x7e\xb4\x0e\x53\xa7\x7b\x02\xc5\x95\ -\x30\x3e\x74\x18\x85\xb0\xbb\x40\x14\x3e\x78\x0a\x72\xfb\x45\x61\ -\xb1\xa4\x00\x94\xbe\x61\xfc\x22\xcd\x75\xa7\xb2\x80\x37\x49\x08\ -\x3b\xda\x22\x6c\xe1\x22\x4c\x21\x12\x58\x13\xd4\x2a\xc6\xe6\xe9\ -\x64\x8d\xf5\xb4\xb8\xa5\x77\x04\xc1\x84\x30\x9e\xb0\x9b\x70\x99\ -\xb0\x96\xe0\x2c\xee\x48\xd1\x20\xf8\xb3\x03\x41\xbc\xfd\x5f\x08\ -\xbf\x42\x02\xce\x4b\xc1\x38\xc7\x0c\x51\x3d\xa0\xd3\x36\x7f\x1a\ -\xfe\x8b\xd6\x81\x69\xa4\xe8\xcf\x6c\xbf\x20\xef\x65\xd5\xf0\xb5\ -\xa7\x24\xa3\x7c\x93\xce\xf4\x1a\x0b\x09\xf7\x69\xfd\xa6\x21\xa9\ -\x73\xb8\x3a\x01\xde\xdb\x4c\xc4\x3b\x08\xc8\x06\xce\x22\x70\x90\ -\x7b\xee\x33\x9a\x1b\xba\x6a\x7c\xdb\xc8\xa9\x6e\x1e\x48\xc3\xb7\ -\xb3\x73\x43\x15\x15\x51\x17\x33\xed\xfc\x99\xf0\x23\x2b\xc8\xff\ -\x9f\xdd\xb0\x7e\xa2\xf0\x49\xf0\xdc\xbd\x25\x02\xcd\x3d\xa5\xf1\ -\x1d\x0e\x09\xaf\xad\x7f\x9f\xeb\xb3\x29\x37\xc7\xe7\x4f\xc2\x8e\ -\xdc\xf7\xbc\x5d\xf9\xd9\xbe\xfb\x3e\x64\xfb\xed\xfd\xf0\xd6\xd7\ -\x32\xfa\xde\x35\x48\x9c\x2d\x80\x08\xd7\x7c\xf0\x34\x7f\x0f\x1e\ -\x1c\xf2\xfd\x24\xc4\xd5\x84\x11\x04\x53\x82\xbc\x28\xd4\xec\xea\ -\x26\x70\x70\xb9\x5c\x54\x56\x56\xc3\x46\x8d\x9a\xa2\x9e\x5e\x6b\ -\x6c\xdd\xba\x03\x5a\x5a\xf5\xc1\x3f\x16\xb7\xfd\x3a\x01\x44\x8f\ -\x03\xce\x0b\xc1\x78\xa7\xcc\x32\x87\xe9\xa2\x8a\xe0\x81\xe9\x09\ -\xbc\x8e\xb3\x52\xd2\xd9\xd0\x75\xa6\xa5\xa2\x7a\xcf\x6d\x28\xa7\ -\xd5\xa1\xe2\xba\xea\xca\x80\x13\x1c\x00\x6f\x6e\x00\x2c\xba\x01\ -\x58\x7a\xbb\x9c\x9c\xd3\xf0\x4c\x22\xfc\xdc\xb8\x61\x32\xbc\xfd\ -\xae\x50\x30\x63\x84\xc3\x1b\x1a\xbc\xb3\xf3\x32\xd4\xd2\x6a\xc1\ -\x36\xeb\xdc\x68\x5d\xe9\xa7\x18\x0b\x20\x2f\x17\xfa\xc1\x18\x8d\ -\x3a\x8a\xbd\x97\x3d\x2f\x1a\xbc\x15\x4b\x09\x48\x19\xb9\x4f\x18\ -\x71\xa4\xe0\x4d\xe6\x19\x61\x06\xb2\x1c\xe3\xbf\x2e\xe8\x16\x1e\ -\x1c\x6b\x19\x1e\x8a\xc6\x31\xd7\x78\x90\x38\x0b\x45\x3c\x99\x91\ -\x0a\x67\xdb\xc5\x81\x17\x23\x14\x89\x40\xd9\x07\x28\xb7\x82\x8b\ -\x4d\x5c\x95\xb1\xfd\xb4\x26\xd8\x6b\xbd\xbe\x70\xd8\x49\xb3\xd2\ -\x95\x07\x37\xe1\xc9\x93\x8f\xfe\xc1\xdc\xdf\xef\x65\x8d\x5b\x1e\ -\x86\x2b\x96\x8e\xc5\xb7\xf7\x81\xff\x35\x12\xe4\xde\xe7\x3c\x13\ -\x8c\x1d\x18\x93\xd0\x36\x24\xa9\xcf\xbc\x77\x91\xda\x0b\x32\x51\ -\x65\xfa\x29\x94\xd7\xed\x4e\x1e\x4f\xe5\x13\x44\x54\x95\x00\x5d\ -\xec\x00\xaf\xae\x03\x2c\xbc\xfe\x29\xf4\x4a\x9c\x67\x82\x49\xf0\ -\x65\x59\x03\x65\x7d\x8f\x4d\x81\xec\x3d\x93\x98\x82\x09\x23\x26\ -\x08\xc6\x8c\xd9\x8c\x2d\x5b\xd2\x92\x83\xa1\x2d\x22\x77\x5a\x62\ -\xfd\x54\x83\x41\xf4\x99\x4d\x6b\xae\x8a\x6a\xba\x68\xbf\xf4\x39\ -\xb2\xe1\xb3\x0c\xdc\x54\xf4\x61\xd1\x9d\xd8\x34\x1a\xfe\xb6\x94\ -\x48\x61\xdb\x2b\x17\x91\x86\xcf\xa2\x7b\x6f\x47\xbc\x6c\xc2\xf4\ -\x97\xca\x4f\x5c\x1f\x37\x79\xe2\x12\x6d\x1e\xda\x27\xca\xfa\xa6\ -\xee\x8b\x7e\xde\x46\x85\xa3\x03\xcd\xd0\x35\xcc\x12\xdd\xc2\x2b\ -\xe3\x1a\x6a\xc5\xdf\x72\xce\x2b\x53\x32\xfc\xcd\x7b\xc2\x22\x68\ -\xf8\x2c\x8b\xd7\x79\x66\xbd\x7b\x20\x93\xfe\x45\x01\xfc\x98\x92\ -\xfc\x20\xe5\x80\x81\xee\x81\x17\x65\x02\x62\xa3\x21\x2a\x0a\x99\ -\x07\x0f\xb0\x37\xf9\xf8\xfa\xc8\x00\x2e\xe9\x08\x78\x71\x26\xe0\ -\xf5\x79\x80\xb7\x17\x41\x49\xc4\x06\x78\x99\xe1\x0e\x21\xef\x0f\ -\xc1\x43\x42\x60\xee\x61\xc2\x51\x08\xcc\xda\xc8\x5c\xb9\x34\x81\ -\x49\xf2\x72\x03\x5c\x37\x4a\xb6\x64\xdc\xc8\x05\x68\x65\x35\x00\ -\x65\x64\x2a\x9e\xf3\xed\x7e\xda\xd1\x40\xf1\xb3\xf8\x8d\xa2\x5a\ -\x33\x34\xe8\x3a\x0b\x5b\x74\x5f\x80\xc6\x76\x2b\xb1\x8d\xe3\x06\ -\x34\x1d\xb0\x0d\xcd\x9d\xf7\xa2\xdd\xcc\x95\xa8\x37\xd5\x15\x35\ -\x47\x0c\x47\x55\x5b\x1b\xd4\xed\x6d\x8c\x63\x3d\xb4\x71\xe6\x05\ -\x4d\x74\xbb\xa8\xfd\xe1\xb7\xc0\x16\xb1\xf3\x1e\x99\x06\xcd\x8c\ -\x6a\x17\x33\x2d\xd2\xe2\xc5\xd4\x70\xcb\xdc\xaa\xc1\x57\xa6\x7d\ -\xf1\xae\xbf\x8f\x24\xd0\xf0\xbd\x0e\xc5\x04\x8f\x5f\xf1\x29\x7c\ -\x96\x99\x6b\xee\xe4\xbf\xba\xab\x11\x47\x42\x2e\xcc\x8b\x90\x4b\ -\xfe\x98\xa4\x14\x5c\x98\xde\x80\x57\x9c\xaf\xe5\xc7\x2f\xd1\x89\ -\x2c\x15\xea\xbe\x2e\xc5\x66\x82\x37\x45\xda\x41\xbd\xa2\xa6\x05\ -\x40\x4c\x0c\x52\x34\x3d\x3c\x8a\x4e\x93\x8f\xef\x73\x5c\x56\x81\ -\x17\x7e\x66\xc0\x8b\x71\x84\xb0\x27\x83\xa1\xd0\xcf\x19\x42\x68\ -\xf8\xf3\x07\xab\x62\x4f\xdb\x91\xa8\xa4\xa4\xce\xce\x2c\x1a\xf9\ -\xaf\x18\x0e\xa6\x03\x16\x6c\x53\x8c\xe0\x4d\x7b\xbd\x24\xfe\xcc\ -\x7e\x18\x67\xc5\x1d\x23\xb4\x9d\xfc\x17\xc3\x40\x5e\x37\x67\x40\ -\xaf\x28\xc0\xfb\xf8\x4f\xee\x0a\x99\x92\xbf\x8b\xe5\xd3\x4f\x17\ -\x28\x3f\x39\xf4\xae\x41\xd8\xee\xf4\x26\xfe\x1b\x53\x9a\xf3\x56\ -\x26\x19\xf1\x16\xc6\xb5\x09\x98\x1b\x63\x11\xb4\xfb\xe2\x5f\xb7\ -\x7f\x59\x12\x14\x3a\x70\x61\x40\x80\xdd\x5c\x3f\x5e\x97\xd9\xde\ -\x3c\xd3\xe9\xb7\x02\xf4\xa7\x5c\x0f\x6f\x38\xe1\x52\x62\x83\x91\ -\x57\xef\xbe\x2b\x68\xf5\x88\x04\x8d\x35\x90\xbb\x3b\x86\x9b\xb5\ -\x3e\x8c\xc1\x7a\x31\xbe\x31\x54\x80\x11\xae\xae\xe1\x55\x03\x3f\ -\xcb\x85\x8f\x77\x74\x21\x24\xcc\x06\x78\xf1\x83\x20\x85\x84\x8e\ -\x92\x78\xf6\x02\x81\x4b\x2f\x0d\x6c\xa4\xa9\xc7\x36\xeb\x68\x25\ -\x56\xe9\x5f\x35\x1f\x40\xdc\x2c\x72\xa8\xd2\xf7\xdd\x91\x36\xd3\ -\x6a\xf8\xfa\x06\x74\x84\x8d\x36\xe1\xac\x07\x01\x7a\x44\x54\x2f\ -\xc2\x03\x84\xe2\x50\x52\x53\x48\x44\x78\xf8\x06\xc1\xa7\x10\x21\ -\xb8\x90\x2f\x9f\xb0\xe0\xfc\x6a\x1f\x9d\x8d\xf7\x6f\xc2\xc8\xd3\ -\x58\x2d\xc3\xcf\x47\x42\xb7\x50\x81\xae\xf3\xcd\xcc\x62\xa1\x5e\ -\x56\x75\x02\xf8\xa4\xa9\xf9\x6f\x08\x03\xa4\xcc\x0a\x33\x7e\x4e\ -\x04\x28\x76\xd7\xd4\xcc\xa5\xa1\x5f\x52\x81\x67\xfe\xe6\xf0\x20\ -\xd6\x11\x42\xe9\x5d\x4e\xe0\xc7\x3b\x43\x46\xf4\x00\x48\xf2\xb5\ -\x87\xe4\xd3\x5d\xe1\xfd\x36\x0b\xf2\x7d\x2d\x01\xad\x1a\x71\x59\ -\xd1\xaf\xd4\x96\x59\x44\x75\x69\x30\xa7\x05\xe1\x22\xfd\x00\x3b\ -\xf7\x07\xdc\x43\x2a\x5a\x97\x7d\x00\x03\x23\xa0\xa4\xb0\x98\x8e\ -\xc0\x41\x99\xe4\xb7\xf0\x12\xad\xa3\x7a\x6c\xbd\x9a\x62\xbd\xf1\ -\x56\x1e\x73\x3e\x39\xa5\x7a\x01\xce\x64\x82\x6d\xd0\x1b\xe8\x1a\ -\x86\x14\xfb\x79\xfb\x63\x49\xe0\x7c\xc9\xf0\xf3\x4a\x75\x22\xd9\ -\xf0\x59\xfa\xde\x72\xbb\x7a\x51\x1b\x6e\xdd\xb4\x82\xbb\xb7\xac\ -\x21\xf0\x82\x15\x3c\x3a\x6a\x06\x19\x07\xdb\x40\xd1\x01\x63\xc0\ -\xfd\x84\xe5\xcd\x01\xfb\x37\x00\xd4\x95\xaf\xd4\x32\xa1\x4d\xd1\ -\x3e\xd2\x19\x41\xff\x9d\x08\xd6\x84\x4b\xea\xea\xc0\x0f\x0a\xfa\ -\xe7\x97\xe4\x16\xd6\xcf\x99\x74\x64\x4f\x40\xd7\x4d\x37\x91\x62\ -\xbe\xd7\x2f\x1c\xee\x65\xd0\xb0\x73\xab\x08\x50\x06\xbd\x7d\xa2\ -\xd9\xf0\x59\x36\x9d\x9a\xe5\x2b\x21\x40\xa1\xc7\x23\xd9\x97\x6c\ -\xf0\x0b\xbc\x8c\xe2\x06\x19\xac\x0c\x18\x2c\xbf\xde\x9f\x06\x2d\ -\x89\x3b\xb9\xc3\x67\xe9\x00\xda\xa8\x92\x16\x81\x4c\xa5\xd0\xe9\ -\x80\xcd\x49\x3a\xe8\x25\x9d\x12\xf6\x7d\x45\x68\xa4\xac\x0c\xaf\ -\xee\xdd\xfb\xf4\xd7\x67\x43\x07\x05\xd9\x6c\xbe\xfe\x96\x0d\x9f\ -\xd2\xe0\xdc\x93\xd7\x22\x01\x26\x9c\x8f\xaa\x24\x40\xff\x9b\x3e\ -\x55\xc3\x67\x09\x4b\xec\xee\x4f\x05\x08\x7b\xdb\x80\xb7\x3e\x98\ -\x53\x3a\x7d\x6e\xaf\x87\x7d\x95\xff\x7c\xd4\x07\xf6\x21\xc5\x41\ -\x66\x51\x1a\x0d\x7d\xab\x21\x69\xfa\x35\x06\x34\x27\xed\x7e\x59\ -\xa6\x22\xf0\x5c\x82\x0f\x61\x1b\x61\x6c\x6d\x9f\x35\x5c\xe7\x67\ -\xf8\xc8\x93\x22\x76\xa7\x97\xa6\x70\xf0\x9e\xc3\x51\x92\xc1\x53\ -\xac\x37\xdf\xca\x65\xee\xa6\xa3\x48\x80\x05\x77\x78\x9f\x9e\xfb\ -\x17\x42\x49\xd0\xc2\x9a\x04\x90\xef\x11\x58\x1c\xff\xdc\xe2\xbe\ -\x4b\xdf\xf1\x3e\xf6\x9c\x3d\x19\x6c\xf0\x2c\xa6\xdc\x5e\xa8\xa7\ -\x00\xc8\x94\x07\xfe\x4c\xdc\xfb\x48\xbb\xb9\x0d\x7e\xf4\xb0\xb6\ -\x54\x80\xca\x02\x4c\x94\x51\x50\xc4\xf6\x73\xfe\xc4\xaa\xe1\x53\ -\x4c\x3c\x02\x9e\x88\xc2\xa7\x6c\x0f\x7d\x28\x16\x20\x0d\x6c\x42\ -\xde\x57\x13\xfc\x47\x85\x2e\x01\x61\x7a\xed\x2f\x3d\xe8\xd9\x6a\ -\x5b\xdc\xd0\xa6\xab\x5f\xf6\x01\xf7\x5c\xc9\xe0\x7b\xc3\x4e\xd4\ -\x02\x4b\xf6\x4e\x4f\xa1\xd7\xff\x9e\x03\x33\x52\x01\xbe\x5d\x80\ -\x4d\x1a\x46\x6d\x70\x52\x50\x2a\x0e\x38\x19\x14\x42\x42\xff\x20\ -\x29\x80\xfa\xc5\xa4\xdc\x0a\x01\xce\x3e\x7b\x41\xc2\xe7\x43\x2f\ -\xbf\xc7\xe2\xc0\xf9\xb2\xd6\x41\xd1\x3a\xed\xaf\xde\xef\xda\x7a\ -\x6f\xf8\x30\xfd\x95\x45\x23\xf4\x97\xa3\x24\x83\x1a\x6c\x08\x92\ -\x14\xa0\x09\x54\x0c\xc7\x7a\xd0\x29\x66\xd2\xa5\x61\xff\x7b\x01\ -\x22\xf4\x7b\xf5\x17\x09\x40\x19\x73\x3f\xf9\x79\xb7\xcd\xb7\x12\ -\xc5\xc5\x7f\x36\x73\x4f\x5c\xfc\x53\xee\x66\x94\xc9\x0d\xb8\x7a\ -\xb9\x91\xd5\xad\x7b\x1d\x4c\x3c\x03\x9c\x0d\x57\xbf\xaf\x1a\x78\ -\x75\x38\xca\xef\xf0\xa3\xe1\x77\x83\x35\xa4\xc8\x17\x75\xfb\xae\ -\x94\x2e\x0f\xaf\x25\x6b\xf6\x09\x65\x6d\x27\xcc\xae\x10\x80\x32\ -\xf1\xe1\xab\xa2\xde\x1e\xbe\xbe\x66\xfb\x7c\x83\xc8\xf3\x3f\x53\ -\xf5\x68\x9c\xaf\xd9\xf4\x4b\xbc\x41\x6d\xff\x48\xfd\x9a\xc0\xab\ -\x32\x5c\x6f\x45\xa1\x0d\xb3\xb6\xb4\x29\xd8\xb0\x13\x31\xe4\xa4\ -\x02\xd4\x0e\x01\x3a\xd0\xe2\xd8\x76\xd5\x8e\x4a\x02\x50\x5c\x1e\ -\xa6\xc4\x1b\xfd\x7a\xe1\x82\x8b\xe9\x2a\xff\xb1\x6d\x56\xf8\x8d\ -\x36\x5e\xc6\x1b\x65\x44\x59\xee\x3f\xd2\x60\x45\xe8\x2f\x06\xcb\ -\x1f\x8f\xd0\x5f\xf6\x92\x04\x9c\xf7\x35\x12\x74\x6b\xe0\x82\x1c\ -\x90\xa5\x17\x5d\x22\xdd\x20\xa2\xf6\x08\xe0\x4a\x05\xe8\x7f\xe0\ -\x8a\x64\xf8\x02\xbb\x7b\xb1\x3e\x9c\xc3\xe7\xf8\x30\xe7\x52\x54\ -\xc3\xf1\x27\x92\x27\x59\xae\x4c\x99\xd2\x7e\x05\x7e\x06\xfe\xa4\ -\x76\x2b\x32\x27\xb6\x5d\x91\x34\xc1\x74\x79\xd4\x78\x93\x15\x81\ -\x63\x5b\x2f\xf7\x1d\x63\xbc\xc2\x67\xa4\xd1\x72\x1f\x6b\xad\xd1\ -\x69\xb2\x1c\x79\xb6\xeb\xb6\xa1\x54\x80\xda\x23\x00\x9d\x40\x8a\ -\x63\x6e\xc7\x8a\xc2\x9f\x10\xf8\xea\x75\xd3\x73\x77\x62\xe0\xd0\ -\x59\x14\x31\xfb\x72\x02\xcc\xb8\x8c\x72\x6e\x17\x72\x47\x76\x59\ -\x13\xfa\x05\x09\x24\x29\x1d\xd5\x6e\x75\x58\x3b\x83\x9d\x01\xb2\ -\x8d\x8f\x15\x80\xa2\x93\x50\x5c\xf1\x3b\x26\xdd\x22\xa6\x76\x09\ -\xb0\x55\xbe\xbe\x9a\x28\x7c\xe7\x07\x4f\x82\xe4\x8e\x5e\xc8\xab\ -\x08\x9f\x32\xe3\xca\x6b\x2a\x40\x39\x97\x84\xbd\xfb\x6c\xf7\x21\ -\xe1\x0a\x6b\x08\x5d\x30\xb6\xed\xef\x51\x1d\x8c\xb6\xfb\x29\x68\ -\x1d\xcd\x06\xcd\xe3\x58\x81\x7c\x45\xb3\xcf\x46\x2a\x40\xed\x12\ -\x60\x87\x9a\xb1\x29\x1a\x6e\xf2\xc0\x4a\xc1\xb3\x4c\xbf\x94\xff\ -\x49\x80\x72\x8c\x46\x1c\xa2\x25\x41\x2e\x1b\xfc\xb8\x76\xab\x1e\ -\x5b\x1b\x6f\xe5\x29\x69\x1d\xce\xac\x14\xba\x24\x32\xba\xec\x28\ -\x25\x23\x15\xa0\xf6\x84\xdf\x5d\xb4\x79\x53\x27\x13\x04\xa7\x65\ -\x08\xbb\x8f\xa5\x57\x0a\xff\xe0\xd9\xb2\xaa\xe1\xb3\x68\x8c\x39\ -\xf1\xd0\xc6\x78\xab\x8f\x6a\x93\xc3\xaf\x6a\x0c\x5d\x12\x46\x81\ -\x5e\x70\xa7\x74\x97\xb0\xda\x13\x3e\x9d\xdf\xff\x1e\xe4\x49\xad\ -\x7c\x8e\x1d\x42\xa7\xf9\xaf\xc1\xca\x4b\x00\x53\x8f\xf9\xc3\x81\ -\x33\xaf\x45\x02\x78\x9d\xcb\xae\x14\xfc\xb4\xcb\x2f\x60\xe0\x79\ -\x1e\x18\x9d\x7c\xf6\x55\xa1\xb3\x34\x70\x67\x8b\xff\x61\x52\x01\ -\x6a\x8f\x00\x83\x45\xa1\x74\x32\x40\x58\xe0\x88\x30\x75\x54\x12\ -\x58\x7a\xa1\x88\x8e\x5e\x25\xf0\xdb\x5f\x3c\xd8\x75\x2e\x86\x3c\ -\x02\xd2\x60\x18\x09\xbd\xcd\xc9\x27\xdf\x14\xba\x24\xf5\x67\xb0\ -\xa3\x79\x9a\x52\x01\x6a\x8f\x00\xbb\x80\xcb\x21\xcf\xf8\x9e\xe5\ -\x02\x50\x3a\xed\x8e\xae\x90\x80\xd2\xc1\x33\x48\xad\xe1\xb6\x6b\ -\x8a\x1a\xfb\x83\x48\x90\x1f\xfe\x63\x01\x64\x8d\xe9\x05\x4f\x4a\ -\x77\x0a\xad\x5d\x02\x3c\x06\xb3\xa6\x9f\xc2\xa7\x8c\x9e\x11\x54\ -\x49\x00\xf3\x6d\xbe\xd0\x72\x0d\xea\x68\x2e\x78\x68\xcd\x4c\xcb\ -\x32\x52\x5c\x19\x5c\x4f\xdd\x33\x80\x84\x9a\xfb\xd5\xe1\xab\x6f\ -\x62\x8b\xff\x8e\x52\x01\x6a\x59\xef\x1f\x8c\x53\x25\xcf\xff\x9e\ -\xf9\x9f\x24\xe8\x5b\x46\x82\x4f\xab\x10\xc0\x78\xc3\x0b\x2a\x00\ -\x45\x51\x7f\x55\x6a\x17\x8e\xdb\x23\x1b\x98\x82\xdd\xc0\x95\xdf\ -\x4a\x61\x69\xb8\x8a\xba\xbb\x1f\x09\xf9\xed\x67\x05\x50\xb4\xa3\ -\x17\x8c\x97\xee\x15\x5c\xbb\x04\x38\x06\xaa\x44\x00\x4f\x82\xbb\ -\xcc\x47\x58\xd6\x2d\xa0\x42\x82\x7e\x2b\x7c\xca\x05\xf0\x7c\xc5\ -\x86\xcf\xc2\x18\xad\x29\x6d\xad\x3c\xdb\x87\x48\x20\xa0\x22\x88\ -\x11\x98\xc8\x2d\x89\x56\x53\xdb\xc3\x63\x34\x8f\xa5\x55\x0a\xbf\ -\xe1\x41\x52\xfb\x57\xa4\x17\x9c\x23\x15\xa0\xf6\x84\xaf\x44\xf8\ -\x08\xb6\xe2\x05\x21\x2c\x9b\xf4\x1e\x12\x01\xf2\xe0\xd7\x41\xd9\ -\x44\x80\x22\xb0\xd8\xe3\x5b\x55\x00\x16\x0d\x9d\x25\xd1\xa4\x14\ -\x48\x97\x90\xa0\x02\x33\xd9\x05\x71\xea\xaa\xbb\x78\x8c\xc6\x4e\ -\x3e\xa8\x4c\xa2\x17\x2c\x20\xa8\x49\x05\xa8\x6d\xb5\xff\x39\x55\ -\x04\xa0\xec\xad\x97\x0a\x8b\xed\x62\xa1\xc7\x66\x7f\x30\xd9\x12\ -\x52\x93\x00\x14\x19\xc3\xd5\xef\x3b\xca\x4e\x0d\xae\x4e\x02\x16\ -\x65\x68\x8c\x3f\xcb\x5e\xc0\x3f\x93\x00\x5d\x45\x6b\x08\xa6\x56\ -\x23\x00\xc5\x93\x29\xe3\xce\xed\x7d\x9d\xd3\x6a\xed\xeb\xcf\x09\ -\xc0\xa2\xaf\x31\xd7\x8f\x84\x5d\x58\x35\xfc\x8e\x30\x0a\x45\x25\ -\xcd\x4f\xdc\xf4\xab\xcb\x75\x80\x67\xd0\xaa\x5a\x01\x3e\x5a\x9c\ -\x52\xe1\x9d\x08\x36\x7f\xf7\x21\x5d\xe1\x65\x7c\x58\xc3\x94\xcd\ -\x3b\xba\x05\xd8\x8e\x9c\xc8\x53\x6d\xbf\xec\x11\x09\xbc\xb8\x3a\ -\x09\x94\x9b\x2f\x7f\x6a\xcd\xb8\x26\x49\x0a\x60\x00\x9d\xbf\xeb\ -\x4e\x5f\x52\x01\xbe\xaf\x00\x0f\x44\x8f\x81\xf5\x50\x26\x0e\xbe\ -\xc8\xfc\xb4\x0a\xef\x78\x90\xf9\x9b\x1b\xe1\x96\x48\x89\x4f\x6a\ -\xea\x8b\x6f\xc8\x97\x4b\xc0\xcf\xe0\xf2\x7d\xef\x36\x8f\x9f\xb7\ -\xca\xc1\xd7\xbc\xef\xf4\x00\x79\xd3\x55\xcf\x88\x00\x42\x2a\x01\ -\xc7\x68\x75\x91\xb9\xe2\x4c\x5f\x56\x80\xfa\xe5\xc5\xbf\x41\x0d\ -\x13\x50\xe8\x32\x73\x2d\xf1\x76\x33\x2d\xc4\x4b\xde\xe9\x6a\xa7\ -\x5e\x84\xe1\xe2\xbd\x0b\x56\x8a\x17\x7e\x3c\x15\xcf\x0e\x3e\x21\ -\xde\x0d\x65\xad\x78\x51\x0c\x5d\xc2\x3e\x44\xbc\xd1\x83\xd5\xff\ -\x7a\x8c\xa1\x2e\x85\x6f\x4a\x67\x00\x81\x31\x09\x7e\x3b\x60\xeb\ -\x03\x4a\xe1\x47\x03\xcd\x32\xd9\xe0\x59\xbc\xa3\xcc\x43\xaa\x0a\ -\x50\x1d\x39\xcf\x14\xf2\xce\x9d\x6d\x13\x31\x76\xf6\x50\x9f\x66\ -\x36\xf3\x42\x74\x1a\x8f\xce\x6d\xcf\x0c\xa3\x17\x12\x8a\x17\x70\ -\x44\x88\x43\x7c\x2b\xee\x0d\xc4\xaf\x45\x56\x59\x09\x1b\xb4\x6f\ -\x8b\x6d\x46\x4f\x47\xdb\xa1\xd3\xd0\xa2\xa7\x33\x1a\xb6\xed\x8a\ -\x8d\x74\x5b\xa0\x82\x52\xfd\xaa\x5f\xef\x4b\xd7\x4d\x4a\x05\xf8\ -\x7c\xf8\xa2\x0d\x1e\xb8\x26\x0c\xea\x2d\x55\x44\xf7\x7b\x6d\xf2\ -\xab\x06\x2f\xc1\x07\x41\x16\x53\xf2\x35\x12\x88\xc8\x84\x14\x7c\ -\xc6\x04\xf8\xb8\xcb\xf0\x17\x2e\x6b\x8d\x93\xcf\x0d\xc7\x89\x67\ -\x86\xe1\x84\x53\x43\x71\xdc\xf1\x21\xe8\x72\x6c\x30\x8e\x3d\x3c\ -\x08\x47\x1f\x1c\x88\xa3\xf6\x0f\xc0\x5f\x3c\xfa\xe3\x88\x7d\x4e\ -\x38\x7c\x77\x5f\x1c\xba\xd3\x11\x87\x6c\x77\x40\xe7\xad\xf6\xe8\ -\xf0\x7b\x1f\x6c\xbb\x68\x16\xda\xfe\xe5\x89\x8e\xf7\xff\x46\xcd\ -\x33\xe7\x0a\x06\x5c\x0a\x2b\xf6\x0a\x47\xac\xca\xde\xc0\x22\xb4\ -\x58\xf8\x17\x82\xe9\x04\x04\x35\x03\xfa\x0b\x66\xfe\xaf\xb6\x9e\ -\xaf\x2b\x02\xf8\xa9\xb4\x51\xc0\x31\xbb\x9a\xe1\xce\x73\x6d\xf0\ -\x82\x9f\x45\xe9\x67\x04\xc0\x8c\x97\x6a\x91\xd5\x86\x9d\x05\x05\ -\xf8\x9a\x89\xc4\x58\xae\x0f\xde\x96\x0d\xc5\xa3\x0a\xd9\xb8\x5f\ -\x11\xf9\x5e\xf5\x9e\xfd\x7d\xd2\xbc\x6c\xc1\xb9\x01\x25\x8b\xc2\ -\xdd\xf0\x5b\x19\x7b\x73\x59\x92\xc3\x9d\x8b\xd9\x8e\xde\x57\x91\ -\xd2\x34\x24\x24\x12\x22\x9f\xe0\x9a\x88\x92\x77\xd5\x09\xe0\xf2\ -\xe0\x59\x02\x2c\x79\x16\x07\x83\x6e\xbe\x03\x7b\x2f\x04\xc5\x86\ -\xf4\x97\xec\x2b\x15\xa0\xfa\xf0\xe9\xd6\x2f\xd8\x7c\xa5\x16\x76\ -\x7a\x6e\x52\x41\xef\xa7\x26\x6f\x5d\x12\xcd\xa2\x97\xc6\xb5\xf5\ -\xdb\x17\xd3\x8e\x77\x26\xd2\x22\xec\x5a\x84\xe5\x2b\x22\x80\x20\ -\xf4\x91\x11\x8f\x04\x2e\xc4\x0c\x78\x8a\x49\x8c\x3f\x06\xc8\xf8\ -\xe1\x59\xf9\x64\x3c\xa0\x28\xa0\x81\x4b\xf2\xf1\x88\xfa\x43\x1e\ -\xaf\xeb\xc7\x7b\xbe\x36\x69\x8b\xc3\xdd\x84\xdf\x12\xfc\x82\xb0\ -\x99\xb9\x83\x79\x27\xfd\xd9\xe0\x29\x7a\x41\x41\x31\x34\x7c\x42\ -\x91\x67\x35\xe1\x2f\x0e\xcb\x4f\x80\xa0\xd0\x5c\x98\x23\xcc\x81\ -\xd9\xa5\x6f\xc1\xe1\x00\x82\xac\x32\xfd\x45\xb7\x49\x05\xa8\x5e\ -\x00\xba\x32\x18\xdb\xde\x6f\x51\x49\x80\x6a\xe0\xf7\x4e\x6e\x15\ -\xbb\x3c\x58\xcf\xfb\xe9\x85\x06\xa1\x78\x44\x21\xaf\x6a\xd8\x55\ -\xe0\xa7\x9e\xd7\xe3\xdd\xf7\xb5\x41\xca\x59\x7f\x47\xde\xb7\x84\ -\xef\x16\xb8\x26\xcc\xd1\xfb\xef\x0c\xc9\xf0\x0d\x03\x03\xe3\xc4\ -\xe1\xa3\x62\x44\x62\x42\xd5\xf0\x37\x86\x17\xa7\x71\x82\x82\x33\ -\x95\xce\x3e\x8a\xa2\x7d\x8c\x22\xfa\x9e\x10\x02\x47\x86\xfe\xa2\ -\x7b\xa5\x02\x54\x2f\xc0\x19\x86\xcb\xa0\xf9\x9d\xca\x02\x74\x7c\ -\x6e\x52\x6c\x9f\xd8\x2a\x7a\x71\xb0\x9e\x8f\xf7\x95\x86\x91\x45\ -\x87\x94\x3e\x7e\x21\xf0\x4f\x1c\xa8\x97\x19\x75\xd3\x3c\x86\x0d\ -\x9f\xb2\x31\x78\x4c\xfc\xd7\x04\x3f\x2f\x6c\x46\xde\x60\x9f\xe3\ -\x95\xee\x7a\x4a\xcb\x87\x01\x09\x6c\xf8\x14\xfd\x88\x94\x20\xc9\ -\xf0\x77\x86\x97\xe5\xc9\x06\x85\x24\x43\x50\x10\x26\xa4\xf4\x0d\ -\x4b\x7a\xe4\xec\xab\xb7\xfc\x75\x22\x8c\x89\x65\x2b\x83\x9e\x52\ -\x01\xaa\x17\x60\x8f\x68\xc3\xa7\xfa\x5c\xb4\xd8\xa3\x8d\x13\x8f\ -\x68\xe7\xfa\x5f\x6a\x18\x5d\x72\xb0\x5e\xd1\x57\x07\x2e\x01\xff\ -\xa0\x4a\x8c\xbf\x77\xe7\x2c\xc9\xf0\xef\xfa\xd9\xbe\xfc\x9a\xf0\ -\x5d\x83\xd6\x84\x3b\xde\xbf\x92\x5e\x35\xfc\xd6\x81\x7e\xc9\x24\ -\x74\xa1\xa4\x00\x76\x91\x59\x81\x6c\xf8\xee\xe1\x42\x7e\xfd\xa0\ -\xf0\x48\x1a\xbe\x6a\xf0\x83\x22\x4c\xb3\x2c\x26\xa0\x20\xd5\xb2\ -\x70\xc5\xfa\xbe\xa8\xd0\x40\xb4\xdf\x6f\xa9\x78\xc3\x08\x39\xa9\ -\x00\xd5\x6f\x9b\x5e\xd8\xc3\x1a\xf0\xdc\x06\xe6\x4d\xf6\x2e\x05\ -\x7f\x12\x66\xf1\xb7\x86\x9f\x73\xb2\x31\xcf\x9b\xd7\xad\x54\x32\ -\x7c\xca\xe9\xc0\xbe\x3e\x9f\x7d\xd6\x87\xce\xc8\x77\xe6\x1d\xf7\ -\xab\x1a\x3c\xc5\x24\x80\x97\x42\x02\x17\x48\x86\x4f\x99\x19\x51\ -\x10\xc7\x0a\xa0\x13\x1c\x13\x40\xc3\xa7\x2c\x7d\xbc\x26\x84\x86\ -\x2f\xc9\x8b\x60\x33\x1c\xda\x4f\x9d\x2d\x09\x62\xfe\x3f\xf7\x0a\ -\xaa\x4b\xfd\x00\xf7\xb8\x5c\xc0\xb4\x58\x10\x86\x9c\x86\xe0\xe3\ -\x33\x98\xb7\xc9\x9b\x64\x7d\x84\xfb\x15\x33\xbf\x22\xfc\x0f\xc9\ -\x57\x5a\x3e\xac\x1a\x3c\xcb\xfa\x90\x71\x49\x35\x3e\xeb\x83\x7e\ -\x8f\xe8\xeb\x7d\x25\xad\xba\xf0\xcd\xfd\x7d\x52\x49\xd8\xa5\x55\ -\xc3\xa7\x6c\x8f\x28\x2b\xa0\xe1\x5b\x86\x26\xf9\xb0\xe1\x53\x96\ -\x4f\x33\xf6\x7e\xb9\x59\xfb\x9a\xf0\x6e\xab\x20\x0c\x34\xf1\xc3\ -\xc7\xe6\x91\xfc\x64\x8b\x3c\x2a\x82\xcf\x85\x96\x68\xde\x5a\x91\ -\x9d\x85\xb4\xea\x7b\x37\x0d\xc5\xd3\xe9\x74\xea\xaa\x00\x4b\xe9\ -\x1d\x12\x78\xb3\xbc\x49\x97\x74\x1f\x7c\xe9\x86\x4b\x07\xa6\x02\ -\x3f\x68\x95\x4c\x00\xdf\x5d\xe1\x71\x75\xe1\x0b\x0f\xd6\x7b\x11\ -\x72\xa7\x7d\x72\x4d\xe1\xdf\xf5\xb5\x7d\x56\x5d\xf0\xf3\xc3\x67\ -\x14\x38\xf3\xfe\xaa\xf6\xae\xa7\xb4\xbf\xb6\x2b\x9d\x7b\xf7\x98\ -\x3f\x44\xc6\xa7\x55\x0d\x9f\x13\xf1\x24\x83\x86\x3f\x20\xe4\xb5\ -\xbf\x64\xf8\x1a\xb7\xce\xe7\xae\x19\x02\xc2\xdd\x7d\x74\x7c\x22\ -\x5c\x14\xee\x15\x1c\xd6\x7d\x8c\x37\x5a\x22\x45\x78\xc3\x28\xf7\ -\xd5\x09\xc3\x77\x01\xee\x7a\xb8\x75\xa6\x16\xb6\x6c\x2a\x87\xe2\ -\xce\x28\xd3\xef\xf0\xd9\xa9\x11\x22\xc5\x9d\x5c\x74\xc7\x55\xd5\ -\xba\x28\xc0\x1d\x83\xe6\x80\xc2\xac\x4f\xed\xfa\xf4\x10\xf0\xf3\ -\x9a\x0a\x42\x2a\x02\xe5\xfa\x7c\x4e\xec\xfb\x5d\x0a\x0f\x69\x0d\ -\x9f\x86\x5f\x74\x44\x35\x98\xf7\xc0\x3a\xbf\xa6\xf0\x29\xa7\x02\ -\xfa\xfd\xa3\xf8\x9f\x1a\xb4\x2a\x92\xdc\xf5\xaf\x6b\x0a\xbf\xf3\ -\x8d\xfd\x6f\xc0\xab\xef\x47\xf0\x72\x44\xf0\x72\x2a\x83\x4b\xeb\ -\x83\x20\x24\x3c\x9a\x15\x40\x2d\xf2\x69\x98\x5b\xd8\xbb\x08\x12\ -\x7a\xa9\xa4\x00\x43\x56\xf5\x8d\x20\x02\xe0\xba\xc1\x72\xa9\xbb\ -\x7b\x19\xe6\x06\x3b\xcb\x06\x26\xcd\x50\xf4\x2d\xbb\xd4\x22\x97\ -\x15\x81\xa5\xf8\x8a\x11\xf6\xb5\xaa\xd8\x33\x79\xda\x7f\xb9\x49\ -\xe7\xd5\x7a\x5a\xda\x68\x7f\xec\x0a\xfb\x98\x59\x5b\xd7\xea\x00\ -\xb4\xef\x5d\xb8\x6e\x89\x38\xfc\x34\x48\xc3\x27\xc0\xc3\x48\x48\ -\x4c\x3c\x09\x37\xf7\x4f\x85\x52\x56\x02\xca\x89\x59\x4c\x46\xd4\ -\x8e\xfa\x37\xef\xf3\x6c\xde\x7e\x2e\x7c\xca\xba\xb0\x71\xcf\x2b\ -\xee\xfa\xb0\xe9\x1f\x86\xfa\x1e\xf3\x25\x21\x0b\x6b\x0a\xdf\xe6\ -\xf6\x5f\xef\x18\xaf\xbe\xef\xcb\xc3\xaf\xc2\x09\xb7\x24\xf0\xbf\ -\xe3\xa7\x1d\x96\x78\x8b\x04\x9e\x2f\x19\x3e\x13\xf8\x10\x57\x0e\ -\x97\xcf\xa2\x02\x50\xb6\xda\x37\xe1\xed\xe8\xd9\xa2\xcc\x7f\x90\ -\x82\x5f\xd4\x30\x78\x9b\xb5\xb1\x61\x40\x55\x09\x3e\x5e\x6a\x85\ -\xc3\xac\x0d\xd8\xd0\x8e\xfe\x27\x87\x58\x93\xd7\x64\xfa\xfd\x03\ -\x2e\xf3\xd0\x2d\x0b\x51\xd5\xc0\x88\xfe\xe5\x8d\xba\x26\x40\x7f\ -\xfa\x4b\x5c\xdc\xc7\x60\xb6\x0f\xa4\x60\x38\x08\x09\x28\x26\xeb\ -\xce\x16\x08\x22\x12\x14\x4a\x4a\x40\xd9\x3f\x8d\x53\x74\x7e\x4f\ -\x33\xbf\x7b\xde\x5d\x13\xaa\x0b\xff\xb6\x6f\xf7\xc4\x8a\xbb\x3e\ -\x70\x55\x54\x5f\xef\xbf\x53\x6b\x0a\x9e\xd2\xe3\xce\xe9\x5c\xce\ -\xfe\x7e\x69\xd5\x86\x4f\x60\x3c\xec\xcb\x1a\x4d\x33\x0e\x30\x1c\ -\x62\x98\xac\xb7\x65\x6b\x68\xf3\x9d\x1e\x48\x69\x46\xff\xbb\xc3\ -\x1d\x7f\x99\x65\x1b\x4b\xc2\x2f\x13\x49\x30\x98\x93\x47\x04\xc8\ -\x26\xe0\xbd\x01\x4a\x3e\x51\x43\x01\x1f\x8d\x96\x89\x2a\x3c\xd1\ -\xfc\x99\xa4\x04\xd9\xa7\xac\x70\x56\xef\xa1\xa8\x28\x5b\x8f\xad\ -\x20\xb6\xff\x86\xcf\xad\x3e\x2d\xf2\x9b\x3b\x0c\x14\x85\x4f\x69\ -\xe9\xe2\x4a\xff\xc1\xa7\xae\x09\x20\xea\x0d\xfc\x63\xa3\x1d\x66\ -\xdd\xe3\xbe\x91\x08\x5f\xc4\x9b\xdb\xe0\x73\x6b\x13\x3c\x3e\x38\ -\x1d\x72\xaa\x4a\x50\x51\x2a\xac\xd1\x88\xba\x7d\xa3\x63\x30\x09\ -\xbe\x8c\x15\xe0\xaf\xc0\x01\x3e\x0b\xc2\x67\x92\xbb\xfe\xe8\x67\ -\xef\x7a\x8a\xdd\xdd\xf3\x1f\xb8\xfb\x07\x24\xd7\x14\xbe\xca\x72\ -\xab\x44\x43\xa7\x86\xa9\x2d\x1c\x35\x50\x8c\xd0\x60\xf6\x48\x6f\ -\x12\x7c\x3e\x0d\x9f\xc5\x7a\xe5\xac\xcc\x95\x43\xb9\xb4\x2e\x80\ -\x9b\x1c\x1b\xfa\x53\x01\x28\x57\x9d\xd4\x7c\x89\x04\x42\x42\xc9\ -\xf3\xb9\xca\x3e\x82\xab\x46\x85\xac\x04\x39\x67\x2c\x84\x2b\x9d\ -\x96\xa2\x89\x56\x3b\x14\x0d\x88\x01\xec\x24\xa8\x7c\xc5\xe7\xb6\ -\x96\x23\x23\x83\x23\x1e\x26\x60\xdf\x98\x8c\xcc\x46\x17\x1f\x16\ -\x28\xfc\xf6\x3b\xfd\x87\xb8\xba\x26\x00\x1d\x8a\x15\xae\xde\x3e\ -\x0e\xb7\x7a\x4d\x2c\x4b\xf1\xd1\xe1\x55\x29\x05\x0a\xfd\x76\x41\ -\xfa\xed\xcd\x90\x7c\x70\x26\x64\xd6\x24\x01\xe5\xf0\x7c\xc5\xd4\ -\xab\x67\x4d\x79\xe4\xf1\x90\xb3\x30\x68\x81\x8f\xe3\x83\x2b\xaf\ -\x68\xc0\x0e\x77\xaf\x7c\x74\xb8\x76\x3e\xd3\xfe\xf2\x99\xe7\x76\ -\x67\x8e\xc7\xd9\x1d\x3d\x14\xd9\x6b\xbf\x57\x70\xcf\xbd\x7b\xfd\ -\x7b\x6c\xdb\xce\x93\xdb\x3f\x38\xba\xba\xe0\xe5\x36\xdb\xe4\xeb\ -\xf6\x6b\xf4\x86\x0d\xde\xa0\xb7\xba\xd0\xb0\x8f\xba\x90\xfd\xb3\ -\xfe\x40\x83\x2c\xdd\xd5\x6b\x93\x25\x25\x68\xbd\x71\x03\x4e\xee\ -\xa3\x80\xbf\x3b\x33\xb8\xd1\x56\xff\x2d\x2b\xc1\x05\x27\xcd\x87\ -\x44\x00\x3e\x2d\x0d\xa2\x87\x33\xa9\xd9\x3b\xb5\x42\x59\x09\xf2\ -\xcf\xb5\x13\xac\xed\xbf\x01\xc7\x58\x4d\x45\x25\x39\x51\xd7\xf1\ -\x6b\x3a\xac\xfc\x99\xcf\x8b\x4a\x82\xda\xd6\x63\xd0\x72\xd7\x89\ -\x8f\xca\xa1\x19\xa5\x04\x94\x9b\x30\x87\x6d\x65\xc8\xd5\xb5\x4a\ -\xe0\xb3\x65\x1b\x47\xe3\x16\x2f\x37\x11\xd7\xce\xdb\x85\x0b\xc3\ -\x98\x4c\x56\x82\x7c\x5f\x08\x20\x8f\x02\x24\xbc\x3e\x3c\x0b\x52\ -\x6a\x12\xc0\xd3\x0d\xf2\x56\x4d\x82\x80\x4e\x2e\x72\xc1\x5d\xa6\ -\x2d\x88\xef\x3a\x75\x41\x0e\xa1\x8c\x80\x35\xd1\x74\xce\xf0\x7f\ -\x14\xfb\xdc\xdd\xbd\x51\xd3\x59\x57\xa8\xd7\x4b\x0d\xb5\x2c\x94\ -\x51\xad\xb9\x02\xca\xab\x70\x45\x67\x1b\x30\x1c\x40\x45\x0d\x19\ -\xd4\x68\xa1\x88\x3a\x1d\x55\xd0\xa0\x8f\x06\x36\x1e\x6c\x87\xcd\ -\x36\xef\x14\x09\x50\xbf\x57\x1f\xe4\x90\xaf\xeb\x63\x0a\x38\xa3\ -\x9b\x12\xb2\x02\x50\x4e\xd8\x6b\x45\x44\x0e\x85\x0f\x54\x02\x4a\ -\xdc\x04\xd9\xe0\x92\x33\xfa\x69\x54\x82\xc2\x4b\xe6\xa5\x1b\x06\ -\xae\x2f\x59\xe3\xb4\x1b\xcd\xb5\xad\xd8\xba\xc1\x55\xf6\xc0\x28\ -\x71\x91\x4f\x0f\xa5\xc8\xaa\x27\xd7\x0c\x5b\xb6\xde\x89\xcc\xaf\ -\xe1\xc2\xdd\x57\x03\x72\x56\xde\x7f\x94\xde\xe8\xef\xd0\x62\xb9\ -\x21\x2e\xec\xee\xea\x75\x4e\x80\x93\x0b\x56\x8f\xa8\x10\x80\xb2\ -\xe7\xd0\xb8\xec\xbc\x20\x95\x60\xb1\x04\xc2\x90\xfd\x10\x2f\x96\ -\xe0\xdd\xd1\x39\xf0\x44\x22\xf4\x82\xd5\x93\x21\xa0\xdb\x28\x6e\ -\x88\x8c\xb3\x4c\x31\x0c\x96\x41\x4a\x8b\xa1\xf6\xaf\xab\x0b\xdc\ -\xda\x6d\x3e\x5a\x0c\x9f\x80\x46\x3d\x1c\xb1\xf1\xb0\x5e\x08\x1e\ -\x0e\x08\xfb\xec\x11\xf6\xf4\x41\xd8\x65\x87\x32\x0b\x3b\xa0\x72\ -\x5b\x75\x94\x53\xe6\x4a\x8e\xeb\x17\x88\xb7\xbe\xa5\x5b\xdd\x9e\ -\x13\xef\x1e\x26\xfa\x37\x56\x08\x15\xa3\x26\xa8\xd4\xbe\x03\x5b\ -\x8c\xff\x45\x8f\xb1\x33\x6e\x02\xf8\x9b\x95\x4e\x25\x09\x0e\xf5\ -\xd6\x49\x88\x1a\x02\xd9\xac\x04\x84\x0f\xaf\x96\xa9\xf2\x84\xd7\ -\x8c\x4a\x4b\x2e\x99\xf1\xb7\x0d\xdd\xf2\x66\xbb\xf3\x51\x9c\xd4\ -\x79\x0e\x1a\x36\x34\x66\xa7\xb0\xd1\x93\x48\x72\x64\x38\xca\x68\ -\xd2\x64\x25\x76\xea\x1c\x82\xcc\xfc\xd8\x32\x58\xf1\x12\xbd\xee\ -\x86\x17\x12\x70\xcb\xd9\xbb\xd8\xb0\x45\x2b\xfa\xf5\x7b\xea\x62\ -\x33\xd0\x6b\xce\x32\xe7\x4a\x02\xb0\x04\x5f\xb7\xf0\x23\x02\x7c\ -\x2c\x09\x82\x68\xb1\x00\x78\x67\x33\x14\xac\x9c\x0b\xf7\xba\x4f\ -\xe0\x44\x28\x0c\x95\x29\x62\x43\x97\x44\x61\x80\x06\x5a\xbb\xce\ -\xc5\x76\xc3\xc6\x8b\xc2\x6e\x62\x62\x81\x2a\x8d\x9b\x20\x7d\x6e\ -\x56\x33\xd1\xa3\x4c\x7c\xe7\xd0\x49\x22\x61\xb4\x63\x4a\x7c\x8c\ -\xeb\x2c\x71\x1d\x85\xa9\xe1\x94\x92\xf6\xe2\x23\x5f\x27\x11\x42\ -\xc4\xb3\x9a\x4c\x25\xef\x58\x55\x05\xee\x9b\x3e\xcd\xd5\x71\x59\ -\xe7\xe6\x9f\x44\xe8\xde\x34\x37\x7c\x08\xa4\x4b\x48\x80\xd1\x23\ -\x98\xe4\x7c\x2f\x9d\x58\xfe\x15\x93\xd2\x7d\x23\x77\x24\x52\x09\ -\x28\x8b\xed\xfe\x40\x0b\xdd\x49\xd8\xc3\xe8\x2e\x8e\x68\x5f\x8c\ -\x1d\xba\x45\xf1\x61\xe9\xd3\x22\x1a\xbe\xcc\xaa\x97\x1f\x68\xf8\ -\x7f\x9e\xbb\x8b\x4d\x9a\xe9\xd3\x8b\x26\x4b\x9e\xc7\x54\x97\x04\ -\x08\x9c\x36\x7f\x40\xb5\x02\x50\x8e\xfc\x35\xec\x39\x3f\x44\x36\ -\x3e\xee\x04\x04\x53\x01\x96\xad\x01\x3f\xdb\x65\x20\x24\x20\xa5\ -\xdb\x12\x28\xea\xb8\x10\xde\x58\xcc\x65\x22\x9a\x3a\x32\xe9\x4a\ -\x2d\x18\x04\x25\x06\x19\x2e\xf7\x99\x78\xd7\xaf\x75\x84\x79\xe2\ -\xa0\xe8\x49\x23\x3d\x68\x97\x2c\xa1\xb9\x38\x28\xe6\x07\xfe\x6e\ -\x0a\x84\x99\x74\xeb\x39\x7d\x55\x05\xfc\xa5\x55\x23\xdc\x68\x63\ -\x80\x9b\xbb\xe9\x0a\xef\xf7\x67\xde\x4b\x4a\x40\x2b\x8a\x09\x6e\ -\x0a\xfe\x85\x67\x8d\xdf\x1c\x18\xbb\x27\x8a\x95\x60\xa0\x99\x6f\ -\xc1\x48\x4b\x44\x4b\xfb\xe7\x2f\x49\xf0\x1f\x69\xf8\x14\xb5\xa5\ -\x51\x1f\x69\xf8\xda\xcd\x0d\xd8\x3d\x8e\x9a\x7f\xb7\x9e\x40\xf2\ -\xea\x26\x3e\xe0\x60\x9f\x78\x20\x43\xfd\x07\x9e\x38\x52\x30\x69\ -\xb6\x63\x8d\x02\x50\xfe\xdc\x3f\x85\x1f\x7e\xad\xd5\xed\x75\x6b\ -\x18\x9e\xed\x12\x88\xb7\x59\x08\x01\x5d\x7f\x85\xbb\x9d\xa7\xc0\ -\xdd\x0e\x63\xc1\xcf\x72\x28\x3c\x6e\xd7\x1f\x0a\x2d\x06\x02\x2a\ -\x37\xac\xd8\xb4\x99\xa9\x45\x92\xcb\x88\x0f\xd7\x48\x90\xe5\x30\ -\xd8\xbe\xb1\x0a\xba\x99\x35\xc2\x33\x76\x9c\xb2\x2a\x12\x60\xf4\ -\x30\xc8\x4e\x5d\xa3\xe5\x7b\x68\xdc\x8e\x30\x2a\xc0\xe8\x0e\xfc\ -\xc7\x96\xce\xf9\x41\x24\xf4\x12\x36\x7c\x91\x00\xb3\xef\xa0\xb6\ -\x9e\x21\x7b\xcc\xbd\xee\x7f\x3d\x16\x20\xde\xbd\x7b\x66\xf9\xc1\ -\xc5\x0c\x6a\x69\x59\x60\x8b\x16\x0e\xa8\xa0\x20\x3a\x41\x3b\x94\ -\xa0\xfc\x03\x3e\x18\x1d\x5a\x0c\xf7\x18\xd4\x06\xef\x25\xad\xc9\ -\x7b\x8a\x5e\x58\x13\x87\x5e\x2d\xe2\x99\x8d\x94\x7b\x4d\x43\xae\ -\x89\xe6\xed\x45\xe1\xbf\xa7\x13\x3c\x6b\x69\x69\x47\x8f\xd5\x1b\ -\x45\x97\xa6\xd1\xdf\x5b\x43\x81\x8b\x03\xf4\xf5\xd0\xa5\x8d\x7d\ -\xe9\x62\xab\xee\x2f\xf7\xf7\xe8\x18\x7a\xc9\xb1\x45\xec\x83\x81\ -\x1a\xa9\x61\x23\x95\x22\x0e\x8c\xdc\x16\x60\x3a\xea\xfd\x03\x12\ -\x78\x99\x64\xf8\x22\x86\xba\xb3\x67\x32\xea\x7c\xf5\x60\x10\x0d\ -\x51\xfc\xec\xa2\x73\xf1\x1d\xc5\x56\xd2\xe2\xf1\x06\x1d\xb6\xe4\ -\x70\x64\x50\x53\xb3\x0d\x9a\x9a\xfe\x82\xd6\xd6\x0b\xd0\xcd\x2d\ -\x1c\x47\x8d\xba\x8a\x9c\xf2\x89\x0d\x93\x7f\xd0\x06\xd1\xe8\x34\ -\xae\x2d\x0e\x76\xb5\x2c\x3c\x13\x38\x3f\xb4\x9a\xf0\x8b\xa6\x06\ -\x8f\x0e\x00\x2f\x03\x34\x1b\xca\xc9\xab\x29\x7c\xf3\xbe\x80\x32\ -\xe5\x3b\x78\x4f\xad\x03\x8f\x3d\x2a\xc2\x48\xda\x6e\xa7\xbf\x7f\ -\x13\x65\x23\x1c\x6c\x3c\x1f\x47\x99\xac\xae\x84\x8b\xf9\xea\x57\ -\x8a\x2e\x57\x7d\x60\x46\xa0\x1f\x2c\x4a\x0c\x83\x15\x29\x39\x15\ -\x02\xb4\xe9\x2f\xea\x45\xfc\xe2\x68\xa0\xb8\xf8\x99\x2a\xee\x6d\ -\x12\x56\x54\x7e\x68\x9b\x53\x9d\x3c\x3f\x9a\x76\x41\x68\x35\x08\ -\x65\x4c\x47\xa3\x99\xb9\x0b\x5a\x5a\xba\x89\xe8\xd2\x65\x6e\x19\ -\x15\x80\x62\x6a\x64\x57\xa9\x97\xe9\x3b\x7e\x10\xa3\x64\xe5\xb9\ -\xe8\xec\x66\xc9\x22\xfc\xdd\x73\xb8\x4f\xb2\xd0\x53\x40\xc3\x4f\ -\x14\x7a\x64\x74\xba\xd6\x3d\x9e\x86\x4f\x10\x58\x0c\x02\x61\x4d\ -\x02\x68\xea\x57\x0c\xb9\x72\xeb\x50\xfd\x87\x8a\x30\x82\x96\xba\ -\x72\x5c\x05\xec\xa2\x33\xa4\x92\x00\x13\x2d\x36\xa4\xd9\x0f\x3c\ -\x14\x0c\x03\xbc\xb0\x82\xc1\x47\xf2\x60\xd0\x1e\x92\x9f\x68\x3c\ -\xc1\xe9\x6b\x04\x58\x29\x3a\xcc\x71\xd0\x7a\x84\x9d\xb9\x08\x8b\ -\x83\x11\xa6\xdd\x44\x70\xf5\x21\xff\x8d\x40\x20\x01\xb3\x70\xc7\ -\xdc\x7a\x61\xda\x79\x5e\x06\x15\xa0\x43\x87\xe9\xa2\xf0\x27\xba\ -\x5c\xcf\xf3\x73\x11\x1d\x70\x28\xa8\x5a\xd1\xf8\x0e\x1f\xc0\x2a\ -\x8d\x26\xf5\xb0\xff\x74\x13\x1c\xfc\x9b\x79\xce\x90\xb9\xe6\xd9\ -\xce\x73\xdb\xbe\x9f\xb4\xa1\x9b\xbf\x6f\xee\xc6\x50\xad\x13\x6d\ -\xb3\xc4\xe1\x23\x78\x1a\xe4\xd6\x14\xbe\xb1\x6d\xc5\x19\x84\xbd\ -\xeb\xe8\xd6\x38\x8c\x78\x53\xea\x94\xa6\x2a\xad\xd0\xd9\x78\x81\ -\x48\x80\xf1\x16\xab\xd2\xb7\x38\x1f\x2d\xe3\x0c\xd8\xff\xa6\x92\ -\x04\x66\xa3\xd8\xdd\xcb\xe5\x3e\x2b\x80\xf8\x28\xd3\x12\x70\xde\ -\x58\x7e\x9e\x5f\x55\x3c\x05\x7c\xd8\x51\x90\x0c\x1b\xdf\x06\xc3\ -\xe2\xa7\x71\x30\x2d\x92\x4f\x44\x78\xdf\xdc\x61\x47\x30\x95\xc0\ -\xcd\x35\x2c\x37\xf1\xb7\x89\x69\xb8\xb4\x1f\x5a\x6a\x89\xea\x02\ -\x17\xbe\xe7\x9e\x7a\xb4\x6e\x61\x3c\x03\xf0\x97\x37\x95\x71\x7c\ -\x2a\xef\xcf\x39\xa0\x5f\x5c\x11\x3e\x81\xf1\x34\x48\xad\x56\x80\ -\x01\x80\xf5\xca\xcf\x1c\xbc\x52\xe7\x57\xf3\x94\x0f\xef\x5e\x92\ -\xe7\xd6\xc3\xae\xba\xc3\x71\xb8\xe9\x6f\xa2\x96\x80\x75\x7f\xaf\ -\xa8\x8a\xf0\x9d\x76\x23\xc8\x8b\xd6\x20\x1c\xff\xe2\x84\x10\xf1\ -\x71\xae\x61\xd0\xdc\x12\x61\x6b\x06\x0d\x1c\xc1\xa3\x14\x19\x0f\ -\xbe\x90\xf1\x2c\x2b\x61\xbc\xf0\x23\xb3\x1f\xf3\x09\xb9\x1c\x2f\ -\x7c\xcf\xf1\x12\x66\x2a\x7a\xf0\x43\xd4\xb6\xe7\xdc\x6f\x35\xcf\ -\xef\xda\xcd\xd9\x6b\xee\xd3\xf0\x29\x57\x87\x59\x61\x63\x25\x79\ -\xb6\xe2\x41\x7b\xa5\x14\xbe\xc3\x48\x20\xf6\xb8\x58\x29\xfc\x32\ -\xab\x10\x55\x9e\x64\xf0\x2c\x1c\x77\xfd\x84\xea\x04\xd0\x35\x07\ -\x76\xda\x95\xd1\x4f\xb3\xac\xab\xbc\x0f\xa2\xd8\xb0\x81\x29\xce\ -\xef\xb5\x16\xd7\x0f\x3e\x84\x4c\xbf\x7d\x7c\x91\x00\xe4\x71\x2d\ -\x7e\x8c\xdb\x7f\xd5\x8c\x20\xf2\x6a\x29\x3e\xa9\x52\xf4\x8d\x0d\ -\x9a\x34\x47\xa7\x49\xcb\x71\xc3\xd5\xe7\x9f\x66\xb6\x86\x10\x78\ -\x28\xf4\xba\x8e\xaf\xbd\x4e\xe1\x63\xcf\x83\xf8\x6c\xe7\xce\x9c\ -\xd7\xcb\x7a\xf6\xbc\x9e\x60\xa7\x7f\x25\xdb\xb5\x4b\x6c\xde\x5c\ -\x87\xb2\x92\x45\x7d\xf1\xf8\x80\x76\xd8\x49\x5b\x8d\x3d\xfd\x72\ -\xc3\x7f\x7a\x46\x0e\x5d\x4e\x25\xa3\x0c\x38\x3c\xad\x22\xfc\xdc\ -\x66\x37\x1a\x87\x57\x17\x3e\x45\x66\x4f\xf3\xe8\xaa\xe1\x9b\x39\ -\x00\x72\x65\x45\xbf\x97\xfb\x4f\xb7\xb6\xaf\x7c\xc5\x14\xed\x98\ -\x42\xfd\x06\x2d\xb1\xa1\x95\x2b\x42\x97\xb9\x28\x7a\x9c\x03\x2c\ -\xff\xa6\x29\x61\xb4\x87\x48\x7c\x5a\x17\x6d\xd3\x1f\xa7\x77\x31\ -\xc3\x30\x68\xd2\xc5\x01\xe7\xfc\x7e\x0b\xb7\xef\x78\x87\x5e\xe4\ -\x91\x20\xc9\xc2\x85\xb7\x43\xa6\x8e\xdf\xfa\x76\xae\x31\x93\xe2\ -\xae\x0a\xde\xcf\x0c\x99\xd0\xb7\x76\x8d\x53\x82\x86\x98\x15\x65\ -\xcc\xee\x8d\xa1\x13\xba\xa2\x8b\xa9\x0e\xca\x71\x39\x42\xf1\x49\ -\x61\xa3\xbe\xe5\xa4\x2c\xba\x22\xa8\xa9\x53\x79\xf8\xc3\x32\x99\ -\x67\xaa\xa7\x75\x5f\x54\x1b\xfe\x7e\xc3\xb7\x70\xac\x65\x98\xf2\ -\x6e\xad\xbf\xab\x0a\xa0\xa1\x2b\x0a\xff\x43\x6d\x6d\xf6\x7d\x07\ -\x09\xb8\xe2\xd2\xb6\x58\xa2\xe7\xf2\xab\x16\xb8\x7e\xcd\xa6\x0c\ -\x13\xc4\x7d\xdc\xa4\x99\xc7\x25\x4d\xbf\xbe\xa4\xd2\x77\x0e\xdd\ -\xdd\xf9\x42\x4f\xcf\x52\xf2\xff\xfb\xf3\xe9\xb1\xa7\xb3\xbb\x98\ -\xf2\x7e\x33\x86\xe2\x0d\x0d\xc1\xff\xb8\x2c\x84\x67\xb5\x82\x24\ -\xb4\x04\x61\x72\x37\xf5\x22\x9f\x7e\xad\x30\x6c\x5c\x17\x5c\x67\ -\x6b\x8c\x3a\x2a\xa2\x7d\xf7\xf2\x09\x07\xe8\x96\x2f\x5f\xb8\xbe\ -\x21\xed\x82\xed\xb0\x1d\xb0\x97\x1f\xf7\x8d\xec\x61\xbd\x7c\x71\ -\xe0\xc5\x70\xc4\x30\x09\xce\xb7\x0e\x03\xef\x76\x8f\x21\xaa\x63\ -\x2e\xc4\x5b\x0b\xe0\x9c\x1a\x4f\x71\x0b\x04\x48\x86\x6f\xd4\x15\ -\xfe\x31\x0b\xe6\xa7\x5e\xed\x0b\x20\x2f\x39\xe5\xeb\xbb\x4d\x0a\ -\x25\x2f\x3b\xf1\x81\xc8\xa2\x0f\x54\x49\x49\x03\x1d\x1d\xe7\x89\ -\x0e\x3b\x16\x31\x7e\x4b\x1e\x11\xe0\x03\x01\x97\xe9\x02\xcf\x93\ -\x54\x2a\x2f\x2a\x82\x7f\x81\x19\x64\x10\x11\x90\xf2\xbc\xb3\xb2\ -\xc0\xdb\xde\x10\x77\xd9\x18\x62\xb7\xa6\x1a\x6c\x30\xe1\xe2\x93\ -\x37\x7a\x8b\x9b\xa1\x7b\xc5\xa7\x68\xe6\x92\xc6\x0f\x82\x61\x3d\ -\x34\x3a\x5c\x1f\x99\xc3\x46\x69\x70\xc7\x2c\x19\x42\x3a\x7c\x24\ -\x61\x23\x3c\x91\x20\xae\xf3\x47\x38\xa6\x10\x4c\x57\x0c\xab\xfc\ -\x01\x3c\x36\xfc\x76\xfd\x01\x15\x54\x2a\x4e\xf7\xa8\xf7\x6f\x59\ -\xf2\xfd\x43\x67\x05\x93\x57\x6b\xf1\x81\x48\x6f\x15\x15\xeb\xa3\ -\xb5\xf5\x2f\x38\x79\xf2\x3e\x91\x04\x63\x2d\x5a\xbd\xa4\x02\x50\ -\x16\x18\x42\xc4\x3e\x2e\xe4\x78\x01\x14\xde\x56\x05\x9f\x92\xb6\ -\x90\xcf\x8a\x40\x49\xeb\xa8\x88\x17\x2c\x35\x70\x54\xd3\x7a\xa8\ -\xc0\x65\x10\x28\xc6\xf5\x10\x9c\x1b\x21\xac\xd0\x47\x38\x65\x86\ -\x10\xd5\xb9\x72\xd0\xd5\x11\x63\x95\x09\x07\x39\xf1\xec\x5e\x01\ -\xea\xab\x3e\x09\xa0\xdd\xa6\xe2\xee\xef\x2f\x0d\xfb\x3b\x4f\x0b\ -\xa7\x6d\x4b\xda\xc1\x40\x38\xa4\xac\xac\x2e\xb0\xb5\x1d\x8b\xe3\ -\x46\xaf\xc3\x99\x46\x20\x60\x25\x98\xdb\x12\x52\x76\xc9\xc3\x53\ -\x22\x01\x12\xde\x05\x36\x02\x5f\x41\x7b\x28\x95\x14\x81\x12\x66\ -\x29\x83\x1c\x5f\xab\x2f\x87\x5d\x95\x50\xf3\x44\x38\xc0\xa4\x49\ -\x6e\x16\xa1\xb9\x04\x7c\x69\xf8\x26\x7d\xc8\x23\x8b\x2b\x0a\xff\ -\x92\x34\xe8\x1f\xbc\x2e\x80\xee\xa4\x4d\x78\x51\xbf\xbe\x26\xda\ -\xea\xaa\x21\x2b\x00\xe5\x57\x63\x28\xd8\xa2\x0a\x21\x62\x09\xf0\ -\x20\x03\x2f\xe2\x9a\x43\x50\x15\x09\x84\x23\x16\xeb\x46\x7c\x53\ -\xf8\xbe\x46\xa1\x24\xf0\x82\xaa\xbb\x85\xe8\xcc\x81\x10\x2a\x80\ -\x5a\x93\x8a\x31\xfa\xa6\xd2\xa0\xff\x1f\x16\x86\x88\xc7\x0f\xe8\ -\xa8\xa0\xd0\x88\x3c\x77\xa7\xb7\x00\x49\x11\x84\xbf\x37\x01\x1f\ -\x56\x02\xca\x51\x19\x88\x4d\x35\x82\x47\xac\x04\x59\xe6\x70\x53\ -\x25\xac\xd3\xe3\xaf\x0a\xff\x7a\x13\x5f\x12\x76\x59\x75\x7b\x05\ -\xe9\x8c\x86\xf7\x86\x9d\x2a\x8a\xfe\x79\xd2\x90\xff\x9f\x57\x06\ -\x89\x77\xf2\x7e\xaa\x2c\x43\x9a\x6d\xba\x95\x24\xc0\x45\xfa\x10\ -\xe8\xc1\x81\x8f\x92\x22\x9c\x51\x80\xa0\x1c\x13\x48\x79\xd7\x09\ -\x1e\xdc\xb2\x51\xe3\x91\x80\x05\x35\x06\x4f\x6b\xfa\x67\x69\x07\ -\x50\x0d\x1b\x45\x11\x9a\x0e\x06\x94\xab\x27\x0a\xff\x61\x5d\x3f\ -\xd2\xad\xce\x2e\x0d\x13\x2f\x41\xf2\xa4\x07\x2b\x5a\x69\x00\xce\ -\x69\xf9\x49\x02\x52\x2f\x48\xd8\x23\x07\x69\x92\x12\x10\xf8\x57\ -\x35\xe0\x46\x96\x2e\xf8\x5a\x9f\x32\xf3\xad\x36\xfc\x47\x1d\x3f\ -\xc0\x11\xb9\xd0\xcf\x85\x2f\x6a\x05\x94\xf7\xf8\xe5\x7c\xef\xf1\ -\x08\xa9\x00\xff\x99\x08\x74\xd3\x24\x7e\x23\x05\xc0\xf1\xfa\x95\ -\x4a\x83\x77\x7f\x2a\x41\x4c\x25\x09\x18\xc8\x88\xea\x06\x05\xef\ -\xea\xcb\xa4\x71\xe2\xac\xdf\x56\x0a\x3f\xca\x32\x9d\xd4\xf4\x13\ -\x6a\x08\xfd\x03\xec\x81\x48\x98\x49\x9e\xf7\x83\xc8\x9f\xcb\x2b\ -\x7e\xc3\xa5\xe1\xd6\x92\xc5\xa1\xe2\x39\x05\x19\x32\xa4\x4d\xdf\ -\xbb\x71\x25\x09\xf8\x6b\x35\xc1\x4f\x52\x82\xc0\x4e\x90\x5e\xa0\ -\x0a\xbe\x1b\xa6\xea\x04\x54\x84\x1f\x6c\xf6\x04\xbc\x98\xcc\x4f\ -\xfb\x01\x42\x2e\xec\x85\x50\xd8\x00\x3c\x98\x0f\x8f\xc1\x0d\x4a\ -\x09\x08\x2e\x04\x25\x51\xf8\x07\xa4\xc1\xd6\xb2\xd5\xc1\xe2\xad\ -\xd5\x68\x37\x30\x1a\x2a\x03\x4e\x93\xa8\x20\x2e\x6b\x06\xbe\xe4\ -\x59\xc1\xa7\x02\xdc\x34\x82\xe0\xf8\xf6\x90\x4c\xbe\x43\xa8\x11\ -\xdc\x31\x06\x1e\xb4\x08\x21\x81\xbf\x82\xdd\x10\x04\xeb\x48\xe0\ -\x73\x21\x81\x84\x2d\x10\x05\x2e\x89\x2b\x41\x5b\x14\xfe\x13\x69\ -\x87\x4f\x2d\x5d\x1e\x2e\x9e\xd4\xb0\x86\xce\x17\x50\x22\x15\xc4\ -\x21\x4d\x3f\x49\x30\xdf\x08\xa2\xf7\x72\xe1\xdd\x19\x75\xf0\x0d\ -\xef\x0e\xc8\x97\x85\xe8\x64\x1d\xb9\x64\x99\x39\x90\xf4\x8f\xb0\ -\xab\xa3\xfc\xb9\x5f\x44\x68\x2b\x0d\xb5\x96\xef\x0f\x20\xee\x52\ -\x7e\x45\x4b\x83\xf6\xea\x80\xb3\xc5\x15\x44\x52\x51\xcc\x70\xd7\ -\x00\x6f\x2a\x40\x6a\x0b\x08\xa4\x6f\xcd\xab\x35\xb9\xeb\xbf\x14\ -\xbe\x5d\x45\x93\x6f\xbc\x34\xd0\x3a\xb2\x41\x84\x78\x90\x69\x0a\ -\xad\x1b\x68\xca\x03\x8e\x13\x57\x10\x7f\x6d\x09\x85\xa1\xdd\x41\ -\x10\x61\x0b\x7c\x64\x20\x53\x08\xf0\xa1\xd9\x28\xc8\xa8\x31\xfc\ -\xe1\x84\xf2\x61\xde\xdd\xd2\x30\xeb\xe0\x0e\x21\xe2\xd9\xbe\x77\ -\x64\x18\xc0\x9e\xe2\x0a\xe2\xc5\x4e\x90\x49\x4b\x81\x9c\x86\xe0\ -\x43\xdf\x5e\x52\x7d\x08\xac\x36\xfc\x09\x84\xf2\x19\x3e\xb7\xfe\ -\x57\x9b\x2c\x4a\x05\xf8\x7e\x22\xd0\xd2\xa0\x4c\x9f\xd4\xe2\x97\ -\x1b\x83\x80\x0a\x10\xdb\x09\x32\xc8\xbf\x94\xd2\xb7\x38\xa2\x37\ -\x44\xfc\x43\x00\x3d\x51\xf8\x8f\x7e\xc4\x34\x74\xa9\x00\xff\x3b\ -\x09\x70\x38\xa9\xcd\x53\x01\x28\x45\x8a\x10\x44\xdf\x62\x31\x07\ -\x5e\xc8\x4d\x81\x92\x8a\xf0\x3b\x8a\xc2\x7f\xf7\x9f\xce\x32\x92\ -\x52\x4b\xf7\x08\x22\xaf\xdf\xe5\x38\xa4\x42\x68\x00\x48\xea\x02\ -\x18\x6c\x0c\xa9\xa2\xb7\x48\xc7\x0e\x0c\x21\x40\x14\x7e\xbf\x8a\ -\x99\xbd\x4e\xd2\x00\x7f\xc2\x5d\xc2\xd8\x9d\x41\xcd\xea\x03\x9e\ -\xef\x00\xf8\x9c\x53\xfe\x18\x20\x6d\x3c\xa1\x5e\x6f\x12\xbc\x02\ -\x54\x5a\xdd\x2a\xe5\x27\xdc\x26\x4e\xbc\x1a\x26\x9d\x96\x06\x6e\ -\x24\xf0\x25\xe4\x6d\xf6\xa3\x4b\xa4\x98\x8a\x4a\x9f\x82\x34\xbc\ -\x9f\x7c\x9f\x40\xf1\x80\x12\x3d\x7c\x21\x5d\xbc\xb8\x81\x72\x50\ -\x5a\xe3\xff\x17\x9f\x18\x22\x45\x2a\x80\x94\xef\xcc\xff\x01\x19\ -\x47\x8e\x78\xd3\x1b\x66\xf0\x00\x00\x00\x00\x49\x45\x4e\x44\xae\ -\x42\x60\x82\ -\x00\x00\x05\x24\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0\x77\x3d\xf8\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x06\xec\x00\x00\x06\xec\ -\x01\x1e\x75\x38\x35\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ -\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ -\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x13\x74\x45\ -\x58\x74\x41\x75\x74\x68\x6f\x72\x00\x52\x6f\x64\x6e\x65\x79\x20\ -\x44\x61\x77\x65\x73\x0e\xd8\x7e\x1d\x00\x00\x04\x82\x49\x44\x41\ -\x54\x48\x89\x8d\x96\x6d\x88\x54\x55\x18\xc7\x7f\xe7\xbe\xcd\xdc\ -\xb9\xb3\xb3\x33\xfb\x1a\xeb\xbe\xe9\x6e\xad\x2f\x69\xea\x4a\x68\ -\x8a\x22\x24\x94\x58\x7e\xaa\x48\x23\x30\x41\xa1\xec\x5b\x51\x41\ -\xf8\x31\xa3\x3e\x44\x94\x44\x92\x09\xa5\x46\x54\x84\x21\x59\x44\ -\x18\x96\x19\xe8\xb2\x5a\x9b\xba\xea\xa4\xfb\x66\xee\xce\xce\xec\ -\xec\xcc\xdc\xbd\xf3\x76\xef\xe9\x43\xb3\x63\xe6\xbe\x3d\xf0\x7c\ -\xfb\x9f\xff\xef\x3c\xcf\x3d\xf7\x39\x47\x48\x29\x99\x2d\x36\xbf\ -\x27\x7c\x6a\x40\x7f\xc0\x6f\xaa\x8f\x02\x64\x1d\xf7\x84\x3b\x51\ -\xb8\xf0\xed\x8b\x32\x37\xdb\x5a\x31\x1d\x60\xf7\x01\xa1\x67\x2a\ -\x42\x6f\x37\xd7\x76\xac\xaf\x0a\xd6\xd5\x07\x2c\x7f\xb5\x69\x05\ -\x7c\x52\xba\x38\xb6\x93\xb3\xed\x6c\x62\xdc\x8e\xdd\xea\x8b\x5d\ -\x39\x15\x4c\xa7\x5e\xfe\x70\x97\x2c\xcc\x19\xb0\xf5\xa0\xb1\xa2\ -\xb9\x7e\xfe\xa1\x65\x1d\xab\x96\x7a\x5a\x56\xc9\xbb\x0e\x92\x3b\ -\x75\x02\x81\xa1\x9a\x50\x50\xbd\x9e\x2b\xe7\xff\xe8\x1f\xbe\xb1\ -\xe3\xd8\xce\x7c\xf7\xac\x80\x6d\x9f\x87\xf7\x2d\x69\x59\xf9\x5c\ -\x5d\x7d\x4d\x9d\xe3\xa6\x67\xeb\x00\x00\x01\x35\x44\x7c\x78\x74\ -\xe4\x42\x5f\xd7\xc7\x47\x9f\x4a\xbe\x36\x2d\xe0\x89\xc3\xe6\xb3\ -\x6b\x96\xae\xdf\x6f\x04\xf5\xa0\x27\xdd\x39\x99\x87\xb4\x7a\xaa\ -\xb4\x05\xa4\x8a\x43\x8c\x24\xaf\x65\xce\xf4\xfc\xfa\xc2\x17\xcf\ -\x38\x9f\xdc\x05\x78\xfc\x80\xa8\x59\xdc\xb6\xfc\xb7\xc6\xd6\x79\ -\x6d\x73\x33\x17\x6c\xa8\xdc\x43\x83\xb1\x0c\x9f\x08\x72\xc9\xf9\ -\x9e\x73\x99\x4f\x19\xb8\xd1\x17\xbd\x18\xed\x59\xfd\xcd\x2e\x39\ -\x0a\xa0\x4c\xca\xab\xab\xea\x8e\x34\x35\x37\xcc\xd1\x1c\x56\x5a\ -\x4f\xd2\xea\x5b\x8d\x5f\x54\x10\x2b\x46\x39\x97\x39\x8c\x2b\x5d\ -\x5a\x9a\x5b\xdb\xaa\xab\x6a\x8f\x4c\xea\x14\xf8\xf7\xa3\xde\xd7\ -\xd2\xb1\xd6\x15\x73\x33\xaf\xd1\xdb\xb9\xd7\xdc\x88\x82\x4a\xda\ -\x1d\xe1\x97\xd4\x07\xb8\xb2\x08\x40\x51\xb8\xb4\x37\xcf\x5f\xbb\ -\xf5\xa0\xb1\xa2\x0c\xf0\xfb\xd4\x2d\x56\xd0\xb4\x40\x20\xc4\x54\ -\xa9\x94\x53\x53\xfc\xac\x0b\xed\x22\xa0\x86\xc9\x4b\x87\x4b\xce\ -\x09\xd2\xee\x2d\x14\xa1\xa2\x08\x0d\x21\x54\x02\x96\xdf\xf2\xfb\ -\xd4\x2d\x00\x1a\x80\x65\x55\x76\xaa\x86\x86\xbc\xdd\xb1\x52\x97\ -\xc5\x5d\xbb\x5f\x57\xb1\x9b\x88\xd6\x02\xc0\xad\x42\x0f\x97\xb3\ -\x3f\xa2\x08\xed\x0e\x8d\x66\xe8\x58\x66\xa8\xb3\x0c\x08\x9b\x91\ -\x26\x4f\x4a\x54\x45\x9d\xd1\x7c\x81\xff\x21\x1a\x7d\xcb\xf1\x3c\ -\x8f\x91\x42\x2f\xc7\xe3\x7b\x31\xd5\x10\x86\x12\xb8\x43\x57\x14\ -\x0a\xe1\x40\xa8\xa9\x0c\x30\x0d\xb3\x5a\x11\x3a\x0a\x0a\x4c\x61\ -\x0c\x10\x50\x23\x2c\x31\x1f\x43\xb8\x3a\x49\xf7\x26\xc7\xe3\x7b\ -\x89\xe5\xa3\x00\x58\x6a\x15\x11\xa3\x11\x53\x0d\x23\x10\xe4\xbd\ -\x1c\xa6\xe1\xab\x2e\x03\x9c\xfc\x44\x5c\x57\x8c\x96\xff\x9a\x77\ -\x98\x9b\x08\x28\x61\xba\xed\x2f\xf1\x64\x91\x4e\x73\x1b\x16\xb5\ -\x14\x65\x8e\x0b\xf6\x57\x0c\xe5\xcf\x97\xb5\xb6\x9b\xc0\x76\x12\ -\xf8\x14\x8b\xb0\xde\x48\xde\x4b\xe1\xe4\xb3\xf1\x32\x20\x69\x8f\ -\x0d\x08\xe9\xad\x14\xc2\x00\x60\x51\xe0\x11\x96\x04\x36\x23\x50\ -\x18\x2b\x0c\x52\x70\x73\xdc\xa3\x2d\x06\xa0\x3f\x77\x96\x53\xa9\ -\xfd\x53\x56\x99\xf3\x6c\x86\x73\xbd\x54\xa9\xb5\x24\x27\xc6\x07\ -\xca\x00\xdb\x49\x75\x25\x9d\xa1\xad\xe8\x3a\xa6\x5a\x89\x81\x85\ -\xf0\x34\x3c\xcf\xa3\xcd\xd8\x80\x2e\x4c\x14\xa1\x13\x2b\x5c\xe5\ -\x58\xe2\x55\x60\xe6\x09\xac\xba\x3a\xb6\x93\xe9\x2a\x03\xb2\x39\ -\xf7\x78\x2e\x53\x7c\x65\x3c\xd0\x67\x49\x3c\xbc\xa2\xa4\xb5\x76\ -\x0d\x9a\xf0\x53\x55\x3a\x31\xb6\x1b\xe7\x64\xea\x1d\x26\xbc\xc4\ -\x8c\xe6\x02\x85\x82\x2d\xed\x6c\xce\x3d\x0e\xa5\xff\xe0\xd8\xce\ -\x7c\x77\x74\xa0\xff\x74\x8d\xd2\x00\xc0\x60\xbe\x9b\x44\xb1\xbf\ -\xbc\xc8\x93\x2e\x97\x9c\xef\xb8\xea\xfc\x34\xa3\x39\x40\x8d\x52\ -\x47\x74\x70\xf0\xf4\xe4\x64\x2d\x1f\xfc\x78\x22\xb6\x3d\x31\xe4\ -\x44\x4d\xc5\xc2\xc3\xe5\x77\xfb\x6b\x3c\x59\xa4\x20\x1d\x06\xf2\ -\xe7\xf8\x21\xf9\xe6\xac\xe6\xa6\x12\x20\x31\xe4\x44\xe3\x89\xf8\ -\xf6\x72\x45\xff\x9f\xa6\x9d\x0b\xef\x7f\x3f\xe9\xff\xbb\xc2\x95\ -\x1e\x0b\xcd\x87\xc9\xca\x34\xd7\xb3\x67\x98\xad\xef\x9a\xd0\x08\ -\x3b\xb5\xe9\xae\xde\xde\x3d\x53\x4e\xd3\xc9\xd8\xf6\x59\x78\x5f\ -\xc7\xbc\x96\x1d\x6a\x4d\xa1\x3e\xe9\xc6\x67\xdd\x35\x40\x58\x8d\ -\xe0\x8e\x6a\xc3\xbd\x43\xfd\x87\x8e\x3e\x9d\x9a\xfe\x3e\x98\x8c\ -\x4d\x6f\x19\xab\xdb\xda\xeb\x3f\x5a\xd0\x5e\xb7\x28\xad\x24\x94\ -\xac\x9c\xfa\x46\xf3\x0b\x3f\x15\x6e\xd8\xfb\xeb\xda\xc8\xe5\x8b\ -\x5d\xb1\xe7\x7f\xde\x57\x3c\x2d\x65\x69\xea\x4d\x05\x10\x42\xa8\ -\x40\x35\x10\x36\x2b\xa8\xdd\xf8\x7a\xf0\xa5\x96\xe6\xea\x65\xa1\ -\x50\x20\x12\x08\x2b\x41\xa3\x42\xd1\x01\x72\x29\xaf\x38\x91\x2c\ -\x66\xd2\xa9\x6c\xf2\xfa\xf5\xd8\x9f\x27\xdf\x98\x78\x37\x67\x33\ -\x0a\x8c\x01\x49\x29\x65\x72\xda\x0a\x84\x10\x16\x10\x01\x2a\x4b\ -\x59\x61\x04\x89\x34\x3c\xa8\x2c\x6c\x5a\xa5\xad\x90\x12\x65\xe0\ -\x6c\xf1\xfc\xcd\xb3\x5e\xb4\x60\x33\x06\x64\x80\x14\x30\x5e\xca\ -\x84\x94\xb7\x1f\x00\xd3\xbe\x2a\x4a\x30\x1d\x30\x00\x5f\x29\x75\ -\x40\x05\x8a\x40\x0e\xc8\x03\x59\xa0\x20\xe5\xd4\x37\xd5\x3f\x13\ -\x05\x02\x8c\xec\xcf\x7e\xae\x00\x00\x00\x00\x49\x45\x4e\x44\xae\ -\x42\x60\x82\ -\x00\x00\x05\x64\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0\x77\x3d\xf8\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\ -\x01\x42\x28\x9b\x78\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ -\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ -\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x04\xe1\x49\x44\ -\x41\x54\x48\x89\xb5\x95\x4b\x6c\x54\x55\x18\xc7\x7f\xe7\x9c\x3b\ -\xd3\x99\x32\xb5\xf4\x41\x1f\x38\x02\xa5\x8d\x4c\x54\xb0\x25\x48\ -\xa2\x12\x62\x14\x17\x46\x12\x64\x81\x26\xb0\x31\x18\x12\x36\x04\ -\xd2\x45\xe9\xca\x84\x5d\x29\x1a\x43\x80\x6e\x0c\x2b\xdc\x88\x09\ -\xa0\x21\x04\x35\x18\xa2\x26\x3e\xa3\x62\xa8\x4f\x7c\x61\x95\x81\ -\xb6\x4c\xcb\xbc\x67\xee\x39\x9f\x8b\xdb\xde\x4e\x2b\x10\x37\xde\ -\xe4\x4b\xee\xf3\xf7\xff\xfe\xdf\xf9\xee\x77\x94\x88\xf0\x7f\x1e\ -\xde\xdd\x1e\x9e\x53\xaa\x23\x6a\xcc\x33\xf1\x58\xec\xc9\xc6\xae\ -\x15\x2b\xeb\x97\x76\x2e\xb5\xc5\x62\x29\xfb\xd7\x5f\x63\xb9\x6b\ -\xd7\x7f\x2c\x55\x2a\xe7\x2b\xd6\x7e\xb0\x59\xa4\x70\x27\x86\xba\ -\xad\x03\xa5\xd4\x87\xb1\xd8\xcb\xc9\xc7\x1e\xdd\xdd\xd9\xde\xd6\ -\x11\xab\x8b\xa3\x4a\x45\xc8\xe5\xc1\x18\x48\x2c\xc2\x79\x86\x5c\ -\x21\xef\x7e\x1f\xbd\x7c\x65\xe2\xa7\x5f\xf7\x3c\x59\xad\xbe\xf7\ -\x9f\x04\xce\x2b\xb5\xa2\x75\x69\xe7\x1b\xa9\x0d\x8f\xaf\x4f\x54\ -\x6d\x84\x52\x29\x7c\x26\xc0\xec\xfb\x32\x13\x34\x24\xb8\x91\x9f\ -\x9e\xfa\xe5\xe3\x4f\xdf\xb6\xb7\xb2\xbb\x9f\x10\x29\xd5\xf2\xe6\ -\x09\xbc\xa7\x54\x6a\xc5\xda\xde\xf7\x7b\xba\xbb\x93\x3a\x57\x98\ -\x83\x2c\x80\x2e\x14\x11\xcf\xc3\xc6\x23\xf2\xdd\xa7\x9f\x7d\x91\ -\x1f\xbb\xf6\xf8\x13\x22\xfe\x2c\x53\xcf\x9e\x1c\x50\x4a\xb7\x2e\ -\x5f\x76\xa2\x67\xf9\xb2\x24\xd9\x3c\x4e\x24\x0c\x0b\xd8\x9a\x6b\ -\x07\xc1\x3d\xc0\x01\xce\xf7\x21\x5b\x54\x3d\x0f\xaf\x5e\xa7\xeb\ -\xeb\x5f\xab\x75\x10\x0a\x6c\x8a\xc7\x87\xee\x7f\xe8\x81\x3e\x29\ -\x56\x02\x80\x08\x95\xed\xdb\xa9\x6e\xd9\x32\x27\x52\x03\xb5\x9e\ -\x87\x1d\x18\xc0\xef\xed\x0d\xc5\x8d\x55\x3a\xd9\xf7\xe0\x0b\xef\ -\x46\xa3\xeb\x67\xb9\x1e\xc0\x05\xa5\xee\x5f\xf9\xd8\xfa\x17\xeb\ -\x7c\x67\x1c\x0a\x01\xec\x8e\x1d\xf8\xcf\x3d\x17\x64\xa1\x14\xfa\ -\xf4\xe9\xb0\x3c\x2e\x12\x81\xfd\xfb\xa1\xb7\x17\xfa\xfa\x70\x43\ -\x43\xf0\xf5\xd7\x88\x15\x9a\x1b\x9b\x96\x34\x25\xdb\x5f\x47\xa9\ -\x5e\x44\x44\x03\x78\x9e\xf7\x52\xc7\xe2\xa6\x25\x0e\x15\x64\xe9\ -\x79\xd8\x9e\x9e\xd0\xa6\xdb\xbe\x1d\x7f\xeb\xd6\x20\xd3\x68\x14\ -\x06\x07\x51\x7d\x7d\x28\xa5\x50\x91\x08\x74\x77\x63\x95\xc2\x29\ -\x85\xad\x38\x5a\xdb\x5a\x97\x9d\x85\x9e\xd0\xc1\xa2\xd6\x96\x07\ -\x11\x70\x5a\x07\x59\x3a\x07\x43\x43\xa8\xc1\x41\x58\xbd\x3a\x50\ -\xd9\xb1\x03\x17\x89\xa0\x52\x29\xd4\x9a\x35\xa1\xb8\x3d\x79\x12\ -\xff\xcc\x19\xc4\x18\x10\x41\xb4\xa6\x2e\x16\x5b\x1c\xd1\xfa\x29\ -\xe0\x67\x0d\x10\x6b\x6d\xba\xcf\x39\xc1\x69\x3d\x17\xd6\xe2\x86\ -\x87\x91\xcb\x97\xe7\x16\xec\xf9\xe7\x43\xb8\x88\xe0\xbf\xf9\x26\ -\xd5\x53\xa7\x82\xc4\xb4\xc6\x79\x1e\x62\x0c\x26\x12\xa5\x2e\x11\ -\xdf\x00\xa0\xdf\x52\x2a\x51\xb7\x28\xde\xee\xbc\xc8\x7c\x01\xad\ -\x71\xce\xe1\xbf\xf2\x0a\x32\x3a\x3a\x57\x2e\xe7\xf0\x7d\x9f\xe2\ -\xa1\x43\x64\x07\x06\x28\x5f\xb9\x82\x2d\x95\x70\xc6\x04\xdf\x18\ -\x83\xad\x5a\xe2\x4b\x9a\x97\x87\x25\xc2\x98\x60\xe1\x6a\x7b\x7b\ -\x06\x28\xc6\x20\x4a\x81\x13\x9c\xb8\xf0\x1f\x70\x80\xb5\x16\x3f\ -\x9d\x46\xd2\x69\x74\x4b\x0b\x5e\x32\x89\x6e\x6e\xc6\x96\xcb\x88\ -\xb8\xa0\x8b\xb6\x89\xe4\xbe\x79\x68\xd5\x75\xb9\xd7\xb4\x8b\x95\ -\x00\xae\x54\x00\x8f\x44\x88\xec\xdb\x07\xa9\x14\xd6\xd9\xda\xf6\ -\x26\xd6\xdf\x8f\x03\x0a\x87\x0f\x07\x5d\x37\x31\x41\x65\x7c\x1c\ -\xe2\x71\xbc\xce\x26\xf2\x13\x53\x7f\x84\xff\x41\x71\x32\xf3\xa7\ -\x3f\x39\x81\xd3\x26\xb4\xea\x97\xca\xe8\x5d\xbb\x90\x55\xab\x70\ -\x2e\xc8\xa6\xf0\xea\xab\x54\x3f\xfa\x28\x14\xa9\xef\xef\x27\xb6\ -\x77\x2f\x56\x24\x08\xc0\x2f\x14\xa8\x7a\x9a\x72\xae\xf8\x71\x58\ -\xa2\xc2\x44\x66\xb4\x9c\xcd\x3c\xeb\x46\x7f\x40\x25\x12\x38\xdf\ -\x27\x3e\x32\x82\x5e\xbb\x36\x84\xe5\x87\x87\x29\x1c\x3b\x86\x44\ -\xa3\x34\x1e\x3f\x4e\xdd\xc6\x8d\x00\x24\xfa\xfb\x71\xbe\x4f\xf6\ -\xc8\x91\xb0\xb4\x3e\x4c\x39\xe7\x2e\x84\x0e\xf0\xfd\xe3\x19\xe7\ -\xc6\x2d\x8e\xca\xf8\x38\xd5\x4c\x06\xff\xd2\xa5\x10\x9e\x3b\x78\ -\x90\xdc\xd1\xa3\x41\x96\xe5\x32\x93\x3b\x77\x52\xba\x78\x31\x28\ -\x63\xb9\x4c\xe9\xdb\x6f\xf1\x67\x5c\xe8\xf6\x26\xa6\x27\x32\x57\ -\x37\xc3\x15\xa8\x19\x76\x17\xea\xeb\x87\xbb\xfa\x52\xfd\xf6\xf2\ -\x2f\x46\x24\x58\x8b\xc4\xe0\x20\xf6\xd6\x2d\x72\x23\x23\x41\x8f\ -\xd7\x36\x41\x34\x4a\xeb\xc8\x08\xd9\x13\x27\x28\x5c\xbc\x18\x0c\ -\xc4\xa8\x87\xac\xec\x18\xff\xfb\xab\x9f\x36\x6f\x16\xf9\x7c\x9e\ -\xc0\x01\xa5\xf4\xa6\x64\xfb\x67\x1d\x0d\x0d\xeb\x2a\x63\xd7\x91\ -\x05\xc0\xd9\xde\x9f\x27\x52\x73\x0d\x50\x97\xba\xcf\xa5\xbf\xff\ -\x7d\xe4\xe9\x42\x69\xcf\xac\xfb\x79\xe3\xfa\xac\x52\xa9\xb6\x54\ -\xd7\xfb\x8d\xc6\x24\x2b\x7f\xa4\xff\x05\x58\x08\x0d\x47\xb7\x67\ -\x88\x76\x77\xca\x74\xfa\xe6\x17\x92\x9e\x9c\x37\xae\x6f\xbb\xe1\ -\xc4\xda\x9a\xdf\x58\xb2\x62\xe9\x23\xfe\x6f\xd7\xa2\x36\x5f\x9c\ -\x2f\xb0\x40\xc4\x6b\x6b\x44\x9a\x13\x99\xc9\x1f\xc7\xde\x89\x15\ -\x4a\x77\xdf\x70\x00\x94\x52\x0d\x31\x68\x39\x12\x31\x03\xa9\x55\ -\xcb\xb7\x35\x18\xd3\xaa\x7c\x8b\x9d\xce\x53\x9d\xce\x83\xd1\x98\ -\xc5\x09\x74\x43\x0c\xab\x95\x4c\x66\x72\x63\x17\xae\x5e\x3f\x78\ -\x08\xde\x05\xa6\x80\x29\xb9\x93\x03\xa5\x54\x04\x68\x01\x16\x03\ -\x4d\xbd\xd0\xb5\x49\xeb\x8d\x5d\x9e\xe9\x6b\x69\x69\x68\x6b\x6c\ -\x5c\xd4\x58\xf5\xad\xbd\x99\xc9\x4d\xdf\x98\xce\x5f\x1b\xf5\xed\ -\x97\xe7\xe0\x93\x71\x48\xcf\xc2\x81\x8c\x88\x64\xef\xe8\x60\x46\ -\xa8\x1e\xb8\x67\x26\xea\x81\x38\x50\x07\x18\x40\x11\xec\x3b\x15\ -\xa0\x0c\xe4\x81\x1c\x70\x0b\xc8\xca\xec\x8c\x98\x39\xfe\x01\x76\ -\x95\xba\xf1\x06\x3a\xff\x81\x00\x00\x00\x00\x49\x45\x4e\x44\xae\ -\x42\x60\x82\ -" - -qt_resource_name = "\ -\x00\x06\ -\x07\x03\x7d\xc3\ -\x00\x69\ -\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\ -\x00\x0e\ -\x05\xcd\xf4\xe7\ -\x00\x63\ -\x00\x6f\x00\x6e\x00\x6e\x00\x5f\x00\x65\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x13\ -\x09\xd2\x6c\x67\ -\x00\x45\ -\x00\x6d\x00\x62\x00\x6c\x00\x65\x00\x6d\x00\x2d\x00\x71\x00\x75\x00\x65\x00\x73\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x2e\x00\x70\ -\x00\x6e\x00\x67\ -\x00\x12\ -\x04\xe4\x91\x47\ -\x00\x63\ -\x00\x6f\x00\x6e\x00\x6e\x00\x5f\x00\x63\x00\x6f\x00\x6e\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\ -\x00\x67\ -\x00\x13\ -\x0d\x76\x37\xc7\ -\x00\x63\ -\x00\x6f\x00\x6e\x00\x6e\x00\x5f\x00\x63\x00\x6f\x00\x6e\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x69\x00\x6e\x00\x67\x00\x2e\x00\x70\ -\x00\x6e\x00\x67\ -\x00\x14\ -\x00\xe9\x23\x87\ -\x00\x6c\ -\x00\x65\x00\x61\x00\x70\x00\x2d\x00\x63\x00\x6f\x00\x6c\x00\x6f\x00\x72\x00\x2d\x00\x73\x00\x6d\x00\x61\x00\x6c\x00\x6c\x00\x2e\ -\x00\x70\x00\x6e\x00\x67\ -\x00\x11\ -\x06\x1a\x44\xa7\ -\x00\x44\ -\x00\x69\x00\x61\x00\x6c\x00\x6f\x00\x67\x00\x2d\x00\x61\x00\x63\x00\x63\x00\x65\x00\x70\x00\x74\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\ -\x00\x10\ -\x0f\xc3\x90\x67\ -\x00\x44\ -\x00\x69\x00\x61\x00\x6c\x00\x6f\x00\x67\x00\x2d\x00\x65\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x2e\x00\x70\x00\x6e\x00\x67\ -" - -qt_resource_struct = "\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x07\x00\x00\x00\x02\ -\x00\x00\x00\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x0f\x03\ -\x00\x00\x00\x60\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x89\ -\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x00\xe4\x00\x00\x00\x00\x00\x01\x00\x00\x36\x7b\ -\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x05\x99\ -\x00\x00\x00\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x37\ -\x00\x00\x01\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x3b\xa3\ -" - -def qInitResources(): - QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -def qCleanupResources(): - QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() diff --git a/src/leap/gui/progress.py b/src/leap/gui/progress.py deleted file mode 100644 index ca4f6cc3..00000000 --- a/src/leap/gui/progress.py +++ /dev/null @@ -1,488 +0,0 @@ -""" -classes used in progress pages -from first run wizard -""" -try: - from collections import OrderedDict -except ImportError: # pragma: no cover - # We must be in 2.6 - from leap.util.dicts import OrderedDict - -import logging - -from PyQt4 import QtCore -from PyQt4 import QtGui - -from leap.gui.threads import FunThread - -from leap.gui import mainwindow_rc - -ICON_CHECKMARK = ":/images/Dialog-accept.png" -ICON_FAILED = ":/images/Dialog-error.png" -ICON_WAITING = ":/images/Emblem-question.png" - -logger = logging.getLogger(__name__) - - -class ImgWidget(QtGui.QWidget): - - # XXX move to widgets - - def __init__(self, parent=None, img=None): - super(ImgWidget, self).__init__(parent) - self.pic = QtGui.QPixmap(img) - - def paintEvent(self, event): - painter = QtGui.QPainter(self) - painter.drawPixmap(0, 0, self.pic) - - -class ProgressStep(object): - """ - Data model for sequential steps - to be used in a progress page in - connection wizard - """ - NAME = 0 - DONE = 1 - - def __init__(self, stepname, done, index=None): - """ - @param step: the name of the step - @type step: str - @param done: whether is completed or not - @type done: bool - """ - self.index = int(index) if index else 0 - self.name = unicode(stepname) - self.done = bool(done) - - @classmethod - def columns(self): - return ('name', 'done') - - -class ProgressStepContainer(object): - """ - a container for ProgressSteps objects - access data in the internal dict - """ - - def __init__(self): - self.dirty = False - self.steps = {} - - def step(self, identity): - return self.steps.get(identity, None) - - def addStep(self, step): - self.steps[step.index] = step - - def removeStep(self, step): - if step and self.steps.get(step.index, None): - del self.steps[step.index] - del step - self.dirty = True - - def removeAllSteps(self): - for item in iter(self): - self.removeStep(item) - - @property - def columns(self): - return ProgressStep.columns() - - def __len__(self): - return len(self.steps) - - def __iter__(self): - for step in self.steps.values(): - yield step - - -class StepsTableWidget(QtGui.QTableWidget): - """ - initializes a TableWidget - suitable for our display purposes, like removing - header info and grid display - """ - - def __init__(self, parent=None): - super(StepsTableWidget, self).__init__(parent=parent) - - # remove headers and all edit/select behavior - self.horizontalHeader().hide() - self.verticalHeader().hide() - self.setEditTriggers( - QtGui.QAbstractItemView.NoEditTriggers) - self.setSelectionMode( - QtGui.QAbstractItemView.NoSelection) - width = self.width() - - # WTF? Here init width is 100... - # but on populating is 456... :( - #logger.debug('init table. width=%s' % width) - - # XXX do we need this initial? - self.horizontalHeader().resizeSection(0, width * 0.7) - - # this disables the table grid. - # we should add alignment to the ImgWidget (it's top-left now) - self.setShowGrid(False) - self.setFocusPolicy(QtCore.Qt.NoFocus) - #self.setStyleSheet("QTableView{outline: 0;}") - - # XXX change image for done to rc - - # Note about the "done" status painting: - # - # XXX currently we are setting the CellWidget - # for the whole table on a per-row basis - # (on add_status_line method on ValidationPage). - # However, a more generic solution might be - # to implement a custom Delegate that overwrites - # the paint method (so it paints a checked tickmark if - # done is True and some other thing if checking or false). - # What we have now is quick and works because - # I'm supposing that on first fail we will - # go back to previous wizard page to signal the failure. - # A more generic solution could be used for - # some failing tests if they are not critical. - - -class WithStepsMixIn(object): - """ - This Class is a mixin that can be inherited - by InlineValidation pages (which will display - a progress steps widget in the same page as the form) - or by Validation Pages (which will only display - the progress steps in the page, below a progress bar widget) - """ - STEPS_TIMER_MS = 100 - - # - # methods related to worker threads - # launched for individual checks - # - - def setupStepsProcessingQueue(self): - """ - should be called from the init method - of the derived classes - """ - self.steps_queue = Queue.Queue() - self.stepscheck_timer = QtCore.QTimer() - self.stepscheck_timer.timeout.connect(self.processStepsQueue) - self.stepscheck_timer.start(self.STEPS_TIMER_MS) - # we need to keep a reference to child threads - self.threads = [] - - def do_checks(self): - """ - main entry point for checks. - it calls _do_checks in derived classes, - and it expects it to be a generator - yielding a tuple in the form (("message", progress_int), checkfunction) - """ - - # yo dawg, I heard you like checks - # so I put a __do_checks in your do_checks - # for calling others' _do_checks - - def __do_checks(fun=None, queue=None): - - for checkcase in fun(): # pragma: no cover - checkmsg, checkfun = checkcase - - queue.put(checkmsg) - if checkfun() is False: - queue.put("failed") - break - - t = FunThread(fun=partial( - __do_checks, - fun=self._do_checks, - queue=self.steps_queue)) - if hasattr(self, 'on_checks_validation_ready'): - t.finished.connect(self.on_checks_validation_ready) - t.begin() - self.threads.append(t) - - def processStepsQueue(self): - """ - consume steps queue - and pass messages - to the ui updater functions - """ - while self.steps_queue.qsize(): - try: - status = self.steps_queue.get(0) - if status == "failed": - self.set_failed_icon() - else: - self.onStepStatusChanged(*status) - except Queue.Empty: # pragma: no cover - pass - - def fail(self, err=None): - """ - return failed state - and send error notification as - a nice side effect. this function is called from - the _do_checks check functions returned in the - generator. - """ - wizard = self.wizard() - senderr = lambda err: wizard.set_validation_error( - self.current_page, err) - self.set_undone() - if err: - senderr(err) - return False - - @QtCore.pyqtSlot() - def launch_checks(self): - self.do_checks() - - # (gui) presentation stuff begins ##################### - - # slot - #@QtCore.pyqtSlot(str, int) - def onStepStatusChanged(self, status, progress=None): - status = unicode(status) - if status not in ("head_sentinel", "end_sentinel"): - self.add_status_line(status) - if status in ("end_sentinel"): - #self.checks_finished = True - self.set_checked_icon() - if progress and hasattr(self, 'progress'): - self.progress.setValue(progress) - self.progress.update() - - def setupSteps(self): - self.steps = ProgressStepContainer() - # steps table widget - if isinstance(self, QtCore.QObject): - parent = self - else: - parent = None - self.stepsTableWidget = StepsTableWidget(parent=parent) - zeros = (0, 0, 0, 0) - self.stepsTableWidget.setContentsMargins(*zeros) - self.errors = OrderedDict() - - def set_error(self, name, error): - self.errors[name] = error - - def pop_first_error(self): - errkey, errval = list(reversed(self.errors.items())).pop() - del self.errors[errkey] - return errkey, errval - - def clean_errors(self): - self.errors = OrderedDict() - - def clean_wizard_errors(self, pagename=None): - if pagename is None: # pragma: no cover - pagename = getattr(self, 'prev_page', None) - if pagename is None: # pragma: no cover - return - #logger.debug('cleaning wizard errors for %s' % pagename) - self.wizard().set_validation_error(pagename, None) - - def populateStepsTable(self): - # from examples, - # but I guess it's not needed to re-populate - # the whole table. - table = self.stepsTableWidget - table.setRowCount(len(self.steps)) - columns = self.steps.columns - table.setColumnCount(len(columns)) - - for row, step in enumerate(self.steps): - item = QtGui.QTableWidgetItem(step.name) - item.setData(QtCore.Qt.UserRole, - long(id(step))) - table.setItem(row, columns.index('name'), item) - table.setItem(row, columns.index('done'), - QtGui.QTableWidgetItem(step.done)) - self.resizeTable() - self.update() - - def clearTable(self): - # ??? -- not sure what's the difference - #self.stepsTableWidget.clear() - self.stepsTableWidget.clearContents() - - def resizeTable(self): - # resize first column to ~80% - table = self.stepsTableWidget - FIRST_COLUMN_PERCENT = 0.70 - width = table.width() - #logger.debug('populate table. width=%s' % width) - table.horizontalHeader().resizeSection(0, width * FIRST_COLUMN_PERCENT) - - def set_item_icon(self, img=ICON_CHECKMARK, current=True): - """ - mark the last item - as done - """ - # setting cell widget. - # see note on StepsTableWidget about plans to - # change this for a better solution. - if not hasattr(self, 'steps'): - return - index = len(self.steps) - table = self.stepsTableWidget - _index = index - 1 if current else index - 2 - table.setCellWidget( - _index, - ProgressStep.DONE, - ImgWidget(img=img)) - table.update() - - def set_failed_icon(self): - self.set_item_icon(img=ICON_FAILED, current=True) - - def set_checking_icon(self): - self.set_item_icon(img=ICON_WAITING, current=True) - - def set_checked_icon(self, current=True): - self.set_item_icon(current=current) - - def add_status_line(self, message): - """ - adds a new status line - and mark the next-to-last item - as done - """ - index = len(self.steps) - step = ProgressStep(message, False, index=index) - self.steps.addStep(step) - self.populateStepsTable() - self.set_checking_icon() - self.set_checked_icon(current=False) - - # Sets/unsets done flag - # for isComplete checks - - def set_done(self): - self.done = True - self.completeChanged.emit() - - def set_undone(self): - self.done = False - self.completeChanged.emit() - - def is_done(self): - return self.done - - # convenience for going back and forth - # in the wizard pages. - - def go_back(self): - self.wizard().back() - - def go_next(self): - self.wizard().next() - - -""" -We will use one base class for the intermediate pages -and another one for the in-page validations, both sharing the creation -of the tablewidgets. -The logic of this split comes from where I was trying to solve -the ui update using signals, but now that it's working well with -queues I could join them again. -""" - -import Queue -from functools import partial - - -class InlineValidationPage(QtGui.QWizardPage, WithStepsMixIn): - - def __init__(self, parent=None): - super(InlineValidationPage, self).__init__(parent) - self.setupStepsProcessingQueue() - self.done = False - - # slot - - @QtCore.pyqtSlot() - def showStepsFrame(self): - self.valFrame.show() - self.update() - - # progress frame - - def setupValidationFrame(self): - qframe = QtGui.QFrame - valFrame = qframe() - valFrame.setFrameStyle(qframe.NoFrame) - valframeLayout = QtGui.QVBoxLayout() - zeros = (0, 0, 0, 0) - valframeLayout.setContentsMargins(*zeros) - - valframeLayout.addWidget(self.stepsTableWidget) - valFrame.setLayout(valframeLayout) - self.valFrame = valFrame - - -class ValidationPage(QtGui.QWizardPage, WithStepsMixIn): - """ - class to be used as an intermediate - between two pages in a wizard. - shows feedback to the user and goes back if errors, - goes forward if ok. - initializePage triggers a one shot timer - that calls do_checks. - Derived classes should implement - _do_checks and - _do_validation - """ - - # signals - stepChanged = QtCore.pyqtSignal([str, int]) - - def __init__(self, parent=None): - super(ValidationPage, self).__init__(parent) - self.setupSteps() - #self.connect_step_status() - - layout = QtGui.QVBoxLayout() - self.progress = QtGui.QProgressBar(self) - layout.addWidget(self.progress) - layout.addWidget(self.stepsTableWidget) - - self.setLayout(layout) - self.layout = layout - - self.timer = QtCore.QTimer() - self.done = False - - self.setupStepsProcessingQueue() - - def isComplete(self): - return self.is_done() - - ######################## - - def show_progress(self): - self.progress.show() - self.stepsTableWidget.show() - - def hide_progress(self): - self.progress.hide() - self.stepsTableWidget.hide() - - # pagewizard methods. - # if overriden, child classes should call super. - - def initializePage(self): - self.clean_errors() - self.clean_wizard_errors() - self.steps.removeAllSteps() - self.clearTable() - self.resizeTable() - self.timer.singleShot(0, self.do_checks) diff --git a/src/leap/gui/styles.py b/src/leap/gui/styles.py deleted file mode 100644 index b482922e..00000000 --- a/src/leap/gui/styles.py +++ /dev/null @@ -1,16 +0,0 @@ -GreenLineEdit = "QLabel {color: green; font-weight: bold}" -ErrorLabelStyleSheet = """QLabel { color: red; font-weight: bold }""" -ErrorLineEdit = """QLineEdit { border: 1px solid red; }""" - - -# XXX this is bad. -# and you should feel bad for it. -# The original style has a sort of box color -# white/beige left-top/right-bottom or something like -# that. - -RegularLineEdit = """ -QLineEdit { - border: 1px solid black; -} -""" diff --git a/src/leap/gui/tests/__init__.py b/src/leap/gui/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/gui/tests/integration/fake_user_signup.py b/src/leap/gui/tests/integration/fake_user_signup.py deleted file mode 100644 index 78873749..00000000 --- a/src/leap/gui/tests/integration/fake_user_signup.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -simple server to test registration and -authentication - -To test: - -curl -d login=python_test_user -d password_salt=54321\ - -d password_verifier=12341234 \ - http://localhost:8000/users.json - -""" -from BaseHTTPServer import HTTPServer -from BaseHTTPServer import BaseHTTPRequestHandler -import cgi -import json -import urlparse - -HOST = "localhost" -PORT = 8000 - -LOGIN_ERROR = """{"errors":{"login":["has already been taken"]}}""" - -from leap.base.tests.test_providers import EXPECTED_DEFAULT_CONFIG - - -class request_handler(BaseHTTPRequestHandler): - responses = { - '/': ['ok\n'], - '/users.json': ['ok\n'], - '/timeout': ['ok\n'], - '/provider.json': ['%s\n' % json.dumps(EXPECTED_DEFAULT_CONFIG)] - } - - def do_GET(self): - path = urlparse.urlparse(self.path) - message = '\n'.join( - self.responses.get( - path.path, None)) - self.send_response(200) - self.end_headers() - self.wfile.write(message) - - def do_POST(self): - form = cgi.FieldStorage( - fp=self.rfile, - headers=self.headers, - environ={'REQUEST_METHOD': 'POST', - 'CONTENT_TYPE': self.headers['Content-Type'], - }) - data = dict( - (key, form[key].value) for key in form.keys()) - path = urlparse.urlparse(self.path) - message = '\n'.join( - self.responses.get( - path.path, '')) - - login = data.get('login', None) - #password_salt = data.get('password_salt', None) - #password_verifier = data.get('password_verifier', None) - - if path.geturl() == "/timeout": - print 'timeout' - self.send_response(200) - self.end_headers() - self.wfile.write(message) - import time - time.sleep(10) - return - - ok = True if (login == "python_test_user") else False - if ok: - self.send_response(200) - self.end_headers() - self.wfile.write(message) - - else: - self.send_response(500) - self.end_headers() - self.wfile.write(LOGIN_ERROR) - - -if __name__ == "__main__": - server = HTTPServer((HOST, PORT), request_handler) - server.serve_forever() diff --git a/src/leap/gui/tests/test_firstrun_login.py b/src/leap/gui/tests/test_firstrun_login.py deleted file mode 100644 index 6c45b8ef..00000000 --- a/src/leap/gui/tests/test_firstrun_login.py +++ /dev/null @@ -1,212 +0,0 @@ -import sys -import unittest - -import mock - -from leap.testing import qunittest -#from leap.testing import pyqt - -from PyQt4 import QtGui -#from PyQt4 import QtCore -#import PyQt4.QtCore # some weirdness with mock module - -from PyQt4.QtTest import QTest -from PyQt4.QtCore import Qt - -from leap.gui import firstrun - -try: - from collections import OrderedDict -except ImportError: - # We must be in 2.6 - from leap.util.dicts import OrderedDict - - -class TestPage(firstrun.login.LogInPage): - pass - - -class LogInPageLogicTestCase(qunittest.TestCase): - - # XXX can spy on signal connections - __name__ = "register user page logic tests" - - def setUp(self): - self.app = QtGui.QApplication(sys.argv) - QtGui.qApp = self.app - self.page = TestPage(None) - self.page.wizard = mock.MagicMock() - - def tearDown(self): - QtGui.qApp = None - self.app = None - self.page = None - - def test__do_checks(self): - eq = self.assertEqual - - self.page.userNameLineEdit.setText('testuser@domain') - self.page.userPasswordLineEdit.setText('testpassword') - - # fake register process - with mock.patch('leap.base.auth.LeapSRPRegister') as mockAuth: - mockSignup = mock.MagicMock() - - reqMockup = mock.Mock() - # XXX should inject bad json to get error - reqMockup.content = '{"errors": null}' - mockSignup.register_user.return_value = (True, reqMockup) - mockAuth.return_value = mockSignup - checks = [x for x in self.page._do_checks()] - - eq(len(checks), 4) - labels = [str(x) for (x, y), z in checks] - eq(labels, ['head_sentinel', - 'Resolving domain name', - 'Validating credentials', - 'end_sentinel']) - progress = [y for (x, y), z in checks] - eq(progress, [0, 20, 60, 100]) - - # normal run, ie, no exceptions - - checkfuns = [z for (x, y), z in checks] - checkusername, resolvedomain, valcreds = checkfuns[:-1] - - self.assertTrue(checkusername()) - #self.mocknetchecker.check_name_resolution.assert_called_with( - #'test_provider1') - - self.assertTrue(resolvedomain()) - #self.mockpcertchecker.is_https_working.assert_called_with( - #"https://test_provider1", verify=True) - - self.assertTrue(valcreds()) - - # XXX missing: inject failing exceptions - # XXX TODO make it break - - -class RegisterUserPageUITestCase(qunittest.TestCase): - - # XXX can spy on signal connections - __name__ = "Register User Page UI tests" - - def setUp(self): - self.app = QtGui.QApplication(sys.argv) - QtGui.qApp = self.app - - self.pagename = "signup" - pages = OrderedDict(( - (self.pagename, TestPage), - ('providersetupvalidation', - firstrun.connect.ConnectionPage))) - self.wizard = firstrun.wizard.FirstRunWizard(None, pages_dict=pages) - self.page = self.wizard.page(self.wizard.get_page_index(self.pagename)) - - self.page.do_checks = mock.Mock() - - # wizard would do this for us - self.page.initializePage() - - def tearDown(self): - QtGui.qApp = None - self.app = None - self.wizard = None - - # XXX refactor out - def fill_field(self, field, text): - """ - fills a field (line edit) that is passed along - :param field: the qLineEdit - :param text: the text to be filled - :type field: QLineEdit widget - :type text: str - """ - keyp = QTest.keyPress - field.setFocus(True) - for c in text: - keyp(field, c) - self.assertEqual(field.text(), text) - - def del_field(self, field): - """ - deletes entried text in - field line edit - :param field: the QLineEdit - :type field: QLineEdit widget - """ - keyp = QTest.keyPress - for c in range(len(field.text())): - keyp(field, Qt.Key_Backspace) - self.assertEqual(field.text(), "") - - def test_buttons_disabled_until_textentry(self): - # it's a commit button this time - nextbutton = self.wizard.button(QtGui.QWizard.CommitButton) - - self.assertFalse(nextbutton.isEnabled()) - - f_username = self.page.userNameLineEdit - f_password = self.page.userPasswordLineEdit - - self.fill_field(f_username, "testuser") - self.fill_field(f_password, "testpassword") - - # commit should be enabled - # XXX Need a workaround here - # because the isComplete is not being evaluated... - # (no event loop running??) - #import ipdb;ipdb.set_trace() - #self.assertTrue(nextbutton.isEnabled()) - self.assertTrue(self.page.isComplete()) - - self.del_field(f_username) - self.del_field(f_password) - - # after rm fields commit button - # should be disabled again - #self.assertFalse(nextbutton.isEnabled()) - self.assertFalse(self.page.isComplete()) - - def test_validate_page(self): - self.assertFalse(self.page.validatePage()) - # XXX TODO MOAR CASES... - # add errors, False - # change done, False - # not done, do_checks called - # click confirm, True - # done and do_confirm, True - - def test_next_id(self): - self.assertEqual(self.page.nextId(), 1) - - def test_paint_event(self): - self.page.populateErrors = mock.Mock() - self.page.paintEvent(None) - self.page.populateErrors.assert_called_with() - - def test_validation_ready(self): - f_username = self.page.userNameLineEdit - f_password = self.page.userPasswordLineEdit - - self.fill_field(f_username, "testuser") - self.fill_field(f_password, "testpassword") - - self.page.done = True - self.page.on_checks_validation_ready() - self.assertFalse(f_username.isEnabled()) - self.assertFalse(f_password.isEnabled()) - - self.assertEqual(self.page.validationMsg.text(), - "Credentials validated.") - self.assertEqual(self.page.do_confirm_next, True) - - def test_regex(self): - # XXX enter invalid username with key presses - # check text is not updated - pass - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/gui/tests/test_firstrun_providerselect.py b/src/leap/gui/tests/test_firstrun_providerselect.py deleted file mode 100644 index 18d89010..00000000 --- a/src/leap/gui/tests/test_firstrun_providerselect.py +++ /dev/null @@ -1,203 +0,0 @@ -import sys -import unittest - -import mock - -from leap.testing import qunittest -#from leap.testing import pyqt - -from PyQt4 import QtGui -#from PyQt4 import QtCore -#import PyQt4.QtCore # some weirdness with mock module - -from PyQt4.QtTest import QTest -from PyQt4.QtCore import Qt - -from leap.gui import firstrun - -try: - from collections import OrderedDict -except ImportError: - # We must be in 2.6 - from leap.util.dicts import OrderedDict - - -class TestPage(firstrun.providerselect.SelectProviderPage): - pass - - -class SelectProviderPageLogicTestCase(qunittest.TestCase): - - # XXX can spy on signal connections - - def setUp(self): - self.app = QtGui.QApplication(sys.argv) - QtGui.qApp = self.app - self.page = TestPage(None) - self.page.wizard = mock.MagicMock() - - mocknetchecker = mock.Mock() - self.page.wizard().netchecker.return_value = mocknetchecker - self.mocknetchecker = mocknetchecker - - mockpcertchecker = mock.Mock() - self.page.wizard().providercertchecker.return_value = mockpcertchecker - self.mockpcertchecker = mockpcertchecker - - mockeipconfchecker = mock.Mock() - self.page.wizard().eipconfigchecker.return_value = mockeipconfchecker - self.mockeipconfchecker = mockeipconfchecker - - def tearDown(self): - QtGui.qApp = None - self.app = None - self.page = None - - def test__do_checks(self): - eq = self.assertEqual - - self.page.providerNameEdit.setText('test_provider1') - - checks = [x for x in self.page._do_checks()] - eq(len(checks), 5) - labels = [str(x) for (x, y), z in checks] - eq(labels, ['head_sentinel', - 'Checking if it is a valid provider', - 'Checking for a secure connection', - 'Getting info from the provider', - 'end_sentinel']) - progress = [y for (x, y), z in checks] - eq(progress, [0, 20, 40, 80, 100]) - - # normal run, ie, no exceptions - - checkfuns = [z for (x, y), z in checks] - namecheck, httpscheck, fetchinfo = checkfuns[1:-1] - - self.assertTrue(namecheck()) - self.mocknetchecker.check_name_resolution.assert_called_with( - 'test_provider1') - - self.assertTrue(httpscheck()) - self.mockpcertchecker.is_https_working.assert_called_with( - "https://test_provider1", verify=True) - - self.assertTrue(fetchinfo()) - self.mockeipconfchecker.fetch_definition.assert_called_with( - domain="test_provider1") - - # XXX missing: inject failing exceptions - # XXX TODO make it break - - -class SelectProviderPageUITestCase(qunittest.TestCase): - - # XXX can spy on signal connections - __name__ = "Select Provider Page UI tests" - - def setUp(self): - self.app = QtGui.QApplication(sys.argv) - QtGui.qApp = self.app - - self.pagename = "providerselection" - pages = OrderedDict(( - (self.pagename, TestPage), - ('providerinfo', - firstrun.providerinfo.ProviderInfoPage))) - self.wizard = firstrun.wizard.FirstRunWizard(None, pages_dict=pages) - self.page = self.wizard.page(self.wizard.get_page_index(self.pagename)) - - self.page.do_checks = mock.Mock() - - # wizard would do this for us - self.page.initializePage() - - def tearDown(self): - QtGui.qApp = None - self.app = None - self.wizard = None - - def fill_provider(self): - """ - fills provider line edit - """ - keyp = QTest.keyPress - pedit = self.page.providerNameEdit - pedit.setFocus(True) - for c in "testprovider": - keyp(pedit, c) - self.assertEqual(pedit.text(), "testprovider") - - def del_provider(self): - """ - deletes entried provider in - line edit - """ - keyp = QTest.keyPress - pedit = self.page.providerNameEdit - for c in range(len("testprovider")): - keyp(pedit, Qt.Key_Backspace) - self.assertEqual(pedit.text(), "") - - def test_buttons_disabled_until_textentry(self): - nextbutton = self.wizard.button(QtGui.QWizard.NextButton) - checkbutton = self.page.providerCheckButton - - self.assertFalse(nextbutton.isEnabled()) - self.assertFalse(checkbutton.isEnabled()) - - self.fill_provider() - # checkbutton should be enabled - self.assertTrue(checkbutton.isEnabled()) - self.assertFalse(nextbutton.isEnabled()) - - self.del_provider() - # after rm provider checkbutton disabled again - self.assertFalse(checkbutton.isEnabled()) - self.assertFalse(nextbutton.isEnabled()) - - def test_check_button_triggers_tests(self): - checkbutton = self.page.providerCheckButton - self.assertFalse(checkbutton.isEnabled()) - self.assertFalse(self.page.do_checks.called) - - self.fill_provider() - - self.assertTrue(checkbutton.isEnabled()) - mclick = QTest.mouseClick - # click! - mclick(checkbutton, Qt.LeftButton) - self.waitFor(seconds=0.1) - self.assertTrue(self.page.do_checks.called) - - # XXX - # can play with different side_effects for do_checks mock... - # so we can see what happens with errors and so on - - def test_page_completed_after_checks(self): - nextbutton = self.wizard.button(QtGui.QWizard.NextButton) - self.assertFalse(nextbutton.isEnabled()) - - self.assertFalse(self.page.isComplete()) - self.fill_provider() - # simulate checks done - self.page.done = True - self.page.on_checks_validation_ready() - self.assertTrue(self.page.isComplete()) - # cannot test for nexbutton enabled - # cause it's the the wizard loop - # that would do that I think - - def test_validate_page(self): - self.assertTrue(self.page.validatePage()) - - def test_next_id(self): - self.assertEqual(self.page.nextId(), 1) - - def test_paint_event(self): - self.page.populateErrors = mock.Mock() - self.page.paintEvent(None) - self.page.populateErrors.assert_called_with() - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/gui/tests/test_firstrun_register.py b/src/leap/gui/tests/test_firstrun_register.py deleted file mode 100644 index 9d62f808..00000000 --- a/src/leap/gui/tests/test_firstrun_register.py +++ /dev/null @@ -1,244 +0,0 @@ -import sys -import unittest - -import mock - -from leap.testing import qunittest -#from leap.testing import pyqt - -from PyQt4 import QtGui -#from PyQt4 import QtCore -#import PyQt4.QtCore # some weirdness with mock module - -from PyQt4.QtTest import QTest -from PyQt4.QtCore import Qt - -from leap.gui import firstrun - -try: - from collections import OrderedDict -except ImportError: - # We must be in 2.6 - from leap.util.dicts import OrderedDict - - -class TestPage(firstrun.register.RegisterUserPage): - - def field(self, field): - if field == "provider_domain": - return "testprovider" - - -class RegisterUserPageLogicTestCase(qunittest.TestCase): - - # XXX can spy on signal connections - __name__ = "register user page logic tests" - - def setUp(self): - self.app = QtGui.QApplication(sys.argv) - QtGui.qApp = self.app - self.page = TestPage(None) - self.page.wizard = mock.MagicMock() - - #mocknetchecker = mock.Mock() - #self.page.wizard().netchecker.return_value = mocknetchecker - #self.mocknetchecker = mocknetchecker -# - #mockpcertchecker = mock.Mock() - #self.page.wizard().providercertchecker.return_value = mockpcertchecker - #self.mockpcertchecker = mockpcertchecker -# - #mockeipconfchecker = mock.Mock() - #self.page.wizard().eipconfigchecker.return_value = mockeipconfchecker - #self.mockeipconfchecker = mockeipconfchecker - - def tearDown(self): - QtGui.qApp = None - self.app = None - self.page = None - - def test__do_checks(self): - eq = self.assertEqual - - self.page.userNameLineEdit.setText('testuser') - self.page.userPasswordLineEdit.setText('testpassword') - self.page.userPassword2LineEdit.setText('testpassword') - - # fake register process - with mock.patch('leap.base.auth.LeapSRPRegister') as mockAuth: - mockSignup = mock.MagicMock() - - reqMockup = mock.Mock() - # XXX should inject bad json to get error - reqMockup.content = '{"errors": null}' - mockSignup.register_user.return_value = (True, reqMockup) - mockAuth.return_value = mockSignup - checks = [x for x in self.page._do_checks()] - - eq(len(checks), 3) - labels = [str(x) for (x, y), z in checks] - eq(labels, ['head_sentinel', - 'Registering username', - 'end_sentinel']) - progress = [y for (x, y), z in checks] - eq(progress, [0, 40, 100]) - - # normal run, ie, no exceptions - - checkfuns = [z for (x, y), z in checks] - passcheck, register = checkfuns[:-1] - - self.assertTrue(passcheck()) - #self.mocknetchecker.check_name_resolution.assert_called_with( - #'test_provider1') - - self.assertTrue(register()) - #self.mockpcertchecker.is_https_working.assert_called_with( - #"https://test_provider1", verify=True) - - # XXX missing: inject failing exceptions - # XXX TODO make it break - - -class RegisterUserPageUITestCase(qunittest.TestCase): - - # XXX can spy on signal connections - __name__ = "Register User Page UI tests" - - def setUp(self): - self.app = QtGui.QApplication(sys.argv) - QtGui.qApp = self.app - - self.pagename = "signup" - pages = OrderedDict(( - (self.pagename, TestPage), - ('connect', - firstrun.connect.ConnectionPage))) - self.wizard = firstrun.wizard.FirstRunWizard(None, pages_dict=pages) - self.page = self.wizard.page(self.wizard.get_page_index(self.pagename)) - - self.page.do_checks = mock.Mock() - - # wizard would do this for us - self.page.initializePage() - - def tearDown(self): - QtGui.qApp = None - self.app = None - self.wizard = None - - def fill_field(self, field, text): - """ - fills a field (line edit) that is passed along - :param field: the qLineEdit - :param text: the text to be filled - :type field: QLineEdit widget - :type text: str - """ - keyp = QTest.keyPress - field.setFocus(True) - for c in text: - keyp(field, c) - self.assertEqual(field.text(), text) - - def del_field(self, field): - """ - deletes entried text in - field line edit - :param field: the QLineEdit - :type field: QLineEdit widget - """ - keyp = QTest.keyPress - for c in range(len(field.text())): - keyp(field, Qt.Key_Backspace) - self.assertEqual(field.text(), "") - - def test_buttons_disabled_until_textentry(self): - # it's a commit button this time - nextbutton = self.wizard.button(QtGui.QWizard.CommitButton) - - self.assertFalse(nextbutton.isEnabled()) - - f_username = self.page.userNameLineEdit - f_password = self.page.userPasswordLineEdit - f_passwor2 = self.page.userPassword2LineEdit - - self.fill_field(f_username, "testuser") - self.fill_field(f_password, "testpassword") - self.fill_field(f_passwor2, "testpassword") - - # commit should be enabled - # XXX Need a workaround here - # because the isComplete is not being evaluated... - # (no event loop running??) - #import ipdb;ipdb.set_trace() - #self.assertTrue(nextbutton.isEnabled()) - self.assertTrue(self.page.isComplete()) - - self.del_field(f_username) - self.del_field(f_password) - self.del_field(f_passwor2) - - # after rm fields commit button - # should be disabled again - #self.assertFalse(nextbutton.isEnabled()) - self.assertFalse(self.page.isComplete()) - - @unittest.skip - def test_check_button_triggers_tests(self): - checkbutton = self.page.providerCheckButton - self.assertFalse(checkbutton.isEnabled()) - self.assertFalse(self.page.do_checks.called) - - self.fill_provider() - - self.assertTrue(checkbutton.isEnabled()) - mclick = QTest.mouseClick - # click! - mclick(checkbutton, Qt.LeftButton) - self.waitFor(seconds=0.1) - self.assertTrue(self.page.do_checks.called) - - # XXX - # can play with different side_effects for do_checks mock... - # so we can see what happens with errors and so on - - def test_validate_page(self): - self.assertFalse(self.page.validatePage()) - # XXX TODO MOAR CASES... - # add errors, False - # change done, False - # not done, do_checks called - # click confirm, True - # done and do_confirm, True - - def test_next_id(self): - self.assertEqual(self.page.nextId(), 1) - - def test_paint_event(self): - self.page.populateErrors = mock.Mock() - self.page.paintEvent(None) - self.page.populateErrors.assert_called_with() - - def test_validation_ready(self): - f_username = self.page.userNameLineEdit - f_password = self.page.userPasswordLineEdit - f_passwor2 = self.page.userPassword2LineEdit - - self.fill_field(f_username, "testuser") - self.fill_field(f_password, "testpassword") - self.fill_field(f_passwor2, "testpassword") - - self.page.done = True - self.page.on_checks_validation_ready() - self.assertFalse(f_username.isEnabled()) - self.assertFalse(f_password.isEnabled()) - self.assertFalse(f_passwor2.isEnabled()) - - self.assertEqual(self.page.validationMsg.text(), - "Registration succeeded!") - self.assertEqual(self.page.do_confirm_next, True) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/gui/tests/test_firstrun_wizard.py b/src/leap/gui/tests/test_firstrun_wizard.py deleted file mode 100644 index 395604d3..00000000 --- a/src/leap/gui/tests/test_firstrun_wizard.py +++ /dev/null @@ -1,137 +0,0 @@ -import sys -import unittest - -import mock - -from leap.testing import qunittest -from leap.testing import pyqt - -from PyQt4 import QtGui -#from PyQt4 import QtCore -import PyQt4.QtCore # some weirdness with mock module - -from PyQt4.QtTest import QTest -#from PyQt4.QtCore import Qt - -from leap.gui import firstrun - - -class TestWizard(firstrun.wizard.FirstRunWizard): - pass - - -PAGES_DICT = dict(( - ('intro', firstrun.intro.IntroPage), - ('providerselection', - firstrun.providerselect.SelectProviderPage), - ('login', firstrun.login.LogInPage), - ('providerinfo', firstrun.providerinfo.ProviderInfoPage), - ('providersetupvalidation', - firstrun.providersetup.ProviderSetupValidationPage), - ('signup', firstrun.register.RegisterUserPage), - ('connect', - firstrun.connect.ConnectionPage), - ('lastpage', firstrun.last.LastPage) -)) - - -mockQSettings = mock.MagicMock() -mockQSettings().setValue.return_value = True - -#PyQt4.QtCore.QSettings = mockQSettings - - -class FirstRunWizardTestCase(qunittest.TestCase): - - # XXX can spy on signal connections - - def setUp(self): - self.app = QtGui.QApplication(sys.argv) - QtGui.qApp = self.app - self.wizard = TestWizard(None) - - def tearDown(self): - QtGui.qApp = None - self.app = None - self.wizard = None - - def test_defaults(self): - self.assertEqual(self.wizard.pages_dict, PAGES_DICT) - - @mock.patch('PyQt4.QtCore.QSettings', mockQSettings) - def test_accept(self): - """ - test the main accept method - that gets called when user has gone - thru all the wizard and click on finish button - """ - - self.wizard.success_cb = mock.Mock() - self.wizard.success_cb.return_value = True - - # dummy values; we inject them in the field - # mocks (where wizard gets them) and then - # we check that they are passed to QSettings.setValue - field_returns = ["testuser", "1234", "testprovider", True] - - def field_side_effects(*args): - return field_returns.pop(0) - - self.wizard.field = mock.Mock(side_effect=field_side_effects) - self.wizard.get_random_str = mock.Mock() - RANDOMSTR = "thisisarandomstringTM" - self.wizard.get_random_str.return_value = RANDOMSTR - - # mocked settings (see decorator on this method) - mqs = PyQt4.QtCore.QSettings - - # go! call accept... - self.wizard.accept() - - # did settings().setValue get called with the proper - # arguments? - call = mock.call - calls = [call("FirstRunWizardDone", True), - call("provider_domain", "testprovider"), - call("remember_user_and_pass", True), - call("username", "testuser@testprovider"), - call("testprovider_seed", RANDOMSTR)] - mqs().setValue.assert_has_calls(calls, any_order=True) - - # assert success callback is success oh boy - self.wizard.success_cb.assert_called_with() - - def test_random_str(self): - r = self.wizard.get_random_str(42) - self.assertTrue(len(r) == 42) - - def test_page_index(self): - """ - we test both the get_page_index function - and the correct ordering of names - """ - # remember it's implemented as an ordered dict - - pagenames = ('intro', 'providerselection', 'login', 'providerinfo', - 'providersetupvalidation', 'signup', 'connect', - 'lastpage') - eq = self.assertEqual - w = self.wizard - for index, name in enumerate(pagenames): - eq(w.get_page_index(name), index) - - def test_validation_errors(self): - """ - tests getters and setters for validation errors - """ - page = "testpage" - eq = self.assertEqual - w = self.wizard - eq(w.get_validation_error(page), None) - w.set_validation_error(page, "error") - eq(w.get_validation_error(page), "error") - w.clean_validation_error(page) - eq(w.get_validation_error(page), None) - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/gui/tests/test_mainwindow_rc.py b/src/leap/gui/tests/test_mainwindow_rc.py deleted file mode 100644 index 9f5172f7..00000000 --- a/src/leap/gui/tests/test_mainwindow_rc.py +++ /dev/null @@ -1,32 +0,0 @@ -import unittest -import hashlib - -try: - import sip - sip.setapi('QVariant', 2) -except ValueError: - pass - -from leap.gui import mainwindow_rc - -# I have to admit that there's something -# perverse in testing this. -# Even though, I still think that it _is_ a good idea -# to put a check to avoid non-updated resources files. - -# so, if you came here because an updated resource -# did break a test, what you have to do is getting -# the md5 hash of your qt_resource_data and change it here. - -# annoying? yep. try making a script for that :P - - -class MainWindowResourcesTest(unittest.TestCase): - - def test_mainwindow_resources_hash(self): - self.assertEqual( - hashlib.md5(mainwindow_rc.qt_resource_data).hexdigest(), - 'e04cb467985ba38b9eb91e7689f9458f') - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/gui/tests/test_progress.py b/src/leap/gui/tests/test_progress.py deleted file mode 100644 index 1f9f9e38..00000000 --- a/src/leap/gui/tests/test_progress.py +++ /dev/null @@ -1,449 +0,0 @@ -from collections import namedtuple -import sys -import unittest -import Queue - -import mock - -from leap.testing import qunittest -from leap.testing import pyqt - -from PyQt4 import QtGui -from PyQt4 import QtCore -from PyQt4.QtTest import QTest -from PyQt4.QtCore import Qt - -from leap.gui import progress - - -class ProgressStepTestCase(unittest.TestCase): - - def test_step_attrs(self): - ps = progress.ProgressStep - step = ps('test', False, 1) - # instance - self.assertEqual(step.index, 1) - self.assertEqual(step.name, "test") - self.assertEqual(step.done, False) - step = ps('test2', True, 2) - self.assertEqual(step.index, 2) - self.assertEqual(step.name, "test2") - self.assertEqual(step.done, True) - - # class methods and attrs - self.assertEqual(ps.columns(), ('name', 'done')) - self.assertEqual(ps.NAME, 0) - self.assertEqual(ps.DONE, 1) - - -class ProgressStepContainerTestCase(unittest.TestCase): - def setUp(self): - self.psc = progress.ProgressStepContainer() - - def addSteps(self, number): - Step = progress.ProgressStep - for n in range(number): - self.psc.addStep(Step("%s" % n, False, n)) - - def test_attrs(self): - self.assertEqual(self.psc.columns, - ('name', 'done')) - - def test_add_steps(self): - Step = progress.ProgressStep - self.assertTrue(len(self.psc) == 0) - self.psc.addStep(Step('one', False, 0)) - self.assertTrue(len(self.psc) == 1) - self.psc.addStep(Step('two', False, 1)) - self.assertTrue(len(self.psc) == 2) - - def test_del_all_steps(self): - self.assertTrue(len(self.psc) == 0) - self.addSteps(5) - self.assertTrue(len(self.psc) == 5) - self.psc.removeAllSteps() - self.assertTrue(len(self.psc) == 0) - - def test_del_step(self): - Step = progress.ProgressStep - self.addSteps(5) - self.assertTrue(len(self.psc) == 5) - self.psc.removeStep(self.psc.step(4)) - self.assertTrue(len(self.psc) == 4) - self.psc.removeStep(self.psc.step(4)) - self.psc.removeStep(Step('none', False, 5)) - self.psc.removeStep(self.psc.step(4)) - - def test_iter(self): - self.addSteps(10) - self.assertEqual( - [x.index for x in self.psc], - [x for x in range(10)]) - - -class StepsTableWidgetTestCase(unittest.TestCase): - - def setUp(self): - self.app = QtGui.QApplication(sys.argv) - QtGui.qApp = self.app - self.stw = progress.StepsTableWidget() - - def tearDown(self): - QtGui.qApp = None - self.app = None - - def test_defaults(self): - self.assertTrue(isinstance(self.stw, QtGui.QTableWidget)) - self.assertEqual(self.stw.focusPolicy(), 0) - - -class TestWithStepsClass(QtGui.QWidget, progress.WithStepsMixIn): - - def __init__(self, parent=None): - super(TestWithStepsClass, self).__init__(parent=parent) - self.setupStepsProcessingQueue() - self.statuses = [] - self.current_page = "testpage" - - def onStepStatusChanged(self, *args): - """ - blank out this gui method - that will add status lines - """ - self.statuses.append(args) - - -class WithStepsMixInTestCase(qunittest.TestCase): - - TIMER_WAIT = 2 * progress.WithStepsMixIn.STEPS_TIMER_MS / 1000.0 - - # XXX can spy on signal connections - - def setUp(self): - self.app = QtGui.QApplication(sys.argv) - QtGui.qApp = self.app - self.stepy = TestWithStepsClass() - #self.connects = [] - #pyqt.enableSignalDebugging( - #connectCall=lambda *args: self.connects.append(args)) - #self.assertEqual(self.connects, []) - #self.stepy.stepscheck_timer.timeout.disconnect( - #self.stepy.processStepsQueue) - - def tearDown(self): - QtGui.qApp = None - self.app = None - - def test_has_queue(self): - s = self.stepy - self.assertTrue(hasattr(s, 'steps_queue')) - self.assertTrue(isinstance(s.steps_queue, Queue.Queue)) - self.assertTrue(isinstance(s.stepscheck_timer, QtCore.QTimer)) - - def test_do_checks_delegation(self): - s = self.stepy - - _do_checks = mock.Mock() - _do_checks.return_value = ( - (("test", 0), lambda: None), - (("test", 0), lambda: None)) - s._do_checks = _do_checks - s.do_checks() - self.waitFor(seconds=self.TIMER_WAIT) - _do_checks.assert_called_with() - self.assertEqual(len(s.statuses), 2) - - # test that a failed test interrupts the run - - s.statuses = [] - _do_checks = mock.Mock() - _do_checks.return_value = ( - (("test", 0), lambda: None), - (("test", 0), lambda: False), - (("test", 0), lambda: None)) - s._do_checks = _do_checks - s.do_checks() - self.waitFor(seconds=self.TIMER_WAIT) - _do_checks.assert_called_with() - self.assertEqual(len(s.statuses), 2) - - def test_process_queue(self): - s = self.stepy - q = s.steps_queue - s.set_failed_icon = mock.MagicMock() - with self.assertRaises(AssertionError): - q.put('foo') - self.waitFor(seconds=self.TIMER_WAIT) - s.set_failed_icon.assert_called_with() - q.put("failed") - self.waitFor(seconds=self.TIMER_WAIT) - s.set_failed_icon.assert_called_with() - - def test_on_checks_validation_ready_called(self): - s = self.stepy - s.on_checks_validation_ready = mock.MagicMock() - - _do_checks = mock.Mock() - _do_checks.return_value = ( - (("test", 0), lambda: None),) - s._do_checks = _do_checks - s.do_checks() - - self.waitFor(seconds=self.TIMER_WAIT) - s.on_checks_validation_ready.assert_called_with() - - def test_fail(self): - s = self.stepy - - s.wizard = mock.Mock() - wizard = s.wizard.return_value - wizard.set_validation_error.return_value = True - s.completeChanged = mock.Mock() - s.completeChanged.emit.return_value = True - - self.assertFalse(s.fail(err="foo")) - self.waitFor(seconds=self.TIMER_WAIT) - wizard.set_validation_error.assert_called_with('testpage', 'foo') - s.completeChanged.emit.assert_called_with() - - # with no args - s.wizard = mock.Mock() - wizard = s.wizard.return_value - wizard.set_validation_error.return_value = True - s.completeChanged = mock.Mock() - s.completeChanged.emit.return_value = True - - self.assertFalse(s.fail()) - self.waitFor(seconds=self.TIMER_WAIT) - with self.assertRaises(AssertionError): - wizard.set_validation_error.assert_called_with() - s.completeChanged.emit.assert_called_with() - - def test_done(self): - s = self.stepy - s.done = False - - s.completeChanged = mock.Mock() - s.completeChanged.emit.return_value = True - - self.assertFalse(s.is_done()) - s.set_done() - self.assertTrue(s.is_done()) - s.completeChanged.emit.assert_called_with() - - s.completeChanged = mock.Mock() - s.completeChanged.emit.return_value = True - s.set_undone() - self.assertFalse(s.is_done()) - - def test_back_and_next(self): - s = self.stepy - s.wizard = mock.Mock() - wizard = s.wizard.return_value - wizard.back.return_value = True - wizard.next.return_value = True - s.go_back() - wizard.back.assert_called_with() - s.go_next() - wizard.next.assert_called_with() - - def test_on_step_statuschanged_slot(self): - s = self.stepy - s.onStepStatusChanged = progress.WithStepsMixIn.onStepStatusChanged - s.add_status_line = mock.Mock() - s.set_checked_icon = mock.Mock() - s.progress = mock.Mock() - s.progress.setValue.return_value = True - s.progress.update.return_value = True - - s.onStepStatusChanged(s, "end_sentinel") - s.set_checked_icon.assert_called_with() - - s.onStepStatusChanged(s, "foo") - s.add_status_line.assert_called_with("foo") - - s.onStepStatusChanged(s, "bar", 42) - s.progress.setValue.assert_called_with(42) - s.progress.update.assert_called_with() - - def test_steps_and_errors(self): - s = self.stepy - s.setupSteps() - self.assertTrue(isinstance(s.steps, progress.ProgressStepContainer)) - self.assertEqual(s.errors, {}) - s.set_error('fooerror', 'barerror') - self.assertEqual(s.errors, {'fooerror': 'barerror'}) - s.set_error('2', 42) - self.assertEqual(s.errors, {'fooerror': 'barerror', '2': 42}) - fe = s.pop_first_error() - self.assertEqual(fe, ('fooerror', 'barerror')) - self.assertEqual(s.errors, {'2': 42}) - s.clean_errors() - self.assertEqual(s.errors, {}) - - def test_launch_chechs_slot(self): - s = self.stepy - s.do_checks = mock.Mock() - s.launch_checks() - s.do_checks.assert_called_with() - - def test_clean_wizard_errors(self): - s = self.stepy - s.wizard = mock.Mock() - wizard = s.wizard.return_value - wizard.set_validation_error.return_value = True - s.clean_wizard_errors(pagename="foopage") - wizard.set_validation_error.assert_called_with("foopage", None) - - def test_clear_table(self): - s = self.stepy - s.stepsTableWidget = mock.Mock() - s.stepsTableWidget.clearContents.return_value = True - s.clearTable() - s.stepsTableWidget.clearContents.assert_called_with() - - def test_populate_steps_table(self): - s = self.stepy - Step = namedtuple('Step', ['name', 'done']) - - class Steps(object): - columns = ("name", "done") - _items = (Step('step1', False), Step('step2', False)) - - def __len__(self): - return 2 - - def __iter__(self): - for i in self._items: - yield i - - s.steps = Steps() - - s.stepsTableWidget = mock.Mock() - s.stepsTableWidget.setItem.return_value = True - s.resizeTable = mock.Mock() - s.update = mock.Mock() - s.populateStepsTable() - s.update.assert_called_with() - s.resizeTable.assert_called_with() - - # assert stepsTableWidget.setItem called ... - # we do not want to get into the actual - # - call_list = s.stepsTableWidget.setItem.call_args_list - indexes = [(y, z) for y, z, xx in [x[0] for x in call_list]] - self.assertEqual(indexes, - [(0, 0), (0, 1), (1, 0), (1, 1)]) - - def test_add_status_line(self): - s = self.stepy - s.steps = progress.ProgressStepContainer() - s.stepsTableWidget = mock.Mock() - s.stepsTableWidget.width.return_value = 100 - s.set_item = mock.Mock() - s.set_item_icon = mock.Mock() - s.add_status_line("new status") - s.set_item_icon.assert_called_with(current=False) - - def test_set_item_icon(self): - s = self.stepy - s.steps = progress.ProgressStepContainer() - s.stepsTableWidget = mock.Mock() - s.stepsTableWidget.setCellWidget.return_value = True - s.stepsTableWidget.width.return_value = 100 - #s.set_item = mock.Mock() - #s.set_item_icon = mock.Mock() - s.add_status_line("new status") - s.add_status_line("new 2 status") - s.add_status_line("new 3 status") - call_list = s.stepsTableWidget.setCellWidget.call_args_list - indexes = [(y, z) for y, z, xx in [x[0] for x in call_list]] - self.assertEqual( - indexes, - [(0, 1), (-1, 1), (1, 1), (0, 1), (2, 1), (1, 1)]) - - -class TestInlineValidationPage(progress.InlineValidationPage): - pass - - -class InlineValidationPageTestCase(unittest.TestCase): - - def setUp(self): - self.app = QtGui.QApplication(sys.argv) - QtGui.qApp = self.app - self.page = TestInlineValidationPage() - - def tearDown(self): - QtGui.qApp = None - self.app = None - - def test_defaults(self): - self.assertFalse(self.page.done) - # if setupProcessingQueue was called - self.assertTrue(isinstance(self.page.stepscheck_timer, QtCore.QTimer)) - self.assertTrue(isinstance(self.page.steps_queue, Queue.Queue)) - - def test_validation_frame(self): - # test frame creation - self.page.stepsTableWidget = progress.StepsTableWidget( - parent=self.page) - self.page.setupValidationFrame() - self.assertTrue(isinstance(self.page.valFrame, QtGui.QFrame)) - - # test show steps calls frame.show - self.page.valFrame = mock.Mock() - self.page.valFrame.show.return_value = True - self.page.showStepsFrame() - self.page.valFrame.show.assert_called_with() - - -class TestValidationPage(progress.ValidationPage): - pass - - -class ValidationPageTestCase(unittest.TestCase): - - def setUp(self): - self.app = QtGui.QApplication(sys.argv) - QtGui.qApp = self.app - self.page = TestValidationPage() - - def tearDown(self): - QtGui.qApp = None - self.app = None - - def test_defaults(self): - self.assertFalse(self.page.done) - # if setupProcessingQueue was called - self.assertTrue(isinstance(self.page.timer, QtCore.QTimer)) - self.assertTrue(isinstance(self.page.stepscheck_timer, QtCore.QTimer)) - self.assertTrue(isinstance(self.page.steps_queue, Queue.Queue)) - - def test_is_complete(self): - self.assertFalse(self.page.isComplete()) - self.page.done = True - self.assertTrue(self.page.isComplete()) - self.page.done = False - self.assertFalse(self.page.isComplete()) - - def test_show_hide_progress(self): - p = self.page - p.progress = mock.Mock() - p.progress.show.return_code = True - p.show_progress() - p.progress.show.assert_called_with() - p.progress.hide.return_code = True - p.hide_progress() - p.progress.hide.assert_called_with() - - def test_initialize_page(self): - p = self.page - p.timer = mock.Mock() - p.timer.singleShot.return_code = True - p.initializePage() - p.timer.singleShot.assert_called_with(0, p.do_checks) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/gui/tests/test_threads.py b/src/leap/gui/tests/test_threads.py deleted file mode 100644 index 06c19606..00000000 --- a/src/leap/gui/tests/test_threads.py +++ /dev/null @@ -1,27 +0,0 @@ -import unittest - -import mock -from leap.gui import threads - - -class FunThreadTestCase(unittest.TestCase): - - def setUp(self): - self.fun = mock.MagicMock() - self.fun.return_value = "foo" - self.t = threads.FunThread(fun=self.fun) - - def test_thread(self): - self.t.begin() - self.t.wait() - self.fun.assert_called() - del self.t - - def test_run(self): - # this is called by PyQt - self.t.run() - del self.t - self.fun.assert_called() - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/gui/threads.py b/src/leap/gui/threads.py deleted file mode 100644 index 8aad8866..00000000 --- a/src/leap/gui/threads.py +++ /dev/null @@ -1,21 +0,0 @@ -from PyQt4 import QtCore - - -class FunThread(QtCore.QThread): - - def __init__(self, fun=None, parent=None): - - QtCore.QThread.__init__(self, parent) - self.exiting = False - self.fun = fun - - def __del__(self): - self.exiting = True - self.wait() - - def run(self): - if self.fun: - self.fun() - - def begin(self): - self.start() diff --git a/src/leap/gui/utils.py b/src/leap/gui/utils.py deleted file mode 100644 index f91ac3ef..00000000 --- a/src/leap/gui/utils.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -utility functions to work with gui objects -""" -from PyQt4 import QtCore - - -def layout_widgets(layout): - """ - return a generator with all widgets in a layout - """ - return (layout.itemAt(i) for i in range(layout.count())) - - -DELAY_MSECS = 50 - - -def delay(obj, method_str=None, call_args=None): - """ - Triggers a function or slot with a small delay. - this is a mainly a hack to get responsiveness in the ui - in cases in which the event loop freezes and the task - is not heavy enough to setup a processing queue. - """ - if callable(obj) and not method_str: - fun = lambda: obj() - - if method_str: - invoke = QtCore.QMetaObject.invokeMethod - if call_args: - fun = lambda: invoke(obj, method_str, call_args) - else: - fun = lambda: invoke(obj, method_str) - - QtCore.QTimer().singleShot(DELAY_MSECS, fun) diff --git a/src/leap/soledad/README b/src/leap/soledad/README deleted file mode 100644 index b14d5932..00000000 --- a/src/leap/soledad/README +++ /dev/null @@ -1,35 +0,0 @@ -Soledad -- Synchronization Of Locally Encrypted Data Among Devices -================================================================== - -This software is under development. - -Dependencies ------------- - -Soledad depends on the following python libraries: - - * u1db 0.1.4 [1] - * python-gnupg 0.3.1 [2] - * CouchDB 0.8 [3] - * hmac 20101005 [4] - -[1] http://pypi.python.org/pypi/u1db/0.1.4 -[2] http://pypi.python.org/pypi/python-gnupg/0.3.1 -[3] http://pypi.python.org/pypi/CouchDB/0.8 -[4] http://pypi.python.org/pypi/hmac/20101005 - - -Tests ------ - -Soledad's tests should be run with nose2, like this: - - nose2 leap.soledad.tests - -Right now, there are 3 conditions that have to be met for all Soledad tests to -pass without problems: - - 1. Use nose2. - 2. Have an http CouchDB instance running on `localhost:5984`. - 3. Have sqlcipher configured (using LD_PRELOAD or LD_LIBRARY_CONFIG to point - to the place where libsqlite3.so.0 is located). diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py deleted file mode 100644 index 4b7a12df..00000000 --- a/src/leap/soledad/__init__.py +++ /dev/null @@ -1,221 +0,0 @@ -# License? - -"""A U1DB implementation for using Object Stores as its persistence layer.""" - -import os -import string -import random -import hmac -from leap.soledad.backends import sqlcipher -from leap.soledad.util import GPGWrapper - - -class Soledad(object): - - # paths - PREFIX = os.environ['HOME'] + '/.config/leap/soledad' - SECRET_PATH = PREFIX + '/secret.gpg' - GNUPG_HOME = PREFIX + '/gnupg' - LOCAL_DB_PATH = PREFIX + '/soledad.u1db' - - # other configs - SECRET_LENGTH = 50 - - def __init__(self, user_email, gpghome=None, initialize=True, - prefix=None, secret_path=None, local_db_path=None): - self._user_email = user_email - self.PREFIX = prefix or self.PREFIX - self.SECRET_PATH = secret_path or self.SECRET_PATH - self.LOCAL_DB_PATH = local_db_path or self.LOCAL_DB_PATH - if not os.path.isdir(self.PREFIX): - os.makedirs(self.PREFIX) - self._gpg = GPGWrapper(gpghome=(gpghome or self.GNUPG_HOME)) - if initialize: - self._initialize() - - def _initialize(self): - # load/generate OpenPGP keypair - if not self._has_openpgp_keypair(): - self._gen_openpgp_keypair() - self._load_openpgp_keypair() - # load/generate secret - if not self._has_secret(): - self._gen_secret() - self._load_secret() - # instantiate u1db - # TODO: verify if secret for sqlcipher should be the same as the one - # for symmetric encryption. - self._db = sqlcipher.open(self.LOCAL_DB_PATH, True, self._secret, - soledad=self) - - def close(self): - self._db.close() - - #------------------------------------------------------------------------- - # Management of secret for symmetric encryption - #------------------------------------------------------------------------- - - def _has_secret(self): - """ - Verify if secret for symmetric encryption exists on local encrypted - file. - """ - # TODO: verify if file is a GPG-encrypted file and if we have the - # corresponding private key for decryption. - if os.path.isfile(self.SECRET_PATH): - return True - return False - - def _load_secret(self): - """ - Load secret for symmetric encryption from local encrypted file. - """ - try: - with open(self.SECRET_PATH) as f: - self._secret = str(self._gpg.decrypt(f.read())) - except IOError: - raise IOError('Failed to open secret file %s.' % self.SECRET_PATH) - - def _gen_secret(self): - """ - Generate a secret for symmetric encryption and store in a local - encrypted file. - """ - self._secret = ''.join(random.choice(string.ascii_uppercase + - string.digits) for x in - range(self.SECRET_LENGTH)) - ciphertext = self._gpg.encrypt(self._secret, self._fingerprint, - self._fingerprint) - f = open(self.SECRET_PATH, 'w') - f.write(str(ciphertext)) - f.close() - - #------------------------------------------------------------------------- - # Management of OpenPGP keypair - #------------------------------------------------------------------------- - - def _has_openpgp_keypair(self): - """ - Verify if there exists an OpenPGP keypair for this user. - """ - # TODO: verify if we have the corresponding private key. - try: - self._gpg.find_key(self._user_email) - return True - except LookupError: - return False - - def _gen_openpgp_keypair(self): - """ - Generate an OpenPGP keypair for this user. - """ - params = self._gpg.gen_key_input( - key_type='RSA', - key_length=4096, - name_real=self._user_email, - name_email=self._user_email, - name_comment='Generated by LEAP Soledad.') - self._gpg.gen_key(params) - - def _load_openpgp_keypair(self): - """ - Find fingerprint for this user's OpenPGP keypair. - """ - self._fingerprint = self._gpg.find_key(self._user_email)['fingerprint'] - - def publish_pubkey(self, keyserver): - """ - Publish OpenPGP public key to a keyserver. - """ - # TODO: this has to talk to LEAP's Nickserver. - pass - - #------------------------------------------------------------------------- - # Data encryption and decryption - #------------------------------------------------------------------------- - - def encrypt(self, data, sign=None, passphrase=None, symmetric=False): - """ - Encrypt data. - """ - return str(self._gpg.encrypt(data, self._fingerprint, sign=sign, - passphrase=passphrase, - symmetric=symmetric)) - - def encrypt_symmetric(self, doc_id, data, sign=None): - """ - Encrypt data using symmetric secret. - """ - h = hmac.new(self._secret, doc_id).hexdigest() - return self.encrypt(data, sign=sign, passphrase=h, symmetric=True) - - def decrypt(self, data, passphrase=None, symmetric=False): - """ - Decrypt data. - """ - return str(self._gpg.decrypt(data, passphrase=passphrase)) - - def decrypt_symmetric(self, doc_id, data): - """ - Decrypt data using symmetric secret. - """ - h = hmac.new(self._secret, doc_id).hexdigest() - return self.decrypt(data, passphrase=h) - - #------------------------------------------------------------------------- - # Document storage, retrieval and sync - #------------------------------------------------------------------------- - - def put_doc(self, doc): - """ - Update a document in the local encrypted database. - """ - return self._db.put_doc(doc) - - def delete_doc(self, doc): - """ - Delete a document from the local encrypted database. - """ - return self._db.delete_doc(doc) - - def get_doc(self, doc_id, include_deleted=False): - """ - Retrieve a document from the local encrypted database. - """ - return self._db.get_doc(doc_id, include_deleted=include_deleted) - - def get_docs(self, doc_ids, check_for_conflicts=True, - include_deleted=False): - """ - Get the content for many documents. - """ - return self._db.get_docs(doc_ids, - check_for_conflicts=check_for_conflicts, - include_deleted=include_deleted) - - def create_doc(self, content, doc_id=None): - """ - Create a new document in the local encrypted database. - """ - return self._db.create_doc(content, doc_id=doc_id) - - def get_doc_conflicts(self, doc_id): - """ - Get the list of conflicts for the given document. - """ - return self._db.get_doc_conflicts(doc_id) - - def resolve_doc(self, doc, conflicted_doc_revs): - """ - Mark a document as no longer conflicted. - """ - return self._db.resolve_doc(doc, conflicted_doc_revs) - - def sync(self, url): - """ - Synchronize the local encrypted database with LEAP server. - """ - # TODO: create authentication scheme for sync with server. - return self._db.sync(url, creds=None, autocreate=True) - -__all__ = ['util'] diff --git a/src/leap/soledad/backends/__init__.py b/src/leap/soledad/backends/__init__.py deleted file mode 100644 index 72907f37..00000000 --- a/src/leap/soledad/backends/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -import objectstore - - -__all__ = [ - 'objectstore'] diff --git a/src/leap/soledad/backends/couch.py b/src/leap/soledad/backends/couch.py deleted file mode 100644 index d349efaf..00000000 --- a/src/leap/soledad/backends/couch.py +++ /dev/null @@ -1,269 +0,0 @@ -# general imports -import uuid -from base64 import b64encode, b64decode -import re -# u1db -from u1db import errors -from u1db.sync import LocalSyncTarget -from u1db.backends.inmemory import InMemoryIndex -from u1db.remote.server_state import ServerState -from u1db.errors import DatabaseDoesNotExist -# couchdb -from couchdb.client import Server, Document as CouchDocument -from couchdb.http import ResourceNotFound -# leap -from leap.soledad.backends.objectstore import ObjectStore -from leap.soledad.backends.leap_backend import LeapDocument - -try: - import simplejson as json -except ImportError: - import json # noqa - - -class InvalidURLError(Exception): - pass - - -class CouchDatabase(ObjectStore): - """A U1DB implementation that uses Couch as its persistence layer.""" - - @classmethod - def open_database(cls, url, create): - # get database from url - m = re.match('(^https?://[^/]+)/(.+)$', url) - if not m: - raise InvalidURLError - url = m.group(1) - dbname = m.group(2) - server = Server(url=url) - try: - server[dbname] - except ResourceNotFound: - if not create: - raise DatabaseDoesNotExist() - return cls(url, dbname) - - def __init__(self, url, database, replica_uid=None, full_commit=True, - session=None): - """Create a new Couch data container.""" - self._url = url - self._full_commit = full_commit - self._session = session - self._server = Server(url=self._url, - full_commit=self._full_commit, - session=self._session) - self._dbname = database - # this will ensure that transaction and sync logs exist and are - # up-to-date. - try: - self._database = self._server[database] - except ResourceNotFound: - self._server.create(database) - self._database = self._server[database] - super(CouchDatabase, self).__init__(replica_uid=replica_uid, - document_factory=LeapDocument) - - #------------------------------------------------------------------------- - # methods from Database - #------------------------------------------------------------------------- - - def _get_doc(self, doc_id, check_for_conflicts=False): - """ - Get just the document content, without fancy handling. - """ - cdoc = self._database.get(doc_id) - if cdoc is None: - return None - has_conflicts = False - if check_for_conflicts: - has_conflicts = self._has_conflicts(doc_id) - doc = self._factory( - doc_id=doc_id, - rev=cdoc['u1db_rev'], - has_conflicts=has_conflicts) - contents = self._database.get_attachment(cdoc, 'u1db_json') - if contents: - doc.content = json.loads(contents.getvalue()) - else: - doc.make_tombstone() - return doc - - def get_all_docs(self, include_deleted=False): - """Get all documents from the database.""" - generation = self._get_generation() - results = [] - for doc_id in self._database: - if doc_id == self.U1DB_DATA_DOC_ID: - continue - doc = self._get_doc(doc_id, check_for_conflicts=True) - if doc.content is None and not include_deleted: - continue - results.append(doc) - return (generation, results) - - def _put_doc(self, doc): - # prepare couch's Document - cdoc = CouchDocument() - cdoc['_id'] = doc.doc_id - # we have to guarantee that couch's _rev is cosistent - old_cdoc = self._database.get(doc.doc_id) - if old_cdoc is not None: - cdoc['_rev'] = old_cdoc['_rev'] - # store u1db's rev - cdoc['u1db_rev'] = doc.rev - # save doc in db - self._database.save(cdoc) - # store u1db's content as json string - if not doc.is_tombstone(): - self._database.put_attachment(cdoc, doc.get_json(), - filename='u1db_json') - else: - self._database.delete_attachment(cdoc, 'u1db_json') - - def get_sync_target(self): - return CouchSyncTarget(self) - - def create_index(self, index_name, *index_expressions): - if index_name in self._indexes: - if self._indexes[index_name]._definition == list( - index_expressions): - return - raise errors.IndexNameTakenError - index = InMemoryIndex(index_name, list(index_expressions)) - for doc_id in self._database: - if doc_id == self.U1DB_DATA_DOC_ID: - continue - doc = self._get_doc(doc_id) - if doc.content is not None: - index.add_json(doc_id, doc.get_json()) - self._indexes[index_name] = index - # save data in object store - self._set_u1db_data() - - def close(self): - # TODO: fix this method so the connection is properly closed and - # test_close (+tearDown, which deletes the db) works without problems. - self._url = None - self._full_commit = None - self._session = None - #self._server = None - self._database = None - return True - - def sync(self, url, creds=None, autocreate=True): - from u1db.sync import Synchronizer - return Synchronizer(self, CouchSyncTarget(url, creds=creds)).sync( - autocreate=autocreate) - - #------------------------------------------------------------------------- - # methods from ObjectStore - #------------------------------------------------------------------------- - - def _init_u1db_data(self): - if self._replica_uid is None: - self._replica_uid = uuid.uuid4().hex - doc = self._factory(doc_id=self.U1DB_DATA_DOC_ID) - doc.content = {'transaction_log': [], - 'conflicts': b64encode(json.dumps({})), - 'other_generations': {}, - 'indexes': b64encode(json.dumps({})), - 'replica_uid': self._replica_uid} - self._put_doc(doc) - - def _get_u1db_data(self): - # retrieve u1db data from couch db - cdoc = self._database.get(self.U1DB_DATA_DOC_ID) - jsonstr = self._database.get_attachment(cdoc, 'u1db_json').getvalue() - content = json.loads(jsonstr) - # set u1db database info - #self._sync_log = content['sync_log'] - self._transaction_log = content['transaction_log'] - self._conflicts = json.loads(b64decode(content['conflicts'])) - self._other_generations = content['other_generations'] - self._indexes = self._load_indexes_from_json( - b64decode(content['indexes'])) - self._replica_uid = content['replica_uid'] - # save couch _rev - self._couch_rev = cdoc['_rev'] - - def _set_u1db_data(self): - doc = self._factory(doc_id=self.U1DB_DATA_DOC_ID) - doc.content = { - 'transaction_log': self._transaction_log, - # Here, the b64 encode ensures that document content - # does not cause strange behaviour in couchdb because - # of encoding. - 'conflicts': b64encode(json.dumps(self._conflicts)), - 'other_generations': self._other_generations, - 'indexes': b64encode(self._dump_indexes_as_json()), - 'replica_uid': self._replica_uid, - '_rev': self._couch_rev} - self._put_doc(doc) - - #------------------------------------------------------------------------- - # Couch specific methods - #------------------------------------------------------------------------- - - def delete_database(self): - del(self._server[self._dbname]) - - def _dump_indexes_as_json(self): - indexes = {} - for name, idx in self._indexes.iteritems(): - indexes[name] = {} - for attr in ['name', 'definition', 'values']: - indexes[name][attr] = getattr(idx, '_' + attr) - return json.dumps(indexes) - - def _load_indexes_from_json(self, indexes): - dict = {} - for name, idx_dict in json.loads(indexes).iteritems(): - idx = InMemoryIndex(name, idx_dict['definition']) - idx._values = idx_dict['values'] - dict[name] = idx - return dict - - -class CouchSyncTarget(LocalSyncTarget): - - def get_sync_info(self, source_replica_uid): - source_gen, source_trans_id = self._db._get_replica_gen_and_trans_id( - source_replica_uid) - my_gen, my_trans_id = self._db._get_generation_info() - return ( - self._db._replica_uid, my_gen, my_trans_id, source_gen, - source_trans_id) - - def record_sync_info(self, source_replica_uid, source_replica_generation, - source_replica_transaction_id): - if self._trace_hook: - self._trace_hook('record_sync_info') - self._db._set_replica_gen_and_trans_id( - source_replica_uid, source_replica_generation, - source_replica_transaction_id) - - -class CouchServerState(ServerState): - """ - Inteface of the WSGI server with the CouchDB backend. - """ - - def __init__(self, couch_url): - self.couch_url = couch_url - - def open_database(self, dbname): - # TODO: open couch - from leap.soledad.backends.couch import CouchDatabase - return CouchDatabase.open_database(self.couch_url + '/' + dbname, - create=False) - - def ensure_database(self, dbname): - from leap.soledad.backends.couch import CouchDatabase - db = CouchDatabase.open_database(self.couch_url + '/' + dbname, - create=True) - return db, db._replica_uid - - def delete_database(self, dbname): - from leap.soledad.backends.couch import CouchDatabase - CouchDatabase.delete_database(self.couch_url + '/' + dbname) diff --git a/src/leap/soledad/backends/leap_backend.py b/src/leap/soledad/backends/leap_backend.py deleted file mode 100644 index c3c52ee6..00000000 --- a/src/leap/soledad/backends/leap_backend.py +++ /dev/null @@ -1,193 +0,0 @@ -try: - import simplejson as json -except ImportError: - import json # noqa - -from u1db import Document -from u1db.remote import utils -from u1db.remote.http_target import HTTPSyncTarget -from u1db.remote.http_database import HTTPDatabase -from u1db.errors import BrokenSyncStream - -import uuid - - -class NoDefaultKey(Exception): - pass - - -class NoSoledadInstance(Exception): - pass - - -class DocumentEncryptionFailed(Exception): - pass - - -class LeapDocument(Document): - """ - Encryptable and syncable document. - - LEAP Documents are standard u1db documents with cabability of returning an - encrypted version of the document json string as well as setting document - content based on an encrypted version of json string. - """ - - def __init__(self, doc_id=None, rev=None, json='{}', has_conflicts=False, - encrypted_json=None, soledad=None, syncable=True): - super(LeapDocument, self).__init__(doc_id, rev, json, has_conflicts) - self._soledad = soledad - self._syncable = syncable - if encrypted_json: - self.set_encrypted_json(encrypted_json) - - def get_encrypted_json(self): - """ - Return document's json serialization encrypted with user's public key. - """ - if not self._soledad: - raise NoSoledadInstance() - ciphertext = self._soledad.encrypt_symmetric(self.doc_id, - self.get_json()) - return json.dumps({'_encrypted_json': ciphertext}) - - def set_encrypted_json(self, encrypted_json): - """ - Set document's content based on encrypted version of json string. - """ - if not self._soledad: - raise NoSoledadInstance() - ciphertext = json.loads(encrypted_json)['_encrypted_json'] - plaintext = self._soledad.decrypt_symmetric(self.doc_id, ciphertext) - return self.set_json(plaintext) - - def _get_syncable(self): - return self._syncable - - def _set_syncable(self, syncable=True): - self._syncable = syncable - - syncable = property( - _get_syncable, - _set_syncable, - doc="Determine if document should be synced with server." - ) - - # Returning the revision as string solves the following exception in - # Twisted web: - # exceptions.TypeError: Can only pass-through bytes on Python 2 - def _get_rev(self): - if self._rev is None: - return None - return str(self._rev) - - def _set_rev(self, rev): - self._rev = rev - - rev = property( - _get_rev, - _set_rev, - doc="Wrapper to ensure `doc.rev` is always returned as bytes.") - - -class LeapSyncTarget(HTTPSyncTarget): - """ - A SyncTarget that encrypts data before sending and decrypts data after - receiving. - """ - - def __init__(self, url, creds=None, soledad=None): - super(LeapSyncTarget, self).__init__(url, creds) - self._soledad = soledad - - def _parse_sync_stream(self, data, return_doc_cb, ensure_callback=None): - """ - Does the same as parent's method but ensures incoming content will be - decrypted. - """ - parts = data.splitlines() # one at a time - if not parts or parts[0] != '[': - raise BrokenSyncStream - data = parts[1:-1] - comma = False - if data: - line, comma = utils.check_and_strip_comma(data[0]) - res = json.loads(line) - if ensure_callback and 'replica_uid' in res: - ensure_callback(res['replica_uid']) - for entry in data[1:]: - if not comma: # missing in between comma - raise BrokenSyncStream - line, comma = utils.check_and_strip_comma(entry) - entry = json.loads(line) - # decrypt after receiving from server. - doc = LeapDocument(entry['id'], entry['rev'], - encrypted_json=entry['content'], - soledad=self._soledad) - return_doc_cb(doc, entry['gen'], entry['trans_id']) - if parts[-1] != ']': - try: - partdic = json.loads(parts[-1]) - except ValueError: - pass - else: - if isinstance(partdic, dict): - self._error(partdic) - raise BrokenSyncStream - if not data or comma: # no entries or bad extra comma - raise BrokenSyncStream - return res - - def sync_exchange(self, docs_by_generations, source_replica_uid, - last_known_generation, last_known_trans_id, - return_doc_cb, ensure_callback=None): - """ - Does the same as parent's method but encrypts content before syncing. - """ - self._ensure_connection() - if self._trace_hook: # for tests - self._trace_hook('sync_exchange') - url = '%s/sync-from/%s' % (self._url.path, source_replica_uid) - self._conn.putrequest('POST', url) - self._conn.putheader('content-type', 'application/x-u1db-sync-stream') - for header_name, header_value in self._sign_request('POST', url, {}): - self._conn.putheader(header_name, header_value) - entries = ['['] - size = 1 - - def prepare(**dic): - entry = comma + '\r\n' + json.dumps(dic) - entries.append(entry) - return len(entry) - - comma = '' - size += prepare( - last_known_generation=last_known_generation, - last_known_trans_id=last_known_trans_id, - ensure=ensure_callback is not None) - comma = ',' - for doc, gen, trans_id in docs_by_generations: - if doc.syncable: - # encrypt and verify before sending to server. - doc_content = doc.get_encrypted_json() - if doc_content == doc.get_json(): - raise DocumentEncryptionFailed - enc_doc = LeapDocument(doc.doc_id, doc.rev, - encrypted_json=doc_content, - soledad=self._soledad) - if doc.get_json() != enc_doc.get_json(): - raise DocumentEncryptionFailed - size += prepare(id=doc.doc_id, rev=doc.rev, - content=doc_content, - gen=gen, trans_id=trans_id) - entries.append('\r\n]') - size += len(entries[-1]) - self._conn.putheader('content-length', str(size)) - self._conn.endheaders() - for entry in entries: - self._conn.send(entry) - entries = None - data, _ = self._response() - res = self._parse_sync_stream(data, return_doc_cb, ensure_callback) - data = None - return res['new_generation'], res['new_transaction_id'] diff --git a/src/leap/soledad/backends/objectstore.py b/src/leap/soledad/backends/objectstore.py deleted file mode 100644 index 199107af..00000000 --- a/src/leap/soledad/backends/objectstore.py +++ /dev/null @@ -1,114 +0,0 @@ -from u1db.backends.inmemory import InMemoryDatabase -from u1db import errors - - -class ObjectStore(InMemoryDatabase): - """ - A backend for storing u1db data in an object store. - """ - - @classmethod - def open_database(cls, url, create, document_factory=None): - raise NotImplementedError(cls.open_database) - - def __init__(self, replica_uid=None, document_factory=None): - super(ObjectStore, self).__init__(replica_uid, - document_factory=document_factory) - # sync data in memory with data in object store - if not self._get_doc(self.U1DB_DATA_DOC_ID): - self._init_u1db_data() - self._get_u1db_data() - - #------------------------------------------------------------------------- - # methods from Database - #------------------------------------------------------------------------- - - def _set_replica_uid(self, replica_uid): - super(ObjectStore, self)._set_replica_uid(replica_uid) - self._set_u1db_data() - - def _put_doc(self, doc): - raise NotImplementedError(self._put_doc) - - def _get_doc(self, doc): - raise NotImplementedError(self._get_doc) - - def get_all_docs(self, include_deleted=False): - raise NotImplementedError(self.get_all_docs) - - def delete_doc(self, doc): - old_doc = self._get_doc(doc.doc_id, check_for_conflicts=True) - if old_doc is None: - raise errors.DocumentDoesNotExist - if old_doc.rev != doc.rev: - raise errors.RevisionConflict() - if old_doc.is_tombstone(): - raise errors.DocumentAlreadyDeleted - if old_doc.has_conflicts: - raise errors.ConflictedDoc() - new_rev = self._allocate_doc_rev(doc.rev) - doc.rev = new_rev - doc.make_tombstone() - self._put_and_update_indexes(old_doc, doc) - return new_rev - - # index-related methods - - def create_index(self, index_name, *index_expressions): - raise NotImplementedError(self.create_index) - - def delete_index(self, index_name): - super(ObjectStore, self).delete_index(index_name) - self._set_u1db_data() - - def _replace_conflicts(self, doc, conflicts): - super(ObjectStore, self)._replace_conflicts(doc, conflicts) - self._set_u1db_data() - - def _do_set_replica_gen_and_trans_id(self, other_replica_uid, - other_generation, - other_transaction_id): - super(ObjectStore, self)._do_set_replica_gen_and_trans_id( - other_replica_uid, - other_generation, - other_transaction_id) - self._set_u1db_data() - - #------------------------------------------------------------------------- - # implemented methods from CommonBackend - #------------------------------------------------------------------------- - - def _put_and_update_indexes(self, old_doc, doc): - for index in self._indexes.itervalues(): - if old_doc is not None and not old_doc.is_tombstone(): - index.remove_json(old_doc.doc_id, old_doc.get_json()) - if not doc.is_tombstone(): - index.add_json(doc.doc_id, doc.get_json()) - trans_id = self._allocate_transaction_id() - self._put_doc(doc) - self._transaction_log.append((doc.doc_id, trans_id)) - self._set_u1db_data() - - #------------------------------------------------------------------------- - # methods specific for object stores - #------------------------------------------------------------------------- - - U1DB_DATA_DOC_ID = 'u1db_data' - - def _get_u1db_data(self): - """ - Fetch u1db configuration data from backend storage. - """ - NotImplementedError(self._get_u1db_data) - - def _set_u1db_data(self): - """ - Save u1db configuration data on backend storage. - """ - NotImplementedError(self._set_u1db_data) - - def _init_u1db_data(self): - """ - Initialize u1db configuration data on backend storage. - """ - NotImplementedError(self._init_u1db_data) diff --git a/src/leap/soledad/backends/sqlcipher.py b/src/leap/soledad/backends/sqlcipher.py deleted file mode 100644 index c902b466..00000000 --- a/src/leap/soledad/backends/sqlcipher.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright 2011 Canonical Ltd. -# -# This file is part of u1db. -# -# u1db is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 -# as published by the Free Software Foundation. -# -# u1db is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with u1db. If not, see . - -"""A U1DB implementation that uses SQLCipher as its persistence layer.""" - -import os -from pysqlcipher import dbapi2 -import time - -from u1db.backends.sqlite_backend import ( - SQLiteDatabase, - SQLitePartialExpandDatabase, -) -from u1db import ( - errors, -) - -from leap.soledad.backends.leap_backend import LeapDocument - - -def open(path, password, create=True, document_factory=None, soledad=None): - """Open a database at the given location. - - Will raise u1db.errors.DatabaseDoesNotExist if create=False and the - database does not already exist. - - :param path: The filesystem path for the database to open. - :param create: True/False, should the database be created if it doesn't - already exist? - :param document_factory: A function that will be called with the same - parameters as Document.__init__. - :return: An instance of Database. - """ - return SQLCipherDatabase.open_database( - path, password, create=create, document_factory=document_factory, - soledad=soledad) - - -class DatabaseIsNotEncrypted(Exception): - """ - Exception raised when trying to open non-encrypted databases. - """ - pass - - -class SQLCipherDatabase(SQLitePartialExpandDatabase): - """A U1DB implementation that uses SQLCipher as its persistence layer.""" - - _index_storage_value = 'expand referenced encrypted' - - @classmethod - def set_pragma_key(cls, db_handle, key): - db_handle.cursor().execute("PRAGMA key = '%s'" % key) - - def __init__(self, sqlite_file, password, document_factory=None, - soledad=None): - """Create a new sqlcipher file.""" - self._check_if_db_is_encrypted(sqlite_file) - self._db_handle = dbapi2.connect(sqlite_file) - SQLCipherDatabase.set_pragma_key(self._db_handle, password) - self._real_replica_uid = None - self._ensure_schema() - self._soledad = soledad - - def factory(doc_id=None, rev=None, json='{}', has_conflicts=False, - encrypted_json=None, syncable=True): - return LeapDocument(doc_id=doc_id, rev=rev, json=json, - has_conflicts=has_conflicts, - encrypted_json=encrypted_json, - syncable=syncable, soledad=self._soledad) - self.set_document_factory(factory) - - def _check_if_db_is_encrypted(self, sqlite_file): - if not os.path.exists(sqlite_file): - return - else: - try: - # try to open an encrypted database with the regular u1db - # backend should raise a DatabaseError exception. - SQLitePartialExpandDatabase(sqlite_file) - raise DatabaseIsNotEncrypted() - except dbapi2.DatabaseError: - pass - - @classmethod - def _open_database(cls, sqlite_file, password, document_factory=None, - soledad=None): - if not os.path.isfile(sqlite_file): - raise errors.DatabaseDoesNotExist() - tries = 2 - while True: - # Note: There seems to be a bug in sqlite 3.5.9 (with python2.6) - # where without re-opening the database on Windows, it - # doesn't see the transaction that was just committed - db_handle = dbapi2.connect(sqlite_file) - SQLCipherDatabase.set_pragma_key(db_handle, password) - c = db_handle.cursor() - v, err = cls._which_index_storage(c) - db_handle.close() - if v is not None: - break - # possibly another process is initializing it, wait for it to be - # done - if tries == 0: - raise err # go for the richest error? - tries -= 1 - time.sleep(cls.WAIT_FOR_PARALLEL_INIT_HALF_INTERVAL) - return SQLCipherDatabase._sqlite_registry[v]( - sqlite_file, password, document_factory=document_factory, - soledad=soledad) - - @classmethod - def open_database(cls, sqlite_file, password, create, backend_cls=None, - document_factory=None, soledad=None): - try: - return cls._open_database(sqlite_file, password, - document_factory=document_factory, - soledad=soledad) - except errors.DatabaseDoesNotExist: - if not create: - raise - if backend_cls is None: - # default is SQLCipherPartialExpandDatabase - backend_cls = SQLCipherDatabase - return backend_cls(sqlite_file, password, - document_factory=document_factory, - soledad=soledad) - - def sync(self, url, creds=None, autocreate=True): - """ - Synchronize encrypted documents with remote replica exposed at url. - """ - from u1db.sync import Synchronizer - from leap.soledad.backends.leap_backend import LeapSyncTarget - return Synchronizer( - self, - LeapSyncTarget(url, - creds=creds, - soledad=self._soledad)).sync(autocreate=autocreate) - - def _extra_schema_init(self, c): - c.execute( - 'ALTER TABLE document ' - 'ADD COLUMN syncable BOOL NOT NULL DEFAULT TRUE') - - def _put_and_update_indexes(self, old_doc, doc): - super(SQLCipherDatabase, self)._put_and_update_indexes(old_doc, doc) - c = self._db_handle.cursor() - c.execute('UPDATE document SET syncable=? WHERE doc_id=?', - (doc.syncable, doc.doc_id)) - - def _get_doc(self, doc_id, check_for_conflicts=False): - doc = super(SQLCipherDatabase, self)._get_doc(doc_id, - check_for_conflicts) - if doc: - c = self._db_handle.cursor() - c.execute('SELECT syncable FROM document WHERE doc_id=?', - (doc.doc_id,)) - doc.syncable = bool(c.fetchone()[0]) - return doc - - -SQLiteDatabase.register_implementation(SQLCipherDatabase) diff --git a/src/leap/soledad/server.py b/src/leap/soledad/server.py deleted file mode 100644 index 4fc97be5..00000000 --- a/src/leap/soledad/server.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -An u1db server that stores data using couchdb. - -This should be run with: - twistd -n web --wsgi=leap.soledad.server.application -""" - -from twisted.web.wsgi import WSGIResource -from twisted.internet import reactor -from u1db.remote import http_app -from leap.soledad.backends.couch import CouchServerState - -couch_url = 'http://localhost:5984' -state = CouchServerState(couch_url) -# TODO: change working dir to something meaningful -state.set_workingdir('/tmp') -# TODO: write a LeapHTTPApp that will use Couch as backend instead of SQLite -application = http_app.HTTPApp(state) - -resource = WSGIResource(reactor, reactor.getThreadPool(), application) diff --git a/src/leap/soledad/tests/__init__.py b/src/leap/soledad/tests/__init__.py deleted file mode 100644 index 6135e648..00000000 --- a/src/leap/soledad/tests/__init__.py +++ /dev/null @@ -1,199 +0,0 @@ -import u1db -from leap.soledad import Soledad -from leap.soledad.backends.leap_backend import LeapDocument -from leap.testing.basetest import BaseLeapTest - - -#----------------------------------------------------------------------------- -# Some tests inherit from BaseSoledadTest in order to have a working Soledad -# instance in each test. -#----------------------------------------------------------------------------- - -class BaseSoledadTest(BaseLeapTest): - - def setUp(self): - # config info - self.gnupg_home = "%s/gnupg" % self.tempdir - self.db1_file = "%s/db1.u1db" % self.tempdir - self.db2_file = "%s/db2.u1db" % self.tempdir - self.email = 'leap@leap.se' - # open test dbs - self._db1 = u1db.open(self.db1_file, create=True, - document_factory=LeapDocument) - self._db2 = u1db.open(self.db2_file, create=True, - document_factory=LeapDocument) - # open a soledad instance - self._soledad = Soledad(self.email, gpghome=self.gnupg_home, - initialize=False) - self._soledad._gpg.import_keys(PUBLIC_KEY) - self._soledad._gpg.import_keys(PRIVATE_KEY) - self._soledad._initialize() - - def tearDown(self): - self._db1.close() - self._db2.close() - self._soledad.close() - - -# Key material for testing -KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF" -PUBLIC_KEY = """ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -mQINBFC9+dkBEADNRfwV23TWEoGc/x0wWH1P7PlXt8MnC2Z1kKaKKmfnglVrpOiz -iLWoiU58sfZ0L5vHkzXHXCBf6Eiy/EtUIvdiWAn+yASJ1mk5jZTBKO/WMAHD8wTO -zpMsFmWyg3xc4DkmFa9KQ5EVU0o/nqPeyQxNMQN7px5pPwrJtJFmPxnxm+aDkPYx -irDmz/4DeDNqXliazGJKw7efqBdlwTHkl9Akw2gwy178pmsKwHHEMOBOFFvX61AT -huKqHYmlCGSliwbrJppTG7jc1/ls3itrK+CWTg4txREkSpEVmfcASvw/ZqLbjgfs -d/INMwXnR9U81O8+7LT6yw/ca4ppcFoJD7/XJbkRiML6+bJ4Dakiy6i727BzV17g -wI1zqNvm5rAhtALKfACha6YO43aJzairO4II1wxVHvRDHZn2IuKDDephQ3Ii7/vb -hUOf6XCSmchkAcpKXUOvbxm1yfB1LRa64mMc2RcZxf4mW7KQkulBsdV5QG2276lv -U2UUy2IutXcGP5nXC+f6sJJGJeEToKJ57yiO/VWJFjKN8SvP+7AYsQSqINUuEf6H -T5gCPCraGMkTUTPXrREvu7NOohU78q6zZNaL3GW8ai7eSeANSuQ8Vzffx7Wd8Y7i -Pw9sYj0SMFs1UgjbuL6pO5ueHh+qyumbtAq2K0Bci0kqOcU4E9fNtdiovQARAQAB -tBxMZWFwIFRlc3QgS2V5IDxsZWFwQGxlYXAuc2U+iQI3BBMBCAAhBQJQvfnZAhsD -BQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEC9FXigk0Y3fT7EQAKH3IuRniOpb -T/DDIgwwjz3oxB/W0DDMyPXowlhSOuM0rgGfntBpBb3boezEXwL86NPQxNGGruF5 -hkmecSiuPSvOmQlqlS95NGQp6hNG0YaKColh+Q5NTspFXCAkFch9oqUje0LdxfSP -QfV9UpeEvGyPmk1I9EJV/YDmZ4+Djge1d7qhVZInz4Rx1NrSyF/Tc2EC0VpjQFsU -Y9Kb2YBBR7ivG6DBc8ty0jJXi7B4WjkFcUEJviQpMF2dCLdonCehYs1PqsN1N7j+ -eFjQd+hqVMJgYuSGKjvuAEfClM6MQw7+FmFwMyLgK/Ew/DttHEDCri77SPSkOGSI -txCzhTg6798f6mJr7WcXmHX1w1Vcib5FfZ8vTDFVhz/XgAgArdhPo9V6/1dgSSiB -KPQ/spsco6u5imdOhckERE0lnAYvVT6KE81TKuhF/b23u7x+Wdew6kK0EQhYA7wy -7LmlaNXc7rMBQJ9Z60CJ4JDtatBWZ0kNrt2VfdDHVdqBTOpl0CraNUjWE5YMDasr -K2dF5IX8D3uuYtpZnxqg0KzyLg0tzL0tvOL1C2iudgZUISZNPKbS0z0v+afuAAnx -2pTC3uezbh2Jt8SWTLhll4i0P4Ps5kZ6HQUO56O+/Z1cWovX+mQekYFmERySDR9n -3k1uAwLilJmRmepGmvYbB8HloV8HqwgguQINBFC9+dkBEAC0I/xn1uborMgDvBtf -H0sEhwnXBC849/32zic6udB6/3Efk9nzbSpL3FSOuXITZsZgCHPkKarnoQ2ztMcS -sh1ke1C5gQGms75UVmM/nS+2YI4vY8OX/GC/on2vUyncqdH+bR6xH5hx4NbWpfTs -iQHmz5C6zzS/kuabGdZyKRaZHt23WQ7JX/4zpjqbC99DjHcP9BSk7tJ8wI4bkMYD -uFVQdT9O6HwyKGYwUU4sAQRAj7XCTGvVbT0dpgJwH4RmrEtJoHAx4Whg8mJ710E0 -GCmzf2jqkNuOw76ivgk27Kge+Hw00jmJjQhHY0yVbiaoJwcRrPKzaSjEVNgrpgP3 -lXPRGQArgESsIOTeVVHQ8fhK2YtTeCY9rIiO+L0OX2xo9HK7hfHZZWL6rqymXdyS -fhzh/f6IPyHFWnvj7Brl7DR8heMikygcJqv+ed2yx7iLyCUJ10g12I48+aEj1aLe -dP7lna32iY8/Z0SHQLNH6PXO9SlPcq2aFUgKqE75A/0FMk7CunzU1OWr2ZtTLNO1 -WT/13LfOhhuEq9jTyTosn0WxBjJKq18lnhzCXlaw6EAtbA7CUwsD3CTPR56aAXFK -3I7KXOVAqggrvMe5Tpdg5drfYpI8hZovL5aAgb+7Y5ta10TcJdUhS5K3kFAWe/td -U0cmWUMDP1UMSQ5Jg6JIQVWhSwARAQABiQIfBBgBCAAJBQJQvfnZAhsMAAoJEC9F -Xigk0Y3fRwsP/i0ElYCyxeLpWJTwo1iCLkMKz2yX1lFVa9nT1BVTPOQwr/IAc5OX -NdtbJ14fUsKL5pWgW8OmrXtwZm1y4euI1RPWWubG01ouzwnGzv26UcuHeqC5orZj -cOnKtL40y8VGMm8LoicVkRJH8blPORCnaLjdOtmA3rx/v2EXrJpSa3AhOy0ZSRXk -ZSrK68AVNwamHRoBSYyo0AtaXnkPX4+tmO8X8BPfj125IljubvwZPIW9VWR9UqCE -VPfDR1XKegVb6VStIywF7kmrknM1C5qUY28rdZYWgKorw01hBGV4jTW0cqde3N51 -XT1jnIAa+NoXUM9uQoGYMiwrL7vNsLlyyiW5ayDyV92H/rIuiqhFgbJsHTlsm7I8 -oGheR784BagAA1NIKD1qEO9T6Kz9lzlDaeWS5AUKeXrb7ZJLI1TTCIZx5/DxjLqM -Tt/RFBpVo9geZQrvLUqLAMwdaUvDXC2c6DaCPXTh65oCZj/hqzlJHH+RoTWWzKI+ -BjXxgUWF9EmZUBrg68DSmI+9wuDFsjZ51BcqvJwxyfxtTaWhdoYqH/UQS+D1FP3/ -diZHHlzwVwPICzM9ooNTgbrcDzyxRkIVqsVwBq7EtzcvgYUyX53yG25Giy6YQaQ2 -ZtQ/VymwFL3XdUWV6B/hU4PVAFvO3qlOtdJ6TpE+nEWgcWjCv5g7RjXX -=MuOY ------END PGP PUBLIC KEY BLOCK----- -""" -PRIVATE_KEY = """ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -lQcYBFC9+dkBEADNRfwV23TWEoGc/x0wWH1P7PlXt8MnC2Z1kKaKKmfnglVrpOiz -iLWoiU58sfZ0L5vHkzXHXCBf6Eiy/EtUIvdiWAn+yASJ1mk5jZTBKO/WMAHD8wTO -zpMsFmWyg3xc4DkmFa9KQ5EVU0o/nqPeyQxNMQN7px5pPwrJtJFmPxnxm+aDkPYx -irDmz/4DeDNqXliazGJKw7efqBdlwTHkl9Akw2gwy178pmsKwHHEMOBOFFvX61AT -huKqHYmlCGSliwbrJppTG7jc1/ls3itrK+CWTg4txREkSpEVmfcASvw/ZqLbjgfs -d/INMwXnR9U81O8+7LT6yw/ca4ppcFoJD7/XJbkRiML6+bJ4Dakiy6i727BzV17g -wI1zqNvm5rAhtALKfACha6YO43aJzairO4II1wxVHvRDHZn2IuKDDephQ3Ii7/vb -hUOf6XCSmchkAcpKXUOvbxm1yfB1LRa64mMc2RcZxf4mW7KQkulBsdV5QG2276lv -U2UUy2IutXcGP5nXC+f6sJJGJeEToKJ57yiO/VWJFjKN8SvP+7AYsQSqINUuEf6H -T5gCPCraGMkTUTPXrREvu7NOohU78q6zZNaL3GW8ai7eSeANSuQ8Vzffx7Wd8Y7i -Pw9sYj0SMFs1UgjbuL6pO5ueHh+qyumbtAq2K0Bci0kqOcU4E9fNtdiovQARAQAB -AA/+JHtlL39G1wsH9R6UEfUQJGXR9MiIiwZoKcnRB2o8+DS+OLjg0JOh8XehtuCs -E/8oGQKtQqa5bEIstX7IZoYmYFiUQi9LOzIblmp2vxOm+HKkxa4JszWci2/ZmC3t -KtaA4adl9XVnshoQ7pijuCMUKB3naBEOAxd8s9d/JeReGIYkJErdrnVfNk5N71Ds -FmH5Ll3XtEDvgBUQP3nkA6QFjpsaB94FHjL3gDwum/cxzj6pCglcvHOzEhfY0Ddb -J967FozQTaf2JW3O+w3LOqtcKWpq87B7+O61tVidQPSSuzPjCtFF0D2LC9R/Hpky -KTMQ6CaKja4MPhjwywd4QPcHGYSqjMpflvJqi+kYIt8psUK/YswWjnr3r4fbuqVY -VhtiHvnBHQjz135lUqWvEz4hM3Xpnxydx7aRlv5NlevK8+YIO5oFbWbGNTWsPZI5 -jpoFBpSsnR1Q5tnvtNHauvoWV+XN2qAOBTG+/nEbDYH6Ak3aaE9jrpTdYh0CotYF -q7csANsDy3JvkAzeU6WnYpsHHaAjqOGyiZGsLej1UcXPFMosE/aUo4WQhiS8Zx2c -zOVKOi/X5vQ2GdNT9Qolz8AriwzsvFR+bxPzyd8V6ALwDsoXvwEYinYBKK8j0OPv -OOihSR6HVsuP9NUZNU9ewiGzte/+/r6pNXHvR7wTQ8EWLcEIAN6Zyrb0bHZTIlxt -VWur/Ht2mIZrBaO50qmM5RD3T5oXzWXi/pjLrIpBMfeZR9DWfwQwjYzwqi7pxtYx -nJvbMuY505rfnMoYxb4J+cpRXV8MS7Dr1vjjLVUC9KiwSbM3gg6emfd2yuA93ihv -Pe3mffzLIiQa4mRE3wtGcioC43nWuV2K2e1KjxeFg07JhrezA/1Cak505ab/tmvP -4YmjR5c44+yL/YcQ3HdFgs4mV+nVbptRXvRcPpolJsgxPccGNdvHhsoR4gwXMS3F -RRPD2z6x8xeN73Q4KH3bm01swQdwFBZbWVfmUGLxvN7leCdfs9+iFJyqHiCIB6Iv -mQfp8F0IAOwSo8JhWN+V1dwML4EkIrM8wUb4yecNLkyR6TpPH/qXx4PxVMC+vy6x -sCtjeHIwKE+9vqnlhd5zOYh7qYXEJtYwdeDDmDbL8oks1LFfd+FyAuZXY33DLwn0 -cRYsr2OEZmaajqUB3NVmj3H4uJBN9+paFHyFSXrH68K1Fk2o3n+RSf2EiX+eICwI -L6rqoF5sSVUghBWdNegV7qfy4anwTQwrIMGjgU5S6PKW0Dr/3iO5z3qQpGPAj5OW -ATqPWkDICLbObPxD5cJlyyNE2wCA9VVc6/1d6w4EVwSq9h3/WTpATEreXXxTGptd -LNiTA1nmakBYNO2Iyo3djhaqBdWjk+EIAKtVEnJH9FAVwWOvaj1RoZMA5DnDMo7e -SnhrCXl8AL7Z1WInEaybasTJXn1uQ8xY52Ua4b8cbuEKRKzw/70NesFRoMLYoHTO -dyeszvhoDHberpGRTciVmpMu7Hyi33rM31K9epA4ib6QbbCHnxkWOZB+Bhgj1hJ8 -xb4RBYWiWpAYcg0+DAC3w9gfxQhtUlZPIbmbrBmrVkO2GVGUj8kH6k4UV6kUHEGY -HQWQR0HcbKcXW81ZXCCD0l7ROuEWQtTe5Jw7dJ4/QFuqZnPutXVRNOZqpl6eRShw -7X2/a29VXBpmHA95a88rSQsL+qm7Fb3prqRmuMCtrUZgFz7HLSTuUMR867QcTGVh -cCBUZXN0IEtleSA8bGVhcEBsZWFwLnNlPokCNwQTAQgAIQUCUL352QIbAwULCQgH -AwUVCgkICwUWAgMBAAIeAQIXgAAKCRAvRV4oJNGN30+xEACh9yLkZ4jqW0/wwyIM -MI896MQf1tAwzMj16MJYUjrjNK4Bn57QaQW926HsxF8C/OjT0MTRhq7heYZJnnEo -rj0rzpkJapUveTRkKeoTRtGGigqJYfkOTU7KRVwgJBXIfaKlI3tC3cX0j0H1fVKX -hLxsj5pNSPRCVf2A5mePg44HtXe6oVWSJ8+EcdTa0shf03NhAtFaY0BbFGPSm9mA -QUe4rxugwXPLctIyV4uweFo5BXFBCb4kKTBdnQi3aJwnoWLNT6rDdTe4/nhY0Hfo -alTCYGLkhio77gBHwpTOjEMO/hZhcDMi4CvxMPw7bRxAwq4u+0j0pDhkiLcQs4U4 -Ou/fH+pia+1nF5h19cNVXIm+RX2fL0wxVYc/14AIAK3YT6PVev9XYEkogSj0P7Kb -HKOruYpnToXJBERNJZwGL1U+ihPNUyroRf29t7u8flnXsOpCtBEIWAO8Muy5pWjV -3O6zAUCfWetAieCQ7WrQVmdJDa7dlX3Qx1XagUzqZdAq2jVI1hOWDA2rKytnReSF -/A97rmLaWZ8aoNCs8i4NLcy9Lbzi9QtornYGVCEmTTym0tM9L/mn7gAJ8dqUwt7n -s24dibfElky4ZZeItD+D7OZGeh0FDuejvv2dXFqL1/pkHpGBZhEckg0fZ95NbgMC -4pSZkZnqRpr2GwfB5aFfB6sIIJ0HGARQvfnZARAAtCP8Z9bm6KzIA7wbXx9LBIcJ -1wQvOPf99s4nOrnQev9xH5PZ820qS9xUjrlyE2bGYAhz5Cmq56ENs7THErIdZHtQ -uYEBprO+VFZjP50vtmCOL2PDl/xgv6J9r1Mp3KnR/m0esR+YceDW1qX07IkB5s+Q -us80v5LmmxnWcikWmR7dt1kOyV/+M6Y6mwvfQ4x3D/QUpO7SfMCOG5DGA7hVUHU/ -Tuh8MihmMFFOLAEEQI+1wkxr1W09HaYCcB+EZqxLSaBwMeFoYPJie9dBNBgps39o -6pDbjsO+or4JNuyoHvh8NNI5iY0IR2NMlW4mqCcHEazys2koxFTYK6YD95Vz0RkA -K4BErCDk3lVR0PH4StmLU3gmPayIjvi9Dl9saPRyu4Xx2WVi+q6spl3ckn4c4f3+ -iD8hxVp74+wa5ew0fIXjIpMoHCar/nndsse4i8glCddINdiOPPmhI9Wi3nT+5Z2t -9omPP2dEh0CzR+j1zvUpT3KtmhVICqhO+QP9BTJOwrp81NTlq9mbUyzTtVk/9dy3 -zoYbhKvY08k6LJ9FsQYySqtfJZ4cwl5WsOhALWwOwlMLA9wkz0eemgFxStyOylzl -QKoIK7zHuU6XYOXa32KSPIWaLy+WgIG/u2ObWtdE3CXVIUuSt5BQFnv7XVNHJllD -Az9VDEkOSYOiSEFVoUsAEQEAAQAP/1AagnZQZyzHDEgw4QELAspYHCWLXE5aZInX -wTUJhK31IgIXNn9bJ0hFiSpQR2xeMs9oYtRuPOu0P8oOFMn4/z374fkjZy8QVY3e -PlL+3EUeqYtkMwlGNmVw5a/NbNuNfm5Darb7pEfbYd1gPcni4MAYw7R2SG/57GbC -9gucvspHIfOSfBNLBthDzmK8xEKe1yD2eimfc2T7IRYb6hmkYfeds5GsqvGI6mwI -85h4uUHWRc5JOlhVM6yX8hSWx0L60Z3DZLChmc8maWnFXd7C8eQ6P1azJJbW71Ih -7CoK0XW4LE82vlQurSRFgTwfl7wFYszW2bOzCuhHDDtYnwH86Nsu0DC78ZVRnvxn -E8Ke/AJgrdhIOo4UAyR+aZD2+2mKd7/waOUTUrUtTzc7i8N3YXGi/EIaNReBXaq+ -ZNOp24BlFzRp+FCF/pptDW9HjPdiV09x0DgICmeZS4Gq/4vFFIahWctg52NGebT0 -Idxngjj+xDtLaZlLQoOz0n5ByjO/Wi0ANmMv1sMKCHhGvdaSws2/PbMR2r4caj8m -KXpIgdinM/wUzHJ5pZyF2U/qejsRj8Kw8KH/tfX4JCLhiaP/mgeTuWGDHeZQERAT -xPmRFHaLP9/ZhvGNh6okIYtrKjWTLGoXvKLHcrKNisBLSq+P2WeFrlme1vjvJMo/ -jPwLT5o9CADQmcbKZ+QQ1ZM9v99iDZol7SAMZX43JC019sx6GK0u6xouJBcLfeB4 -OXacTgmSYdTa9RM9fbfVpti01tJ84LV2SyL/VJq/enJF4XQPSynT/tFTn1PAor6o -tEAAd8fjKdJ6LnD5wb92SPHfQfXqI84rFEO8rUNIE/1ErT6DYifDzVCbfD2KZdoF -cOSp7TpD77sY1bs74ocBX5ejKtd+aH99D78bJSMM4pSDZsIEwnomkBHTziubPwJb -OwnATy0LmSMAWOw5rKbsh5nfwCiUTM20xp0t5JeXd+wPVWbpWqI2EnkCEN+RJr9i -7dp/ymDQ+Yt5wrsN3NwoyiexPOG91WQVCADdErHsnglVZZq9Z8Wx7KwecGCUurJ2 -H6lKudv5YOxPnAzqZS5HbpZd/nRTMZh2rdXCr5m2YOuewyYjvM757AkmUpM09zJX -MQ1S67/UX2y8/74TcRF97Ncx9HeELs92innBRXoFitnNguvcO6Esx4BTe1OdU6qR -ER3zAmVf22Le9ciXbu24DN4mleOH+OmBx7X2PqJSYW9GAMTsRB081R6EWKH7romQ -waxFrZ4DJzZ9ltyosEJn5F32StyLrFxpcrdLUoEaclZCv2qka7sZvi0EvovDVEBU -e10jOx9AOwf8Gj2ufhquQ6qgVYCzbP+YrodtkFrXRS3IsljIchj1M2ffB/0bfoUs -rtER9pLvYzCjBPg8IfGLw0o754Qbhh/ReplCRTusP/fQMybvCvfxreS3oyEriu/G -GufRomjewZ8EMHDIgUsLcYo2UHZsfF7tcazgxMGmMvazp4r8vpgrvW/8fIN/6Adu -tF+WjWDTvJLFJCe6O+BFJOWrssNrrra1zGtLC1s8s+Wfpe+bGPL5zpHeebGTwH1U -22eqgJArlEKxrfarz7W5+uHZJHSjF/K9ZvunLGD0n9GOPMpji3UO3zeM8IYoWn7E -/EWK1XbjnssNemeeTZ+sDh+qrD7BOi+vCX1IyBxbfqnQfJZvmcPWpruy1UsO+aIC -0GY8Jr3OL69dDQ21jueJAh8EGAEIAAkFAlC9+dkCGwwACgkQL0VeKCTRjd9HCw/+ -LQSVgLLF4ulYlPCjWIIuQwrPbJfWUVVr2dPUFVM85DCv8gBzk5c121snXh9Swovm -laBbw6ate3BmbXLh64jVE9Za5sbTWi7PCcbO/bpRy4d6oLmitmNw6cq0vjTLxUYy -bwuiJxWREkfxuU85EKdouN062YDevH+/YResmlJrcCE7LRlJFeRlKsrrwBU3BqYd -GgFJjKjQC1peeQ9fj62Y7xfwE9+PXbkiWO5u/Bk8hb1VZH1SoIRU98NHVcp6BVvp -VK0jLAXuSauSczULmpRjbyt1lhaAqivDTWEEZXiNNbRyp17c3nVdPWOcgBr42hdQ -z25CgZgyLCsvu82wuXLKJblrIPJX3Yf+si6KqEWBsmwdOWybsjygaF5HvzgFqAAD -U0goPWoQ71PorP2XOUNp5ZLkBQp5etvtkksjVNMIhnHn8PGMuoxO39EUGlWj2B5l -Cu8tSosAzB1pS8NcLZzoNoI9dOHrmgJmP+GrOUkcf5GhNZbMoj4GNfGBRYX0SZlQ -GuDrwNKYj73C4MWyNnnUFyq8nDHJ/G1NpaF2hiof9RBL4PUU/f92JkceXPBXA8gL -Mz2ig1OButwPPLFGQhWqxXAGrsS3Ny+BhTJfnfIbbkaLLphBpDZm1D9XKbAUvdd1 -RZXoH+FTg9UAW87eqU610npOkT6cRaBxaMK/mDtGNdc= -=JTFu ------END PGP PRIVATE KEY BLOCK----- -""" diff --git a/src/leap/soledad/tests/couchdb.ini.template b/src/leap/soledad/tests/couchdb.ini.template deleted file mode 100644 index 7d0316f0..00000000 --- a/src/leap/soledad/tests/couchdb.ini.template +++ /dev/null @@ -1,222 +0,0 @@ -; etc/couchdb/default.ini.tpl. Generated from default.ini.tpl.in by configure. - -; Upgrading CouchDB will overwrite this file. - -[couchdb] -database_dir = %(tempdir)s/lib -view_index_dir = %(tempdir)s/lib -max_document_size = 4294967296 ; 4 GB -os_process_timeout = 5000 ; 5 seconds. for view and external servers. -max_dbs_open = 100 -delayed_commits = true ; set this to false to ensure an fsync before 201 Created is returned -uri_file = %(tempdir)s/lib/couch.uri -file_compression = snappy - -[database_compaction] -; larger buffer sizes can originate smaller files -doc_buffer_size = 524288 ; value in bytes -checkpoint_after = 5242880 ; checkpoint after every N bytes were written - -[view_compaction] -; larger buffer sizes can originate smaller files -keyvalue_buffer_size = 2097152 ; value in bytes - -[httpd] -port = 0 -bind_address = 127.0.0.1 -authentication_handlers = {couch_httpd_oauth, oauth_authentication_handler}, {couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler} -default_handler = {couch_httpd_db, handle_request} -secure_rewrites = true -vhost_global_handlers = _utils, _uuids, _session, _oauth, _users -allow_jsonp = false -; Options for the MochiWeb HTTP server. -;server_options = [{backlog, 128}, {acceptor_pool_size, 16}] -; For more socket options, consult Erlang's module 'inet' man page. -;socket_options = [{recbuf, 262144}, {sndbuf, 262144}, {nodelay, true}] -log_max_chunk_size = 1000000 - -[log] -file = %(tempdir)s/log/couch.log -level = info -include_sasl = true - -[couch_httpd_auth] -authentication_db = _users -authentication_redirect = /_utils/session.html -require_valid_user = false -timeout = 600 ; number of seconds before automatic logout -auth_cache_size = 50 ; size is number of cache entries -allow_persistent_cookies = false ; set to true to allow persistent cookies - -[couch_httpd_oauth] -; If set to 'true', oauth token and consumer secrets will be looked up -; in the authentication database (_users). These secrets are stored in -; a top level property named "oauth" in user documents. Example: -; { -; "_id": "org.couchdb.user:joe", -; "type": "user", -; "name": "joe", -; "password_sha": "fe95df1ca59a9b567bdca5cbaf8412abd6e06121", -; "salt": "4e170ffeb6f34daecfd814dfb4001a73" -; "roles": ["foo", "bar"], -; "oauth": { -; "consumer_keys": { -; "consumerKey1": "key1Secret", -; "consumerKey2": "key2Secret" -; }, -; "tokens": { -; "token1": "token1Secret", -; "token2": "token2Secret" -; } -; } -; } -use_users_db = false - -[query_servers] -; javascript = %(tempdir)s/server/main.js - - -; Changing reduce_limit to false will disable reduce_limit. -; If you think you're hitting reduce_limit with a "good" reduce function, -; please let us know on the mailing list so we can fine tune the heuristic. -[query_server_config] -reduce_limit = true -os_process_limit = 25 - -[daemons] -view_manager={couch_view, start_link, []} -external_manager={couch_external_manager, start_link, []} -query_servers={couch_query_servers, start_link, []} -vhosts={couch_httpd_vhost, start_link, []} -httpd={couch_httpd, start_link, []} -stats_aggregator={couch_stats_aggregator, start, []} -stats_collector={couch_stats_collector, start, []} -uuids={couch_uuids, start, []} -auth_cache={couch_auth_cache, start_link, []} -replication_manager={couch_replication_manager, start_link, []} -os_daemons={couch_os_daemons, start_link, []} -compaction_daemon={couch_compaction_daemon, start_link, []} - -[httpd_global_handlers] -/ = {couch_httpd_misc_handlers, handle_welcome_req, <<"Welcome">>} - -_all_dbs = {couch_httpd_misc_handlers, handle_all_dbs_req} -_active_tasks = {couch_httpd_misc_handlers, handle_task_status_req} -_config = {couch_httpd_misc_handlers, handle_config_req} -_replicate = {couch_httpd_replicator, handle_req} -_uuids = {couch_httpd_misc_handlers, handle_uuids_req} -_restart = {couch_httpd_misc_handlers, handle_restart_req} -_stats = {couch_httpd_stats_handlers, handle_stats_req} -_log = {couch_httpd_misc_handlers, handle_log_req} -_session = {couch_httpd_auth, handle_session_req} -_oauth = {couch_httpd_oauth, handle_oauth_req} - -[httpd_db_handlers] -_view_cleanup = {couch_httpd_db, handle_view_cleanup_req} -_compact = {couch_httpd_db, handle_compact_req} -_design = {couch_httpd_db, handle_design_req} -_temp_view = {couch_httpd_view, handle_temp_view_req} -_changes = {couch_httpd_db, handle_changes_req} - -; The external module takes an optional argument allowing you to narrow it to a -; single script. Otherwise the script name is inferred from the first path section -; after _external's own path. -; _mypath = {couch_httpd_external, handle_external_req, <<"mykey">>} -; _external = {couch_httpd_external, handle_external_req} - -[httpd_design_handlers] -_view = {couch_httpd_view, handle_view_req} -_show = {couch_httpd_show, handle_doc_show_req} -_list = {couch_httpd_show, handle_view_list_req} -_info = {couch_httpd_db, handle_design_info_req} -_rewrite = {couch_httpd_rewrite, handle_rewrite_req} -_update = {couch_httpd_show, handle_doc_update_req} - -; enable external as an httpd handler, then link it with commands here. -; note, this api is still under consideration. -; [external] -; mykey = /path/to/mycommand - -; Here you can setup commands for CouchDB to manage -; while it is alive. It will attempt to keep each command -; alive if it exits. -; [os_daemons] -; some_daemon_name = /path/to/script -with args - - -[uuids] -; Known algorithms: -; random - 128 bits of random awesome -; All awesome, all the time. -; sequential - monotonically increasing ids with random increments -; First 26 hex characters are random. Last 6 increment in -; random amounts until an overflow occurs. On overflow, the -; random prefix is regenerated and the process starts over. -; utc_random - Time since Jan 1, 1970 UTC with microseconds -; First 14 characters are the time in hex. Last 18 are random. -algorithm = sequential - -[stats] -; rate is in milliseconds -rate = 1000 -; sample intervals are in seconds -samples = [0, 60, 300, 900] - -[attachments] -compression_level = 8 ; from 1 (lowest, fastest) to 9 (highest, slowest), 0 to disable compression -compressible_types = text/*, application/javascript, application/json, application/xml - -[replicator] -db = _replicator -; Maximum replicaton retry count can be a non-negative integer or "infinity". -max_replication_retry_count = 10 -; More worker processes can give higher network throughput but can also -; imply more disk and network IO. -worker_processes = 4 -; With lower batch sizes checkpoints are done more frequently. Lower batch sizes -; also reduce the total amount of used RAM memory. -worker_batch_size = 500 -; Maximum number of HTTP connections per replication. -http_connections = 20 -; HTTP connection timeout per replication. -; Even for very fast/reliable networks it might need to be increased if a remote -; database is too busy. -connection_timeout = 30000 -; If a request fails, the replicator will retry it up to N times. -retries_per_request = 10 -; Some socket options that might boost performance in some scenarios: -; {nodelay, boolean()} -; {sndbuf, integer()} -; {recbuf, integer()} -; {priority, integer()} -; See the `inet` Erlang module's man page for the full list of options. -socket_options = [{keepalive, true}, {nodelay, false}] -; Path to a file containing the user's certificate. -;cert_file = /full/path/to/server_cert.pem -; Path to file containing user's private PEM encoded key. -;key_file = /full/path/to/server_key.pem -; String containing the user's password. Only used if the private keyfile is password protected. -;password = somepassword -; Set to true to validate peer certificates. -verify_ssl_certificates = false -; File containing a list of peer trusted certificates (in the PEM format). -;ssl_trusted_certificates_file = /etc/ssl/certs/ca-certificates.crt -; Maximum peer certificate depth (must be set even if certificate validation is off). -ssl_certificate_max_depth = 3 - -[compaction_daemon] -; The delay, in seconds, between each check for which database and view indexes -; need to be compacted. -check_interval = 300 -; If a database or view index file is smaller then this value (in bytes), -; compaction will not happen. Very small files always have a very high -; fragmentation therefore it's not worth to compact them. -min_file_size = 131072 - -[compactions] -; List of compaction rules for the compaction daemon. - - -;[admins] -;testuser = -hashed-f50a252c12615697c5ed24ec5cd56b05d66fe91e,b05471ba260132953930cf9f97f327f5 -; pass for above user is 'testpass' \ No newline at end of file diff --git a/src/leap/soledad/tests/test_couch.py b/src/leap/soledad/tests/test_couch.py deleted file mode 100644 index bbdf44b4..00000000 --- a/src/leap/soledad/tests/test_couch.py +++ /dev/null @@ -1,298 +0,0 @@ -"""Test ObjectStore backend bits. - -For these tests to run, a couch server has to be running on (default) port -5984. -""" - -import copy -from leap.soledad.backends import couch -from leap.soledad.tests import u1db_tests as tests -from leap.soledad.tests.u1db_tests import test_backends -from leap.soledad.tests.u1db_tests import test_sync -try: - import simplejson as json -except ImportError: - import json # noqa - - -#----------------------------------------------------------------------------- -# A wrapper for running couchdb locally. -#----------------------------------------------------------------------------- - -import re -import os -import tempfile -import subprocess -import time -import unittest - - -# from: https://github.com/smcq/paisley/blob/master/paisley/test/util.py -# TODO: include license of above project. -class CouchDBWrapper(object): - """ - Wrapper for external CouchDB instance which is started and stopped for - testing. - """ - - def start(self): - self.tempdir = tempfile.mkdtemp(suffix='.couch.test') - - path = os.path.join(os.path.dirname(__file__), - 'couchdb.ini.template') - handle = open(path) - conf = handle.read() % { - 'tempdir': self.tempdir, - } - handle.close() - - confPath = os.path.join(self.tempdir, 'test.ini') - handle = open(confPath, 'w') - handle.write(conf) - handle.close() - - # create the dirs from the template - os.mkdir(os.path.join(self.tempdir, 'lib')) - os.mkdir(os.path.join(self.tempdir, 'log')) - args = ['couchdb', '-n' '-a', confPath] - #null = open('/dev/null', 'w') - self.process = subprocess.Popen( - args, env=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - close_fds=True) - # find port - logPath = os.path.join(self.tempdir, 'log', 'couch.log') - while not os.path.exists(logPath): - if self.process.poll() is not None: - raise Exception(""" -couchdb exited with code %d. -stdout: -%s -stderr: -%s""" % ( - self.process.returncode, self.process.stdout.read(), - self.process.stderr.read())) - time.sleep(0.01) - while os.stat(logPath).st_size == 0: - time.sleep(0.01) - PORT_RE = re.compile( - 'Apache CouchDB has started on http://127.0.0.1:(?P\d+)') - - handle = open(logPath) - line = handle.read() - handle.close() - m = PORT_RE.search(line) - if not m: - self.stop() - raise Exception("Cannot find port in line %s" % line) - self.port = int(m.group('port')) - - def stop(self): - self.process.terminate() - self.process.communicate() - os.system("rm -rf %s" % self.tempdir) - - -class CouchDBTestCase(unittest.TestCase): - """ - TestCase base class for tests against a real CouchDB server. - """ - - def setUp(self): - self.wrapper = CouchDBWrapper() - self.wrapper.start() - #self.db = self.wrapper.db - super(CouchDBTestCase, self).setUp() - - def tearDown(self): - self.wrapper.stop() - super(CouchDBTestCase, self).tearDown() - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_common_backend`. -#----------------------------------------------------------------------------- - -class TestCouchBackendImpl(CouchDBTestCase): - - def test__allocate_doc_id(self): - db = couch.CouchDatabase('http://localhost:' + str(self.wrapper.port), - 'u1db_tests') - doc_id1 = db._allocate_doc_id() - self.assertTrue(doc_id1.startswith('D-')) - self.assertEqual(34, len(doc_id1)) - int(doc_id1[len('D-'):], 16) - self.assertNotEqual(doc_id1, db._allocate_doc_id()) - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_backends`. -#----------------------------------------------------------------------------- - -def make_couch_database_for_test(test, replica_uid): - port = str(test.wrapper.port) - return couch.CouchDatabase('http://localhost:' + port, replica_uid, - replica_uid=replica_uid or 'test') - - -def copy_couch_database_for_test(test, db): - port = str(test.wrapper.port) - new_db = couch.CouchDatabase('http://localhost:' + port, - db._replica_uid + '_copy', - replica_uid=db._replica_uid or 'test') - gen, docs = db.get_all_docs(include_deleted=True) - for doc in docs: - new_db._put_doc(doc) - new_db._transaction_log = copy.deepcopy(db._transaction_log) - new_db._conflicts = copy.deepcopy(db._conflicts) - new_db._other_generations = copy.deepcopy(db._other_generations) - new_db._indexes = copy.deepcopy(db._indexes) - new_db._set_u1db_data() - return new_db - - -COUCH_SCENARIOS = [ - ('couch', {'make_database_for_test': make_couch_database_for_test, - 'copy_database_for_test': copy_couch_database_for_test, - 'make_document_for_test': tests.make_document_for_test, }), -] - - -class CouchTests(test_backends.AllDatabaseTests, CouchDBTestCase): - - scenarios = COUCH_SCENARIOS - - def tearDown(self): - self.db.delete_database() - super(CouchTests, self).tearDown() - - -class CouchDatabaseTests(test_backends.LocalDatabaseTests, CouchDBTestCase): - - scenarios = COUCH_SCENARIOS - - def tearDown(self): - self.db.delete_database() - super(CouchDatabaseTests, self).tearDown() - - -class CouchValidateGenNTransIdTests( - test_backends.LocalDatabaseValidateGenNTransIdTests, CouchDBTestCase): - - scenarios = COUCH_SCENARIOS - - def tearDown(self): - self.db.delete_database() - super(CouchValidateGenNTransIdTests, self).tearDown() - - -class CouchValidateSourceGenTests( - test_backends.LocalDatabaseValidateSourceGenTests, CouchDBTestCase): - - scenarios = COUCH_SCENARIOS - - def tearDown(self): - self.db.delete_database() - super(CouchValidateSourceGenTests, self).tearDown() - - -class CouchWithConflictsTests( - test_backends.LocalDatabaseWithConflictsTests, CouchDBTestCase): - - scenarios = COUCH_SCENARIOS - - def tearDown(self): - self.db.delete_database() - super(CouchWithConflictsTests, self).tearDown() - - -# Notice: the CouchDB backend is currently used for storing encrypted data in -# the server, so indexing makes no sense. Thus, we ignore index testing for -# now. - -class CouchIndexTests(test_backends.DatabaseIndexTests, CouchDBTestCase): - - scenarios = COUCH_SCENARIOS - - def tearDown(self): - self.db.delete_database() - super(CouchIndexTests, self).tearDown() - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_sync`. -#----------------------------------------------------------------------------- - -target_scenarios = [ - ('local', {'create_db_and_target': test_sync._make_local_db_and_target}), ] - - -simple_doc = tests.simple_doc -nested_doc = tests.nested_doc - - -class CouchDatabaseSyncTargetTests(test_sync.DatabaseSyncTargetTests, - CouchDBTestCase): - - scenarios = (tests.multiply_scenarios(COUCH_SCENARIOS, target_scenarios)) - - def tearDown(self): - self.db.delete_database() - super(CouchDatabaseSyncTargetTests, self).tearDown() - - def test_sync_exchange_returns_many_new_docs(self): - # This test was replicated to allow dictionaries to be compared after - # JSON expansion (because one dictionary may have many different - # serialized representations). - doc = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(nested_doc) - self.assertTransactionLog([doc.doc_id, doc2.doc_id], self.db) - new_gen, _ = self.st.sync_exchange( - [], 'other-replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertTransactionLog([doc.doc_id, doc2.doc_id], self.db) - self.assertEqual(2, new_gen) - self.assertEqual( - [(doc.doc_id, doc.rev, json.loads(simple_doc), 1), - (doc2.doc_id, doc2.rev, json.loads(nested_doc), 2)], - [c[:-3] + (json.loads(c[-3]), c[-2]) for c in self.other_changes]) - if self.whitebox: - self.assertEqual( - self.db._last_exchange_log['return'], - {'last_gen': 2, 'docs': - [(doc.doc_id, doc.rev), (doc2.doc_id, doc2.rev)]}) - - -sync_scenarios = [] -for name, scenario in COUCH_SCENARIOS: - scenario = dict(scenario) - scenario['do_sync'] = test_sync.sync_via_synchronizer - sync_scenarios.append((name, scenario)) - scenario = dict(scenario) - - -class CouchDatabaseSyncTests(test_sync.DatabaseSyncTests, CouchDBTestCase): - - scenarios = sync_scenarios - - def setUp(self): - self.db = None - self.db1 = None - self.db2 = None - self.db3 = None - super(CouchDatabaseSyncTests, self).setUp() - - def tearDown(self): - self.db and self.db.delete_database() - self.db1 and self.db1.delete_database() - self.db2 and self.db2.delete_database() - self.db3 and self.db3.delete_database() - db = self.create_database('test1_copy', 'source') - db.delete_database() - db = self.create_database('test2_copy', 'target') - db.delete_database() - db = self.create_database('test3', 'target') - db.delete_database() - super(CouchDatabaseSyncTests, self).tearDown() - - -load_tests = tests.load_with_scenarios diff --git a/src/leap/soledad/tests/test_encrypted.py b/src/leap/soledad/tests/test_encrypted.py deleted file mode 100644 index 9fc81bc3..00000000 --- a/src/leap/soledad/tests/test_encrypted.py +++ /dev/null @@ -1,15 +0,0 @@ -from leap.soledad.backends.leap_backend import LeapDocument -from leap.soledad.tests import BaseSoledadTest - - -class EncryptedSyncTestCase(BaseSoledadTest): - - def test_get_set_encrypted(self): - doc1 = LeapDocument(soledad=self._soledad) - doc1.content = {'key': 'val'} - doc2 = LeapDocument(doc_id=doc1.doc_id, - encrypted_json=doc1.get_encrypted_json(), - soledad=self._soledad) - res1 = doc1.get_json() - res2 = doc2.get_json() - self.assertEqual(res1, res2, 'incorrect document encryption') diff --git a/src/leap/soledad/tests/test_leap_backend.py b/src/leap/soledad/tests/test_leap_backend.py deleted file mode 100644 index a061533c..00000000 --- a/src/leap/soledad/tests/test_leap_backend.py +++ /dev/null @@ -1,207 +0,0 @@ -"""Test ObjectStore backend bits. - -For these tests to run, a leap server has to be running on (default) port -5984. -""" - -import u1db -from leap.soledad.backends import leap_backend -from leap.soledad.tests import u1db_tests as tests -from leap.soledad.tests.u1db_tests.test_remote_sync_target import ( - make_http_app, - make_oauth_http_app, -) -from leap.soledad.tests import BaseSoledadTest -from leap.soledad.tests.u1db_tests import test_backends -from leap.soledad.tests.u1db_tests import test_http_database -from leap.soledad.tests.u1db_tests import test_http_client -from leap.soledad.tests.u1db_tests import test_document -from leap.soledad.tests.u1db_tests import test_remote_sync_target -from leap.soledad.tests.u1db_tests import test_https - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_backends`. -#----------------------------------------------------------------------------- - -def make_leap_document_for_test(test, doc_id, rev, content, - has_conflicts=False): - return leap_backend.LeapDocument( - doc_id, rev, content, has_conflicts=has_conflicts, - soledad=test._soledad) - - -def make_leap_encrypted_document_for_test(test, doc_id, rev, encrypted_content, - has_conflicts=False): - return leap_backend.LeapDocument( - doc_id, rev, encrypted_json=encrypted_content, - has_conflicts=has_conflicts, - soledad=test._soledad) - - -LEAP_SCENARIOS = [ - ('http', { - 'make_database_for_test': test_backends.make_http_database_for_test, - 'copy_database_for_test': test_backends.copy_http_database_for_test, - 'make_document_for_test': make_leap_document_for_test, - 'make_app_with_state': make_http_app}), -] - - -class LeapTests(test_backends.AllDatabaseTests, BaseSoledadTest): - - scenarios = LEAP_SCENARIOS - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_http_client`. -#----------------------------------------------------------------------------- - -class TestLeapClientBase(test_http_client.TestHTTPClientBase): - pass - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_document`. -#----------------------------------------------------------------------------- - -class TestLeapDocument(test_document.TestDocument, BaseSoledadTest): - - scenarios = ([( - 'leap', {'make_document_for_test': make_leap_document_for_test})]) - - -class TestLeapPyDocument(test_document.TestPyDocument, BaseSoledadTest): - - scenarios = ([( - 'leap', {'make_document_for_test': make_leap_document_for_test})]) - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_remote_sync_target`. -#----------------------------------------------------------------------------- - -class TestLeapSyncTargetBasics( - test_remote_sync_target.TestHTTPSyncTargetBasics): - - def test_parse_url(self): - remote_target = leap_backend.LeapSyncTarget('http://127.0.0.1:12345/') - self.assertEqual('http', remote_target._url.scheme) - self.assertEqual('127.0.0.1', remote_target._url.hostname) - self.assertEqual(12345, remote_target._url.port) - self.assertEqual('/', remote_target._url.path) - - -class TestLeapParsingSyncStream(test_remote_sync_target.TestParsingSyncStream): - - def test_wrong_start(self): - tgt = leap_backend.LeapSyncTarget("http://foo/foo") - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "{}\r\n]", None) - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "\r\n{}\r\n]", None) - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "", None) - - def test_wrong_end(self): - tgt = leap_backend.LeapSyncTarget("http://foo/foo") - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "[\r\n{}", None) - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "[\r\n", None) - - def test_missing_comma(self): - tgt = leap_backend.LeapSyncTarget("http://foo/foo") - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, - '[\r\n{}\r\n{"id": "i", "rev": "r", ' - '"content": "c", "gen": 3}\r\n]', None) - - def test_no_entries(self): - tgt = leap_backend.LeapSyncTarget("http://foo/foo") - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "[\r\n]", None) - - def test_extra_comma(self): - tgt = leap_backend.LeapSyncTarget("http://foo/foo") - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "[\r\n{},\r\n]", None) - - self.assertRaises(leap_backend.NoSoledadInstance, - tgt._parse_sync_stream, - '[\r\n{},\r\n{"id": "i", "rev": "r", ' - '"content": "{}", "gen": 3, "trans_id": "T-sid"}' - ',\r\n]', - lambda doc, gen, trans_id: None) - - def test_error_in_stream(self): - tgt = leap_backend.LeapSyncTarget("http://foo/foo") - - self.assertRaises(u1db.errors.Unavailable, - tgt._parse_sync_stream, - '[\r\n{"new_generation": 0},' - '\r\n{"error": "unavailable"}\r\n', None) - - self.assertRaises(u1db.errors.Unavailable, - tgt._parse_sync_stream, - '[\r\n{"error": "unavailable"}\r\n', None) - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, - '[\r\n{"error": "?"}\r\n', None) - - -def leap_sync_target(test, path): - return leap_backend.LeapSyncTarget(test.getURL(path)) - - -def oauth_leap_sync_target(test, path): - st = leap_sync_target(test, '~/' + path) - st.set_oauth_credentials(tests.consumer1.key, tests.consumer1.secret, - tests.token1.key, tests.token1.secret) - return st - - -class TestRemoteSyncTargets(tests.TestCaseWithServer): - - scenarios = [ - ('http', {'make_app_with_state': make_http_app, - 'make_document_for_test': make_leap_document_for_test, - 'sync_target': leap_sync_target}), - ('oauth_http', {'make_app_with_state': make_oauth_http_app, - 'make_document_for_test': make_leap_document_for_test, - 'sync_target': oauth_leap_sync_target}), - ] - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_https`. -#----------------------------------------------------------------------------- - -def oauth_https_sync_target(test, host, path): - _, port = test.server.server_address - st = leap_backend.LeapSyncTarget('https://%s:%d/~/%s' % (host, port, path)) - st.set_oauth_credentials(tests.consumer1.key, tests.consumer1.secret, - tests.token1.key, tests.token1.secret) - return st - - -class TestLeapSyncTargetHttpsSupport(test_https.TestHttpSyncTargetHttpsSupport, - BaseSoledadTest): - - scenarios = [ - ('oauth_https', {'server_def': test_https.https_server_def, - 'make_app_with_state': make_oauth_http_app, - 'make_document_for_test': make_leap_document_for_test, - 'sync_target': oauth_https_sync_target, - }), ] - -load_tests = tests.load_with_scenarios diff --git a/src/leap/soledad/tests/test_sqlcipher.py b/src/leap/soledad/tests/test_sqlcipher.py deleted file mode 100644 index ee9b38dd..00000000 --- a/src/leap/soledad/tests/test_sqlcipher.py +++ /dev/null @@ -1,501 +0,0 @@ -"""Test sqlcipher backend internals.""" - -import os -import time -from pysqlcipher import dbapi2 -import unittest -from StringIO import StringIO -import threading - -# u1db stuff. -from u1db import ( - errors, - query_parser, - sync, -) -from u1db.backends.sqlite_backend import SQLitePartialExpandDatabase - -# soledad stuff. -from leap.soledad.backends.sqlcipher import ( - SQLCipherDatabase, - DatabaseIsNotEncrypted, -) -from leap.soledad.backends.sqlcipher import open as u1db_open -from leap.soledad.backends.leap_backend import LeapDocument - -# u1db tests stuff. -from leap.soledad.tests import u1db_tests as tests -from leap.soledad.tests.u1db_tests import test_sqlite_backend -from leap.soledad.tests.u1db_tests import test_backends -from leap.soledad.tests.u1db_tests import test_open -from leap.soledad.tests.u1db_tests import test_sync -from leap.soledad.backends.leap_backend import LeapSyncTarget -from leap.testing.basetest import BaseLeapTest - -PASSWORD = '123456' - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_common_backend`. -#----------------------------------------------------------------------------- - -class TestSQLCipherBackendImpl(tests.TestCase): - - def test__allocate_doc_id(self): - db = SQLCipherDatabase(':memory:', PASSWORD) - doc_id1 = db._allocate_doc_id() - self.assertTrue(doc_id1.startswith('D-')) - self.assertEqual(34, len(doc_id1)) - int(doc_id1[len('D-'):], 16) - self.assertNotEqual(doc_id1, db._allocate_doc_id()) - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_backends`. -#----------------------------------------------------------------------------- - -def make_sqlcipher_database_for_test(test, replica_uid): - db = SQLCipherDatabase(':memory:', PASSWORD) - db._set_replica_uid(replica_uid) - return db - - -def copy_sqlcipher_database_for_test(test, db): - # DO NOT COPY OR REUSE THIS CODE OUTSIDE TESTS: COPYING U1DB DATABASES IS - # THE WRONG THING TO DO, THE ONLY REASON WE DO SO HERE IS TO TEST THAT WE - # CORRECTLY DETECT IT HAPPENING SO THAT WE CAN RAISE ERRORS RATHER THAN - # CORRUPT USER DATA. USE SYNC INSTEAD, OR WE WILL SEND NINJA TO YOUR - # HOUSE. - new_db = SQLCipherDatabase(':memory:', PASSWORD) - tmpfile = StringIO() - for line in db._db_handle.iterdump(): - if not 'sqlite_sequence' in line: # work around bug in iterdump - tmpfile.write('%s\n' % line) - tmpfile.seek(0) - new_db._db_handle = dbapi2.connect(':memory:') - new_db._db_handle.cursor().executescript(tmpfile.read()) - new_db._db_handle.commit() - new_db._set_replica_uid(db._replica_uid) - new_db._factory = db._factory - return new_db - - -def make_document_for_test(test, doc_id, rev, content, has_conflicts=False): - return LeapDocument(doc_id, rev, content, has_conflicts=has_conflicts) - - -SQLCIPHER_SCENARIOS = [ - ('sqlcipher', {'make_database_for_test': make_sqlcipher_database_for_test, - 'copy_database_for_test': copy_sqlcipher_database_for_test, - 'make_document_for_test': make_document_for_test, }), -] - - -class SQLCipherTests(test_backends.AllDatabaseTests): - scenarios = SQLCIPHER_SCENARIOS - - -class SQLCipherDatabaseTests(test_backends.LocalDatabaseTests): - scenarios = SQLCIPHER_SCENARIOS - - -class SQLCipherValidateGenNTransIdTests( - test_backends.LocalDatabaseValidateGenNTransIdTests): - scenarios = SQLCIPHER_SCENARIOS - - -class SQLCipherValidateSourceGenTests( - test_backends.LocalDatabaseValidateSourceGenTests): - scenarios = SQLCIPHER_SCENARIOS - - -class SQLCipherWithConflictsTests( - test_backends.LocalDatabaseWithConflictsTests): - scenarios = SQLCIPHER_SCENARIOS - - -class SQLCipherIndexTests(test_backends.DatabaseIndexTests): - scenarios = SQLCIPHER_SCENARIOS - - -load_tests = tests.load_with_scenarios - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_sqlite_backend`. -#----------------------------------------------------------------------------- - -class TestSQLCipherDatabase(test_sqlite_backend.TestSQLiteDatabase): - - def test_atomic_initialize(self): - tmpdir = self.createTempDir() - dbname = os.path.join(tmpdir, 'atomic.db') - - t2 = None # will be a thread - - class SQLCipherDatabaseTesting(SQLCipherDatabase): - _index_storage_value = "testing" - - def __init__(self, dbname, ntry): - self._try = ntry - self._is_initialized_invocations = 0 - super(SQLCipherDatabaseTesting, self).__init__(dbname, PASSWORD) - - def _is_initialized(self, c): - res = super(SQLCipherDatabaseTesting, self)._is_initialized(c) - if self._try == 1: - self._is_initialized_invocations += 1 - if self._is_initialized_invocations == 2: - t2.start() - # hard to do better and have a generic test - time.sleep(0.05) - return res - - outcome2 = [] - - def second_try(): - try: - db2 = SQLCipherDatabaseTesting(dbname, 2) - except Exception, e: - outcome2.append(e) - else: - outcome2.append(db2) - - t2 = threading.Thread(target=second_try) - db1 = SQLCipherDatabaseTesting(dbname, 1) - t2.join() - - self.assertIsInstance(outcome2[0], SQLCipherDatabaseTesting) - db2 = outcome2[0] - self.assertTrue(db2._is_initialized(db1._get_sqlite_handle().cursor())) - - -class TestAlternativeDocument(LeapDocument): - """A (not very) alternative implementation of Document.""" - - -class TestSQLCipherPartialExpandDatabase( - test_sqlite_backend.TestSQLitePartialExpandDatabase): - - # The following tests had to be cloned from u1db because they all - # instantiate the backend directly, so we need to change that in order to - # our backend be instantiated in place. - - def setUp(self): - super(test_sqlite_backend.TestSQLitePartialExpandDatabase, - self).setUp() - self.db = SQLCipherDatabase(':memory:', PASSWORD) - self.db._set_replica_uid('test') - - def test_default_replica_uid(self): - self.db = SQLCipherDatabase(':memory:', PASSWORD) - self.assertIsNot(None, self.db._replica_uid) - self.assertEqual(32, len(self.db._replica_uid)) - int(self.db._replica_uid, 16) - - def test__parse_index(self): - self.db = SQLCipherDatabase(':memory:', PASSWORD) - g = self.db._parse_index_definition('fieldname') - self.assertIsInstance(g, query_parser.ExtractField) - self.assertEqual(['fieldname'], g.field) - - def test__update_indexes(self): - self.db = SQLCipherDatabase(':memory:', PASSWORD) - g = self.db._parse_index_definition('fieldname') - c = self.db._get_sqlite_handle().cursor() - self.db._update_indexes('doc-id', {'fieldname': 'val'}, - [('fieldname', g)], c) - c.execute('SELECT doc_id, field_name, value FROM document_fields') - self.assertEqual([('doc-id', 'fieldname', 'val')], - c.fetchall()) - - def test__set_replica_uid(self): - # Start from scratch, so that replica_uid isn't set. - self.db = SQLCipherDatabase(':memory:', PASSWORD) - self.assertIsNot(None, self.db._real_replica_uid) - self.assertIsNot(None, self.db._replica_uid) - self.db._set_replica_uid('foo') - c = self.db._get_sqlite_handle().cursor() - c.execute("SELECT value FROM u1db_config WHERE name='replica_uid'") - self.assertEqual(('foo',), c.fetchone()) - self.assertEqual('foo', self.db._real_replica_uid) - self.assertEqual('foo', self.db._replica_uid) - self.db._close_sqlite_handle() - self.assertEqual('foo', self.db._replica_uid) - - def test__open_database(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/test.sqlite' - SQLCipherDatabase(path, PASSWORD) - db2 = SQLCipherDatabase._open_database(path, PASSWORD) - self.assertIsInstance(db2, SQLCipherDatabase) - - def test__open_database_with_factory(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/test.sqlite' - SQLCipherDatabase(path, PASSWORD) - db2 = SQLCipherDatabase._open_database( - path, PASSWORD, - document_factory=TestAlternativeDocument) - doc = db2.create_doc({}) - self.assertTrue(isinstance(doc, LeapDocument)) - - def test__open_database_non_existent(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/non-existent.sqlite' - self.assertRaises(errors.DatabaseDoesNotExist, - SQLCipherDatabase._open_database, - path, PASSWORD) - - def test__open_database_during_init(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/initialised.db' - db = SQLCipherDatabase.__new__( - SQLCipherDatabase) - db._db_handle = dbapi2.connect(path) # db is there but not yet init-ed - c = db._db_handle.cursor() - c.execute('PRAGMA key="%s"' % PASSWORD) - self.addCleanup(db.close) - observed = [] - - class SQLiteDatabaseTesting(SQLCipherDatabase): - WAIT_FOR_PARALLEL_INIT_HALF_INTERVAL = 0.1 - - @classmethod - def _which_index_storage(cls, c): - res = super(SQLiteDatabaseTesting, cls)._which_index_storage(c) - db._ensure_schema() # init db - observed.append(res[0]) - return res - - db2 = SQLiteDatabaseTesting._open_database(path, PASSWORD) - self.addCleanup(db2.close) - self.assertIsInstance(db2, SQLCipherDatabase) - self.assertEqual( - [None, - SQLCipherDatabase._index_storage_value], - observed) - - def test__open_database_invalid(self): - class SQLiteDatabaseTesting(SQLCipherDatabase): - WAIT_FOR_PARALLEL_INIT_HALF_INTERVAL = 0.1 - temp_dir = self.createTempDir(prefix='u1db-test-') - path1 = temp_dir + '/invalid1.db' - with open(path1, 'wb') as f: - f.write("") - self.assertRaises(dbapi2.OperationalError, - SQLiteDatabaseTesting._open_database, path1, - PASSWORD) - with open(path1, 'wb') as f: - f.write("invalid") - self.assertRaises(dbapi2.DatabaseError, - SQLiteDatabaseTesting._open_database, path1, - PASSWORD) - - - def test_open_database_existing(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/existing.sqlite' - SQLCipherDatabase(path, PASSWORD) - db2 = SQLCipherDatabase.open_database(path, PASSWORD, create=False) - self.assertIsInstance(db2, SQLCipherDatabase) - - def test_open_database_with_factory(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/existing.sqlite' - SQLCipherDatabase(path, PASSWORD) - db2 = SQLCipherDatabase.open_database( - path, PASSWORD, create=False, - document_factory=TestAlternativeDocument) - doc = db2.create_doc({}) - self.assertTrue(isinstance(doc, LeapDocument)) - - def test_open_database_create(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/new.sqlite' - SQLCipherDatabase.open_database(path, PASSWORD, create=True) - db2 = SQLCipherDatabase.open_database(path, PASSWORD, create=False) - self.assertIsInstance(db2, SQLCipherDatabase) - - def test_create_database_initializes_schema(self): - # This test had to be cloned because our implementation of SQLCipher - # backend is referenced with an index_storage_value that includes the - # word "encrypted". See u1db's sqlite_backend and our - # sqlcipher_backend for reference. - raw_db = self.db._get_sqlite_handle() - c = raw_db.cursor() - c.execute("SELECT * FROM u1db_config") - config = dict([(r[0], r[1]) for r in c.fetchall()]) - self.assertEqual({'sql_schema': '0', 'replica_uid': 'test', - 'index_storage': 'expand referenced encrypted'}, - config) - - def test_store_syncable(self): - doc = self.db.create_doc_from_json(tests.simple_doc) - # assert that docs are syncable by default - self.assertEqual(True, doc.syncable) - # assert that we can store syncable = False - doc.syncable = False - self.db.put_doc(doc) - self.assertEqual(False, self.db.get_doc(doc.doc_id).syncable) - # assert that we can store syncable = True - doc.syncable = True - self.db.put_doc(doc) - self.assertEqual(True, self.db.get_doc(doc.doc_id).syncable) - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_open`. -#----------------------------------------------------------------------------- - -class SQLCipherOpen(test_open.TestU1DBOpen): - - def test_open_no_create(self): - self.assertRaises(errors.DatabaseDoesNotExist, - u1db_open, self.db_path, - password=PASSWORD, - create=False) - self.assertFalse(os.path.exists(self.db_path)) - - def test_open_create(self): - db = u1db_open(self.db_path, password=PASSWORD, create=True) - self.addCleanup(db.close) - self.assertTrue(os.path.exists(self.db_path)) - self.assertIsInstance(db, SQLCipherDatabase) - - def test_open_with_factory(self): - db = u1db_open(self.db_path, password=PASSWORD, create=True, - document_factory=TestAlternativeDocument) - self.addCleanup(db.close) - doc = db.create_doc({}) - self.assertTrue(isinstance(doc, LeapDocument)) - - def test_open_existing(self): - db = SQLCipherDatabase(self.db_path, PASSWORD) - self.addCleanup(db.close) - doc = db.create_doc_from_json(tests.simple_doc) - # Even though create=True, we shouldn't wipe the db - db2 = u1db_open(self.db_path, password=PASSWORD, create=True) - self.addCleanup(db2.close) - doc2 = db2.get_doc(doc.doc_id) - self.assertEqual(doc, doc2) - - def test_open_existing_no_create(self): - db = SQLCipherDatabase(self.db_path, PASSWORD) - self.addCleanup(db.close) - db2 = u1db_open(self.db_path, password=PASSWORD, create=False) - self.addCleanup(db2.close) - self.assertIsInstance(db2, SQLCipherDatabase) - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_sync`. -#----------------------------------------------------------------------------- - -sync_scenarios = [] -for name, scenario in SQLCIPHER_SCENARIOS: - scenario = dict(scenario) - scenario['do_sync'] = test_sync.sync_via_synchronizer - sync_scenarios.append((name, scenario)) - scenario = dict(scenario) - - -def sync_via_synchronizer_and_leap(test, db_source, db_target, - trace_hook=None, trace_hook_shallow=None): - if trace_hook: - test.skipTest("full trace hook unsupported over http") - path = test._http_at[db_target] - target = LeapSyncTarget.connect(test.getURL(path)) - if trace_hook_shallow: - target._set_trace_hook_shallow(trace_hook_shallow) - return sync.Synchronizer(db_source, target).sync() - - -sync_scenarios.append(('pyleap', { - 'make_database_for_test': test_sync.make_database_for_http_test, - 'copy_database_for_test': test_sync.copy_database_for_http_test, - 'make_document_for_test': tests.make_document_for_test, - 'make_app_with_state': tests.test_remote_sync_target.make_http_app, - 'do_sync': sync_via_synchronizer_and_leap, -})) - - -class SQLCipherDatabaseSyncTests(test_sync.DatabaseSyncTests): - - scenarios = sync_scenarios - - -def _make_local_db_and_leap_target(test, path='test'): - test.startServer() - db = test.request_state._create_database(os.path.basename(path)) - st = LeapSyncTarget.connect(test.getURL(path)) - return db, st - - -target_scenarios = [ - ('leap', { - 'create_db_and_target': _make_local_db_and_leap_target, - 'make_app_with_state': tests.test_remote_sync_target.make_http_app}), -] - - -class SQLCipherSyncTargetTests(test_sync.DatabaseSyncTargetTests): - - scenarios = (tests.multiply_scenarios(SQLCIPHER_SCENARIOS, - target_scenarios)) - - -#----------------------------------------------------------------------------- -# Tests for actual encryption of the database -#----------------------------------------------------------------------------- - -class SQLCipherEncryptionTest(BaseLeapTest): - - def delete_dbfiles(self): - for dbfile in [self.DB_FILE]: - if os.path.exists(dbfile): - os.unlink(dbfile) - - def setUp(self): - self.DB_FILE = self.tempdir + '/test.db' - self.delete_dbfiles() - - def tearDown(self): - self.delete_dbfiles() - - def test_try_to_open_encrypted_db_with_sqlite_backend(self): - db = SQLCipherDatabase(self.DB_FILE, PASSWORD) - doc = db.create_doc_from_json(tests.simple_doc) - db.close() - try: - # trying to open an encrypted database with the regular u1db - # backend should raise a DatabaseError exception. - SQLitePartialExpandDatabase(self.DB_FILE, - document_factory=LeapDocument) - raise DatabaseIsNotEncrypted() - except dbapi2.DatabaseError: - # at this point we know that the regular U1DB sqlcipher backend - # did not succeed on opening the database, so it was indeed - # encrypted. - db = SQLCipherDatabase(self.DB_FILE, PASSWORD) - doc = db.get_doc(doc.doc_id) - self.assertEqual(tests.simple_doc, doc.get_json(), - 'decrypted content mismatch') - - def test_try_to_open_raw_db_with_sqlcipher_backend(self): - db = SQLitePartialExpandDatabase(self.DB_FILE, - document_factory=LeapDocument) - db.create_doc_from_json(tests.simple_doc) - db.close() - try: - # trying to open the a non-encrypted database with sqlcipher - # backend should raise a DatabaseIsNotEncrypted exception. - SQLCipherDatabase(self.DB_FILE, PASSWORD) - raise db1pi2.DatabaseError( - "SQLCipher backend should not be able to open non-encrypted " - "dbs.") - except DatabaseIsNotEncrypted: - pass - - -load_tests = tests.load_with_scenarios diff --git a/src/leap/soledad/tests/u1db_tests/README b/src/leap/soledad/tests/u1db_tests/README deleted file mode 100644 index 605f01fa..00000000 --- a/src/leap/soledad/tests/u1db_tests/README +++ /dev/null @@ -1,34 +0,0 @@ -General info ------------- - -Test files in this directory are derived from u1db-0.1.4 tests. The main -difference is that: - - (1) they include the test infrastructure packed with soledad; and - (2) they do not include c_backend_wrapper testing. - -Dependencies ------------- - -u1db tests depend on the following python packages: - - nose2 - unittest2 - mercurial - hgtools - testtools - discover - oauth - testscenarios - dirspec - paste - routes - simplejson - cython - -Running tests -------------- - -Use nose2 to run tests: - - nose2 leap.soledad.tests.u1db_tests diff --git a/src/leap/soledad/tests/u1db_tests/__init__.py b/src/leap/soledad/tests/u1db_tests/__init__.py deleted file mode 100644 index 43304b43..00000000 --- a/src/leap/soledad/tests/u1db_tests/__init__.py +++ /dev/null @@ -1,421 +0,0 @@ -# Copyright 2011-2012 Canonical Ltd. -# -# This file is part of u1db. -# -# u1db is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 -# as published by the Free Software Foundation. -# -# u1db is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with u1db. If not, see . - -"""Test infrastructure for U1DB""" - -import copy -import shutil -import socket -import tempfile -import threading - -try: - import simplejson as json -except ImportError: - import json # noqa - -from wsgiref import simple_server - -from oauth import oauth -from pysqlcipher import dbapi2 -from StringIO import StringIO - -import testscenarios -import testtools - -from u1db import ( - errors, - Document, -) -from u1db.backends import ( - inmemory, - sqlite_backend, -) -from u1db.remote import ( - server_state, -) - - -class TestCase(testtools.TestCase): - - def createTempDir(self, prefix='u1db-tmp-'): - """Create a temporary directory to do some work in. - - This directory will be scheduled for cleanup when the test ends. - """ - tempdir = tempfile.mkdtemp(prefix=prefix) - self.addCleanup(shutil.rmtree, tempdir) - return tempdir - - def make_document(self, doc_id, doc_rev, content, has_conflicts=False): - return self.make_document_for_test( - self, doc_id, doc_rev, content, has_conflicts) - - def make_document_for_test(self, test, doc_id, doc_rev, content, - has_conflicts): - return make_document_for_test( - test, doc_id, doc_rev, content, has_conflicts) - - def assertGetDoc(self, db, doc_id, doc_rev, content, has_conflicts): - """Assert that the document in the database looks correct.""" - exp_doc = self.make_document(doc_id, doc_rev, content, - has_conflicts=has_conflicts) - self.assertEqual(exp_doc, db.get_doc(doc_id)) - - def assertGetDocIncludeDeleted(self, db, doc_id, doc_rev, content, - has_conflicts): - """Assert that the document in the database looks correct.""" - exp_doc = self.make_document(doc_id, doc_rev, content, - has_conflicts=has_conflicts) - self.assertEqual(exp_doc, db.get_doc(doc_id, include_deleted=True)) - - def assertGetDocConflicts(self, db, doc_id, conflicts): - """Assert what conflicts are stored for a given doc_id. - - :param conflicts: A list of (doc_rev, content) pairs. - The first item must match the first item returned from the - database, however the rest can be returned in any order. - """ - if conflicts: - conflicts = [(rev, - (json.loads(cont) if isinstance(cont, basestring) - else cont)) for (rev, cont) in conflicts] - conflicts = conflicts[:1] + sorted(conflicts[1:]) - actual = db.get_doc_conflicts(doc_id) - if actual: - actual = [ - (doc.rev, (json.loads(doc.get_json()) - if doc.get_json() is not None else None)) - for doc in actual] - actual = actual[:1] + sorted(actual[1:]) - self.assertEqual(conflicts, actual) - - -def multiply_scenarios(a_scenarios, b_scenarios): - """Create the cross-product of scenarios.""" - - all_scenarios = [] - for a_name, a_attrs in a_scenarios: - for b_name, b_attrs in b_scenarios: - name = '%s,%s' % (a_name, b_name) - attrs = dict(a_attrs) - attrs.update(b_attrs) - all_scenarios.append((name, attrs)) - return all_scenarios - - -simple_doc = '{"key": "value"}' -nested_doc = '{"key": "value", "sub": {"doc": "underneath"}}' - - -def make_memory_database_for_test(test, replica_uid): - return inmemory.InMemoryDatabase(replica_uid) - - -def copy_memory_database_for_test(test, db): - # DO NOT COPY OR REUSE THIS CODE OUTSIDE TESTS: COPYING U1DB DATABASES IS - # THE WRONG THING TO DO, THE ONLY REASON WE DO SO HERE IS TO TEST THAT WE - # CORRECTLY DETECT IT HAPPENING SO THAT WE CAN RAISE ERRORS RATHER THAN - # CORRUPT USER DATA. USE SYNC INSTEAD, OR WE WILL SEND NINJA TO YOUR - # HOUSE. - new_db = inmemory.InMemoryDatabase(db._replica_uid) - new_db._transaction_log = db._transaction_log[:] - new_db._docs = copy.deepcopy(db._docs) - new_db._conflicts = copy.deepcopy(db._conflicts) - new_db._indexes = copy.deepcopy(db._indexes) - new_db._factory = db._factory - return new_db - - -def make_sqlite_partial_expanded_for_test(test, replica_uid): - db = sqlite_backend.SQLitePartialExpandDatabase(':memory:') - db._set_replica_uid(replica_uid) - return db - - -def copy_sqlite_partial_expanded_for_test(test, db): - # DO NOT COPY OR REUSE THIS CODE OUTSIDE TESTS: COPYING U1DB DATABASES IS - # THE WRONG THING TO DO, THE ONLY REASON WE DO SO HERE IS TO TEST THAT WE - # CORRECTLY DETECT IT HAPPENING SO THAT WE CAN RAISE ERRORS RATHER THAN - # CORRUPT USER DATA. USE SYNC INSTEAD, OR WE WILL SEND NINJA TO YOUR - # HOUSE. - new_db = sqlite_backend.SQLitePartialExpandDatabase(':memory:') - tmpfile = StringIO() - for line in db._db_handle.iterdump(): - if not 'sqlite_sequence' in line: # work around bug in iterdump - tmpfile.write('%s\n' % line) - tmpfile.seek(0) - new_db._db_handle = dbapi2.connect(':memory:') - new_db._db_handle.cursor().executescript(tmpfile.read()) - new_db._db_handle.commit() - new_db._set_replica_uid(db._replica_uid) - new_db._factory = db._factory - return new_db - - -def make_document_for_test(test, doc_id, rev, content, has_conflicts=False): - return Document(doc_id, rev, content, has_conflicts=has_conflicts) - - -LOCAL_DATABASES_SCENARIOS = [ - ('mem', {'make_database_for_test': make_memory_database_for_test, - 'copy_database_for_test': copy_memory_database_for_test, - 'make_document_for_test': make_document_for_test}), - ('sql', {'make_database_for_test': - make_sqlite_partial_expanded_for_test, - 'copy_database_for_test': - copy_sqlite_partial_expanded_for_test, - 'make_document_for_test': make_document_for_test}), -] - - -class DatabaseBaseTests(TestCase): - - accept_fixed_trans_id = False # set to True assertTransactionLog - # is happy with all trans ids = '' - - scenarios = LOCAL_DATABASES_SCENARIOS - - def create_database(self, replica_uid): - return self.make_database_for_test(self, replica_uid) - - def copy_database(self, db): - # DO NOT COPY OR REUSE THIS CODE OUTSIDE TESTS: COPYING U1DB DATABASES - # IS THE WRONG THING TO DO, THE ONLY REASON WE DO SO HERE IS TO TEST - # THAT WE CORRECTLY DETECT IT HAPPENING SO THAT WE CAN RAISE ERRORS - # RATHER THAN CORRUPT USER DATA. USE SYNC INSTEAD, OR WE WILL SEND - # NINJA TO YOUR HOUSE. - return self.copy_database_for_test(self, db) - - def setUp(self): - super(DatabaseBaseTests, self).setUp() - self.db = self.create_database('test') - - def tearDown(self): - # TODO: Add close_database parameterization - # self.close_database(self.db) - super(DatabaseBaseTests, self).tearDown() - - def assertTransactionLog(self, doc_ids, db): - """Assert that the given docs are in the transaction log.""" - log = db._get_transaction_log() - just_ids = [] - seen_transactions = set() - for doc_id, transaction_id in log: - just_ids.append(doc_id) - self.assertIsNot(None, transaction_id, - "Transaction id should not be None") - if transaction_id == '' and self.accept_fixed_trans_id: - continue - self.assertNotEqual('', transaction_id, - "Transaction id should be a unique string") - self.assertTrue(transaction_id.startswith('T-')) - self.assertNotIn(transaction_id, seen_transactions) - seen_transactions.add(transaction_id) - self.assertEqual(doc_ids, just_ids) - - def getLastTransId(self, db): - """Return the transaction id for the last database update.""" - return self.db._get_transaction_log()[-1][-1] - - -class ServerStateForTests(server_state.ServerState): - """Used in the test suite, so we don't have to touch disk, etc.""" - - def __init__(self): - super(ServerStateForTests, self).__init__() - self._dbs = {} - - def open_database(self, path): - try: - return self._dbs[path] - except KeyError: - raise errors.DatabaseDoesNotExist - - def check_database(self, path): - # cares only about the possible exception - self.open_database(path) - - def ensure_database(self, path): - try: - db = self.open_database(path) - except errors.DatabaseDoesNotExist: - db = self._create_database(path) - return db, db._replica_uid - - def _copy_database(self, db): - # DO NOT COPY OR REUSE THIS CODE OUTSIDE TESTS: COPYING U1DB DATABASES - # IS THE WRONG THING TO DO, THE ONLY REASON WE DO SO HERE IS TO TEST - # THAT WE CORRECTLY DETECT IT HAPPENING SO THAT WE CAN RAISE ERRORS - # RATHER THAN CORRUPT USER DATA. USE SYNC INSTEAD, OR WE WILL SEND - # NINJA TO YOUR HOUSE. - new_db = copy_memory_database_for_test(None, db) - path = db._replica_uid - while path in self._dbs: - path += 'copy' - self._dbs[path] = new_db - return new_db - - def _create_database(self, path): - db = inmemory.InMemoryDatabase(path) - self._dbs[path] = db - return db - - def delete_database(self, path): - del self._dbs[path] - - -class ResponderForTests(object): - """Responder for tests.""" - _started = False - sent_response = False - status = None - - def start_response(self, status='success', **kwargs): - self._started = True - self.status = status - self.kwargs = kwargs - - def send_response(self, status='success', **kwargs): - self.start_response(status, **kwargs) - self.finish_response() - - def finish_response(self): - self.sent_response = True - - -class TestCaseWithServer(TestCase): - - @staticmethod - def server_def(): - # hook point - # should return (ServerClass, "shutdown method name", "url_scheme") - class _RequestHandler(simple_server.WSGIRequestHandler): - def log_request(*args): - pass # suppress - - def make_server(host_port, application): - assert application, "forgot to override make_app(_with_state)?" - srv = simple_server.WSGIServer(host_port, _RequestHandler) - # patch the value in if it's None - if getattr(application, 'base_url', 1) is None: - application.base_url = "http://%s:%s" % srv.server_address - srv.set_app(application) - return srv - - return make_server, "shutdown", "http" - - @staticmethod - def make_app_with_state(state): - # hook point - return None - - def make_app(self): - # potential hook point - self.request_state = ServerStateForTests() - return self.make_app_with_state(self.request_state) - - def setUp(self): - super(TestCaseWithServer, self).setUp() - self.server = self.server_thread = None - - @property - def url_scheme(self): - return self.server_def()[-1] - - def startServer(self): - server_def = self.server_def() - server_class, shutdown_meth, _ = server_def - application = self.make_app() - self.server = server_class(('127.0.0.1', 0), application) - self.server_thread = threading.Thread(target=self.server.serve_forever, - kwargs=dict(poll_interval=0.01)) - self.server_thread.start() - self.addCleanup(self.server_thread.join) - self.addCleanup(getattr(self.server, shutdown_meth)) - - def getURL(self, path=None): - host, port = self.server.server_address - if path is None: - path = '' - return '%s://%s:%s/%s' % (self.url_scheme, host, port, path) - - -def socket_pair(): - """Return a pair of TCP sockets connected to each other. - - Unlike socket.socketpair, this should work on Windows. - """ - sock_pair = getattr(socket, 'socket_pair', None) - if sock_pair: - return sock_pair(socket.AF_INET, socket.SOCK_STREAM) - listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - listen_sock.bind(('127.0.0.1', 0)) - listen_sock.listen(1) - client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - client_sock.connect(listen_sock.getsockname()) - server_sock, addr = listen_sock.accept() - listen_sock.close() - return server_sock, client_sock - - -# OAuth related testing - -consumer1 = oauth.OAuthConsumer('K1', 'S1') -token1 = oauth.OAuthToken('kkkk1', 'XYZ') -consumer2 = oauth.OAuthConsumer('K2', 'S2') -token2 = oauth.OAuthToken('kkkk2', 'ZYX') -token3 = oauth.OAuthToken('kkkk3', 'ZYX') - - -class TestingOAuthDataStore(oauth.OAuthDataStore): - """In memory predefined OAuthDataStore for testing.""" - - consumers = { - consumer1.key: consumer1, - consumer2.key: consumer2, - } - - tokens = { - token1.key: token1, - token2.key: token2 - } - - def lookup_consumer(self, key): - return self.consumers.get(key) - - def lookup_token(self, token_type, token_token): - return self.tokens.get(token_token) - - def lookup_nonce(self, oauth_consumer, oauth_token, nonce): - return None - -testingOAuthStore = TestingOAuthDataStore() - -sign_meth_HMAC_SHA1 = oauth.OAuthSignatureMethod_HMAC_SHA1() -sign_meth_PLAINTEXT = oauth.OAuthSignatureMethod_PLAINTEXT() - - -def load_with_scenarios(loader, standard_tests, pattern): - """Load the tests in a given module. - - This just applies testscenarios.generate_scenarios to all the tests that - are present. We do it at load time rather than at run time, because it - plays nicer with various tools. - """ - suite = loader.suiteClass() - suite.addTests(testscenarios.generate_scenarios(standard_tests)) - return suite diff --git a/src/leap/soledad/tests/u1db_tests/test_backends.py b/src/leap/soledad/tests/u1db_tests/test_backends.py deleted file mode 100644 index a53b01ba..00000000 --- a/src/leap/soledad/tests/u1db_tests/test_backends.py +++ /dev/null @@ -1,1907 +0,0 @@ -# Copyright 2011 Canonical Ltd. -# -# This file is part of u1db. -# -# u1db is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 -# as published by the Free Software Foundation. -# -# u1db is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with u1db. If not, see . - -"""The backend class for U1DB. This deals with hiding storage details.""" - -try: - import simplejson as json -except ImportError: - import json # noqa -from u1db import ( - DocumentBase, - errors, - vectorclock, -) - -from leap.soledad.tests import u1db_tests as tests - -simple_doc = tests.simple_doc -nested_doc = tests.nested_doc - -from leap.soledad.tests.u1db_tests.test_remote_sync_target import ( - make_http_app, - make_oauth_http_app, -) - -from u1db.remote import ( - http_database, -) - - -def make_http_database_for_test(test, replica_uid, path='test'): - test.startServer() - test.request_state._create_database(replica_uid) - return http_database.HTTPDatabase(test.getURL(path)) - - -def copy_http_database_for_test(test, db): - # DO NOT COPY OR REUSE THIS CODE OUTSIDE TESTS: COPYING U1DB DATABASES IS - # THE WRONG THING TO DO, THE ONLY REASON WE DO SO HERE IS TO TEST THAT WE - # CORRECTLY DETECT IT HAPPENING SO THAT WE CAN RAISE ERRORS RATHER THAN - # CORRUPT USER DATA. USE SYNC INSTEAD, OR WE WILL SEND NINJA TO YOUR - # HOUSE. - return test.request_state._copy_database(db) - - -def make_oauth_http_database_for_test(test, replica_uid): - http_db = make_http_database_for_test(test, replica_uid, '~/test') - http_db.set_oauth_credentials(tests.consumer1.key, tests.consumer1.secret, - tests.token1.key, tests.token1.secret) - return http_db - - -def copy_oauth_http_database_for_test(test, db): - # DO NOT COPY OR REUSE THIS CODE OUTSIDE TESTS: COPYING U1DB DATABASES IS - # THE WRONG THING TO DO, THE ONLY REASON WE DO SO HERE IS TO TEST THAT WE - # CORRECTLY DETECT IT HAPPENING SO THAT WE CAN RAISE ERRORS RATHER THAN - # CORRUPT USER DATA. USE SYNC INSTEAD, OR WE WILL SEND NINJA TO YOUR - # HOUSE. - http_db = test.request_state._copy_database(db) - http_db.set_oauth_credentials(tests.consumer1.key, tests.consumer1.secret, - tests.token1.key, tests.token1.secret) - return http_db - - -class TestAlternativeDocument(DocumentBase): - """A (not very) alternative implementation of Document.""" - - -class AllDatabaseTests(tests.DatabaseBaseTests, tests.TestCaseWithServer): - - scenarios = tests.LOCAL_DATABASES_SCENARIOS + [ - ('http', {'make_database_for_test': make_http_database_for_test, - 'copy_database_for_test': copy_http_database_for_test, - 'make_document_for_test': tests.make_document_for_test, - 'make_app_with_state': make_http_app}), - ('oauth_http', {'make_database_for_test': - make_oauth_http_database_for_test, - 'copy_database_for_test': - copy_oauth_http_database_for_test, - 'make_document_for_test': tests.make_document_for_test, - 'make_app_with_state': make_oauth_http_app}) - ] - - def test_close(self): - self.db.close() - - def test_create_doc_allocating_doc_id(self): - doc = self.db.create_doc_from_json(simple_doc) - self.assertNotEqual(None, doc.doc_id) - self.assertNotEqual(None, doc.rev) - self.assertGetDoc(self.db, doc.doc_id, doc.rev, simple_doc, False) - - def test_create_doc_different_ids_same_db(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(nested_doc) - self.assertNotEqual(doc1.doc_id, doc2.doc_id) - - def test_create_doc_with_id(self): - doc = self.db.create_doc_from_json(simple_doc, doc_id='my-id') - self.assertEqual('my-id', doc.doc_id) - self.assertNotEqual(None, doc.rev) - self.assertGetDoc(self.db, doc.doc_id, doc.rev, simple_doc, False) - - def test_create_doc_existing_id(self): - doc = self.db.create_doc_from_json(simple_doc) - new_content = '{"something": "else"}' - self.assertRaises( - errors.RevisionConflict, self.db.create_doc_from_json, - new_content, doc.doc_id) - self.assertGetDoc(self.db, doc.doc_id, doc.rev, simple_doc, False) - - def test_put_doc_creating_initial(self): - doc = self.make_document('my_doc_id', None, simple_doc) - new_rev = self.db.put_doc(doc) - self.assertIsNot(None, new_rev) - self.assertGetDoc(self.db, 'my_doc_id', new_rev, simple_doc, False) - - def test_put_doc_space_in_id(self): - doc = self.make_document('my doc id', None, simple_doc) - self.assertRaises(errors.InvalidDocId, self.db.put_doc, doc) - - def test_put_doc_update(self): - doc = self.db.create_doc_from_json(simple_doc, doc_id='my_doc_id') - orig_rev = doc.rev - doc.set_json('{"updated": "stuff"}') - new_rev = self.db.put_doc(doc) - self.assertNotEqual(new_rev, orig_rev) - self.assertGetDoc(self.db, 'my_doc_id', new_rev, - '{"updated": "stuff"}', False) - self.assertEqual(doc.rev, new_rev) - - def test_put_non_ascii_key(self): - content = json.dumps({u'key\xe5': u'val'}) - doc = self.db.create_doc_from_json(content, doc_id='my_doc') - self.assertGetDoc(self.db, 'my_doc', doc.rev, content, False) - - def test_put_non_ascii_value(self): - content = json.dumps({'key': u'\xe5'}) - doc = self.db.create_doc_from_json(content, doc_id='my_doc') - self.assertGetDoc(self.db, 'my_doc', doc.rev, content, False) - - def test_put_doc_refuses_no_id(self): - doc = self.make_document(None, None, simple_doc) - self.assertRaises(errors.InvalidDocId, self.db.put_doc, doc) - doc = self.make_document("", None, simple_doc) - self.assertRaises(errors.InvalidDocId, self.db.put_doc, doc) - - def test_put_doc_refuses_slashes(self): - doc = self.make_document('a/b', None, simple_doc) - self.assertRaises(errors.InvalidDocId, self.db.put_doc, doc) - doc = self.make_document(r'\b', None, simple_doc) - self.assertRaises(errors.InvalidDocId, self.db.put_doc, doc) - - def test_put_doc_url_quoting_is_fine(self): - doc_id = "%2F%2Ffoo%2Fbar" - doc = self.make_document(doc_id, None, simple_doc) - new_rev = self.db.put_doc(doc) - self.assertGetDoc(self.db, doc_id, new_rev, simple_doc, False) - - def test_put_doc_refuses_non_existing_old_rev(self): - doc = self.make_document('doc-id', 'test:4', simple_doc) - self.assertRaises(errors.RevisionConflict, self.db.put_doc, doc) - - def test_put_doc_refuses_non_ascii_doc_id(self): - doc = self.make_document('d\xc3\xa5c-id', None, simple_doc) - self.assertRaises(errors.InvalidDocId, self.db.put_doc, doc) - - def test_put_fails_with_bad_old_rev(self): - doc = self.db.create_doc_from_json(simple_doc, doc_id='my_doc_id') - old_rev = doc.rev - bad_doc = self.make_document(doc.doc_id, 'other:1', - '{"something": "else"}') - self.assertRaises(errors.RevisionConflict, self.db.put_doc, bad_doc) - self.assertGetDoc(self.db, 'my_doc_id', old_rev, simple_doc, False) - - def test_create_succeeds_after_delete(self): - doc = self.db.create_doc_from_json(simple_doc, doc_id='my_doc_id') - self.db.delete_doc(doc) - deleted_doc = self.db.get_doc('my_doc_id', include_deleted=True) - deleted_vc = vectorclock.VectorClockRev(deleted_doc.rev) - new_doc = self.db.create_doc_from_json(simple_doc, doc_id='my_doc_id') - self.assertGetDoc(self.db, 'my_doc_id', new_doc.rev, simple_doc, False) - new_vc = vectorclock.VectorClockRev(new_doc.rev) - self.assertTrue( - new_vc.is_newer(deleted_vc), - "%s does not supersede %s" % (new_doc.rev, deleted_doc.rev)) - - def test_put_succeeds_after_delete(self): - doc = self.db.create_doc_from_json(simple_doc, doc_id='my_doc_id') - self.db.delete_doc(doc) - deleted_doc = self.db.get_doc('my_doc_id', include_deleted=True) - deleted_vc = vectorclock.VectorClockRev(deleted_doc.rev) - doc2 = self.make_document('my_doc_id', None, simple_doc) - self.db.put_doc(doc2) - self.assertGetDoc(self.db, 'my_doc_id', doc2.rev, simple_doc, False) - new_vc = vectorclock.VectorClockRev(doc2.rev) - self.assertTrue( - new_vc.is_newer(deleted_vc), - "%s does not supersede %s" % (doc2.rev, deleted_doc.rev)) - - def test_get_doc_after_put(self): - doc = self.db.create_doc_from_json(simple_doc, doc_id='my_doc_id') - self.assertGetDoc(self.db, 'my_doc_id', doc.rev, simple_doc, False) - - def test_get_doc_nonexisting(self): - self.assertIs(None, self.db.get_doc('non-existing')) - - def test_get_doc_deleted(self): - doc = self.db.create_doc_from_json(simple_doc, doc_id='my_doc_id') - self.db.delete_doc(doc) - self.assertIs(None, self.db.get_doc('my_doc_id')) - - def test_get_doc_include_deleted(self): - doc = self.db.create_doc_from_json(simple_doc, doc_id='my_doc_id') - self.db.delete_doc(doc) - self.assertGetDocIncludeDeleted( - self.db, doc.doc_id, doc.rev, None, False) - - def test_get_docs(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(nested_doc) - self.assertEqual([doc1, doc2], - list(self.db.get_docs([doc1.doc_id, doc2.doc_id]))) - - def test_get_docs_deleted(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(nested_doc) - self.db.delete_doc(doc1) - self.assertEqual([doc2], - list(self.db.get_docs([doc1.doc_id, doc2.doc_id]))) - - def test_get_docs_include_deleted(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(nested_doc) - self.db.delete_doc(doc1) - self.assertEqual( - [doc1, doc2], - list(self.db.get_docs([doc1.doc_id, doc2.doc_id], - include_deleted=True))) - - def test_get_docs_request_ordered(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(nested_doc) - self.assertEqual([doc1, doc2], - list(self.db.get_docs([doc1.doc_id, doc2.doc_id]))) - self.assertEqual([doc2, doc1], - list(self.db.get_docs([doc2.doc_id, doc1.doc_id]))) - - def test_get_docs_empty_list(self): - self.assertEqual([], list(self.db.get_docs([]))) - - def test_handles_nested_content(self): - doc = self.db.create_doc_from_json(nested_doc) - self.assertGetDoc(self.db, doc.doc_id, doc.rev, nested_doc, False) - - def test_handles_doc_with_null(self): - doc = self.db.create_doc_from_json('{"key": null}') - self.assertGetDoc(self.db, doc.doc_id, doc.rev, '{"key": null}', False) - - def test_delete_doc(self): - doc = self.db.create_doc_from_json(simple_doc) - self.assertGetDoc(self.db, doc.doc_id, doc.rev, simple_doc, False) - orig_rev = doc.rev - self.db.delete_doc(doc) - self.assertNotEqual(orig_rev, doc.rev) - self.assertGetDocIncludeDeleted( - self.db, doc.doc_id, doc.rev, None, False) - self.assertIs(None, self.db.get_doc(doc.doc_id)) - - def test_delete_doc_non_existent(self): - doc = self.make_document('non-existing', 'other:1', simple_doc) - self.assertRaises(errors.DocumentDoesNotExist, self.db.delete_doc, doc) - - def test_delete_doc_already_deleted(self): - doc = self.db.create_doc_from_json(simple_doc) - self.db.delete_doc(doc) - self.assertRaises(errors.DocumentAlreadyDeleted, - self.db.delete_doc, doc) - self.assertGetDocIncludeDeleted( - self.db, doc.doc_id, doc.rev, None, False) - - def test_delete_doc_bad_rev(self): - doc1 = self.db.create_doc_from_json(simple_doc) - self.assertGetDoc(self.db, doc1.doc_id, doc1.rev, simple_doc, False) - doc2 = self.make_document(doc1.doc_id, 'other:1', simple_doc) - self.assertRaises(errors.RevisionConflict, self.db.delete_doc, doc2) - self.assertGetDoc(self.db, doc1.doc_id, doc1.rev, simple_doc, False) - - def test_delete_doc_sets_content_to_None(self): - doc = self.db.create_doc_from_json(simple_doc) - self.db.delete_doc(doc) - self.assertIs(None, doc.get_json()) - - def test_delete_doc_rev_supersedes(self): - doc = self.db.create_doc_from_json(simple_doc) - doc.set_json(nested_doc) - self.db.put_doc(doc) - doc.set_json('{"fishy": "content"}') - self.db.put_doc(doc) - old_rev = doc.rev - self.db.delete_doc(doc) - cur_vc = vectorclock.VectorClockRev(old_rev) - deleted_vc = vectorclock.VectorClockRev(doc.rev) - self.assertTrue(deleted_vc.is_newer(cur_vc), - "%s does not supersede %s" % (doc.rev, old_rev)) - - def test_delete_then_put(self): - doc = self.db.create_doc_from_json(simple_doc) - self.db.delete_doc(doc) - self.assertGetDocIncludeDeleted( - self.db, doc.doc_id, doc.rev, None, False) - doc.set_json(nested_doc) - self.db.put_doc(doc) - self.assertGetDoc(self.db, doc.doc_id, doc.rev, nested_doc, False) - - -class DocumentSizeTests(tests.DatabaseBaseTests): - - scenarios = tests.LOCAL_DATABASES_SCENARIOS - - def test_put_doc_refuses_oversized_documents(self): - self.db.set_document_size_limit(1) - doc = self.make_document('doc-id', None, simple_doc) - self.assertRaises(errors.DocumentTooBig, self.db.put_doc, doc) - - def test_create_doc_refuses_oversized_documents(self): - self.db.set_document_size_limit(1) - self.assertRaises( - errors.DocumentTooBig, self.db.create_doc_from_json, simple_doc, - doc_id='my_doc_id') - - def test_set_document_size_limit_zero(self): - self.db.set_document_size_limit(0) - self.assertEqual(0, self.db.document_size_limit) - - def test_set_document_size_limit(self): - self.db.set_document_size_limit(1000000) - self.assertEqual(1000000, self.db.document_size_limit) - - -class LocalDatabaseTests(tests.DatabaseBaseTests): - - scenarios = tests.LOCAL_DATABASES_SCENARIOS - - def test_create_doc_different_ids_diff_db(self): - doc1 = self.db.create_doc_from_json(simple_doc) - db2 = self.create_database('other-uid') - doc2 = db2.create_doc_from_json(simple_doc) - self.assertNotEqual(doc1.doc_id, doc2.doc_id) - - def test_put_doc_refuses_slashes_picky(self): - doc = self.make_document('/a', None, simple_doc) - self.assertRaises(errors.InvalidDocId, self.db.put_doc, doc) - - def test_get_all_docs_empty(self): - self.assertEqual([], list(self.db.get_all_docs()[1])) - - def test_get_all_docs(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(nested_doc) - self.assertEqual( - sorted([doc1, doc2]), sorted(list(self.db.get_all_docs()[1]))) - - def test_get_all_docs_exclude_deleted(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(nested_doc) - self.db.delete_doc(doc2) - self.assertEqual([doc1], list(self.db.get_all_docs()[1])) - - def test_get_all_docs_include_deleted(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(nested_doc) - self.db.delete_doc(doc2) - self.assertEqual( - sorted([doc1, doc2]), - sorted(list(self.db.get_all_docs(include_deleted=True)[1]))) - - def test_get_all_docs_generation(self): - self.db.create_doc_from_json(simple_doc) - self.db.create_doc_from_json(nested_doc) - self.assertEqual(2, self.db.get_all_docs()[0]) - - def test_simple_put_doc_if_newer(self): - doc = self.make_document('my-doc-id', 'test:1', simple_doc) - state_at_gen = self.db._put_doc_if_newer( - doc, save_conflict=False, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertEqual(('inserted', 1), state_at_gen) - self.assertGetDoc(self.db, 'my-doc-id', 'test:1', simple_doc, False) - - def test_simple_put_doc_if_newer_deleted(self): - self.db.create_doc_from_json('{}', doc_id='my-doc-id') - doc = self.make_document('my-doc-id', 'test:2', None) - state_at_gen = self.db._put_doc_if_newer( - doc, save_conflict=False, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertEqual(('inserted', 2), state_at_gen) - self.assertGetDocIncludeDeleted( - self.db, 'my-doc-id', 'test:2', None, False) - - def test_put_doc_if_newer_already_superseded(self): - orig_doc = '{"new": "doc"}' - doc1 = self.db.create_doc_from_json(orig_doc) - doc1_rev1 = doc1.rev - doc1.set_json(simple_doc) - self.db.put_doc(doc1) - doc1_rev2 = doc1.rev - # Nothing is inserted, because the document is already superseded - doc = self.make_document(doc1.doc_id, doc1_rev1, orig_doc) - state, _ = self.db._put_doc_if_newer( - doc, save_conflict=False, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertEqual('superseded', state) - self.assertGetDoc(self.db, doc1.doc_id, doc1_rev2, simple_doc, False) - - def test_put_doc_if_newer_autoresolve(self): - doc1 = self.db.create_doc_from_json(simple_doc) - rev = doc1.rev - doc = self.make_document(doc1.doc_id, "whatever:1", doc1.get_json()) - state, _ = self.db._put_doc_if_newer( - doc, save_conflict=False, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertEqual('superseded', state) - doc2 = self.db.get_doc(doc1.doc_id) - v2 = vectorclock.VectorClockRev(doc2.rev) - self.assertTrue(v2.is_newer(vectorclock.VectorClockRev("whatever:1"))) - self.assertTrue(v2.is_newer(vectorclock.VectorClockRev(rev))) - # strictly newer locally - self.assertTrue(rev not in doc2.rev) - - def test_put_doc_if_newer_already_converged(self): - orig_doc = '{"new": "doc"}' - doc1 = self.db.create_doc_from_json(orig_doc) - state_at_gen = self.db._put_doc_if_newer( - doc1, save_conflict=False, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertEqual(('converged', 1), state_at_gen) - - def test_put_doc_if_newer_conflicted(self): - doc1 = self.db.create_doc_from_json(simple_doc) - # Nothing is inserted, the document id is returned as would-conflict - alt_doc = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) - state, _ = self.db._put_doc_if_newer( - alt_doc, save_conflict=False, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertEqual('conflicted', state) - # The database wasn't altered - self.assertGetDoc(self.db, doc1.doc_id, doc1.rev, simple_doc, False) - - def test_put_doc_if_newer_newer_generation(self): - self.db._set_replica_gen_and_trans_id('other', 1, 'T-sid') - doc = self.make_document('doc_id', 'other:2', simple_doc) - state, _ = self.db._put_doc_if_newer( - doc, save_conflict=False, replica_uid='other', replica_gen=2, - replica_trans_id='T-irrelevant') - self.assertEqual('inserted', state) - - def test_put_doc_if_newer_same_generation_same_txid(self): - self.db._set_replica_gen_and_trans_id('other', 1, 'T-sid') - doc = self.db.create_doc_from_json(simple_doc) - self.make_document(doc.doc_id, 'other:1', simple_doc) - state, _ = self.db._put_doc_if_newer( - doc, save_conflict=False, replica_uid='other', replica_gen=1, - replica_trans_id='T-sid') - self.assertEqual('converged', state) - - def test_put_doc_if_newer_wrong_transaction_id(self): - self.db._set_replica_gen_and_trans_id('other', 1, 'T-sid') - doc = self.make_document('doc_id', 'other:1', simple_doc) - self.assertRaises( - errors.InvalidTransactionId, - self.db._put_doc_if_newer, doc, save_conflict=False, - replica_uid='other', replica_gen=1, replica_trans_id='T-sad') - - def test_put_doc_if_newer_old_generation_older_doc(self): - orig_doc = '{"new": "doc"}' - doc = self.db.create_doc_from_json(orig_doc) - doc_rev1 = doc.rev - doc.set_json(simple_doc) - self.db.put_doc(doc) - self.db._set_replica_gen_and_trans_id('other', 3, 'T-sid') - older_doc = self.make_document(doc.doc_id, doc_rev1, simple_doc) - state, _ = self.db._put_doc_if_newer( - older_doc, save_conflict=False, replica_uid='other', replica_gen=8, - replica_trans_id='T-irrelevant') - self.assertEqual('superseded', state) - - def test_put_doc_if_newer_old_generation_newer_doc(self): - self.db._set_replica_gen_and_trans_id('other', 5, 'T-sid') - doc = self.make_document('doc_id', 'other:1', simple_doc) - self.assertRaises( - errors.InvalidGeneration, - self.db._put_doc_if_newer, doc, save_conflict=False, - replica_uid='other', replica_gen=1, replica_trans_id='T-sad') - - def test_put_doc_if_newer_replica_uid(self): - doc1 = self.db.create_doc_from_json(simple_doc) - self.db._set_replica_gen_and_trans_id('other', 1, 'T-sid') - doc2 = self.make_document(doc1.doc_id, doc1.rev + '|other:1', - nested_doc) - self.assertEqual('inserted', - self.db._put_doc_if_newer( - doc2, - save_conflict=False, - replica_uid='other', - replica_gen=2, - replica_trans_id='T-id2')[0]) - self.assertEqual((2, 'T-id2'), self.db._get_replica_gen_and_trans_id( - 'other')) - # Compare to the old rev, should be superseded - doc2 = self.make_document(doc1.doc_id, doc1.rev, nested_doc) - self.assertEqual('superseded', - self.db._put_doc_if_newer( - doc2, - save_conflict=False, - replica_uid='other', - replica_gen=3, - replica_trans_id='T-id3')[0]) - self.assertEqual( - (3, 'T-id3'), self.db._get_replica_gen_and_trans_id('other')) - # A conflict that isn't saved still records the sync gen, because we - # don't need to see it again - doc2 = self.make_document(doc1.doc_id, doc1.rev + '|fourth:1', - '{}') - self.assertEqual('conflicted', - self.db._put_doc_if_newer( - doc2, - save_conflict=False, - replica_uid='other', - replica_gen=4, - replica_trans_id='T-id4')[0]) - self.assertEqual( - (4, 'T-id4'), self.db._get_replica_gen_and_trans_id('other')) - - def test__get_replica_gen_and_trans_id(self): - self.assertEqual( - (0, ''), self.db._get_replica_gen_and_trans_id('other-db')) - self.db._set_replica_gen_and_trans_id('other-db', 2, 'T-transaction') - self.assertEqual( - (2, 'T-transaction'), - self.db._get_replica_gen_and_trans_id('other-db')) - - def test_put_updates_transaction_log(self): - doc = self.db.create_doc_from_json(simple_doc) - self.assertTransactionLog([doc.doc_id], self.db) - doc.set_json('{"something": "else"}') - self.db.put_doc(doc) - self.assertTransactionLog([doc.doc_id, doc.doc_id], self.db) - last_trans_id = self.getLastTransId(self.db) - self.assertEqual((2, last_trans_id, [(doc.doc_id, 2, last_trans_id)]), - self.db.whats_changed()) - - def test_delete_updates_transaction_log(self): - doc = self.db.create_doc_from_json(simple_doc) - db_gen, _, _ = self.db.whats_changed() - self.db.delete_doc(doc) - last_trans_id = self.getLastTransId(self.db) - self.assertEqual((2, last_trans_id, [(doc.doc_id, 2, last_trans_id)]), - self.db.whats_changed(db_gen)) - - def test_whats_changed_initial_database(self): - self.assertEqual((0, '', []), self.db.whats_changed()) - - def test_whats_changed_returns_one_id_for_multiple_changes(self): - doc = self.db.create_doc_from_json(simple_doc) - doc.set_json('{"new": "contents"}') - self.db.put_doc(doc) - last_trans_id = self.getLastTransId(self.db) - self.assertEqual((2, last_trans_id, [(doc.doc_id, 2, last_trans_id)]), - self.db.whats_changed()) - self.assertEqual((2, last_trans_id, []), self.db.whats_changed(2)) - - def test_whats_changed_returns_last_edits_ascending(self): - doc = self.db.create_doc_from_json(simple_doc) - doc1 = self.db.create_doc_from_json(simple_doc) - doc.set_json('{"new": "contents"}') - self.db.delete_doc(doc1) - delete_trans_id = self.getLastTransId(self.db) - self.db.put_doc(doc) - put_trans_id = self.getLastTransId(self.db) - self.assertEqual((4, put_trans_id, - [(doc1.doc_id, 3, delete_trans_id), - (doc.doc_id, 4, put_trans_id)]), - self.db.whats_changed()) - - def test_whats_changed_doesnt_include_old_gen(self): - self.db.create_doc_from_json(simple_doc) - self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(simple_doc) - last_trans_id = self.getLastTransId(self.db) - self.assertEqual((3, last_trans_id, [(doc2.doc_id, 3, last_trans_id)]), - self.db.whats_changed(2)) - - -class LocalDatabaseValidateGenNTransIdTests(tests.DatabaseBaseTests): - - scenarios = tests.LOCAL_DATABASES_SCENARIOS - - def test_validate_gen_and_trans_id(self): - self.db.create_doc_from_json(simple_doc) - gen, trans_id = self.db._get_generation_info() - self.db.validate_gen_and_trans_id(gen, trans_id) - - def test_validate_gen_and_trans_id_invalid_txid(self): - self.db.create_doc_from_json(simple_doc) - gen, _ = self.db._get_generation_info() - self.assertRaises( - errors.InvalidTransactionId, - self.db.validate_gen_and_trans_id, gen, 'wrong') - - def test_validate_gen_and_trans_id_invalid_gen(self): - self.db.create_doc_from_json(simple_doc) - gen, trans_id = self.db._get_generation_info() - self.assertRaises( - errors.InvalidGeneration, - self.db.validate_gen_and_trans_id, gen + 1, trans_id) - - -class LocalDatabaseValidateSourceGenTests(tests.DatabaseBaseTests): - - scenarios = tests.LOCAL_DATABASES_SCENARIOS - - def test_validate_source_gen_and_trans_id_same(self): - self.db._set_replica_gen_and_trans_id('other', 1, 'T-sid') - self.db._validate_source('other', 1, 'T-sid') - - def test_validate_source_gen_newer(self): - self.db._set_replica_gen_and_trans_id('other', 1, 'T-sid') - self.db._validate_source('other', 2, 'T-whatevs') - - def test_validate_source_wrong_txid(self): - self.db._set_replica_gen_and_trans_id('other', 1, 'T-sid') - self.assertRaises( - errors.InvalidTransactionId, - self.db._validate_source, 'other', 1, 'T-sad') - - -class LocalDatabaseWithConflictsTests(tests.DatabaseBaseTests): - # test supporting/functionality around storing conflicts - - scenarios = tests.LOCAL_DATABASES_SCENARIOS - - def test_get_docs_conflicted(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertEqual([doc2], list(self.db.get_docs([doc1.doc_id]))) - - def test_get_docs_conflicts_ignored(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(nested_doc) - alt_doc = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - alt_doc, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - no_conflict_doc = self.make_document(doc1.doc_id, 'alternate:1', - nested_doc) - self.assertEqual([no_conflict_doc, doc2], - list(self.db.get_docs([doc1.doc_id, doc2.doc_id], - check_for_conflicts=False))) - - def test_get_doc_conflicts(self): - doc = self.db.create_doc_from_json(simple_doc) - alt_doc = self.make_document(doc.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - alt_doc, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertEqual([alt_doc, doc], - self.db.get_doc_conflicts(doc.doc_id)) - - def test_get_all_docs_sees_conflicts(self): - doc = self.db.create_doc_from_json(simple_doc) - alt_doc = self.make_document(doc.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - alt_doc, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - _, docs = self.db.get_all_docs() - self.assertTrue(list(docs)[0].has_conflicts) - - def test_get_doc_conflicts_unconflicted(self): - doc = self.db.create_doc_from_json(simple_doc) - self.assertEqual([], self.db.get_doc_conflicts(doc.doc_id)) - - def test_get_doc_conflicts_no_such_id(self): - self.assertEqual([], self.db.get_doc_conflicts('doc-id')) - - def test_resolve_doc(self): - doc = self.db.create_doc_from_json(simple_doc) - alt_doc = self.make_document(doc.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - alt_doc, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertGetDocConflicts(self.db, doc.doc_id, - [('alternate:1', nested_doc), - (doc.rev, simple_doc)]) - orig_rev = doc.rev - self.db.resolve_doc(doc, [alt_doc.rev, doc.rev]) - self.assertNotEqual(orig_rev, doc.rev) - self.assertFalse(doc.has_conflicts) - self.assertGetDoc(self.db, doc.doc_id, doc.rev, simple_doc, False) - self.assertGetDocConflicts(self.db, doc.doc_id, []) - - def test_resolve_doc_picks_biggest_vcr(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertGetDocConflicts(self.db, doc1.doc_id, - [(doc2.rev, nested_doc), - (doc1.rev, simple_doc)]) - orig_doc1_rev = doc1.rev - self.db.resolve_doc(doc1, [doc2.rev, doc1.rev]) - self.assertFalse(doc1.has_conflicts) - self.assertNotEqual(orig_doc1_rev, doc1.rev) - self.assertGetDoc(self.db, doc1.doc_id, doc1.rev, simple_doc, False) - self.assertGetDocConflicts(self.db, doc1.doc_id, []) - vcr_1 = vectorclock.VectorClockRev(orig_doc1_rev) - vcr_2 = vectorclock.VectorClockRev(doc2.rev) - vcr_new = vectorclock.VectorClockRev(doc1.rev) - self.assertTrue(vcr_new.is_newer(vcr_1)) - self.assertTrue(vcr_new.is_newer(vcr_2)) - - def test_resolve_doc_partial_not_winning(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertGetDocConflicts(self.db, doc1.doc_id, - [(doc2.rev, nested_doc), - (doc1.rev, simple_doc)]) - content3 = '{"key": "valin3"}' - doc3 = self.make_document(doc1.doc_id, 'third:1', content3) - self.db._put_doc_if_newer( - doc3, save_conflict=True, replica_uid='r', replica_gen=2, - replica_trans_id='bar') - self.assertGetDocConflicts(self.db, doc1.doc_id, - [(doc3.rev, content3), - (doc1.rev, simple_doc), - (doc2.rev, nested_doc)]) - self.db.resolve_doc(doc1, [doc2.rev, doc1.rev]) - self.assertTrue(doc1.has_conflicts) - self.assertGetDoc(self.db, doc1.doc_id, doc3.rev, content3, True) - self.assertGetDocConflicts(self.db, doc1.doc_id, - [(doc3.rev, content3), - (doc1.rev, simple_doc)]) - - def test_resolve_doc_partial_winning(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - content3 = '{"key": "valin3"}' - doc3 = self.make_document(doc1.doc_id, 'third:1', content3) - self.db._put_doc_if_newer( - doc3, save_conflict=True, replica_uid='r', replica_gen=2, - replica_trans_id='bar') - self.assertGetDocConflicts(self.db, doc1.doc_id, - [(doc3.rev, content3), - (doc1.rev, simple_doc), - (doc2.rev, nested_doc)]) - self.db.resolve_doc(doc1, [doc3.rev, doc1.rev]) - self.assertTrue(doc1.has_conflicts) - self.assertGetDocConflicts(self.db, doc1.doc_id, - [(doc1.rev, simple_doc), - (doc2.rev, nested_doc)]) - - def test_resolve_doc_with_delete_conflict(self): - doc1 = self.db.create_doc_from_json(simple_doc) - self.db.delete_doc(doc1) - doc2 = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertGetDocConflicts(self.db, doc1.doc_id, - [(doc2.rev, nested_doc), - (doc1.rev, None)]) - self.db.resolve_doc(doc2, [doc1.rev, doc2.rev]) - self.assertGetDocConflicts(self.db, doc1.doc_id, []) - self.assertGetDoc(self.db, doc2.doc_id, doc2.rev, nested_doc, False) - - def test_resolve_doc_with_delete_to_delete(self): - doc1 = self.db.create_doc_from_json(simple_doc) - self.db.delete_doc(doc1) - doc2 = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertGetDocConflicts(self.db, doc1.doc_id, - [(doc2.rev, nested_doc), - (doc1.rev, None)]) - self.db.resolve_doc(doc1, [doc1.rev, doc2.rev]) - self.assertGetDocConflicts(self.db, doc1.doc_id, []) - self.assertGetDocIncludeDeleted( - self.db, doc1.doc_id, doc1.rev, None, False) - - def test_put_doc_if_newer_save_conflicted(self): - doc1 = self.db.create_doc_from_json(simple_doc) - # Document is inserted as a conflict - doc2 = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) - state, _ = self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertEqual('conflicted', state) - # The database was updated - self.assertGetDoc(self.db, doc1.doc_id, doc2.rev, nested_doc, True) - - def test_force_doc_conflict_supersedes_properly(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.make_document(doc1.doc_id, 'alternate:1', '{"b": 1}') - self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - doc3 = self.make_document(doc1.doc_id, 'altalt:1', '{"c": 1}') - self.db._put_doc_if_newer( - doc3, save_conflict=True, replica_uid='r', replica_gen=2, - replica_trans_id='bar') - doc22 = self.make_document(doc1.doc_id, 'alternate:2', '{"b": 2}') - self.db._put_doc_if_newer( - doc22, save_conflict=True, replica_uid='r', replica_gen=3, - replica_trans_id='zed') - self.assertGetDocConflicts(self.db, doc1.doc_id, - [('alternate:2', doc22.get_json()), - ('altalt:1', doc3.get_json()), - (doc1.rev, simple_doc)]) - - def test_put_doc_if_newer_save_conflict_was_deleted(self): - doc1 = self.db.create_doc_from_json(simple_doc) - self.db.delete_doc(doc1) - doc2 = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertTrue(doc2.has_conflicts) - self.assertGetDoc( - self.db, doc1.doc_id, 'alternate:1', nested_doc, True) - self.assertGetDocConflicts(self.db, doc1.doc_id, - [('alternate:1', nested_doc), - (doc1.rev, None)]) - - def test_put_doc_if_newer_propagates_full_resolution(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - resolved_vcr = vectorclock.VectorClockRev(doc1.rev) - vcr_2 = vectorclock.VectorClockRev(doc2.rev) - resolved_vcr.maximize(vcr_2) - resolved_vcr.increment('alternate') - doc_resolved = self.make_document(doc1.doc_id, resolved_vcr.as_str(), - '{"good": 1}') - state, _ = self.db._put_doc_if_newer( - doc_resolved, save_conflict=True, replica_uid='r', replica_gen=2, - replica_trans_id='foo2') - self.assertEqual('inserted', state) - self.assertFalse(doc_resolved.has_conflicts) - self.assertGetDocConflicts(self.db, doc1.doc_id, []) - doc3 = self.db.get_doc(doc1.doc_id) - self.assertFalse(doc3.has_conflicts) - - def test_put_doc_if_newer_propagates_partial_resolution(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.make_document(doc1.doc_id, 'altalt:1', '{}') - self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - doc3 = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - doc3, save_conflict=True, replica_uid='r', replica_gen=2, - replica_trans_id='foo2') - self.assertGetDocConflicts(self.db, doc1.doc_id, - [('alternate:1', nested_doc), - ('test:1', simple_doc), - ('altalt:1', '{}')]) - resolved_vcr = vectorclock.VectorClockRev(doc1.rev) - vcr_3 = vectorclock.VectorClockRev(doc3.rev) - resolved_vcr.maximize(vcr_3) - resolved_vcr.increment('alternate') - doc_resolved = self.make_document(doc1.doc_id, resolved_vcr.as_str(), - '{"good": 1}') - state, _ = self.db._put_doc_if_newer( - doc_resolved, save_conflict=True, replica_uid='r', replica_gen=3, - replica_trans_id='foo3') - self.assertEqual('inserted', state) - self.assertTrue(doc_resolved.has_conflicts) - doc4 = self.db.get_doc(doc1.doc_id) - self.assertTrue(doc4.has_conflicts) - self.assertGetDocConflicts(self.db, doc1.doc_id, - [('alternate:2|test:1', '{"good": 1}'), - ('altalt:1', '{}')]) - - def test_put_doc_if_newer_replica_uid(self): - doc1 = self.db.create_doc_from_json(simple_doc) - self.db._set_replica_gen_and_trans_id('other', 1, 'T-id') - doc2 = self.make_document(doc1.doc_id, doc1.rev + '|other:1', - nested_doc) - self.db._put_doc_if_newer(doc2, save_conflict=True, - replica_uid='other', replica_gen=2, - replica_trans_id='T-id2') - # Conflict vs the current update - doc2 = self.make_document(doc1.doc_id, doc1.rev + '|third:3', - '{}') - self.assertEqual('conflicted', - self.db._put_doc_if_newer( - doc2, - save_conflict=True, - replica_uid='other', - replica_gen=3, - replica_trans_id='T-id3')[0]) - self.assertEqual( - (3, 'T-id3'), self.db._get_replica_gen_and_trans_id('other')) - - def test_put_doc_if_newer_autoresolve_2(self): - # this is an ordering variant of _3, but that already works - # adding the test explicitly to catch the regression easily - doc_a1 = self.db.create_doc_from_json(simple_doc) - doc_a2 = self.make_document(doc_a1.doc_id, 'test:2', "{}") - doc_a1b1 = self.make_document(doc_a1.doc_id, 'test:1|other:1', - '{"a":"42"}') - doc_a3 = self.make_document(doc_a1.doc_id, 'test:2|other:1', "{}") - state, _ = self.db._put_doc_if_newer( - doc_a2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertEqual(state, 'inserted') - state, _ = self.db._put_doc_if_newer( - doc_a1b1, save_conflict=True, replica_uid='r', replica_gen=2, - replica_trans_id='foo2') - self.assertEqual(state, 'conflicted') - state, _ = self.db._put_doc_if_newer( - doc_a3, save_conflict=True, replica_uid='r', replica_gen=3, - replica_trans_id='foo3') - self.assertEqual(state, 'inserted') - self.assertFalse(self.db.get_doc(doc_a1.doc_id).has_conflicts) - - def test_put_doc_if_newer_autoresolve_3(self): - doc_a1 = self.db.create_doc_from_json(simple_doc) - doc_a1b1 = self.make_document(doc_a1.doc_id, 'test:1|other:1', "{}") - doc_a2 = self.make_document(doc_a1.doc_id, 'test:2', '{"a":"42"}') - doc_a3 = self.make_document(doc_a1.doc_id, 'test:3', "{}") - state, _ = self.db._put_doc_if_newer( - doc_a1b1, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertEqual(state, 'inserted') - state, _ = self.db._put_doc_if_newer( - doc_a2, save_conflict=True, replica_uid='r', replica_gen=2, - replica_trans_id='foo2') - self.assertEqual(state, 'conflicted') - state, _ = self.db._put_doc_if_newer( - doc_a3, save_conflict=True, replica_uid='r', replica_gen=3, - replica_trans_id='foo3') - self.assertEqual(state, 'superseded') - doc = self.db.get_doc(doc_a1.doc_id, True) - self.assertFalse(doc.has_conflicts) - rev = vectorclock.VectorClockRev(doc.rev) - rev_a3 = vectorclock.VectorClockRev('test:3') - rev_a1b1 = vectorclock.VectorClockRev('test:1|other:1') - self.assertTrue(rev.is_newer(rev_a3)) - self.assertTrue('test:4' in doc.rev) # locally increased - self.assertTrue(rev.is_newer(rev_a1b1)) - - def test_put_doc_if_newer_autoresolve_4(self): - doc_a1 = self.db.create_doc_from_json(simple_doc) - doc_a1b1 = self.make_document(doc_a1.doc_id, 'test:1|other:1', None) - doc_a2 = self.make_document(doc_a1.doc_id, 'test:2', '{"a":"42"}') - doc_a3 = self.make_document(doc_a1.doc_id, 'test:3', None) - state, _ = self.db._put_doc_if_newer( - doc_a1b1, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertEqual(state, 'inserted') - state, _ = self.db._put_doc_if_newer( - doc_a2, save_conflict=True, replica_uid='r', replica_gen=2, - replica_trans_id='foo2') - self.assertEqual(state, 'conflicted') - state, _ = self.db._put_doc_if_newer( - doc_a3, save_conflict=True, replica_uid='r', replica_gen=3, - replica_trans_id='foo3') - self.assertEqual(state, 'superseded') - doc = self.db.get_doc(doc_a1.doc_id, True) - self.assertFalse(doc.has_conflicts) - rev = vectorclock.VectorClockRev(doc.rev) - rev_a3 = vectorclock.VectorClockRev('test:3') - rev_a1b1 = vectorclock.VectorClockRev('test:1|other:1') - self.assertTrue(rev.is_newer(rev_a3)) - self.assertTrue('test:4' in doc.rev) # locally increased - self.assertTrue(rev.is_newer(rev_a1b1)) - - def test_put_refuses_to_update_conflicted(self): - doc1 = self.db.create_doc_from_json(simple_doc) - content2 = '{"key": "altval"}' - doc2 = self.make_document(doc1.doc_id, 'altrev:1', content2) - self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertGetDoc(self.db, doc1.doc_id, doc2.rev, content2, True) - content3 = '{"key": "local"}' - doc2.set_json(content3) - self.assertRaises(errors.ConflictedDoc, self.db.put_doc, doc2) - - def test_delete_refuses_for_conflicted(self): - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.make_document(doc1.doc_id, 'altrev:1', nested_doc) - self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertGetDoc(self.db, doc2.doc_id, doc2.rev, nested_doc, True) - self.assertRaises(errors.ConflictedDoc, self.db.delete_doc, doc2) - - -class DatabaseIndexTests(tests.DatabaseBaseTests): - - scenarios = tests.LOCAL_DATABASES_SCENARIOS - - def assertParseError(self, definition): - self.db.create_doc_from_json(nested_doc) - self.assertRaises( - errors.IndexDefinitionParseError, self.db.create_index, 'idx', - definition) - - def assertIndexCreatable(self, definition): - name = "idx" - self.db.create_doc_from_json(nested_doc) - self.db.create_index(name, definition) - self.assertEqual( - [(name, [definition])], self.db.list_indexes()) - - def test_create_index(self): - self.db.create_index('test-idx', 'name') - self.assertEqual([('test-idx', ['name'])], - self.db.list_indexes()) - - def test_create_index_on_non_ascii_field_name(self): - doc = self.db.create_doc_from_json(json.dumps({u'\xe5': 'value'})) - self.db.create_index('test-idx', u'\xe5') - self.assertEqual([doc], self.db.get_from_index('test-idx', 'value')) - - def test_list_indexes_with_non_ascii_field_names(self): - self.db.create_index('test-idx', u'\xe5') - self.assertEqual( - [('test-idx', [u'\xe5'])], self.db.list_indexes()) - - def test_create_index_evaluates_it(self): - doc = self.db.create_doc_from_json(simple_doc) - self.db.create_index('test-idx', 'key') - self.assertEqual([doc], self.db.get_from_index('test-idx', 'value')) - - def test_wildcard_matches_unicode_value(self): - doc = self.db.create_doc_from_json(json.dumps({"key": u"valu\xe5"})) - self.db.create_index('test-idx', 'key') - self.assertEqual([doc], self.db.get_from_index('test-idx', '*')) - - def test_retrieve_unicode_value_from_index(self): - doc = self.db.create_doc_from_json(json.dumps({"key": u"valu\xe5"})) - self.db.create_index('test-idx', 'key') - self.assertEqual( - [doc], self.db.get_from_index('test-idx', u"valu\xe5")) - - def test_create_index_fails_if_name_taken(self): - self.db.create_index('test-idx', 'key') - self.assertRaises(errors.IndexNameTakenError, - self.db.create_index, - 'test-idx', 'stuff') - - def test_create_index_does_not_fail_if_name_taken_with_same_index(self): - self.db.create_index('test-idx', 'key') - self.db.create_index('test-idx', 'key') - self.assertEqual([('test-idx', ['key'])], self.db.list_indexes()) - - def test_create_index_does_not_duplicate_indexed_fields(self): - self.db.create_doc_from_json(simple_doc) - self.db.create_index('test-idx', 'key') - self.db.delete_index('test-idx') - self.db.create_index('test-idx', 'key') - self.assertEqual(1, len(self.db.get_from_index('test-idx', 'value'))) - - def test_delete_index_does_not_remove_fields_from_other_indexes(self): - self.db.create_doc_from_json(simple_doc) - self.db.create_index('test-idx', 'key') - self.db.create_index('test-idx2', 'key') - self.db.delete_index('test-idx') - self.assertEqual(1, len(self.db.get_from_index('test-idx2', 'value'))) - - def test_create_index_after_deleting_document(self): - doc = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(simple_doc) - self.db.delete_doc(doc2) - self.db.create_index('test-idx', 'key') - self.assertEqual([doc], self.db.get_from_index('test-idx', 'value')) - - def test_delete_index(self): - self.db.create_index('test-idx', 'key') - self.assertEqual([('test-idx', ['key'])], self.db.list_indexes()) - self.db.delete_index('test-idx') - self.assertEqual([], self.db.list_indexes()) - - def test_create_adds_to_index(self): - self.db.create_index('test-idx', 'key') - doc = self.db.create_doc_from_json(simple_doc) - self.assertEqual([doc], self.db.get_from_index('test-idx', 'value')) - - def test_get_from_index_unmatched(self): - self.db.create_doc_from_json(simple_doc) - self.db.create_index('test-idx', 'key') - self.assertEqual([], self.db.get_from_index('test-idx', 'novalue')) - - def test_create_index_multiple_exact_matches(self): - doc = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(simple_doc) - self.db.create_index('test-idx', 'key') - self.assertEqual( - sorted([doc, doc2]), - sorted(self.db.get_from_index('test-idx', 'value'))) - - def test_get_from_index(self): - doc = self.db.create_doc_from_json(simple_doc) - self.db.create_index('test-idx', 'key') - self.assertEqual([doc], self.db.get_from_index('test-idx', 'value')) - - def test_get_from_index_multi(self): - content = '{"key": "value", "key2": "value2"}' - doc = self.db.create_doc_from_json(content) - self.db.create_index('test-idx', 'key', 'key2') - self.assertEqual( - [doc], self.db.get_from_index('test-idx', 'value', 'value2')) - - def test_get_from_index_multi_list(self): - doc = self.db.create_doc_from_json( - '{"key": "value", "key2": ["value2-1", "value2-2", "value2-3"]}') - self.db.create_index('test-idx', 'key', 'key2') - self.assertEqual( - [doc], self.db.get_from_index('test-idx', 'value', 'value2-1')) - self.assertEqual( - [doc], self.db.get_from_index('test-idx', 'value', 'value2-2')) - self.assertEqual( - [doc], self.db.get_from_index('test-idx', 'value', 'value2-3')) - self.assertEqual( - [('value', 'value2-1'), ('value', 'value2-2'), - ('value', 'value2-3')], - sorted(self.db.get_index_keys('test-idx'))) - - def test_get_from_index_sees_conflicts(self): - doc = self.db.create_doc_from_json(simple_doc) - self.db.create_index('test-idx', 'key', 'key2') - alt_doc = self.make_document( - doc.doc_id, 'alternate:1', - '{"key": "value", "key2": ["value2-1", "value2-2", "value2-3"]}') - self.db._put_doc_if_newer( - alt_doc, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - docs = self.db.get_from_index('test-idx', 'value', 'value2-1') - self.assertTrue(docs[0].has_conflicts) - - def test_get_index_keys_multi_list_list(self): - self.db.create_doc_from_json( - '{"key": "value1-1 value1-2 value1-3", ' - '"key2": ["value2-1", "value2-2", "value2-3"]}') - self.db.create_index('test-idx', 'split_words(key)', 'key2') - self.assertEqual( - [(u'value1-1', u'value2-1'), (u'value1-1', u'value2-2'), - (u'value1-1', u'value2-3'), (u'value1-2', u'value2-1'), - (u'value1-2', u'value2-2'), (u'value1-2', u'value2-3'), - (u'value1-3', u'value2-1'), (u'value1-3', u'value2-2'), - (u'value1-3', u'value2-3')], - sorted(self.db.get_index_keys('test-idx'))) - - def test_get_from_index_multi_ordered(self): - doc1 = self.db.create_doc_from_json( - '{"key": "value3", "key2": "value4"}') - doc2 = self.db.create_doc_from_json( - '{"key": "value2", "key2": "value3"}') - doc3 = self.db.create_doc_from_json( - '{"key": "value2", "key2": "value2"}') - doc4 = self.db.create_doc_from_json( - '{"key": "value1", "key2": "value1"}') - self.db.create_index('test-idx', 'key', 'key2') - self.assertEqual( - [doc4, doc3, doc2, doc1], - self.db.get_from_index('test-idx', 'v*', '*')) - - def test_get_range_from_index_start_end(self): - doc1 = self.db.create_doc_from_json('{"key": "value3"}') - doc2 = self.db.create_doc_from_json('{"key": "value2"}') - self.db.create_doc_from_json('{"key": "value4"}') - self.db.create_doc_from_json('{"key": "value1"}') - self.db.create_index('test-idx', 'key') - self.assertEqual( - [doc2, doc1], - self.db.get_range_from_index('test-idx', 'value2', 'value3')) - - def test_get_range_from_index_start(self): - doc1 = self.db.create_doc_from_json('{"key": "value3"}') - doc2 = self.db.create_doc_from_json('{"key": "value2"}') - doc3 = self.db.create_doc_from_json('{"key": "value4"}') - self.db.create_doc_from_json('{"key": "value1"}') - self.db.create_index('test-idx', 'key') - self.assertEqual( - [doc2, doc1, doc3], - self.db.get_range_from_index('test-idx', 'value2')) - - def test_get_range_from_index_sees_conflicts(self): - doc = self.db.create_doc_from_json(simple_doc) - self.db.create_index('test-idx', 'key') - alt_doc = self.make_document( - doc.doc_id, 'alternate:1', '{"key": "valuedepalue"}') - self.db._put_doc_if_newer( - alt_doc, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - docs = self.db.get_range_from_index('test-idx', 'a') - self.assertTrue(docs[0].has_conflicts) - - def test_get_range_from_index_end(self): - self.db.create_doc_from_json('{"key": "value3"}') - doc2 = self.db.create_doc_from_json('{"key": "value2"}') - self.db.create_doc_from_json('{"key": "value4"}') - doc4 = self.db.create_doc_from_json('{"key": "value1"}') - self.db.create_index('test-idx', 'key') - self.assertEqual( - [doc4, doc2], - self.db.get_range_from_index('test-idx', None, 'value2')) - - def test_get_wildcard_range_from_index_start(self): - doc1 = self.db.create_doc_from_json('{"key": "value4"}') - doc2 = self.db.create_doc_from_json('{"key": "value23"}') - doc3 = self.db.create_doc_from_json('{"key": "value2"}') - doc4 = self.db.create_doc_from_json('{"key": "value22"}') - self.db.create_doc_from_json('{"key": "value1"}') - self.db.create_index('test-idx', 'key') - self.assertEqual( - [doc3, doc4, doc2, doc1], - self.db.get_range_from_index('test-idx', 'value2*')) - - def test_get_wildcard_range_from_index_end(self): - self.db.create_doc_from_json('{"key": "value4"}') - doc2 = self.db.create_doc_from_json('{"key": "value23"}') - doc3 = self.db.create_doc_from_json('{"key": "value2"}') - doc4 = self.db.create_doc_from_json('{"key": "value22"}') - doc5 = self.db.create_doc_from_json('{"key": "value1"}') - self.db.create_index('test-idx', 'key') - self.assertEqual( - [doc5, doc3, doc4, doc2], - self.db.get_range_from_index('test-idx', None, 'value2*')) - - def test_get_wildcard_range_from_index_start_end(self): - self.db.create_doc_from_json('{"key": "a"}') - self.db.create_doc_from_json('{"key": "boo3"}') - doc3 = self.db.create_doc_from_json('{"key": "catalyst"}') - doc4 = self.db.create_doc_from_json('{"key": "whaever"}') - self.db.create_doc_from_json('{"key": "zerg"}') - self.db.create_index('test-idx', 'key') - self.assertEqual( - [doc3, doc4], - self.db.get_range_from_index('test-idx', 'cat*', 'zap*')) - - def test_get_range_from_index_multi_column_start_end(self): - self.db.create_doc_from_json('{"key": "value3", "key2": "value4"}') - doc2 = self.db.create_doc_from_json( - '{"key": "value2", "key2": "value3"}') - doc3 = self.db.create_doc_from_json( - '{"key": "value2", "key2": "value2"}') - self.db.create_doc_from_json('{"key": "value1", "key2": "value1"}') - self.db.create_index('test-idx', 'key', 'key2') - self.assertEqual( - [doc3, doc2], - self.db.get_range_from_index( - 'test-idx', ('value2', 'value2'), ('value2', 'value3'))) - - def test_get_range_from_index_multi_column_start(self): - doc1 = self.db.create_doc_from_json( - '{"key": "value3", "key2": "value4"}') - doc2 = self.db.create_doc_from_json( - '{"key": "value2", "key2": "value3"}') - self.db.create_doc_from_json('{"key": "value2", "key2": "value2"}') - self.db.create_doc_from_json('{"key": "value1", "key2": "value1"}') - self.db.create_index('test-idx', 'key', 'key2') - self.assertEqual( - [doc2, doc1], - self.db.get_range_from_index('test-idx', ('value2', 'value3'))) - - def test_get_range_from_index_multi_column_end(self): - self.db.create_doc_from_json('{"key": "value3", "key2": "value4"}') - doc2 = self.db.create_doc_from_json( - '{"key": "value2", "key2": "value3"}') - doc3 = self.db.create_doc_from_json( - '{"key": "value2", "key2": "value2"}') - doc4 = self.db.create_doc_from_json( - '{"key": "value1", "key2": "value1"}') - self.db.create_index('test-idx', 'key', 'key2') - self.assertEqual( - [doc4, doc3, doc2], - self.db.get_range_from_index( - 'test-idx', None, ('value2', 'value3'))) - - def test_get_wildcard_range_from_index_multi_column_start(self): - doc1 = self.db.create_doc_from_json( - '{"key": "value3", "key2": "value4"}') - doc2 = self.db.create_doc_from_json( - '{"key": "value2", "key2": "value23"}') - doc3 = self.db.create_doc_from_json( - '{"key": "value2", "key2": "value2"}') - self.db.create_doc_from_json('{"key": "value1", "key2": "value1"}') - self.db.create_index('test-idx', 'key', 'key2') - self.assertEqual( - [doc3, doc2, doc1], - self.db.get_range_from_index('test-idx', ('value2', 'value2*'))) - - def test_get_wildcard_range_from_index_multi_column_end(self): - self.db.create_doc_from_json('{"key": "value3", "key2": "value4"}') - doc2 = self.db.create_doc_from_json( - '{"key": "value2", "key2": "value23"}') - doc3 = self.db.create_doc_from_json( - '{"key": "value2", "key2": "value2"}') - doc4 = self.db.create_doc_from_json( - '{"key": "value1", "key2": "value1"}') - self.db.create_index('test-idx', 'key', 'key2') - self.assertEqual( - [doc4, doc3, doc2], - self.db.get_range_from_index( - 'test-idx', None, ('value2', 'value2*'))) - - def test_get_glob_range_from_index_multi_column_start(self): - doc1 = self.db.create_doc_from_json( - '{"key": "value3", "key2": "value4"}') - doc2 = self.db.create_doc_from_json( - '{"key": "value2", "key2": "value23"}') - self.db.create_doc_from_json('{"key": "value1", "key2": "value2"}') - self.db.create_doc_from_json('{"key": "value1", "key2": "value1"}') - self.db.create_index('test-idx', 'key', 'key2') - self.assertEqual( - [doc2, doc1], - self.db.get_range_from_index('test-idx', ('value2', '*'))) - - def test_get_glob_range_from_index_multi_column_end(self): - self.db.create_doc_from_json('{"key": "value3", "key2": "value4"}') - doc2 = self.db.create_doc_from_json( - '{"key": "value2", "key2": "value23"}') - doc3 = self.db.create_doc_from_json( - '{"key": "value1", "key2": "value2"}') - doc4 = self.db.create_doc_from_json( - '{"key": "value1", "key2": "value1"}') - self.db.create_index('test-idx', 'key', 'key2') - self.assertEqual( - [doc4, doc3, doc2], - self.db.get_range_from_index('test-idx', None, ('value2', '*'))) - - def test_get_range_from_index_illegal_wildcard_order(self): - self.db.create_index('test-idx', 'k1', 'k2') - self.assertRaises( - errors.InvalidGlobbing, - self.db.get_range_from_index, 'test-idx', ('*', 'v2')) - - def test_get_range_from_index_illegal_glob_after_wildcard(self): - self.db.create_index('test-idx', 'k1', 'k2') - self.assertRaises( - errors.InvalidGlobbing, - self.db.get_range_from_index, 'test-idx', ('*', 'v*')) - - def test_get_range_from_index_illegal_wildcard_order_end(self): - self.db.create_index('test-idx', 'k1', 'k2') - self.assertRaises( - errors.InvalidGlobbing, - self.db.get_range_from_index, 'test-idx', None, ('*', 'v2')) - - def test_get_range_from_index_illegal_glob_after_wildcard_end(self): - self.db.create_index('test-idx', 'k1', 'k2') - self.assertRaises( - errors.InvalidGlobbing, - self.db.get_range_from_index, 'test-idx', None, ('*', 'v*')) - - def test_get_from_index_fails_if_no_index(self): - self.assertRaises( - errors.IndexDoesNotExist, self.db.get_from_index, 'foo') - - def test_get_index_keys_fails_if_no_index(self): - self.assertRaises(errors.IndexDoesNotExist, - self.db.get_index_keys, - 'foo') - - def test_get_index_keys_works_if_no_docs(self): - self.db.create_index('test-idx', 'key') - self.assertEqual([], self.db.get_index_keys('test-idx')) - - def test_put_updates_index(self): - doc = self.db.create_doc_from_json(simple_doc) - self.db.create_index('test-idx', 'key') - new_content = '{"key": "altval"}' - doc.set_json(new_content) - self.db.put_doc(doc) - self.assertEqual([], self.db.get_from_index('test-idx', 'value')) - self.assertEqual([doc], self.db.get_from_index('test-idx', 'altval')) - - def test_delete_updates_index(self): - doc = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(simple_doc) - self.db.create_index('test-idx', 'key') - self.assertEqual( - sorted([doc, doc2]), - sorted(self.db.get_from_index('test-idx', 'value'))) - self.db.delete_doc(doc) - self.assertEqual([doc2], self.db.get_from_index('test-idx', 'value')) - - def test_get_from_index_illegal_number_of_entries(self): - self.db.create_index('test-idx', 'k1', 'k2') - self.assertRaises( - errors.InvalidValueForIndex, self.db.get_from_index, 'test-idx') - self.assertRaises( - errors.InvalidValueForIndex, - self.db.get_from_index, 'test-idx', 'v1') - self.assertRaises( - errors.InvalidValueForIndex, - self.db.get_from_index, 'test-idx', 'v1', 'v2', 'v3') - - def test_get_from_index_illegal_wildcard_order(self): - self.db.create_index('test-idx', 'k1', 'k2') - self.assertRaises( - errors.InvalidGlobbing, - self.db.get_from_index, 'test-idx', '*', 'v2') - - def test_get_from_index_illegal_glob_after_wildcard(self): - self.db.create_index('test-idx', 'k1', 'k2') - self.assertRaises( - errors.InvalidGlobbing, - self.db.get_from_index, 'test-idx', '*', 'v*') - - def test_get_all_from_index(self): - self.db.create_index('test-idx', 'key') - doc1 = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(nested_doc) - # This one should not be in the index - self.db.create_doc_from_json('{"no": "key"}') - diff_value_doc = '{"key": "diff value"}' - doc4 = self.db.create_doc_from_json(diff_value_doc) - # This is essentially a 'prefix' match, but we match every entry. - self.assertEqual( - sorted([doc1, doc2, doc4]), - sorted(self.db.get_from_index('test-idx', '*'))) - - def test_get_all_from_index_ordered(self): - self.db.create_index('test-idx', 'key') - doc1 = self.db.create_doc_from_json('{"key": "value x"}') - doc2 = self.db.create_doc_from_json('{"key": "value b"}') - doc3 = self.db.create_doc_from_json('{"key": "value a"}') - doc4 = self.db.create_doc_from_json('{"key": "value m"}') - # This is essentially a 'prefix' match, but we match every entry. - self.assertEqual( - [doc3, doc2, doc4, doc1], self.db.get_from_index('test-idx', '*')) - - def test_put_updates_when_adding_key(self): - doc = self.db.create_doc_from_json("{}") - self.db.create_index('test-idx', 'key') - self.assertEqual([], self.db.get_from_index('test-idx', '*')) - doc.set_json(simple_doc) - self.db.put_doc(doc) - self.assertEqual([doc], self.db.get_from_index('test-idx', '*')) - - def test_get_from_index_empty_string(self): - self.db.create_index('test-idx', 'key') - doc1 = self.db.create_doc_from_json(simple_doc) - content2 = '{"key": ""}' - doc2 = self.db.create_doc_from_json(content2) - self.assertEqual([doc2], self.db.get_from_index('test-idx', '')) - # Empty string matches the wildcard. - self.assertEqual( - sorted([doc1, doc2]), - sorted(self.db.get_from_index('test-idx', '*'))) - - def test_get_from_index_not_null(self): - self.db.create_index('test-idx', 'key') - doc1 = self.db.create_doc_from_json(simple_doc) - self.db.create_doc_from_json('{"key": null}') - self.assertEqual([doc1], self.db.get_from_index('test-idx', '*')) - - def test_get_partial_from_index(self): - content1 = '{"k1": "v1", "k2": "v2"}' - content2 = '{"k1": "v1", "k2": "x2"}' - content3 = '{"k1": "v1", "k2": "y2"}' - # doc4 has a different k1 value, so it doesn't match the prefix. - content4 = '{"k1": "NN", "k2": "v2"}' - doc1 = self.db.create_doc_from_json(content1) - doc2 = self.db.create_doc_from_json(content2) - doc3 = self.db.create_doc_from_json(content3) - self.db.create_doc_from_json(content4) - self.db.create_index('test-idx', 'k1', 'k2') - self.assertEqual( - sorted([doc1, doc2, doc3]), - sorted(self.db.get_from_index('test-idx', "v1", "*"))) - - def test_get_glob_match(self): - # Note: the exact glob syntax is probably subject to change - content1 = '{"k1": "v1", "k2": "v1"}' - content2 = '{"k1": "v1", "k2": "v2"}' - content3 = '{"k1": "v1", "k2": "v3"}' - # doc4 has a different k2 prefix value, so it doesn't match - content4 = '{"k1": "v1", "k2": "ZZ"}' - self.db.create_index('test-idx', 'k1', 'k2') - doc1 = self.db.create_doc_from_json(content1) - doc2 = self.db.create_doc_from_json(content2) - doc3 = self.db.create_doc_from_json(content3) - self.db.create_doc_from_json(content4) - self.assertEqual( - sorted([doc1, doc2, doc3]), - sorted(self.db.get_from_index('test-idx', "v1", "v*"))) - - def test_nested_index(self): - doc = self.db.create_doc_from_json(nested_doc) - self.db.create_index('test-idx', 'sub.doc') - self.assertEqual( - [doc], self.db.get_from_index('test-idx', 'underneath')) - doc2 = self.db.create_doc_from_json(nested_doc) - self.assertEqual( - sorted([doc, doc2]), - sorted(self.db.get_from_index('test-idx', 'underneath'))) - - def test_nested_nonexistent(self): - self.db.create_doc_from_json(nested_doc) - # sub exists, but sub.foo does not: - self.db.create_index('test-idx', 'sub.foo') - self.assertEqual([], self.db.get_from_index('test-idx', '*')) - - def test_nested_nonexistent2(self): - self.db.create_doc_from_json(nested_doc) - self.db.create_index('test-idx', 'sub.foo.bar.baz.qux.fnord') - self.assertEqual([], self.db.get_from_index('test-idx', '*')) - - def test_nested_traverses_lists(self): - # subpath finds dicts in list - doc = self.db.create_doc_from_json( - '{"foo": [{"zap": "bar"}, {"zap": "baz"}]}') - # subpath only finds dicts in list - self.db.create_doc_from_json('{"foo": ["zap", "baz"]}') - self.db.create_index('test-idx', 'foo.zap') - self.assertEqual([doc], self.db.get_from_index('test-idx', 'bar')) - self.assertEqual([doc], self.db.get_from_index('test-idx', 'baz')) - - def test_nested_list_traversal(self): - # subpath finds dicts in list - doc = self.db.create_doc_from_json( - '{"foo": [{"zap": [{"qux": "fnord"}, {"qux": "zombo"}]},' - '{"zap": "baz"}]}') - # subpath only finds dicts in list - self.db.create_index('test-idx', 'foo.zap.qux') - self.assertEqual([doc], self.db.get_from_index('test-idx', 'fnord')) - self.assertEqual([doc], self.db.get_from_index('test-idx', 'zombo')) - - def test_index_list1(self): - self.db.create_index("index", "name") - content = '{"name": ["foo", "bar"]}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "bar") - self.assertEqual([doc], rows) - - def test_index_list2(self): - self.db.create_index("index", "name") - content = '{"name": ["foo", "bar"]}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "foo") - self.assertEqual([doc], rows) - - def test_get_from_index_case_sensitive(self): - self.db.create_index('test-idx', 'key') - doc1 = self.db.create_doc_from_json(simple_doc) - self.assertEqual([], self.db.get_from_index('test-idx', 'V*')) - self.assertEqual([doc1], self.db.get_from_index('test-idx', 'v*')) - - def test_get_from_index_illegal_glob_before_value(self): - self.db.create_index('test-idx', 'k1', 'k2') - self.assertRaises( - errors.InvalidGlobbing, - self.db.get_from_index, 'test-idx', 'v*', 'v2') - - def test_get_from_index_illegal_glob_after_glob(self): - self.db.create_index('test-idx', 'k1', 'k2') - self.assertRaises( - errors.InvalidGlobbing, - self.db.get_from_index, 'test-idx', 'v*', 'v*') - - def test_get_from_index_with_sql_wildcards(self): - self.db.create_index('test-idx', 'key') - content1 = '{"key": "va%lue"}' - content2 = '{"key": "value"}' - content3 = '{"key": "va_lue"}' - doc1 = self.db.create_doc_from_json(content1) - self.db.create_doc_from_json(content2) - doc3 = self.db.create_doc_from_json(content3) - # The '%' in the search should be treated literally, not as a sql - # globbing character. - self.assertEqual([doc1], self.db.get_from_index('test-idx', 'va%*')) - # Same for '_' - self.assertEqual([doc3], self.db.get_from_index('test-idx', 'va_*')) - - def test_get_from_index_with_lower(self): - self.db.create_index("index", "lower(name)") - content = '{"name": "Foo"}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "foo") - self.assertEqual([doc], rows) - - def test_get_from_index_with_lower_matches_same_case(self): - self.db.create_index("index", "lower(name)") - content = '{"name": "foo"}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "foo") - self.assertEqual([doc], rows) - - def test_index_lower_doesnt_match_different_case(self): - self.db.create_index("index", "lower(name)") - content = '{"name": "Foo"}' - self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "Foo") - self.assertEqual([], rows) - - def test_index_lower_doesnt_match_other_index(self): - self.db.create_index("index", "lower(name)") - self.db.create_index("other_index", "name") - content = '{"name": "Foo"}' - self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "Foo") - self.assertEqual(0, len(rows)) - - def test_index_split_words_match_first(self): - self.db.create_index("index", "split_words(name)") - content = '{"name": "foo bar"}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "foo") - self.assertEqual([doc], rows) - - def test_index_split_words_match_second(self): - self.db.create_index("index", "split_words(name)") - content = '{"name": "foo bar"}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "bar") - self.assertEqual([doc], rows) - - def test_index_split_words_match_both(self): - self.db.create_index("index", "split_words(name)") - content = '{"name": "foo foo"}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "foo") - self.assertEqual([doc], rows) - - def test_index_split_words_double_space(self): - self.db.create_index("index", "split_words(name)") - content = '{"name": "foo bar"}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "bar") - self.assertEqual([doc], rows) - - def test_index_split_words_leading_space(self): - self.db.create_index("index", "split_words(name)") - content = '{"name": " foo bar"}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "foo") - self.assertEqual([doc], rows) - - def test_index_split_words_trailing_space(self): - self.db.create_index("index", "split_words(name)") - content = '{"name": "foo bar "}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "bar") - self.assertEqual([doc], rows) - - def test_get_from_index_with_number(self): - self.db.create_index("index", "number(foo, 5)") - content = '{"foo": 12}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "00012") - self.assertEqual([doc], rows) - - def test_get_from_index_with_number_bigger_than_padding(self): - self.db.create_index("index", "number(foo, 5)") - content = '{"foo": 123456}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "123456") - self.assertEqual([doc], rows) - - def test_number_mapping_ignores_non_numbers(self): - self.db.create_index("index", "number(foo, 5)") - content = '{"foo": 56}' - doc1 = self.db.create_doc_from_json(content) - content = '{"foo": "this is not a maigret painting"}' - self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "*") - self.assertEqual([doc1], rows) - - def test_get_from_index_with_bool(self): - self.db.create_index("index", "bool(foo)") - content = '{"foo": true}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "1") - self.assertEqual([doc], rows) - - def test_get_from_index_with_bool_false(self): - self.db.create_index("index", "bool(foo)") - content = '{"foo": false}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "0") - self.assertEqual([doc], rows) - - def test_get_from_index_with_non_bool(self): - self.db.create_index("index", "bool(foo)") - content = '{"foo": 42}' - self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "*") - self.assertEqual([], rows) - - def test_get_from_index_with_combine(self): - self.db.create_index("index", "combine(foo, bar)") - content = '{"foo": "value1", "bar": "value2"}' - doc = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "value1") - self.assertEqual([doc], rows) - rows = self.db.get_from_index("index", "value2") - self.assertEqual([doc], rows) - - def test_get_complex_combine(self): - self.db.create_index( - "index", "combine(number(foo, 5), lower(bar), split_words(baz))") - content = '{"foo": 12, "bar": "ALLCAPS", "baz": "qux nox"}' - doc = self.db.create_doc_from_json(content) - content = '{"foo": "not a number", "bar": "something"}' - doc2 = self.db.create_doc_from_json(content) - rows = self.db.get_from_index("index", "00012") - self.assertEqual([doc], rows) - rows = self.db.get_from_index("index", "allcaps") - self.assertEqual([doc], rows) - rows = self.db.get_from_index("index", "nox") - self.assertEqual([doc], rows) - rows = self.db.get_from_index("index", "something") - self.assertEqual([doc2], rows) - - def test_get_index_keys_from_index(self): - self.db.create_index('test-idx', 'key') - content1 = '{"key": "value1"}' - content2 = '{"key": "value2"}' - content3 = '{"key": "value2"}' - self.db.create_doc_from_json(content1) - self.db.create_doc_from_json(content2) - self.db.create_doc_from_json(content3) - self.assertEqual( - [('value1',), ('value2',)], - sorted(self.db.get_index_keys('test-idx'))) - - def test_get_index_keys_from_multicolumn_index(self): - self.db.create_index('test-idx', 'key1', 'key2') - content1 = '{"key1": "value1", "key2": "val2-1"}' - content2 = '{"key1": "value2", "key2": "val2-2"}' - content3 = '{"key1": "value2", "key2": "val2-2"}' - content4 = '{"key1": "value2", "key2": "val3"}' - self.db.create_doc_from_json(content1) - self.db.create_doc_from_json(content2) - self.db.create_doc_from_json(content3) - self.db.create_doc_from_json(content4) - self.assertEqual([ - ('value1', 'val2-1'), - ('value2', 'val2-2'), - ('value2', 'val3')], - sorted(self.db.get_index_keys('test-idx'))) - - def test_empty_expr(self): - self.assertParseError('') - - def test_nested_unknown_operation(self): - self.assertParseError('unknown_operation(field1)') - - def test_parse_missing_close_paren(self): - self.assertParseError("lower(a") - - def test_parse_trailing_close_paren(self): - self.assertParseError("lower(ab))") - - def test_parse_trailing_chars(self): - self.assertParseError("lower(ab)adsf") - - def test_parse_empty_op(self): - self.assertParseError("(ab)") - - def test_parse_top_level_commas(self): - self.assertParseError("a, b") - - def test_invalid_field_name(self): - self.assertParseError("a.") - - def test_invalid_inner_field_name(self): - self.assertParseError("lower(a.)") - - def test_gobbledigook(self): - self.assertParseError("(@#@cc @#!*DFJSXV(()jccd") - - def test_leading_space(self): - self.assertIndexCreatable(" lower(a)") - - def test_trailing_space(self): - self.assertIndexCreatable("lower(a) ") - - def test_spaces_before_open_paren(self): - self.assertIndexCreatable("lower (a)") - - def test_spaces_after_open_paren(self): - self.assertIndexCreatable("lower( a)") - - def test_spaces_before_close_paren(self): - self.assertIndexCreatable("lower(a )") - - def test_spaces_before_comma(self): - self.assertIndexCreatable("combine(a , b , c)") - - def test_spaces_after_comma(self): - self.assertIndexCreatable("combine(a, b, c)") - - def test_all_together_now(self): - self.assertParseError(' (a) ') - - def test_all_together_now2(self): - self.assertParseError('combine(lower(x)x,foo)') - - -class PythonBackendTests(tests.DatabaseBaseTests): - - def setUp(self): - super(PythonBackendTests, self).setUp() - self.simple_doc = json.loads(simple_doc) - - def test_create_doc_with_factory(self): - self.db.set_document_factory(TestAlternativeDocument) - doc = self.db.create_doc(self.simple_doc, doc_id='my_doc_id') - self.assertTrue(isinstance(doc, TestAlternativeDocument)) - - def test_get_doc_after_put_with_factory(self): - doc = self.db.create_doc(self.simple_doc, doc_id='my_doc_id') - self.db.set_document_factory(TestAlternativeDocument) - result = self.db.get_doc('my_doc_id') - self.assertTrue(isinstance(result, TestAlternativeDocument)) - self.assertEqual(doc.doc_id, result.doc_id) - self.assertEqual(doc.rev, result.rev) - self.assertEqual(doc.get_json(), result.get_json()) - self.assertEqual(False, result.has_conflicts) - - def test_get_doc_nonexisting_with_factory(self): - self.db.set_document_factory(TestAlternativeDocument) - self.assertIs(None, self.db.get_doc('non-existing')) - - def test_get_all_docs_with_factory(self): - self.db.set_document_factory(TestAlternativeDocument) - self.db.create_doc(self.simple_doc) - self.assertTrue(isinstance( - list(self.db.get_all_docs()[1])[0], TestAlternativeDocument)) - - def test_get_docs_conflicted_with_factory(self): - self.db.set_document_factory(TestAlternativeDocument) - doc1 = self.db.create_doc(self.simple_doc) - doc2 = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) - self.db._put_doc_if_newer( - doc2, save_conflict=True, replica_uid='r', replica_gen=1, - replica_trans_id='foo') - self.assertTrue( - isinstance( - list(self.db.get_docs([doc1.doc_id]))[0], - TestAlternativeDocument)) - - def test_get_from_index_with_factory(self): - self.db.set_document_factory(TestAlternativeDocument) - self.db.create_doc(self.simple_doc) - self.db.create_index('test-idx', 'key') - self.assertTrue( - isinstance( - self.db.get_from_index('test-idx', 'value')[0], - TestAlternativeDocument)) - - def test_sync_exchange_updates_indexes(self): - doc = self.db.create_doc(self.simple_doc) - self.db.create_index('test-idx', 'key') - new_content = '{"key": "altval"}' - other_rev = 'test:1|z:2' - st = self.db.get_sync_target() - - def ignore(doc_id, doc_rev, doc): - pass - - doc_other = self.make_document(doc.doc_id, other_rev, new_content) - docs_by_gen = [(doc_other, 10, 'T-sid')] - st.sync_exchange( - docs_by_gen, 'other-replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=ignore) - self.assertGetDoc(self.db, doc.doc_id, other_rev, new_content, False) - self.assertEqual( - [doc_other], self.db.get_from_index('test-idx', 'altval')) - self.assertEqual([], self.db.get_from_index('test-idx', 'value')) - - -# Use a custom loader to apply the scenarios at load time. -load_tests = tests.load_with_scenarios diff --git a/src/leap/soledad/tests/u1db_tests/test_document.py b/src/leap/soledad/tests/u1db_tests/test_document.py deleted file mode 100644 index e706e1a9..00000000 --- a/src/leap/soledad/tests/u1db_tests/test_document.py +++ /dev/null @@ -1,150 +0,0 @@ -# Copyright 2011 Canonical Ltd. -# -# This file is part of u1db. -# -# u1db is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 -# as published by the Free Software Foundation. -# -# u1db is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with u1db. If not, see . - - -from u1db import errors - -from leap.soledad.tests import u1db_tests as tests - - -class TestDocument(tests.TestCase): - - scenarios = ([( - 'py', {'make_document_for_test': tests.make_document_for_test})]) # + - #tests.C_DATABASE_SCENARIOS) - - def test_create_doc(self): - doc = self.make_document('doc-id', 'uid:1', tests.simple_doc) - self.assertEqual('doc-id', doc.doc_id) - self.assertEqual('uid:1', doc.rev) - self.assertEqual(tests.simple_doc, doc.get_json()) - self.assertFalse(doc.has_conflicts) - - def test__repr__(self): - doc = self.make_document('doc-id', 'uid:1', tests.simple_doc) - self.assertEqual( - '%s(doc-id, uid:1, \'{"key": "value"}\')' - % (doc.__class__.__name__,), - repr(doc)) - - def test__repr__conflicted(self): - doc = self.make_document('doc-id', 'uid:1', tests.simple_doc, - has_conflicts=True) - self.assertEqual( - '%s(doc-id, uid:1, conflicted, \'{"key": "value"}\')' - % (doc.__class__.__name__,), - repr(doc)) - - def test__lt__(self): - doc_a = self.make_document('a', 'b', '{}') - doc_b = self.make_document('b', 'b', '{}') - self.assertTrue(doc_a < doc_b) - self.assertTrue(doc_b > doc_a) - doc_aa = self.make_document('a', 'a', '{}') - self.assertTrue(doc_aa < doc_a) - - def test__eq__(self): - doc_a = self.make_document('a', 'b', '{}') - doc_b = self.make_document('a', 'b', '{}') - self.assertTrue(doc_a == doc_b) - doc_b = self.make_document('a', 'b', '{}', has_conflicts=True) - self.assertFalse(doc_a == doc_b) - - def test_non_json_dict(self): - self.assertRaises( - errors.InvalidJSON, self.make_document, 'id', 'uid:1', - '"not a json dictionary"') - - def test_non_json(self): - self.assertRaises( - errors.InvalidJSON, self.make_document, 'id', 'uid:1', - 'not a json dictionary') - - def test_get_size(self): - doc_a = self.make_document('a', 'b', '{"some": "content"}') - self.assertEqual( - len('a' + 'b' + '{"some": "content"}'), doc_a.get_size()) - - def test_get_size_empty_document(self): - doc_a = self.make_document('a', 'b', None) - self.assertEqual(len('a' + 'b'), doc_a.get_size()) - - -class TestPyDocument(tests.TestCase): - - scenarios = ([( - 'py', {'make_document_for_test': tests.make_document_for_test})]) - - def test_get_content(self): - doc = self.make_document('id', 'rev', '{"content":""}') - self.assertEqual({"content": ""}, doc.content) - doc.set_json('{"content": "new"}') - self.assertEqual({"content": "new"}, doc.content) - - def test_set_content(self): - doc = self.make_document('id', 'rev', '{"content":""}') - doc.content = {"content": "new"} - self.assertEqual('{"content": "new"}', doc.get_json()) - - def test_set_bad_content(self): - doc = self.make_document('id', 'rev', '{"content":""}') - self.assertRaises( - errors.InvalidContent, setattr, doc, 'content', - '{"content": "new"}') - - def test_is_tombstone(self): - doc_a = self.make_document('a', 'b', '{}') - self.assertFalse(doc_a.is_tombstone()) - doc_a.set_json(None) - self.assertTrue(doc_a.is_tombstone()) - - def test_make_tombstone(self): - doc_a = self.make_document('a', 'b', '{}') - self.assertFalse(doc_a.is_tombstone()) - doc_a.make_tombstone() - self.assertTrue(doc_a.is_tombstone()) - - def test_same_content_as(self): - doc_a = self.make_document('a', 'b', '{}') - doc_b = self.make_document('d', 'e', '{}') - self.assertTrue(doc_a.same_content_as(doc_b)) - doc_b = self.make_document('p', 'q', '{}', has_conflicts=True) - self.assertTrue(doc_a.same_content_as(doc_b)) - doc_b.content['key'] = 'value' - self.assertFalse(doc_a.same_content_as(doc_b)) - - def test_same_content_as_json_order(self): - doc_a = self.make_document( - 'a', 'b', '{"key1": "val1", "key2": "val2"}') - doc_b = self.make_document( - 'c', 'd', '{"key2": "val2", "key1": "val1"}') - self.assertTrue(doc_a.same_content_as(doc_b)) - - def test_set_json(self): - doc = self.make_document('id', 'rev', '{"content":""}') - doc.set_json('{"content": "new"}') - self.assertEqual('{"content": "new"}', doc.get_json()) - - def test_set_json_non_dict(self): - doc = self.make_document('id', 'rev', '{"content":""}') - self.assertRaises(errors.InvalidJSON, doc.set_json, '"is not a dict"') - - def test_set_json_error(self): - doc = self.make_document('id', 'rev', '{"content":""}') - self.assertRaises(errors.InvalidJSON, doc.set_json, 'is not json') - - -load_tests = tests.load_with_scenarios diff --git a/src/leap/soledad/tests/u1db_tests/test_http_app.py b/src/leap/soledad/tests/u1db_tests/test_http_app.py deleted file mode 100644 index e0729aa2..00000000 --- a/src/leap/soledad/tests/u1db_tests/test_http_app.py +++ /dev/null @@ -1,1135 +0,0 @@ -# Copyright 2011-2012 Canonical Ltd. -# -# This file is part of u1db. -# -# u1db is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 -# as published by the Free Software Foundation. -# -# u1db is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with u1db. If not, see . - -"""Test the WSGI app.""" - -import paste.fixture -import sys -try: - import simplejson as json -except ImportError: - import json # noqa -import StringIO - -from u1db import ( - __version__ as _u1db_version, - errors, - sync, -) - -from leap.soledad.tests import u1db_tests as tests - -from u1db.remote import ( - http_app, - http_errors, -) - - -class TestFencedReader(tests.TestCase): - - def test_init(self): - reader = http_app._FencedReader(StringIO.StringIO(""), 25, 100) - self.assertEqual(25, reader.remaining) - - def test_read_chunk(self): - inp = StringIO.StringIO("abcdef") - reader = http_app._FencedReader(inp, 5, 10) - data = reader.read_chunk(2) - self.assertEqual("ab", data) - self.assertEqual(2, inp.tell()) - self.assertEqual(3, reader.remaining) - - def test_read_chunk_remaining(self): - inp = StringIO.StringIO("abcdef") - reader = http_app._FencedReader(inp, 4, 10) - data = reader.read_chunk(9999) - self.assertEqual("abcd", data) - self.assertEqual(4, inp.tell()) - self.assertEqual(0, reader.remaining) - - def test_read_chunk_nothing_left(self): - inp = StringIO.StringIO("abc") - reader = http_app._FencedReader(inp, 2, 10) - reader.read_chunk(2) - self.assertEqual(2, inp.tell()) - self.assertEqual(0, reader.remaining) - data = reader.read_chunk(2) - self.assertEqual("", data) - self.assertEqual(2, inp.tell()) - self.assertEqual(0, reader.remaining) - - def test_read_chunk_kept(self): - inp = StringIO.StringIO("abcde") - reader = http_app._FencedReader(inp, 4, 10) - reader._kept = "xyz" - data = reader.read_chunk(2) # atmost ignored - self.assertEqual("xyz", data) - self.assertEqual(0, inp.tell()) - self.assertEqual(4, reader.remaining) - self.assertIsNone(reader._kept) - - def test_getline(self): - inp = StringIO.StringIO("abc\r\nde") - reader = http_app._FencedReader(inp, 6, 10) - reader.MAXCHUNK = 6 - line = reader.getline() - self.assertEqual("abc\r\n", line) - self.assertEqual("d", reader._kept) - - def test_getline_exact(self): - inp = StringIO.StringIO("abcd\r\nef") - reader = http_app._FencedReader(inp, 6, 10) - reader.MAXCHUNK = 6 - line = reader.getline() - self.assertEqual("abcd\r\n", line) - self.assertIs(None, reader._kept) - - def test_getline_no_newline(self): - inp = StringIO.StringIO("abcd") - reader = http_app._FencedReader(inp, 4, 10) - reader.MAXCHUNK = 6 - line = reader.getline() - self.assertEqual("abcd", line) - - def test_getline_many_chunks(self): - inp = StringIO.StringIO("abcde\r\nf") - reader = http_app._FencedReader(inp, 8, 10) - reader.MAXCHUNK = 4 - line = reader.getline() - self.assertEqual("abcde\r\n", line) - self.assertEqual("f", reader._kept) - line = reader.getline() - self.assertEqual("f", line) - - def test_getline_empty(self): - inp = StringIO.StringIO("") - reader = http_app._FencedReader(inp, 0, 10) - reader.MAXCHUNK = 4 - line = reader.getline() - self.assertEqual("", line) - line = reader.getline() - self.assertEqual("", line) - - def test_getline_just_newline(self): - inp = StringIO.StringIO("\r\n") - reader = http_app._FencedReader(inp, 2, 10) - reader.MAXCHUNK = 4 - line = reader.getline() - self.assertEqual("\r\n", line) - line = reader.getline() - self.assertEqual("", line) - - def test_getline_too_large(self): - inp = StringIO.StringIO("x" * 50) - reader = http_app._FencedReader(inp, 50, 25) - reader.MAXCHUNK = 4 - self.assertRaises(http_app.BadRequest, reader.getline) - - def test_getline_too_large_complete(self): - inp = StringIO.StringIO("x" * 25 + "\r\n") - reader = http_app._FencedReader(inp, 50, 25) - reader.MAXCHUNK = 4 - self.assertRaises(http_app.BadRequest, reader.getline) - - -class TestHTTPMethodDecorator(tests.TestCase): - - def test_args(self): - @http_app.http_method() - def f(self, a, b): - return self, a, b - res = f("self", {"a": "x", "b": "y"}, None) - self.assertEqual(("self", "x", "y"), res) - - def test_args_missing(self): - @http_app.http_method() - def f(self, a, b): - return a, b - self.assertRaises(http_app.BadRequest, f, "self", {"a": "x"}, None) - - def test_args_unexpected(self): - @http_app.http_method() - def f(self, a): - return a - self.assertRaises(http_app.BadRequest, f, "self", - {"a": "x", "c": "z"}, None) - - def test_args_default(self): - @http_app.http_method() - def f(self, a, b="z"): - return a, b - res = f("self", {"a": "x"}, None) - self.assertEqual(("x", "z"), res) - - def test_args_conversion(self): - @http_app.http_method(b=int) - def f(self, a, b): - return self, a, b - res = f("self", {"a": "x", "b": "2"}, None) - self.assertEqual(("self", "x", 2), res) - - self.assertRaises(http_app.BadRequest, f, "self", - {"a": "x", "b": "foo"}, None) - - def test_args_conversion_with_default(self): - @http_app.http_method(b=str) - def f(self, a, b=None): - return self, a, b - res = f("self", {"a": "x"}, None) - self.assertEqual(("self", "x", None), res) - - def test_args_content(self): - @http_app.http_method() - def f(self, a, content): - return a, content - res = f(self, {"a": "x"}, "CONTENT") - self.assertEqual(("x", "CONTENT"), res) - - def test_args_content_as_args(self): - @http_app.http_method(b=int, content_as_args=True) - def f(self, a, b): - return self, a, b - res = f("self", {"a": "x"}, '{"b": "2"}') - self.assertEqual(("self", "x", 2), res) - - self.assertRaises(http_app.BadRequest, f, "self", {}, 'not-json') - - def test_args_content_no_query(self): - @http_app.http_method(no_query=True, - content_as_args=True) - def f(self, a='a', b='b'): - return a, b - res = f("self", {}, '{"b": "y"}') - self.assertEqual(('a', 'y'), res) - - self.assertRaises(http_app.BadRequest, f, "self", {'a': 'x'}, - '{"b": "y"}') - - -class TestResource(object): - - @http_app.http_method() - def get(self, a, b): - self.args = dict(a=a, b=b) - return 'Get' - - @http_app.http_method() - def put(self, a, content): - self.args = dict(a=a) - self.content = content - return 'Put' - - @http_app.http_method(content_as_args=True) - def put_args(self, a, b): - self.args = dict(a=a, b=b) - self.order = ['a'] - self.entries = [] - - @http_app.http_method() - def put_stream_entry(self, content): - self.entries.append(content) - self.order.append('s') - - def put_end(self): - self.order.append('e') - return "Put/end" - - -class parameters: - max_request_size = 200000 - max_entry_size = 100000 - - -class TestHTTPInvocationByMethodWithBody(tests.TestCase): - - def test_get(self): - resource = TestResource() - environ = {'QUERY_STRING': 'a=1&b=2', 'REQUEST_METHOD': 'GET'} - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, - parameters) - res = invoke() - self.assertEqual('Get', res) - self.assertEqual({'a': '1', 'b': '2'}, resource.args) - - def test_put_json(self): - resource = TestResource() - body = '{"body": true}' - environ = {'QUERY_STRING': 'a=1', 'REQUEST_METHOD': 'PUT', - 'wsgi.input': StringIO.StringIO(body), - 'CONTENT_LENGTH': str(len(body)), - 'CONTENT_TYPE': 'application/json'} - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, - parameters) - res = invoke() - self.assertEqual('Put', res) - self.assertEqual({'a': '1'}, resource.args) - self.assertEqual('{"body": true}', resource.content) - - def test_put_sync_stream(self): - resource = TestResource() - body = ( - '[\r\n' - '{"b": 2},\r\n' # args - '{"entry": "x"},\r\n' # stream entry - '{"entry": "y"}\r\n' # stream entry - ']' - ) - environ = {'QUERY_STRING': 'a=1', 'REQUEST_METHOD': 'PUT', - 'wsgi.input': StringIO.StringIO(body), - 'CONTENT_LENGTH': str(len(body)), - 'CONTENT_TYPE': 'application/x-u1db-sync-stream'} - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, - parameters) - res = invoke() - self.assertEqual('Put/end', res) - self.assertEqual({'a': '1', 'b': 2}, resource.args) - self.assertEqual( - ['{"entry": "x"}', '{"entry": "y"}'], resource.entries) - self.assertEqual(['a', 's', 's', 'e'], resource.order) - - def _put_sync_stream(self, body): - resource = TestResource() - environ = {'QUERY_STRING': 'a=1&b=2', 'REQUEST_METHOD': 'PUT', - 'wsgi.input': StringIO.StringIO(body), - 'CONTENT_LENGTH': str(len(body)), - 'CONTENT_TYPE': 'application/x-u1db-sync-stream'} - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, - parameters) - invoke() - - def test_put_sync_stream_wrong_start(self): - self.assertRaises(http_app.BadRequest, - self._put_sync_stream, "{}\r\n]") - - self.assertRaises(http_app.BadRequest, - self._put_sync_stream, "\r\n{}\r\n]") - - self.assertRaises(http_app.BadRequest, - self._put_sync_stream, "") - - def test_put_sync_stream_wrong_end(self): - self.assertRaises(http_app.BadRequest, - self._put_sync_stream, "[\r\n{}") - - self.assertRaises(http_app.BadRequest, - self._put_sync_stream, "[\r\n") - - self.assertRaises(http_app.BadRequest, - self._put_sync_stream, "[\r\n{}\r\n]\r\n...") - - def test_put_sync_stream_missing_comma(self): - self.assertRaises(http_app.BadRequest, - self._put_sync_stream, "[\r\n{}\r\n{}\r\n]") - - def test_put_sync_stream_extra_comma(self): - self.assertRaises(http_app.BadRequest, - self._put_sync_stream, "[\r\n{},\r\n]") - - self.assertRaises(http_app.BadRequest, - self._put_sync_stream, "[\r\n{},\r\n{},\r\n]") - - def test_bad_request_decode_failure(self): - resource = TestResource() - environ = {'QUERY_STRING': 'a=\xff', 'REQUEST_METHOD': 'PUT', - 'wsgi.input': StringIO.StringIO('{}'), - 'CONTENT_LENGTH': '2', - 'CONTENT_TYPE': 'application/json'} - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, - parameters) - self.assertRaises(http_app.BadRequest, invoke) - - def test_bad_request_unsupported_content_type(self): - resource = TestResource() - environ = {'QUERY_STRING': '', 'REQUEST_METHOD': 'PUT', - 'wsgi.input': StringIO.StringIO('{}'), - 'CONTENT_LENGTH': '2', - 'CONTENT_TYPE': 'text/plain'} - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, - parameters) - self.assertRaises(http_app.BadRequest, invoke) - - def test_bad_request_content_length_too_large(self): - resource = TestResource() - environ = {'QUERY_STRING': '', 'REQUEST_METHOD': 'PUT', - 'wsgi.input': StringIO.StringIO('{}'), - 'CONTENT_LENGTH': '10000', - 'CONTENT_TYPE': 'text/plain'} - - resource.max_request_size = 5000 - resource.max_entry_size = sys.maxint # we don't get to use this - - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, - parameters) - self.assertRaises(http_app.BadRequest, invoke) - - def test_bad_request_no_content_length(self): - resource = TestResource() - environ = {'QUERY_STRING': '', 'REQUEST_METHOD': 'PUT', - 'wsgi.input': StringIO.StringIO('a'), - 'CONTENT_TYPE': 'application/json'} - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, - parameters) - self.assertRaises(http_app.BadRequest, invoke) - - def test_bad_request_invalid_content_length(self): - resource = TestResource() - environ = {'QUERY_STRING': '', 'REQUEST_METHOD': 'PUT', - 'wsgi.input': StringIO.StringIO('abc'), - 'CONTENT_LENGTH': '1unk', - 'CONTENT_TYPE': 'application/json'} - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, - parameters) - self.assertRaises(http_app.BadRequest, invoke) - - def test_bad_request_empty_body(self): - resource = TestResource() - environ = {'QUERY_STRING': '', 'REQUEST_METHOD': 'PUT', - 'wsgi.input': StringIO.StringIO(''), - 'CONTENT_LENGTH': '0', - 'CONTENT_TYPE': 'application/json'} - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, - parameters) - self.assertRaises(http_app.BadRequest, invoke) - - def test_bad_request_unsupported_method_get_like(self): - resource = TestResource() - environ = {'QUERY_STRING': '', 'REQUEST_METHOD': 'DELETE'} - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, - parameters) - self.assertRaises(http_app.BadRequest, invoke) - - def test_bad_request_unsupported_method_put_like(self): - resource = TestResource() - environ = {'QUERY_STRING': '', 'REQUEST_METHOD': 'PUT', - 'wsgi.input': StringIO.StringIO('{}'), - 'CONTENT_LENGTH': '2', - 'CONTENT_TYPE': 'application/json'} - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, - parameters) - self.assertRaises(http_app.BadRequest, invoke) - - def test_bad_request_unsupported_method_put_like_multi_json(self): - resource = TestResource() - body = '{}\r\n{}\r\n' - environ = {'QUERY_STRING': '', 'REQUEST_METHOD': 'POST', - 'wsgi.input': StringIO.StringIO(body), - 'CONTENT_LENGTH': str(len(body)), - 'CONTENT_TYPE': 'application/x-u1db-multi-json'} - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, - parameters) - self.assertRaises(http_app.BadRequest, invoke) - - -class TestHTTPResponder(tests.TestCase): - - def start_response(self, status, headers): - self.status = status - self.headers = dict(headers) - self.response_body = [] - - def write(data): - self.response_body.append(data) - - return write - - def test_send_response_content_w_headers(self): - responder = http_app.HTTPResponder(self.start_response) - responder.send_response_content('foo', headers={'x-a': '1'}) - self.assertEqual('200 OK', self.status) - self.assertEqual({'content-type': 'application/json', - 'cache-control': 'no-cache', - 'x-a': '1', 'content-length': '3'}, self.headers) - self.assertEqual([], self.response_body) - self.assertEqual(['foo'], responder.content) - - def test_send_response_json(self): - responder = http_app.HTTPResponder(self.start_response) - responder.send_response_json(value='success') - self.assertEqual('200 OK', self.status) - expected_body = '{"value": "success"}\r\n' - self.assertEqual({'content-type': 'application/json', - 'content-length': str(len(expected_body)), - 'cache-control': 'no-cache'}, self.headers) - self.assertEqual([], self.response_body) - self.assertEqual([expected_body], responder.content) - - def test_send_response_json_status_fail(self): - responder = http_app.HTTPResponder(self.start_response) - responder.send_response_json(400) - self.assertEqual('400 Bad Request', self.status) - expected_body = '{}\r\n' - self.assertEqual({'content-type': 'application/json', - 'content-length': str(len(expected_body)), - 'cache-control': 'no-cache'}, self.headers) - self.assertEqual([], self.response_body) - self.assertEqual([expected_body], responder.content) - - def test_start_finish_response_status_fail(self): - responder = http_app.HTTPResponder(self.start_response) - responder.start_response(404, {'error': 'not found'}) - responder.finish_response() - self.assertEqual('404 Not Found', self.status) - self.assertEqual({'content-type': 'application/json', - 'cache-control': 'no-cache'}, self.headers) - self.assertEqual(['{"error": "not found"}\r\n'], self.response_body) - self.assertEqual([], responder.content) - - def test_send_stream_entry(self): - responder = http_app.HTTPResponder(self.start_response) - responder.content_type = "application/x-u1db-multi-json" - responder.start_response(200) - responder.start_stream() - responder.stream_entry({'entry': 1}) - responder.stream_entry({'entry': 2}) - responder.end_stream() - responder.finish_response() - self.assertEqual('200 OK', self.status) - self.assertEqual({'content-type': 'application/x-u1db-multi-json', - 'cache-control': 'no-cache'}, self.headers) - self.assertEqual(['[', - '\r\n', '{"entry": 1}', - ',\r\n', '{"entry": 2}', - '\r\n]\r\n'], self.response_body) - self.assertEqual([], responder.content) - - def test_send_stream_w_error(self): - responder = http_app.HTTPResponder(self.start_response) - responder.content_type = "application/x-u1db-multi-json" - responder.start_response(200) - responder.start_stream() - responder.stream_entry({'entry': 1}) - responder.send_response_json(503, error="unavailable") - self.assertEqual('200 OK', self.status) - self.assertEqual({'content-type': 'application/x-u1db-multi-json', - 'cache-control': 'no-cache'}, self.headers) - self.assertEqual(['[', - '\r\n', '{"entry": 1}'], self.response_body) - self.assertEqual([',\r\n', '{"error": "unavailable"}\r\n'], - responder.content) - - -class TestHTTPApp(tests.TestCase): - - def setUp(self): - super(TestHTTPApp, self).setUp() - self.state = tests.ServerStateForTests() - self.http_app = http_app.HTTPApp(self.state) - self.app = paste.fixture.TestApp(self.http_app) - self.db0 = self.state._create_database('db0') - - def test_bad_request_broken(self): - resp = self.app.put('/db0/doc/doc1', params='{"x": 1}', - headers={'content-type': 'application/foo'}, - expect_errors=True) - self.assertEqual(400, resp.status) - - def test_bad_request_dispatch(self): - resp = self.app.put('/db0/foo/doc1', params='{"x": 1}', - headers={'content-type': 'application/json'}, - expect_errors=True) - self.assertEqual(400, resp.status) - - def test_version(self): - resp = self.app.get('/') - self.assertEqual(200, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual({"version": _u1db_version}, json.loads(resp.body)) - - def test_create_database(self): - resp = self.app.put('/db1', params='{}', - headers={'content-type': 'application/json'}) - self.assertEqual(200, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual({'ok': True}, json.loads(resp.body)) - - resp = self.app.put('/db1', params='{}', - headers={'content-type': 'application/json'}) - self.assertEqual(200, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual({'ok': True}, json.loads(resp.body)) - - def test_delete_database(self): - resp = self.app.delete('/db0') - self.assertEqual(200, resp.status) - self.assertRaises(errors.DatabaseDoesNotExist, - self.state.check_database, 'db0') - - def test_get_database(self): - resp = self.app.get('/db0') - self.assertEqual(200, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual({}, json.loads(resp.body)) - - def test_valid_database_names(self): - resp = self.app.get('/a-database', expect_errors=True) - self.assertEqual(404, resp.status) - - resp = self.app.get('/db1', expect_errors=True) - self.assertEqual(404, resp.status) - - resp = self.app.get('/0', expect_errors=True) - self.assertEqual(404, resp.status) - - resp = self.app.get('/0-0', expect_errors=True) - self.assertEqual(404, resp.status) - - resp = self.app.get('/org.future', expect_errors=True) - self.assertEqual(404, resp.status) - - def test_invalid_database_names(self): - resp = self.app.get('/.a', expect_errors=True) - self.assertEqual(400, resp.status) - - resp = self.app.get('/-a', expect_errors=True) - self.assertEqual(400, resp.status) - - resp = self.app.get('/_a', expect_errors=True) - self.assertEqual(400, resp.status) - - def test_put_doc_create(self): - resp = self.app.put('/db0/doc/doc1', params='{"x": 1}', - headers={'content-type': 'application/json'}) - doc = self.db0.get_doc('doc1') - self.assertEqual(201, resp.status) # created - self.assertEqual('{"x": 1}', doc.get_json()) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual({'rev': doc.rev}, json.loads(resp.body)) - - def test_put_doc(self): - doc = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc1') - resp = self.app.put('/db0/doc/doc1?old_rev=%s' % doc.rev, - params='{"x": 2}', - headers={'content-type': 'application/json'}) - doc = self.db0.get_doc('doc1') - self.assertEqual(200, resp.status) - self.assertEqual('{"x": 2}', doc.get_json()) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual({'rev': doc.rev}, json.loads(resp.body)) - - def test_put_doc_too_large(self): - self.http_app.max_request_size = 15000 - doc = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc1') - resp = self.app.put('/db0/doc/doc1?old_rev=%s' % doc.rev, - params='{"%s": 2}' % ('z' * 16000), - headers={'content-type': 'application/json'}, - expect_errors=True) - self.assertEqual(400, resp.status) - - def test_delete_doc(self): - doc = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc1') - resp = self.app.delete('/db0/doc/doc1?old_rev=%s' % doc.rev) - doc = self.db0.get_doc('doc1', include_deleted=True) - self.assertEqual(None, doc.content) - self.assertEqual(200, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual({'rev': doc.rev}, json.loads(resp.body)) - - def test_get_doc(self): - doc = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc1') - resp = self.app.get('/db0/doc/%s' % doc.doc_id) - self.assertEqual(200, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual('{"x": 1}', resp.body) - self.assertEqual(doc.rev, resp.header('x-u1db-rev')) - self.assertEqual('false', resp.header('x-u1db-has-conflicts')) - - def test_get_doc_non_existing(self): - resp = self.app.get('/db0/doc/not-there', expect_errors=True) - self.assertEqual(404, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual( - {"error": "document does not exist"}, json.loads(resp.body)) - self.assertEqual('', resp.header('x-u1db-rev')) - self.assertEqual('false', resp.header('x-u1db-has-conflicts')) - - def test_get_doc_deleted(self): - doc = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc1') - self.db0.delete_doc(doc) - resp = self.app.get('/db0/doc/doc1', expect_errors=True) - self.assertEqual(404, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual( - {"error": errors.DocumentDoesNotExist.wire_description}, - json.loads(resp.body)) - - def test_get_doc_deleted_explicit_exclude(self): - doc = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc1') - self.db0.delete_doc(doc) - resp = self.app.get( - '/db0/doc/doc1?include_deleted=false', expect_errors=True) - self.assertEqual(404, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual( - {"error": errors.DocumentDoesNotExist.wire_description}, - json.loads(resp.body)) - - def test_get_deleted_doc(self): - doc = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc1') - self.db0.delete_doc(doc) - resp = self.app.get( - '/db0/doc/doc1?include_deleted=true', expect_errors=True) - self.assertEqual(404, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual( - {"error": errors.DOCUMENT_DELETED}, json.loads(resp.body)) - self.assertEqual(doc.rev, resp.header('x-u1db-rev')) - self.assertEqual('false', resp.header('x-u1db-has-conflicts')) - - def test_get_doc_non_existing_dabase(self): - resp = self.app.get('/not-there/doc/doc1', expect_errors=True) - self.assertEqual(404, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual( - {"error": "database does not exist"}, json.loads(resp.body)) - - def test_get_docs(self): - doc1 = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc1') - doc2 = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc2') - ids = ','.join([doc1.doc_id, doc2.doc_id]) - resp = self.app.get('/db0/docs?doc_ids=%s' % ids) - self.assertEqual(200, resp.status) - self.assertEqual( - 'application/json', resp.header('content-type')) - expected = [ - {"content": '{"x": 1}', "doc_rev": "db0:1", "doc_id": "doc1", - "has_conflicts": False}, - {"content": '{"x": 1}', "doc_rev": "db0:1", "doc_id": "doc2", - "has_conflicts": False}] - self.assertEqual(expected, json.loads(resp.body)) - - def test_get_docs_missing_doc_ids(self): - resp = self.app.get('/db0/docs', expect_errors=True) - self.assertEqual(400, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual( - {"error": "missing document ids"}, json.loads(resp.body)) - - def test_get_docs_empty_doc_ids(self): - resp = self.app.get('/db0/docs?doc_ids=', expect_errors=True) - self.assertEqual(400, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual( - {"error": "missing document ids"}, json.loads(resp.body)) - - def test_get_docs_percent(self): - doc1 = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc%1') - doc2 = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc2') - ids = ','.join([doc1.doc_id, doc2.doc_id]) - resp = self.app.get('/db0/docs?doc_ids=%s' % ids) - self.assertEqual(200, resp.status) - self.assertEqual( - 'application/json', resp.header('content-type')) - expected = [ - {"content": '{"x": 1}', "doc_rev": "db0:1", "doc_id": "doc%1", - "has_conflicts": False}, - {"content": '{"x": 1}', "doc_rev": "db0:1", "doc_id": "doc2", - "has_conflicts": False}] - self.assertEqual(expected, json.loads(resp.body)) - - def test_get_docs_deleted(self): - doc1 = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc1') - doc2 = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc2') - self.db0.delete_doc(doc2) - ids = ','.join([doc1.doc_id, doc2.doc_id]) - resp = self.app.get('/db0/docs?doc_ids=%s' % ids) - self.assertEqual(200, resp.status) - self.assertEqual( - 'application/json', resp.header('content-type')) - expected = [ - {"content": '{"x": 1}', "doc_rev": "db0:1", "doc_id": "doc1", - "has_conflicts": False}] - self.assertEqual(expected, json.loads(resp.body)) - - def test_get_docs_include_deleted(self): - doc1 = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc1') - doc2 = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc2') - self.db0.delete_doc(doc2) - ids = ','.join([doc1.doc_id, doc2.doc_id]) - resp = self.app.get('/db0/docs?doc_ids=%s&include_deleted=true' % ids) - self.assertEqual(200, resp.status) - self.assertEqual( - 'application/json', resp.header('content-type')) - expected = [ - {"content": '{"x": 1}', "doc_rev": "db0:1", "doc_id": "doc1", - "has_conflicts": False}, - {"content": None, "doc_rev": "db0:2", "doc_id": "doc2", - "has_conflicts": False}] - self.assertEqual(expected, json.loads(resp.body)) - - def test_get_sync_info(self): - self.db0._set_replica_gen_and_trans_id('other-id', 1, 'T-transid') - resp = self.app.get('/db0/sync-from/other-id') - self.assertEqual(200, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual(dict(target_replica_uid='db0', - target_replica_generation=0, - target_replica_transaction_id='', - source_replica_uid='other-id', - source_replica_generation=1, - source_transaction_id='T-transid'), - json.loads(resp.body)) - - def test_record_sync_info(self): - resp = self.app.put('/db0/sync-from/other-id', - params='{"generation": 2, "transaction_id": ' - '"T-transid"}', - headers={'content-type': 'application/json'}) - self.assertEqual(200, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual({'ok': True}, json.loads(resp.body)) - self.assertEqual( - (2, 'T-transid'), - self.db0._get_replica_gen_and_trans_id('other-id')) - - def test_sync_exchange_send(self): - entries = { - 10: {'id': 'doc-here', 'rev': 'replica:1', 'content': - '{"value": "here"}', 'gen': 10, 'trans_id': 'T-sid'}, - 11: {'id': 'doc-here2', 'rev': 'replica:1', 'content': - '{"value": "here2"}', 'gen': 11, 'trans_id': 'T-sed'} - } - - gens = [] - _do_set_replica_gen_and_trans_id = \ - self.db0._do_set_replica_gen_and_trans_id - - def set_sync_generation_witness(other_uid, other_gen, other_trans_id): - gens.append((other_uid, other_gen)) - _do_set_replica_gen_and_trans_id( - other_uid, other_gen, other_trans_id) - self.assertGetDoc(self.db0, entries[other_gen]['id'], - entries[other_gen]['rev'], - entries[other_gen]['content'], False) - - self.patch( - self.db0, '_do_set_replica_gen_and_trans_id', - set_sync_generation_witness) - - args = dict(last_known_generation=0) - body = ("[\r\n" + - "%s,\r\n" % json.dumps(args) + - "%s,\r\n" % json.dumps(entries[10]) + - "%s\r\n" % json.dumps(entries[11]) + - "]\r\n") - resp = self.app.post('/db0/sync-from/replica', - params=body, - headers={'content-type': - 'application/x-u1db-sync-stream'}) - self.assertEqual(200, resp.status) - self.assertEqual('application/x-u1db-sync-stream', - resp.header('content-type')) - bits = resp.body.split('\r\n') - self.assertEqual('[', bits[0]) - last_trans_id = self.db0._get_transaction_log()[-1][1] - self.assertEqual({'new_generation': 2, - 'new_transaction_id': last_trans_id}, - json.loads(bits[1])) - self.assertEqual(']', bits[2]) - self.assertEqual('', bits[3]) - self.assertEqual([('replica', 10), ('replica', 11)], gens) - - def test_sync_exchange_send_ensure(self): - entries = { - 10: {'id': 'doc-here', 'rev': 'replica:1', 'content': - '{"value": "here"}', 'gen': 10, 'trans_id': 'T-sid'}, - 11: {'id': 'doc-here2', 'rev': 'replica:1', 'content': - '{"value": "here2"}', 'gen': 11, 'trans_id': 'T-sed'} - } - - args = dict(last_known_generation=0, ensure=True) - body = ("[\r\n" + - "%s,\r\n" % json.dumps(args) + - "%s,\r\n" % json.dumps(entries[10]) + - "%s\r\n" % json.dumps(entries[11]) + - "]\r\n") - resp = self.app.post('/dbnew/sync-from/replica', - params=body, - headers={'content-type': - 'application/x-u1db-sync-stream'}) - self.assertEqual(200, resp.status) - self.assertEqual('application/x-u1db-sync-stream', - resp.header('content-type')) - bits = resp.body.split('\r\n') - self.assertEqual('[', bits[0]) - dbnew = self.state.open_database("dbnew") - last_trans_id = dbnew._get_transaction_log()[-1][1] - self.assertEqual({'new_generation': 2, - 'new_transaction_id': last_trans_id, - 'replica_uid': dbnew._replica_uid}, - json.loads(bits[1])) - self.assertEqual(']', bits[2]) - self.assertEqual('', bits[3]) - - def test_sync_exchange_send_entry_too_large(self): - self.patch(http_app.SyncResource, 'max_request_size', 20000) - self.patch(http_app.SyncResource, 'max_entry_size', 10000) - entries = { - 10: {'id': 'doc-here', 'rev': 'replica:1', 'content': - '{"value": "%s"}' % ('H' * 11000), 'gen': 10}, - } - args = dict(last_known_generation=0) - body = ("[\r\n" + - "%s,\r\n" % json.dumps(args) + - "%s\r\n" % json.dumps(entries[10]) + - "]\r\n") - resp = self.app.post('/db0/sync-from/replica', - params=body, - headers={'content-type': - 'application/x-u1db-sync-stream'}, - expect_errors=True) - self.assertEqual(400, resp.status) - - def test_sync_exchange_receive(self): - doc = self.db0.create_doc_from_json('{"value": "there"}') - doc2 = self.db0.create_doc_from_json('{"value": "there2"}') - args = dict(last_known_generation=0) - body = "[\r\n%s\r\n]" % json.dumps(args) - resp = self.app.post('/db0/sync-from/replica', - params=body, - headers={'content-type': - 'application/x-u1db-sync-stream'}) - self.assertEqual(200, resp.status) - self.assertEqual('application/x-u1db-sync-stream', - resp.header('content-type')) - parts = resp.body.splitlines() - self.assertEqual(5, len(parts)) - self.assertEqual('[', parts[0]) - last_trans_id = self.db0._get_transaction_log()[-1][1] - self.assertEqual({'new_generation': 2, - 'new_transaction_id': last_trans_id}, - json.loads(parts[1].rstrip(","))) - part2 = json.loads(parts[2].rstrip(",")) - self.assertTrue(part2['trans_id'].startswith('T-')) - self.assertEqual('{"value": "there"}', part2['content']) - self.assertEqual(doc.rev, part2['rev']) - self.assertEqual(doc.doc_id, part2['id']) - self.assertEqual(1, part2['gen']) - part3 = json.loads(parts[3].rstrip(",")) - self.assertTrue(part3['trans_id'].startswith('T-')) - self.assertEqual('{"value": "there2"}', part3['content']) - self.assertEqual(doc2.rev, part3['rev']) - self.assertEqual(doc2.doc_id, part3['id']) - self.assertEqual(2, part3['gen']) - self.assertEqual(']', parts[4]) - - def test_sync_exchange_error_in_stream(self): - args = dict(last_known_generation=0) - body = "[\r\n%s\r\n]" % json.dumps(args) - - def boom(self, return_doc_cb): - raise errors.Unavailable - - self.patch(sync.SyncExchange, 'return_docs', - boom) - resp = self.app.post('/db0/sync-from/replica', - params=body, - headers={'content-type': - 'application/x-u1db-sync-stream'}) - self.assertEqual(200, resp.status) - self.assertEqual('application/x-u1db-sync-stream', - resp.header('content-type')) - parts = resp.body.splitlines() - self.assertEqual(3, len(parts)) - self.assertEqual('[', parts[0]) - self.assertEqual({'new_generation': 0, 'new_transaction_id': ''}, - json.loads(parts[1].rstrip(","))) - self.assertEqual({'error': 'unavailable'}, json.loads(parts[2])) - - -class TestRequestHooks(tests.TestCase): - - def setUp(self): - super(TestRequestHooks, self).setUp() - self.state = tests.ServerStateForTests() - self.http_app = http_app.HTTPApp(self.state) - self.app = paste.fixture.TestApp(self.http_app) - self.db0 = self.state._create_database('db0') - - def test_begin_and_done(self): - calls = [] - - def begin(environ): - self.assertTrue('PATH_INFO' in environ) - calls.append('begin') - - def done(environ): - self.assertTrue('PATH_INFO' in environ) - calls.append('done') - - self.http_app.request_begin = begin - self.http_app.request_done = done - - doc = self.db0.create_doc_from_json('{"x": 1}', doc_id='doc1') - self.app.get('/db0/doc/%s' % doc.doc_id) - - self.assertEqual(['begin', 'done'], calls) - - def test_bad_request(self): - calls = [] - - def begin(environ): - self.assertTrue('PATH_INFO' in environ) - calls.append('begin') - - def bad_request(environ): - self.assertTrue('PATH_INFO' in environ) - calls.append('bad-request') - - self.http_app.request_begin = begin - self.http_app.request_bad_request = bad_request - # shouldn't be called - self.http_app.request_done = lambda env: 1 / 0 - - resp = self.app.put('/db0/foo/doc1', params='{"x": 1}', - headers={'content-type': 'application/json'}, - expect_errors=True) - self.assertEqual(400, resp.status) - self.assertEqual(['begin', 'bad-request'], calls) - - -class TestHTTPErrors(tests.TestCase): - - def test_wire_description_to_status(self): - self.assertNotIn("error", http_errors.wire_description_to_status) - - -class TestHTTPAppErrorHandling(tests.TestCase): - - def setUp(self): - super(TestHTTPAppErrorHandling, self).setUp() - self.exc = None - self.state = tests.ServerStateForTests() - - class ErroringResource(object): - - def post(_, args, content): - raise self.exc - - def lookup_resource(environ, responder): - return ErroringResource() - - self.http_app = http_app.HTTPApp(self.state) - self.http_app._lookup_resource = lookup_resource - self.app = paste.fixture.TestApp(self.http_app) - - def test_RevisionConflict_etc(self): - self.exc = errors.RevisionConflict() - resp = self.app.post('/req', params='{}', - headers={'content-type': 'application/json'}, - expect_errors=True) - self.assertEqual(409, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual({"error": "revision conflict"}, - json.loads(resp.body)) - - def test_Unavailable(self): - self.exc = errors.Unavailable - resp = self.app.post('/req', params='{}', - headers={'content-type': 'application/json'}, - expect_errors=True) - self.assertEqual(503, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual({"error": "unavailable"}, - json.loads(resp.body)) - - def test_generic_u1db_errors(self): - self.exc = errors.U1DBError() - resp = self.app.post('/req', params='{}', - headers={'content-type': 'application/json'}, - expect_errors=True) - self.assertEqual(500, resp.status) - self.assertEqual('application/json', resp.header('content-type')) - self.assertEqual({"error": "error"}, - json.loads(resp.body)) - - def test_generic_u1db_errors_hooks(self): - calls = [] - - def begin(environ): - self.assertTrue('PATH_INFO' in environ) - calls.append('begin') - - def u1db_error(environ, exc): - self.assertTrue('PATH_INFO' in environ) - calls.append(('error', exc)) - - self.http_app.request_begin = begin - self.http_app.request_u1db_error = u1db_error - # shouldn't be called - self.http_app.request_done = lambda env: 1 / 0 - - self.exc = errors.U1DBError() - resp = self.app.post('/req', params='{}', - headers={'content-type': 'application/json'}, - expect_errors=True) - self.assertEqual(500, resp.status) - self.assertEqual(['begin', ('error', self.exc)], calls) - - def test_failure(self): - class Failure(Exception): - pass - self.exc = Failure() - self.assertRaises(Failure, self.app.post, '/req', params='{}', - headers={'content-type': 'application/json'}) - - def test_failure_hooks(self): - class Failure(Exception): - pass - calls = [] - - def begin(environ): - calls.append('begin') - - def failed(environ): - self.assertTrue('PATH_INFO' in environ) - calls.append(('failed', sys.exc_info())) - - self.http_app.request_begin = begin - self.http_app.request_failed = failed - # shouldn't be called - self.http_app.request_done = lambda env: 1 / 0 - - self.exc = Failure() - self.assertRaises(Failure, self.app.post, '/req', params='{}', - headers={'content-type': 'application/json'}) - - self.assertEqual(2, len(calls)) - self.assertEqual('begin', calls[0]) - marker, (exc_type, exc, tb) = calls[1] - self.assertEqual('failed', marker) - self.assertEqual(self.exc, exc) - - -class TestPluggableSyncExchange(tests.TestCase): - - def setUp(self): - super(TestPluggableSyncExchange, self).setUp() - self.state = tests.ServerStateForTests() - self.state.ensure_database('foo') - - def test_plugging(self): - - class MySyncExchange(object): - def __init__(self, db, source_replica_uid, last_known_generation): - pass - - class MySyncResource(http_app.SyncResource): - sync_exchange_class = MySyncExchange - - sync_res = MySyncResource('foo', 'src', self.state, None) - sync_res.post_args( - {'last_known_generation': 0, 'last_known_trans_id': None}, '{}') - self.assertIsInstance(sync_res.sync_exch, MySyncExchange) diff --git a/src/leap/soledad/tests/u1db_tests/test_http_client.py b/src/leap/soledad/tests/u1db_tests/test_http_client.py deleted file mode 100644 index 42e98461..00000000 --- a/src/leap/soledad/tests/u1db_tests/test_http_client.py +++ /dev/null @@ -1,363 +0,0 @@ -# Copyright 2011-2012 Canonical Ltd. -# -# This file is part of u1db. -# -# u1db is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 -# as published by the Free Software Foundation. -# -# u1db is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with u1db. If not, see . - -"""Tests for HTTPDatabase""" - -from oauth import oauth -try: - import simplejson as json -except ImportError: - import json # noqa - -from u1db import ( - errors, -) - -from leap.soledad.tests import u1db_tests as tests - -from u1db.remote import ( - http_client, -) - - -class TestEncoder(tests.TestCase): - - def test_encode_string(self): - self.assertEqual("foo", http_client._encode_query_parameter("foo")) - - def test_encode_true(self): - self.assertEqual("true", http_client._encode_query_parameter(True)) - - def test_encode_false(self): - self.assertEqual("false", http_client._encode_query_parameter(False)) - - -class TestHTTPClientBase(tests.TestCaseWithServer): - - def setUp(self): - super(TestHTTPClientBase, self).setUp() - self.errors = 0 - - def app(self, environ, start_response): - if environ['PATH_INFO'].endswith('echo'): - start_response("200 OK", [('Content-Type', 'application/json')]) - ret = {} - for name in ('REQUEST_METHOD', 'PATH_INFO', 'QUERY_STRING'): - ret[name] = environ[name] - if environ['REQUEST_METHOD'] in ('PUT', 'POST'): - ret['CONTENT_TYPE'] = environ['CONTENT_TYPE'] - content_length = int(environ['CONTENT_LENGTH']) - ret['body'] = environ['wsgi.input'].read(content_length) - return [json.dumps(ret)] - elif environ['PATH_INFO'].endswith('error_then_accept'): - if self.errors >= 3: - start_response( - "200 OK", [('Content-Type', 'application/json')]) - ret = {} - for name in ('REQUEST_METHOD', 'PATH_INFO', 'QUERY_STRING'): - ret[name] = environ[name] - if environ['REQUEST_METHOD'] in ('PUT', 'POST'): - ret['CONTENT_TYPE'] = environ['CONTENT_TYPE'] - content_length = int(environ['CONTENT_LENGTH']) - ret['body'] = '{"oki": "doki"}' - return [json.dumps(ret)] - self.errors += 1 - content_length = int(environ['CONTENT_LENGTH']) - error = json.loads( - environ['wsgi.input'].read(content_length)) - response = error['response'] - # In debug mode, wsgiref has an assertion that the status parameter - # is a 'str' object. However error['status'] returns a unicode - # object. - status = str(error['status']) - if isinstance(response, unicode): - response = str(response) - if isinstance(response, str): - start_response(status, [('Content-Type', 'text/plain')]) - return [str(response)] - else: - start_response(status, [('Content-Type', 'application/json')]) - return [json.dumps(response)] - elif environ['PATH_INFO'].endswith('error'): - self.errors += 1 - content_length = int(environ['CONTENT_LENGTH']) - error = json.loads( - environ['wsgi.input'].read(content_length)) - response = error['response'] - # In debug mode, wsgiref has an assertion that the status parameter - # is a 'str' object. However error['status'] returns a unicode - # object. - status = str(error['status']) - if isinstance(response, unicode): - response = str(response) - if isinstance(response, str): - start_response(status, [('Content-Type', 'text/plain')]) - return [str(response)] - else: - start_response(status, [('Content-Type', 'application/json')]) - return [json.dumps(response)] - elif '/oauth' in environ['PATH_INFO']: - base_url = self.getURL('').rstrip('/') - oauth_req = oauth.OAuthRequest.from_request( - http_method=environ['REQUEST_METHOD'], - http_url=base_url + environ['PATH_INFO'], - headers={'Authorization': environ['HTTP_AUTHORIZATION']}, - query_string=environ['QUERY_STRING'] - ) - oauth_server = oauth.OAuthServer(tests.testingOAuthStore) - oauth_server.add_signature_method(tests.sign_meth_HMAC_SHA1) - try: - consumer, token, params = oauth_server.verify_request( - oauth_req) - except oauth.OAuthError, e: - start_response("401 Unauthorized", - [('Content-Type', 'application/json')]) - return [json.dumps({"error": "unauthorized", - "message": e.message})] - start_response("200 OK", [('Content-Type', 'application/json')]) - return [json.dumps([environ['PATH_INFO'], token.key, params])] - - def make_app(self): - return self.app - - def getClient(self, **kwds): - self.startServer() - return http_client.HTTPClientBase(self.getURL('dbase'), **kwds) - - def test_construct(self): - self.startServer() - url = self.getURL() - cli = http_client.HTTPClientBase(url) - self.assertEqual(url, cli._url.geturl()) - self.assertIs(None, cli._conn) - - def test_parse_url(self): - cli = http_client.HTTPClientBase( - '%s://127.0.0.1:12345/' % self.url_scheme) - self.assertEqual(self.url_scheme, cli._url.scheme) - self.assertEqual('127.0.0.1', cli._url.hostname) - self.assertEqual(12345, cli._url.port) - self.assertEqual('/', cli._url.path) - - def test__ensure_connection(self): - cli = self.getClient() - self.assertIs(None, cli._conn) - cli._ensure_connection() - self.assertIsNot(None, cli._conn) - conn = cli._conn - cli._ensure_connection() - self.assertIs(conn, cli._conn) - - def test_close(self): - cli = self.getClient() - cli._ensure_connection() - cli.close() - self.assertIs(None, cli._conn) - - def test__request(self): - cli = self.getClient() - res, headers = cli._request('PUT', ['echo'], {}, {}) - self.assertEqual({'CONTENT_TYPE': 'application/json', - 'PATH_INFO': '/dbase/echo', - 'QUERY_STRING': '', - 'body': '{}', - 'REQUEST_METHOD': 'PUT'}, json.loads(res)) - - res, headers = cli._request('GET', ['doc', 'echo'], {'a': 1}) - self.assertEqual({'PATH_INFO': '/dbase/doc/echo', - 'QUERY_STRING': 'a=1', - 'REQUEST_METHOD': 'GET'}, json.loads(res)) - - res, headers = cli._request('GET', ['doc', '%FFFF', 'echo'], {'a': 1}) - self.assertEqual({'PATH_INFO': '/dbase/doc/%FFFF/echo', - 'QUERY_STRING': 'a=1', - 'REQUEST_METHOD': 'GET'}, json.loads(res)) - - res, headers = cli._request('POST', ['echo'], {'b': 2}, 'Body', - 'application/x-test') - self.assertEqual({'CONTENT_TYPE': 'application/x-test', - 'PATH_INFO': '/dbase/echo', - 'QUERY_STRING': 'b=2', - 'body': 'Body', - 'REQUEST_METHOD': 'POST'}, json.loads(res)) - - def test__request_json(self): - cli = self.getClient() - res, headers = cli._request_json( - 'POST', ['echo'], {'b': 2}, {'a': 'x'}) - self.assertEqual('application/json', headers['content-type']) - self.assertEqual({'CONTENT_TYPE': 'application/json', - 'PATH_INFO': '/dbase/echo', - 'QUERY_STRING': 'b=2', - 'body': '{"a": "x"}', - 'REQUEST_METHOD': 'POST'}, res) - - def test_unspecified_http_error(self): - cli = self.getClient() - self.assertRaises(errors.HTTPError, - cli._request_json, 'POST', ['error'], {}, - {'status': "500 Internal Error", - 'response': "Crash."}) - try: - cli._request_json('POST', ['error'], {}, - {'status': "500 Internal Error", - 'response': "Fail."}) - except errors.HTTPError, e: - pass - - self.assertEqual(500, e.status) - self.assertEqual("Fail.", e.message) - self.assertTrue("content-type" in e.headers) - - def test_revision_conflict(self): - cli = self.getClient() - self.assertRaises(errors.RevisionConflict, - cli._request_json, 'POST', ['error'], {}, - {'status': "409 Conflict", - 'response': {"error": "revision conflict"}}) - - def test_unavailable_proper(self): - cli = self.getClient() - cli._delays = (0, 0, 0, 0, 0) - self.assertRaises(errors.Unavailable, - cli._request_json, 'POST', ['error'], {}, - {'status': "503 Service Unavailable", - 'response': {"error": "unavailable"}}) - self.assertEqual(5, self.errors) - - def test_unavailable_then_available(self): - cli = self.getClient() - cli._delays = (0, 0, 0, 0, 0) - res, headers = cli._request_json( - 'POST', ['error_then_accept'], {'b': 2}, - {'status': "503 Service Unavailable", - 'response': {"error": "unavailable"}}) - self.assertEqual('application/json', headers['content-type']) - self.assertEqual({'CONTENT_TYPE': 'application/json', - 'PATH_INFO': '/dbase/error_then_accept', - 'QUERY_STRING': 'b=2', - 'body': '{"oki": "doki"}', - 'REQUEST_METHOD': 'POST'}, res) - self.assertEqual(3, self.errors) - - def test_unavailable_random_source(self): - cli = self.getClient() - cli._delays = (0, 0, 0, 0, 0) - try: - cli._request_json('POST', ['error'], {}, - {'status': "503 Service Unavailable", - 'response': "random unavailable."}) - except errors.Unavailable, e: - pass - - self.assertEqual(503, e.status) - self.assertEqual("random unavailable.", e.message) - self.assertTrue("content-type" in e.headers) - self.assertEqual(5, self.errors) - - def test_document_too_big(self): - cli = self.getClient() - self.assertRaises(errors.DocumentTooBig, - cli._request_json, 'POST', ['error'], {}, - {'status': "403 Forbidden", - 'response': {"error": "document too big"}}) - - def test_user_quota_exceeded(self): - cli = self.getClient() - self.assertRaises(errors.UserQuotaExceeded, - cli._request_json, 'POST', ['error'], {}, - {'status': "403 Forbidden", - 'response': {"error": "user quota exceeded"}}) - - def test_user_needs_subscription(self): - cli = self.getClient() - self.assertRaises(errors.SubscriptionNeeded, - cli._request_json, 'POST', ['error'], {}, - {'status': "403 Forbidden", - 'response': {"error": "user needs subscription"}}) - - def test_generic_u1db_error(self): - cli = self.getClient() - self.assertRaises(errors.U1DBError, - cli._request_json, 'POST', ['error'], {}, - {'status': "400 Bad Request", - 'response': {"error": "error"}}) - try: - cli._request_json('POST', ['error'], {}, - {'status': "400 Bad Request", - 'response': {"error": "error"}}) - except errors.U1DBError, e: - pass - self.assertIs(e.__class__, errors.U1DBError) - - def test_unspecified_bad_request(self): - cli = self.getClient() - self.assertRaises(errors.HTTPError, - cli._request_json, 'POST', ['error'], {}, - {'status': "400 Bad Request", - 'response': ""}) - try: - cli._request_json('POST', ['error'], {}, - {'status': "400 Bad Request", - 'response': ""}) - except errors.HTTPError, e: - pass - - self.assertEqual(400, e.status) - self.assertEqual("", e.message) - self.assertTrue("content-type" in e.headers) - - def test_oauth(self): - cli = self.getClient() - cli.set_oauth_credentials(tests.consumer1.key, tests.consumer1.secret, - tests.token1.key, tests.token1.secret) - params = {'x': u'\xf0', 'y': "foo"} - res, headers = cli._request('GET', ['doc', 'oauth'], params) - self.assertEqual( - ['/dbase/doc/oauth', tests.token1.key, params], json.loads(res)) - - # oauth does its own internal quoting - params = {'x': u'\xf0', 'y': "foo"} - res, headers = cli._request('GET', ['doc', 'oauth', 'foo bar'], params) - self.assertEqual( - ['/dbase/doc/oauth/foo bar', tests.token1.key, params], - json.loads(res)) - - def test_oauth_ctr_creds(self): - cli = self.getClient(creds={'oauth': { - 'consumer_key': tests.consumer1.key, - 'consumer_secret': tests.consumer1.secret, - 'token_key': tests.token1.key, - 'token_secret': tests.token1.secret, - }}) - params = {'x': u'\xf0', 'y': "foo"} - res, headers = cli._request('GET', ['doc', 'oauth'], params) - self.assertEqual( - ['/dbase/doc/oauth', tests.token1.key, params], json.loads(res)) - - def test_unknown_creds(self): - self.assertRaises(errors.UnknownAuthMethod, - self.getClient, creds={'foo': {}}) - self.assertRaises(errors.UnknownAuthMethod, - self.getClient, creds={}) - - def test_oauth_Unauthorized(self): - cli = self.getClient() - cli.set_oauth_credentials(tests.consumer1.key, tests.consumer1.secret, - tests.token1.key, "WRONG") - params = {'y': 'foo'} - self.assertRaises(errors.Unauthorized, cli._request, 'GET', - ['doc', 'oauth'], params) diff --git a/src/leap/soledad/tests/u1db_tests/test_http_database.py b/src/leap/soledad/tests/u1db_tests/test_http_database.py deleted file mode 100644 index f21e6da1..00000000 --- a/src/leap/soledad/tests/u1db_tests/test_http_database.py +++ /dev/null @@ -1,260 +0,0 @@ -# Copyright 2011 Canonical Ltd. -# -# This file is part of u1db. -# -# u1db is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 -# as published by the Free Software Foundation. -# -# u1db is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with u1db. If not, see . - -"""Tests for HTTPDatabase""" - -import inspect -try: - import simplejson as json -except ImportError: - import json # noqa - -from u1db import ( - errors, - Document, -) - -from leap.soledad.tests import u1db_tests as tests - -from u1db.remote import ( - http_database, - http_target, -) -from leap.soledad.tests.u1db_tests.test_remote_sync_target import ( - make_http_app, -) - - -class TestHTTPDatabaseSimpleOperations(tests.TestCase): - - def setUp(self): - super(TestHTTPDatabaseSimpleOperations, self).setUp() - self.db = http_database.HTTPDatabase('dbase') - self.db._conn = object() # crash if used - self.got = None - self.response_val = None - - def _request(method, url_parts, params=None, body=None, - content_type=None): - self.got = method, url_parts, params, body, content_type - if isinstance(self.response_val, Exception): - raise self.response_val - return self.response_val - - def _request_json(method, url_parts, params=None, body=None, - content_type=None): - self.got = method, url_parts, params, body, content_type - if isinstance(self.response_val, Exception): - raise self.response_val - return self.response_val - - self.db._request = _request - self.db._request_json = _request_json - - def test__sanity_same_signature(self): - my_request_sig = inspect.getargspec(self.db._request) - my_request_sig = (['self'] + my_request_sig[0],) + my_request_sig[1:] - self.assertEqual( - my_request_sig, - inspect.getargspec(http_database.HTTPDatabase._request)) - my_request_json_sig = inspect.getargspec(self.db._request_json) - my_request_json_sig = ((['self'] + my_request_json_sig[0],) + - my_request_json_sig[1:]) - self.assertEqual( - my_request_json_sig, - inspect.getargspec(http_database.HTTPDatabase._request_json)) - - def test__ensure(self): - self.response_val = {'ok': True}, {} - self.db._ensure() - self.assertEqual(('PUT', [], {}, {}, None), self.got) - - def test__delete(self): - self.response_val = {'ok': True}, {} - self.db._delete() - self.assertEqual(('DELETE', [], {}, {}, None), self.got) - - def test__check(self): - self.response_val = {}, {} - res = self.db._check() - self.assertEqual({}, res) - self.assertEqual(('GET', [], None, None, None), self.got) - - def test_put_doc(self): - self.response_val = {'rev': 'doc-rev'}, {} - doc = Document('doc-id', None, '{"v": 1}') - res = self.db.put_doc(doc) - self.assertEqual('doc-rev', res) - self.assertEqual('doc-rev', doc.rev) - self.assertEqual(('PUT', ['doc', 'doc-id'], {}, - '{"v": 1}', 'application/json'), self.got) - - self.response_val = {'rev': 'doc-rev-2'}, {} - doc.content = {"v": 2} - res = self.db.put_doc(doc) - self.assertEqual('doc-rev-2', res) - self.assertEqual('doc-rev-2', doc.rev) - self.assertEqual(('PUT', ['doc', 'doc-id'], {'old_rev': 'doc-rev'}, - '{"v": 2}', 'application/json'), self.got) - - def test_get_doc(self): - self.response_val = '{"v": 2}', {'x-u1db-rev': 'doc-rev', - 'x-u1db-has-conflicts': 'false'} - self.assertGetDoc(self.db, 'doc-id', 'doc-rev', '{"v": 2}', False) - self.assertEqual( - ('GET', ['doc', 'doc-id'], {'include_deleted': False}, None, None), - self.got) - - def test_get_doc_non_existing(self): - self.response_val = errors.DocumentDoesNotExist() - self.assertIs(None, self.db.get_doc('not-there')) - self.assertEqual( - ('GET', ['doc', 'not-there'], {'include_deleted': False}, None, - None), self.got) - - def test_get_doc_deleted(self): - self.response_val = errors.DocumentDoesNotExist() - self.assertIs(None, self.db.get_doc('deleted')) - self.assertEqual( - ('GET', ['doc', 'deleted'], {'include_deleted': False}, None, - None), self.got) - - def test_get_doc_deleted_include_deleted(self): - self.response_val = errors.HTTPError(404, - json.dumps( - {"error": errors.DOCUMENT_DELETED} - ), - {'x-u1db-rev': 'doc-rev-gone', - 'x-u1db-has-conflicts': 'false'}) - doc = self.db.get_doc('deleted', include_deleted=True) - self.assertEqual('deleted', doc.doc_id) - self.assertEqual('doc-rev-gone', doc.rev) - self.assertIs(None, doc.content) - self.assertEqual( - ('GET', ['doc', 'deleted'], {'include_deleted': True}, None, None), - self.got) - - def test_get_doc_pass_through_errors(self): - self.response_val = errors.HTTPError(500, 'Crash.') - self.assertRaises(errors.HTTPError, - self.db.get_doc, 'something-something') - - def test_create_doc_with_id(self): - self.response_val = {'rev': 'doc-rev'}, {} - new_doc = self.db.create_doc_from_json('{"v": 1}', doc_id='doc-id') - self.assertEqual('doc-rev', new_doc.rev) - self.assertEqual('doc-id', new_doc.doc_id) - self.assertEqual('{"v": 1}', new_doc.get_json()) - self.assertEqual(('PUT', ['doc', 'doc-id'], {}, - '{"v": 1}', 'application/json'), self.got) - - def test_create_doc_without_id(self): - self.response_val = {'rev': 'doc-rev-2'}, {} - new_doc = self.db.create_doc_from_json('{"v": 3}') - self.assertEqual('D-', new_doc.doc_id[:2]) - self.assertEqual('doc-rev-2', new_doc.rev) - self.assertEqual('{"v": 3}', new_doc.get_json()) - self.assertEqual(('PUT', ['doc', new_doc.doc_id], {}, - '{"v": 3}', 'application/json'), self.got) - - def test_delete_doc(self): - self.response_val = {'rev': 'doc-rev-gone'}, {} - doc = Document('doc-id', 'doc-rev', None) - self.db.delete_doc(doc) - self.assertEqual('doc-rev-gone', doc.rev) - self.assertEqual(('DELETE', ['doc', 'doc-id'], {'old_rev': 'doc-rev'}, - None, None), self.got) - - def test_get_sync_target(self): - st = self.db.get_sync_target() - self.assertIsInstance(st, http_target.HTTPSyncTarget) - self.assertEqual(st._url, self.db._url) - - def test_get_sync_target_inherits_oauth_credentials(self): - self.db.set_oauth_credentials(tests.consumer1.key, - tests.consumer1.secret, - tests.token1.key, tests.token1.secret) - st = self.db.get_sync_target() - self.assertEqual(self.db._creds, st._creds) - - -class TestHTTPDatabaseCtrWithCreds(tests.TestCase): - - def test_ctr_with_creds(self): - db1 = http_database.HTTPDatabase('http://dbs/db', creds={'oauth': { - 'consumer_key': tests.consumer1.key, - 'consumer_secret': tests.consumer1.secret, - 'token_key': tests.token1.key, - 'token_secret': tests.token1.secret - }}) - self.assertIn('oauth', db1._creds) - - -class TestHTTPDatabaseIntegration(tests.TestCaseWithServer): - - make_app_with_state = staticmethod(make_http_app) - - def setUp(self): - super(TestHTTPDatabaseIntegration, self).setUp() - self.startServer() - - def test_non_existing_db(self): - db = http_database.HTTPDatabase(self.getURL('not-there')) - self.assertRaises(errors.DatabaseDoesNotExist, db.get_doc, 'doc1') - - def test__ensure(self): - db = http_database.HTTPDatabase(self.getURL('new')) - db._ensure() - self.assertIs(None, db.get_doc('doc1')) - - def test__delete(self): - self.request_state._create_database('db0') - db = http_database.HTTPDatabase(self.getURL('db0')) - db._delete() - self.assertRaises(errors.DatabaseDoesNotExist, - self.request_state.check_database, 'db0') - - def test_open_database_existing(self): - self.request_state._create_database('db0') - db = http_database.HTTPDatabase.open_database(self.getURL('db0'), - create=False) - self.assertIs(None, db.get_doc('doc1')) - - def test_open_database_non_existing(self): - self.assertRaises(errors.DatabaseDoesNotExist, - http_database.HTTPDatabase.open_database, - self.getURL('not-there'), - create=False) - - def test_open_database_create(self): - db = http_database.HTTPDatabase.open_database(self.getURL('new'), - create=True) - self.assertIs(None, db.get_doc('doc1')) - - def test_delete_database_existing(self): - self.request_state._create_database('db0') - http_database.HTTPDatabase.delete_database(self.getURL('db0')) - self.assertRaises(errors.DatabaseDoesNotExist, - self.request_state.check_database, 'db0') - - def test_doc_ids_needing_quoting(self): - db0 = self.request_state._create_database('db0') - db = http_database.HTTPDatabase.open_database(self.getURL('db0'), - create=False) - doc = Document('%fff', None, '{}') - db.put_doc(doc) - self.assertGetDoc(db0, '%fff', doc.rev, '{}', False) - self.assertGetDoc(db, '%fff', doc.rev, '{}', False) diff --git a/src/leap/soledad/tests/u1db_tests/test_https.py b/src/leap/soledad/tests/u1db_tests/test_https.py deleted file mode 100644 index 3f8797d8..00000000 --- a/src/leap/soledad/tests/u1db_tests/test_https.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Test support for client-side https support.""" - -import os -import ssl -import sys - -from paste import httpserver - -from leap.soledad.tests import u1db_tests as tests - -from u1db.remote import ( - http_client, - http_target, -) - -from leap.soledad.tests.u1db_tests.test_remote_sync_target import ( - make_oauth_http_app, -) - - -def https_server_def(): - def make_server(host_port, application): - from OpenSSL import SSL - cert_file = os.path.join(os.path.dirname(__file__), 'testing-certs', - 'testing.cert') - key_file = os.path.join(os.path.dirname(__file__), 'testing-certs', - 'testing.key') - ssl_context = SSL.Context(SSL.SSLv23_METHOD) - ssl_context.use_privatekey_file(key_file) - ssl_context.use_certificate_chain_file(cert_file) - srv = httpserver.WSGIServerBase(application, host_port, - httpserver.WSGIHandler, - ssl_context=ssl_context - ) - - def shutdown_request(req): - req.shutdown() - srv.close_request(req) - - srv.shutdown_request = shutdown_request - application.base_url = "https://localhost:%s" % srv.server_address[1] - return srv - return make_server, "shutdown", "https" - - -def oauth_https_sync_target(test, host, path): - _, port = test.server.server_address - st = http_target.HTTPSyncTarget('https://%s:%d/~/%s' % (host, port, path)) - st.set_oauth_credentials(tests.consumer1.key, tests.consumer1.secret, - tests.token1.key, tests.token1.secret) - return st - - -class TestHttpSyncTargetHttpsSupport(tests.TestCaseWithServer): - - scenarios = [ - ('oauth_https', {'server_def': https_server_def, - 'make_app_with_state': make_oauth_http_app, - 'make_document_for_test': - tests.make_document_for_test, - 'sync_target': oauth_https_sync_target - }), - ] - - def setUp(self): - try: - import OpenSSL # noqa - except ImportError: - self.skipTest("Requires pyOpenSSL") - self.cacert_pem = os.path.join(os.path.dirname(__file__), - 'testing-certs', 'cacert.pem') - super(TestHttpSyncTargetHttpsSupport, self).setUp() - - def getSyncTarget(self, host, path=None): - if self.server is None: - self.startServer() - return self.sync_target(self, host, path) - - def test_working(self): - self.startServer() - db = self.request_state._create_database('test') - self.patch(http_client, 'CA_CERTS', self.cacert_pem) - remote_target = self.getSyncTarget('localhost', 'test') - remote_target.record_sync_info('other-id', 2, 'T-id') - self.assertEqual( - (2, 'T-id'), db._get_replica_gen_and_trans_id('other-id')) - - def test_cannot_verify_cert(self): - if not sys.platform.startswith('linux'): - self.skipTest( - "XXX certificate verification happens on linux only for now") - self.startServer() - # don't print expected traceback server-side - self.server.handle_error = lambda req, cli_addr: None - self.request_state._create_database('test') - remote_target = self.getSyncTarget('localhost', 'test') - try: - remote_target.record_sync_info('other-id', 2, 'T-id') - except ssl.SSLError, e: - self.assertIn("certificate verify failed", str(e)) - else: - self.fail("certificate verification should have failed.") - - def test_host_mismatch(self): - if not sys.platform.startswith('linux'): - self.skipTest( - "XXX certificate verification happens on linux only for now") - self.startServer() - self.request_state._create_database('test') - self.patch(http_client, 'CA_CERTS', self.cacert_pem) - remote_target = self.getSyncTarget('127.0.0.1', 'test') - self.assertRaises( - http_client.CertificateError, remote_target.record_sync_info, - 'other-id', 2, 'T-id') - - -load_tests = tests.load_with_scenarios diff --git a/src/leap/soledad/tests/u1db_tests/test_open.py b/src/leap/soledad/tests/u1db_tests/test_open.py deleted file mode 100644 index 0ff307e8..00000000 --- a/src/leap/soledad/tests/u1db_tests/test_open.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2011 Canonical Ltd. -# -# This file is part of u1db. -# -# u1db is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 -# as published by the Free Software Foundation. -# -# u1db is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with u1db. If not, see . - -"""Test u1db.open""" - -import os - -from u1db import ( - errors, - open as u1db_open, -) -from leap.soledad.tests import u1db_tests as tests -from u1db.backends import sqlite_backend -from leap.soledad.tests.u1db_tests.test_backends import TestAlternativeDocument - - -class TestU1DBOpen(tests.TestCase): - - def setUp(self): - super(TestU1DBOpen, self).setUp() - tmpdir = self.createTempDir() - self.db_path = tmpdir + '/test.db' - - def test_open_no_create(self): - self.assertRaises(errors.DatabaseDoesNotExist, - u1db_open, self.db_path, create=False) - self.assertFalse(os.path.exists(self.db_path)) - - def test_open_create(self): - db = u1db_open(self.db_path, create=True) - self.addCleanup(db.close) - self.assertTrue(os.path.exists(self.db_path)) - self.assertIsInstance(db, sqlite_backend.SQLiteDatabase) - - def test_open_with_factory(self): - db = u1db_open(self.db_path, create=True, - document_factory=TestAlternativeDocument) - self.addCleanup(db.close) - self.assertEqual(TestAlternativeDocument, db._factory) - - def test_open_existing(self): - db = sqlite_backend.SQLitePartialExpandDatabase(self.db_path) - self.addCleanup(db.close) - doc = db.create_doc_from_json(tests.simple_doc) - # Even though create=True, we shouldn't wipe the db - db2 = u1db_open(self.db_path, create=True) - self.addCleanup(db2.close) - doc2 = db2.get_doc(doc.doc_id) - self.assertEqual(doc, doc2) - - def test_open_existing_no_create(self): - db = sqlite_backend.SQLitePartialExpandDatabase(self.db_path) - self.addCleanup(db.close) - db2 = u1db_open(self.db_path, create=False) - self.addCleanup(db2.close) - self.assertIsInstance(db2, sqlite_backend.SQLitePartialExpandDatabase) diff --git a/src/leap/soledad/tests/u1db_tests/test_remote_sync_target.py b/src/leap/soledad/tests/u1db_tests/test_remote_sync_target.py deleted file mode 100644 index 66d404d2..00000000 --- a/src/leap/soledad/tests/u1db_tests/test_remote_sync_target.py +++ /dev/null @@ -1,317 +0,0 @@ -# Copyright 2011-2012 Canonical Ltd. -# -# This file is part of u1db. -# -# u1db is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 -# as published by the Free Software Foundation. -# -# u1db is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with u1db. If not, see . - -"""Tests for the remote sync targets""" - -import cStringIO - -from u1db import ( - errors, -) - -from leap.soledad.tests import u1db_tests as tests - -from u1db.remote import ( - http_app, - http_target, - oauth_middleware, -) - - -class TestHTTPSyncTargetBasics(tests.TestCase): - - def test_parse_url(self): - remote_target = http_target.HTTPSyncTarget('http://127.0.0.1:12345/') - self.assertEqual('http', remote_target._url.scheme) - self.assertEqual('127.0.0.1', remote_target._url.hostname) - self.assertEqual(12345, remote_target._url.port) - self.assertEqual('/', remote_target._url.path) - - -class TestParsingSyncStream(tests.TestCase): - - def test_wrong_start(self): - tgt = http_target.HTTPSyncTarget("http://foo/foo") - - self.assertRaises(errors.BrokenSyncStream, - tgt._parse_sync_stream, "{}\r\n]", None) - - self.assertRaises(errors.BrokenSyncStream, - tgt._parse_sync_stream, "\r\n{}\r\n]", None) - - self.assertRaises(errors.BrokenSyncStream, - tgt._parse_sync_stream, "", None) - - def test_wrong_end(self): - tgt = http_target.HTTPSyncTarget("http://foo/foo") - - self.assertRaises(errors.BrokenSyncStream, - tgt._parse_sync_stream, "[\r\n{}", None) - - self.assertRaises(errors.BrokenSyncStream, - tgt._parse_sync_stream, "[\r\n", None) - - def test_missing_comma(self): - tgt = http_target.HTTPSyncTarget("http://foo/foo") - - self.assertRaises(errors.BrokenSyncStream, - tgt._parse_sync_stream, - '[\r\n{}\r\n{"id": "i", "rev": "r", ' - '"content": "c", "gen": 3}\r\n]', None) - - def test_no_entries(self): - tgt = http_target.HTTPSyncTarget("http://foo/foo") - - self.assertRaises(errors.BrokenSyncStream, - tgt._parse_sync_stream, "[\r\n]", None) - - def test_extra_comma(self): - tgt = http_target.HTTPSyncTarget("http://foo/foo") - - self.assertRaises(errors.BrokenSyncStream, - tgt._parse_sync_stream, "[\r\n{},\r\n]", None) - - self.assertRaises(errors.BrokenSyncStream, - tgt._parse_sync_stream, - '[\r\n{},\r\n{"id": "i", "rev": "r", ' - '"content": "{}", "gen": 3, "trans_id": "T-sid"}' - ',\r\n]', - lambda doc, gen, trans_id: None) - - def test_error_in_stream(self): - tgt = http_target.HTTPSyncTarget("http://foo/foo") - - self.assertRaises(errors.Unavailable, - tgt._parse_sync_stream, - '[\r\n{"new_generation": 0},' - '\r\n{"error": "unavailable"}\r\n', None) - - self.assertRaises(errors.Unavailable, - tgt._parse_sync_stream, - '[\r\n{"error": "unavailable"}\r\n', None) - - self.assertRaises(errors.BrokenSyncStream, - tgt._parse_sync_stream, - '[\r\n{"error": "?"}\r\n', None) - - -def make_http_app(state): - return http_app.HTTPApp(state) - - -def http_sync_target(test, path): - return http_target.HTTPSyncTarget(test.getURL(path)) - - -def make_oauth_http_app(state): - app = http_app.HTTPApp(state) - application = oauth_middleware.OAuthMiddleware(app, None, prefix='/~/') - application.get_oauth_data_store = lambda: tests.testingOAuthStore - return application - - -def oauth_http_sync_target(test, path): - st = http_sync_target(test, '~/' + path) - st.set_oauth_credentials(tests.consumer1.key, tests.consumer1.secret, - tests.token1.key, tests.token1.secret) - return st - - -class TestRemoteSyncTargets(tests.TestCaseWithServer): - - scenarios = [ - ('http', {'make_app_with_state': make_http_app, - 'make_document_for_test': tests.make_document_for_test, - 'sync_target': http_sync_target}), - ('oauth_http', {'make_app_with_state': make_oauth_http_app, - 'make_document_for_test': tests.make_document_for_test, - 'sync_target': oauth_http_sync_target}), - ] - - def getSyncTarget(self, path=None): - if self.server is None: - self.startServer() - return self.sync_target(self, path) - - def test_get_sync_info(self): - self.startServer() - db = self.request_state._create_database('test') - db._set_replica_gen_and_trans_id('other-id', 1, 'T-transid') - remote_target = self.getSyncTarget('test') - self.assertEqual(('test', 0, '', 1, 'T-transid'), - remote_target.get_sync_info('other-id')) - - def test_record_sync_info(self): - self.startServer() - db = self.request_state._create_database('test') - remote_target = self.getSyncTarget('test') - remote_target.record_sync_info('other-id', 2, 'T-transid') - self.assertEqual( - (2, 'T-transid'), db._get_replica_gen_and_trans_id('other-id')) - - def test_sync_exchange_send(self): - self.startServer() - db = self.request_state._create_database('test') - remote_target = self.getSyncTarget('test') - other_docs = [] - - def receive_doc(doc): - other_docs.append((doc.doc_id, doc.rev, doc.get_json())) - - doc = self.make_document('doc-here', 'replica:1', '{"value": "here"}') - new_gen, trans_id = remote_target.sync_exchange( - [(doc, 10, 'T-sid')], 'replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=receive_doc) - self.assertEqual(1, new_gen) - self.assertGetDoc( - db, 'doc-here', 'replica:1', '{"value": "here"}', False) - - def test_sync_exchange_send_failure_and_retry_scenario(self): - self.startServer() - - def blackhole_getstderr(inst): - return cStringIO.StringIO() - - self.patch(self.server.RequestHandlerClass, 'get_stderr', - blackhole_getstderr) - db = self.request_state._create_database('test') - _put_doc_if_newer = db._put_doc_if_newer - trigger_ids = ['doc-here2'] - - def bomb_put_doc_if_newer(doc, save_conflict, - replica_uid=None, replica_gen=None, - replica_trans_id=None): - if doc.doc_id in trigger_ids: - raise Exception - return _put_doc_if_newer(doc, save_conflict=save_conflict, - replica_uid=replica_uid, - replica_gen=replica_gen, - replica_trans_id=replica_trans_id) - self.patch(db, '_put_doc_if_newer', bomb_put_doc_if_newer) - remote_target = self.getSyncTarget('test') - other_changes = [] - - def receive_doc(doc, gen, trans_id): - other_changes.append( - (doc.doc_id, doc.rev, doc.get_json(), gen, trans_id)) - - doc1 = self.make_document('doc-here', 'replica:1', '{"value": "here"}') - doc2 = self.make_document('doc-here2', 'replica:1', - '{"value": "here2"}') - self.assertRaises( - errors.HTTPError, - remote_target.sync_exchange, - [(doc1, 10, 'T-sid'), (doc2, 11, 'T-sud')], - 'replica', last_known_generation=0, last_known_trans_id=None, - return_doc_cb=receive_doc) - self.assertGetDoc(db, 'doc-here', 'replica:1', '{"value": "here"}', - False) - self.assertEqual( - (10, 'T-sid'), db._get_replica_gen_and_trans_id('replica')) - self.assertEqual([], other_changes) - # retry - trigger_ids = [] - new_gen, trans_id = remote_target.sync_exchange( - [(doc2, 11, 'T-sud')], 'replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=receive_doc) - self.assertGetDoc(db, 'doc-here2', 'replica:1', '{"value": "here2"}', - False) - self.assertEqual( - (11, 'T-sud'), db._get_replica_gen_and_trans_id('replica')) - self.assertEqual(2, new_gen) - # bounced back to us - self.assertEqual( - ('doc-here', 'replica:1', '{"value": "here"}', 1), - other_changes[0][:-1]) - - def test_sync_exchange_in_stream_error(self): - self.startServer() - - def blackhole_getstderr(inst): - return cStringIO.StringIO() - - self.patch(self.server.RequestHandlerClass, 'get_stderr', - blackhole_getstderr) - db = self.request_state._create_database('test') - doc = db.create_doc_from_json('{"value": "there"}') - - def bomb_get_docs(doc_ids, check_for_conflicts=None, - include_deleted=False): - yield doc - # delayed failure case - raise errors.Unavailable - - self.patch(db, 'get_docs', bomb_get_docs) - remote_target = self.getSyncTarget('test') - other_changes = [] - - def receive_doc(doc, gen, trans_id): - other_changes.append( - (doc.doc_id, doc.rev, doc.get_json(), gen, trans_id)) - - self.assertRaises( - errors.Unavailable, remote_target.sync_exchange, [], 'replica', - last_known_generation=0, last_known_trans_id=None, - return_doc_cb=receive_doc) - self.assertEqual( - (doc.doc_id, doc.rev, '{"value": "there"}', 1), - other_changes[0][:-1]) - - def test_sync_exchange_receive(self): - self.startServer() - db = self.request_state._create_database('test') - doc = db.create_doc_from_json('{"value": "there"}') - remote_target = self.getSyncTarget('test') - other_changes = [] - - def receive_doc(doc, gen, trans_id): - other_changes.append( - (doc.doc_id, doc.rev, doc.get_json(), gen, trans_id)) - - new_gen, trans_id = remote_target.sync_exchange( - [], 'replica', last_known_generation=0, last_known_trans_id=None, - return_doc_cb=receive_doc) - self.assertEqual(1, new_gen) - self.assertEqual( - (doc.doc_id, doc.rev, '{"value": "there"}', 1), - other_changes[0][:-1]) - - def test_sync_exchange_send_ensure_callback(self): - self.startServer() - remote_target = self.getSyncTarget('test') - other_docs = [] - replica_uid_box = [] - - def receive_doc(doc): - other_docs.append((doc.doc_id, doc.rev, doc.get_json())) - - def ensure_cb(replica_uid): - replica_uid_box.append(replica_uid) - - doc = self.make_document('doc-here', 'replica:1', '{"value": "here"}') - new_gen, trans_id = remote_target.sync_exchange( - [(doc, 10, 'T-sid')], 'replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=receive_doc, - ensure_callback=ensure_cb) - self.assertEqual(1, new_gen) - db = self.request_state.open_database('test') - self.assertEqual(1, len(replica_uid_box)) - self.assertEqual(db._replica_uid, replica_uid_box[0]) - self.assertGetDoc( - db, 'doc-here', 'replica:1', '{"value": "here"}', False) - - -load_tests = tests.load_with_scenarios diff --git a/src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py b/src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py deleted file mode 100644 index 1380e4b1..00000000 --- a/src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py +++ /dev/null @@ -1,494 +0,0 @@ -# Copyright 2011 Canonical Ltd. -# -# This file is part of u1db. -# -# u1db is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 -# as published by the Free Software Foundation. -# -# u1db is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with u1db. If not, see . - -"""Test sqlite backend internals.""" - -import os -import time -import threading - -from pysqlcipher import dbapi2 - -from u1db import ( - errors, - query_parser, -) - -from leap.soledad.tests import u1db_tests as tests - -from u1db.backends import sqlite_backend -from leap.soledad.tests.u1db_tests.test_backends import TestAlternativeDocument - - -simple_doc = '{"key": "value"}' -nested_doc = '{"key": "value", "sub": {"doc": "underneath"}}' - - -class TestSQLiteDatabase(tests.TestCase): - - def test_atomic_initialize(self): - tmpdir = self.createTempDir() - dbname = os.path.join(tmpdir, 'atomic.db') - - t2 = None # will be a thread - - class SQLiteDatabaseTesting(sqlite_backend.SQLiteDatabase): - _index_storage_value = "testing" - - def __init__(self, dbname, ntry): - self._try = ntry - self._is_initialized_invocations = 0 - super(SQLiteDatabaseTesting, self).__init__(dbname) - - def _is_initialized(self, c): - res = super(SQLiteDatabaseTesting, self)._is_initialized(c) - if self._try == 1: - self._is_initialized_invocations += 1 - if self._is_initialized_invocations == 2: - t2.start() - # hard to do better and have a generic test - time.sleep(0.05) - return res - - outcome2 = [] - - def second_try(): - try: - db2 = SQLiteDatabaseTesting(dbname, 2) - except Exception, e: - outcome2.append(e) - else: - outcome2.append(db2) - - t2 = threading.Thread(target=second_try) - db1 = SQLiteDatabaseTesting(dbname, 1) - t2.join() - - self.assertIsInstance(outcome2[0], SQLiteDatabaseTesting) - db2 = outcome2[0] - self.assertTrue(db2._is_initialized(db1._get_sqlite_handle().cursor())) - - -class TestSQLitePartialExpandDatabase(tests.TestCase): - - def setUp(self): - super(TestSQLitePartialExpandDatabase, self).setUp() - self.db = sqlite_backend.SQLitePartialExpandDatabase(':memory:') - self.db._set_replica_uid('test') - - def test_create_database(self): - raw_db = self.db._get_sqlite_handle() - self.assertNotEqual(None, raw_db) - - def test_default_replica_uid(self): - self.db = sqlite_backend.SQLitePartialExpandDatabase(':memory:') - self.assertIsNot(None, self.db._replica_uid) - self.assertEqual(32, len(self.db._replica_uid)) - int(self.db._replica_uid, 16) - - def test__close_sqlite_handle(self): - raw_db = self.db._get_sqlite_handle() - self.db._close_sqlite_handle() - self.assertRaises(dbapi2.ProgrammingError, - raw_db.cursor) - - def test_create_database_initializes_schema(self): - raw_db = self.db._get_sqlite_handle() - c = raw_db.cursor() - c.execute("SELECT * FROM u1db_config") - config = dict([(r[0], r[1]) for r in c.fetchall()]) - self.assertEqual({'sql_schema': '0', 'replica_uid': 'test', - 'index_storage': 'expand referenced'}, config) - - # These tables must exist, though we don't care what is in them yet - c.execute("SELECT * FROM transaction_log") - c.execute("SELECT * FROM document") - c.execute("SELECT * FROM document_fields") - c.execute("SELECT * FROM sync_log") - c.execute("SELECT * FROM conflicts") - c.execute("SELECT * FROM index_definitions") - - def test__parse_index(self): - self.db = sqlite_backend.SQLitePartialExpandDatabase(':memory:') - g = self.db._parse_index_definition('fieldname') - self.assertIsInstance(g, query_parser.ExtractField) - self.assertEqual(['fieldname'], g.field) - - def test__update_indexes(self): - self.db = sqlite_backend.SQLitePartialExpandDatabase(':memory:') - g = self.db._parse_index_definition('fieldname') - c = self.db._get_sqlite_handle().cursor() - self.db._update_indexes('doc-id', {'fieldname': 'val'}, - [('fieldname', g)], c) - c.execute('SELECT doc_id, field_name, value FROM document_fields') - self.assertEqual([('doc-id', 'fieldname', 'val')], - c.fetchall()) - - def test__set_replica_uid(self): - # Start from scratch, so that replica_uid isn't set. - self.db = sqlite_backend.SQLitePartialExpandDatabase(':memory:') - self.assertIsNot(None, self.db._real_replica_uid) - self.assertIsNot(None, self.db._replica_uid) - self.db._set_replica_uid('foo') - c = self.db._get_sqlite_handle().cursor() - c.execute("SELECT value FROM u1db_config WHERE name='replica_uid'") - self.assertEqual(('foo',), c.fetchone()) - self.assertEqual('foo', self.db._real_replica_uid) - self.assertEqual('foo', self.db._replica_uid) - self.db._close_sqlite_handle() - self.assertEqual('foo', self.db._replica_uid) - - def test__get_generation(self): - self.assertEqual(0, self.db._get_generation()) - - def test__get_generation_info(self): - self.assertEqual((0, ''), self.db._get_generation_info()) - - def test_create_index(self): - self.db.create_index('test-idx', "key") - self.assertEqual([('test-idx', ["key"])], self.db.list_indexes()) - - def test_create_index_multiple_fields(self): - self.db.create_index('test-idx', "key", "key2") - self.assertEqual([('test-idx', ["key", "key2"])], - self.db.list_indexes()) - - def test__get_index_definition(self): - self.db.create_index('test-idx', "key", "key2") - # TODO: How would you test that an index is getting used for an SQL - # request? - self.assertEqual(["key", "key2"], - self.db._get_index_definition('test-idx')) - - def test_list_index_mixed(self): - # Make sure that we properly order the output - c = self.db._get_sqlite_handle().cursor() - # We intentionally insert the data in weird ordering, to make sure the - # query still gets it back correctly. - c.executemany("INSERT INTO index_definitions VALUES (?, ?, ?)", - [('idx-1', 0, 'key10'), - ('idx-2', 2, 'key22'), - ('idx-1', 1, 'key11'), - ('idx-2', 0, 'key20'), - ('idx-2', 1, 'key21')]) - self.assertEqual([('idx-1', ['key10', 'key11']), - ('idx-2', ['key20', 'key21', 'key22'])], - self.db.list_indexes()) - - def test_no_indexes_no_document_fields(self): - self.db.create_doc_from_json( - '{"key1": "val1", "key2": "val2"}') - c = self.db._get_sqlite_handle().cursor() - c.execute("SELECT doc_id, field_name, value FROM document_fields" - " ORDER BY doc_id, field_name, value") - self.assertEqual([], c.fetchall()) - - def test_create_extracts_fields(self): - doc1 = self.db.create_doc_from_json('{"key1": "val1", "key2": "val2"}') - doc2 = self.db.create_doc_from_json('{"key1": "valx", "key2": "valy"}') - c = self.db._get_sqlite_handle().cursor() - c.execute("SELECT doc_id, field_name, value FROM document_fields" - " ORDER BY doc_id, field_name, value") - self.assertEqual([], c.fetchall()) - self.db.create_index('test', 'key1', 'key2') - c.execute("SELECT doc_id, field_name, value FROM document_fields" - " ORDER BY doc_id, field_name, value") - self.assertEqual(sorted( - [(doc1.doc_id, "key1", "val1"), - (doc1.doc_id, "key2", "val2"), - (doc2.doc_id, "key1", "valx"), - (doc2.doc_id, "key2", "valy"), ]), sorted(c.fetchall())) - - def test_put_updates_fields(self): - self.db.create_index('test', 'key1', 'key2') - doc1 = self.db.create_doc_from_json( - '{"key1": "val1", "key2": "val2"}') - doc1.content = {"key1": "val1", "key2": "valy"} - self.db.put_doc(doc1) - c = self.db._get_sqlite_handle().cursor() - c.execute("SELECT doc_id, field_name, value FROM document_fields" - " ORDER BY doc_id, field_name, value") - self.assertEqual([(doc1.doc_id, "key1", "val1"), - (doc1.doc_id, "key2", "valy"), ], c.fetchall()) - - def test_put_updates_nested_fields(self): - self.db.create_index('test', 'key', 'sub.doc') - doc1 = self.db.create_doc_from_json(nested_doc) - c = self.db._get_sqlite_handle().cursor() - c.execute("SELECT doc_id, field_name, value FROM document_fields" - " ORDER BY doc_id, field_name, value") - self.assertEqual([(doc1.doc_id, "key", "value"), - (doc1.doc_id, "sub.doc", "underneath"), ], - c.fetchall()) - - def test__ensure_schema_rollback(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/rollback.db' - - class SQLitePartialExpandDbTesting( - sqlite_backend.SQLitePartialExpandDatabase): - - def _set_replica_uid_in_transaction(self, uid): - super(SQLitePartialExpandDbTesting, - self)._set_replica_uid_in_transaction(uid) - if fail: - raise Exception() - - db = SQLitePartialExpandDbTesting.__new__(SQLitePartialExpandDbTesting) - db._db_handle = dbapi2.connect(path) # db is there but not yet init-ed - fail = True - self.assertRaises(Exception, db._ensure_schema) - fail = False - db._initialize(db._db_handle.cursor()) - - def test__open_database(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/test.sqlite' - sqlite_backend.SQLitePartialExpandDatabase(path) - db2 = sqlite_backend.SQLiteDatabase._open_database(path) - self.assertIsInstance(db2, sqlite_backend.SQLitePartialExpandDatabase) - - def test__open_database_with_factory(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/test.sqlite' - sqlite_backend.SQLitePartialExpandDatabase(path) - db2 = sqlite_backend.SQLiteDatabase._open_database( - path, document_factory=TestAlternativeDocument) - self.assertEqual(TestAlternativeDocument, db2._factory) - - def test__open_database_non_existent(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/non-existent.sqlite' - self.assertRaises(errors.DatabaseDoesNotExist, - sqlite_backend.SQLiteDatabase._open_database, path) - - def test__open_database_during_init(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/initialised.db' - db = sqlite_backend.SQLitePartialExpandDatabase.__new__( - sqlite_backend.SQLitePartialExpandDatabase) - db._db_handle = dbapi2.connect(path) # db is there but not yet init-ed - self.addCleanup(db.close) - observed = [] - - class SQLiteDatabaseTesting(sqlite_backend.SQLiteDatabase): - WAIT_FOR_PARALLEL_INIT_HALF_INTERVAL = 0.1 - - @classmethod - def _which_index_storage(cls, c): - res = super(SQLiteDatabaseTesting, cls)._which_index_storage(c) - db._ensure_schema() # init db - observed.append(res[0]) - return res - - db2 = SQLiteDatabaseTesting._open_database(path) - self.addCleanup(db2.close) - self.assertIsInstance(db2, sqlite_backend.SQLitePartialExpandDatabase) - self.assertEqual( - [None, - sqlite_backend.SQLitePartialExpandDatabase._index_storage_value], - observed) - - def test__open_database_invalid(self): - class SQLiteDatabaseTesting(sqlite_backend.SQLiteDatabase): - WAIT_FOR_PARALLEL_INIT_HALF_INTERVAL = 0.1 - temp_dir = self.createTempDir(prefix='u1db-test-') - path1 = temp_dir + '/invalid1.db' - with open(path1, 'wb') as f: - f.write("") - self.assertRaises(dbapi2.OperationalError, - SQLiteDatabaseTesting._open_database, path1) - with open(path1, 'wb') as f: - f.write("invalid") - self.assertRaises(dbapi2.DatabaseError, - SQLiteDatabaseTesting._open_database, path1) - - def test_open_database_existing(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/existing.sqlite' - sqlite_backend.SQLitePartialExpandDatabase(path) - db2 = sqlite_backend.SQLiteDatabase.open_database(path, create=False) - self.assertIsInstance(db2, sqlite_backend.SQLitePartialExpandDatabase) - - def test_open_database_with_factory(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/existing.sqlite' - sqlite_backend.SQLitePartialExpandDatabase(path) - db2 = sqlite_backend.SQLiteDatabase.open_database( - path, create=False, document_factory=TestAlternativeDocument) - self.assertEqual(TestAlternativeDocument, db2._factory) - - def test_open_database_create(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/new.sqlite' - sqlite_backend.SQLiteDatabase.open_database(path, create=True) - db2 = sqlite_backend.SQLiteDatabase.open_database(path, create=False) - self.assertIsInstance(db2, sqlite_backend.SQLitePartialExpandDatabase) - - def test_open_database_non_existent(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/non-existent.sqlite' - self.assertRaises(errors.DatabaseDoesNotExist, - sqlite_backend.SQLiteDatabase.open_database, path, - create=False) - - def test_delete_database_existent(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/new.sqlite' - db = sqlite_backend.SQLiteDatabase.open_database(path, create=True) - db.close() - sqlite_backend.SQLiteDatabase.delete_database(path) - self.assertRaises(errors.DatabaseDoesNotExist, - sqlite_backend.SQLiteDatabase.open_database, path, - create=False) - - def test_delete_database_nonexistent(self): - temp_dir = self.createTempDir(prefix='u1db-test-') - path = temp_dir + '/non-existent.sqlite' - self.assertRaises(errors.DatabaseDoesNotExist, - sqlite_backend.SQLiteDatabase.delete_database, path) - - def test__get_indexed_fields(self): - self.db.create_index('idx1', 'a', 'b') - self.assertEqual(set(['a', 'b']), self.db._get_indexed_fields()) - self.db.create_index('idx2', 'b', 'c') - self.assertEqual(set(['a', 'b', 'c']), self.db._get_indexed_fields()) - - def test_indexed_fields_expanded(self): - self.db.create_index('idx1', 'key1') - doc1 = self.db.create_doc_from_json('{"key1": "val1", "key2": "val2"}') - self.assertEqual(set(['key1']), self.db._get_indexed_fields()) - c = self.db._get_sqlite_handle().cursor() - c.execute("SELECT doc_id, field_name, value FROM document_fields" - " ORDER BY doc_id, field_name, value") - self.assertEqual([(doc1.doc_id, 'key1', 'val1')], c.fetchall()) - - def test_create_index_updates_fields(self): - doc1 = self.db.create_doc_from_json('{"key1": "val1", "key2": "val2"}') - self.db.create_index('idx1', 'key1') - self.assertEqual(set(['key1']), self.db._get_indexed_fields()) - c = self.db._get_sqlite_handle().cursor() - c.execute("SELECT doc_id, field_name, value FROM document_fields" - " ORDER BY doc_id, field_name, value") - self.assertEqual([(doc1.doc_id, 'key1', 'val1')], c.fetchall()) - - def assertFormatQueryEquals(self, exp_statement, exp_args, definition, - values): - statement, args = self.db._format_query(definition, values) - self.assertEqual(exp_statement, statement) - self.assertEqual(exp_args, args) - - def test__format_query(self): - self.assertFormatQueryEquals( - "SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM " - "document d, document_fields d0 LEFT OUTER JOIN conflicts c ON " - "c.doc_id = d.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name " - "= ? AND d0.value = ? GROUP BY d.doc_id, d.doc_rev, d.content " - "ORDER BY d0.value;", ["key1", "a"], - ["key1"], ["a"]) - - def test__format_query2(self): - self.assertFormatQueryEquals( - 'SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM ' - 'document d, document_fields d0, document_fields d1, ' - 'document_fields d2 LEFT OUTER JOIN conflicts c ON c.doc_id = ' - 'd.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name = ? AND ' - 'd0.value = ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND ' - 'd1.value = ? AND d.doc_id = d2.doc_id AND d2.field_name = ? AND ' - 'd2.value = ? GROUP BY d.doc_id, d.doc_rev, d.content ORDER BY ' - 'd0.value, d1.value, d2.value;', - ["key1", "a", "key2", "b", "key3", "c"], - ["key1", "key2", "key3"], ["a", "b", "c"]) - - def test__format_query_wildcard(self): - self.assertFormatQueryEquals( - 'SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM ' - 'document d, document_fields d0, document_fields d1, ' - 'document_fields d2 LEFT OUTER JOIN conflicts c ON c.doc_id = ' - 'd.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name = ? AND ' - 'd0.value = ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND ' - 'd1.value GLOB ? AND d.doc_id = d2.doc_id AND d2.field_name = ? ' - 'AND d2.value NOT NULL GROUP BY d.doc_id, d.doc_rev, d.content ' - 'ORDER BY d0.value, d1.value, d2.value;', - ["key1", "a", "key2", "b*", "key3"], ["key1", "key2", "key3"], - ["a", "b*", "*"]) - - def assertFormatRangeQueryEquals(self, exp_statement, exp_args, definition, - start_value, end_value): - statement, args = self.db._format_range_query( - definition, start_value, end_value) - self.assertEqual(exp_statement, statement) - self.assertEqual(exp_args, args) - - def test__format_range_query(self): - self.assertFormatRangeQueryEquals( - 'SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM ' - 'document d, document_fields d0, document_fields d1, ' - 'document_fields d2 LEFT OUTER JOIN conflicts c ON c.doc_id = ' - 'd.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name = ? AND ' - 'd0.value >= ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND ' - 'd1.value >= ? AND d.doc_id = d2.doc_id AND d2.field_name = ? AND ' - 'd2.value >= ? AND d.doc_id = d0.doc_id AND d0.field_name = ? AND ' - 'd0.value <= ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND ' - 'd1.value <= ? AND d.doc_id = d2.doc_id AND d2.field_name = ? AND ' - 'd2.value <= ? GROUP BY d.doc_id, d.doc_rev, d.content ORDER BY ' - 'd0.value, d1.value, d2.value;', - ['key1', 'a', 'key2', 'b', 'key3', 'c', 'key1', 'p', 'key2', 'q', - 'key3', 'r'], - ["key1", "key2", "key3"], ["a", "b", "c"], ["p", "q", "r"]) - - def test__format_range_query_no_start(self): - self.assertFormatRangeQueryEquals( - 'SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM ' - 'document d, document_fields d0, document_fields d1, ' - 'document_fields d2 LEFT OUTER JOIN conflicts c ON c.doc_id = ' - 'd.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name = ? AND ' - 'd0.value <= ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND ' - 'd1.value <= ? AND d.doc_id = d2.doc_id AND d2.field_name = ? AND ' - 'd2.value <= ? GROUP BY d.doc_id, d.doc_rev, d.content ORDER BY ' - 'd0.value, d1.value, d2.value;', - ['key1', 'a', 'key2', 'b', 'key3', 'c'], - ["key1", "key2", "key3"], None, ["a", "b", "c"]) - - def test__format_range_query_no_end(self): - self.assertFormatRangeQueryEquals( - 'SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM ' - 'document d, document_fields d0, document_fields d1, ' - 'document_fields d2 LEFT OUTER JOIN conflicts c ON c.doc_id = ' - 'd.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name = ? AND ' - 'd0.value >= ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND ' - 'd1.value >= ? AND d.doc_id = d2.doc_id AND d2.field_name = ? AND ' - 'd2.value >= ? GROUP BY d.doc_id, d.doc_rev, d.content ORDER BY ' - 'd0.value, d1.value, d2.value;', - ['key1', 'a', 'key2', 'b', 'key3', 'c'], - ["key1", "key2", "key3"], ["a", "b", "c"], None) - - def test__format_range_query_wildcard(self): - self.assertFormatRangeQueryEquals( - 'SELECT d.doc_id, d.doc_rev, d.content, count(c.doc_rev) FROM ' - 'document d, document_fields d0, document_fields d1, ' - 'document_fields d2 LEFT OUTER JOIN conflicts c ON c.doc_id = ' - 'd.doc_id WHERE d.doc_id = d0.doc_id AND d0.field_name = ? AND ' - 'd0.value >= ? AND d.doc_id = d1.doc_id AND d1.field_name = ? AND ' - 'd1.value >= ? AND d.doc_id = d2.doc_id AND d2.field_name = ? AND ' - 'd2.value NOT NULL AND d.doc_id = d0.doc_id AND d0.field_name = ? ' - 'AND d0.value <= ? AND d.doc_id = d1.doc_id AND d1.field_name = ? ' - 'AND (d1.value < ? OR d1.value GLOB ?) AND d.doc_id = d2.doc_id ' - 'AND d2.field_name = ? AND d2.value NOT NULL GROUP BY d.doc_id, ' - 'd.doc_rev, d.content ORDER BY d0.value, d1.value, d2.value;', - ['key1', 'a', 'key2', 'b', 'key3', 'key1', 'p', 'key2', 'q', 'q*', - 'key3'], - ["key1", "key2", "key3"], ["a", "b*", "*"], ["p", "q*", "*"]) diff --git a/src/leap/soledad/tests/u1db_tests/test_sync.py b/src/leap/soledad/tests/u1db_tests/test_sync.py deleted file mode 100644 index 96aa2736..00000000 --- a/src/leap/soledad/tests/u1db_tests/test_sync.py +++ /dev/null @@ -1,1242 +0,0 @@ -# Copyright 2011-2012 Canonical Ltd. -# -# This file is part of u1db. -# -# u1db is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 -# as published by the Free Software Foundation. -# -# u1db is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with u1db. If not, see . - -"""The Synchronization class for U1DB.""" - -import os -from wsgiref import simple_server - -from u1db import ( - errors, - sync, - vectorclock, - SyncTarget, -) - -from leap.soledad.tests import u1db_tests as tests - -from u1db.backends import ( - inmemory, -) -from u1db.remote import ( - http_target, -) - -from leap.soledad.tests.u1db_tests.test_remote_sync_target import ( - make_http_app, - make_oauth_http_app, -) - -simple_doc = tests.simple_doc -nested_doc = tests.nested_doc - - -def _make_local_db_and_target(test): - db = test.create_database('test') - st = db.get_sync_target() - return db, st - - -def _make_local_db_and_http_target(test, path='test'): - test.startServer() - db = test.request_state._create_database(os.path.basename(path)) - st = http_target.HTTPSyncTarget.connect(test.getURL(path)) - return db, st - - -def _make_local_db_and_oauth_http_target(test): - db, st = _make_local_db_and_http_target(test, '~/test') - st.set_oauth_credentials(tests.consumer1.key, tests.consumer1.secret, - tests.token1.key, tests.token1.secret) - return db, st - - -target_scenarios = [ - ('local', {'create_db_and_target': _make_local_db_and_target}), - ('http', {'create_db_and_target': _make_local_db_and_http_target, - 'make_app_with_state': make_http_app}), - ('oauth_http', {'create_db_and_target': - _make_local_db_and_oauth_http_target, - 'make_app_with_state': make_oauth_http_app}), -] - - -class DatabaseSyncTargetTests(tests.DatabaseBaseTests, - tests.TestCaseWithServer): - - scenarios = (tests.multiply_scenarios(tests.DatabaseBaseTests.scenarios, - target_scenarios)) - #+ c_db_scenarios) - # whitebox true means self.db is the actual local db object - # against which the sync is performed - whitebox = True - - def setUp(self): - super(DatabaseSyncTargetTests, self).setUp() - self.db, self.st = self.create_db_and_target(self) - self.other_changes = [] - - def tearDown(self): - # We delete them explicitly, so that connections are cleanly closed - del self.st - self.db.close() - del self.db - super(DatabaseSyncTargetTests, self).tearDown() - - def receive_doc(self, doc, gen, trans_id): - self.other_changes.append( - (doc.doc_id, doc.rev, doc.get_json(), gen, trans_id)) - - def set_trace_hook(self, callback, shallow=False): - setter = (self.st._set_trace_hook if not shallow else - self.st._set_trace_hook_shallow) - try: - setter(callback) - except NotImplementedError: - self.skipTest("%s does not implement _set_trace_hook" - % (self.st.__class__.__name__,)) - - def test_get_sync_target(self): - self.assertIsNot(None, self.st) - - def test_get_sync_info(self): - self.assertEqual( - ('test', 0, '', 0, ''), self.st.get_sync_info('other')) - - def test_create_doc_updates_sync_info(self): - self.assertEqual( - ('test', 0, '', 0, ''), self.st.get_sync_info('other')) - self.db.create_doc_from_json(simple_doc) - self.assertEqual(1, self.st.get_sync_info('other')[1]) - - def test_record_sync_info(self): - self.st.record_sync_info('replica', 10, 'T-transid') - self.assertEqual( - ('test', 0, '', 10, 'T-transid'), self.st.get_sync_info('replica')) - - def test_sync_exchange(self): - docs_by_gen = [ - (self.make_document('doc-id', 'replica:1', simple_doc), 10, - 'T-sid')] - new_gen, trans_id = self.st.sync_exchange( - docs_by_gen, 'replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertGetDoc(self.db, 'doc-id', 'replica:1', simple_doc, False) - self.assertTransactionLog(['doc-id'], self.db) - last_trans_id = self.getLastTransId(self.db) - self.assertEqual(([], 1, last_trans_id), - (self.other_changes, new_gen, last_trans_id)) - self.assertEqual(10, self.st.get_sync_info('replica')[3]) - - def test_sync_exchange_deleted(self): - doc = self.db.create_doc_from_json('{}') - edit_rev = 'replica:1|' + doc.rev - docs_by_gen = [ - (self.make_document(doc.doc_id, edit_rev, None), 10, 'T-sid')] - new_gen, trans_id = self.st.sync_exchange( - docs_by_gen, 'replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertGetDocIncludeDeleted( - self.db, doc.doc_id, edit_rev, None, False) - self.assertTransactionLog([doc.doc_id, doc.doc_id], self.db) - last_trans_id = self.getLastTransId(self.db) - self.assertEqual(([], 2, last_trans_id), - (self.other_changes, new_gen, trans_id)) - self.assertEqual(10, self.st.get_sync_info('replica')[3]) - - def test_sync_exchange_push_many(self): - docs_by_gen = [ - (self.make_document('doc-id', 'replica:1', simple_doc), 10, 'T-1'), - (self.make_document('doc-id2', 'replica:1', nested_doc), 11, - 'T-2')] - new_gen, trans_id = self.st.sync_exchange( - docs_by_gen, 'replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertGetDoc(self.db, 'doc-id', 'replica:1', simple_doc, False) - self.assertGetDoc(self.db, 'doc-id2', 'replica:1', nested_doc, False) - self.assertTransactionLog(['doc-id', 'doc-id2'], self.db) - last_trans_id = self.getLastTransId(self.db) - self.assertEqual(([], 2, last_trans_id), - (self.other_changes, new_gen, trans_id)) - self.assertEqual(11, self.st.get_sync_info('replica')[3]) - - def test_sync_exchange_refuses_conflicts(self): - doc = self.db.create_doc_from_json(simple_doc) - self.assertTransactionLog([doc.doc_id], self.db) - new_doc = '{"key": "altval"}' - docs_by_gen = [ - (self.make_document(doc.doc_id, 'replica:1', new_doc), 10, - 'T-sid')] - new_gen, _ = self.st.sync_exchange( - docs_by_gen, 'replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertTransactionLog([doc.doc_id], self.db) - self.assertEqual( - (doc.doc_id, doc.rev, simple_doc, 1), self.other_changes[0][:-1]) - self.assertEqual(1, new_gen) - if self.whitebox: - self.assertEqual(self.db._last_exchange_log['return'], - {'last_gen': 1, 'docs': [(doc.doc_id, doc.rev)]}) - - def test_sync_exchange_ignores_convergence(self): - doc = self.db.create_doc_from_json(simple_doc) - self.assertTransactionLog([doc.doc_id], self.db) - gen, txid = self.db._get_generation_info() - docs_by_gen = [ - (self.make_document(doc.doc_id, doc.rev, simple_doc), 10, 'T-sid')] - new_gen, _ = self.st.sync_exchange( - docs_by_gen, 'replica', last_known_generation=gen, - last_known_trans_id=txid, return_doc_cb=self.receive_doc) - self.assertTransactionLog([doc.doc_id], self.db) - self.assertEqual(([], 1), (self.other_changes, new_gen)) - - def test_sync_exchange_returns_new_docs(self): - doc = self.db.create_doc_from_json(simple_doc) - self.assertTransactionLog([doc.doc_id], self.db) - new_gen, _ = self.st.sync_exchange( - [], 'other-replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertTransactionLog([doc.doc_id], self.db) - self.assertEqual( - (doc.doc_id, doc.rev, simple_doc, 1), self.other_changes[0][:-1]) - self.assertEqual(1, new_gen) - if self.whitebox: - self.assertEqual(self.db._last_exchange_log['return'], - {'last_gen': 1, 'docs': [(doc.doc_id, doc.rev)]}) - - def test_sync_exchange_returns_deleted_docs(self): - doc = self.db.create_doc_from_json(simple_doc) - self.db.delete_doc(doc) - self.assertTransactionLog([doc.doc_id, doc.doc_id], self.db) - new_gen, _ = self.st.sync_exchange( - [], 'other-replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertTransactionLog([doc.doc_id, doc.doc_id], self.db) - self.assertEqual( - (doc.doc_id, doc.rev, None, 2), self.other_changes[0][:-1]) - self.assertEqual(2, new_gen) - if self.whitebox: - self.assertEqual(self.db._last_exchange_log['return'], - {'last_gen': 2, 'docs': [(doc.doc_id, doc.rev)]}) - - def test_sync_exchange_returns_many_new_docs(self): - doc = self.db.create_doc_from_json(simple_doc) - doc2 = self.db.create_doc_from_json(nested_doc) - self.assertTransactionLog([doc.doc_id, doc2.doc_id], self.db) - new_gen, _ = self.st.sync_exchange( - [], 'other-replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertTransactionLog([doc.doc_id, doc2.doc_id], self.db) - self.assertEqual(2, new_gen) - self.assertEqual( - [(doc.doc_id, doc.rev, simple_doc, 1), - (doc2.doc_id, doc2.rev, nested_doc, 2)], - [c[:-1] for c in self.other_changes]) - if self.whitebox: - self.assertEqual( - self.db._last_exchange_log['return'], - {'last_gen': 2, 'docs': - [(doc.doc_id, doc.rev), (doc2.doc_id, doc2.rev)]}) - - def test_sync_exchange_getting_newer_docs(self): - doc = self.db.create_doc_from_json(simple_doc) - self.assertTransactionLog([doc.doc_id], self.db) - new_doc = '{"key": "altval"}' - docs_by_gen = [ - (self.make_document(doc.doc_id, 'test:1|z:2', new_doc), 10, - 'T-sid')] - new_gen, _ = self.st.sync_exchange( - docs_by_gen, 'other-replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertTransactionLog([doc.doc_id, doc.doc_id], self.db) - self.assertEqual(([], 2), (self.other_changes, new_gen)) - - def test_sync_exchange_with_concurrent_updates_of_synced_doc(self): - expected = [] - - def before_whatschanged_cb(state): - if state != 'before whats_changed': - return - cont = '{"key": "cuncurrent"}' - conc_rev = self.db.put_doc( - self.make_document(doc.doc_id, 'test:1|z:2', cont)) - expected.append((doc.doc_id, conc_rev, cont, 3)) - - self.set_trace_hook(before_whatschanged_cb) - doc = self.db.create_doc_from_json(simple_doc) - self.assertTransactionLog([doc.doc_id], self.db) - new_doc = '{"key": "altval"}' - docs_by_gen = [ - (self.make_document(doc.doc_id, 'test:1|z:2', new_doc), 10, - 'T-sid')] - new_gen, _ = self.st.sync_exchange( - docs_by_gen, 'other-replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertEqual(expected, [c[:-1] for c in self.other_changes]) - self.assertEqual(3, new_gen) - - def test_sync_exchange_with_concurrent_updates(self): - - def after_whatschanged_cb(state): - if state != 'after whats_changed': - return - self.db.create_doc_from_json('{"new": "doc"}') - - self.set_trace_hook(after_whatschanged_cb) - doc = self.db.create_doc_from_json(simple_doc) - self.assertTransactionLog([doc.doc_id], self.db) - new_doc = '{"key": "altval"}' - docs_by_gen = [ - (self.make_document(doc.doc_id, 'test:1|z:2', new_doc), 10, - 'T-sid')] - new_gen, _ = self.st.sync_exchange( - docs_by_gen, 'other-replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertEqual(([], 2), (self.other_changes, new_gen)) - - def test_sync_exchange_converged_handling(self): - doc = self.db.create_doc_from_json(simple_doc) - docs_by_gen = [ - (self.make_document('new', 'other:1', '{}'), 4, 'T-foo'), - (self.make_document(doc.doc_id, doc.rev, doc.get_json()), 5, - 'T-bar')] - new_gen, _ = self.st.sync_exchange( - docs_by_gen, 'other-replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertEqual(([], 2), (self.other_changes, new_gen)) - - def test_sync_exchange_detect_incomplete_exchange(self): - def before_get_docs_explode(state): - if state != 'before get_docs': - return - raise errors.U1DBError("fail") - self.set_trace_hook(before_get_docs_explode) - # suppress traceback printing in the wsgiref server - self.patch(simple_server.ServerHandler, - 'log_exception', lambda h, exc_info: None) - doc = self.db.create_doc_from_json(simple_doc) - self.assertTransactionLog([doc.doc_id], self.db) - self.assertRaises( - (errors.U1DBError, errors.BrokenSyncStream), - self.st.sync_exchange, [], 'other-replica', - last_known_generation=0, last_known_trans_id=None, - return_doc_cb=self.receive_doc) - - def test_sync_exchange_doc_ids(self): - sync_exchange_doc_ids = getattr(self.st, 'sync_exchange_doc_ids', None) - if sync_exchange_doc_ids is None: - self.skipTest("sync_exchange_doc_ids not implemented") - db2 = self.create_database('test2') - doc = db2.create_doc_from_json(simple_doc) - new_gen, trans_id = sync_exchange_doc_ids( - db2, [(doc.doc_id, 10, 'T-sid')], 0, None, - return_doc_cb=self.receive_doc) - self.assertGetDoc(self.db, doc.doc_id, doc.rev, simple_doc, False) - self.assertTransactionLog([doc.doc_id], self.db) - last_trans_id = self.getLastTransId(self.db) - self.assertEqual(([], 1, last_trans_id), - (self.other_changes, new_gen, trans_id)) - self.assertEqual(10, self.st.get_sync_info(db2._replica_uid)[3]) - - def test__set_trace_hook(self): - called = [] - - def cb(state): - called.append(state) - - self.set_trace_hook(cb) - self.st.sync_exchange([], 'replica', 0, None, self.receive_doc) - self.st.record_sync_info('replica', 0, 'T-sid') - self.assertEqual(['before whats_changed', - 'after whats_changed', - 'before get_docs', - 'record_sync_info', - ], - called) - - def test__set_trace_hook_shallow(self): - if (self.st._set_trace_hook_shallow == self.st._set_trace_hook - or - self.st._set_trace_hook_shallow.im_func == - SyncTarget._set_trace_hook_shallow.im_func): - # shallow same as full - expected = ['before whats_changed', - 'after whats_changed', - 'before get_docs', - 'record_sync_info', - ] - else: - expected = ['sync_exchange', 'record_sync_info'] - - called = [] - - def cb(state): - called.append(state) - - self.set_trace_hook(cb, shallow=True) - self.st.sync_exchange([], 'replica', 0, None, self.receive_doc) - self.st.record_sync_info('replica', 0, 'T-sid') - self.assertEqual(expected, called) - - -def sync_via_synchronizer(test, db_source, db_target, trace_hook=None, - trace_hook_shallow=None): - target = db_target.get_sync_target() - trace_hook = trace_hook or trace_hook_shallow - if trace_hook: - target._set_trace_hook(trace_hook) - return sync.Synchronizer(db_source, target).sync() - - -sync_scenarios = [] -for name, scenario in tests.LOCAL_DATABASES_SCENARIOS: - scenario = dict(scenario) - scenario['do_sync'] = sync_via_synchronizer - sync_scenarios.append((name, scenario)) - scenario = dict(scenario) - - -def make_database_for_http_test(test, replica_uid): - if test.server is None: - test.startServer() - db = test.request_state._create_database(replica_uid) - try: - http_at = test._http_at - except AttributeError: - http_at = test._http_at = {} - http_at[db] = replica_uid - return db - - -def copy_database_for_http_test(test, db): - # DO NOT COPY OR REUSE THIS CODE OUTSIDE TESTS: COPYING U1DB DATABASES IS - # THE WRONG THING TO DO, THE ONLY REASON WE DO SO HERE IS TO TEST THAT WE - # CORRECTLY DETECT IT HAPPENING SO THAT WE CAN RAISE ERRORS RATHER THAN - # CORRUPT USER DATA. USE SYNC INSTEAD, OR WE WILL SEND NINJA TO YOUR HOUSE. - if test.server is None: - test.startServer() - new_db = test.request_state._copy_database(db) - try: - http_at = test._http_at - except AttributeError: - http_at = test._http_at = {} - path = db._replica_uid - while path in http_at.values(): - path += 'copy' - http_at[new_db] = path - return new_db - - -def sync_via_synchronizer_and_http(test, db_source, db_target, - trace_hook=None, trace_hook_shallow=None): - if trace_hook: - test.skipTest("full trace hook unsupported over http") - path = test._http_at[db_target] - target = http_target.HTTPSyncTarget.connect(test.getURL(path)) - if trace_hook_shallow: - target._set_trace_hook_shallow(trace_hook_shallow) - return sync.Synchronizer(db_source, target).sync() - - -sync_scenarios.append(('pyhttp', { - 'make_database_for_test': make_database_for_http_test, - 'copy_database_for_test': copy_database_for_http_test, - 'make_document_for_test': tests.make_document_for_test, - 'make_app_with_state': make_http_app, - 'do_sync': sync_via_synchronizer_and_http -})) - - -class DatabaseSyncTests(tests.DatabaseBaseTests, - tests.TestCaseWithServer): - - scenarios = sync_scenarios - do_sync = None # set by scenarios - - def create_database(self, replica_uid, sync_role=None): - if replica_uid == 'test' and sync_role is None: - # created up the chain by base class but unused - return None - db = self.create_database_for_role(replica_uid, sync_role) - if sync_role: - self._use_tracking[db] = (replica_uid, sync_role) - return db - - def create_database_for_role(self, replica_uid, sync_role): - # hook point for reuse - return super(DatabaseSyncTests, self).create_database(replica_uid) - - def copy_database(self, db, sync_role=None): - # DO NOT COPY OR REUSE THIS CODE OUTSIDE TESTS: COPYING U1DB DATABASES - # IS THE WRONG THING TO DO, THE ONLY REASON WE DO SO HERE IS TO TEST - # THAT WE CORRECTLY DETECT IT HAPPENING SO THAT WE CAN RAISE ERRORS - # RATHER THAN CORRUPT USER DATA. USE SYNC INSTEAD, OR WE WILL SEND - # NINJA TO YOUR HOUSE. - db_copy = super(DatabaseSyncTests, self).copy_database(db) - name, orig_sync_role = self._use_tracking[db] - self._use_tracking[db_copy] = (name + '(copy)', sync_role - or orig_sync_role) - return db_copy - - def sync(self, db_from, db_to, trace_hook=None, - trace_hook_shallow=None): - from_name, from_sync_role = self._use_tracking[db_from] - to_name, to_sync_role = self._use_tracking[db_to] - if from_sync_role not in ('source', 'both'): - raise Exception("%s marked for %s use but used as source" % - (from_name, from_sync_role)) - if to_sync_role not in ('target', 'both'): - raise Exception("%s marked for %s use but used as target" % - (to_name, to_sync_role)) - return self.do_sync(self, db_from, db_to, trace_hook, - trace_hook_shallow) - - def setUp(self): - self._use_tracking = {} - super(DatabaseSyncTests, self).setUp() - - def assertLastExchangeLog(self, db, expected): - log = getattr(db, '_last_exchange_log', None) - if log is None: - return - self.assertEqual(expected, log) - - def test_sync_tracks_db_generation_of_other(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - self.assertEqual(0, self.sync(self.db1, self.db2)) - self.assertEqual( - (0, ''), self.db1._get_replica_gen_and_trans_id('test2')) - self.assertEqual( - (0, ''), self.db2._get_replica_gen_and_trans_id('test1')) - self.assertLastExchangeLog(self.db2, - {'receive': - {'docs': [], 'last_known_gen': 0}, - 'return': - {'docs': [], 'last_gen': 0}}) - - def test_sync_autoresolves(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - doc1 = self.db1.create_doc_from_json(simple_doc, doc_id='doc') - rev1 = doc1.rev - doc2 = self.db2.create_doc_from_json(simple_doc, doc_id='doc') - rev2 = doc2.rev - self.sync(self.db1, self.db2) - doc = self.db1.get_doc('doc') - self.assertFalse(doc.has_conflicts) - self.assertEqual(doc.rev, self.db2.get_doc('doc').rev) - v = vectorclock.VectorClockRev(doc.rev) - self.assertTrue(v.is_newer(vectorclock.VectorClockRev(rev1))) - self.assertTrue(v.is_newer(vectorclock.VectorClockRev(rev2))) - - def test_sync_autoresolves_moar(self): - # here we test that when a database that has a conflicted document is - # the source of a sync, and the target database has a revision of the - # conflicted document that is newer than the source database's, and - # that target's database's document's content is the same as the - # source's document's conflict's, the source's document's conflict gets - # autoresolved, and the source's document's revision bumped. - # - # idea is as follows: - # A B - # a1 - - # `-------> - # a1 a1 - # v v - # a2 a1b1 - # `-------> - # a1b1+a2 a1b1 - # v - # a1b1+a2 a1b2 (a1b2 has same content as a2) - # `-------> - # a3b2 a1b2 (autoresolved) - # `-------> - # a3b2 a3b2 - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - self.db1.create_doc_from_json(simple_doc, doc_id='doc') - self.sync(self.db1, self.db2) - for db, content in [(self.db1, '{}'), (self.db2, '{"hi": 42}')]: - doc = db.get_doc('doc') - doc.set_json(content) - db.put_doc(doc) - self.sync(self.db1, self.db2) - # db1 and db2 now both have a doc of {hi:42}, but db1 has a conflict - doc = self.db1.get_doc('doc') - rev1 = doc.rev - self.assertTrue(doc.has_conflicts) - # set db2 to have a doc of {} (same as db1 before the conflict) - doc = self.db2.get_doc('doc') - doc.set_json('{}') - self.db2.put_doc(doc) - rev2 = doc.rev - # sync it across - self.sync(self.db1, self.db2) - # tadaa! - doc = self.db1.get_doc('doc') - self.assertFalse(doc.has_conflicts) - vec1 = vectorclock.VectorClockRev(rev1) - vec2 = vectorclock.VectorClockRev(rev2) - vec3 = vectorclock.VectorClockRev(doc.rev) - self.assertTrue(vec3.is_newer(vec1)) - self.assertTrue(vec3.is_newer(vec2)) - # because the conflict is on the source, sync it another time - self.sync(self.db1, self.db2) - # make sure db2 now has the exact same thing - self.assertEqual(self.db1.get_doc('doc'), self.db2.get_doc('doc')) - - def test_sync_autoresolves_moar_backwards(self): - # here we test that when a database that has a conflicted document is - # the target of a sync, and the source database has a revision of the - # conflicted document that is newer than the target database's, and - # that source's database's document's content is the same as the - # target's document's conflict's, the target's document's conflict gets - # autoresolved, and the document's revision bumped. - # - # idea is as follows: - # A B - # a1 - - # `-------> - # a1 a1 - # v v - # a2 a1b1 - # `-------> - # a1b1+a2 a1b1 - # v - # a1b1+a2 a1b2 (a1b2 has same content as a2) - # <-------' - # a3b2 a3b2 (autoresolved and propagated) - self.db1 = self.create_database('test1', 'both') - self.db2 = self.create_database('test2', 'both') - self.db1.create_doc_from_json(simple_doc, doc_id='doc') - self.sync(self.db1, self.db2) - for db, content in [(self.db1, '{}'), (self.db2, '{"hi": 42}')]: - doc = db.get_doc('doc') - doc.set_json(content) - db.put_doc(doc) - self.sync(self.db1, self.db2) - # db1 and db2 now both have a doc of {hi:42}, but db1 has a conflict - doc = self.db1.get_doc('doc') - rev1 = doc.rev - self.assertTrue(doc.has_conflicts) - revc = self.db1.get_doc_conflicts('doc')[-1].rev - # set db2 to have a doc of {} (same as db1 before the conflict) - doc = self.db2.get_doc('doc') - doc.set_json('{}') - self.db2.put_doc(doc) - rev2 = doc.rev - # sync it across - self.sync(self.db2, self.db1) - # tadaa! - doc = self.db1.get_doc('doc') - self.assertFalse(doc.has_conflicts) - vec1 = vectorclock.VectorClockRev(rev1) - vec2 = vectorclock.VectorClockRev(rev2) - vec3 = vectorclock.VectorClockRev(doc.rev) - vecc = vectorclock.VectorClockRev(revc) - self.assertTrue(vec3.is_newer(vec1)) - self.assertTrue(vec3.is_newer(vec2)) - self.assertTrue(vec3.is_newer(vecc)) - # make sure db2 now has the exact same thing - self.assertEqual(self.db1.get_doc('doc'), self.db2.get_doc('doc')) - - def test_sync_autoresolves_moar_backwards_three(self): - # same as autoresolves_moar_backwards, but with three databases (note - # all the syncs go in the same direction -- this is a more natural - # scenario): - # - # A B C - # a1 - - - # `-------> - # a1 a1 - - # `-------> - # a1 a1 a1 - # v v - # a2 a1b1 a1 - # `-------------------> - # a2 a1b1 a2 - # `-------> - # a2+a1b1 a2 - # v - # a2 a2+a1b1 a2c1 (same as a1b1) - # `-------------------> - # a2c1 a2+a1b1 a2c1 - # `-------> - # a2b2c1 a2b2c1 a2c1 - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'both') - self.db3 = self.create_database('test3', 'target') - self.db1.create_doc_from_json(simple_doc, doc_id='doc') - self.sync(self.db1, self.db2) - self.sync(self.db2, self.db3) - for db, content in [(self.db2, '{"hi": 42}'), - (self.db1, '{}'), - ]: - doc = db.get_doc('doc') - doc.set_json(content) - db.put_doc(doc) - self.sync(self.db1, self.db3) - self.sync(self.db2, self.db3) - # db2 and db3 now both have a doc of {}, but db2 has a - # conflict - doc = self.db2.get_doc('doc') - self.assertTrue(doc.has_conflicts) - revc = self.db2.get_doc_conflicts('doc')[-1].rev - self.assertEqual('{}', doc.get_json()) - self.assertEqual(self.db3.get_doc('doc').get_json(), doc.get_json()) - self.assertEqual(self.db3.get_doc('doc').rev, doc.rev) - # set db3 to have a doc of {hi:42} (same as db2 before the conflict) - doc = self.db3.get_doc('doc') - doc.set_json('{"hi": 42}') - self.db3.put_doc(doc) - rev3 = doc.rev - # sync it across to db1 - self.sync(self.db1, self.db3) - # db1 now has hi:42, with a rev that is newer than db2's doc - doc = self.db1.get_doc('doc') - rev1 = doc.rev - self.assertFalse(doc.has_conflicts) - self.assertEqual('{"hi": 42}', doc.get_json()) - VCR = vectorclock.VectorClockRev - self.assertTrue(VCR(rev1).is_newer(VCR(self.db2.get_doc('doc').rev))) - # so sync it to db2 - self.sync(self.db1, self.db2) - # tadaa! - doc = self.db2.get_doc('doc') - self.assertFalse(doc.has_conflicts) - # db2's revision of the document is strictly newer than db1's before - # the sync, and db3's before that sync way back when - self.assertTrue(VCR(doc.rev).is_newer(VCR(rev1))) - self.assertTrue(VCR(doc.rev).is_newer(VCR(rev3))) - self.assertTrue(VCR(doc.rev).is_newer(VCR(revc))) - # make sure both dbs now have the exact same thing - self.assertEqual(self.db1.get_doc('doc'), self.db2.get_doc('doc')) - - def test_sync_puts_changes(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - doc = self.db1.create_doc_from_json(simple_doc) - self.assertEqual(1, self.sync(self.db1, self.db2)) - self.assertGetDoc(self.db2, doc.doc_id, doc.rev, simple_doc, False) - self.assertEqual(1, self.db1._get_replica_gen_and_trans_id('test2')[0]) - self.assertEqual(1, self.db2._get_replica_gen_and_trans_id('test1')[0]) - self.assertLastExchangeLog(self.db2, - {'receive': - {'docs': [(doc.doc_id, doc.rev)], - 'source_uid': 'test1', - 'source_gen': 1, - 'last_known_gen': 0}, - 'return': {'docs': [], 'last_gen': 1}}) - - def test_sync_pulls_changes(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - doc = self.db2.create_doc_from_json(simple_doc) - self.db1.create_index('test-idx', 'key') - self.assertEqual(0, self.sync(self.db1, self.db2)) - self.assertGetDoc(self.db1, doc.doc_id, doc.rev, simple_doc, False) - self.assertEqual(1, self.db1._get_replica_gen_and_trans_id('test2')[0]) - self.assertEqual(1, self.db2._get_replica_gen_and_trans_id('test1')[0]) - self.assertLastExchangeLog(self.db2, - {'receive': - {'docs': [], 'last_known_gen': 0}, - 'return': - {'docs': [(doc.doc_id, doc.rev)], - 'last_gen': 1}}) - self.assertEqual([doc], self.db1.get_from_index('test-idx', 'value')) - - def test_sync_pulling_doesnt_update_other_if_changed(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - doc = self.db2.create_doc_from_json(simple_doc) - # After the local side has sent its list of docs, before we start - # receiving the "targets" response, we update the local database with a - # new record. - # When we finish synchronizing, we can notice that something locally - # was updated, and we cannot tell c2 our new updated generation - - def before_get_docs(state): - if state != 'before get_docs': - return - self.db1.create_doc_from_json(simple_doc) - - self.assertEqual(0, self.sync(self.db1, self.db2, - trace_hook=before_get_docs)) - self.assertLastExchangeLog(self.db2, - {'receive': - {'docs': [], 'last_known_gen': 0}, - 'return': - {'docs': [(doc.doc_id, doc.rev)], - 'last_gen': 1}}) - self.assertEqual(1, self.db1._get_replica_gen_and_trans_id('test2')[0]) - # c2 should not have gotten a '_record_sync_info' call, because the - # local database had been updated more than just by the messages - # returned from c2. - self.assertEqual( - (0, ''), self.db2._get_replica_gen_and_trans_id('test1')) - - def test_sync_doesnt_update_other_if_nothing_pulled(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - self.db1.create_doc_from_json(simple_doc) - - def no_record_sync_info(state): - if state != 'record_sync_info': - return - self.fail('SyncTarget.record_sync_info was called') - self.assertEqual(1, self.sync(self.db1, self.db2, - trace_hook_shallow=no_record_sync_info)) - self.assertEqual( - 1, - self.db2._get_replica_gen_and_trans_id(self.db1._replica_uid)[0]) - - def test_sync_ignores_convergence(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'both') - doc = self.db1.create_doc_from_json(simple_doc) - self.db3 = self.create_database('test3', 'target') - self.assertEqual(1, self.sync(self.db1, self.db3)) - self.assertEqual(0, self.sync(self.db2, self.db3)) - self.assertEqual(1, self.sync(self.db1, self.db2)) - self.assertLastExchangeLog(self.db2, - {'receive': - {'docs': [(doc.doc_id, doc.rev)], - 'source_uid': 'test1', - 'source_gen': 1, 'last_known_gen': 0}, - 'return': {'docs': [], 'last_gen': 1}}) - - def test_sync_ignores_superseded(self): - self.db1 = self.create_database('test1', 'both') - self.db2 = self.create_database('test2', 'both') - doc = self.db1.create_doc_from_json(simple_doc) - doc_rev1 = doc.rev - self.db3 = self.create_database('test3', 'target') - self.sync(self.db1, self.db3) - self.sync(self.db2, self.db3) - new_content = '{"key": "altval"}' - doc.set_json(new_content) - self.db1.put_doc(doc) - doc_rev2 = doc.rev - self.sync(self.db2, self.db1) - self.assertLastExchangeLog(self.db1, - {'receive': - {'docs': [(doc.doc_id, doc_rev1)], - 'source_uid': 'test2', - 'source_gen': 1, 'last_known_gen': 0}, - 'return': - {'docs': [(doc.doc_id, doc_rev2)], - 'last_gen': 2}}) - self.assertGetDoc(self.db1, doc.doc_id, doc_rev2, new_content, False) - - def test_sync_sees_remote_conflicted(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - doc1 = self.db1.create_doc_from_json(simple_doc) - doc_id = doc1.doc_id - doc1_rev = doc1.rev - self.db1.create_index('test-idx', 'key') - new_doc = '{"key": "altval"}' - doc2 = self.db2.create_doc_from_json(new_doc, doc_id=doc_id) - doc2_rev = doc2.rev - self.assertTransactionLog([doc1.doc_id], self.db1) - self.sync(self.db1, self.db2) - self.assertLastExchangeLog(self.db2, - {'receive': - {'docs': [(doc_id, doc1_rev)], - 'source_uid': 'test1', - 'source_gen': 1, 'last_known_gen': 0}, - 'return': - {'docs': [(doc_id, doc2_rev)], - 'last_gen': 1}}) - self.assertTransactionLog([doc_id, doc_id], self.db1) - self.assertGetDoc(self.db1, doc_id, doc2_rev, new_doc, True) - self.assertGetDoc(self.db2, doc_id, doc2_rev, new_doc, False) - from_idx = self.db1.get_from_index('test-idx', 'altval')[0] - self.assertEqual(doc2.doc_id, from_idx.doc_id) - self.assertEqual(doc2.rev, from_idx.rev) - self.assertTrue(from_idx.has_conflicts) - self.assertEqual([], self.db1.get_from_index('test-idx', 'value')) - - def test_sync_sees_remote_delete_conflicted(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - doc1 = self.db1.create_doc_from_json(simple_doc) - doc_id = doc1.doc_id - self.db1.create_index('test-idx', 'key') - self.sync(self.db1, self.db2) - doc2 = self.make_document(doc1.doc_id, doc1.rev, doc1.get_json()) - new_doc = '{"key": "altval"}' - doc1.set_json(new_doc) - self.db1.put_doc(doc1) - self.db2.delete_doc(doc2) - self.assertTransactionLog([doc_id, doc_id], self.db1) - self.sync(self.db1, self.db2) - self.assertLastExchangeLog(self.db2, - {'receive': - {'docs': [(doc_id, doc1.rev)], - 'source_uid': 'test1', - 'source_gen': 2, 'last_known_gen': 1}, - 'return': {'docs': [(doc_id, doc2.rev)], - 'last_gen': 2}}) - self.assertTransactionLog([doc_id, doc_id, doc_id], self.db1) - self.assertGetDocIncludeDeleted(self.db1, doc_id, doc2.rev, None, True) - self.assertGetDocIncludeDeleted( - self.db2, doc_id, doc2.rev, None, False) - self.assertEqual([], self.db1.get_from_index('test-idx', 'value')) - - def test_sync_local_race_conflicted(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - doc = self.db1.create_doc_from_json(simple_doc) - doc_id = doc.doc_id - doc1_rev = doc.rev - self.db1.create_index('test-idx', 'key') - self.sync(self.db1, self.db2) - content1 = '{"key": "localval"}' - content2 = '{"key": "altval"}' - doc.set_json(content2) - self.db2.put_doc(doc) - doc2_rev2 = doc.rev - triggered = [] - - def after_whatschanged(state): - if state != 'after whats_changed': - return - triggered.append(True) - doc = self.make_document(doc_id, doc1_rev, content1) - self.db1.put_doc(doc) - - self.sync(self.db1, self.db2, trace_hook=after_whatschanged) - self.assertEqual([True], triggered) - self.assertGetDoc(self.db1, doc_id, doc2_rev2, content2, True) - from_idx = self.db1.get_from_index('test-idx', 'altval')[0] - self.assertEqual(doc.doc_id, from_idx.doc_id) - self.assertEqual(doc.rev, from_idx.rev) - self.assertTrue(from_idx.has_conflicts) - self.assertEqual([], self.db1.get_from_index('test-idx', 'value')) - self.assertEqual([], self.db1.get_from_index('test-idx', 'localval')) - - def test_sync_propagates_deletes(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'both') - doc1 = self.db1.create_doc_from_json(simple_doc) - doc_id = doc1.doc_id - self.db1.create_index('test-idx', 'key') - self.sync(self.db1, self.db2) - self.db2.create_index('test-idx', 'key') - self.db3 = self.create_database('test3', 'target') - self.sync(self.db1, self.db3) - self.db1.delete_doc(doc1) - deleted_rev = doc1.rev - self.sync(self.db1, self.db2) - self.assertLastExchangeLog(self.db2, - {'receive': - {'docs': [(doc_id, deleted_rev)], - 'source_uid': 'test1', - 'source_gen': 2, 'last_known_gen': 1}, - 'return': {'docs': [], 'last_gen': 2}}) - self.assertGetDocIncludeDeleted( - self.db1, doc_id, deleted_rev, None, False) - self.assertGetDocIncludeDeleted( - self.db2, doc_id, deleted_rev, None, False) - self.assertEqual([], self.db1.get_from_index('test-idx', 'value')) - self.assertEqual([], self.db2.get_from_index('test-idx', 'value')) - self.sync(self.db2, self.db3) - self.assertLastExchangeLog(self.db3, - {'receive': - {'docs': [(doc_id, deleted_rev)], - 'source_uid': 'test2', - 'source_gen': 2, - 'last_known_gen': 0}, - 'return': - {'docs': [], 'last_gen': 2}}) - self.assertGetDocIncludeDeleted( - self.db3, doc_id, deleted_rev, None, False) - - def test_sync_propagates_resolution(self): - self.db1 = self.create_database('test1', 'both') - self.db2 = self.create_database('test2', 'both') - doc1 = self.db1.create_doc_from_json('{"a": 1}', doc_id='the-doc') - db3 = self.create_database('test3', 'both') - self.sync(self.db2, self.db1) - self.assertEqual( - self.db1._get_generation_info(), - self.db2._get_replica_gen_and_trans_id(self.db1._replica_uid)) - self.assertEqual( - self.db2._get_generation_info(), - self.db1._get_replica_gen_and_trans_id(self.db2._replica_uid)) - self.sync(db3, self.db1) - # update on 2 - doc2 = self.make_document('the-doc', doc1.rev, '{"a": 2}') - self.db2.put_doc(doc2) - self.sync(self.db2, db3) - self.assertEqual(db3.get_doc('the-doc').rev, doc2.rev) - # update on 1 - doc1.set_json('{"a": 3}') - self.db1.put_doc(doc1) - # conflicts - self.sync(self.db2, self.db1) - self.sync(db3, self.db1) - self.assertTrue(self.db2.get_doc('the-doc').has_conflicts) - self.assertTrue(db3.get_doc('the-doc').has_conflicts) - # resolve - conflicts = self.db2.get_doc_conflicts('the-doc') - doc4 = self.make_document('the-doc', None, '{"a": 4}') - revs = [doc.rev for doc in conflicts] - self.db2.resolve_doc(doc4, revs) - doc2 = self.db2.get_doc('the-doc') - self.assertEqual(doc4.get_json(), doc2.get_json()) - self.assertFalse(doc2.has_conflicts) - self.sync(self.db2, db3) - doc3 = db3.get_doc('the-doc') - self.assertEqual(doc4.get_json(), doc3.get_json()) - self.assertFalse(doc3.has_conflicts) - - def test_sync_supersedes_conflicts(self): - self.db1 = self.create_database('test1', 'both') - self.db2 = self.create_database('test2', 'target') - db3 = self.create_database('test3', 'both') - doc1 = self.db1.create_doc_from_json('{"a": 1}', doc_id='the-doc') - self.db2.create_doc_from_json('{"b": 1}', doc_id='the-doc') - db3.create_doc_from_json('{"c": 1}', doc_id='the-doc') - self.sync(db3, self.db1) - self.assertEqual( - self.db1._get_generation_info(), - db3._get_replica_gen_and_trans_id(self.db1._replica_uid)) - self.assertEqual( - db3._get_generation_info(), - self.db1._get_replica_gen_and_trans_id(db3._replica_uid)) - self.sync(db3, self.db2) - self.assertEqual( - self.db2._get_generation_info(), - db3._get_replica_gen_and_trans_id(self.db2._replica_uid)) - self.assertEqual( - db3._get_generation_info(), - self.db2._get_replica_gen_and_trans_id(db3._replica_uid)) - self.assertEqual(3, len(db3.get_doc_conflicts('the-doc'))) - doc1.set_json('{"a": 2}') - self.db1.put_doc(doc1) - self.sync(db3, self.db1) - # original doc1 should have been removed from conflicts - self.assertEqual(3, len(db3.get_doc_conflicts('the-doc'))) - - def test_sync_stops_after_get_sync_info(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - self.db1.create_doc_from_json(tests.simple_doc) - self.sync(self.db1, self.db2) - - def put_hook(state): - self.fail("Tracehook triggered for %s" % (state,)) - - self.sync(self.db1, self.db2, trace_hook_shallow=put_hook) - - def test_sync_detects_rollback_in_source(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc1') - self.sync(self.db1, self.db2) - db1_copy = self.copy_database(self.db1) - self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc2') - self.sync(self.db1, self.db2) - self.assertRaises( - errors.InvalidGeneration, self.sync, db1_copy, self.db2) - - def test_sync_detects_rollback_in_target(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - self.db1.create_doc_from_json(tests.simple_doc, doc_id="divergent") - self.sync(self.db1, self.db2) - db2_copy = self.copy_database(self.db2) - self.db2.create_doc_from_json(tests.simple_doc, doc_id='doc2') - self.sync(self.db1, self.db2) - self.assertRaises( - errors.InvalidGeneration, self.sync, self.db1, db2_copy) - - def test_sync_detects_diverged_source(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - db3 = self.copy_database(self.db1) - self.db1.create_doc_from_json(tests.simple_doc, doc_id="divergent") - db3.create_doc_from_json(tests.simple_doc, doc_id="divergent") - self.sync(self.db1, self.db2) - self.assertRaises( - errors.InvalidTransactionId, self.sync, db3, self.db2) - - def test_sync_detects_diverged_target(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - db3 = self.copy_database(self.db2) - db3.create_doc_from_json(tests.nested_doc, doc_id="divergent") - self.db1.create_doc_from_json(tests.simple_doc, doc_id="divergent") - self.sync(self.db1, self.db2) - self.assertRaises( - errors.InvalidTransactionId, self.sync, self.db1, db3) - - def test_sync_detects_rollback_and_divergence_in_source(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc1') - self.sync(self.db1, self.db2) - db1_copy = self.copy_database(self.db1) - self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc2') - self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc3') - self.sync(self.db1, self.db2) - db1_copy.create_doc_from_json(tests.simple_doc, doc_id='doc2') - db1_copy.create_doc_from_json(tests.simple_doc, doc_id='doc3') - self.assertRaises( - errors.InvalidTransactionId, self.sync, db1_copy, self.db2) - - def test_sync_detects_rollback_and_divergence_in_target(self): - self.db1 = self.create_database('test1', 'source') - self.db2 = self.create_database('test2', 'target') - self.db1.create_doc_from_json(tests.simple_doc, doc_id="divergent") - self.sync(self.db1, self.db2) - db2_copy = self.copy_database(self.db2) - self.db2.create_doc_from_json(tests.simple_doc, doc_id='doc2') - self.db2.create_doc_from_json(tests.simple_doc, doc_id='doc3') - self.sync(self.db1, self.db2) - db2_copy.create_doc_from_json(tests.simple_doc, doc_id='doc2') - db2_copy.create_doc_from_json(tests.simple_doc, doc_id='doc3') - self.assertRaises( - errors.InvalidTransactionId, self.sync, self.db1, db2_copy) - - -class TestDbSync(tests.TestCaseWithServer): - """Test db.sync remote sync shortcut""" - - scenarios = [ - ('py-http', { - 'make_app_with_state': make_http_app, - 'make_database_for_test': tests.make_memory_database_for_test, - }), - ('py-oauth-http', { - 'make_app_with_state': make_oauth_http_app, - 'make_database_for_test': tests.make_memory_database_for_test, - 'oauth': True - }), - ] - - oauth = False - - def do_sync(self, target_name): - if self.oauth: - path = '~/' + target_name - extra = dict(creds={'oauth': { - 'consumer_key': tests.consumer1.key, - 'consumer_secret': tests.consumer1.secret, - 'token_key': tests.token1.key, - 'token_secret': tests.token1.secret, - }}) - else: - path = target_name - extra = {} - target_url = self.getURL(path) - return self.db.sync(target_url, **extra) - - def setUp(self): - super(TestDbSync, self).setUp() - self.startServer() - self.db = self.make_database_for_test(self, 'test1') - self.db2 = self.request_state._create_database('test2.db') - - def test_db_sync(self): - doc1 = self.db.create_doc_from_json(tests.simple_doc) - doc2 = self.db2.create_doc_from_json(tests.nested_doc) - local_gen_before_sync = self.do_sync('test2.db') - gen, _, changes = self.db.whats_changed(local_gen_before_sync) - self.assertEqual(1, len(changes)) - self.assertEqual(doc2.doc_id, changes[0][0]) - self.assertEqual(1, gen - local_gen_before_sync) - self.assertGetDoc(self.db2, doc1.doc_id, doc1.rev, tests.simple_doc, - False) - self.assertGetDoc(self.db, doc2.doc_id, doc2.rev, tests.nested_doc, - False) - - def test_db_sync_autocreate(self): - doc1 = self.db.create_doc_from_json(tests.simple_doc) - local_gen_before_sync = self.do_sync('test3.db') - gen, _, changes = self.db.whats_changed(local_gen_before_sync) - self.assertEqual(0, gen - local_gen_before_sync) - db3 = self.request_state.open_database('test3.db') - gen, _, changes = db3.whats_changed() - self.assertEqual(1, len(changes)) - self.assertEqual(doc1.doc_id, changes[0][0]) - self.assertGetDoc(db3, doc1.doc_id, doc1.rev, tests.simple_doc, - False) - t_gen, _ = self.db._get_replica_gen_and_trans_id('test3.db') - s_gen, _ = db3._get_replica_gen_and_trans_id('test1') - self.assertEqual(1, t_gen) - self.assertEqual(1, s_gen) - - -class TestRemoteSyncIntegration(tests.TestCaseWithServer): - """Integration tests for the most common sync scenario local -> remote""" - - make_app_with_state = staticmethod(make_http_app) - - def setUp(self): - super(TestRemoteSyncIntegration, self).setUp() - self.startServer() - self.db1 = inmemory.InMemoryDatabase('test1') - self.db2 = self.request_state._create_database('test2') - - def test_sync_tracks_generations_incrementally(self): - doc11 = self.db1.create_doc_from_json('{"a": 1}') - doc12 = self.db1.create_doc_from_json('{"a": 2}') - doc21 = self.db2.create_doc_from_json('{"b": 1}') - doc22 = self.db2.create_doc_from_json('{"b": 2}') - #sanity - self.assertEqual(2, len(self.db1._get_transaction_log())) - self.assertEqual(2, len(self.db2._get_transaction_log())) - progress1 = [] - progress2 = [] - _do_set_replica_gen_and_trans_id = \ - self.db1._do_set_replica_gen_and_trans_id - - def set_sync_generation_witness1(other_uid, other_gen, trans_id): - progress1.append((other_uid, other_gen, - [d for d, t in - self.db1._get_transaction_log()[2:]])) - _do_set_replica_gen_and_trans_id(other_uid, other_gen, trans_id) - self.patch(self.db1, '_do_set_replica_gen_and_trans_id', - set_sync_generation_witness1) - _do_set_replica_gen_and_trans_id2 = \ - self.db2._do_set_replica_gen_and_trans_id - - def set_sync_generation_witness2(other_uid, other_gen, trans_id): - progress2.append((other_uid, other_gen, - [d for d, t in - self.db2._get_transaction_log()[2:]])) - _do_set_replica_gen_and_trans_id2(other_uid, other_gen, trans_id) - self.patch(self.db2, '_do_set_replica_gen_and_trans_id', - set_sync_generation_witness2) - - db2_url = self.getURL('test2') - self.db1.sync(db2_url) - - self.assertEqual([('test2', 1, [doc21.doc_id]), - ('test2', 2, [doc21.doc_id, doc22.doc_id]), - ('test2', 4, [doc21.doc_id, doc22.doc_id])], - progress1) - self.assertEqual([('test1', 1, [doc11.doc_id]), - ('test1', 2, [doc11.doc_id, doc12.doc_id]), - ('test1', 4, [doc11.doc_id, doc12.doc_id])], - progress2) - - -load_tests = tests.load_with_scenarios diff --git a/src/leap/soledad/tests/u1db_tests/testing-certs/Makefile b/src/leap/soledad/tests/u1db_tests/testing-certs/Makefile deleted file mode 100644 index 2385e75b..00000000 --- a/src/leap/soledad/tests/u1db_tests/testing-certs/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -CATOP=./demoCA -ORIG_CONF=/usr/lib/ssl/openssl.cnf -ELEVEN_YEARS=-days 4015 - -init: - cp $(ORIG_CONF) ca.conf - install -d $(CATOP) - install -d $(CATOP)/certs - install -d $(CATOP)/crl - install -d $(CATOP)/newcerts - install -d $(CATOP)/private - touch $(CATOP)/index.txt - echo 01>$(CATOP)/crlnumber - @echo '**** Making CA certificate ...' - openssl req -nodes -new \ - -newkey rsa -keyout $(CATOP)/private/cakey.pem \ - -out $(CATOP)/careq.pem \ - -multivalue-rdn \ - -subj "/C=UK/ST=-/O=u1db LOCAL TESTING ONLY, DO NO TRUST/CN=u1db testing CA" - openssl ca -config ./ca.conf -create_serial \ - -out $(CATOP)/cacert.pem $(ELEVEN_YEARS) -batch \ - -keyfile $(CATOP)/private/cakey.pem -selfsign \ - -extensions v3_ca -infiles $(CATOP)/careq.pem - -pems: - cp ./demoCA/cacert.pem . - openssl req -new -config ca.conf \ - -multivalue-rdn \ - -subj "/O=u1db LOCAL TESTING ONLY, DO NOT TRUST/CN=localhost" \ - -nodes -keyout testing.key -out newreq.pem $(ELEVEN_YEARS) - openssl ca -batch -config ./ca.conf $(ELEVEN_YEARS) \ - -policy policy_anything \ - -out testing.cert -infiles newreq.pem - -.PHONY: init pems diff --git a/src/leap/soledad/tests/u1db_tests/testing-certs/cacert.pem b/src/leap/soledad/tests/u1db_tests/testing-certs/cacert.pem deleted file mode 100644 index c019a730..00000000 --- a/src/leap/soledad/tests/u1db_tests/testing-certs/cacert.pem +++ /dev/null @@ -1,58 +0,0 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: - e4:de:01:76:c4:78:78:7e - Signature Algorithm: sha1WithRSAEncryption - Issuer: C=UK, ST=-, O=u1db LOCAL TESTING ONLY, DO NO TRUST, CN=u1db testing CA - Validity - Not Before: May 3 11:11:11 2012 GMT - Not After : May 1 11:11:11 2023 GMT - Subject: C=UK, ST=-, O=u1db LOCAL TESTING ONLY, DO NO TRUST, CN=u1db testing CA - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (1024 bit) - Modulus: - 00:bc:91:a5:7f:7d:37:f7:06:c7:db:5b:83:6a:6b: - 63:c3:8b:5c:f7:84:4d:97:6d:d4:be:bf:e7:79:a8: - c1:03:57:ec:90:d4:20:e7:02:95:d9:a6:49:e3:f9: - 9a:ea:37:b9:b2:02:62:ab:40:d3:42:bb:4a:4e:a2: - 47:71:0f:1d:a2:c5:94:a1:cf:35:d3:23:32:42:c0: - 1e:8d:cb:08:58:fb:8a:5c:3e:ea:eb:d5:2c:ed:d6: - aa:09:b4:b5:7d:e3:45:c9:ae:c2:82:b2:ae:c0:81: - bc:24:06:65:a9:e7:e0:61:ac:25:ee:53:d3:d7:be: - 22:f7:00:a2:ad:c6:0e:3a:39 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Subject Key Identifier: - DB:3D:93:51:6C:32:15:54:8F:10:50:FC:49:4F:36:15:28:BB:95:6D - X509v3 Authority Key Identifier: - keyid:DB:3D:93:51:6C:32:15:54:8F:10:50:FC:49:4F:36:15:28:BB:95:6D - - X509v3 Basic Constraints: - CA:TRUE - Signature Algorithm: sha1WithRSAEncryption - 72:9b:c1:f7:07:65:83:36:25:4e:01:2f:b7:4a:f2:a4:00:28: - 80:c7:56:2c:32:39:90:13:61:4b:bb:12:c5:44:9d:42:57:85: - 28:19:70:69:e1:43:c8:bd:11:f6:94:df:91:2d:c3:ea:82:8d: - b4:8f:5d:47:a3:00:99:53:29:93:27:6c:c5:da:c1:20:6f:ab: - ec:4a:be:34:f3:8f:02:e5:0c:c0:03:ac:2b:33:41:71:4f:0a: - 72:5a:b4:26:1a:7f:81:bc:c0:95:8a:06:87:a8:11:9f:5c:73: - 38:df:5a:69:40:21:29:ad:46:23:56:75:e1:e9:8b:10:18:4c: - 7b:54 ------BEGIN CERTIFICATE----- -MIICkjCCAfugAwIBAgIJAOTeAXbEeHh+MA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNV -BAYTAlVLMQowCAYDVQQIDAEtMS0wKwYDVQQKDCR1MWRiIExPQ0FMIFRFU1RJTkcg -T05MWSwgRE8gTk8gVFJVU1QxGDAWBgNVBAMMD3UxZGIgdGVzdGluZyBDQTAeFw0x -MjA1MDMxMTExMTFaFw0yMzA1MDExMTExMTFaMGIxCzAJBgNVBAYTAlVLMQowCAYD -VQQIDAEtMS0wKwYDVQQKDCR1MWRiIExPQ0FMIFRFU1RJTkcgT05MWSwgRE8gTk8g -VFJVU1QxGDAWBgNVBAMMD3UxZGIgdGVzdGluZyBDQTCBnzANBgkqhkiG9w0BAQEF -AAOBjQAwgYkCgYEAvJGlf3039wbH21uDamtjw4tc94RNl23Uvr/neajBA1fskNQg -5wKV2aZJ4/ma6je5sgJiq0DTQrtKTqJHcQ8dosWUoc810yMyQsAejcsIWPuKXD7q -69Us7daqCbS1feNFya7CgrKuwIG8JAZlqefgYawl7lPT174i9wCircYOOjkCAwEA -AaNQME4wHQYDVR0OBBYEFNs9k1FsMhVUjxBQ/ElPNhUou5VtMB8GA1UdIwQYMBaA -FNs9k1FsMhVUjxBQ/ElPNhUou5VtMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF -BQADgYEAcpvB9wdlgzYlTgEvt0rypAAogMdWLDI5kBNhS7sSxUSdQleFKBlwaeFD -yL0R9pTfkS3D6oKNtI9dR6MAmVMpkydsxdrBIG+r7Eq+NPOPAuUMwAOsKzNBcU8K -clq0Jhp/gbzAlYoGh6gRn1xzON9aaUAhKa1GI1Z14emLEBhMe1Q= ------END CERTIFICATE----- diff --git a/src/leap/soledad/tests/u1db_tests/testing-certs/testing.cert b/src/leap/soledad/tests/u1db_tests/testing-certs/testing.cert deleted file mode 100644 index 985684fb..00000000 --- a/src/leap/soledad/tests/u1db_tests/testing-certs/testing.cert +++ /dev/null @@ -1,61 +0,0 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: - e4:de:01:76:c4:78:78:7f - Signature Algorithm: sha1WithRSAEncryption - Issuer: C=UK, ST=-, O=u1db LOCAL TESTING ONLY, DO NO TRUST, CN=u1db testing CA - Validity - Not Before: May 3 11:11:14 2012 GMT - Not After : May 1 11:11:14 2023 GMT - Subject: O=u1db LOCAL TESTING ONLY, DO NOT TRUST, CN=localhost - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (1024 bit) - Modulus: - 00:c6:1d:72:d3:c5:e4:fc:d1:4c:d9:e4:08:3e:90: - 10:ce:3f:1f:87:4a:1d:4f:7f:2a:5a:52:c9:65:4f: - d9:2c:bf:69:75:18:1a:b5:c9:09:32:00:47:f5:60: - aa:c6:dd:3a:87:37:5f:16:be:de:29:b5:ea:fc:41: - 7e:eb:77:bb:df:63:c3:06:1e:ed:e9:a0:67:1a:f1: - ec:e1:9d:f7:9c:8f:1c:fa:c3:66:7b:39:dc:70:ae: - 09:1b:9c:c0:9a:c4:90:77:45:8e:39:95:a9:2f:92: - 43:bd:27:07:5a:99:51:6e:76:a0:af:dd:b1:2c:8f: - ca:8b:8c:47:0d:f6:6e:fc:69 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Basic Constraints: - CA:FALSE - Netscape Comment: - OpenSSL Generated Certificate - X509v3 Subject Key Identifier: - 1C:63:85:E1:1D:F3:89:2E:6C:4E:3F:FB:D0:10:64:5A:C1:22:6A:2A - X509v3 Authority Key Identifier: - keyid:DB:3D:93:51:6C:32:15:54:8F:10:50:FC:49:4F:36:15:28:BB:95:6D - - Signature Algorithm: sha1WithRSAEncryption - 1d:6d:3e:bd:93:fd:bd:3e:17:b8:9f:f0:99:7f:db:50:5c:b2: - 01:42:03:b5:d5:94:05:d3:f6:8e:80:82:55:47:1f:58:f2:18: - 6c:ab:ef:43:2c:2f:10:e1:7c:c4:5c:cc:ac:50:50:22:42:aa: - 35:33:f5:b9:f3:a6:66:55:d9:36:f4:f2:e4:d4:d9:b5:2c:52: - 66:d4:21:17:97:22:b8:9b:d7:0e:7c:3d:ce:85:19:ca:c4:d2: - 58:62:31:c6:18:3e:44:fc:f4:30:b6:95:87:ee:21:4a:08:f0: - af:3c:8f:c4:ba:5e:a1:5c:37:1a:7d:7b:fe:66:ae:62:50:17: - 31:ca ------BEGIN CERTIFICATE----- -MIICnzCCAgigAwIBAgIJAOTeAXbEeHh/MA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNV -BAYTAlVLMQowCAYDVQQIDAEtMS0wKwYDVQQKDCR1MWRiIExPQ0FMIFRFU1RJTkcg -T05MWSwgRE8gTk8gVFJVU1QxGDAWBgNVBAMMD3UxZGIgdGVzdGluZyBDQTAeFw0x -MjA1MDMxMTExMTRaFw0yMzA1MDExMTExMTRaMEQxLjAsBgNVBAoMJXUxZGIgTE9D -QUwgVEVTVElORyBPTkxZLCBETyBOT1QgVFJVU1QxEjAQBgNVBAMMCWxvY2FsaG9z -dDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAxh1y08Xk/NFM2eQIPpAQzj8f -h0odT38qWlLJZU/ZLL9pdRgatckJMgBH9WCqxt06hzdfFr7eKbXq/EF+63e732PD -Bh7t6aBnGvHs4Z33nI8c+sNmeznccK4JG5zAmsSQd0WOOZWpL5JDvScHWplRbnag -r92xLI/Ki4xHDfZu/GkCAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0E -HxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFBxjheEd -84kubE4/+9AQZFrBImoqMB8GA1UdIwQYMBaAFNs9k1FsMhVUjxBQ/ElPNhUou5Vt -MA0GCSqGSIb3DQEBBQUAA4GBAB1tPr2T/b0+F7if8Jl/21BcsgFCA7XVlAXT9o6A -glVHH1jyGGyr70MsLxDhfMRczKxQUCJCqjUz9bnzpmZV2Tb08uTU2bUsUmbUIReX -Irib1w58Pc6FGcrE0lhiMcYYPkT89DC2lYfuIUoI8K88j8S6XqFcNxp9e/5mrmJQ -FzHK ------END CERTIFICATE----- diff --git a/src/leap/soledad/tests/u1db_tests/testing-certs/testing.key b/src/leap/soledad/tests/u1db_tests/testing-certs/testing.key deleted file mode 100644 index d83d4920..00000000 --- a/src/leap/soledad/tests/u1db_tests/testing-certs/testing.key +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMYdctPF5PzRTNnk -CD6QEM4/H4dKHU9/KlpSyWVP2Sy/aXUYGrXJCTIAR/VgqsbdOoc3Xxa+3im16vxB -fut3u99jwwYe7emgZxrx7OGd95yPHPrDZns53HCuCRucwJrEkHdFjjmVqS+SQ70n -B1qZUW52oK/dsSyPyouMRw32bvxpAgMBAAECgYBs3lXxhjg1rhabTjIxnx19GTcM -M3Az9V+izweZQu3HJ1CeZiaXauhAr+LbNsniCkRVddotN6oCJdQB10QVxXBZc9Jz -HPJ4zxtZfRZlNMTMmG7eLWrfxpgWnb/BUjDb40yy1nhr9yhDUnI/8RoHDRHnAEHZ -/CnHGUrqcVcrY5zJAQJBAPLhBJg9W88JVmcOKdWxRgs7dLHnZb999Kv1V5mczmAi -jvGvbUmucqOqke6pTUHNYyNHqU6pySzGUi2cH+BAkFECQQDQ0VoAOysg6FVoT15v -tGh57t5sTiCZZ7PS8jwvtThsgA+vcf6c16XWzXgjGXSap4r2QDOY2rI5lsWLaQ8T -+fyZAkAfyFJRmbXp4c7srW3MCOahkaYzoZQu+syJtBFCiMJ40gzik5I5khpuUGPI -V19EvRu8AiSlppIsycb3MPb64XgBAkEAy7DrUf5le5wmc7G4NM6OeyJ+5LbxJbL6 -vnJ8My1a9LuWkVVpQCU7J+UVo2dZTuLPspW9vwTVhUeFOxAoHRxlQQJAFem93f7m -el2BkB2EFqU3onPejkZ5UrDmfmeOQR1axMQNSXqSxcJxqa16Ru1BWV2gcWRbwajQ -oc+kuJThu/r/Ug== ------END PRIVATE KEY----- diff --git a/src/leap/soledad/util.py b/src/leap/soledad/util.py deleted file mode 100644 index 4bc4d2c9..00000000 --- a/src/leap/soledad/util.py +++ /dev/null @@ -1,55 +0,0 @@ -import os -import gnupg -import re - - -class GPGWrapper(gnupg.GPG): - """ - This is a temporary class for handling GPG requests, and should be - replaced by a more general class used throughout the project. - """ - - GNUPG_HOME = os.environ['HOME'] + "/.config/leap/gnupg" - GNUPG_BINARY = "/usr/bin/gpg" # this has to be changed based on OS - - def __init__(self, gpghome=GNUPG_HOME, gpgbinary=GNUPG_BINARY): - super(GPGWrapper, self).__init__(gnupghome=gpghome, - gpgbinary=gpgbinary) - - def find_key(self, email): - """ - Find user's key based on their email. - """ - for key in self.list_keys(): - for uid in key['uids']: - if re.search(email, uid): - return key - raise LookupError("GnuPG public key for %s not found!" % email) - - def encrypt(self, data, recipient, sign=None, always_trust=True, - passphrase=None, symmetric=False): - # TODO: devise a way so we don't need to "always trust". - return super(GPGWrapper, self).encrypt(data, recipient, sign=sign, - always_trust=always_trust, - passphrase=passphrase, - symmetric=symmetric) - - def decrypt(self, data, always_trust=True, passphrase=None): - # TODO: devise a way so we don't need to "always trust". - return super(GPGWrapper, self).decrypt(data, - always_trust=always_trust, - passphrase=passphrase) - - def send_keys(self, keyserver, *keyids): - """ - Send keys to a keyserver - """ - result = self.result_map['list'](self) - gnupg.logger.debug('send_keys: %r', keyids) - data = gnupg._make_binary_stream("", self.encoding) - args = ['--keyserver', keyserver, '--send-keys'] - args.extend(keyids) - self._handle_io(args, data, result, binary=True) - gnupg.logger.debug('send_keys result: %r', result.__dict__) - data.close() - return result diff --git a/src/leap/testing/__init__.py b/src/leap/testing/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/testing/basetest.py b/src/leap/testing/basetest.py deleted file mode 100644 index 3186e1eb..00000000 --- a/src/leap/testing/basetest.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -import platform -import shutil -import tempfile - -try: - import unittest2 as unittest -except ImportError: - import unittest - -from leap.base.config import get_username, get_groupname -from leap.util.fileutil import mkdir_p, check_and_fix_urw_only - -_system = platform.system() - - -class BaseLeapTest(unittest.TestCase): - - __name__ = "leap_test" - - @classmethod - def setUpClass(cls): - cls.old_path = os.environ['PATH'] - cls.old_home = os.environ['HOME'] - cls.tempdir = tempfile.mkdtemp(prefix="leap_tests-") - cls.home = cls.tempdir - bin_tdir = os.path.join( - cls.tempdir, - 'bin') - os.environ["PATH"] = bin_tdir - os.environ["HOME"] = cls.tempdir - - @classmethod - def tearDownClass(cls): - os.environ["PATH"] = cls.old_path - os.environ["HOME"] = cls.old_home - # safety check - assert cls.tempdir.startswith('/tmp/leap_tests-') - shutil.rmtree(cls.tempdir) - - # you have to override these methods - # this way we ensure we did not put anything - # here that you can forget to call. - - def setUp(self): - raise NotImplementedError("abstract base class") - - def tearDown(self): - raise NotImplementedError("abstract base class") - - # - # helper methods - # - - def get_tempfile(self, filename): - return os.path.join(self.tempdir, filename) - - def get_username(self): - return get_username() - - def get_groupname(self): - return get_groupname() - - def _missing_test_for_plat(self, do_raise=False): - if do_raise: - raise NotImplementedError( - "This test is not implemented " - "for the running platform: %s" % - _system) - - def touch(self, filepath): - folder, filename = os.path.split(filepath) - if not os.path.isdir(folder): - mkdir_p(folder) - # XXX should move to test_basetest - self.assertTrue(os.path.isdir(folder)) - - with open(filepath, 'w') as fp: - fp.write(' ') - - # XXX should move to test_basetest - self.assertTrue(os.path.isfile(filepath)) - - def chmod600(self, filepath): - check_and_fix_urw_only(filepath) diff --git a/src/leap/testing/cacert.pem b/src/leap/testing/cacert.pem deleted file mode 100644 index 6989c480..00000000 --- a/src/leap/testing/cacert.pem +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID1TCCAr2gAwIBAgIJAOv0BS09D8byMA0GCSqGSIb3DQEBBQUAMIGAMQswCQYD -VQQGEwJVUzETMBEGA1UECAwKY3liZXJzcGFjZTEnMCUGA1UECgweTEVBUCBFbmNy -eXB0aW9uIEFjY2VzcyBQcm9qZWN0MRYwFAYDVQQDDA10ZXN0cy1sZWFwLnNlMRsw -GQYJKoZIhvcNAQkBFgxpbmZvQGxlYXAuc2UwHhcNMTIwODMxMTYyNjMwWhcNMTUw -ODMxMTYyNjMwWjCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCmN5YmVyc3BhY2Ux -JzAlBgNVBAoMHkxFQVAgRW5jcnlwdGlvbiBBY2Nlc3MgUHJvamVjdDEWMBQGA1UE -AwwNdGVzdHMtbGVhcC5zZTEbMBkGCSqGSIb3DQEJARYMaW5mb0BsZWFwLnNlMIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1pU7OU+abrUXFZwp6X0LlF0f -xQvC1Nmr5sFH7N9RTu3bdwY2t57ECP2TPkH6+x7oOvCTgAMxIE1scWEEkfgKViqW -FH/Om1UW1PMaiDYGtFuqEuxM95FvaYxp2K6rzA37WNsedA28sCYzhRD+/5HqbCNT -3rRS2cPaVO8kXI/5bgd8bUk3009pWTg4SvTtOW/9MWJbBH5f5JWmMn7Ayt6hIdT/ -E6npofEK/UCqAlEscARYFXSB/F8nK1whjo9mGFjMUd7d/25UbFHqOk4K7ishD4DH -F7LaS84rS+Sjwn3YtDdDQblGghJfz8X1AfPSGivGnvLVdkmMF9Y2hJlSQ7+C5wID -AQABo1AwTjAdBgNVHQ4EFgQUnpJEv4FnlqKbfm7mprudKdrnOAowHwYDVR0jBBgw -FoAUnpJEv4FnlqKbfm7mprudKdrnOAowDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B -AQUFAAOCAQEAGW66qwdK/ATRVZkTpI2sgi+2dWD5tY4VyZuJIrRwfXsGPeVvmdsa -zDmwW5dMkth1Of5yO6o7ijvUvfnw/UCLNLNICKZhH5G0DHstfBeFc0jnP2MqOZCp -puRGPBlO2nxUCvoGcPRUKGQK9XSYmxcmaSFyzKVDMLnmH+Lakj5vaY9a8ZAcZTz7 -T5qePxKAxg+RIlH8Ftc485QP3fhqPYPrRsL3g6peiqCvIRshoP1MSoh19boI+1uX -wHQ/NyDkL5ErKC5JCSpaeF8VG1ek570kKWQLuQAbnlXZw+Sqfu35CIdizHaYGEcx -xA8oXH4L2JaT2x9GKDSpCmB2xXy/NVamUg== ------END CERTIFICATE----- diff --git a/src/leap/testing/https_server.py b/src/leap/testing/https_server.py deleted file mode 100644 index 21191c32..00000000 --- a/src/leap/testing/https_server.py +++ /dev/null @@ -1,68 +0,0 @@ -from BaseHTTPServer import HTTPServer -import os -import ssl -import SocketServer -import threading -import unittest - -_where = os.path.split(__file__)[0] - - -def where(filename): - return os.path.join(_where, filename) - - -class HTTPSServer(HTTPServer): - def server_bind(self): - SocketServer.TCPServer.server_bind(self) - self.socket = ssl.wrap_socket( - self.socket, server_side=True, - certfile=where("leaptestscert.pem"), - keyfile=where("leaptestskey.pem"), - ca_certs=where("cacert.pem"), - ssl_version=ssl.PROTOCOL_SSLv23) - - -class TestServerThread(threading.Thread): - def __init__(self, test_object, request_handler): - threading.Thread.__init__(self) - self.request_handler = request_handler - self.test_object = test_object - - def run(self): - self.server = HTTPSServer(('localhost', 0), self.request_handler) - host, port = self.server.socket.getsockname() - self.test_object.HOST, self.test_object.PORT = host, port - self.test_object.server_started.set() - self.test_object = None - try: - self.server.serve_forever(0.05) - finally: - self.server.server_close() - - def stop(self): - self.server.shutdown() - - -class BaseHTTPSServerTestCase(unittest.TestCase): - """ - derived classes need to implement a request_handler - """ - def setUp(self): - self.server_started = threading.Event() - self.thread = TestServerThread(self, self.request_handler) - self.thread.start() - self.server_started.wait() - - def tearDown(self): - self.thread.stop() - - def get_server(self): - host, port = self.HOST, self.PORT - if host == "127.0.0.1": - host = "localhost" - return "%s:%s" % (host, port) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/testing/leaptestscert.pem b/src/leap/testing/leaptestscert.pem deleted file mode 100644 index 65596b1a..00000000 --- a/src/leap/testing/leaptestscert.pem +++ /dev/null @@ -1,84 +0,0 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: - eb:f4:05:2d:3d:0f:c6:f3 - Signature Algorithm: sha1WithRSAEncryption - Issuer: C=US, ST=cyberspace, O=LEAP Encryption Access Project, CN=tests-leap.se/emailAddress=info@leap.se - Validity - Not Before: Aug 31 16:30:17 2012 GMT - Not After : Aug 31 16:30:17 2013 GMT - Subject: C=US, ST=cyberspace, L=net, O=LEAP Encryption Access Project, CN=localhost/emailAddress=info@leap.se - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (2048 bit) - Modulus: - 00:bc:f1:c4:05:ce:4b:d5:9b:9a:fa:c1:a5:0c:89: - 15:7e:05:69:b6:a4:62:38:3a:d6:14:4a:36:aa:3c: - 31:70:54:2e:bf:7d:05:19:ad:7b:0c:a9:a6:7d:46: - be:83:62:cb:ea:b9:48:6c:7d:78:a0:10:0b:ad:8a: - 74:7a:b8:ff:32:85:64:36:90:dc:38:dd:90:6e:07: - 82:70:ae:5f:4e:1f:f4:46:98:f3:98:b4:fa:08:65: - bf:d6:ec:a9:ba:7e:a8:f0:40:a2:d0:1a:cb:e6:fc: - 95:c5:54:63:92:5b:b8:0a:36:cc:26:d3:2b:ad:16: - ff:49:53:f4:65:7c:64:27:9a:f5:12:75:11:a5:0c: - 5a:ea:1e:e4:31:f3:a6:2b:db:0e:4a:5d:aa:47:3a: - f0:5e:2a:d5:6f:74:b6:f8:bc:9a:73:d0:fa:8a:be: - a8:69:47:9b:07:45:d9:b5:cd:1c:9b:c5:41:9a:65: - cc:99:a0:bd:bf:b5:e8:9f:66:5f:69:c9:6d:c8:68: - 50:68:74:ae:8e:12:7e:9c:24:4f:dc:05:61:b7:8a: - 6d:2a:95:43:d9:3f:fe:d8:c9:a7:ae:63:cd:30:d5: - 95:84:18:2d:12:b5:2d:a6:fe:37:dd:74:b8:f8:a5: - 59:18:8f:ca:f7:ae:63:0d:9d:66:51:7d:9c:40:48: - 9b:a1 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Basic Constraints: - CA:FALSE - Netscape Comment: - OpenSSL Generated Certificate - X509v3 Subject Key Identifier: - B2:50:B4:C6:38:8F:BA:C4:3B:69:4C:6B:45:7C:CF:08:48:36:02:E0 - X509v3 Authority Key Identifier: - keyid:9E:92:44:BF:81:67:96:A2:9B:7E:6E:E6:A6:BB:9D:29:DA:E7:38:0A - - Signature Algorithm: sha1WithRSAEncryption - aa:ab:d4:27:e3:cb:42:05:55:fd:24:b3:e5:55:7d:fb:ce:6c: - ff:c7:96:f0:7d:30:a1:53:4a:04:eb:a4:24:5e:96:ee:65:ef: - e5:aa:08:47:9d:aa:95:2a:bb:6a:28:9f:51:62:63:d9:7d:1a: - 81:a0:72:f7:9f:33:6b:3b:f4:dc:85:cd:2a:ee:83:a9:93:3d: - 75:53:91:fa:0b:1b:10:83:11:2c:03:4e:ac:bf:c3:e6:25:74: - 9f:14:13:4a:43:66:c2:d7:1c:6c:94:3e:a6:f3:a5:bd:01:2c: - 9f:20:29:2e:62:82:12:d8:8b:70:1b:88:2b:18:68:5a:45:80: - 46:2a:6a:d5:df:1f:d3:e8:57:39:0a:be:1a:d8:b0:3e:e5:b6: - c3:69:b7:5e:c0:7b:b3:a8:a6:78:ee:0a:3d:a0:74:40:fb:42: - 9f:f4:98:7f:47:cc:15:28:eb:b1:95:77:82:a8:65:9b:46:c3: - 4f:f9:f4:72:be:bd:24:28:5c:0d:b3:89:e4:13:71:c8:a7:54: - 1b:26:15:f3:c1:b2:a9:13:77:54:c2:b9:b0:c7:24:39:00:4c: - 1a:a7:9b:e7:ad:4a:3a:32:c2:81:0d:13:2d:27:ea:98:00:a9: - 0e:9e:38:3b:8f:80:34:17:17:3d:49:7e:f4:a5:19:05:28:08: - 7d:de:d3:1f ------BEGIN CERTIFICATE----- -MIIECjCCAvKgAwIBAgIJAOv0BS09D8bzMA0GCSqGSIb3DQEBBQUAMIGAMQswCQYD -VQQGEwJVUzETMBEGA1UECAwKY3liZXJzcGFjZTEnMCUGA1UECgweTEVBUCBFbmNy -eXB0aW9uIEFjY2VzcyBQcm9qZWN0MRYwFAYDVQQDDA10ZXN0cy1sZWFwLnNlMRsw -GQYJKoZIhvcNAQkBFgxpbmZvQGxlYXAuc2UwHhcNMTIwODMxMTYzMDE3WhcNMTMw -ODMxMTYzMDE3WjCBijELMAkGA1UEBhMCVVMxEzARBgNVBAgMCmN5YmVyc3BhY2Ux -DDAKBgNVBAcMA25ldDEnMCUGA1UECgweTEVBUCBFbmNyeXB0aW9uIEFjY2VzcyBQ -cm9qZWN0MRIwEAYDVQQDDAlsb2NhbGhvc3QxGzAZBgkqhkiG9w0BCQEWDGluZm9A -bGVhcC5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALzxxAXOS9Wb -mvrBpQyJFX4FabakYjg61hRKNqo8MXBULr99BRmtewyppn1GvoNiy+q5SGx9eKAQ -C62KdHq4/zKFZDaQ3DjdkG4HgnCuX04f9EaY85i0+ghlv9bsqbp+qPBAotAay+b8 -lcVUY5JbuAo2zCbTK60W/0lT9GV8ZCea9RJ1EaUMWuoe5DHzpivbDkpdqkc68F4q -1W90tvi8mnPQ+oq+qGlHmwdF2bXNHJvFQZplzJmgvb+16J9mX2nJbchoUGh0ro4S -fpwkT9wFYbeKbSqVQ9k//tjJp65jzTDVlYQYLRK1Lab+N910uPilWRiPyveuYw2d -ZlF9nEBIm6ECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3Bl -blNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFLJQtMY4j7rEO2lM -a0V8zwhINgLgMB8GA1UdIwQYMBaAFJ6SRL+BZ5aim35u5qa7nSna5zgKMA0GCSqG -SIb3DQEBBQUAA4IBAQCqq9Qn48tCBVX9JLPlVX37zmz/x5bwfTChU0oE66QkXpbu -Ze/lqghHnaqVKrtqKJ9RYmPZfRqBoHL3nzNrO/Tchc0q7oOpkz11U5H6CxsQgxEs -A06sv8PmJXSfFBNKQ2bC1xxslD6m86W9ASyfICkuYoIS2ItwG4grGGhaRYBGKmrV -3x/T6Fc5Cr4a2LA+5bbDabdewHuzqKZ47go9oHRA+0Kf9Jh/R8wVKOuxlXeCqGWb -RsNP+fRyvr0kKFwNs4nkE3HIp1QbJhXzwbKpE3dUwrmwxyQ5AEwap5vnrUo6MsKB -DRMtJ+qYAKkOnjg7j4A0Fxc9SX70pRkFKAh93tMf ------END CERTIFICATE----- diff --git a/src/leap/testing/leaptestskey.pem b/src/leap/testing/leaptestskey.pem deleted file mode 100644 index fe6291a1..00000000 --- a/src/leap/testing/leaptestskey.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAvPHEBc5L1Zua+sGlDIkVfgVptqRiODrWFEo2qjwxcFQuv30F -Ga17DKmmfUa+g2LL6rlIbH14oBALrYp0erj/MoVkNpDcON2QbgeCcK5fTh/0Rpjz -mLT6CGW/1uypun6o8ECi0BrL5vyVxVRjklu4CjbMJtMrrRb/SVP0ZXxkJ5r1EnUR -pQxa6h7kMfOmK9sOSl2qRzrwXirVb3S2+Lyac9D6ir6oaUebB0XZtc0cm8VBmmXM -maC9v7Xon2ZfacltyGhQaHSujhJ+nCRP3AVht4ptKpVD2T/+2MmnrmPNMNWVhBgt -ErUtpv433XS4+KVZGI/K965jDZ1mUX2cQEiboQIDAQABAoIBAQCh/+yhSbrtoCgm -PegEsnix/3QfPBxWt+Obq/HozglZlWQrnMbFuF+bgM4V9ZUdU5UhYNF+66mEG53X -orGyE3IDYCmHO3cGbroKDPhDIs7mTjGEYlniIbGLh6oPXgU8uKKis9ik84TGPOUx -NuTUtT07zLYHx+FX3DLwLUKLzTaWWSRgA7nxNwCY8aPqDxCkXEyZHvSlm9KYZnhe -nVevycoHR+chxL6X/ebbBt2FKR7tl4328mlDXvMXr0vahPH94CuXEvfTj+f6ZxZF -OctdikyRfd8O3ebrUw0XjafPYyTsDMH0/rQovEBVlecEHqh6Z9dBFlogRq5DSun9 -jem4bBXRAoGBAPGPi4g21pTQPqTFxpqea8TsPqIfo3csfMDPdzT246MxzALHqCfG -yZi4g2JYJrReSWHulZDORO5skSKNEb5VTA/3xFhKLt8CULZOakKBDLkzRXlnDFXg -Jsu9vtjDWjQcJsdsRx1tc5V6s+hmel70aaUu/maUlEYZnyIXaTe+1SB1AoGBAMg9 -EMEO5YN52pOI5qPH8j7uyVKtZWKRiR6jb5KA5TxWqZalSdPV6YwDqV/e+HjWrZNw -kSEFONY0seKpIHwXchx91aym7rDHUgOoBQfCWufRMYvRXLhfOTBu4X+U52++i8wt -FvKgh6eSmc7VayAaDfHp7yfrIfS03IiN0T35mGj9AoGAPCoXg7a83VW8tId5/trE -VsjMlM6yhSU0cUV7GFsBuYzWlj6qODX/0iTqvFzeTwBI4LZu1CE78/Jgd62RJMnT -5wo8Ag1//RVziuSe/K9tvtbxT9qFrQHmR8qbtRt65Q257uOeFstDBZEJLDIR+oJ/ -qZ+5x0zsXUVWaERSdYr3RF0CgYEApKDgN3oB5Ti4Jnh1984aMver+heptYKmU9RX -lQH4dsVhpQO8UTgcTgtso+/0JZWLHB9+ksFyW1rzrcETfjLglOA4XzzYHeuiWHM5 -v4lhqBpsO+Ij80oHAPUI3RYVud/VnEauCUlGftWfM1hwPPJu6KhHAnDleAWDE5pV -oDinwBkCgYEAnn/OceaqA2fNYp1IRegbFzpewjUlHLq3bXiCIVhO7W/HqsdfUxjE -VVdjEno/pAG7ZCO5j8u+rLkG2ZIVY3qsUENUiXz52Q08qEltgM8nfirK7vIQkfd9 -YISRE3QHYJd+ArY4v+7rNeF1O5eIEyzPAbvG5raeZFcZ6POxy66uWKo= ------END RSA PRIVATE KEY----- diff --git a/src/leap/testing/pyqt.py b/src/leap/testing/pyqt.py deleted file mode 100644 index 6edaf059..00000000 --- a/src/leap/testing/pyqt.py +++ /dev/null @@ -1,52 +0,0 @@ -from PyQt4 import QtCore - -_oldConnect = QtCore.QObject.connect -_oldDisconnect = QtCore.QObject.disconnect -_oldEmit = QtCore.QObject.emit - - -def _wrapConnect(callableObject): - """ - Returns a wrapped call to the old version of QtCore.QObject.connect - """ - @staticmethod - def call(*args): - callableObject(*args) - _oldConnect(*args) - return call - - -def _wrapDisconnect(callableObject): - """ - Returns a wrapped call to the old version of QtCore.QObject.disconnect - """ - @staticmethod - def call(*args): - callableObject(*args) - _oldDisconnect(*args) - return call - - -def enableSignalDebugging(**kwargs): - """ - Call this to enable Qt Signal debugging. This will trap all - connect, and disconnect calls. - """ - - f = lambda *args: None - connectCall = kwargs.get('connectCall', f) - disconnectCall = kwargs.get('disconnectCall', f) - emitCall = kwargs.get('emitCall', f) - - def printIt(msg): - def call(*args): - print msg, args - return call - QtCore.QObject.connect = _wrapConnect(connectCall) - QtCore.QObject.disconnect = _wrapDisconnect(disconnectCall) - - def new_emit(self, *args): - emitCall(self, *args) - _oldEmit(self, *args) - - QtCore.QObject.emit = new_emit diff --git a/src/leap/testing/qunittest.py b/src/leap/testing/qunittest.py deleted file mode 100644 index b89ccec3..00000000 --- a/src/leap/testing/qunittest.py +++ /dev/null @@ -1,302 +0,0 @@ -# -*- coding: utf-8 -*- - -# **qunittest** is an standard Python `unittest` enhancement for PyQt4, -# allowing -# you to test asynchronous code using standard synchronous testing facility. -# -# The source for `qunittest` is available on [GitHub][gh], and released under -# the MIT license. -# -# Slightly modified by The Leap Project. - -### Prerequisites - -# Import unittest2 or unittest -try: - import unittest2 as unittest -except ImportError: - import unittest - -# ... and some standard Python libraries -import sys -import functools -import contextlib -import re - -# ... and several PyQt classes -from PyQt4.QtCore import QTimer -from PyQt4.QtTest import QTest -from PyQt4 import QtGui - -### The code - - -# Override standard main method, by invoking it inside PyQt event loop - -def main(*args, **kwargs): - qapplication = QtGui.QApplication(sys.argv) - - QTimer.singleShot(0, unittest.main(*args, **kwargs)) - qapplication.exec_() - -""" -This main substitute does not integrate with unittest. - -Note about mixing the event loop and unittests: - -Unittest will fail if we keep more than one reference to a QApplication. -(pyqt expects to be and only one). -So, for the things that need a QApplication to exist, do something like: - - self.app = QApplication() - QtGui.qApp = self.app - -in the class setUp, and:: - - QtGui.qApp = None - self.app = None - -in the class tearDown. - -For some explanation about this, see - http://stuvel.eu/blog/127/multiple-instances-of-qapplication-in-one-process -and - http://www.riverbankcomputing.com/pipermail/pyqt/2010-September/027705.html -""" - - -# Helper returning the name of a given signal - -def _signal_name(signal): - s = repr(signal) - name_re = "signal (\w+) of (\w+)" - match = re.search(name_re, s, re.I) - if not match: - return "??" - return "%s#%s" % (match.group(2), match.group(1)) - - -class _SignalConnector(object): - """ Encapsulates signal assertion testing """ - def __init__(self, test, signal, callable_): - self.test = test - self.callable_ = callable_ - self.called_with = None - self.emited = False - self.signal = signal - self._asserted = False - - signal.connect(self.on_signal_emited) - - # Store given parameters and mark signal as `emited` - def on_signal_emited(self, *args, **kwargs): - self.called_with = (args, kwargs) - self.emited = True - - def assertEmission(self): - # Assert once wheter signal was emited or not - was_asserted = self._asserted - self._asserted = True - - if not was_asserted: - if not self.emited: - self.test.fail( - "signal %s not emited" % (_signal_name(self.signal))) - - # Call given callable is necessary - if self.callable_: - args, kwargs = self.called_with - self.callable_(*args, **kwargs) - - def __enter__(self): - # Assert emission when context is entered - self.assertEmission() - return self.called_with - - def __exit__(self, *_): - return False - -### Unit Testing - -# `qunittest` does not force much abould how test should look - it just adds -# several helpers for asynchronous code testing. -# -# Common test case may look like this: -# -# import qunittest -# from calculator import Calculator -# -# class TestCalculator(qunittest.TestCase): -# def setUp(self): -# self.calc = Calculator() -# -# def test_should_add_two_numbers_synchronously(self): -# # given -# a, b = 2, 3 -# -# # when -# r = self.calc.add(a, b) -# -# # then -# self.assertEqual(5, r) -# -# def test_should_calculate_factorial_in_background(self): -# # given -# -# # when -# self.calc.factorial(20) -# -# # then -# self.assertEmited(self.calc.done) with (args, kwargs): -# self.assertEqual([2432902008176640000], args) -# -# if __name__ == "__main__": -# main() -# -# Test can be run by typing: -# -# python test_calculator.py -# -# Automatic test discovery is not supported now, because testing PyQt needs -# an instance of `QApplication` and its `exec_` method is blocking. -# - - -### TestCase class - -class TestCase(unittest.TestCase): - """ - Extends standard `unittest.TestCase` with several PyQt4 testing features - useful for asynchronous testing. - """ - def __init__(self, *args, **kwargs): - super(TestCase, self).__init__(*args, **kwargs) - - self._clearSignalConnectors() - self._succeeded = False - self.addCleanup(self._clearSignalConnectors) - self.tearDown = self._decorateTearDown(self.tearDown) - - ### Protected methods - - def _clearSignalConnectors(self): - self._connectedSignals = [] - - def _decorateTearDown(self, tearDown): - @functools.wraps(tearDown) - def decorator(): - self._ensureEmitedSignals() - return tearDown() - return decorator - - def _ensureEmitedSignals(self): - """ - Checks if signals were acually emited. Raises AssertionError if no. - """ - # TODO: add information about line - for signal in self._connectedSignals: - signal.assertEmission() - - ### Assertions - - def assertEmited(self, signal, callable_=None, timeout=1): - """ - Asserts if given `signal` was emited. Waits 1 second by default, - before asserts signal emission. - - If `callable_` is given, it should be a function which takes two - arguments: `args` and `kwargs`. It will be called after blocking - operation or when assertion about signal emission is made and - signal was emited. - - When timeout is not `False`, method call is blocking, and ends - after `timeout` seconds. After that time, it validates wether - signal was emited. - - When timeout is `False`, method is non blocking, and test should wait - for signals afterwards. Otherwise, at the end of the test, all - signal emissions are checked if appeared. - - Function returns context, which yields to list of parameters given - to signal. It can be useful for testing given parameters. Following - code: - - with self.assertEmited(widget.signal) as (args, kwargs): - self.assertEqual(1, len(args)) - self.assertEqual("Hello World!", args[0]) - - will wait 1 second and test for correct parameters, is signal was - emtied. - - Note that code: - - with self.assertEmited(widget.signal, timeout=False) as (a, k): - # Will not be invoked - - will always fail since signal cannot be emited in the time of its - connection - code inside the context will not be invoked at all. - """ - - connector = _SignalConnector(self, signal, callable_) - self._connectedSignals.append(connector) - if timeout: - self.waitFor(timeout) - connector.assertEmission() - - return connector - - ### Helper methods - - @contextlib.contextmanager - def invokeAfter(self, seconds, callable_=None): - """ - Waits given amount of time and executes the context. - - If `callable_` is given, executes it, instead of context. - """ - self.waitFor(seconds) - if callable_: - callable_() - else: - yield - - def waitFor(self, seconds): - """ - Waits given amount of time. - - self.widget.loadImage(url) - self.waitFor(seconds=10) - """ - QTest.qWait(seconds * 1000) - - def succeed(self, bool_=True): - """ Marks test as suceeded for next `failAfter()` invocation. """ - self._succeeded = self._succeeded or bool_ - - def failAfter(self, seconds, message=None): - """ - Waits given amount of time, and fails the test if `succeed(bool)` - is not called - in most common case, `succeed(bool)` should be called - asynchronously (in signal handler): - - self.widget.signal.connect(lambda: self.succeed()) - self.failAfter(1, "signal not emited?") - - After invocation, test is no longer consider as succeeded. - """ - self.waitFor(seconds) - if not self._succeeded: - self.fail(message) - - self._succeeded = False - -### Credits -# -# * **Who is responsible:** [Dawid Fatyga][df] -# * **Source:** [GitHub][gh] -# * **Doc. generator:** [rocco][ro] -# -# [gh]: https://www.github.com/dejw/qunittest -# [df]: https://github.com/dejw -# [ro]: http://rtomayko.github.com/rocco/ -# diff --git a/src/leap/testing/test_basetest.py b/src/leap/testing/test_basetest.py deleted file mode 100644 index 14d8f8a3..00000000 --- a/src/leap/testing/test_basetest.py +++ /dev/null @@ -1,91 +0,0 @@ -"""becase it's oh so meta""" -try: - import unittest2 as unittest -except ImportError: - import unittest - -import os -import StringIO - -from leap.testing.basetest import BaseLeapTest - -# global for tempdir checking -_tempdir = None - - -class _TestCaseRunner(object): - def run_testcase(self, testcase=None): - if not testcase: - return None - loader = unittest.TestLoader() - suite = loader.loadTestsFromTestCase(testcase) - - # Create runner, and run testcase - io = StringIO.StringIO() - runner = unittest.TextTestRunner(stream=io) - results = runner.run(suite) - return results - - -class TestAbstractBaseLeapTest(unittest.TestCase, _TestCaseRunner): - - def test_abstract_base_class(self): - class _BaseTest(BaseLeapTest): - def test_dummy_method(self): - pass - - def test_tautology(self): - assert True - - results = self.run_testcase(_BaseTest) - - # should be 2 errors: NotImplemented - # raised for setUp/tearDown - self.assertEquals(results.testsRun, 2) - self.assertEquals(len(results.failures), 0) - self.assertEquals(len(results.errors), 2) - - -class TestInitBaseLeapTest(BaseLeapTest): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_path_is_changed(self): - os_path = os.environ['PATH'] - self.assertTrue(os_path.startswith(self.tempdir)) - - def test_old_path_is_saved(self): - self.assertTrue(len(self.old_path) > 1) - - -class TestCleanedBaseLeapTest(unittest.TestCase, _TestCaseRunner): - - def test_tempdir_is_cleaned_after_tests(self): - class _BaseTest(BaseLeapTest): - def setUp(self): - global _tempdir - _tempdir = self.tempdir - - def tearDown(self): - pass - - def test_tempdir_created(self): - self.assertTrue(os.path.isdir(self.tempdir)) - - def test_tempdir_created_on_setupclass(self): - self.assertEqual(_tempdir, self.tempdir) - - results = self.run_testcase(_BaseTest) - self.assertEquals(results.testsRun, 2) - self.assertEquals(len(results.failures), 0) - self.assertEquals(len(results.errors), 0) - - # did we cleaned the tempdir? - self.assertFalse(os.path.isdir(_tempdir)) - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/util/__init__.py b/src/leap/util/__init__.py deleted file mode 100644 index a70a9a8b..00000000 --- a/src/leap/util/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -import logging -logger = logging.getLogger(__name__) - -try: - import pygeoip - HAS_GEOIP = True -except ImportError: - logger.debug('PyGeoIP not found. Disabled Geo support.') - HAS_GEOIP = False diff --git a/src/leap/util/certs.py b/src/leap/util/certs.py deleted file mode 100644 index f0f790e9..00000000 --- a/src/leap/util/certs.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -import logging - -logger = logging.getLogger(__name__) - - -def get_mac_cabundle(): - # hackaround bundle error - # XXX this needs a better fix! - f = os.path.split(__file__)[0] - sep = os.path.sep - f_ = sep.join(f.split(sep)[:-2]) - verify = os.path.join(f_, 'cacert.pem') - #logger.error('VERIFY PATH = %s' % verify) - exists = os.path.isfile(verify) - #logger.error('do exist? %s', exists) - if exists: - return verify diff --git a/src/leap/util/coroutines.py b/src/leap/util/coroutines.py deleted file mode 100644 index 0657fc04..00000000 --- a/src/leap/util/coroutines.py +++ /dev/null @@ -1,109 +0,0 @@ -# the problem of watching a stdout pipe from -# openvpn binary: using subprocess and coroutines -# acting as event consumers - -from __future__ import division, print_function - -import logging -from subprocess import PIPE, Popen -import sys -from threading import Thread - -logger = logging.getLogger(__name__) - -ON_POSIX = 'posix' in sys.builtin_module_names - - -# -# Coroutines goodies -# - -def coroutine(func): - def start(*args, **kwargs): - cr = func(*args, **kwargs) - cr.next() - return cr - return start - - -@coroutine -def process_events(callback): - """ - coroutine loop that receives - events sent and dispatch the callback. - :param callback: callback to be called\ -for each event - :type callback: callable - """ - try: - while True: - m = (yield) - if callable(callback): - callback(m) - else: - logger.debug('not a callable passed') - except GeneratorExit: - return - -# -# Threads -# - - -def launch_thread(target, args): - """ - launch and demonize thread. - :param target: target function that will run in thread - :type target: function - :param args: args to be passed to thread - :type args: list - """ - t = Thread(target=target, - args=args) - t.daemon = True - t.start() - return t - - -def watch_output(out, observers): - """ - initializes dict of observer coroutines - and pushes lines to each of them as they are received - from the watched output. - :param out: stdout of a process. - :type out: fd - :param observers: tuple of coroutines to send data\ -for each event - :type observers: tuple - """ - observer_dict = dict(((observer, process_events(observer)) - for observer in observers)) - for line in iter(out.readline, b''): - for obs in observer_dict: - observer_dict[obs].send(line) - out.close() - - -def spawn_and_watch_process(command, args, observers=None): - """ - spawns a subprocess with command, args, and launch - a watcher thread. - :param command: command to be executed in the subprocess - :type command: str - :param args: arguments - :type args: list - :param observers: tuple of observer functions to be called \ -for each line in the subprocess output. - :type observers: tuple - :return: a tuple containing the child process instance, and watcher_thread, - :rtype: (Subprocess, Thread) - """ - subp = Popen([command] + args, - stdout=PIPE, - stderr=PIPE, - bufsize=1, - close_fds=ON_POSIX) - watcher = launch_thread( - watch_output, - (subp.stdout, observers)) - return subp, watcher diff --git a/src/leap/util/dicts.py b/src/leap/util/dicts.py deleted file mode 100644 index 001ca96b..00000000 --- a/src/leap/util/dicts.py +++ /dev/null @@ -1,268 +0,0 @@ -# Backport of OrderedDict() class that runs -# on Python 2.4, 2.5, 2.6, 2.7 and pypy. -# Passes Python2.7's test suite and incorporates all the latest updates. - -try: - from thread import get_ident as _get_ident -except ImportError: - from dummy_thread import get_ident as _get_ident - -try: - from _abcoll import KeysView, ValuesView, ItemsView -except ImportError: - pass - - -class OrderedDict(dict): - 'Dictionary that remembers insertion order' - # An inherited dict maps keys to values. - # The inherited dict provides __getitem__, __len__, __contains__, and get. - # The remaining methods are order-aware. - # Big-O running times for all methods are the same as for regular - # dictionaries. - - # The internal self.__map dictionary maps keys to links in a doubly - # linked list. - # The circular doubly linked list starts and ends with a sentinel element. - # The sentinel element never gets deleted (this simplifies the algorithm). - # Each link is stored as a list of length three: [PREV, NEXT, KEY]. - - def __init__(self, *args, **kwds): - '''Initialize an ordered dictionary. Signature is the same as for - regular dictionaries, but keyword arguments are not recommended - because their insertion order is arbitrary. - - ''' - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__root - except AttributeError: - self.__root = root = [] # sentinel node - root[:] = [root, root, None] - self.__map = {} - self.__update(*args, **kwds) - - def __setitem__(self, key, value, dict_setitem=dict.__setitem__): - 'od.__setitem__(i, y) <==> od[i]=y' - # Setting a new item creates a new link which goes at the end - # of the linked list, and the inherited dictionary is updated - # with the new key/value pair. - if key not in self: - root = self.__root - last = root[0] - last[1] = root[0] = self.__map[key] = [last, root, key] - dict_setitem(self, key, value) - - def __delitem__(self, key, dict_delitem=dict.__delitem__): - 'od.__delitem__(y) <==> del od[y]' - # Deleting an existing item uses self.__map to find the link which is - # then removed by updating the links in the predecessor and successor - # nodes. - dict_delitem(self, key) - link_prev, link_next, key = self.__map.pop(key) - link_prev[1] = link_next - link_next[0] = link_prev - - def __iter__(self): - 'od.__iter__() <==> iter(od)' - root = self.__root - curr = root[1] - while curr is not root: - yield curr[2] - curr = curr[1] - - def __reversed__(self): - 'od.__reversed__() <==> reversed(od)' - root = self.__root - curr = root[0] - while curr is not root: - yield curr[2] - curr = curr[0] - - def clear(self): - 'od.clear() -> None. Remove all items from od.' - try: - for node in self.__map.itervalues(): - del node[:] - root = self.__root - root[:] = [root, root, None] - self.__map.clear() - except AttributeError: - pass - dict.clear(self) - - def popitem(self, last=True): - '''od.popitem() -> (k, v), return and remove a (key, value) pair. - Pairs are returned in LIFO order if last is true or FIFO order if - false. - ''' - if not self: - raise KeyError('dictionary is empty') - root = self.__root - if last: - link = root[0] - link_prev = link[0] - link_prev[1] = root - root[0] = link_prev - else: - link = root[1] - link_next = link[1] - root[1] = link_next - link_next[0] = root - key = link[2] - del self.__map[key] - value = dict.pop(self, key) - return key, value - - # -- the following methods do not depend on the internal structure -- - - def keys(self): - 'od.keys() -> list of keys in od' - return list(self) - - def values(self): - 'od.values() -> list of values in od' - return [self[key] for key in self] - - def items(self): - 'od.items() -> list of (key, value) pairs in od' - return [(key, self[key]) for key in self] - - def iterkeys(self): - 'od.iterkeys() -> an iterator over the keys in od' - return iter(self) - - def itervalues(self): - 'od.itervalues -> an iterator over the values in od' - for k in self: - yield self[k] - - def iteritems(self): - 'od.iteritems -> an iterator over the (key, value) items in od' - for k in self: - yield (k, self[k]) - - def update(*args, **kwds): - '''od.update(E, **F) -> None. Update od from dict/iterable E and F. - - If E is a dict instance, does: for k in E: od[k] = E[k] - If E has a .keys() method, does: for k in E.keys(): - od[k] = E[k] - Or if E is an iterable of items, does: for k, v in E: od[k] = v - In either case, this is followed by: for k, v in F.items(): - od[k] = v - ''' - - if len(args) > 2: - raise TypeError('update() takes at most 2 positional ' - 'arguments (%d given)' % (len(args),)) - elif not args: - raise TypeError('update() takes at least 1 argument (0 given)') - self = args[0] - # Make progressively weaker assumptions about "other" - other = () - if len(args) == 2: - other = args[1] - if isinstance(other, dict): - for key in other: - self[key] = other[key] - elif hasattr(other, 'keys'): - for key in other.keys(): - self[key] = other[key] - else: - for key, value in other: - self[key] = value - for key, value in kwds.items(): - self[key] = value - - __update = update # let subclasses override update - # without breaking __init__ - - __marker = object() - - def pop(self, key, default=__marker): - '''od.pop(k[,d]) -> v - remove specified key and return the corresponding value. - If key is not found, d is returned if given, - otherwise KeyError is raised. - - ''' - if key in self: - result = self[key] - del self[key] - return result - if default is self.__marker: - raise KeyError(key) - return default - - def setdefault(self, key, default=None): - 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' - if key in self: - return self[key] - self[key] = default - return default - - def __repr__(self, _repr_running={}): - 'od.__repr__() <==> repr(od)' - call_key = id(self), _get_ident() - if call_key in _repr_running: - return '...' - _repr_running[call_key] = 1 - try: - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, self.items()) - finally: - del _repr_running[call_key] - - def __reduce__(self): - 'Return state information for pickling' - items = [[k, self[k]] for k in self] - inst_dict = vars(self).copy() - for k in vars(OrderedDict()): - inst_dict.pop(k, None) - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) - - def copy(self): - 'od.copy() -> a shallow copy of od' - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S - and values equal to v (which defaults to None). - - ''' - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - '''od.__eq__(y) <==> od==y. - Comparison to another OD is order-sensitive - while comparison to a regular mapping is order-insensitive. - ''' - if isinstance(other, OrderedDict): - return len(self) == len(other) and self.items() == other.items() - return dict.__eq__(self, other) - - def __ne__(self, other): - return not self == other - - # -- the following methods are only used in Python 2.7 -- - - def viewkeys(self): - "od.viewkeys() -> a set-like object providing a view on od's keys" - return KeysView(self) - - def viewvalues(self): - "od.viewvalues() -> an object providing a view on od's values" - return ValuesView(self) - - def viewitems(self): - "od.viewitems() -> a set-like object providing a view on od's items" - return ItemsView(self) diff --git a/src/leap/util/fileutil.py b/src/leap/util/fileutil.py deleted file mode 100644 index 820ffe46..00000000 --- a/src/leap/util/fileutil.py +++ /dev/null @@ -1,120 +0,0 @@ -import errno -from itertools import chain -import logging -import os -import platform -import stat - - -logger = logging.getLogger() - - -def is_user_executable(fpath): - st = os.stat(fpath) - return bool(st.st_mode & stat.S_IXUSR) - - -def extend_path(): - ourplatform = platform.system() - if ourplatform == "Linux": - return "/usr/local/sbin:/usr/sbin" - # XXX add mac / win extended search paths? - - -def which(program, path=None): - """ - an implementation of which - that extends the path with - other locations, like sbin - (f.i., openvpn binary is likely to be there) - @param program: a string representing the binary we're looking for. - """ - def is_exe(fpath): - """ - check that path exists, - it's a file, - and is executable by the owner - """ - # we would check for access, - # but it's likely that we're - # using uid 0 + polkitd - - return os.path.isfile(fpath)\ - and is_user_executable(fpath) - - def ext_candidates(fpath): - yield fpath - for ext in os.environ.get("PATHEXT", "").split(os.pathsep): - yield fpath + ext - - def iter_path(pathset): - """ - returns iterator with - full path for a given path list - and the current target bin. - """ - for path in pathset.split(os.pathsep): - exe_file = os.path.join(path, program) - #print 'file=%s' % exe_file - for candidate in ext_candidates(exe_file): - if is_exe(candidate): - yield candidate - - fpath, fname = os.path.split(program) - if fpath: - if is_exe(program): - return program - else: - # extended iterator - # with extra path - if path is None: - path = os.environ['PATH'] - extended_path = chain( - iter_path(path), - iter_path(extend_path())) - for candidate in extended_path: - if candidate is not None: - return candidate - - # sorry bro. - return None - - -def mkdir_p(path): - """ - implements mkdir -p functionality - """ - try: - os.makedirs(path) - except OSError as exc: - if exc.errno == errno.EEXIST: - pass - else: - raise - - -def mkdir_f(path): - folder, fname = os.path.split(path) - mkdir_p(folder) - - -def check_and_fix_urw_only(_file): - """ - test for 600 mode and try - to set it if anything different found - """ - mode = stat.S_IMODE( - os.stat(_file).st_mode) - - if mode != int('600', 8): - try: - logger.warning( - 'bad permission on %s ' - 'attempting to set 600', - _file) - os.chmod(_file, stat.S_IRUSR | stat.S_IWUSR) - except OSError: - logger.error( - 'error while trying to chmod 600 %s', - _file) - raise diff --git a/src/leap/util/geo.py b/src/leap/util/geo.py deleted file mode 100644 index 54b29596..00000000 --- a/src/leap/util/geo.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -experimental geo support. -not yet a feature. -in debian, we rely on the (optional) geoip-database -""" -import os -import platform - -from leap.util import HAS_GEOIP - -GEOIP = None - -if HAS_GEOIP: - import pygeoip # we know we can :) - - GEOIP_PATH = None - - if platform.system() == "Linux": - PATH = "/usr/share/GeoIP/GeoIP.dat" - if os.path.isfile(PATH): - GEOIP_PATH = PATH - GEOIP = pygeoip.GeoIP(GEOIP_PATH, pygeoip.MEMORY_CACHE) - - -def get_country_name(ip): - if not GEOIP: - return - try: - country = GEOIP.country_name_by_addr(ip) - except pygeoip.GeoIPError: - country = None - return country if country else "-" diff --git a/src/leap/util/leap_argparse.py b/src/leap/util/leap_argparse.py deleted file mode 100644 index 3412a72c..00000000 --- a/src/leap/util/leap_argparse.py +++ /dev/null @@ -1,44 +0,0 @@ -import argparse - - -def build_parser(): - """ - all the options for the leap arg parser - Some of these could be switched on only if debug flag is present! - """ - epilog = "Copyright 2012 The LEAP Encryption Access Project" - parser = argparse.ArgumentParser(description=""" -Launches the LEAP Client""", epilog=epilog) - parser.add_argument('-d', '--debug', action="store_true", - help=("Launches client in debug mode, writing debug" - "info to stdout")) - parser.add_argument('-l', '--logfile', metavar="LOG FILE", nargs='?', - action="store", dest="log_file", - #type=argparse.FileType('w'), - help='optional log file') - parser.add_argument('--openvpn-verbosity', nargs='?', - type=int, - action="store", dest="openvpn_verb", - help='verbosity level for openvpn logs [1-6]') - - # Not in use, we might want to reintroduce them. - #parser.add_argument('-i', '--no-provider-checks', - #action="store_true", default=False, - #help="skips download of provider config files. gets " - #"config from local files only. Will fail if cannot " - #"find any") - #parser.add_argument('-k', '--no-ca-verify', - #action="store_true", default=False, - #help="(insecure). Skips verification of the server " - #"certificate used in TLS handshake.") - #parser.add_argument('-c', '--config', metavar="CONFIG FILE", nargs='?', - #action="store", dest="config_file", - #type=argparse.FileType('r'), - #help='optional config file') - return parser - - -def init_leapc_args(): - parser = build_parser() - opts, unknown = parser.parse_known_args() - return parser, opts diff --git a/src/leap/util/misc.py b/src/leap/util/misc.py deleted file mode 100644 index d869a1ba..00000000 --- a/src/leap/util/misc.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -misc utils -""" -import psutil - -from leap.base.constants import OPENVPN_BIN - - -class ImproperlyConfigured(Exception): - """ - """ - - -def null_check(value, value_name): - try: - assert value is not None - except AssertionError: - raise ImproperlyConfigured( - "%s parameter cannot be None" % value_name) - - -def get_openvpn_pids(): - # binary name might change - - openvpn_pids = [] - for p in psutil.process_iter(): - try: - # XXX Not exact! - # Will give false positives. - # we should check that cmdline BEGINS - # with openvpn or with our wrapper - # (pkexec / osascript / whatever) - if OPENVPN_BIN in ' '.join(p.cmdline): - openvpn_pids.append(p.pid) - except psutil.error.AccessDenied: - pass - return openvpn_pids diff --git a/src/leap/util/tests/__init__.py b/src/leap/util/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/util/tests/test_fileutil.py b/src/leap/util/tests/test_fileutil.py deleted file mode 100644 index f5131b3d..00000000 --- a/src/leap/util/tests/test_fileutil.py +++ /dev/null @@ -1,100 +0,0 @@ -import os -import platform -import shutil -import stat -import tempfile -import unittest - -from leap.util import fileutil - - -class FileUtilTest(unittest.TestCase): - """ - test our file utils - """ - - def setUp(self): - self.system = platform.system() - self.create_temp_dir() - - def tearDown(self): - self.remove_temp_dir() - - # - # helpers - # - - def create_temp_dir(self): - self.tmpdir = tempfile.mkdtemp() - - def remove_temp_dir(self): - shutil.rmtree(self.tmpdir) - - def get_file_path(self, filename): - return os.path.join( - self.tmpdir, - filename) - - def touch_exec_file(self): - fp = self.get_file_path('testexec') - open(fp, 'w').close() - os.chmod( - fp, - stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) - return fp - - def get_mode(self, fp): - return stat.S_IMODE(os.stat(fp).st_mode) - - # - # tests - # - - def test_is_user_executable(self): - """ - touch_exec_file creates in mode 700? - """ - # XXX could check access X_OK - - fp = self.touch_exec_file() - mode = self.get_mode(fp) - self.assertEqual(mode, int('700', 8)) - - def test_which(self): - """ - which implementation ok? - not a very reliable test, - but I cannot think of anything smarter now - I guess it's highly improbable that copy - """ - # XXX yep, we can change the syspath - # for the test... ! - - if self.system == "Linux": - self.assertEqual( - fileutil.which('cp'), - '/bin/cp') - - def test_mkdir_p(self): - """ - our own mkdir -p implementation ok? - """ - testdir = self.get_file_path( - os.path.join('test', 'foo', 'bar')) - self.assertEqual(os.path.isdir(testdir), False) - fileutil.mkdir_p(testdir) - self.assertEqual(os.path.isdir(testdir), True) - - def test_check_and_fix_urw_only(self): - """ - ensure check_and_fix_urx_only ok? - """ - fp = self.touch_exec_file() - mode = self.get_mode(fp) - self.assertEqual(mode, int('700', 8)) - fileutil.check_and_fix_urw_only(fp) - mode = self.get_mode(fp) - self.assertEqual(mode, int('600', 8)) - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/util/tests/test_leap_argparse.py b/src/leap/util/tests/test_leap_argparse.py deleted file mode 100644 index 4e2b811f..00000000 --- a/src/leap/util/tests/test_leap_argparse.py +++ /dev/null @@ -1,35 +0,0 @@ -from argparse import Namespace -import unittest - -from leap.util import leap_argparse - - -class LeapArgParseTest(unittest.TestCase): - """ - Test argparse options for eip client - """ - - def setUp(self): - """ - get the parser - """ - self.parser = leap_argparse.build_parser() - - def test_debug_mode(self): - """ - test debug mode option - """ - opts = self.parser.parse_args( - ['--debug']) - self.assertEqual( - opts, - Namespace( - debug=True, - log_file=None, - #config_file=None, - #no_provider_checks=False, - #no_ca_verify=False, - openvpn_verb=None)) - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/util/tests/test_translations.py b/src/leap/util/tests/test_translations.py deleted file mode 100644 index 794daeba..00000000 --- a/src/leap/util/tests/test_translations.py +++ /dev/null @@ -1,22 +0,0 @@ -import unittest - -from leap.util import translations - - -class TrasnlationsTestCase(unittest.TestCase): - """ - tests for translation functions and classes - """ - - def setUp(self): - self.trClass = translations.LEAPTranslatable - - def test_trasnlatable(self): - tr = self.trClass({"en": "house", "es": "casa"}) - eq = self.assertEqual - eq(tr.tr(to="es"), "casa") - eq(tr.tr(to="en"), "house") - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/util/translations.py b/src/leap/util/translations.py deleted file mode 100644 index f55c8fba..00000000 --- a/src/leap/util/translations.py +++ /dev/null @@ -1,82 +0,0 @@ -import inspect -import logging - -from PyQt4.QtCore import QCoreApplication -from PyQt4.QtCore import QLocale - -logger = logging.getLogger(__name__) - -""" -here I could not do all that I wanted. -the context is not getting passed to the xml file. -Looks like pylupdate4 is somehow a hack that does not -parse too well the python ast. -I guess we could generate the xml for ourselves as a last recourse. -""" - -# XXX BIG NOTE: -# RESIST the temptation to get the translate function -# more compact, or have the Context argument passed as a variable -# Its name HAS to be explicit due to how the pylupdate parser -# works. - - -qtTranslate = QCoreApplication.translate - - -def translate(*args, **kwargs): - """ - our magic function. - translate(Context, text, comment) - """ - if len(args) == 1: - obj = args[0] - if isinstance(obj, LEAPTranslatable) and hasattr(obj, 'tr'): - return obj.tr() - - klsname = None - try: - # get class value from instance - # using live object inspection - prev_frame = inspect.stack()[1][0] - locals_ = inspect.getargvalues(prev_frame).locals - self = locals_.get('self') - if self: - - # Trying to get the class name - # but this is useless, the parser - # has already got the context. - klsname = self.__class__.__name__ - #print 'KLSNAME -- ', klsname - except: - logger.error('error getting stack frame') - - if klsname and len(args) == 1: - nargs = (klsname,) + args - return qtTranslate(*nargs) - - else: - return qtTranslate(*args) - - -class LEAPTranslatable(dict): - """ - An extended dict that implements a .tr method - so it can be translated on the fly by our - magic translate method - """ - - try: - locale = str(QLocale.system().name()).split('_')[0] - except: - logger.warning("could not get system locale!") - print "could not get system locale!" - locale = "en" - - def tr(self, to=None): - if not to: - to = self.locale - _tr = self.get(to, None) - if not _tr: - _tr = self.get("en", None) - return _tr diff --git a/src/leap/util/web.py b/src/leap/util/web.py deleted file mode 100644 index 15de0561..00000000 --- a/src/leap/util/web.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -web related utilities -""" - - -class UsageError(Exception): - """ """ - - -def get_https_domain_and_port(full_domain): - """ - returns a tuple with domain and port - from a full_domain string that can - contain a colon - """ - full_domain = unicode(full_domain) - if full_domain is None: - return None, None - - https_sch = "https://" - http_sch = "http://" - - if full_domain.startswith(https_sch): - full_domain = full_domain.lstrip(https_sch) - elif full_domain.startswith(http_sch): - raise UsageError( - "cannot be called with a domain " - "that begins with 'http://'") - - domain_split = full_domain.split(':') - _len = len(domain_split) - if _len == 1: - domain, port = full_domain, 443 - elif _len == 2: - domain, port = domain_split - else: - raise UsageError( - "must be called with one only parameter" - "in the form domain[:port]") - return domain, port -- cgit v1.2.3 From d4452e77afc63ec49684ef4b6cf6459456d293e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 6 Mar 2013 15:36:37 -0300 Subject: Add basic config handling --- src/leap/config/__init__.py | 0 src/leap/config/baseconfig.py | 181 ++++++++++++++ src/leap/config/pluggableconfig.py | 473 +++++++++++++++++++++++++++++++++++++ src/leap/config/prefixers.py | 86 +++++++ src/leap/config/provider_spec.py | 75 ++++++ src/leap/config/providerconfig.py | 144 +++++++++++ 6 files changed, 959 insertions(+) create mode 100644 src/leap/config/__init__.py create mode 100644 src/leap/config/baseconfig.py create mode 100644 src/leap/config/pluggableconfig.py create mode 100644 src/leap/config/prefixers.py create mode 100644 src/leap/config/provider_spec.py create mode 100644 src/leap/config/providerconfig.py (limited to 'src') diff --git a/src/leap/config/__init__.py b/src/leap/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/config/baseconfig.py b/src/leap/config/baseconfig.py new file mode 100644 index 00000000..d553255e --- /dev/null +++ b/src/leap/config/baseconfig.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +# baseconfig.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Implements the abstract base class for configuration +""" + +import logging +import functools +import os +import errno +import copy + +from abc import ABCMeta, abstractmethod + +from leap.config.prefixers import get_platform_prefixer +from leap.config.pluggableconfig import PluggableConfig + +logger = logging.getLogger(__name__) + + +class BaseConfig: + """ + Abstract base class for any JSON based configuration + """ + + __metaclass__ = ABCMeta + + def __init__(self): + self._data = {} + self._config_checker = None + + @abstractmethod + def _get_spec(self): + """ + Returns the spec object for the specific configuration + """ + return None + + def _safe_get_value(self, key): + """ + Tries to return a value only if the config has already been loaded + + @rtype: depends on the config structure, dict, str, array, int + @return: returns the value for the specified key in the config + """ + assert self._config_checker, "Load the config first" + return self._config_checker.config[key] + + def get_path_prefix(self, standalone=False): + """ + Returns the platform dependant path prefixer + + @param standalone: if True it will return the prefix for a + standalone application. Otherwise, it will return the system + default for configuration storage. + @param type: bool + """ + return get_platform_prefixer().get_path_prefix(standalone=standalone) + + def loaded(self): + """ + Returns True if the configuration has been already + loaded. False otherwise + """ + return self._config_checker is not None + + def save(self, path_list): + """ + Saves the current configuration to disk + + @param path: relative path to configuration. The absolute path + will be calculated depending on the platform. + @type path: list + + @return: True if saved to disk correctly, False otherwise + """ + config_path = os.path.join(self.get_path_prefix(), *(path_list[:-1])) + try: + os.makedirs(config_path) + except OSError as e: + if e.errno == errno.EEXIST and os.path.isdir(config_path): + pass + else: + raise + + try: + self._config_checker.serialize(os.path.join(config_path, + path_list[-1])) + except Exception as e: + logger.warning("%s" % (e,)) + raise + return True + + def load(self, path="", data=None): + """ + Loads the configuration from disk + + @type path: str + @param path: relative path to configuration. The absolute path + will be calculated depending on the platform + + @return: True if loaded to disk correctly, False otherwise + """ + + # TODO: retrieve standalone option from app-level config + config_path = os.path.join(self.get_path_prefix(), + path) + + self._config_checker = PluggableConfig(format="json") + self._config_checker.options = copy.deepcopy(self._get_spec()) + + try: + if data is None: + self._config_checker.load(fromfile=config_path) + else: + self._config_checker.load(data) + except Exception as e: + logger.warning("Something went wrong while loading " + + "the config from %s\n%s" % (config_path, e)) + self._config_checker = None + return False + return True + + +class LocalizedKey(object): + """ + Decorator used for keys that are localized in a configuration + """ + + def __init__(self, func, **kwargs): + self._func = func + + def __call__(self, instance, lang="en"): + """ + Tries to return the string for the specified language, otherwise + informs the problem and returns an empty string + + @param lang: language code + @param type: str + + @return: localized value from the possible values returned by + self._func + """ + descriptions = self._func(instance) + description_lang = "" + if lang in descriptions.keys(): + description_lang = descriptions[lang] + else: + logger.warning("Unknown language: %s" % (lang,)) + return description_lang + + def __get__(self, instance, instancetype): + """ + Implement the descriptor protocol to make decorating instance + method possible. + """ + # Return a partial function with the first argument is the instance + # of the class decorated. + return functools.partial(self.__call__, instance) + +if __name__ == "__main__": + try: + config = BaseConfig() # should throw TypeError for _get_spec + except Exception as e: + assert isinstance(e, TypeError), "Something went wrong" + print "Abstract BaseConfig class is working as expected" diff --git a/src/leap/config/pluggableconfig.py b/src/leap/config/pluggableconfig.py new file mode 100644 index 00000000..64aa05cc --- /dev/null +++ b/src/leap/config/pluggableconfig.py @@ -0,0 +1,473 @@ +# -*- coding: utf-8 -*- +# pluggableconfig.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +generic configuration handlers +""" +import copy +import json +import logging +import os +import time +import urlparse + +import jsonschema + +#from leap.base.util.translations import LEAPTranslatable + +logger = logging.getLogger(__name__) + + +__all__ = ['PluggableConfig', + 'adaptors', + 'types', + 'UnknownOptionException', + 'MissingValueException', + 'ConfigurationProviderException', + 'TypeCastException'] + +# exceptions + + +class UnknownOptionException(Exception): + """exception raised when a non-configuration + value is present in the configuration""" + + +class MissingValueException(Exception): + """exception raised when a required value is missing""" + + +class ConfigurationProviderException(Exception): + """exception raised when a configuration provider is missing, etc""" + + +class TypeCastException(Exception): + """exception raised when a + configuration item cannot be coerced to a type""" + + +class ConfigAdaptor(object): + """ + abstract base class for config adaotors for + serialization/deserialization and custom validation + and type casting. + """ + def read(self, filename): + raise NotImplementedError("abstract base class") + + def write(self, config, filename): + with open(filename, 'w') as f: + self._write(f, config) + + def _write(self, fp, config): + raise NotImplementedError("abstract base class") + + def validate(self, config, schema): + raise NotImplementedError("abstract base class") + + +adaptors = {} + + +class JSONSchemaEncoder(json.JSONEncoder): + """ + custom default encoder that + casts python objects to json objects for + the schema validation + """ + def default(self, obj): + if obj is str: + return 'string' + if obj is unicode: + return 'string' + if obj is int: + return 'integer' + if obj is list: + return 'array' + if obj is dict: + return 'object' + if obj is bool: + return 'boolean' + + +class JSONAdaptor(ConfigAdaptor): + indent = 2 + extensions = ['json'] + + def read(self, _from): + if isinstance(_from, file): + _from_string = _from.read() + if isinstance(_from, str): + _from_string = _from + return json.loads(_from_string) + + def _write(self, fp, config): + fp.write(json.dumps(config, + indent=self.indent, + sort_keys=True)) + + def validate(self, config, schema_obj): + schema_json = JSONSchemaEncoder().encode(schema_obj) + schema = json.loads(schema_json) + jsonschema.validate(config, schema) + + +adaptors['json'] = JSONAdaptor() + +# +# Adaptors +# +# Allow to apply a predefined set of types to the +# specs, so it checks the validity of formats and cast it +# to proper python types. + +# TODO: +# - HTTPS uri + + +class DateType(object): + fmt = '%Y-%m-%d' + + def to_python(self, data): + return time.strptime(data, self.fmt) + + def get_prep_value(self, data): + return time.strftime(self.fmt, data) + + +class TranslatableType(object): + """ + a type that casts to LEAPTranslatable objects. + Used for labels we get from providers and stuff. + """ + + def to_python(self, data): + # TODO: add translatable + return data#LEAPTranslatable(data) + + # needed? we already have an extended dict... + #def get_prep_value(self, data): + #return dict(data) + + +class URIType(object): + + def to_python(self, data): + parsed = urlparse.urlparse(data) + if not parsed.scheme: + raise TypeCastException("uri %s has no schema" % data) + return parsed.geturl() + + def get_prep_value(self, data): + return data + + +class HTTPSURIType(object): + + def to_python(self, data): + parsed = urlparse.urlparse(data) + if not parsed.scheme: + raise TypeCastException("uri %s has no schema" % data) + if parsed.scheme != "https": + raise TypeCastException( + "uri %s does not has " + "https schema" % data) + return parsed.geturl() + + def get_prep_value(self, data): + return data + + +types = { + 'date': DateType(), + 'uri': URIType(), + 'https-uri': HTTPSURIType(), + 'translatable': TranslatableType(), +} + + +class PluggableConfig(object): + + options = {} + + def __init__(self, + adaptors=adaptors, + types=types, + format=None): + + self.config = {} + self.adaptors = adaptors + self.types = types + self._format = format + self.mtime = None + self.dirty = False + + @property + def option_dict(self): + if hasattr(self, 'options') and isinstance(self.options, dict): + return self.options.get('properties', None) + + def items(self): + """ + act like an iterator + """ + if isinstance(self.option_dict, dict): + return self.option_dict.items() + return self.options + + def validate(self, config, format=None): + """ + validate config + """ + schema = self.options + if format is None: + format = self._format + + if format: + adaptor = self.get_adaptor(self._format) + adaptor.validate(config, schema) + else: + # we really should make format mandatory... + logger.error('no format passed to validate') + + # first round of validation is ok. + # now we proceed to cast types if any specified. + self.to_python(config) + + def to_python(self, config): + """ + cast types following first type and then format indications. + """ + unseen_options = [i for i in config if i not in self.option_dict] + if unseen_options: + raise UnknownOptionException( + "Unknown options: %s" % ', '.join(unseen_options)) + + for key, value in config.items(): + _type = self.option_dict[key].get('type') + if _type is None and 'default' in self.option_dict[key]: + _type = type(self.option_dict[key]['default']) + if _type is not None: + tocast = True + if not callable(_type) and isinstance(value, _type): + tocast = False + if tocast: + try: + config[key] = _type(value) + except BaseException, e: + raise TypeCastException( + "Could not coerce %s, %s, " + "to type %s: %s" % (key, value, _type.__name__, e)) + _format = self.option_dict[key].get('format', None) + _ftype = self.types.get(_format, None) + if _ftype: + try: + config[key] = _ftype.to_python(value) + except BaseException, e: + raise TypeCastException( + "Could not coerce %s, %s, " + "to format %s: %s" % (key, value, + _ftype.__class__.__name__, + e)) + + return config + + def prep_value(self, config): + """ + the inverse of to_python method, + called just before serialization + """ + for key, value in config.items(): + _format = self.option_dict[key].get('format', None) + _ftype = self.types.get(_format, None) + if _ftype and hasattr(_ftype, 'get_prep_value'): + try: + config[key] = _ftype.get_prep_value(value) + except BaseException, e: + raise TypeCastException( + "Could not serialize %s, %s, " + "by format %s: %s" % (key, value, + _ftype.__class__.__name__, + e)) + else: + config[key] = value + return config + + # methods for adding configuration + + def get_default_values(self): + """ + return a config options from configuration defaults + """ + defaults = {} + for key, value in self.items(): + if 'default' in value: + defaults[key] = value['default'] + return copy.deepcopy(defaults) + + def get_adaptor(self, format): + """ + get specified format adaptor or + guess for a given filename + """ + adaptor = self.adaptors.get(format, None) + if adaptor: + return adaptor + + # not registered in adaptors dict, let's try all + for adaptor in self.adaptors.values(): + if format in adaptor.extensions: + return adaptor + + def filename2format(self, filename): + extension = os.path.splitext(filename)[-1] + return extension.lstrip('.') or None + + def serialize(self, filename, format=None, full=False): + if not format: + format = self._format + if not format: + format = self.filename2format(filename) + if not format: + raise Exception('Please specify a format') + # TODO: more specific exception type + + adaptor = self.get_adaptor(format) + if not adaptor: + raise Exception("Adaptor not found for format: %s" % format) + + config = copy.deepcopy(self.config) + serializable = self.prep_value(config) + adaptor.write(serializable, filename) + + if self.mtime: + self.touch_mtime(filename) + + def touch_mtime(self, filename): + mtime = self.mtime + os.utime(filename, (mtime, mtime)) + + def deserialize(self, string=None, fromfile=None, format=None): + """ + load configuration from a file or string + """ + + def _try_deserialize(): + if fromfile: + with open(fromfile, 'r') as f: + content = adaptor.read(f) + elif string: + content = adaptor.read(string) + return content + + # XXX cleanup this! + + if fromfile: + assert os.path.exists(fromfile) + if not format: + format = self.filename2format(fromfile) + + if not format: + format = self._format + if format: + adaptor = self.get_adaptor(format) + else: + adaptor = None + + if adaptor: + content = _try_deserialize() + return content + + # no adaptor, let's try rest of adaptors + + adaptors = self.adaptors[:] + + if format: + adaptors.sort( + key=lambda x: int( + format in x.extensions), + reverse=True) + + for adaptor in adaptors: + content = _try_deserialize() + return content + + def set_dirty(self): + self.dirty = True + + def is_dirty(self): + return self.dirty + + def load(self, *args, **kwargs): + """ + load from string or file + if no string of fromfile option is given, + it will attempt to load from defaults + defined in the schema. + """ + string = args[0] if args else None + fromfile = kwargs.get("fromfile", None) + mtime = kwargs.pop("mtime", None) + self.mtime = mtime + content = None + + # start with defaults, so we can + # have partial values applied. + content = self.get_default_values() + if string and isinstance(string, str): + content = self.deserialize(string) + + if not string and fromfile is not None: + #import ipdb;ipdb.set_trace() + content = self.deserialize(fromfile=fromfile) + + if not content: + logger.error('no content could be loaded') + # XXX raise! + return + + # lazy evaluation until first level of nesting + # to allow lambdas with context-dependant info + # like os.path.expanduser + for k, v in content.iteritems(): + if callable(v): + content[k] = v() + + self.validate(content) + self.config = content + return True + + +def testmain(): # pragma: no cover + + from tests import test_validation as t + import pprint + + config = PluggableConfig(_format="json") + properties = copy.deepcopy(t.sample_spec) + + config.options = properties + config.load(fromfile='data.json') + + print 'config' + pprint.pprint(config.config) + + config.serialize('/tmp/testserial.json') + +if __name__ == "__main__": + testmain() diff --git a/src/leap/config/prefixers.py b/src/leap/config/prefixers.py new file mode 100644 index 00000000..a33e68aa --- /dev/null +++ b/src/leap/config/prefixers.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# prefixers.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Platform dependant configuration path prefixers +""" +import os +import platform + +from abc import ABCMeta, abstractmethod +from xdg import BaseDirectory + + +class Prefixer: + """ + Abstract prefixer class + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def get_path_prefix(self, standalone=False): + """ + Returns the platform dependant path prefixer + + @param standalone: if True it will return the prefix for a + standalone application. Otherwise, it will return the system + default for configuration storage. + @param type: bool + """ + return "" + + +def get_platform_prefixer(): + prefixer = globals()[platform.system() + "Prefixer"] + assert prefixer, "Unimplemented platform prefixer: %s" % \ + (platform.system(),) + return prefixer() + + +class LinuxPrefixer(Prefixer): + """ + Config prefixer for the Linux platform + """ + + def get_path_prefix(self, standalone=False): + """ + Returns the platform dependant path prefixer. + This method expects an env variable named LEAP_CLIENT_PATH if + standalone is used. + + @param standalone: if True it will return the prefix for a + standalone application. Otherwise, it will return the system + default for configuration storage. + @param type: bool + """ + config_dir = BaseDirectory.xdg_config_home + if not standalone: + return config_dir + return os.getenv("LEAP_CLIENT_PATH", config_dir) + + +if __name__ == "__main__": + try: + abs_prefixer = Prefixer() + except Exception as e: + assert isinstance(e, TypeError), "Something went wrong" + print "Abstract Prefixer class is working as expected" + + linux_prefixer = LinuxPrefixer() + print linux_prefixer.get_path_prefix(standalone=True) + print linux_prefixer.get_path_prefix() diff --git a/src/leap/config/provider_spec.py b/src/leap/config/provider_spec.py new file mode 100644 index 00000000..958f7846 --- /dev/null +++ b/src/leap/config/provider_spec.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# provider_spec.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +leap_provider_spec = { + 'description': 'provider definition', + 'type': 'object', + 'properties': { + 'version': { + 'type': unicode, + 'default': '0.1.0' + }, + "default_language": { + 'type': unicode, + 'default': 'en' + }, + 'domain': { + 'type': unicode, # XXX define uri type + 'default': 'testprovider.example.org' + }, + 'name': { + 'type': dict, + 'format': 'translatable', + 'default': {u'en': u'Test Provider'} + }, + 'description': { + #'type': LEAPTranslatable, + 'type': dict, + 'format': 'translatable', + 'default': {u'en': u'Test provider'} + }, + 'enrollment_policy': { + 'type': unicode, # oneof ?? + 'default': 'open' + }, + 'services': { + 'type': list, # oneof ?? + 'default': ['eip'] + }, + 'api_version': { + 'type': unicode, + 'default': '0.1.0' # version regexp + }, + 'api_uri': { + 'type': unicode # uri + }, + 'public_key': { + 'type': unicode # fingerprint + }, + 'ca_cert_fingerprint': { + 'type': unicode, + }, + 'ca_cert_uri': { + 'type': unicode, + 'format': 'https-uri' + }, + 'languages': { + 'type': list, + 'default': ['en'] + } + } +} diff --git a/src/leap/config/providerconfig.py b/src/leap/config/providerconfig.py new file mode 100644 index 00000000..c3c2c298 --- /dev/null +++ b/src/leap/config/providerconfig.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# providerconfig.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Provider configuration +""" +import logging +import os + +from leap.config.baseconfig import BaseConfig, LocalizedKey +from leap.config.provider_spec import leap_provider_spec + +logger = logging.getLogger(__name__) + + +class ProviderConfig(BaseConfig): + """ + Provider configuration abstraction class + """ + + def __init__(self): + BaseConfig.__init__(self) + + def _get_spec(self): + """ + Returns the spec object for the specific configuration + """ + return leap_provider_spec + + def get_api_uri(self): + return self._safe_get_value("api_uri") + + def get_api_version(self): + return self._safe_get_value("api_version") + + def get_ca_cert_fingerprint(self): + return self._safe_get_value("ca_cert_fingerprint") + + def get_ca_cert_uri(self): + return self._safe_get_value("ca_cert_uri") + + def get_default_language(self): + return self._safe_get_value("default_language") + + @LocalizedKey + def get_description(self): + return self._safe_get_value("description") + + def get_domain(self): + return self._safe_get_value("domain") + + def get_enrollment_policy(self): + return self._safe_get_value("enrollment_policy") + + def get_languages(self): + return self._safe_get_value("languages") + + @LocalizedKey + def get_name(self): + return self._safe_get_value("name") + + def get_services(self): + return self._safe_get_value("services") + + def get_ca_cert_path(self, about_to_download=False): + """ + Returns the path to the certificate for the current provider + + @param about_to_download: defines wether we want the path to + download the cert or not. This helps avoid checking if the + cert exists because we are about to write it. + @type about_to_download: bool + """ + + cert_path = os.path.join(self.get_path_prefix(), + "leap", + "providers", + self.get_domain(), + "keys", + "ca", + "cacert.pem") + + if not about_to_download: + assert os.path.exists(cert_path), \ + "You need to download the certificate first" + logger.debug("Going to verify SSL against %s" % (cert_path,)) + + return cert_path + + def provides_eip(self): + """ + Returns True if this particular provider has the EIP + service. False otherwise + """ + return "openvpn" in self.get_services() + + +if __name__ == "__main__": + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + provider = ProviderConfig() + + try: + provider.get_api_version() + except Exception as e: + assert isinstance(e, AssertionError), "Expected an assert" + print "Safe value getting is working" + + # standalone minitest + #if provider.load("provider_bad.json"): + if provider.load("leap/providers/bitmask.net/provider.json"): + print provider.get_api_version() + print provider.get_ca_cert_fingerprint() + print provider.get_ca_cert_uri() + print provider.get_default_language() + print provider.get_description() + print provider.get_description(lang="asd") + print provider.get_domain() + print provider.get_enrollment_policy() + print provider.get_languages() + print provider.get_name() + print provider.get_services() -- cgit v1.2.3 From 97554d4c413dd60be4ed67c9553cb0976ce420b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 6 Mar 2013 15:37:07 -0300 Subject: Add SRP related code: authentication and registration --- src/leap/crypto/__init__.py | 0 src/leap/crypto/constants.py | 18 ++ src/leap/crypto/srpauth.py | 439 +++++++++++++++++++++++++++++++++++++++++ src/leap/crypto/srpregister.py | 154 +++++++++++++++ 4 files changed, 611 insertions(+) create mode 100644 src/leap/crypto/__init__.py create mode 100644 src/leap/crypto/constants.py create mode 100644 src/leap/crypto/srpauth.py create mode 100644 src/leap/crypto/srpregister.py (limited to 'src') diff --git a/src/leap/crypto/__init__.py b/src/leap/crypto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/crypto/constants.py b/src/leap/crypto/constants.py new file mode 100644 index 00000000..c5eaef1f --- /dev/null +++ b/src/leap/crypto/constants.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# constants.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +SIGNUP_TIMEOUT = 5 diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py new file mode 100644 index 00000000..dbcc95cb --- /dev/null +++ b/src/leap/crypto/srpauth.py @@ -0,0 +1,439 @@ +# -*- coding: utf-8 -*- +# srpauth.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import requests +import srp +import binascii +import logging + +from PySide import QtCore + +from leap.config.providerconfig import ProviderConfig + +logger = logging.getLogger(__name__) + + +class SRPAuthenticationError(Exception): + """ + Exception raised for authentication errors + """ + pass + + +class SRPAuth(QtCore.QThread): + """ + SRPAuth singleton + """ + + class __impl(object): + """ + Implementation of the SRPAuth interface + """ + + LOGIN_KEY = "login" + A_KEY = "A" + CLIENT_AUTH_KEY = "client_auth" + + def __init__(self, provider_config): + """ + Constructor for SRPAuth implementation + + @param server: Server to which we will authenticate + @type server: str + """ + assert provider_config, "We need a provider config to authenticate" + + self._provider_config = provider_config + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + self._srp = srp + self._hashfun = self._srp.SHA256 + self._ng = self._srp.NG_1024 + # **************************************************** # + + self._session = self._fetcher.session() + self._session_id = None + self._session_id_lock = QtCore.QMutex() + self._uid = None + self._uid_lock = QtCore.QMutex() + + self._srp_user = None + self._srp_a = None + + def _safe_unhexlify(self, val): + """ + Rounds the val to a multiple of 2 and returns the + unhexlified value + + @param val: hexlified value + @type val: str + + @rtype: binary hex data + @return: unhexlified val + """ + return binascii.unhexlify(val) \ + if (len(val) % 2 == 0) else binascii.unhexlify('0' + val) + + def _authentication_preprocessing(self, username, password): + """ + Generates the SRP.User to get the A SRP parameter + + @param username: username to login + @type username: str + @param password: password for the username + @type password: str + """ + logger.debug("Authentication preprocessing...") + self._srp_user = self._srp.User(username, + password, + self._hashfun, + self._ng) + _, A = self._srp_user.start_authentication() + + self._srp_a = A + + def _start_authentication(self, username, password): + """ + Sends the first request for authentication to retrieve the + salt and B parameter + + Might raise SRPAuthenticationError + + @param username: username to login + @type username: str + @param password: password for the username + @type password: str + + @return: salt and B parameters + @rtype: tuple + """ + logger.debug("Starting authentication process...") + try: + auth_data = { + self.LOGIN_KEY: username, + self.A_KEY: binascii.hexlify(self._srp_a) + } + sessions_url = "%s/%s/%s/" % \ + (self._provider_config.get_api_uri(), + self._provider_config.get_api_version(), + "sessions") + init_session = self._session.post(sessions_url, + data=auth_data, + verify=self._provider_config. + get_ca_cert_path()) + except requests.exceptions.ConnectionError as e: + logger.error("No connection made (salt): %r" % + (e,)) + raise SRPAuthenticationError("Could not establish a " + "connection") + except Exception as e: + logger.error("Unknown error: %r" % (e,)) + raise SRPAuthenticationError("Unknown error: %r" % + (e,)) + + if init_session.status_code not in (200,): + logger.error("No valid response (salt): " + "Status code = %r. Content: %r" % + (init_session.status_code, init_session.content)) + if init_session.status_code == 422: + raise SRPAuthenticationError("Unknown user") + salt = init_session.json().get("salt", None) + B = init_session.json().get("B", None) + + if salt is None: + logger.error("No salt parameter sent") + raise SRPAuthenticationError("The server did not send the " + + "salt parameter") + if B is None: + logger.error("No B parameter sent") + raise SRPAuthenticationError("The server did not send the " + + "B parameter") + + return salt, B + + def _process_challenge(self, salt, B, username): + """ + Given the salt and B processes the auth challenge and + generates the M2 parameter + + Might throw SRPAuthenticationError + + @param salt: salt for the username + @type salt: str + @param B: B SRP parameter + @type B: str + @param username: username for this session + @type username: str + + @return: the M2 SRP parameter + @rtype: str + """ + logger.debug("Processing challenge...") + try: + unhex_salt = self._safe_unhexlify(salt) + unhex_B = self._safe_unhexlify(B) + except TypeError as e: + logger.error("Bad data from server: %r" % (e,)) + raise SRPAuthenticationError("The data sent from the server " + "had errors") + M = self._srp_user.process_challenge(unhex_salt, unhex_B) + + auth_url = "%s/%s/%s/%s" % (self._provider_config.get_api_uri(), + self._provider_config. + get_api_version(), + "sessions", + username) + + auth_data = { + self.CLIENT_AUTH_KEY: binascii.hexlify(M) + } + + try: + auth_result = self._session.put(auth_url, + data=auth_data, + verify=self._provider_config. + get_ca_cert_path()) + except requests.exceptions.ConnectionError as e: + logger.error("No connection made (HAMK): %r" % (e,)) + raise SRPAuthenticationError("Could not connect to the server") + + if auth_result.status_code == 422: + logger.error("[%s] Wrong password (HAMK): [%s]" % + (auth_result.status_code, + auth_result.json(). + get("errors", ""))) + raise SRPAuthenticationError("Wrong password") + + if auth_result.status_code not in (200,): + logger.error("No valid response (HAMK): " + "Status code = %s. Content = %r" % + (auth_result.status_code, auth_result.content)) + raise SRPAuthenticationError("Unknown error (%s)" % + (auth_result.status_code,)) + + M2 = auth_result.json().get("M2", None) + self.set_uid(auth_result.json().get("id", None)) + + if M2 is None or self.get_uid() is None: + logger.error("Something went wrong. Content = %r" % + (auth_result.content,)) + raise SRPAuthenticationError("Problem getting data from" + " server") + + return M2 + + def _verify_session(self, M2): + """ + Verifies the session based on the M2 parameter. If the + verification succeeds, it sets the session_id for this + session + + Might throw SRPAuthenticationError + + @param M2: M2 SRP parameter + @type M2: str + """ + logger.debug("Verifying session...") + try: + unhex_M2 = self._safe_unhexlify(M2) + except TypeError: + logger.error("Bad data from server (HAWK)") + raise SRPAuthenticationError("Bad data from server") + + self._srp_user.verify_session(unhex_M2) + + if not self._srp_user.authenticated(): + logger.error("Auth verification failed") + raise SRPAuthenticationError("Auth verification failed") + logger.debug("Session verified.") + + self.set_session_id(self._session.cookies["_session_id"]) + + def authenticate(self, username, password): + """ + Executes the whole authentication process for a user + + Might raise SRPAuthenticationError + + @param username: username for this session + @type username: str + @param password: password for this user + @type password: str + """ + assert self.get_session_id() is None, "Already logged in" + + self._authentication_preprocessing(username, password) + salt, B = self._start_authentication(username, password) + M2 = self._process_challenge(salt, B, username) + self._verify_session(M2) + + assert self.get_session_id(), "Something went wrong because" + \ + " we don't have the auth cookie afterwards" + + def logout(self): + """ + Logs out the current session. + Expects a session_id to exists, might raise AssertionError + """ + logger.debug("Starting logout...") + + assert self.get_session_id(), "Cannot logout an unexisting session" + + logout_url = "%s/%s/%s/" % (self._provider_config.get_api_uri(), + self._provider_config. + get_api_version(), + "sessions") + try: + self._session.delete(logout_url, + data=self.get_session_id(), + verify=self._provider_config. + get_ca_cert_path()) + except Exception as e: + logger.warning("Something went wrong with the logout: %r" % + (e,)) + + self.set_session_id(None) + self.set_uid(None) + # Also reset the session + self._session = self._fetcher.session() + logger.debug("Successfully logged out.") + + def set_session_id(self, session_id): + QtCore.QMutexLocker(self._session_id_lock) + self._session_id = session_id + + def get_session_id(self): + QtCore.QMutexLocker(self._session_id_lock) + return self._session_id + + def set_uid(self, uid): + QtCore.QMutexLocker(self._uid_lock) + self._uid = uid + + def get_uid(self): + QtCore.QMutexLocker(self._uid_lock) + return self._uid + + __instance = None + + authentication_finished = QtCore.Signal(bool, str) + logout_finished = QtCore.Signal(bool, str) + + DO_NOTHING = 0 + DO_LOGIN = 1 + DO_LOGOUT = 2 + + def __init__(self, provider_config): + """ + Creates a singleton instance if needed + """ + QtCore.QThread.__init__(self) + + # Check whether we already have an instance + if SRPAuth.__instance is None: + # Create and remember instance + SRPAuth.__instance = SRPAuth.__impl(provider_config) + + # Store instance reference as the only member in the handle + self.__dict__['_SRPAuth__instance'] = SRPAuth.__instance + + self._should_login = self.DO_NOTHING + self._should_login_lock = QtCore.QMutex() + self._username = None + self._password = None + + def authenticate(self, username, password): + """ + Executes the whole authentication process for a user + + Might raise SRPAuthenticationError + + @param username: username for this session + @type username: str + @param password: password for this user + @type password: str + """ + + with QtCore.QMutexLocker(self._should_login_lock): + self._should_login = self.DO_LOGIN + self._username = username + self._password = password + # Detach the start call to Qt's event loop + QtCore.QTimer.singleShot(0, self.start) + + def logout(self): + """ + Logs out the current session. + Expects a session_id to exists, might raise AssertionError + """ + QtCore.QMutexLocker(self._should_login_lock) + self._should_login = self.DO_LOGOUT + # Detach the start call to Qt's event loop + QtCore.QTimer.singleShot(0, self.start) + + def _runLogin(self, username, password): + try: + self.__instance.authenticate(username, password) + self.authentication_finished.emit(True, "Succeeded") + except Exception as e: + self.authentication_finished.emit(False, "%s" % (e,)) + + def _runLogout(self): + try: + self.__instance.logout() + self.logout_finished.emit(True, "Succeeded") + except Exception as e: + self.logout_finished.emit(False, "%s" % (e,)) + + def run(self): + QtCore.QMutexLocker(self._should_login_lock) + if self._should_login == self.DO_LOGIN: + self._runLogin(self._username, self._password) + elif self._should_login == self.DO_LOGOUT: + self._runLogout() + self._should_login = self.DO_NOTHING + + +if __name__ == "__main__": + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + provider = ProviderConfig() + + if provider.load("leap/providers/bitmask.net/provider.json"): + # url = "%s/tickets" % (provider.get_api_uri(),) + # print url + # res = requests.session().get(url, verify=provider.get_ca_cert_path()) + # print res.content + # res.raise_for_status() + auth = SRPAuth(provider) + auth.start() + auth.authenticate("test2", "sarasaaaa") + res = requests.session().get("%s/cert" % (provider.get_api_uri(),), + verify=provider.get_ca_cert_path()) + print res.content + auth.logout() diff --git a/src/leap/crypto/srpregister.py b/src/leap/crypto/srpregister.py new file mode 100644 index 00000000..d9b2b22b --- /dev/null +++ b/src/leap/crypto/srpregister.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# srpregister.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import requests +import binascii +import srp +import logging + +from urlparse import urlparse + +from leap.config.providerconfig import ProviderConfig +from leap.crypto.constants import SIGNUP_TIMEOUT + +logger = logging.getLogger(__name__) + + +class SRPRegister(object): + """ + Registers a user to a specific provider using SRP + """ + + USER_LOGIN_KEY = 'user[login]' + USER_VERIFIER_KEY = 'user[password_verifier]' + USER_SALT_KEY = 'user[password_salt]' + + def __init__(self, + provider_config=None, + register_path="users"): + """ + Constructor + + @param provider_config: provider configuration instance, + properly loaded + @type privider_config: ProviderConfig + @param register_path: webapp path for registering users + @type register_path; str + """ + + assert provider_config, "Please provider a provider" + assert isinstance(provider_config, ProviderConfig), \ + "We need a ProviderConfig instead of %r" % (provider_config,) + + self._provider_config = provider_config + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + self._srp = srp + self._hashfun = self._srp.SHA256 + self._ng = self._srp.NG_1024 + # **************************************************** # + + parsed_url = urlparse(provider_config.get_api_uri()) + self._provider = parsed_url.hostname + self._port = parsed_url.port + + self._register_path = register_path + + self._session = self._fetcher.session() + + def _get_registration_uri(self): + """ + Returns the URI where the register request should be made for + the provider + + @rtype: str + """ + + if self._port: + uri = "https://%s:%s/%s/%s" % ( + self._provider, + self._port, + self._provider_config.get_api_version(), + self._register_path) + else: + uri = "https://%s/%s/%s" % ( + self._provider, + self._provider_config.get_api_version(), + self._register_path) + + return uri + + def register_user(self, username, password): + """ + Registers a user with the validator based on the password provider + + @param username: username to register + @type username: str + @param password: password for this username + @type password: str + + @rtype: tuple + @rparam: (ok, request) + """ + salt, verifier = self._srp.create_salted_verification_key( + username, + password, + self._hashfun, + self._ng) + + user_data = { + self.USER_LOGIN_KEY: username, + self.USER_VERIFIER_KEY: binascii.hexlify(verifier), + self.USER_SALT_KEY: binascii.hexlify(salt) + } + + uri = self._get_registration_uri() + + logger.debug('Post to uri: %s' % uri) + logger.debug("Will try to register user = %s" % (username,)) + logger.debug("user_data => %r" % (user_data,)) + + req = self._session.post(uri, + data=user_data, + timeout=SIGNUP_TIMEOUT, + verify=self._provider_config. + get_ca_cert_path()) + + return (req.ok, req) + + +if __name__ == "__main__": + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + provider = ProviderConfig() + + if provider.load("leap/providers/bitmask.net/provider.json"): + register = SRPRegister(provider_config=provider) + print "Registering user..." + print register.register_user("test1", "sarasaaaa") + print register.register_user("test2", "sarasaaaa") -- cgit v1.2.3 From ee8fbbdc2f3dbccea3a830b40e9eb0be5b392d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 6 Mar 2013 15:38:05 -0300 Subject: Add EIP service --- src/leap/services/__init__.py | 0 src/leap/services/eip/__init__.py | 0 src/leap/services/eip/eipbootstrapper.py | 315 ++++++++++++++++ src/leap/services/eip/eipconfig.py | 123 ++++++ src/leap/services/eip/eipspec.py | 63 ++++ src/leap/services/eip/providerbootstrapper.py | 520 ++++++++++++++++++++++++++ src/leap/services/eip/udstelnet.py | 61 +++ src/leap/services/eip/vpn.py | 359 ++++++++++++++++++ src/leap/services/eip/vpnlaunchers.py | 270 +++++++++++++ 9 files changed, 1711 insertions(+) create mode 100644 src/leap/services/__init__.py create mode 100644 src/leap/services/eip/__init__.py create mode 100644 src/leap/services/eip/eipbootstrapper.py create mode 100644 src/leap/services/eip/eipconfig.py create mode 100644 src/leap/services/eip/eipspec.py create mode 100644 src/leap/services/eip/providerbootstrapper.py create mode 100644 src/leap/services/eip/udstelnet.py create mode 100644 src/leap/services/eip/vpn.py create mode 100644 src/leap/services/eip/vpnlaunchers.py (limited to 'src') diff --git a/src/leap/services/__init__.py b/src/leap/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/services/eip/__init__.py b/src/leap/services/eip/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py new file mode 100644 index 00000000..77d7020a --- /dev/null +++ b/src/leap/services/eip/eipbootstrapper.py @@ -0,0 +1,315 @@ +# -*- coding: utf-8 -*- +# eipbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +EIP bootstrapping +""" + +import requests +import logging +import os +import errno + +from PySide import QtGui, QtCore + +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.eipconfig import EIPConfig + +logger = logging.getLogger(__name__) + + +class EIPBootstrapper(QtCore.QThread): + """ + Sets up EIP for a provider a series of checks and emits signals + after they are passed. + If a check fails, the subsequent checks are not executed + """ + + PASSED_KEY = "passed" + ERROR_KEY = "error" + + IDLE_SLEEP_INTERVAL = 100 + + # All dicts returned are of the form + # {"passed": bool, "error": str} + download_config = QtCore.Signal(dict) + download_client_certificate = QtCore.Signal(dict) + + def __init__(self): + QtCore.QThread.__init__(self) + + self._checks = [] + self._checks_lock = QtCore.QMutex() + + self._should_quit = False + self._should_quit_lock = QtCore.QMutex() + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + # **************************************************** # + + self._session = self._fetcher.session() + self._provider_config = None + self._eip_config = None + self._download_if_needed = False + + def get_should_quit(self): + """ + Returns wether this thread should quit + + @rtype: bool + @return: True if the thread should terminate itself, Flase otherwise + """ + + QtCore.QMutexLocker(self._should_quit_lock) + return self._should_quit + + def set_should_quit(self): + """ + Sets the should_quit flag to True so that this thread + terminates the first chance it gets + """ + QtCore.QMutexLocker(self._should_quit_lock) + self._should_quit = True + self.wait() + + def start(self): + """ + Starts the thread and resets the should_quit flag + """ + with QtCore.QMutexLocker(self._should_quit_lock): + self._should_quit = False + + QtCore.QThread.start(self) + + def _download_config(self): + """ + Downloads the EIP config for the given provider + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + + assert self._provider_config, "We need a provider configuration!" + + logger.debug("Downloading EIP config for %s" % + (self._provider_config.get_domain(),)) + + download_config_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + self._eip_config = EIPConfig() + + if self._download_if_needed and \ + os.path.exists(os.path.join(self._eip_config.get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "eip-service.json")): + download_config_data[self.PASSED_KEY] = True + self.download_config.emit(download_config_data) + return True + + try: + res = self._session.get("%s/%s/%s/%s" % + (self._provider_config.get_api_uri(), + self._provider_config.get_api_version(), + "config", + "eip-service.json"), + verify=self._provider_config + .get_ca_cert_path()) + res.raise_for_status() + + eip_definition = res.content + + self._eip_config.load(data=eip_definition) + self._eip_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "eip-service.json"]) + + download_config_data[self.PASSED_KEY] = True + except Exception as e: + download_config_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting download_config %s" % (download_config_data,)) + self.download_config.emit(download_config_data) + + return download_config_data[self.PASSED_KEY] + + def _download_client_certificates(self): + """ + Downloads the EIP client certificate for the given provider + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + assert self._provider_config, "We need a provider configuration!" + assert self._eip_config, "We need an eip configuration!" + + logger.debug("Downloading EIP client certificate for %s" % + (self._provider_config.get_domain(),)) + + download_cert = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + client_cert_path = self._eip_config.\ + get_client_cert_path(self._provider_config, + about_to_download=True) + + if self._download_if_needed and \ + os.path.exists(client_cert_path): + download_cert[self.PASSED_KEY] = True + self.download_client_certificate.emit(download_cert) + return True + + try: + res = self._session.get("%s/%s/%s/" % + (self._provider_config.get_api_uri(), + self._provider_config.get_api_version(), + "cert"), + verify=self._provider_config + .get_ca_cert_path()) + res.raise_for_status() + + client_cert = res.content + + # TODO: check certificate validity + + try: + os.makedirs(os.path.dirname(client_cert_path)) + except OSError as e: + if e.errno == errno.EEXIST and \ + os.path.isdir(os.path.dirname(client_cert_path)): + pass + else: + raise + + with open(client_cert_path, "w") as f: + f.write(client_cert) + + download_cert[self.PASSED_KEY] = True + except Exception as e: + download_cert[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting download_client_certificates %s" % + (download_cert,)) + self.download_client_certificate.emit(download_cert) + + return download_cert[self.PASSED_KEY] + + def run_eip_setup_checks(self, provider_config, download_if_needed=False): + """ + Starts the checks needed for a new eip setup + + @param provider_config: Provider configuration + @type provider_config: ProviderConfig + """ + assert provider_config, "We need a provider config!" + assert isinstance(provider_config, ProviderConfig), "Expected " + \ + "ProviderConfig type, not %r" % (type(provider_config),) + + self._provider_config = provider_config + self._download_if_needed = download_if_needed + + QtCore.QMutexLocker(self._checks_lock) + self._checks = [ + self._download_config, + self._download_client_certificates + ] + + def run(self): + """ + Main run loop for this thread. Executes the checks. + """ + shouldContinue = False + while True: + if self.get_should_quit(): + logger.debug("Quitting provider bootstrap thread") + return + checkSomething = False + with QtCore.QMutexLocker(self._checks_lock): + if len(self._checks) > 0: + check = self._checks.pop(0) + shouldContinue = check() + checkSomething = True + if not shouldContinue: + logger.debug("Something went wrong with the checks, " + + "clearing...") + self._checks = [] + checkSomething = False + if not checkSomething: + self.usleep(self.IDLE_SLEEP_INTERVAL) + + +if __name__ == "__main__": + import sys + from functools import partial + app = QtGui.QApplication(sys.argv) + + import signal + + def sigint_handler(*args, **kwargs): + logger.debug('SIGINT catched. shutting down...') + bootstrapper_thread = args[0] + bootstrapper_thread.set_should_quit() + QtGui.QApplication.quit() + + def signal_tester(d): + print d + + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + eip_thread = EIPBootstrapper() + + sigint = partial(sigint_handler, eip_thread) + signal.signal(signal.SIGINT, sigint) + + timer = QtCore.QTimer() + timer.start(500) + timer.timeout.connect(lambda: None) + app.connect(app, QtCore.SIGNAL("aboutToQuit()"), + eip_thread.set_should_quit) + w = QtGui.QWidget() + w.resize(100, 100) + w.show() + + eip_thread.start() + + provider_config = ProviderConfig() + if provider_config.load(os.path.join("leap", + "providers", + "bitmask.net", + "provider.json")): + eip_thread.run_eip_setup_checks(provider_config) + + sys.exit(app.exec_()) diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py new file mode 100644 index 00000000..ac06fef1 --- /dev/null +++ b/src/leap/services/eip/eipconfig.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# eipconfig.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Provider configuration +""" +import os +import logging + +from leap.config.baseconfig import BaseConfig +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.eipspec import eipservice_config_spec + +logger = logging.getLogger(__name__) + + +class EIPConfig(BaseConfig): + """ + Provider configuration abstraction class + """ + + def __init__(self): + BaseConfig.__init__(self) + + def _get_spec(self): + """ + Returns the spec object for the specific configuration + """ + return eipservice_config_spec + + def get_clusters(self): + # TODO: create an abstraction for clusters + return self._safe_get_value("clusters") + + def get_gateways(self): + # TODO: create an abstraction for gateways + return self._safe_get_value("gateways") + + def get_openvpn_configuration(self): + return self._safe_get_value("openvpn_configuration") + + def get_serial(self): + return self._safe_get_value("serial") + + def get_version(self): + return self._safe_get_value("version") + + def get_gateway_ip(self, index=0): + gateways = self.get_gateways() + assert len(gateways) > 0, "We don't have any gateway!" + if index > len(gateways): + index = 0 + logger.warning("Provided an unknown gateway index %s, " + + "defaulting to 0") + return gateways[0]["ip_address"] + + def get_client_cert_path(self, + providerconfig=None, + about_to_download=False): + """ + Returns the path to the certificate used by openvpn + """ + + assert providerconfig, "We need a provider" + assert isinstance(providerconfig, ProviderConfig), "The provider " + \ + "needs to be of type ProviderConfig instead of %s" % \ + (type(providerconfig),) + + cert_path = os.path.join(self.get_path_prefix(), + "leap", + "providers", + providerconfig.get_domain(), + "keys", + "client", + "openvpn.pem") + + if not about_to_download: + assert os.path.exists(cert_path), \ + "You need to download the certificate first" + logger.debug("Using OpenVPN cert %s" % (cert_path,)) + + return cert_path + + +if __name__ == "__main__": + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + eipconfig = EIPConfig() + + try: + eipconfig.get_clusters() + except Exception as e: + assert isinstance(e, AssertionError), "Expected an assert" + print "Safe value getting is working" + + if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): + print eipconfig.get_clusters() + print eipconfig.get_gateways() + print eipconfig.get_openvpn_configuration() + print eipconfig.get_serial() + print eipconfig.get_version() diff --git a/src/leap/services/eip/eipspec.py b/src/leap/services/eip/eipspec.py new file mode 100644 index 00000000..d5c73056 --- /dev/null +++ b/src/leap/services/eip/eipspec.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# eipspec.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +eipservice_config_spec = { + 'description': 'sample eip service config', + 'type': 'object', + 'required': [ + 'serial', + 'version' + ], + 'properties': { + 'serial': { + 'type': int, + 'default': 1 + }, + 'version': { + 'type': int, + 'default': 1 + }, + 'clusters': { + 'type': list, + 'default': [ + {"label": { + "en": "Location Unknown"}, + "name": "location_unknown"}] + }, + 'gateways': { + 'type': list, + 'default': [ + {"capabilities": { + "adblock": True, + "filter_dns": True, + "ports": ["80", "53", "443", "1194"], + "protocols": ["udp", "tcp"], + "transport": ["openvpn"], + "user_ips": False}, + "cluster": "location_unknown", + "host": "location.example.org", + "ip_address": "127.0.0.1"}] + }, + 'openvpn_configuration': { + 'type': dict, + 'default': { + "auth": None, + "cipher": None, + "tls-cipher": None} + } + } +} diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py new file mode 100644 index 00000000..babcd47b --- /dev/null +++ b/src/leap/services/eip/providerbootstrapper.py @@ -0,0 +1,520 @@ +# -*- coding: utf-8 -*- +# providerbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Provider bootstrapping +""" + +import requests +import logging +import socket +import os +import errno + +from OpenSSL import crypto +from PySide import QtGui, QtCore + +from leap.config.providerconfig import ProviderConfig + +logger = logging.getLogger(__name__) + + +class ProviderBootstrapper(QtCore.QThread): + """ + Given a provider URL performs a series of checks and emits signals + after they are passed. + If a check fails, the subsequent checks are not executed + """ + + PASSED_KEY = "passed" + ERROR_KEY = "error" + + IDLE_SLEEP_INTERVAL = 100 + + # All dicts returned are of the form + # {"passed": bool, "error": str} + name_resolution = QtCore.Signal(dict) + https_connection = QtCore.Signal(dict) + download_provider_info = QtCore.Signal(dict) + + download_ca_cert = QtCore.Signal(dict) + check_ca_fingerprint = QtCore.Signal(dict) + check_api_certificate = QtCore.Signal(dict) + + def __init__(self): + QtCore.QThread.__init__(self) + + self._checks = [] + self._checks_lock = QtCore.QMutex() + + self._should_quit = False + self._should_quit_lock = QtCore.QMutex() + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + # **************************************************** # + + self._session = self._fetcher.session() + self._domain = None + self._provider_config = None + self._download_if_needed = False + + def get_should_quit(self): + """ + Returns wether this thread should quit + + @rtype: bool + @return: True if the thread should terminate itself, Flase otherwise + """ + + QtCore.QMutexLocker(self._should_quit_lock) + return self._should_quit + + def set_should_quit(self): + """ + Sets the should_quit flag to True so that this thread + terminates the first chance it gets + """ + QtCore.QMutexLocker(self._should_quit_lock) + self._should_quit = True + self.wait() + + def start(self): + """ + Starts the thread and resets the should_quit flag + """ + with QtCore.QMutexLocker(self._should_quit_lock): + self._should_quit = False + + QtCore.QThread.start(self) + + def _should_proceed_provider(self): + """ + Returns False if provider.json already exists for the given + domain. True otherwise + + @rtype: bool + """ + if not self._download_if_needed: + return True + + # We don't really need a provider config at this stage, just + # the path prefix + return not os.path.exists(os.path.join(ProviderConfig() + .get_path_prefix(), + "leap", + "providers", + self._domain, + "provider.json")) + + def _check_name_resolution(self): + """ + Checks that the name resolution for the provider name works + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + + assert self._domain, "Cannot check DNS without a domain" + + logger.debug("Checking name resolution for %s" % (self._domain)) + + name_resolution_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + # We don't skip this check, since it's basic for the whole + # system to work + try: + socket.gethostbyname(self._domain) + name_resolution_data[self.PASSED_KEY] = True + except socket.gaierror as e: + name_resolution_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting name_resolution %s" % (name_resolution_data,)) + self.name_resolution.emit(name_resolution_data) + + return name_resolution_data[self.PASSED_KEY] + + def _check_https(self): + """ + Checks that https is working and that the provided certificate + checks out + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + + assert self._domain, "Cannot check HTTPS without a domain" + + logger.debug("Checking https for %s" % (self._domain)) + + https_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + # We don't skip this check, since it's basic for the whole + # system to work + + try: + res = self._session.get("https://%s" % (self._domain,)) + res.raise_for_status() + https_data[self.PASSED_KEY] = True + except Exception as e: + https_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting https_connection %s" % (https_data,)) + self.https_connection.emit(https_data) + + return https_data[self.PASSED_KEY] + + def _download_provider_info(self): + """ + Downloads the provider.json defition + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + assert self._domain, "Cannot download provider info without a domain" + + logger.debug("Downloading provider info for %s" % (self._domain)) + + download_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + if not self._should_proceed_provider(): + download_data[self.PASSED_KEY] = True + self.download_provider_info.emit(download_data) + return True + + try: + res = self._session.get("https://%s/%s" % (self._domain, + "provider.json")) + res.raise_for_status() + + provider_definition = res.content + + provider_config = ProviderConfig() + provider_config.load(data=provider_definition) + provider_config.save(["leap", + "providers", + self._domain, + "provider.json"]) + + download_data[self.PASSED_KEY] = True + except Exception as e: + download_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting download_provider_info %s" % (download_data,)) + self.download_provider_info.emit(download_data) + + return download_data[self.PASSED_KEY] + + def run_provider_select_checks(self, domain, download_if_needed=False): + """ + Populates the check queue + + @param domain: domain to check + @type domain: str + @param download_if_needed: if True, makes the checks do not + overwrite already downloaded data + @type download_if_needed: bool + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + assert domain and len(domain) > 0, "We need a domain!" + + self._domain = domain + self._download_if_needed = download_if_needed + + QtCore.QMutexLocker(self._checks_lock) + self._checks = [ + self._check_name_resolution, + self._check_https, + self._download_provider_info + ] + + def _should_proceed_cert(self): + """ + Returns False if the certificate already exists for the given + provider. True otherwise + + @rtype: bool + """ + assert self._provider_config, "We need a provider config!" + + if not self._download_if_needed: + return True + + return not os.path.exists(self._provider_config + .get_ca_cert_path(about_to_download=True)) + + def _download_ca_cert(self): + """ + Downloads the CA cert that is going to be used for the api URL + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + + assert self._provider_config, "Cannot download the ca cert " + \ + "without a provider config!" + + logger.debug("Downloading ca cert for %s at %s" % + (self._domain, self._provider_config.get_ca_cert_uri())) + + download_ca_cert_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + if not self._should_proceed_cert(): + download_ca_cert_data[self.PASSED_KEY] = True + self.download_ca_cert.emit(download_ca_cert_data) + return True + + try: + res = self._session.get(self._provider_config.get_ca_cert_uri()) + res.raise_for_status() + + cert_path = self._provider_config.get_ca_cert_path( + about_to_download=True) + + cert_dir = os.path.dirname(cert_path) + + try: + os.makedirs(cert_dir) + except OSError as e: + if e.errno == errno.EEXIST and os.path.isdir(cert_dir): + pass + else: + raise + + with open(cert_path, "w") as f: + f.write(res.content) + + download_ca_cert_data[self.PASSED_KEY] = True + except Exception as e: + download_ca_cert_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting download_ca_cert %s" % (download_ca_cert_data,)) + self.download_ca_cert.emit(download_ca_cert_data) + + return download_ca_cert_data[self.PASSED_KEY] + + def _check_ca_fingerprint(self): + """ + Checks the CA cert fingerprint against the one provided in the + json definition + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + assert self._provider_config, "Cannot check the ca cert " + \ + "without a provider config!" + + logger.debug("Checking ca fingerprint for %s and cert %s" % + (self._domain, + self._provider_config.get_ca_cert_path())) + + check_ca_fingerprint_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + if not self._should_proceed_cert(): + check_ca_fingerprint_data[self.PASSED_KEY] = True + self.check_ca_fingerprint.emit(check_ca_fingerprint_data) + return True + + try: + parts = self._provider_config.get_ca_cert_fingerprint().split(":") + assert len(parts) == 2, "Wrong fingerprint format" + + method = parts[0].strip() + fingerprint = parts[1].strip() + cert_data = None + with open(self._provider_config.get_ca_cert_path()) as f: + cert_data = f.read() + + assert len(cert_data) > 0, "Could not read certificate data" + + x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_data) + digest = x509.digest(method).replace(":", "").lower() + + assert digest == fingerprint, \ + "Downloaded certificate has a different fingerprint!" + + check_ca_fingerprint_data[self.PASSED_KEY] = True + except Exception as e: + check_ca_fingerprint_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting check_ca_fingerprint %s" % + (check_ca_fingerprint_data,)) + self.check_ca_fingerprint.emit(check_ca_fingerprint_data) + + return check_ca_fingerprint_data[self.PASSED_KEY] + + def _check_api_certificate(self): + """ + Tries to make an API call with the downloaded cert and checks + if it validates against it + + @return: True if the checks passed, False otherwise + @rtype: bool + """ + assert self._provider_config, "Cannot check the ca cert " + \ + "without a provider config!" + + logger.debug("Checking api certificate for %s and cert %s" % + (self._provider_config.get_api_uri(), + self._provider_config.get_ca_cert_path())) + + check_api_certificate_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + if not self._should_proceed_cert(): + check_api_certificate_data[self.PASSED_KEY] = True + self.check_api_certificate.emit(check_api_certificate_data) + return True + + try: + test_uri = "%s/%s/cert" % (self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + res = self._session.get(test_uri, + verify=self._provider_config + .get_ca_cert_path()) + res.raise_for_status() + check_api_certificate_data[self.PASSED_KEY] = True + except Exception as e: + check_api_certificate_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting check_api_certificate %s" % + (check_api_certificate_data,)) + self.check_api_certificate.emit(check_api_certificate_data) + + return check_api_certificate_data[self.PASSED_KEY] + + def run_provider_setup_checks(self, provider_config, + download_if_needed=False): + """ + Starts the checks needed for a new provider setup + + @param provider_config: Provider configuration + @type provider_config: ProviderConfig + @param download_if_needed: if True, makes the checks do not + overwrite already downloaded data + @type download_if_needed: bool + """ + assert provider_config, "We need a provider config!" + assert isinstance(provider_config, ProviderConfig), "Expected " + \ + "ProviderConfig type, not %r" % (type(provider_config),) + + self._provider_config = provider_config + self._download_if_needed = download_if_needed + + QtCore.QMutexLocker(self._checks_lock) + self._checks = [ + self._download_ca_cert, + self._check_ca_fingerprint, + self._check_api_certificate + ] + + def run(self): + """ + Main run loop for this thread. Executes the checks. + """ + shouldContinue = False + while True: + if self.get_should_quit(): + logger.debug("Quitting provider bootstrap thread") + return + checkSomething = False + with QtCore.QMutexLocker(self._checks_lock): + if len(self._checks) > 0: + check = self._checks.pop(0) + shouldContinue = check() + checkSomething = True + if not shouldContinue: + logger.debug("Something went wrong with the checks, " + "clearing...") + self._checks = [] + checkSomething = False + if not checkSomething: + self.usleep(self.IDLE_SLEEP_INTERVAL) + + +if __name__ == "__main__": + import sys + from functools import partial + app = QtGui.QApplication(sys.argv) + + import signal + + def sigint_handler(*args, **kwargs): + logger.debug('SIGINT catched. shutting down...') + bootstrapper_thread = args[0] + bootstrapper_thread.set_should_quit() + QtGui.QApplication.quit() + + def signal_tester(d): + print d + + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + bootstrapper_thread = ProviderBootstrapper() + + sigint = partial(sigint_handler, bootstrapper_thread) + signal.signal(signal.SIGINT, sigint) + + timer = QtCore.QTimer() + timer.start(500) + timer.timeout.connect(lambda: None) + app.connect(app, QtCore.SIGNAL("aboutToQuit()"), + bootstrapper_thread.set_should_quit) + w = QtGui.QWidget() + w.resize(100, 100) + w.show() + + bootstrapper_thread.start() + bootstrapper_thread.run_provider_select_checks("bitmask.net") + + provider_config = ProviderConfig() + if provider_config.load(os.path.join("leap", + "providers", + "bitmask.net", + "provider.json")): + bootstrapper_thread.run_provider_setup_checks(provider_config) + + sys.exit(app.exec_()) diff --git a/src/leap/services/eip/udstelnet.py b/src/leap/services/eip/udstelnet.py new file mode 100644 index 00000000..a47c24f4 --- /dev/null +++ b/src/leap/services/eip/udstelnet.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# udstelnet.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import socket +import telnetlib + + +class ConnectionRefusedError(Exception): + pass + + +class MissingSocketError(Exception): + pass + + +class UDSTelnet(telnetlib.Telnet): + """ + A telnet-alike class, that can listen on unix domain sockets + """ + + def open(self, host, port=23, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): + """ + Connect to a host. If port is 'unix', it will open a + connection over unix docmain sockets. + + The optional second argument is the port number, which + defaults to the standard telnet port (23). + + Don't try to reopen an already connected instance. + """ + self.eof = 0 + self.host = host + self.port = port + self.timeout = timeout + + if self.port == "unix": + # unix sockets spoken + if not os.path.exists(self.host): + raise MissingSocketError() + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + self.sock.connect(self.host) + except socket.error: + raise ConnectionRefusedError() + else: + self.sock = socket.create_connection((host, port), timeout) diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py new file mode 100644 index 00000000..f117cdbc --- /dev/null +++ b/src/leap/services/eip/vpn.py @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- +# vpn.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +VPN launcher and watcher thread +""" +import logging +import sys + +from PySide import QtCore, QtGui +from subprocess import Popen, PIPE +from functools import partial + +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.vpnlaunchers import get_platform_launcher +from leap.services.eip.eipconfig import EIPConfig +from leap.services.eip.udstelnet import UDSTelnet + +logger = logging.getLogger(__name__) +ON_POSIX = 'posix' in sys.builtin_module_names + + +# TODO: abstract the thread that can be asked to quit to another +# generic class that Fetcher and VPN inherit from +class VPN(QtCore.QThread): + """ + VPN launcher and watcher thread. It will emit signals based on + different events caught by the management interface + """ + + state_changed = QtCore.Signal(dict) + status_changed = QtCore.Signal(dict) + + CONNECTION_RETRY_TIME = 1000 + POLL_TIME = 100 + + TS_KEY = "ts" + STATUS_STEP_KEY = "status_step" + OK_KEY = "ok" + IP_KEY = "ip" + REMOTE_KEY = "remote" + + TUNTAP_READ_KEY = "tun_tap_read" + TUNTAP_WRITE_KEY = "tun_tap_write" + TCPUDP_READ_KEY = "tcp_udp_read" + TCPUDP_WRITE_KEY = "tcp_udp_write" + AUTH_READ_KEY = "auth_read" + + def __init__(self): + QtCore.QThread.__init__(self) + + self._should_quit = False + self._should_quit_lock = QtCore.QMutex() + + self._launcher = get_platform_launcher() + self._subp = None + self._started = False + + self._tn = None + self._host = None + self._port = None + + self._last_state = None + self._last_status = None + + def get_should_quit(self): + """ + Returns wether this thread should quit + + @rtype: bool + @return: True if the thread should terminate itself, Flase otherwise + """ + QtCore.QMutexLocker(self._should_quit_lock) + return self._should_quit + + def set_should_quit(self): + """ + Sets the should_quit flag to True so that this thread + terminates the first chance it gets. + Also terminates the VPN process and the connection to it + """ + QtCore.QMutexLocker(self._should_quit_lock) + self._should_quit = True + if self._tn is None or self._subp is None: + return + + try: + self._disconnect() + self._subp.terminate() + except Exception as e: + logger.debug("Could not terminate process, trying command " + + "signal SIGNINT: %r" % (e,)) + self._send_command("signal SIGINT") + self._subp.wait() + self.wait() + self._started = False + + def start(self, eipconfig, providerconfig, socket_host, socket_port): + """ + Launches OpenVPN and starts the thread to watch its output + + @param eipconfig: eip configuration object + @type eipconfig: EIPConfig + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + @param socket_host: either socket path (unix) or socket IP + @type socket_host: str + @param socket_port: either string "unix" if it's a unix + socket, or port otherwise + @type socket_port: str + """ + assert eipconfig, "We need an eip config" + assert isinstance(eipconfig, EIPConfig), "Expected EIPConfig " + \ + "object instead of %s" % (type(eipconfig),) + assert providerconfig, "We need a provider config" + assert isinstance(providerconfig, ProviderConfig), "Expected " + \ + "ProviderConfig object instead of %s" % (type(providerconfig),) + assert not self._started, "Starting process more than once!" + + logger.debug("Starting VPN...") + + with QtCore.QMutexLocker(self._should_quit_lock): + self._should_quit = False + + command = self._launcher.get_vpn_command(eipconfig=eipconfig, + providerconfig=providerconfig, + socket_host=socket_host, + socket_port=socket_port) + try: + self._subp = Popen(command, stdout=PIPE, stderr=PIPE, + bufsize=1, close_fds=ON_POSIX) + + self._host = socket_host + self._port = socket_port + + self._started = True + + QtCore.QThread.start(self) + except Exception as e: + logger.warning("Something went wrong while starting OpenVPN: %r" % + (e,)) + + def _connect(self, socket_host, socket_port): + """ + Connects to the specified socket_host socket_port + @param socket_host: either socket path (unix) or socket IP + @type socket_host: str + @param socket_port: either string "unix" if it's a unix + socket, or port otherwise + @type socket_port: str + """ + try: + self._tn = UDSTelnet(socket_host, socket_port) + + # XXX make password optional + # specially for win. we should generate + # the pass on the fly when invoking manager + # from conductor + + # self.tn.read_until('ENTER PASSWORD:', 2) + # self.tn.write(self.password + '\n') + # self.tn.read_until('SUCCESS:', 2) + if self._tn: + self._tn.read_eager() + except Exception as e: + logger.warning("Could not connect to OpenVPN yet: %r" % (e,)) + self._tn = None + + def _disconnect(self): + """ + Disconnects the telnet connection to the openvpn process + """ + logger.debug('Closing socket') + self._tn.write("quit\n") + self._tn.read_all() + self._tn.close() + self._tn = None + + def _send_command(self, command, until=b"END"): + """ + Sends a command to the telnet connection and reads until END + is reached + + @param command: command to send + @type command: str + @param until: byte delimiter string for reading command output + @type until: byte str + @return: response read + @rtype: list + """ + assert self._tn, "We need a tn connection!" + try: + self._tn.write("%s\n" % (command,)) + buf = self._tn.read_until(until, 2) + self._tn.read_eager() + lines = buf.split("\n") + return lines + except Exception as e: + logger.warning("Error sending command %s: %r" % + (command, e)) + return [] + + def _parse_state_and_notify(self, output): + """ + Parses the output of the state command and emits state_changed + signal when the state changes + + @param output: list of lines that the state command printed as + its output + @type output: list + """ + for line in output: + stripped = line.strip() + if stripped == "END": + continue + parts = stripped.split(",") + if len(parts) < 5: + continue + ts, status_step, ok, ip, remote = parts + + state_dict = { + self.TS_KEY: ts, + self.STATUS_STEP_KEY: status_step, + self.OK_KEY: ok, + self.IP_KEY: ip, + self.REMOTE_KEY: remote + } + + if state_dict != self._last_state: + self.state_changed.emit(state_dict) + self._last_state = state_dict + + def _parse_status_and_notify(self, output): + """ + Parses the output of the status command and emits + status_changed signal when the status changes + + @param output: list of lines that the status command printed + as its output + @type output: list + """ + tun_tap_read = "" + tun_tap_write = "" + tcp_udp_read = "" + tcp_udp_write = "" + auth_read = "" + for line in output: + stripped = line.strip() + if stripped.endswith("STATISTICS") or stripped == "END": + continue + parts = stripped.split(",") + if len(parts) < 2: + continue + if parts[0].strip() == "TUN/TAP read bytes": + tun_tap_read = parts[1] + elif parts[0].strip() == "TUN/TAP write bytes": + tun_tap_write = parts[1] + elif parts[0].strip() == "TCP/UDP read bytes": + tcp_udp_read = parts[1] + elif parts[0].strip() == "TCP/UDP write bytes": + tcp_udp_write = parts[1] + elif parts[0].strip() == "Auth read bytes": + auth_read = parts[1] + + status_dict = { + self.TUNTAP_READ_KEY: tun_tap_read, + self.TUNTAP_WRITE_KEY: tun_tap_write, + self.TCPUDP_READ_KEY: tcp_udp_read, + self.TCPUDP_WRITE_KEY: tcp_udp_write, + self.AUTH_READ_KEY: auth_read + } + + if status_dict != self._last_status: + self.status_changed.emit(status_dict) + self._last_status = status_dict + + def run(self): + """ + Main run loop for this thread + """ + while True: + if self.get_should_quit(): + logger.debug("Quitting VPN thread") + return + + if self._tn is None: + self._connect(self._host, self._port) + QtCore.QThread.msleep(self.CONNECTION_RETRY_TIME) + else: + self._parse_state_and_notify(self._send_command("state")) + self._parse_status_and_notify(self._send_command("status")) + QtCore.QThread.msleep(self.POLL_TIME) + + +if __name__ == "__main__": + app = QtGui.QApplication(sys.argv) + + import signal + + def sigint_handler(*args, **kwargs): + logger.debug('SIGINT catched. shutting down...') + vpn_thread = args[0] + vpn_thread.set_should_quit() + QtGui.QApplication.quit() + + def signal_tester(d): + print d + + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + vpn_thread = VPN() + + sigint = partial(sigint_handler, vpn_thread) + signal.signal(signal.SIGINT, sigint) + + eipconfig = EIPConfig() + if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): + provider = ProviderConfig() + if provider.load("leap/providers/bitmask.net/provider.json"): + vpn_thread.start(eipconfig=eipconfig, + providerconfig=provider, + socket_host="/home/chiiph/vpnsock", + socket_port="unix") + + timer = QtCore.QTimer() + timer.start(500) + timer.timeout.connect(lambda: None) + app.connect(app, QtCore.SIGNAL("aboutToQuit()"), + vpn_thread.set_should_quit) + w = QtGui.QWidget() + w.resize(100, 100) + w.show() + + vpn_thread.state_changed.connect(signal_tester) + vpn_thread.status_changed.connect(signal_tester) + + sys.exit(app.exec_()) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py new file mode 100644 index 00000000..68978248 --- /dev/null +++ b/src/leap/services/eip/vpnlaunchers.py @@ -0,0 +1,270 @@ +# -*- coding: utf-8 -*- +# vpnlaunchers.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Platform dependant VPN launchers +""" +import os +import platform +import logging +import commands +import getpass +import grp + +from abc import ABCMeta, abstractmethod + +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.eipconfig import EIPConfig + +logger = logging.getLogger(__name__) + + +class VPNLauncher: + """ + Abstract launcher class + """ + + __metaclass__ = ABCMeta + + # TODO: document parameters + @abstractmethod + def get_vpn_command(self, eipconfig=None, providerconfig=None, + socket_host=None, socket_port=None): + """ + Returns the platform dependant vpn launching command + + @param eipconfig: eip configuration object + @type eipconfig: EIPConfig + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + @param socket_host: either socket path (unix) or socket IP + @type socket_host: str + @param socket_port: either string "unix" if it's a unix + socket, or port otherwise + @type socket_port: str + + @return: A VPN command ready to be launched + @rtype: list + """ + return [] + + +def get_platform_launcher(): + launcher = globals()[platform.system() + "VPNLauncher"] + assert launcher, "Unimplemented platform launcher: %s" % \ + (platform.system(),) + return launcher() + + +# Twisted implementation of which +def which(name, flags=os.X_OK): + """ + Search PATH for executable files with the given name. + + On newer versions of MS-Windows, the PATHEXT environment variable will be + set to the list of file extensions for files considered executable. This + will normally include things like ".EXE". This fuction will also find files + with the given name ending with any of these extensions. + + On MS-Windows the only flag that has any meaning is os.F_OK. Any other + flags will be ignored. + + @type name: C{str} + @param name: The name for which to search. + + @type flags: C{int} + @param flags: Arguments to L{os.access}. + + @rtype: C{list} + @param: A list of the full paths to files found, in the + order in which they were found. + """ + + # TODO: make sure sbin is in path + + result = [] + exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep)) + path = os.environ.get('PATH', None) + if path is None: + return [] + for p in os.environ.get('PATH', '').split(os.pathsep): + p = os.path.join(p, name) + if os.access(p, flags): + result.append(p) + for e in exts: + pext = p + e + if os.access(pext, flags): + result.append(pext) + return result + + +def _is_pkexec_in_system(): + pkexec_path = which('pkexec') + if len(pkexec_path) == 0: + return False + return True + + +def _has_updown_scripts(path): + """ + Checks the existence of the up/down scripts + """ + # XXX should check permissions too + is_file = os.path.isfile(path) + if not is_file: + logger.warning("Could not find up/down scripts. " + + "Might produce DNS leaks.") + return is_file + + +def _is_auth_agent_running(): + return bool( + commands.getoutput( + 'ps aux | grep polkit-[g]nome-authentication-agent-1')) + + +class LinuxVPNLauncher(VPNLauncher): + """ + VPN launcher for the Linux platform + """ + + PKEXEC_BIN = 'pkexec' + OPENVPN_BIN = 'openvpn' + UP_DOWN_SCRIPT = "/etc/leap/resolv-update" + OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" + + def get_vpn_command(self, eipconfig=None, providerconfig=None, + socket_host=None, socket_port="unix"): + """ + Returns the platform dependant vpn launching command + + @param eipconfig: eip configuration object + @type eipconfig: EIPConfig + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + @param socket_host: either socket path (unix) or socket IP + @type socket_host: str + @param socket_port: either string "unix" if it's a unix + socket, or port otherwise + @type socket_port: str + + @return: A VPN command ready to be launched + @rtype: list + """ + assert eipconfig, "We need an eip config" + assert isinstance(eipconfig, EIPConfig), "Expected EIPConfig " + \ + "object instead of %s" % (type(eipconfig),) + assert providerconfig, "We need a provider config" + assert isinstance(providerconfig, ProviderConfig), "Expected " + \ + "ProviderConfig object instead of %s" % (type(providerconfig),) + assert socket_host, "We need a socket host!" + assert socket_port, "We need a socket port!" + + openvpn_possibilities = which(self.OPENVPN_BIN) + assert len(openvpn_possibilities) > 0, "We couldn't find openvpn" + + openvpn = openvpn_possibilities[0] + args = [] + + if _is_pkexec_in_system(): + if _is_auth_agent_running(): + pkexec_possibilities = which(self.PKEXEC_BIN) + assert len(pkexec_possibilities) > 0, "We couldn't find pkexec" + args.append(openvpn) + openvpn = pkexec_possibilities[0] + else: + logger.warning("No polkit auth agent found. pkexec " + + "will use its own auth agent.") + else: + logger.warning("System has no pkexec") + + # TODO: handle verbosity + + gateway_ip = str(eipconfig.get_gateway_ip(0)) + + logger.debug("Using gateway ip %s" % (gateway_ip,)) + + args += [ + '--client', + '--dev', 'tun', + '--persist-tun', + '--persist-key', + '--remote', gateway_ip, '1194', 'udp', + '--tls-client', + '--remote-cert-tls', + 'server' + ] + + openvpn_configuration = eipconfig.get_openvpn_configuration() + for key, value in openvpn_configuration.items(): + args += ['--%s' % (key,), value] + + args += [ + '--user', getpass.getuser(), + '--group', grp.getgrgid(os.getgroups()[-1]).gr_name, + '--management-client-user', getpass.getuser(), + '--management-signal', + '--management', socket_host, socket_port, + '--script-security', '2' + ] + + if _has_updown_scripts(self.UP_DOWN_SCRIPT): + args += [ + '--up', self.UP_DOWN_SCRIPT, + '--down', self.UP_DOWN_SCRIPT, + '--plugin', self.OPENVPN_DOWN_ROOT, + '\'script_type=down %s\'' % self.UP_DOWN_SCRIPT + ] + + args += [ + '--cert', eipconfig.get_client_cert_path(providerconfig), + '--key', eipconfig.get_client_cert_path(providerconfig), + '--ca', providerconfig.get_ca_cert_path() + ] + + logger.debug("Running VPN with command:") + logger.debug("%s %s" % (openvpn, " ".join(args))) + + return [openvpn] + args + + +if __name__ == "__main__": + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + try: + abs_launcher = VPNLauncher() + except Exception as e: + assert isinstance(e, TypeError), "Something went wrong" + print "Abstract Prefixer class is working as expected" + + vpnlauncher = get_platform_launcher() + + eipconfig = EIPConfig() + if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): + provider = ProviderConfig() + if provider.load("leap/providers/bitmask.net/provider.json"): + vpnlauncher.get_vpn_command(eipconfig=eipconfig, + providerconfig=provider, + socket_host="/blah") -- cgit v1.2.3 From 361a18b0e727a68d6e0d1e9d03273630b9c14692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 6 Mar 2013 15:43:36 -0300 Subject: Add UI merging all the code Also add resources --- src/leap/gui/__init__.py | 0 src/leap/gui/mainwindow.py | 600 +++++++++++++++++++++++++++++++ src/leap/gui/ui/mainwindow.ui | 377 ++++++++++++++++++++ src/leap/gui/ui/wizard.ui | 800 ++++++++++++++++++++++++++++++++++++++++++ src/leap/gui/wizard.py | 403 +++++++++++++++++++++ src/leap/gui/wizardpage.py | 39 ++ 6 files changed, 2219 insertions(+) create mode 100644 src/leap/gui/__init__.py create mode 100644 src/leap/gui/mainwindow.py create mode 100644 src/leap/gui/ui/mainwindow.ui create mode 100644 src/leap/gui/ui/wizard.ui create mode 100644 src/leap/gui/wizard.py create mode 100644 src/leap/gui/wizardpage.py (limited to 'src') diff --git a/src/leap/gui/__init__.py b/src/leap/gui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py new file mode 100644 index 00000000..1821e4a6 --- /dev/null +++ b/src/leap/gui/mainwindow.py @@ -0,0 +1,600 @@ +# -*- coding: utf-8 -*- +# mainwindow.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Main window for the leap client +""" +import os +import logging + +from PySide import QtCore, QtGui + +from ui_mainwindow import Ui_MainWindow +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpauth import SRPAuth +from leap.services.eip.vpn import VPN +from leap.services.eip.providerbootstrapper import ProviderBootstrapper +from leap.services.eip.eipbootstrapper import EIPBootstrapper +from leap.services.eip.eipconfig import EIPConfig +from leap.gui.wizard import Wizard + +logger = logging.getLogger(__name__) + + +class MainWindow(QtGui.QMainWindow): + """ + Main window for login and presenting status updates to the user + """ + + # StackedWidget indexes + LOGIN_INDEX = 0 + EIP_STATUS_INDEX = 1 + + def __init__(self): + QtGui.QMainWindow.__init__(self) + + self.CONNECTING_ICON = QtGui.QPixmap(":/images/conn_connecting.png") + self.CONNECTED_ICON = QtGui.QPixmap(":/images/conn_connected.png") + self.ERROR_ICON = QtGui.QPixmap(":/images/conn_error.png") + + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + + self.ui.lnPassword.setEchoMode(QtGui.QLineEdit.Password) + + self.ui.btnLogin.clicked.connect(self._login) + self.ui.lnUser.returnPressed.connect(self._focus_password) + self.ui.lnPassword.returnPressed.connect(self._login) + + self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) + + # This is loaded only once, there's a bug when doing that more + # than once + self._provider_config = ProviderConfig() + self._eip_config = EIPConfig() + # This is created once we have a valid provider config + self._srp_auth = None + + # This thread is always running, although it's quite + # lightweight when it's done setting up provider + # configuration and certificate. + self._provider_bootstrapper = ProviderBootstrapper() + + # TODO: add sigint handler + + # Intermediate stages, only do something if there was an error + self._provider_bootstrapper.name_resolution.connect( + self._intermediate_stage) + self._provider_bootstrapper.https_connection.connect( + self._intermediate_stage) + self._provider_bootstrapper.download_ca_cert.connect( + self._intermediate_stage) + + # Important stages, loads the provider config and checks + # certificates + self._provider_bootstrapper.download_provider_info.connect( + self._load_provider_config) + self._provider_bootstrapper.check_api_certificate.connect( + self._provider_config_loaded) + + # This thread is similar to the provider bootstrapper + self._eip_bootstrapper = EIPBootstrapper() + + self._eip_bootstrapper.download_config.connect( + self._intermediate_stage) + self._eip_bootstrapper.download_client_certificate.connect( + self._start_eip) + + self._vpn = VPN() + self._vpn.state_changed.connect(self._update_vpn_state) + self._vpn.status_changed.connect(self._update_vpn_status) + + QtCore.QCoreApplication.instance().connect( + QtCore.QCoreApplication.instance(), + QtCore.SIGNAL("aboutToQuit()"), + self._vpn.set_should_quit) + QtCore.QCoreApplication.instance().connect( + QtCore.QCoreApplication.instance(), + QtCore.SIGNAL("aboutToQuit()"), + self._provider_bootstrapper.set_should_quit) + QtCore.QCoreApplication.instance().connect( + QtCore.QCoreApplication.instance(), + QtCore.SIGNAL("aboutToQuit()"), + self._eip_bootstrapper.set_should_quit) + + self.ui.action_sign_out.setEnabled(False) + self.ui.action_sign_out.triggered.connect(self._logout) + self.ui.action_about_leap.triggered.connect(self._about) + self.ui.action_quit.triggered.connect(self.quit) + self.ui.action_wizard.triggered.connect(self._launch_wizard) + + # Used to differentiate between real quits and close to tray + self._really_quit = False + + self._systray = None + self._action_visible = QtGui.QAction("Hide", self) + self._action_visible.triggered.connect(self._toggle_visible) + + self._center_window() + self._wizard = None + if self._first_run(): + self._wizard = Wizard() + # Give this window time to finish init and then show the wizard + QtCore.QTimer.singleShot(1, self._launch_wizard) + self._wizard.finished.connect(self._finish_init) + else: + self._finish_init() + + def _launch_wizard(self): + if self._wizard is None: + self._wizard = Wizard() + self._wizard.exec_() + + def _finish_init(self): + self.ui.cmbProviders.addItems(self._configured_providers()) + self._show_systray() + self.show() + if self._wizard: + possible_username = self._wizard.get_username() + if possible_username is not None: + self.ui.lnUser.setText(possible_username) + self._focus_password() + self._wizard = None + + def _show_systray(self): + """ + Sets up the systray icon + """ + systrayMenu = QtGui.QMenu(self) + systrayMenu.addAction(self._action_visible) + systrayMenu.addAction(self.ui.action_sign_out) + systrayMenu.addSeparator() + systrayMenu.addAction(self.ui.action_quit) + self._systray = QtGui.QSystemTrayIcon(self) + self._systray.setContextMenu(systrayMenu) + self._systray.setIcon(QtGui.QIcon(self.ERROR_ICON)) + self._systray.setVisible(True) + self._systray.activated.connect(self._toggle_visible) + + def _toggle_visible(self): + """ + SLOT + TRIGGER: self._systray.activated + + Toggles the window visibility + """ + self.setVisible(not self.isVisible()) + action_visible_text = "Hide" + if not self.isVisible(): + action_visible_text = "Show" + self._action_visible.setText(action_visible_text) + + def _center_window(self): + """ + Centers the mainwindow based on the desktop geometry + """ + app = QtGui.QApplication.instance() + width = app.desktop().width() + height = app.desktop().height() + window_width = self.size().width() + window_height = self.size().height() + x = (width / 2.0) - (window_width / 2.0) + y = (height / 2.0) - (window_height / 2.0) + self.move(x, y) + + def _about(self): + """ + Display the About LEAP dialog + """ + QtGui.QMessageBox.about(self, "About LEAP", + "LEAP is a non-profit dedicated to giving " + "all internet users access to secure " + "communication. Our focus is on adapting " + "encryption technology to make it easy to use " + "and widely available. " + "More about LEAP" + "") + + def quit(self): + self._really_quit = True + if self._wizard: + self._wizard.accept() + self.close() + + def changeEvent(self, e): + """ + Reimplements the changeEvent method to minimize to tray + """ + if QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \ + e.type() == QtCore.QEvent.WindowStateChange and \ + self.isMinimized(): + self._toggle_visible() + e.accept() + return + QtGui.QMainWindow.changeEvent(self, e) + + def closeEvent(self, e): + """ + Reimplementation of closeEvent to close to tray + """ + if QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \ + not self._really_quit: + self._toggle_visible() + e.ignore() + return + QtGui.QMainWindow.closeEvent(self, e) + + def _configured_providers(self): + """ + Returns the available providers based on the file structure + + @rtype: list + """ + providers = os.listdir( + os.path.join(self._provider_config.get_path_prefix(), + "leap", + "providers")) + return providers + + def _first_run(self): + """ + Returns True if there are no configured providers. False otherwise + + @rtype: bool + """ + return len(self._configured_providers()) == 0 + + def _focus_password(self): + """ + Focuses in the password lineedit + """ + self.ui.lnPassword.setFocus() + + def _set_status(self, status): + """ + Sets the status label at the login stage to status + + @param status: status message + @type status: str + """ + self.ui.lblStatus.setText(status) + + def _set_eip_status(self, status): + """ + Sets the status label at the VPN stage to status + + @param status: status message + @type status: str + """ + self.ui.lblEIPStatus.setText(status) + + def _login_set_enabled(self, enabled=False): + """ + Enables or disables all the login widgets + + @param enabled: wether they should be enabled or not + @type enabled: bool + """ + self.ui.lnUser.setEnabled(enabled) + self.ui.lnPassword.setEnabled(enabled) + self.ui.btnLogin.setEnabled(enabled) + self.ui.chkRemember.setEnabled(enabled) + self.ui.cmbProviders.setEnabled(enabled) + + def _download_provider_config(self): + """ + Starts the bootstrapping sequence. It will download the + provider configuration if it's not present, otherwise will + emit the corresponding signals inmediately + """ + provider = self.ui.cmbProviders.currentText() + + self._provider_bootstrapper.start() + self._provider_bootstrapper.run_provider_select_checks( + provider, + download_if_needed=True) + + def _load_provider_config(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.download_provider_info + + Once the provider config has been downloaded, this loads the + self._provider_config instance with it and starts the second + part of the bootstrapping sequence + + @param data: result from the last stage of the + run_provider_select_checks + @type data: dict + """ + if data[self._provider_bootstrapper.PASSED_KEY]: + provider = self.ui.cmbProviders.currentText() + if self._provider_config.loaded() or \ + self._provider_config.load(os.path.join("leap", + "providers", + provider, + "provider.json")): + self._provider_bootstrapper.run_provider_setup_checks( + self._provider_config, + download_if_needed=True) + else: + self._set_status("Could not load provider configuration") + self._login_set_enabled(True) + else: + self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_set_enabled(True) + + def _login(self): + """ + SLOT + TRIGGERS: + self.ui.btnLogin.clicked + self.ui.lnPassword.returnPressed + + Starts the login sequence. Which involves bootstrapping the + selected provider if the selection is valid (not empty), then + start the SRP authentication, and as the last step + bootstrapping the EIP service + """ + assert self._provider_config, "We need a provider config" + + username = self.ui.lnUser.text() + password = self.ui.lnPassword.text() + provider = self.ui.cmbProviders.currentText() + + if len(provider) == 0: + self._set_status("Please select a valid provider") + return + + if len(username) == 0: + self._set_status("Please provide a valid username") + return + + if len(password) == 0: + self._set_status("Please provide a valid Password") + return + + self._set_status("Logging in...") + self._login_set_enabled(False) + + self._download_provider_config() + + def _provider_config_loaded(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.check_api_certificate + + Once the provider configuration is loaded, this starts the SRP + authentication + """ + assert self._provider_config, "We need a provider config!" + + self._provider_bootstrapper.set_should_quit() + + if data[self._provider_bootstrapper.PASSED_KEY]: + username = self.ui.lnUser.text() + password = self.ui.lnPassword.text() + + if self._srp_auth is None: + self._srp_auth = SRPAuth(self._provider_config) + self._srp_auth.authentication_finished.connect( + self._authentication_finished) + self._srp_auth.logout_finished.connect( + self._done_logging_out) + + self._srp_auth.authenticate(username, password) + else: + self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_set_enabled(True) + + def _authentication_finished(self, ok, message): + """ + SLOT + TRIGGER: self._srp_auth.authentication_finished + + Once the user is properly authenticated, try starting the EIP + service + """ + self._set_status(message) + if ok: + self.ui.action_sign_out.setEnabled(True) + # We leave a bit of room for the user to see the + # "Succeeded" message and then we switch to the EIP status + # panel + QtCore.QTimer.singleShot(1000, self._switch_to_status) + else: + self._login_set_enabled(True) + + def _switch_to_status(self): + """ + Changes the stackedWidget index to the EIP status one and + triggers the eip bootstrapping + """ + self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) + self._download_eip_config() + + def _download_eip_config(self): + """ + Starts the EIP bootstrapping sequence + """ + assert self._eip_bootstrapper, "We need an eip bootstrapper!" + assert self._provider_config, "We need a provider config" + + self._set_eip_status("Checking configuration, please wait...") + + if self._provider_config.provides_eip(): + self._eip_bootstrapper.start() + self._eip_bootstrapper.run_eip_setup_checks( + self._provider_config, + download_if_needed=True) + else: + self._set_eip_status("%s does not support EIP" % + (self._provider_config.get_domain(),)) + + def _set_eip_status_icon(self, status): + """ + Given a status step from the VPN thread, set the icon properly + + @param status: status step + @type status: str + """ + selected_pixmap = self.ERROR_ICON + if status in ("AUTH", "GET_CONFIG"): + selected_pixmap = self.CONNECTING_ICON + elif status in ("CONNECTED"): + selected_pixmap = self.CONNECTED_ICON + + self.ui.lblVPNStatusIcon.setPixmap(selected_pixmap) + self._systray.setIcon(QtGui.QIcon(selected_pixmap)) + + def _update_vpn_state(self, data): + """ + SLOT + TRIGGER: self._vpn.state_changed + + Updates the displayed VPN state based on the data provided by + the VPN thread + """ + status = data[self._vpn.STATUS_STEP_KEY] + self._set_eip_status_icon(status) + if status == "AUTH": + self._set_eip_status("VPN: Authenticating...") + elif status == "GET_CONFIG": + self._set_eip_status("VPN: Retrieving configuration...") + elif status == "CONNECTED": + self._set_eip_status("VPN: Connected!") + else: + self._set_eip_status(status) + + def _update_vpn_status(self, data): + """ + SLOT + TRIGGER: self._vpn.status_changed + + Updates the download/upload labels based on the data provided + by the VPN thread + """ + upload = float(data[self._vpn.TUNTAP_WRITE_KEY]) + upload = upload / 1000.0 + self.ui.lblUpload.setText("%s Kb" % (upload,)) + download = float(data[self._vpn.TUNTAP_READ_KEY]) + download = download / 1000.0 + self.ui.lblDownload.setText("%s Kb" % (download,)) + + def _start_eip(self, data): + """ + SLOT + TRIGGER: self._eip_bootstrapper.download_client_certificate + + Starts the VPN thread if the eip configuration is properly + loaded + """ + assert self._eip_config, "We need an eip config!" + assert self._provider_config, "We need a provider config!" + + self._eip_bootstrapper.set_should_quit() + if self._eip_config.loaded() or \ + self._eip_config.load(os.path.join("leap", + "providers", + self._provider_config + .get_domain(), + "eip-service.json")): + self._vpn.start(eipconfig=self._eip_config, + providerconfig=self._provider_config, + socket_host="/home/chiiph/vpnsock", + socket_port="unix") + # TODO: display a message if the EIP configuration cannot be + # loaded + + def _logout(self): + """ + SLOT + TRIGGER: self.ui.action_sign_out.triggered + + Starts the logout sequence + """ + self._set_eip_status_icon("error") + self._set_eip_status("Signing out...") + self._srp_auth.logout() + + def _done_logging_out(self, ok, message): + """ + SLOT + TRIGGER: self._srp_auth.logout_finished + + Switches the stackedWidget back to the login stage after + logging out + """ + self._set_status(message) + self.ui.action_sign_out.setEnabled(False) + self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) + self.ui.lnPassword.setText("") + self._login_set_enabled(True) + self._set_status("") + self._vpn.set_should_quit() + + def _intermediate_stage(self, data): + """ + SLOT + TRIGGERS: + self._provider_bootstrapper.name_resolution + self._provider_bootstrapper.https_connection + self._provider_bootstrapper.download_ca_cert + self._eip_bootstrapper.download_config + + If there was a problem, displays it, otherwise it does nothing. + This is used for intermediate bootstrapping stages, in case + they fail. + """ + passed = data[self._provider_bootstrapper.PASSED_KEY] + if not passed: + self._login_set_enabled(True) + self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) + +if __name__ == "__main__": + import signal + from functools import partial + + def sigint_handler(*args, **kwargs): + logger.debug('SIGINT catched. shutting down...') + mainwindow = args[0] + mainwindow.quit() + + import sys + + logger = logging.getLogger(name='leap') + logger.setLevel(logging.DEBUG) + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + app = QtGui.QApplication(sys.argv) + mainwindow = MainWindow() + mainwindow.show() + + timer = QtCore.QTimer() + timer.start(500) + timer.timeout.connect(lambda: None) + + sigint = partial(sigint_handler, mainwindow) + signal.signal(signal.SIGINT, sigint) + + sys.exit(app.exec_()) diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui new file mode 100644 index 00000000..a527eaf6 --- /dev/null +++ b/src/leap/gui/ui/mainwindow.ui @@ -0,0 +1,377 @@ + + + MainWindow + + + + 0 + 0 + 405 + 579 + + + + LEAP + + + + :/images/leap-color-big.png:/images/leap-color-big.png + + + Qt::ImhHiddenText + + + + 128 + 128 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + 1 + + + + + + + + + + Remember + + + + + + + Login + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + <b>Provider:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + <b>Password:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + + <b>User:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Qt::AlignCenter + + + + + + + + + + + Disconnected + + + Qt::AlignCenter + + + + + + + + + + :/images/conn_error.png + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 70 + 0 + + + + 0.0 Kb + + + + + + + + 70 + 0 + + + + 0.0 Kb + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + :/images/Arrow-Up-32.png + + + + + + + + + + :/images/Arrow-Down-32.png + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + + + + :/images/leap-color-big.png + + + Qt::AlignCenter + + + + + + + + + 0 + 0 + 405 + 25 + + + + + &Session + + + + + + + + Help + + + + + + + + S&ettings + + + + + + + + + + + &Sign out + + + + + &Quit + + + + + About &LEAP + + + + + &Help + + + + + &Wizard + + + + + lnUser + lnPassword + chkRemember + btnLogin + cmbProviders + + + + + + diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui new file mode 100644 index 00000000..86f8d458 --- /dev/null +++ b/src/leap/gui/ui/wizard.ui @@ -0,0 +1,800 @@ + + + Wizard + + + + 0 + 0 + 536 + 452 + + + + LEAP First run + + + + :/images/leap-color-big.png:/images/leap-color-big.png + + + true + + + QWizard::ModernStyle + + + QWizard::IndependentPages + + + + Welcome + + + This is the LEAP Client first run wizard + + + 0 + + + + + + Log In with my credentials + + + + + + + <html><head/><body><p>New we will guide you through some configuration that is needed before you can connect for the first time.</p><p>If you ever need to modify these options again, you can find the wizard in the <span style=" font-style:italic;">'Settings'</span> menu from the main window.</p><p>Do you want to <span style=" font-weight:600;">sign up</span> for a new account, or <span style=" font-weight:600;">log in</span> with an already existing username?</p></body></html> + + + Qt::RichText + + + true + + + + + + + Sign up for a new account + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Provider selection + + + Please enter the domain of the provider you want to user for your connection + + + 1 + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 60 + + + + + + + + https:// + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Checking provider + + + + + + Download provider information + + + + + + + HTTPS Connection + + + + + + + + 0 + 0 + + + + + + + :/images/Emblem-question.png + + + + + + + + 0 + 0 + + + + + + + :/images/Emblem-question.png + + + + + + + + 0 + 0 + + + + + + + :/images/Emblem-question.png + + + + + + + Name resolution + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + + + + Check + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Provider Information + + + Services offered by this provider + + + 2 + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + <b>Enrollment policy:</b> + + + + + + + URL + + + + + + + Name + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + policy + + + + + + + Desc + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Provider setup + + + Gathering configuration options for this provider + + + 3 + + + + + + Checking provider + + + + + + + 0 + 0 + + + + + + + :/images/Emblem-question.png + + + + + + + + 0 + 0 + + + + + + + :/images/Emblem-question.png + + + + + + + Download CA Certificate + + + + + + + Check CA Certificate Fingerprint + + + + + + + Check API Certificate + + + + + + + + 0 + 0 + + + + + + + :/images/Emblem-question.png + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + + + + Qt::Vertical + + + + 20 + 60 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Register new user + + + Register a new user with provider + + + 4 + + + + + + + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <b>User:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + <b>Password:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + <b>Re-enter password:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Register + + + + + + + Qt::Vertical + + + + 20 + 60 + + + + + + + + + EIP Setup + + + Setting up Encrypted Internet + + + 5 + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Checking EIP + + + + + + Download EIP configuration + + + + + + + Download client certificate + + + + + + + + 0 + 0 + + + + + + + :/images/Emblem-question.png + + + + + + + + 0 + 0 + + + + + + + :/images/Emblem-question.png + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Congratulations! + + + You have successfully configured the LEAP client. + + + 6 + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + :/images/leap-color-big.png + + + + + + + + 0 + 0 + + + + + + + :/images/Globe.png + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + WizardPage + QWizardPage +
wizardpage.h
+ 1 +
+
+ + lblUser + lblPassword + lblPassword2 + btnRegister + rdoRegister + rdoLogin + lnProvider + btnCheck + + + + + +
diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py new file mode 100644 index 00000000..7dcc8dd6 --- /dev/null +++ b/src/leap/gui/wizard.py @@ -0,0 +1,403 @@ +# -*- coding: utf-8 -*- +# wizard.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +First run wizard +""" +import os +import logging + +from PySide import QtCore, QtGui + +from ui_wizard import Ui_Wizard +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpregister import SRPRegister +from leap.services.eip.providerbootstrapper import ProviderBootstrapper +from leap.services.eip.eipbootstrapper import EIPBootstrapper + +logger = logging.getLogger(__name__) + + +class Wizard(QtGui.QWizard): + """ + First run wizard to register a user and setup a provider + """ + + INTRO_PAGE = 0 + SELECT_PROVIDER_PAGE = 1 + PRESENT_PROVIDER_PAGE = 2 + SETUP_PROVIDER_PAGE = 3 + REGISTER_USER_PAGE = 4 + SETUP_EIP_PAGE = 5 + FINISH_PATH = 6 + + WEAK_PASSWORDS = ("1234", "12345", "123456", + "password") + + def __init__(self): + QtGui.QWizard.__init__(self) + + self.ui = Ui_Wizard() + self.ui.setupUi(self) + + self.QUESTION_ICON = QtGui.QPixmap(":/images/Emblem-question.png") + self.ERROR_ICON = QtGui.QPixmap(":/images/Dialog-error.png") + self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png") + + self._show_register = False + + self.ui.grpCheckProvider.setVisible(False) + self.ui.btnCheck.clicked.connect(self._check_provider) + self.ui.lnProvider.returnPressed.connect(self._check_provider) + + self._provider_bootstrapper = ProviderBootstrapper() + self._provider_bootstrapper.name_resolution.connect( + self._name_resolution) + self._provider_bootstrapper.https_connection.connect( + self._https_connection) + self._provider_bootstrapper.download_provider_info.connect( + self._download_provider_info) + + self._provider_bootstrapper.download_ca_cert.connect( + self._download_ca_cert) + self._provider_bootstrapper.check_ca_fingerprint.connect( + self._check_ca_fingerprint) + self._provider_bootstrapper.check_api_certificate.connect( + self._check_api_certificate) + + self._eip_bootstrapper = EIPBootstrapper() + + self._eip_bootstrapper.download_config.connect( + self._download_eip_config) + self._eip_bootstrapper.download_client_certificate.connect( + self._download_client_certificate) + + self._domain = None + self._provider_config = ProviderConfig() + + self.currentIdChanged.connect(self._current_id_changed) + + self.ui.lblPassword.setEchoMode(QtGui.QLineEdit.Password) + self.ui.lblPassword2.setEchoMode(QtGui.QLineEdit.Password) + + self.ui.lblUser.returnPressed.connect( + self._focus_password) + self.ui.lblPassword.returnPressed.connect( + self._focus_second_password) + self.ui.lblPassword2.returnPressed.connect( + self._register) + self.ui.btnRegister.clicked.connect( + self._register) + + self._username = None + + def __del__(self): + self._provider_bootstrapper.set_should_quit() + self._eip_bootstrapper.set_should_quit() + self._provider_bootstrapper.wait() + self._eip_bootstrapper.wait() + + def get_username(self): + return self._username + + def _focus_password(self): + """ + Focuses at the password lineedit for the registration page + """ + self.ui.lblPassword.setFocus() + + def _focus_second_password(self): + """ + Focuses at the second password lineedit for the registration page + """ + self.ui.lblPassword2.setFocus() + + def _basic_password_checks(self, username, password, password2): + """ + Performs basic password checks to avoid really easy passwords. + + @param username: username provided at the registrarion form + @type username: str + @param password: password from the registration form + @type password: str + @param password2: second password from the registration form + @type password: str + + @return: returns True if all the checks pass, False otherwise + @rtype: bool + """ + message = None + + try: + username.encode("ascii") + password.encode("ascii") + except: + message = u"Refrain from using non ASCII áñ characters" + + if message is not None and password != password2: + message = "Passwords don't match" + + if message is not None and len(password) < 4: + message = "Password too short" + + if message is not None and password in self.WEAK_PASSWORDS: + message = "Password too easy" + + if message is not None and username == password: + message = "Password equal to username" + + if message is not None: + self._set_register_status(message) + self._focus_password() + return False + + return True + + def _register(self): + """ + Performs the registration based on the values provided in the form + """ + self.ui.btnRegister.setEnabled(False) + # See the disabled button + while QtGui.QApplication.instance().hasPendingEvents(): + QtGui.QApplication.instance().processEvents() + self.button(QtGui.QWizard.NextButton).setFocus() + + username = self.ui.lblUser.text() + password = self.ui.lblPassword.text() + password2 = self.ui.lblPassword2.text() + + if self._basic_password_checks(username, password, password2): + register = SRPRegister(provider_config=self._provider_config) + ok, req = register.register_user(username, password) + if ok: + self._set_register_status("User registration OK") + self._username = username + self.ui.lblPassword2.clearFocus() + # Detach this call to allow UI updates briefly + QtCore.QTimer.singleShot(1, + self.page(self.REGISTER_USER_PAGE) + .set_completed) + else: + print req.content + error_msg = "Unknown error" + try: + error_msg = req.json().get("errors").get("login")[0] + except: + logger.error("Unknown error: %r" % (req.content,)) + self._set_register_status(error_msg) + self.ui.btnRegister.setEnabled(True) + else: + self.ui.btnRegister.setEnabled(True) + + def _set_register_status(self, status): + """ + Sets the status label in the registration page to status + + @param status: status message to display, can be HTML + @type status: str + """ + self.ui.lblRegisterStatus.setText(status) + + def _check_provider(self): + """ + SLOT + TRIGGERS: + self.ui.btnCheck.clicked + self.ui.lnProvider.returnPressed + + Starts the checks for a given provider + """ + self.ui.grpCheckProvider.setVisible(True) + self.ui.btnCheck.setEnabled(False) + self._domain = self.ui.lnProvider.text() + + self._provider_bootstrapper.start() + self._provider_bootstrapper.run_provider_select_checks(self._domain) + + def _complete_task(self, data, label, complete=False, complete_page=-1): + """ + Checks a task and completes a page if specified + + @param data: data as it comes from the bootstrapper thread for + a specific check + @type data: dict + @param label: label that displays the status icon for a + specific check that corresponds to the data + @type label: QtGui.QLabel + @param complete: if True, it completes the page specified, + which must be of type WizardPage + @type complete: bool + @param complete_page: page id to complete + @type complete_page: int + """ + passed = data[self._provider_bootstrapper.PASSED_KEY] + error = data[self._provider_bootstrapper.ERROR_KEY] + if passed: + label.setPixmap(self.OK_ICON) + if complete: + self.page(complete_page).set_completed() + self.button(QtGui.QWizard.NextButton).setFocus() + else: + label.setPixmap(self.ERROR_ICON) + logger.error(error) + + def _name_resolution(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.name_resolution + + Sets the status for the name resolution check + """ + self._complete_task(data, self.ui.lblNameResolution) + + def _https_connection(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.https_connection + + Sets the status for the https connection check + """ + self._complete_task(data, self.ui.lblHTTPS) + + def _download_provider_info(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.download_provider_info + + Sets the status for the provider information download + check. Since this check is the last of this set, it also + completes the page if passed + """ + if self._provider_config.load(os.path.join("leap", + "providers", + self._domain, + "provider.json")): + self._complete_task(data, self.ui.lblProviderInfo, + True, self.SELECT_PROVIDER_PAGE) + else: + new_data = { + self._provider_bootstrapper.PASSED_KEY: False, + self._provider_bootstrapper.ERROR_KEY: + "Unable to load provider configuration" + } + self._complete_task(new_data, self.ui.lblProviderInfo) + + self.ui.btnCheck.setEnabled(True) + + def _download_ca_cert(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.download_ca_cert + + Sets the status for the download of the CA certificate check + """ + self._complete_task(data, self.ui.lblDownloadCaCert) + + def _check_ca_fingerprint(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.check_ca_fingerprint + + Sets the status for the CA fingerprint check + """ + self._complete_task(data, self.ui.lblCheckCaFpr) + + def _check_api_certificate(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.check_api_certificate + + Sets the status for the API certificate check. Also finishes + the provider bootstrapper thread since it's not needed anymore + from this point on, unless the whole check chain is restarted + """ + self._complete_task(data, self.ui.lblCheckApiCert, + True, self.SETUP_PROVIDER_PAGE) + self._provider_bootstrapper.set_should_quit() + + def _download_eip_config(self, data): + """ + SLOT + TRIGGER: self._eip_bootstrapper.download_config + + Sets the status for the EIP config downloading check + """ + self._complete_task(data, self.ui.lblDownloadEIPConfig) + + def _download_client_certificate(self, data): + """ + SLOT + TRIGGER: self._provider_bootstrapper.download_client_certificate + + Sets the status for the download client certificate check and + completes the page if passed. Also stops the eip bootstrapper + thread since it's not needed from this point on unless the + check chain is restarted + """ + self._complete_task(data, self.ui.lblDownloadClientCert, + True, self.SETUP_EIP_PAGE) + self._eip_bootstrapper.set_should_quit() + + def _current_id_changed(self, pageId): + """ + SLOT + TRIGGER: self.currentIdChanged + + Prepares the pages when they appear + """ + if pageId == self.SELECT_PROVIDER_PAGE: + self.ui.grpCheckProvider.setVisible(False) + self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) + self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON) + self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) + + if pageId == self.SETUP_PROVIDER_PAGE: + self._provider_bootstrapper.\ + run_provider_setup_checks(self._provider_config) + + if pageId == self.SETUP_EIP_PAGE: + self._eip_bootstrapper.start() + self._eip_bootstrapper.run_eip_setup_checks(self._provider_config) + + if pageId == self.PRESENT_PROVIDER_PAGE: + # TODO: get the right lang for these + self.ui.lblProviderName.setText( + "%s" % + (self._provider_config.get_name(),)) + self.ui.lblProviderURL.setText(self._provider_config.get_domain()) + self.ui.lblProviderDesc.setText( + "%s" % + (self._provider_config.get_description(),)) + self.ui.lblProviderPolicy.setText(self._provider_config + .get_enrollment_policy()) + + def nextId(self): + """ + Sets the next page id for the wizard based on wether the user + wants to register a new identity or uses an existing one + """ + if self.currentPage() == self.page(self.INTRO_PAGE): + self._show_register = self.ui.rdoRegister.isChecked() + + if self.currentPage() == self.page(self.SETUP_PROVIDER_PAGE): + if self._show_register: + return self.REGISTER_USER_PAGE + else: + return self.SETUP_EIP_PAGE + + return QtGui.QWizard.nextId(self) diff --git a/src/leap/gui/wizardpage.py b/src/leap/gui/wizardpage.py new file mode 100644 index 00000000..2138ac7b --- /dev/null +++ b/src/leap/gui/wizardpage.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# wizardpage.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from PySide import QtGui + + +class WizardPage(QtGui.QWizardPage): + """ + Simple wizard page helper + """ + + def __init__(self): + QtGui.QWizardPage.__init__(self) + self._completed = False + + def set_completed(self): + self._completed = True + self.completeChanged.emit() + + def isComplete(self): + return self._completed + + def cleanupPage(self): + self._completed = False + QtGui.QWizardPage.cleanupPage(self) -- cgit v1.2.3 From a6afbf7b848030573f01480bce0959626330b02e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 6 Mar 2013 15:44:53 -0300 Subject: Add general app --- src/leap/__init__.py | 0 src/leap/app.py | 119 +++++++++++++++++++++++++++++++++++++++++ src/leap/util/__init__.py | 0 src/leap/util/leap_argparse.py | 61 +++++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 src/leap/__init__.py create mode 100644 src/leap/app.py create mode 100644 src/leap/util/__init__.py create mode 100644 src/leap/util/leap_argparse.py (limited to 'src') diff --git a/src/leap/__init__.py b/src/leap/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/app.py b/src/leap/app.py new file mode 100644 index 00000000..fa50cd1e --- /dev/null +++ b/src/leap/app.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# app.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import signal +import sys + +from functools import partial +from PySide import QtCore, QtGui + +# TODO: add version handling +#from leap import __version__ as VERSION +VERSION = "unknown" + +from leap.util import leap_argparse +# TODO: add translations +#from leap.gui import locale_rc +from leap.gui.mainwindow import MainWindow + + +def sigint_handler(*args, **kwargs): + logger = kwargs.get('logger', None) + logger.debug('SIGINT catched. shutting down...') + mainwindow = args[0] + mainwindow.quit() + + +def main(): + """ + Launches the main event loop + """ + + _, opts = leap_argparse.init_leapc_args() + debug = opts.debug + + # TODO: get severity from command line args + if debug: + level = logging.DEBUG + else: + level = logging.WARNING + + logger = logging.getLogger(name='leap') + logger.setLevel(level) + console = logging.StreamHandler() + console.setLevel(level) + formatter = logging.Formatter( + '%(asctime)s ' + '- %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + + logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') + logger.info('LEAP client version %s', VERSION) + logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') + logfile = opts.log_file + if logfile is not None: + logger.debug('Setting logfile to %s ', logfile) + fileh = logging.FileHandler(logfile) + fileh.setLevel(logging.DEBUG) + fileh.setFormatter(formatter) + logger.addHandler(fileh) + + logger.info('Starting app') + app = QtGui.QApplication(sys.argv) + + # To test: + # $ LANG=es ./app.py + locale = QtCore.QLocale.system().name() + qtTranslator = QtCore.QTranslator() + if qtTranslator.load("qt_%s" % locale, ":/translations"): + app.installTranslator(qtTranslator) + appTranslator = QtCore.QTranslator() + if appTranslator.load("leap_client_%s" % locale, ":/translations"): + app.installTranslator(appTranslator) + + # Needed for initializing qsettings it will write + # .config/leap/leap.conf top level app settings in a platform + # independent way + app.setOrganizationName("leap") + app.setApplicationName("leap") + app.setOrganizationDomain("leap.se") + + # TODO: check if the leap-client is already running and quit + # gracefully in that case. + + window = MainWindow() + window.show() + + # This dummy timer ensures that control is given to the outside + # loop, so we can hook our sigint handler. + timer = QtCore.QTimer() + timer.start(500) + timer.timeout.connect(lambda: None) + + sigint_window = partial(sigint_handler, window, logger=logger) + signal.signal(signal.SIGINT, sigint_window) + + if sys.platform == "darwin": + window.raise_() + + # Run main loop + sys.exit(app.exec_()) + +if __name__ == "__main__": + main() diff --git a/src/leap/util/__init__.py b/src/leap/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/util/leap_argparse.py b/src/leap/util/leap_argparse.py new file mode 100644 index 00000000..78597f63 --- /dev/null +++ b/src/leap/util/leap_argparse.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# leap_argparse.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse + + +def build_parser(): + """ + all the options for the leap arg parser + Some of these could be switched on only if debug flag is present! + """ + epilog = "Copyright 2012 The LEAP Encryption Access Project" + parser = argparse.ArgumentParser(description=""" +Launches the LEAP Client""", epilog=epilog) + parser.add_argument('-d', '--debug', action="store_true", + help=("Launches client in debug mode, writing debug" + "info to stdout")) + parser.add_argument('-l', '--logfile', metavar="LOG FILE", nargs='?', + action="store", dest="log_file", + #type=argparse.FileType('w'), + help='optional log file') + parser.add_argument('--openvpn-verbosity', nargs='?', + type=int, + action="store", dest="openvpn_verb", + help='verbosity level for openvpn logs [1-6]') + + # Not in use, we might want to reintroduce them. + #parser.add_argument('-i', '--no-provider-checks', + #action="store_true", default=False, + #help="skips download of provider config files. gets " + #"config from local files only. Will fail if cannot " + #"find any") + #parser.add_argument('-k', '--no-ca-verify', + #action="store_true", default=False, + #help="(insecure). Skips verification of the server " + #"certificate used in TLS handshake.") + #parser.add_argument('-c', '--config', metavar="CONFIG FILE", nargs='?', + #action="store", dest="config_file", + #type=argparse.FileType('r'), + #help='optional config file') + return parser + + +def init_leapc_args(): + parser = build_parser() + opts, unknown = parser.parse_known_args() + return parser, opts -- cgit v1.2.3 From 751638b4eb8208e1eaa1beaaed284da6b412bca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 7 Mar 2013 19:05:11 -0300 Subject: Change asserts for a custom leap_assert method Also: - Make SRPAuth and the Bootstrappers be a QObject instead of a QThread so we can use them inside another more generic thread - Add a generic CheckerThread that runs checks or whatever operation as long as it returns a boolean value - Closes the whole application if the wizard is rejected at the first run - Do not fail when the config directory doesn't exist - Set the wizard pixmap logo as LEAP's logo - Improve wizard checks - Make SRPRegister play nice with the CheckerThread --- src/leap/config/baseconfig.py | 3 +- src/leap/config/pluggableconfig.py | 4 +- src/leap/config/prefixers.py | 6 +- src/leap/config/providerconfig.py | 5 +- src/leap/crypto/srpauth.py | 108 ++++++++++++-------- src/leap/crypto/srpregister.py | 17 ++-- src/leap/gui/mainwindow.py | 91 +++++++++++------ src/leap/gui/ui/wizard.ui | 5 +- src/leap/gui/wizard.py | 91 +++++++++-------- src/leap/services/eip/eipbootstrapper.py | 101 +++++-------------- src/leap/services/eip/eipconfig.py | 13 ++- src/leap/services/eip/providerbootstrapper.py | 137 ++++++++------------------ src/leap/services/eip/vpn.py | 15 ++- src/leap/services/eip/vpnlaunchers.py | 24 ++--- src/leap/util/check.py | 61 ++++++++++++ src/leap/util/checkerthread.py | 110 +++++++++++++++++++++ 16 files changed, 465 insertions(+), 326 deletions(-) create mode 100644 src/leap/util/check.py create mode 100644 src/leap/util/checkerthread.py (limited to 'src') diff --git a/src/leap/config/baseconfig.py b/src/leap/config/baseconfig.py index d553255e..b80fd419 100644 --- a/src/leap/config/baseconfig.py +++ b/src/leap/config/baseconfig.py @@ -29,6 +29,7 @@ from abc import ABCMeta, abstractmethod from leap.config.prefixers import get_platform_prefixer from leap.config.pluggableconfig import PluggableConfig +from leap.util.check import leap_assert logger = logging.getLogger(__name__) @@ -58,7 +59,7 @@ class BaseConfig: @rtype: depends on the config structure, dict, str, array, int @return: returns the value for the specified key in the config """ - assert self._config_checker, "Load the config first" + leap_assert(self._config_checker, "Load the config first") return self._config_checker.config[key] def get_path_prefix(self, standalone=False): diff --git a/src/leap/config/pluggableconfig.py b/src/leap/config/pluggableconfig.py index 64aa05cc..5ed83b3f 100644 --- a/src/leap/config/pluggableconfig.py +++ b/src/leap/config/pluggableconfig.py @@ -28,6 +28,8 @@ import urlparse import jsonschema #from leap.base.util.translations import LEAPTranslatable +from leap.util.check import leap_assert + logger = logging.getLogger(__name__) @@ -378,7 +380,7 @@ class PluggableConfig(object): # XXX cleanup this! if fromfile: - assert os.path.exists(fromfile) + leap_assert(os.path.exists(fromfile)) if not format: format = self.filename2format(fromfile) diff --git a/src/leap/config/prefixers.py b/src/leap/config/prefixers.py index a33e68aa..20d59b2d 100644 --- a/src/leap/config/prefixers.py +++ b/src/leap/config/prefixers.py @@ -24,6 +24,8 @@ import platform from abc import ABCMeta, abstractmethod from xdg import BaseDirectory +from leap.util.check import leap_assert + class Prefixer: """ @@ -47,8 +49,8 @@ class Prefixer: def get_platform_prefixer(): prefixer = globals()[platform.system() + "Prefixer"] - assert prefixer, "Unimplemented platform prefixer: %s" % \ - (platform.system(),) + leap_assert(prefixer, "Unimplemented platform prefixer: %s" % + (platform.system(),)) return prefixer() diff --git a/src/leap/config/providerconfig.py b/src/leap/config/providerconfig.py index c3c2c298..55b33b98 100644 --- a/src/leap/config/providerconfig.py +++ b/src/leap/config/providerconfig.py @@ -23,6 +23,7 @@ import os from leap.config.baseconfig import BaseConfig, LocalizedKey from leap.config.provider_spec import leap_provider_spec +from leap.util.check import leap_assert logger = logging.getLogger(__name__) @@ -95,8 +96,8 @@ class ProviderConfig(BaseConfig): "cacert.pem") if not about_to_download: - assert os.path.exists(cert_path), \ - "You need to download the certificate first" + leap_assert(os.path.exists(cert_path), + "You need to download the certificate first") logger.debug("Going to verify SSL against %s" % (cert_path,)) return cert_path diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index dbcc95cb..28e4f037 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -20,9 +20,11 @@ import srp import binascii import logging -from PySide import QtCore +from PySide import QtCore, QtGui from leap.config.providerconfig import ProviderConfig +from leap.util.check import leap_assert +from leap.util.checkerthread import CheckerThread logger = logging.getLogger(__name__) @@ -34,7 +36,7 @@ class SRPAuthenticationError(Exception): pass -class SRPAuth(QtCore.QThread): +class SRPAuth(QtCore.QObject): """ SRPAuth singleton """ @@ -55,7 +57,8 @@ class SRPAuth(QtCore.QThread): @param server: Server to which we will authenticate @type server: str """ - assert provider_config, "We need a provider config to authenticate" + leap_assert(provider_config, + "We need a provider config to authenticate") self._provider_config = provider_config @@ -277,15 +280,15 @@ class SRPAuth(QtCore.QThread): @param password: password for this user @type password: str """ - assert self.get_session_id() is None, "Already logged in" + leap_assert(self.get_session_id() is None, "Already logged in") self._authentication_preprocessing(username, password) salt, B = self._start_authentication(username, password) M2 = self._process_challenge(salt, B, username) self._verify_session(M2) - assert self.get_session_id(), "Something went wrong because" + \ - " we don't have the auth cookie afterwards" + leap_assert(self.get_session_id(), "Something went wrong because" + " we don't have the auth cookie afterwards") def logout(self): """ @@ -294,7 +297,8 @@ class SRPAuth(QtCore.QThread): """ logger.debug("Starting logout...") - assert self.get_session_id(), "Cannot logout an unexisting session" + leap_assert(self.get_session_id(), + "Cannot logout an unexisting session") logout_url = "%s/%s/%s/" % (self._provider_config.get_api_uri(), self._provider_config. @@ -344,7 +348,7 @@ class SRPAuth(QtCore.QThread): """ Creates a singleton instance if needed """ - QtCore.QThread.__init__(self) + QtCore.QObject.__init__(self) # Check whether we already have an instance if SRPAuth.__instance is None: @@ -371,47 +375,47 @@ class SRPAuth(QtCore.QThread): @type password: str """ - with QtCore.QMutexLocker(self._should_login_lock): - self._should_login = self.DO_LOGIN - self._username = username - self._password = password - # Detach the start call to Qt's event loop - QtCore.QTimer.singleShot(0, self.start) - - def logout(self): - """ - Logs out the current session. - Expects a session_id to exists, might raise AssertionError - """ - QtCore.QMutexLocker(self._should_login_lock) - self._should_login = self.DO_LOGOUT - # Detach the start call to Qt's event loop - QtCore.QTimer.singleShot(0, self.start) - - def _runLogin(self, username, password): try: self.__instance.authenticate(username, password) + + logger.debug("Successful login!") self.authentication_finished.emit(True, "Succeeded") + return True except Exception as e: + logger.error("Error logging in %s" % (e,)) self.authentication_finished.emit(False, "%s" % (e,)) + return False - def _runLogout(self): + def logout(self): + """ + Logs out the current session. + Expects a session_id to exists, might raise AssertionError + """ try: self.__instance.logout() self.logout_finished.emit(True, "Succeeded") + return True except Exception as e: self.logout_finished.emit(False, "%s" % (e,)) - - def run(self): - QtCore.QMutexLocker(self._should_login_lock) - if self._should_login == self.DO_LOGIN: - self._runLogin(self._username, self._password) - elif self._should_login == self.DO_LOGOUT: - self._runLogout() - self._should_login = self.DO_NOTHING + return False if __name__ == "__main__": + import sys + from functools import partial + app = QtGui.QApplication(sys.argv) + + import signal + + def sigint_handler(*args, **kwargs): + logger.debug('SIGINT catched. shutting down...') + checker = args[0] + checker.set_should_quit() + QtGui.QApplication.quit() + + def signal_tester(d): + print d + logger = logging.getLogger(name='leap') logger.setLevel(logging.DEBUG) console = logging.StreamHandler() @@ -422,8 +426,23 @@ if __name__ == "__main__": console.setFormatter(formatter) logger.addHandler(console) - provider = ProviderConfig() + checker = CheckerThread() + + sigint = partial(sigint_handler, checker) + signal.signal(signal.SIGINT, sigint) + timer = QtCore.QTimer() + timer.start(500) + timer.timeout.connect(lambda: None) + app.connect(app, QtCore.SIGNAL("aboutToQuit()"), + checker.set_should_quit) + w = QtGui.QWidget() + w.resize(100, 100) + w.show() + + checker.start() + + provider = ProviderConfig() if provider.load("leap/providers/bitmask.net/provider.json"): # url = "%s/tickets" % (provider.get_api_uri(),) # print url @@ -431,9 +450,14 @@ if __name__ == "__main__": # print res.content # res.raise_for_status() auth = SRPAuth(provider) - auth.start() - auth.authenticate("test2", "sarasaaaa") - res = requests.session().get("%s/cert" % (provider.get_api_uri(),), - verify=provider.get_ca_cert_path()) - print res.content - auth.logout() + auth_instantiated = partial(auth.authenticate, "test2", "sarasaaaa") + + checker.add_checks([auth_instantiated, auth.logout]) + + #auth.authenticate("test2", "sarasaaaa") + #res = requests.session().get("%s/cert" % (provider.get_api_uri(),), + #verify=provider.get_ca_cert_path()) + #print res.content + #auth.logout() + + sys.exit(app.exec_()) diff --git a/src/leap/crypto/srpregister.py b/src/leap/crypto/srpregister.py index d9b2b22b..cf673e35 100644 --- a/src/leap/crypto/srpregister.py +++ b/src/leap/crypto/srpregister.py @@ -20,15 +20,17 @@ import binascii import srp import logging +from PySide import QtCore from urlparse import urlparse from leap.config.providerconfig import ProviderConfig from leap.crypto.constants import SIGNUP_TIMEOUT +from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) -class SRPRegister(object): +class SRPRegister(QtCore.QObject): """ Registers a user to a specific provider using SRP """ @@ -37,6 +39,8 @@ class SRPRegister(object): USER_VERIFIER_KEY = 'user[password_verifier]' USER_SALT_KEY = 'user[password_salt]' + registration_finished = QtCore.Signal(bool, object) + def __init__(self, provider_config=None, register_path="users"): @@ -49,10 +53,9 @@ class SRPRegister(object): @param register_path: webapp path for registering users @type register_path; str """ - - assert provider_config, "Please provider a provider" - assert isinstance(provider_config, ProviderConfig), \ - "We need a ProviderConfig instead of %r" % (provider_config,) + QtCore.QObject.__init__(self) + leap_assert(provider_config, "Please provider a provider") + leap_assert_type(provider_config, ProviderConfig) self._provider_config = provider_config @@ -131,7 +134,9 @@ class SRPRegister(object): verify=self._provider_config. get_ca_cert_path()) - return (req.ok, req) + self.registration_finished.emit(req.ok, req) + + return req.ok if __name__ == "__main__": diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 1821e4a6..50a03fb9 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -31,6 +31,8 @@ from leap.services.eip.providerbootstrapper import ProviderBootstrapper from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig from leap.gui.wizard import Wizard +from leap.util.check import leap_assert +from leap.util.checkerthread import CheckerThread logger = logging.getLogger(__name__) @@ -44,6 +46,9 @@ class MainWindow(QtGui.QMainWindow): LOGIN_INDEX = 0 EIP_STATUS_INDEX = 1 + GEOMETRY_KEY = "Geometry" + WINDOWSTATE_KEY = "WindowState" + def __init__(self): QtGui.QMainWindow.__init__(self) @@ -69,6 +74,9 @@ class MainWindow(QtGui.QMainWindow): # This is created once we have a valid provider config self._srp_auth = None + self._checker_thread = CheckerThread() + self._checker_thread.start() + # This thread is always running, although it's quite # lightweight when it's done setting up provider # configuration and certificate. @@ -110,11 +118,7 @@ class MainWindow(QtGui.QMainWindow): QtCore.QCoreApplication.instance().connect( QtCore.QCoreApplication.instance(), QtCore.SIGNAL("aboutToQuit()"), - self._provider_bootstrapper.set_should_quit) - QtCore.QCoreApplication.instance().connect( - QtCore.QCoreApplication.instance(), - QtCore.SIGNAL("aboutToQuit()"), - self._eip_bootstrapper.set_should_quit) + self._checker_thread.set_should_quit) self.ui.action_sign_out.setEnabled(False) self.ui.action_sign_out.triggered.connect(self._logout) @@ -131,17 +135,26 @@ class MainWindow(QtGui.QMainWindow): self._center_window() self._wizard = None + self._wizard_firstrun = False if self._first_run(): - self._wizard = Wizard() + self._wizard_firstrun = True + self._wizard = Wizard(self._checker_thread) # Give this window time to finish init and then show the wizard QtCore.QTimer.singleShot(1, self._launch_wizard) - self._wizard.finished.connect(self._finish_init) + self._wizard.accepted.connect(self._finish_init) + self._wizard.rejected.connect(self._rejected_wizard) + else: + self._finish_init() + + def _rejected_wizard(self): + if self._wizard_firstrun: + self.quit() else: self._finish_init() def _launch_wizard(self): if self._wizard is None: - self._wizard = Wizard() + self._wizard = Wizard(self._checker_thread) self._wizard.exec_() def _finish_init(self): @@ -187,14 +200,23 @@ class MainWindow(QtGui.QMainWindow): """ Centers the mainwindow based on the desktop geometry """ - app = QtGui.QApplication.instance() - width = app.desktop().width() - height = app.desktop().height() - window_width = self.size().width() - window_height = self.size().height() - x = (width / 2.0) - (window_width / 2.0) - y = (height / 2.0) - (window_height / 2.0) - self.move(x, y) + settings = QtCore.QSettings() + geometry = settings.value(self.GEOMETRY_KEY, None) + state = settings.value(self.WINDOWSTATE_KEY, None) + if geometry is None: + app = QtGui.QApplication.instance() + width = app.desktop().width() + height = app.desktop().height() + window_width = self.size().width() + window_height = self.size().height() + x = (width / 2.0) - (window_width / 2.0) + y = (height / 2.0) - (window_height / 2.0) + self.move(x, y) + else: + self.restoreGeometry(geometry) + + if state is not None: + self.restoreState(state) def _about(self): """ @@ -236,6 +258,9 @@ class MainWindow(QtGui.QMainWindow): self._toggle_visible() e.ignore() return + settings = QtCore.QSettings() + settings.setValue(self.GEOMETRY_KEY, self.saveGeometry()) + settings.setValue(self.WINDOWSTATE_KEY, self.saveState()) QtGui.QMainWindow.closeEvent(self, e) def _configured_providers(self): @@ -244,10 +269,16 @@ class MainWindow(QtGui.QMainWindow): @rtype: list """ - providers = os.listdir( - os.path.join(self._provider_config.get_path_prefix(), - "leap", - "providers")) + providers = [] + try: + providers = os.listdir( + os.path.join(self._provider_config.get_path_prefix(), + "leap", + "providers")) + except Exception as e: + logger.debug("Error listing providers, assume there are none. %r" + % (e,)) + return providers def _first_run(self): @@ -303,8 +334,8 @@ class MainWindow(QtGui.QMainWindow): """ provider = self.ui.cmbProviders.currentText() - self._provider_bootstrapper.start() self._provider_bootstrapper.run_provider_select_checks( + self._checker_thread, provider, download_if_needed=True) @@ -329,6 +360,7 @@ class MainWindow(QtGui.QMainWindow): provider, "provider.json")): self._provider_bootstrapper.run_provider_setup_checks( + self._checker_thread, self._provider_config, download_if_needed=True) else: @@ -350,7 +382,7 @@ class MainWindow(QtGui.QMainWindow): start the SRP authentication, and as the last step bootstrapping the EIP service """ - assert self._provider_config, "We need a provider config" + leap_assert(self._provider_config, "We need a provider config") username = self.ui.lnUser.text() password = self.ui.lnPassword.text() @@ -381,9 +413,7 @@ class MainWindow(QtGui.QMainWindow): Once the provider configuration is loaded, this starts the SRP authentication """ - assert self._provider_config, "We need a provider config!" - - self._provider_bootstrapper.set_should_quit() + leap_assert(self._provider_config, "We need a provider config!") if data[self._provider_bootstrapper.PASSED_KEY]: username = self.ui.lnUser.text() @@ -431,14 +461,14 @@ class MainWindow(QtGui.QMainWindow): """ Starts the EIP bootstrapping sequence """ - assert self._eip_bootstrapper, "We need an eip bootstrapper!" - assert self._provider_config, "We need a provider config" + leap_assert(self._eip_bootstrapper, "We need an eip bootstrapper!") + leap_assert(self._provider_config, "We need a provider config") self._set_eip_status("Checking configuration, please wait...") if self._provider_config.provides_eip(): - self._eip_bootstrapper.start() self._eip_bootstrapper.run_eip_setup_checks( + self._checker_thread, self._provider_config, download_if_needed=True) else: @@ -503,10 +533,9 @@ class MainWindow(QtGui.QMainWindow): Starts the VPN thread if the eip configuration is properly loaded """ - assert self._eip_config, "We need an eip config!" - assert self._provider_config, "We need a provider config!" + leap_assert(self._eip_config, "We need an eip config!") + leap_assert(self._provider_config, "We need a provider config!") - self._eip_bootstrapper.set_should_quit() if self._eip_config.loaded() or \ self._eip_config.load(os.path.join("leap", "providers", diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui index 86f8d458..2d9cb441 100644 --- a/src/leap/gui/ui/wizard.ui +++ b/src/leap/gui/ui/wizard.ui @@ -47,7 +47,7 @@ - <html><head/><body><p>New we will guide you through some configuration that is needed before you can connect for the first time.</p><p>If you ever need to modify these options again, you can find the wizard in the <span style=" font-style:italic;">'Settings'</span> menu from the main window.</p><p>Do you want to <span style=" font-weight:600;">sign up</span> for a new account, or <span style=" font-weight:600;">log in</span> with an already existing username?</p></body></html> + <html><head/><body><p>Now we will guide you through some configuration that is needed before you can connect for the first time.</p><p>If you ever need to modify these options again, you can find the wizard in the <span style=" font-style:italic;">'Settings'</span> menu from the main window.</p><p>Do you want to <span style=" font-weight:600;">sign up</span> for a new account, or <span style=" font-weight:600;">log in</span> with an already existing username?</p></body></html> Qt::RichText @@ -497,6 +497,9 @@ + + Qt::AutoText + Qt::AlignCenter diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 7dcc8dd6..bac74d1d 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -21,7 +21,8 @@ First run wizard import os import logging -from PySide import QtCore, QtGui +from PySide import QtGui +from functools import partial from ui_wizard import Ui_Wizard from leap.config.providerconfig import ProviderConfig @@ -45,15 +46,18 @@ class Wizard(QtGui.QWizard): SETUP_EIP_PAGE = 5 FINISH_PATH = 6 - WEAK_PASSWORDS = ("1234", "12345", "123456", + WEAK_PASSWORDS = ("123456", "qweasd", "qwerty", "password") - def __init__(self): + def __init__(self, checker): QtGui.QWizard.__init__(self) self.ui = Ui_Wizard() self.ui.setupUi(self) + self.setPixmap(QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(":/images/leap-color-small.png")) + self.QUESTION_ICON = QtGui.QPixmap(":/images/Emblem-question.png") self.ERROR_ICON = QtGui.QPixmap(":/images/Dialog-error.png") self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png") @@ -94,6 +98,9 @@ class Wizard(QtGui.QWizard): self.ui.lblPassword.setEchoMode(QtGui.QLineEdit.Password) self.ui.lblPassword2.setEchoMode(QtGui.QLineEdit.Password) + self.ui.lnProvider.textChanged.connect( + self._enable_check) + self.ui.lblUser.returnPressed.connect( self._focus_password) self.ui.lblPassword.returnPressed.connect( @@ -105,15 +112,14 @@ class Wizard(QtGui.QWizard): self._username = None - def __del__(self): - self._provider_bootstrapper.set_should_quit() - self._eip_bootstrapper.set_should_quit() - self._provider_bootstrapper.wait() - self._eip_bootstrapper.wait() + self._checker_thread = checker def get_username(self): return self._username + def _enable_check(self, text): + self.ui.btnCheck.setEnabled(len(self.ui.lnProvider.text()) != 0) + def _focus_password(self): """ Focuses at the password lineedit for the registration page @@ -151,13 +157,13 @@ class Wizard(QtGui.QWizard): if message is not None and password != password2: message = "Passwords don't match" - if message is not None and len(password) < 4: + if message is None and len(password) < 6: message = "Password too short" - if message is not None and password in self.WEAK_PASSWORDS: + if message is None and password in self.WEAK_PASSWORDS: message = "Password too easy" - if message is not None and username == password: + if message is None and username == password: message = "Password equal to username" if message is not None: @@ -172,10 +178,6 @@ class Wizard(QtGui.QWizard): Performs the registration based on the values provided in the form """ self.ui.btnRegister.setEnabled(False) - # See the disabled button - while QtGui.QApplication.instance().hasPendingEvents(): - QtGui.QApplication.instance().processEvents() - self.button(QtGui.QWizard.NextButton).setFocus() username = self.ui.lblUser.text() password = self.ui.lblPassword.text() @@ -183,27 +185,32 @@ class Wizard(QtGui.QWizard): if self._basic_password_checks(username, password, password2): register = SRPRegister(provider_config=self._provider_config) - ok, req = register.register_user(username, password) - if ok: - self._set_register_status("User registration OK") - self._username = username - self.ui.lblPassword2.clearFocus() - # Detach this call to allow UI updates briefly - QtCore.QTimer.singleShot(1, - self.page(self.REGISTER_USER_PAGE) - .set_completed) - else: - print req.content - error_msg = "Unknown error" - try: - error_msg = req.json().get("errors").get("login")[0] - except: - logger.error("Unknown error: %r" % (req.content,)) - self._set_register_status(error_msg) - self.ui.btnRegister.setEnabled(True) + register.registration_finished.connect( + self._registration_finished) + self._checker_thread.add_checks( + [partial(register.register_user, username, password)]) + self._username = username + self._set_register_status("Starting registration...") else: self.ui.btnRegister.setEnabled(True) + def _registration_finished(self, ok, req): + if ok: + self._set_register_status("" + "User registration OK") + self.ui.lblPassword2.clearFocus() + self.page(self.REGISTER_USER_PAGE).set_completed() + self.button(QtGui.QWizard.BackButton).setEnabled(False) + else: + self._username = None + error_msg = "Unknown error" + try: + error_msg = req.json().get("errors").get("login")[0] + except: + logger.error("Unknown error: %r" % (req.content,)) + self._set_register_status(error_msg) + self.ui.btnRegister.setEnabled(True) + def _set_register_status(self, status): """ Sets the status label in the registration page to status @@ -222,12 +229,16 @@ class Wizard(QtGui.QWizard): Starts the checks for a given provider """ + if len(self.ui.lnProvider.text()) == 0: + return + self.ui.grpCheckProvider.setVisible(True) self.ui.btnCheck.setEnabled(False) self._domain = self.ui.lnProvider.text() - self._provider_bootstrapper.start() - self._provider_bootstrapper.run_provider_select_checks(self._domain) + self._provider_bootstrapper.run_provider_select_checks( + self._checker_thread, + self._domain) def _complete_task(self, data, label, complete=False, complete_page=-1): """ @@ -328,7 +339,6 @@ class Wizard(QtGui.QWizard): """ self._complete_task(data, self.ui.lblCheckApiCert, True, self.SETUP_PROVIDER_PAGE) - self._provider_bootstrapper.set_should_quit() def _download_eip_config(self, data): """ @@ -351,7 +361,6 @@ class Wizard(QtGui.QWizard): """ self._complete_task(data, self.ui.lblDownloadClientCert, True, self.SETUP_EIP_PAGE) - self._eip_bootstrapper.set_should_quit() def _current_id_changed(self, pageId): """ @@ -365,14 +374,16 @@ class Wizard(QtGui.QWizard): self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON) self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) + self._enable_check("") if pageId == self.SETUP_PROVIDER_PAGE: self._provider_bootstrapper.\ - run_provider_setup_checks(self._provider_config) + run_provider_setup_checks(self._checker_thread, + self._provider_config) if pageId == self.SETUP_EIP_PAGE: - self._eip_bootstrapper.start() - self._eip_bootstrapper.run_eip_setup_checks(self._provider_config) + self._eip_bootstrapper.run_eip_setup_checks(self._checker_thread, + self._provider_config) if pageId == self.PRESENT_PROVIDER_PAGE: # TODO: get the right lang for these diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 77d7020a..bd6ab715 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -28,11 +28,13 @@ from PySide import QtGui, QtCore from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig +from leap.util.check import leap_assert, leap_assert_type +from leap.util.checkerthread import CheckerThread logger = logging.getLogger(__name__) -class EIPBootstrapper(QtCore.QThread): +class EIPBootstrapper(QtCore.QObject): """ Sets up EIP for a provider a series of checks and emits signals after they are passed. @@ -50,13 +52,7 @@ class EIPBootstrapper(QtCore.QThread): download_client_certificate = QtCore.Signal(dict) def __init__(self): - QtCore.QThread.__init__(self) - - self._checks = [] - self._checks_lock = QtCore.QMutex() - - self._should_quit = False - self._should_quit_lock = QtCore.QMutex() + QtCore.QObject.__init__(self) # **************************************************** # # Dependency injection helpers, override this for more @@ -69,35 +65,6 @@ class EIPBootstrapper(QtCore.QThread): self._eip_config = None self._download_if_needed = False - def get_should_quit(self): - """ - Returns wether this thread should quit - - @rtype: bool - @return: True if the thread should terminate itself, Flase otherwise - """ - - QtCore.QMutexLocker(self._should_quit_lock) - return self._should_quit - - def set_should_quit(self): - """ - Sets the should_quit flag to True so that this thread - terminates the first chance it gets - """ - QtCore.QMutexLocker(self._should_quit_lock) - self._should_quit = True - self.wait() - - def start(self): - """ - Starts the thread and resets the should_quit flag - """ - with QtCore.QMutexLocker(self._should_quit_lock): - self._should_quit = False - - QtCore.QThread.start(self) - def _download_config(self): """ Downloads the EIP config for the given provider @@ -106,7 +73,8 @@ class EIPBootstrapper(QtCore.QThread): @rtype: bool """ - assert self._provider_config, "We need a provider configuration!" + leap_assert(self._provider_config, + "We need a provider configuration!") logger.debug("Downloading EIP config for %s" % (self._provider_config.get_domain(),)) @@ -162,8 +130,8 @@ class EIPBootstrapper(QtCore.QThread): @return: True if the checks passed, False otherwise @rtype: bool """ - assert self._provider_config, "We need a provider configuration!" - assert self._eip_config, "We need an eip configuration!" + leap_assert(self._provider_config, "We need a provider configuration!") + leap_assert(self._eip_config, "We need an eip configuration!") logger.debug("Downloading EIP client certificate for %s" % (self._provider_config.get_domain(),)) @@ -218,49 +186,25 @@ class EIPBootstrapper(QtCore.QThread): return download_cert[self.PASSED_KEY] - def run_eip_setup_checks(self, provider_config, download_if_needed=False): + def run_eip_setup_checks(self, checker, + provider_config, + download_if_needed=False): """ Starts the checks needed for a new eip setup @param provider_config: Provider configuration @type provider_config: ProviderConfig """ - assert provider_config, "We need a provider config!" - assert isinstance(provider_config, ProviderConfig), "Expected " + \ - "ProviderConfig type, not %r" % (type(provider_config),) + leap_assert(provider_config, "We need a provider config!") + leap_assert_type(provider_config, ProviderConfig) self._provider_config = provider_config self._download_if_needed = download_if_needed - QtCore.QMutexLocker(self._checks_lock) - self._checks = [ + checker.add_checks([ self._download_config, self._download_client_certificates - ] - - def run(self): - """ - Main run loop for this thread. Executes the checks. - """ - shouldContinue = False - while True: - if self.get_should_quit(): - logger.debug("Quitting provider bootstrap thread") - return - checkSomething = False - with QtCore.QMutexLocker(self._checks_lock): - if len(self._checks) > 0: - check = self._checks.pop(0) - shouldContinue = check() - checkSomething = True - if not shouldContinue: - logger.debug("Something went wrong with the checks, " - - "clearing...") - self._checks = [] - checkSomething = False - if not checkSomething: - self.usleep(self.IDLE_SLEEP_INTERVAL) + ]) if __name__ == "__main__": @@ -272,8 +216,8 @@ if __name__ == "__main__": def sigint_handler(*args, **kwargs): logger.debug('SIGINT catched. shutting down...') - bootstrapper_thread = args[0] - bootstrapper_thread.set_should_quit() + checker = args[0] + checker.set_should_quit() QtGui.QApplication.quit() def signal_tester(d): @@ -289,27 +233,28 @@ if __name__ == "__main__": console.setFormatter(formatter) logger.addHandler(console) - eip_thread = EIPBootstrapper() + eip_checks = EIPBootstrapper() + checker = CheckerThread() - sigint = partial(sigint_handler, eip_thread) + sigint = partial(sigint_handler, checker) signal.signal(signal.SIGINT, sigint) timer = QtCore.QTimer() timer.start(500) timer.timeout.connect(lambda: None) app.connect(app, QtCore.SIGNAL("aboutToQuit()"), - eip_thread.set_should_quit) + checker.set_should_quit) w = QtGui.QWidget() w.resize(100, 100) w.show() - eip_thread.start() + checker.start() provider_config = ProviderConfig() if provider_config.load(os.path.join("leap", "providers", "bitmask.net", "provider.json")): - eip_thread.run_eip_setup_checks(provider_config) + eip_checks.run_eip_setup_checks(checker, provider_config) sys.exit(app.exec_()) diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index ac06fef1..eab5bfd4 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -24,6 +24,7 @@ import logging from leap.config.baseconfig import BaseConfig from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipspec import eipservice_config_spec +from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -61,7 +62,7 @@ class EIPConfig(BaseConfig): def get_gateway_ip(self, index=0): gateways = self.get_gateways() - assert len(gateways) > 0, "We don't have any gateway!" + leap_assert(len(gateways) > 0, "We don't have any gateway!") if index > len(gateways): index = 0 logger.warning("Provided an unknown gateway index %s, " + @@ -75,10 +76,8 @@ class EIPConfig(BaseConfig): Returns the path to the certificate used by openvpn """ - assert providerconfig, "We need a provider" - assert isinstance(providerconfig, ProviderConfig), "The provider " + \ - "needs to be of type ProviderConfig instead of %s" % \ - (type(providerconfig),) + leap_assert(providerconfig, "We need a provider") + leap_assert_type(providerconfig, ProviderConfig) cert_path = os.path.join(self.get_path_prefix(), "leap", @@ -89,8 +88,8 @@ class EIPConfig(BaseConfig): "openvpn.pem") if not about_to_download: - assert os.path.exists(cert_path), \ - "You need to download the certificate first" + leap_assert(os.path.exists(cert_path), + "You need to download the certificate first") logger.debug("Using OpenVPN cert %s" % (cert_path,)) return cert_path diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index babcd47b..ecdc4e07 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -29,11 +29,13 @@ from OpenSSL import crypto from PySide import QtGui, QtCore from leap.config.providerconfig import ProviderConfig +from leap.util.check import leap_assert, leap_assert_type +from leap.util.checkerthread import CheckerThread logger = logging.getLogger(__name__) -class ProviderBootstrapper(QtCore.QThread): +class ProviderBootstrapper(QtCore.QObject): """ Given a provider URL performs a series of checks and emits signals after they are passed. @@ -56,13 +58,7 @@ class ProviderBootstrapper(QtCore.QThread): check_api_certificate = QtCore.Signal(dict) def __init__(self): - QtCore.QThread.__init__(self) - - self._checks = [] - self._checks_lock = QtCore.QMutex() - - self._should_quit = False - self._should_quit_lock = QtCore.QMutex() + QtCore.QObject.__init__(self) # **************************************************** # # Dependency injection helpers, override this for more @@ -75,35 +71,6 @@ class ProviderBootstrapper(QtCore.QThread): self._provider_config = None self._download_if_needed = False - def get_should_quit(self): - """ - Returns wether this thread should quit - - @rtype: bool - @return: True if the thread should terminate itself, Flase otherwise - """ - - QtCore.QMutexLocker(self._should_quit_lock) - return self._should_quit - - def set_should_quit(self): - """ - Sets the should_quit flag to True so that this thread - terminates the first chance it gets - """ - QtCore.QMutexLocker(self._should_quit_lock) - self._should_quit = True - self.wait() - - def start(self): - """ - Starts the thread and resets the should_quit flag - """ - with QtCore.QMutexLocker(self._should_quit_lock): - self._should_quit = False - - QtCore.QThread.start(self) - def _should_proceed_provider(self): """ Returns False if provider.json already exists for the given @@ -131,7 +98,7 @@ class ProviderBootstrapper(QtCore.QThread): @rtype: bool """ - assert self._domain, "Cannot check DNS without a domain" + leap_assert(self._domain, "Cannot check DNS without a domain") logger.debug("Checking name resolution for %s" % (self._domain)) @@ -162,7 +129,7 @@ class ProviderBootstrapper(QtCore.QThread): @rtype: bool """ - assert self._domain, "Cannot check HTTPS without a domain" + leap_assert(self._domain, "Cannot check HTTPS without a domain") logger.debug("Checking https for %s" % (self._domain)) @@ -193,7 +160,8 @@ class ProviderBootstrapper(QtCore.QThread): @return: True if the checks passed, False otherwise @rtype: bool """ - assert self._domain, "Cannot download provider info without a domain" + leap_assert(self._domain, + "Cannot download provider info without a domain") logger.debug("Downloading provider info for %s" % (self._domain)) @@ -230,7 +198,8 @@ class ProviderBootstrapper(QtCore.QThread): return download_data[self.PASSED_KEY] - def run_provider_select_checks(self, domain, download_if_needed=False): + def run_provider_select_checks(self, checker, + domain, download_if_needed=False): """ Populates the check queue @@ -243,17 +212,16 @@ class ProviderBootstrapper(QtCore.QThread): @return: True if the checks passed, False otherwise @rtype: bool """ - assert domain and len(domain) > 0, "We need a domain!" + leap_assert(domain and len(domain) > 0, "We need a domain!") self._domain = domain self._download_if_needed = download_if_needed - QtCore.QMutexLocker(self._checks_lock) - self._checks = [ + checker.add_checks([ self._check_name_resolution, self._check_https, self._download_provider_info - ] + ]) def _should_proceed_cert(self): """ @@ -262,7 +230,7 @@ class ProviderBootstrapper(QtCore.QThread): @rtype: bool """ - assert self._provider_config, "We need a provider config!" + leap_assert(self._provider_config, "We need a provider config!") if not self._download_if_needed: return True @@ -278,8 +246,8 @@ class ProviderBootstrapper(QtCore.QThread): @rtype: bool """ - assert self._provider_config, "Cannot download the ca cert " + \ - "without a provider config!" + leap_assert(self._provider_config, "Cannot download the ca cert " + "without a provider config!") logger.debug("Downloading ca cert for %s at %s" % (self._domain, self._provider_config.get_ca_cert_uri())) @@ -331,8 +299,8 @@ class ProviderBootstrapper(QtCore.QThread): @return: True if the checks passed, False otherwise @rtype: bool """ - assert self._provider_config, "Cannot check the ca cert " + \ - "without a provider config!" + leap_assert(self._provider_config, "Cannot check the ca cert " + "without a provider config!") logger.debug("Checking ca fingerprint for %s and cert %s" % (self._domain, @@ -350,7 +318,7 @@ class ProviderBootstrapper(QtCore.QThread): try: parts = self._provider_config.get_ca_cert_fingerprint().split(":") - assert len(parts) == 2, "Wrong fingerprint format" + leap_assert(len(parts) == 2, "Wrong fingerprint format") method = parts[0].strip() fingerprint = parts[1].strip() @@ -358,13 +326,13 @@ class ProviderBootstrapper(QtCore.QThread): with open(self._provider_config.get_ca_cert_path()) as f: cert_data = f.read() - assert len(cert_data) > 0, "Could not read certificate data" + leap_assert(len(cert_data) > 0, "Could not read certificate data") x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_data) digest = x509.digest(method).replace(":", "").lower() - assert digest == fingerprint, \ - "Downloaded certificate has a different fingerprint!" + leap_assert(digest == fingerprint, + "Downloaded certificate has a different fingerprint!") check_ca_fingerprint_data[self.PASSED_KEY] = True except Exception as e: @@ -384,8 +352,8 @@ class ProviderBootstrapper(QtCore.QThread): @return: True if the checks passed, False otherwise @rtype: bool """ - assert self._provider_config, "Cannot check the ca cert " + \ - "without a provider config!" + leap_assert(self._provider_config, "Cannot check the ca cert " + "without a provider config!") logger.debug("Checking api certificate for %s and cert %s" % (self._provider_config.get_api_uri(), @@ -418,7 +386,8 @@ class ProviderBootstrapper(QtCore.QThread): return check_api_certificate_data[self.PASSED_KEY] - def run_provider_setup_checks(self, provider_config, + def run_provider_setup_checks(self, checker, + provider_config, download_if_needed=False): """ Starts the checks needed for a new provider setup @@ -429,43 +398,17 @@ class ProviderBootstrapper(QtCore.QThread): overwrite already downloaded data @type download_if_needed: bool """ - assert provider_config, "We need a provider config!" - assert isinstance(provider_config, ProviderConfig), "Expected " + \ - "ProviderConfig type, not %r" % (type(provider_config),) + leap_assert(provider_config, "We need a provider config!") + leap_assert_type(provider_config, ProviderConfig) self._provider_config = provider_config self._download_if_needed = download_if_needed - QtCore.QMutexLocker(self._checks_lock) - self._checks = [ + checker.add_checks([ self._download_ca_cert, self._check_ca_fingerprint, self._check_api_certificate - ] - - def run(self): - """ - Main run loop for this thread. Executes the checks. - """ - shouldContinue = False - while True: - if self.get_should_quit(): - logger.debug("Quitting provider bootstrap thread") - return - checkSomething = False - with QtCore.QMutexLocker(self._checks_lock): - if len(self._checks) > 0: - check = self._checks.pop(0) - shouldContinue = check() - checkSomething = True - if not shouldContinue: - logger.debug("Something went wrong with the checks, " - "clearing...") - self._checks = [] - checkSomething = False - if not checkSomething: - self.usleep(self.IDLE_SLEEP_INTERVAL) - + ]) if __name__ == "__main__": import sys @@ -476,8 +419,8 @@ if __name__ == "__main__": def sigint_handler(*args, **kwargs): logger.debug('SIGINT catched. shutting down...') - bootstrapper_thread = args[0] - bootstrapper_thread.set_should_quit() + bootstrapper_checks = args[0] + bootstrapper_checks.set_should_quit() QtGui.QApplication.quit() def signal_tester(d): @@ -493,28 +436,32 @@ if __name__ == "__main__": console.setFormatter(formatter) logger.addHandler(console) - bootstrapper_thread = ProviderBootstrapper() + bootstrapper_checks = ProviderBootstrapper() + + checker = CheckerThread() + checker.start() - sigint = partial(sigint_handler, bootstrapper_thread) + sigint = partial(sigint_handler, checker) signal.signal(signal.SIGINT, sigint) timer = QtCore.QTimer() timer.start(500) timer.timeout.connect(lambda: None) app.connect(app, QtCore.SIGNAL("aboutToQuit()"), - bootstrapper_thread.set_should_quit) + checker.set_should_quit) w = QtGui.QWidget() w.resize(100, 100) w.show() - bootstrapper_thread.start() - bootstrapper_thread.run_provider_select_checks("bitmask.net") + bootstrapper_checks.run_provider_select_checks(checker, + "bitmask.net") provider_config = ProviderConfig() if provider_config.load(os.path.join("leap", "providers", "bitmask.net", "provider.json")): - bootstrapper_thread.run_provider_setup_checks(provider_config) + bootstrapper_checks.run_provider_setup_checks(checker, + provider_config) sys.exit(app.exec_()) diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index f117cdbc..88692442 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -29,6 +29,7 @@ from leap.config.providerconfig import ProviderConfig from leap.services.eip.vpnlaunchers import get_platform_launcher from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.udstelnet import UDSTelnet +from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) ON_POSIX = 'posix' in sys.builtin_module_names @@ -123,13 +124,11 @@ class VPN(QtCore.QThread): socket, or port otherwise @type socket_port: str """ - assert eipconfig, "We need an eip config" - assert isinstance(eipconfig, EIPConfig), "Expected EIPConfig " + \ - "object instead of %s" % (type(eipconfig),) - assert providerconfig, "We need a provider config" - assert isinstance(providerconfig, ProviderConfig), "Expected " + \ - "ProviderConfig object instead of %s" % (type(providerconfig),) - assert not self._started, "Starting process more than once!" + leap_assert(eipconfig, "We need an eip config") + leap_assert_type(eipconfig, EIPConfig) + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + leap_assert(not self._started, "Starting process more than once!") logger.debug("Starting VPN...") @@ -202,7 +201,7 @@ class VPN(QtCore.QThread): @return: response read @rtype: list """ - assert self._tn, "We need a tn connection!" + leap_assert(self._tn, "We need a tn connection!") try: self._tn.write("%s\n" % (command,)) buf = self._tn.read_until(until, 2) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 68978248..00e9c966 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -29,6 +29,7 @@ from abc import ABCMeta, abstractmethod from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig +from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -65,8 +66,8 @@ class VPNLauncher: def get_platform_launcher(): launcher = globals()[platform.system() + "VPNLauncher"] - assert launcher, "Unimplemented platform launcher: %s" % \ - (platform.system(),) + leap_assert(launcher, "Unimplemented platform launcher: %s" % + (platform.system(),)) return launcher() @@ -165,17 +166,15 @@ class LinuxVPNLauncher(VPNLauncher): @return: A VPN command ready to be launched @rtype: list """ - assert eipconfig, "We need an eip config" - assert isinstance(eipconfig, EIPConfig), "Expected EIPConfig " + \ - "object instead of %s" % (type(eipconfig),) - assert providerconfig, "We need a provider config" - assert isinstance(providerconfig, ProviderConfig), "Expected " + \ - "ProviderConfig object instead of %s" % (type(providerconfig),) - assert socket_host, "We need a socket host!" - assert socket_port, "We need a socket port!" + leap_assert(eipconfig, "We need an eip config") + leap_assert_type(eipconfig, EIPConfig) + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + leap_assert(socket_host, "We need a socket host!") + leap_assert(socket_port, "We need a socket port!") openvpn_possibilities = which(self.OPENVPN_BIN) - assert len(openvpn_possibilities) > 0, "We couldn't find openvpn" + leap_assert(len(openvpn_possibilities) > 0, "We couldn't find openvpn") openvpn = openvpn_possibilities[0] args = [] @@ -183,7 +182,8 @@ class LinuxVPNLauncher(VPNLauncher): if _is_pkexec_in_system(): if _is_auth_agent_running(): pkexec_possibilities = which(self.PKEXEC_BIN) - assert len(pkexec_possibilities) > 0, "We couldn't find pkexec" + leap_assert(len(pkexec_possibilities) > 0, + "We couldn't find pkexec") args.append(openvpn) openvpn = pkexec_possibilities[0] else: diff --git a/src/leap/util/check.py b/src/leap/util/check.py new file mode 100644 index 00000000..9787341a --- /dev/null +++ b/src/leap/util/check.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# check.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Set of functions to help checking situations +""" +import logging +import inspect +import traceback + + +logger = logging.getLogger(__name__) + + +def leap_assert(condition, message=""): + """ + Asserts the condition and displays the message if that's not + met. It also logs the error and its backtrace. + + @param condition: condition to check + @type condition: bool + @param message: message to display if the condition isn't met + @type message: str + """ + if not condition: + logger.error("Bug: %s" % (message,)) + try: + frame = inspect.currentframe() + stack_trace = traceback.format_stack(frame) + logger.error(''.join(stack_trace)) + except Exception as e: + logger.error("Bug in leap_assert: %r" % (e,)) + assert condition, message + + +def leap_assert_type(var, expectedType): + """ + Helper assert check for a variable's expected type + + @param var: variable to check + @type var: any + @param expectedType: type to check agains + @type expectedType: type + """ + leap_assert(isinstance(var, expectedType), + "Expected type %r instead of %r" % + (expectedType, type(var))) diff --git a/src/leap/util/checkerthread.py b/src/leap/util/checkerthread.py new file mode 100644 index 00000000..681c33e1 --- /dev/null +++ b/src/leap/util/checkerthread.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# providerbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Checker thread +""" + +import logging + +from PySide import QtCore + +from leap.util.check import leap_assert_type + +logger = logging.getLogger(__name__) + + +class CheckerThread(QtCore.QThread): + """ + Generic checker thread that can perform any type of operation as + long as it returns a boolean value that identifies how the + execution went. + """ + + IDLE_SLEEP_INTERVAL = 1 + + def __init__(self): + QtCore.QThread.__init__(self) + + self._checks = [] + self._checks_lock = QtCore.QMutex() + + self._should_quit = False + self._should_quit_lock = QtCore.QMutex() + + def get_should_quit(self): + """ + Returns wether this thread should quit + + @rtype: bool + @return: True if the thread should terminate itself, Flase otherwise + """ + + QtCore.QMutexLocker(self._should_quit_lock) + return self._should_quit + + def set_should_quit(self): + """ + Sets the should_quit flag to True so that this thread + terminates the first chance it gets + """ + QtCore.QMutexLocker(self._should_quit_lock) + self._should_quit = True + self.wait() + + def start(self): + """ + Starts the thread and resets the should_quit flag + """ + with QtCore.QMutexLocker(self._should_quit_lock): + self._should_quit = False + + QtCore.QThread.start(self) + + def add_checks(self, checks): + """ + Adds a list of checks to the ones being executed + + @param checks: check functions to perform + @type checkes: list + """ + with QtCore.QMutexLocker(self._checks_lock): + self._checks += checks + + def run(self): + """ + Main run loop for this thread. Executes the checks. + """ + shouldContinue = False + while True: + if self.get_should_quit(): + logger.debug("Quitting checker thread") + return + checkSomething = False + with QtCore.QMutexLocker(self._checks_lock): + if len(self._checks) > 0: + check = self._checks.pop(0) + shouldContinue = check() + leap_assert_type(shouldContinue, bool) + checkSomething = True + if not shouldContinue: + logger.debug("Something went wrong with the checks, " + "clearing...") + self._checks = [] + checkSomething = False + if not checkSomething: + self.sleep(self.IDLE_SLEEP_INTERVAL) -- cgit v1.2.3 From 01a7faa2033ef3ce85bc5a346eca3601f0f4f7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 8 Mar 2013 11:09:23 -0300 Subject: Migrate VPN process to QProcess Also: - Add a new tray icon for the whole app and a VPN specific one - Add a way to start/stop EIP independently - Improve reaction to the process dying --- src/leap/gui/mainwindow.py | 98 ++++++++++++++++++++++++++++++++++++++----- src/leap/gui/ui/mainwindow.ui | 63 +++++++++++++++------------- src/leap/services/eip/vpn.py | 29 ++++++++----- 3 files changed, 142 insertions(+), 48 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 50a03fb9..df21a2bb 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -56,6 +56,9 @@ class MainWindow(QtGui.QMainWindow): self.CONNECTED_ICON = QtGui.QPixmap(":/images/conn_connected.png") self.ERROR_ICON = QtGui.QPixmap(":/images/conn_error.png") + self.LOGGED_OUT_ICON = QtGui.QPixmap(":/images/leap-gray-big.png") + self.LOGGED_IN_ICON = QtGui.QPixmap(":/images/leap-color-big.png") + self.ui = Ui_MainWindow() self.ui.setupUi(self) @@ -67,6 +70,10 @@ class MainWindow(QtGui.QMainWindow): self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) + self.ui.btnEipStartStop.setEnabled(False) + self.ui.btnEipStartStop.clicked.connect( + self._stop_eip) + # This is loaded only once, there's a bug when doing that more # than once self._provider_config = ProviderConfig() @@ -105,11 +112,13 @@ class MainWindow(QtGui.QMainWindow): self._eip_bootstrapper.download_config.connect( self._intermediate_stage) self._eip_bootstrapper.download_client_certificate.connect( - self._start_eip) + self._finish_eip_bootstrap) self._vpn = VPN() self._vpn.state_changed.connect(self._update_vpn_state) self._vpn.status_changed.connect(self._update_vpn_status) + self._vpn.process_finished.connect( + self._eip_finished) QtCore.QCoreApplication.instance().connect( QtCore.QCoreApplication.instance(), @@ -130,6 +139,22 @@ class MainWindow(QtGui.QMainWindow): self._really_quit = False self._systray = None + self._vpn_systray = None + + self._action_eip_status = QtGui.QAction("Encryption is OFF", self) + self._action_eip_status.setEnabled(False) + self._action_eip_stop = QtGui.QAction("Stop", self) + self._action_eip_stop.triggered.connect( + self._stop_eip) + self._action_eip_write = QtGui.QAction( + QtGui.QIcon(":/images/Arrow-Up-32.png"), + "0.0 Kb", self) + self._action_eip_write.setEnabled(False) + self._action_eip_read = QtGui.QAction( + QtGui.QIcon(":/images/Arrow-Down-32.png"), + "0.0 Kb", self) + self._action_eip_read.setEnabled(False) + self._action_visible = QtGui.QAction("Hide", self) self._action_visible.triggered.connect(self._toggle_visible) @@ -179,10 +204,20 @@ class MainWindow(QtGui.QMainWindow): systrayMenu.addAction(self.ui.action_quit) self._systray = QtGui.QSystemTrayIcon(self) self._systray.setContextMenu(systrayMenu) - self._systray.setIcon(QtGui.QIcon(self.ERROR_ICON)) + self._systray.setIcon(QtGui.QIcon(self.LOGGED_OUT_ICON)) self._systray.setVisible(True) self._systray.activated.connect(self._toggle_visible) + vpn_systrayMenu = QtGui.QMenu(self) + vpn_systrayMenu.addAction(self._action_eip_status) + vpn_systrayMenu.addAction(self._action_eip_stop) + vpn_systrayMenu.addAction(self._action_eip_read) + vpn_systrayMenu.addAction(self._action_eip_write) + self._vpn_systray = QtGui.QSystemTrayIcon(self) + self._vpn_systray.setContextMenu(vpn_systrayMenu) + self._vpn_systray.setIcon(QtGui.QIcon(self.ERROR_ICON)) + self._vpn_systray.setVisible(False) + def _toggle_visible(self): """ SLOT @@ -455,8 +490,33 @@ class MainWindow(QtGui.QMainWindow): triggers the eip bootstrapping """ self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) + self._systray.setIcon(self.LOGGED_IN_ICON) self._download_eip_config() + def _start_eip(self): + self._vpn.start(eipconfig=self._eip_config, + providerconfig=self._provider_config, + socket_host="/home/chiiph/vpnsock", + socket_port="unix") + self._vpn_systray.setVisible(True) + self.ui.btnEipStartStop.setEnabled(True) + self.ui.btnEipStartStop.setText("Stop EIP") + self.ui.btnEipStartStop.clicked.disconnect( + self._start_eip) + self.ui.btnEipStartStop.clicked.connect( + self._stop_eip) + + def _stop_eip(self): + self._vpn.set_should_quit() + self._vpn_systray.setVisible(False) + self._set_eip_status("EIP has stopped") + self._set_eip_status_icon("error") + self.ui.btnEipStartStop.setText("Start EIP") + self.ui.btnEipStartStop.clicked.disconnect( + self._stop_eip) + self.ui.btnEipStartStop.clicked.connect( + self._start_eip) + def _download_eip_config(self): """ Starts the EIP bootstrapping sequence @@ -483,13 +543,16 @@ class MainWindow(QtGui.QMainWindow): @type status: str """ selected_pixmap = self.ERROR_ICON + tray_message = "Encryption is OFF" if status in ("AUTH", "GET_CONFIG"): selected_pixmap = self.CONNECTING_ICON elif status in ("CONNECTED"): + tray_message = "Encryption is ON" selected_pixmap = self.CONNECTED_ICON self.ui.lblVPNStatusIcon.setPixmap(selected_pixmap) - self._systray.setIcon(QtGui.QIcon(selected_pixmap)) + self._vpn_systray.setIcon(QtGui.QIcon(selected_pixmap)) + self._action_eip_status.setText(tray_message) def _update_vpn_state(self, data): """ @@ -520,12 +583,16 @@ class MainWindow(QtGui.QMainWindow): """ upload = float(data[self._vpn.TUNTAP_WRITE_KEY]) upload = upload / 1000.0 - self.ui.lblUpload.setText("%s Kb" % (upload,)) + upload_str = "%s Kb" % (upload,) + self.ui.lblUpload.setText(upload_str) + self._action_eip_write.setText(upload_str) download = float(data[self._vpn.TUNTAP_READ_KEY]) download = download / 1000.0 - self.ui.lblDownload.setText("%s Kb" % (download,)) + download_str = "%s Kb" % (download,) + self.ui.lblDownload.setText(download_str) + self._action_eip_read.setText(download_str) - def _start_eip(self, data): + def _finish_eip_bootstrap(self, data): """ SLOT TRIGGER: self._eip_bootstrapper.download_client_certificate @@ -542,10 +609,7 @@ class MainWindow(QtGui.QMainWindow): self._provider_config .get_domain(), "eip-service.json")): - self._vpn.start(eipconfig=self._eip_config, - providerconfig=self._provider_config, - socket_host="/home/chiiph/vpnsock", - socket_port="unix") + self._start_eip() # TODO: display a message if the EIP configuration cannot be # loaded @@ -569,12 +633,14 @@ class MainWindow(QtGui.QMainWindow): logging out """ self._set_status(message) + self._vpn_systray.setIcon(self.LOGGED_OUT_ICON) self.ui.action_sign_out.setEnabled(False) self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) self.ui.lnPassword.setText("") self._login_set_enabled(True) self._set_status("") self._vpn.set_should_quit() + self._vpn_systray.setVisible(False) def _intermediate_stage(self, data): """ @@ -594,6 +660,18 @@ class MainWindow(QtGui.QMainWindow): self._login_set_enabled(True) self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) + def _eip_finished(self, exitCode): + """ + SLOT + TRIGGERS: + self._vpn.process_finished + + Triggered when the EIP/VPN process finishes to set the UI + accordingly + """ + logger.debug("Finished VPN with exitCode %s" % (exitCode,)) + self._stop_eip() + if __name__ == "__main__": import signal from functools import partial diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui index a527eaf6..d8a6d37d 100644 --- a/src/leap/gui/ui/mainwindow.ui +++ b/src/leap/gui/ui/mainwindow.ui @@ -171,16 +171,6 @@ - - - - Disconnected - - - Qt::AlignCenter - - - @@ -194,20 +184,7 @@ - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + @@ -220,7 +197,7 @@ - + @@ -233,7 +210,7 @@ - + Qt::Horizontal @@ -246,7 +223,7 @@ - + @@ -256,7 +233,7 @@ - + @@ -266,6 +243,36 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Disconnected + + + Qt::AlignCenter + + + + + + + Start EIP + + + diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 88692442..71944f50 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -22,7 +22,6 @@ import logging import sys from PySide import QtCore, QtGui -from subprocess import Popen, PIPE from functools import partial from leap.config.providerconfig import ProviderConfig @@ -46,6 +45,8 @@ class VPN(QtCore.QThread): state_changed = QtCore.Signal(dict) status_changed = QtCore.Signal(dict) + process_finished = QtCore.Signal(int) + CONNECTION_RETRY_TIME = 1000 POLL_TIME = 100 @@ -69,7 +70,6 @@ class VPN(QtCore.QThread): self._launcher = get_platform_launcher() self._subp = None - self._started = False self._tn = None self._host = None @@ -100,15 +100,14 @@ class VPN(QtCore.QThread): return try: - self._disconnect() + self._send_command("signal SIGTERM") + self._tn.close() self._subp.terminate() except Exception as e: logger.debug("Could not terminate process, trying command " + "signal SIGNINT: %r" % (e,)) - self._send_command("signal SIGINT") - self._subp.wait() - self.wait() - self._started = False + finally: + self._tn = None def start(self, eipconfig, providerconfig, socket_host, socket_port): """ @@ -128,7 +127,7 @@ class VPN(QtCore.QThread): leap_assert_type(eipconfig, EIPConfig) leap_assert(providerconfig, "We need a provider config") leap_assert_type(providerconfig, ProviderConfig) - leap_assert(not self._started, "Starting process more than once!") + leap_assert(not self.isRunning(), "Starting process more than once!") logger.debug("Starting VPN...") @@ -140,8 +139,12 @@ class VPN(QtCore.QThread): socket_host=socket_host, socket_port=socket_port) try: - self._subp = Popen(command, stdout=PIPE, stderr=PIPE, - bufsize=1, close_fds=ON_POSIX) + self._subp = QtCore.QProcess() + self._subp.finished.connect(self.process_finished) + self._subp.start(command[:1][0], command[1:]) + logger.debug("Waiting for started...") + self._subp.waitForStarted() + logger.debug("Started!") self._host = socket_host self._port = socket_port @@ -296,12 +299,18 @@ class VPN(QtCore.QThread): logger.debug("Quitting VPN thread") return + if self._subp and self._subp.state() != QtCore.QProcess.Running: + QtCore.QThread.msleep(self.CONNECTION_RETRY_TIME) + if self._tn is None: self._connect(self._host, self._port) QtCore.QThread.msleep(self.CONNECTION_RETRY_TIME) else: self._parse_state_and_notify(self._send_command("state")) self._parse_status_and_notify(self._send_command("status")) + output_sofar = self._subp.readAllStandardOutput() + if len(output_sofar) > 0: + logger.debug(output_sofar) QtCore.QThread.msleep(self.POLL_TIME) -- cgit v1.2.3 From 926575bc811e8382100695a3396da7191fb43eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 8 Mar 2013 13:15:38 -0300 Subject: Add translation support Also: - Make OpenVPN use a random port every time - Logout in parallel so the UI doesn't block - Add the WAIT status from OpenVPN to the mainwindow displays - Support non-unix sockets in the LinuxVPNLauncher --- src/leap/app.py | 3 +- src/leap/config/baseconfig.py | 11 ++++-- src/leap/crypto/srpauth.py | 34 +++++++++-------- src/leap/gui/mainwindow.py | 72 +++++++++++++++++++---------------- src/leap/gui/ui/wizard.ui | 1 + src/leap/gui/wizard.py | 28 +++++++------- src/leap/services/eip/vpn.py | 5 ++- src/leap/services/eip/vpnlaunchers.py | 11 +++++- src/leap/util/leap_argparse.py | 2 +- 9 files changed, 95 insertions(+), 72 deletions(-) (limited to 'src') diff --git a/src/leap/app.py b/src/leap/app.py index fa50cd1e..743ea6ea 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -27,8 +27,7 @@ from PySide import QtCore, QtGui VERSION = "unknown" from leap.util import leap_argparse -# TODO: add translations -#from leap.gui import locale_rc +from leap.gui import locale_rc from leap.gui.mainwindow import MainWindow diff --git a/src/leap/config/baseconfig.py b/src/leap/config/baseconfig.py index b80fd419..b6890d09 100644 --- a/src/leap/config/baseconfig.py +++ b/src/leap/config/baseconfig.py @@ -159,10 +159,13 @@ class LocalizedKey(object): """ descriptions = self._func(instance) description_lang = "" - if lang in descriptions.keys(): - description_lang = descriptions[lang] - else: - logger.warning("Unknown language: %s" % (lang,)) + config_lang = "en" + for key in descriptions.keys(): + if lang.startswith(key): + config_lang = key + break + + description_lang = descriptions[config_lang] return description_lang def __get__(self, instance, instancetype): diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index 28e4f037..8530b7da 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -156,18 +156,18 @@ class SRPAuth(QtCore.QObject): "Status code = %r. Content: %r" % (init_session.status_code, init_session.content)) if init_session.status_code == 422: - raise SRPAuthenticationError("Unknown user") + raise SRPAuthenticationError(self.tr("Unknown user")) salt = init_session.json().get("salt", None) B = init_session.json().get("B", None) if salt is None: logger.error("No salt parameter sent") - raise SRPAuthenticationError("The server did not send the " + - "salt parameter") + raise SRPAuthenticationError(self.tr("The server did not send " + "the salt parameter")) if B is None: logger.error("No B parameter sent") - raise SRPAuthenticationError("The server did not send the " + - "B parameter") + raise SRPAuthenticationError(self.tr("The server did not send " + "the B parameter")) return salt, B @@ -194,8 +194,8 @@ class SRPAuth(QtCore.QObject): unhex_B = self._safe_unhexlify(B) except TypeError as e: logger.error("Bad data from server: %r" % (e,)) - raise SRPAuthenticationError("The data sent from the server " - "had errors") + raise SRPAuthenticationError(self.tr("The data sent from " + "the server had errors")) M = self._srp_user.process_challenge(unhex_salt, unhex_B) auth_url = "%s/%s/%s/%s" % (self._provider_config.get_api_uri(), @@ -215,20 +215,21 @@ class SRPAuth(QtCore.QObject): get_ca_cert_path()) except requests.exceptions.ConnectionError as e: logger.error("No connection made (HAMK): %r" % (e,)) - raise SRPAuthenticationError("Could not connect to the server") + raise SRPAuthenticationError(self.tr("Could not connect to " + "the server")) if auth_result.status_code == 422: logger.error("[%s] Wrong password (HAMK): [%s]" % (auth_result.status_code, auth_result.json(). get("errors", ""))) - raise SRPAuthenticationError("Wrong password") + raise SRPAuthenticationError(self.tr("Wrong password")) if auth_result.status_code not in (200,): logger.error("No valid response (HAMK): " "Status code = %s. Content = %r" % (auth_result.status_code, auth_result.content)) - raise SRPAuthenticationError("Unknown error (%s)" % + raise SRPAuthenticationError(self.tr("Unknown error (%s)") % (auth_result.status_code,)) M2 = auth_result.json().get("M2", None) @@ -237,8 +238,8 @@ class SRPAuth(QtCore.QObject): if M2 is None or self.get_uid() is None: logger.error("Something went wrong. Content = %r" % (auth_result.content,)) - raise SRPAuthenticationError("Problem getting data from" - " server") + raise SRPAuthenticationError(self.tr("Problem getting data " + "from server")) return M2 @@ -258,13 +259,14 @@ class SRPAuth(QtCore.QObject): unhex_M2 = self._safe_unhexlify(M2) except TypeError: logger.error("Bad data from server (HAWK)") - raise SRPAuthenticationError("Bad data from server") + raise SRPAuthenticationError(self.tr("Bad data from server")) self._srp_user.verify_session(unhex_M2) if not self._srp_user.authenticated(): logger.error("Auth verification failed") - raise SRPAuthenticationError("Auth verification failed") + raise SRPAuthenticationError(self.tr("Auth verification " + "failed")) logger.debug("Session verified.") self.set_session_id(self._session.cookies["_session_id"]) @@ -379,7 +381,7 @@ class SRPAuth(QtCore.QObject): self.__instance.authenticate(username, password) logger.debug("Successful login!") - self.authentication_finished.emit(True, "Succeeded") + self.authentication_finished.emit(True, self.tr("Succeeded")) return True except Exception as e: logger.error("Error logging in %s" % (e,)) @@ -393,7 +395,7 @@ class SRPAuth(QtCore.QObject): """ try: self.__instance.logout() - self.logout_finished.emit(True, "Succeeded") + self.logout_finished.emit(True, self.tr("Succeeded")) return True except Exception as e: self.logout_finished.emit(False, "%s" % (e,)) diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index df21a2bb..d66ddbb5 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -20,6 +20,7 @@ Main window for the leap client """ import os import logging +import random from PySide import QtCore, QtGui @@ -141,9 +142,10 @@ class MainWindow(QtGui.QMainWindow): self._systray = None self._vpn_systray = None - self._action_eip_status = QtGui.QAction("Encryption is OFF", self) + self._action_eip_status = QtGui.QAction(self.tr("Encryption is OFF"), + self) self._action_eip_status.setEnabled(False) - self._action_eip_stop = QtGui.QAction("Stop", self) + self._action_eip_stop = QtGui.QAction(self.tr("Stop"), self) self._action_eip_stop.triggered.connect( self._stop_eip) self._action_eip_write = QtGui.QAction( @@ -155,7 +157,7 @@ class MainWindow(QtGui.QMainWindow): "0.0 Kb", self) self._action_eip_read.setEnabled(False) - self._action_visible = QtGui.QAction("Hide", self) + self._action_visible = QtGui.QAction(self.tr("Hide"), self) self._action_visible.triggered.connect(self._toggle_visible) self._center_window() @@ -226,9 +228,9 @@ class MainWindow(QtGui.QMainWindow): Toggles the window visibility """ self.setVisible(not self.isVisible()) - action_visible_text = "Hide" + action_visible_text = self.tr("Hide") if not self.isVisible(): - action_visible_text = "Show" + action_visible_text = self.tr("Show") self._action_visible.setText(action_visible_text) def _center_window(self): @@ -257,14 +259,15 @@ class MainWindow(QtGui.QMainWindow): """ Display the About LEAP dialog """ - QtGui.QMessageBox.about(self, "About LEAP", - "LEAP is a non-profit dedicated to giving " - "all internet users access to secure " - "communication. Our focus is on adapting " - "encryption technology to make it easy to use " - "and widely available. " - "More about LEAP" - "") + QtGui.QMessageBox.about( + self, self.tr("About LEAP"), + self.tr("LEAP is a non-profit dedicated to giving " + "all internet users access to secure " + "communication. Our focus is on adapting " + "encryption technology to make it easy to use " + "and widely available. " + "More about LEAP" + "")) def quit(self): self._really_quit = True @@ -399,7 +402,8 @@ class MainWindow(QtGui.QMainWindow): self._provider_config, download_if_needed=True) else: - self._set_status("Could not load provider configuration") + self._set_status( + self.tr("Could not load provider configuration")) self._login_set_enabled(True) else: self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) @@ -424,18 +428,18 @@ class MainWindow(QtGui.QMainWindow): provider = self.ui.cmbProviders.currentText() if len(provider) == 0: - self._set_status("Please select a valid provider") + self._set_status(self.tr("Please select a valid provider")) return if len(username) == 0: - self._set_status("Please provide a valid username") + self._set_status(self.tr("Please provide a valid username")) return if len(password) == 0: - self._set_status("Please provide a valid Password") + self._set_status(self.tr("Please provide a valid Password")) return - self._set_status("Logging in...") + self._set_status(self.tr("Logging in...")) self._login_set_enabled(False) self._download_provider_config() @@ -496,11 +500,11 @@ class MainWindow(QtGui.QMainWindow): def _start_eip(self): self._vpn.start(eipconfig=self._eip_config, providerconfig=self._provider_config, - socket_host="/home/chiiph/vpnsock", - socket_port="unix") + socket_host="localhost", + socket_port=str(random.randint(1000, 9999))) self._vpn_systray.setVisible(True) self.ui.btnEipStartStop.setEnabled(True) - self.ui.btnEipStartStop.setText("Stop EIP") + self.ui.btnEipStartStop.setText(self.tr("Stop EIP")) self.ui.btnEipStartStop.clicked.disconnect( self._start_eip) self.ui.btnEipStartStop.clicked.connect( @@ -509,9 +513,9 @@ class MainWindow(QtGui.QMainWindow): def _stop_eip(self): self._vpn.set_should_quit() self._vpn_systray.setVisible(False) - self._set_eip_status("EIP has stopped") + self._set_eip_status(self.tr("EIP has stopped")) self._set_eip_status_icon("error") - self.ui.btnEipStartStop.setText("Start EIP") + self.ui.btnEipStartStop.setText(self.tr("Start EIP")) self.ui.btnEipStartStop.clicked.disconnect( self._stop_eip) self.ui.btnEipStartStop.clicked.connect( @@ -524,7 +528,7 @@ class MainWindow(QtGui.QMainWindow): leap_assert(self._eip_bootstrapper, "We need an eip bootstrapper!") leap_assert(self._provider_config, "We need a provider config") - self._set_eip_status("Checking configuration, please wait...") + self._set_eip_status(self.tr("Checking configuration, please wait...")) if self._provider_config.provides_eip(): self._eip_bootstrapper.run_eip_setup_checks( @@ -532,7 +536,7 @@ class MainWindow(QtGui.QMainWindow): self._provider_config, download_if_needed=True) else: - self._set_eip_status("%s does not support EIP" % + self._set_eip_status(self.tr("%s does not support EIP") % (self._provider_config.get_domain(),)) def _set_eip_status_icon(self, status): @@ -543,11 +547,11 @@ class MainWindow(QtGui.QMainWindow): @type status: str """ selected_pixmap = self.ERROR_ICON - tray_message = "Encryption is OFF" - if status in ("AUTH", "GET_CONFIG"): + tray_message = self.tr("Encryption is OFF") + if status in ("WAIT", "AUTH", "GET_CONFIG"): selected_pixmap = self.CONNECTING_ICON elif status in ("CONNECTED"): - tray_message = "Encryption is ON" + tray_message = self.tr("Encryption is ON") selected_pixmap = self.CONNECTED_ICON self.ui.lblVPNStatusIcon.setPixmap(selected_pixmap) @@ -565,11 +569,13 @@ class MainWindow(QtGui.QMainWindow): status = data[self._vpn.STATUS_STEP_KEY] self._set_eip_status_icon(status) if status == "AUTH": - self._set_eip_status("VPN: Authenticating...") + self._set_eip_status(self.tr("VPN: Authenticating...")) elif status == "GET_CONFIG": - self._set_eip_status("VPN: Retrieving configuration...") + self._set_eip_status(self.tr("VPN: Retrieving configuration...")) elif status == "CONNECTED": - self._set_eip_status("VPN: Connected!") + self._set_eip_status(self.tr("VPN: Connected!")) + elif status == "WAIT": + self._set_eip_status(self.tr("VPN: Waiting to start...")) else: self._set_eip_status(status) @@ -621,8 +627,8 @@ class MainWindow(QtGui.QMainWindow): Starts the logout sequence """ self._set_eip_status_icon("error") - self._set_eip_status("Signing out...") - self._srp_auth.logout() + self._set_eip_status(self.tr("Signing out...")) + self._checker_thread.add_checks([self._srp_auth.logout]) def _done_logging_out(self, ok, message): """ diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui index 2d9cb441..a7198c5f 100644 --- a/src/leap/gui/ui/wizard.ui +++ b/src/leap/gui/ui/wizard.ui @@ -798,6 +798,7 @@ + diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index bac74d1d..fa705645 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -21,7 +21,7 @@ First run wizard import os import logging -from PySide import QtGui +from PySide import QtCore, QtGui from functools import partial from ui_wizard import Ui_Wizard @@ -152,19 +152,20 @@ class Wizard(QtGui.QWizard): username.encode("ascii") password.encode("ascii") except: - message = u"Refrain from using non ASCII áñ characters" + message = self.tr(u"Refrain from using non " + u"ASCII characters like á, ñ, æ") if message is not None and password != password2: - message = "Passwords don't match" + message = self.tr("Passwords don't match") if message is None and len(password) < 6: - message = "Password too short" + message = self.tr("Password too short") if message is None and password in self.WEAK_PASSWORDS: - message = "Password too easy" + message = self.tr("Password too easy") if message is None and username == password: - message = "Password equal to username" + message = self.tr("Password equal to username") if message is not None: self._set_register_status(message) @@ -190,20 +191,20 @@ class Wizard(QtGui.QWizard): self._checker_thread.add_checks( [partial(register.register_user, username, password)]) self._username = username - self._set_register_status("Starting registration...") + self._set_register_status(self.tr("Starting registration...")) else: self.ui.btnRegister.setEnabled(True) def _registration_finished(self, ok, req): if ok: - self._set_register_status("" - "User registration OK") + self._set_register_status(self.tr("" + "User registration OK")) self.ui.lblPassword2.clearFocus() self.page(self.REGISTER_USER_PAGE).set_completed() self.button(QtGui.QWizard.BackButton).setEnabled(False) else: self._username = None - error_msg = "Unknown error" + error_msg = self.tr("Unknown error") try: error_msg = req.json().get("errors").get("login")[0] except: @@ -304,7 +305,7 @@ class Wizard(QtGui.QWizard): new_data = { self._provider_bootstrapper.PASSED_KEY: False, self._provider_bootstrapper.ERROR_KEY: - "Unable to load provider configuration" + self.tr("Unable to load provider configuration") } self._complete_task(new_data, self.ui.lblProviderInfo) @@ -387,13 +388,14 @@ class Wizard(QtGui.QWizard): if pageId == self.PRESENT_PROVIDER_PAGE: # TODO: get the right lang for these + lang = QtCore.QLocale.system().name() self.ui.lblProviderName.setText( "%s" % - (self._provider_config.get_name(),)) + (self._provider_config.get_name(lang=lang),)) self.ui.lblProviderURL.setText(self._provider_config.get_domain()) self.ui.lblProviderDesc.setText( "%s" % - (self._provider_config.get_description(),)) + (self._provider_config.get_description(lang=lang),)) self.ui.lblProviderPolicy.setText(self._provider_config .get_enrollment_policy()) diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 71944f50..3ec32f6f 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -31,7 +31,6 @@ from leap.services.eip.udstelnet import UDSTelnet from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) -ON_POSIX = 'posix' in sys.builtin_module_names # TODO: abstract the thread that can be asked to quit to another @@ -103,6 +102,7 @@ class VPN(QtCore.QThread): self._send_command("signal SIGTERM") self._tn.close() self._subp.terminate() + self._subp.waitForFinished() except Exception as e: logger.debug("Could not terminate process, trying command " + "signal SIGNINT: %r" % (e,)) @@ -309,6 +309,9 @@ class VPN(QtCore.QThread): self._parse_state_and_notify(self._send_command("state")) self._parse_status_and_notify(self._send_command("status")) output_sofar = self._subp.readAllStandardOutput() + if len(output_sofar) > 0: + logger.debug(output_sofar) + output_sofar = self._subp.readAllStandardError() if len(output_sofar) > 0: logger.debug(output_sofar) QtCore.QThread.msleep(self.POLL_TIME) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 00e9c966..cf817321 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -215,8 +215,15 @@ class LinuxVPNLauncher(VPNLauncher): args += [ '--user', getpass.getuser(), - '--group', grp.getgrgid(os.getgroups()[-1]).gr_name, - '--management-client-user', getpass.getuser(), + '--group', grp.getgrgid(os.getgroups()[-1]).gr_name + ] + + if socket_port == "unix": + args += [ + '--management-client-user', getpass.getuser() + ] + + args += [ '--management-signal', '--management', socket_host, socket_port, '--script-security', '2' diff --git a/src/leap/util/leap_argparse.py b/src/leap/util/leap_argparse.py index 78597f63..83272a3d 100644 --- a/src/leap/util/leap_argparse.py +++ b/src/leap/util/leap_argparse.py @@ -20,7 +20,7 @@ import argparse def build_parser(): """ - all the options for the leap arg parser + All the options for the leap arg parser Some of these could be switched on only if debug flag is present! """ epilog = "Copyright 2012 The LEAP Encryption Access Project" -- cgit v1.2.3 From e4e5f35c3fc7ff02bc20a6ef7eaffae09f485061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 8 Mar 2013 14:24:05 -0300 Subject: Add keyring and username/password saving capabilities Also: - Fix translations in SRPAuth - Support non-ascii passwords - Make the server check if the characters are allowed, just check for easy passwords --- src/leap/crypto/srpauth.py | 4 +++- src/leap/gui/mainwindow.py | 33 +++++++++++++++++++++++++++++++-- src/leap/gui/wizard.py | 11 +++-------- 3 files changed, 37 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index 8530b7da..2877efab 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -41,7 +41,7 @@ class SRPAuth(QtCore.QObject): SRPAuth singleton """ - class __impl(object): + class __impl(QtCore.QObject): """ Implementation of the SRPAuth interface """ @@ -57,6 +57,8 @@ class SRPAuth(QtCore.QObject): @param server: Server to which we will authenticate @type server: str """ + QtCore.QObject.__init__(self) + leap_assert(provider_config, "We need a provider config to authenticate") diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index d66ddbb5..68c73e4d 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -21,6 +21,7 @@ Main window for the leap client import os import logging import random +import keyring from PySide import QtCore, QtGui @@ -49,6 +50,9 @@ class MainWindow(QtGui.QMainWindow): GEOMETRY_KEY = "Geometry" WINDOWSTATE_KEY = "WindowState" + USER_KEY = "User" + + KEYRING_KEY = "leap_client" def __init__(self): QtGui.QMainWindow.__init__(self) @@ -183,6 +187,7 @@ class MainWindow(QtGui.QMainWindow): if self._wizard is None: self._wizard = Wizard(self._checker_thread) self._wizard.exec_() + self._wizard = None def _finish_init(self): self.ui.cmbProviders.addItems(self._configured_providers()) @@ -194,6 +199,17 @@ class MainWindow(QtGui.QMainWindow): self.ui.lnUser.setText(possible_username) self._focus_password() self._wizard = None + else: + settings = QtCore.QSettings() + saved_user = settings.value(self.USER_KEY, None) + + if saved_user is not None: + self.ui.lnUser.setText(saved_user) + self.ui.chkRemember.setChecked(True) + saved_password = keyring.get_password(self.KEYRING_KEY, + saved_user + .encode("utf8")) + self.ui.lnPassword.setText(saved_password.decode("utf8")) def _show_systray(self): """ @@ -442,6 +458,19 @@ class MainWindow(QtGui.QMainWindow): self._set_status(self.tr("Logging in...")) self._login_set_enabled(False) + if self.ui.chkRemember.isChecked(): + try: + keyring.set_password(self.KEYRING_KEY, + username.encode("utf8"), + password.encode("utf8")) + # Only save the username if it was saved correctly in + # the keyring + settings = QtCore.QSettings() + settings.setValue(self.USER_KEY, username) + except Exception as e: + logger.error("Problem saving data to keyring. %r" + % (e,)) + self._download_provider_config() def _provider_config_loaded(self, data): @@ -455,8 +484,8 @@ class MainWindow(QtGui.QMainWindow): leap_assert(self._provider_config, "We need a provider config!") if data[self._provider_bootstrapper.PASSED_KEY]: - username = self.ui.lnUser.text() - password = self.ui.lnPassword.text() + username = self.ui.lnUser.text().encode("utf8") + password = self.ui.lnPassword.text().encode("utf8") if self._srp_auth is None: self._srp_auth = SRPAuth(self._provider_config) diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index fa705645..72e791b3 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -148,13 +148,6 @@ class Wizard(QtGui.QWizard): """ message = None - try: - username.encode("ascii") - password.encode("ascii") - except: - message = self.tr(u"Refrain from using non " - u"ASCII characters like á, ñ, æ") - if message is not None and password != password2: message = self.tr("Passwords don't match") @@ -189,7 +182,9 @@ class Wizard(QtGui.QWizard): register.registration_finished.connect( self._registration_finished) self._checker_thread.add_checks( - [partial(register.register_user, username, password)]) + [partial(register.register_user, + username.encode("utf8"), + password.encode("utf8"))]) self._username = username self._set_register_status(self.tr("Starting registration...")) else: -- cgit v1.2.3 From 235de1cc59756ebf92100a824a95ed1a6fb29c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 8 Mar 2013 14:35:34 -0300 Subject: Add version handling --- src/leap/__init__.py | 29 ++++++++ src/leap/_version.py | 204 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/leap/app.py | 4 +- 3 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 src/leap/_version.py (limited to 'src') diff --git a/src/leap/__init__.py b/src/leap/__init__.py index e69de29b..44235c7c 100644 --- a/src/leap/__init__.py +++ b/src/leap/__init__.py @@ -0,0 +1,29 @@ +""" +LEAP Encryption Access Project +website: U{https://leap.se/} +""" + +__version__ = "unknown" +try: + from ._version import get_versions + __version__ = get_versions()['version'] + del get_versions +except ImportError: + #running on a tree that has not run + #the setup.py setver + pass + +__appname__ = "unknown" +try: + from leap._appname import __appname__ +except ImportError: + #running on a tree that has not run + #the setup.py setver + pass + +__full_version__ = __appname__ + '/' + str(__version__) + +# try: +# from leap._branding import BRANDING as __branding +# except ImportError: +# __branding = {} diff --git a/src/leap/_version.py b/src/leap/_version.py new file mode 100644 index 00000000..6f7e3d05 --- /dev/null +++ b/src/leap/_version.py @@ -0,0 +1,204 @@ + +IN_LONG_VERSION_PY = True +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (build by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.7+ (https://github.com/warner/python-versioneer) + +# these strings will be replaced by git during git-archive +git_refnames = "$Format:%d$" +git_full = "$Format:%H$" + + +import subprocess +import sys + + +def run_command(args, cwd=None, verbose=False): + try: + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) + except EnvironmentError: + e = sys.exc_info()[1] + if verbose: + print("unable to run %s" % args[0]) + print(e) + return None + stdout = p.communicate()[0].strip() + if sys.version >= '3': + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % args[0]) + return None + return stdout + + +import sys +import re +import os.path + + +def get_expanded_variables(versionfile_source): + # the code embedded in _version.py can just fetch the value of these + # variables. When used from setup.py, we don't want to import + # _version.py, so we do it with a regexp instead. This function is not + # used from _version.py. + variables = {} + try: + for line in open(versionfile_source, "r").readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["full"] = mo.group(1) + except EnvironmentError: + pass + return variables + + +def versions_from_expanded_variables(variables, tag_prefix, verbose=False): + refnames = variables["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("variables are unexpanded, not using") + return {} # unexpanded, so not in an unpacked git-archive tarball + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + for ref in list(refs): + if not re.search(r'\d', ref): + if verbose: + print("discarding '%s', no digits" % ref) + refs.discard(ref) + # Assume all version tags have a digit. git's %d expansion + # behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us + # distinguish between branches and tags. By ignoring refnames + # without digits, we filter out many common branch names like + # "release" and "stabilization", as well as "HEAD" and "master". + if verbose: + print("remaining refs: %s" % ",".join(sorted(refs))) + for ref in sorted(refs): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return {"version": r, + "full": variables["full"].strip()} + # no suitable tags, so we use the full revision id + if verbose: + print("no suitable tags, using full revision id") + return {"version": variables["full"].strip(), + "full": variables["full"].strip()} + + +def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): + # this runs 'git' from the root of the source tree. That either means + # someone ran a setup.py command (and this code is in versioneer.py, so + # IN_LONG_VERSION_PY=False, thus the containing directory is the root of + # the source tree), or someone ran a project-specific entry point (and + # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the + # containing directory is somewhere deeper in the source tree). This only + # gets called if the git-archive 'subst' variables were *not* expanded, + # and _version.py hasn't already been rewritten with a short version + # string, meaning we're inside a checked out source tree. + + try: + here = os.path.abspath(__file__) + except NameError: + # some py2exe/bbfreeze/non-CPython implementations don't do __file__ + return {} # not always correct + + # versionfile_source is the relative path from the top of the source tree + # (where the .git directory might live) to this file. Invert this to find + # the root from __file__. + root = here + if IN_LONG_VERSION_PY: + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + root = os.path.dirname(here) + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %s" % root) + return {} + + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], + cwd=root) + if stdout is None: + return {} + if not stdout.startswith(tag_prefix): + if verbose: + print("tag '%s' doesn't start with prefix '%s'" % ( + stdout, tag_prefix)) + return {} + tag = stdout[len(tag_prefix):] + stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) + if stdout is None: + return {} + full = stdout.strip() + if tag.endswith("-dirty"): + full += "-dirty" + return {"version": tag, "full": full} + + +def versions_from_parentdir(parentdir_prefix, versionfile_source, + verbose=False): + if IN_LONG_VERSION_PY: + # We're running from _version.py. If it's from a source tree + # (execute-in-place), we can work upwards to find the root of the + # tree, and then check the parent directory for a version string. If + # it's in an installed application, there's no hope. + try: + here = os.path.abspath(__file__) + except NameError: + # py2exe/bbfreeze/non-CPython don't have __file__ + return {} # without __file__, we have no hope + # versionfile_source is the relative path from the top of the source + # tree to _version.py. Invert this to find the root from __file__. + root = here + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + # we're running from versioneer.py, which means we're running from + # the setup.py in a source tree. sys.argv[0] is setup.py in the root. + here = os.path.abspath(sys.argv[0]) + root = os.path.dirname(here) + + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%s', but '%s' " + "doesn't start with prefix '%s'" % + (root, dirname, parentdir_prefix)) + return None + return {"version": dirname[len(parentdir_prefix):], "full": ""} + +tag_prefix = "" +parentdir_prefix = "leap_client-" +versionfile_source = "src/leap/_version.py" + + +def get_versions(default={"version": "unknown", "full": ""}, verbose=False): + variables = {"refnames": git_refnames, "full": git_full} + ver = versions_from_expanded_variables(variables, tag_prefix, verbose) + if not ver: + ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) + if not ver: + ver = versions_from_parentdir(parentdir_prefix, versionfile_source, + verbose) + if not ver: + ver = default + return ver diff --git a/src/leap/app.py b/src/leap/app.py index 743ea6ea..a790f81b 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -22,9 +22,7 @@ import sys from functools import partial from PySide import QtCore, QtGui -# TODO: add version handling -#from leap import __version__ as VERSION -VERSION = "unknown" +from leap import __version__ as VERSION from leap.util import leap_argparse from leap.gui import locale_rc -- cgit v1.2.3 From 1e3631c9a21f99e08bbb4bace61590b1221bc9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 10:24:25 -0300 Subject: Add AutoLogin checkbox --- src/leap/gui/mainwindow.py | 17 +++++++++++++++-- src/leap/gui/ui/mainwindow.ui | 38 ++++++++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 68c73e4d..399a7cb4 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -48,10 +48,13 @@ class MainWindow(QtGui.QMainWindow): LOGIN_INDEX = 0 EIP_STATUS_INDEX = 1 + # Settings GEOMETRY_KEY = "Geometry" WINDOWSTATE_KEY = "WindowState" USER_KEY = "User" + AUTOLOGIN_KEY = "AutoLogin" + # Keyring KEYRING_KEY = "leap_client" def __init__(self): @@ -202,15 +205,22 @@ class MainWindow(QtGui.QMainWindow): else: settings = QtCore.QSettings() saved_user = settings.value(self.USER_KEY, None) + auto_login = settings.value(self.AUTOLOGIN_KEY, None) if saved_user is not None: self.ui.lnUser.setText(saved_user) self.ui.chkRemember.setChecked(True) + self.ui.chkAutoLogin.setEnabled(True) saved_password = keyring.get_password(self.KEYRING_KEY, saved_user .encode("utf8")) self.ui.lnPassword.setText(saved_password.decode("utf8")) + # Only automatically login if there is a saved user + if auto_login is not None: + self.ui.chkAutoLogin.setChecked(True) + self._login() + def _show_systray(self): """ Sets up the systray icon @@ -315,6 +325,7 @@ class MainWindow(QtGui.QMainWindow): settings = QtCore.QSettings() settings.setValue(self.GEOMETRY_KEY, self.saveGeometry()) settings.setValue(self.WINDOWSTATE_KEY, self.saveState()) + settings.setValue(self.AUTOLOGIN_KEY, self.ui.chkAutoLogin.isChecked()) QtGui.QMainWindow.closeEvent(self, e) def _configured_providers(self): @@ -378,6 +389,7 @@ class MainWindow(QtGui.QMainWindow): self.ui.lnPassword.setEnabled(enabled) self.ui.btnLogin.setEnabled(enabled) self.ui.chkRemember.setEnabled(enabled) + self.ui.chkAutoLogin.setEnabled(enabled) self.ui.cmbProviders.setEnabled(enabled) def _download_provider_config(self): @@ -458,6 +470,8 @@ class MainWindow(QtGui.QMainWindow): self._set_status(self.tr("Logging in...")) self._login_set_enabled(False) + settings = QtCore.QSettings() + if self.ui.chkRemember.isChecked(): try: keyring.set_password(self.KEYRING_KEY, @@ -465,7 +479,6 @@ class MainWindow(QtGui.QMainWindow): password.encode("utf8")) # Only save the username if it was saved correctly in # the keyring - settings = QtCore.QSettings() settings.setValue(self.USER_KEY, username) except Exception as e: logger.error("Problem saving data to keyring. %r" @@ -577,7 +590,7 @@ class MainWindow(QtGui.QMainWindow): """ selected_pixmap = self.ERROR_ICON tray_message = self.tr("Encryption is OFF") - if status in ("WAIT", "AUTH", "GET_CONFIG"): + if status in ("WAIT", "AUTH", "GET_CONFIG", "RECONNECTING"): selected_pixmap = self.CONNECTING_ICON elif status in ("CONNECTED"): tray_message = self.tr("Encryption is ON") diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui index d8a6d37d..d723114c 100644 --- a/src/leap/gui/ui/mainwindow.ui +++ b/src/leap/gui/ui/mainwindow.ui @@ -70,13 +70,10 @@ - 1 + 0 - - - @@ -84,15 +81,11 @@ - - - - Login - - + + - - + + Qt::Horizontal @@ -104,8 +97,15 @@ - - + + + + Login + + + + + Qt::Horizontal @@ -167,6 +167,16 @@ + + + + false + + + Automatically login + + + -- cgit v1.2.3 From eaec4a9fff67134284f45eb850f0d7e0f7f275f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 13:37:14 -0300 Subject: Finish up translation support Also: - Add spanish translations - Fix an issue when disabling autologin --- src/leap/app.py | 2 +- src/leap/gui/mainwindow.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/app.py b/src/leap/app.py index a790f81b..246f2d14 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -81,7 +81,7 @@ def main(): if qtTranslator.load("qt_%s" % locale, ":/translations"): app.installTranslator(qtTranslator) appTranslator = QtCore.QTranslator() - if appTranslator.load("leap_client_%s" % locale, ":/translations"): + if appTranslator.load("%s.qm" % locale[:2], ":/translations"): app.installTranslator(appTranslator) # Needed for initializing qsettings it will write diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 399a7cb4..4e6bb5fe 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -205,7 +205,7 @@ class MainWindow(QtGui.QMainWindow): else: settings = QtCore.QSettings() saved_user = settings.value(self.USER_KEY, None) - auto_login = settings.value(self.AUTOLOGIN_KEY, None) + auto_login = settings.value(self.AUTOLOGIN_KEY, "false") != "false" if saved_user is not None: self.ui.lnUser.setText(saved_user) @@ -217,8 +217,8 @@ class MainWindow(QtGui.QMainWindow): self.ui.lnPassword.setText(saved_password.decode("utf8")) # Only automatically login if there is a saved user - if auto_login is not None: - self.ui.chkAutoLogin.setChecked(True) + self.ui.chkAutoLogin.setChecked(auto_login) + if auto_login: self._login() def _show_systray(self): -- cgit v1.2.3 From 1580197229ce01ebdc6a655d2f5b67ca4b32da88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 16:07:13 -0300 Subject: Auto login after the first run wizard if registered a new user Also add pyxdg as a dep --- src/leap/gui/mainwindow.py | 8 +++++++- src/leap/gui/wizard.py | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 4e6bb5fe..78ff9101 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -198,9 +198,14 @@ class MainWindow(QtGui.QMainWindow): self.show() if self._wizard: possible_username = self._wizard.get_username() + possible_password = self._wizard.get_password() if possible_username is not None: self.ui.lnUser.setText(possible_username) self._focus_password() + if possible_password is not None: + self.ui.lnPassword.setText(possible_password) + self.ui.chkRemember.setChecked(True) + self._login() self._wizard = None else: settings = QtCore.QSettings() @@ -214,7 +219,8 @@ class MainWindow(QtGui.QMainWindow): saved_password = keyring.get_password(self.KEYRING_KEY, saved_user .encode("utf8")) - self.ui.lnPassword.setText(saved_password.decode("utf8")) + if saved_password is not None: + self.ui.lnPassword.setText(saved_password.decode("utf8")) # Only automatically login if there is a saved user self.ui.chkAutoLogin.setChecked(auto_login) diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 72e791b3..f8a1f833 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -111,12 +111,16 @@ class Wizard(QtGui.QWizard): self._register) self._username = None + self._password = None self._checker_thread = checker def get_username(self): return self._username + def get_password(self): + return self._password + def _enable_check(self, text): self.ui.btnCheck.setEnabled(len(self.ui.lnProvider.text()) != 0) @@ -186,6 +190,7 @@ class Wizard(QtGui.QWizard): username.encode("utf8"), password.encode("utf8"))]) self._username = username + self._password = password self._set_register_status(self.tr("Starting registration...")) else: self.ui.btnRegister.setEnabled(True) @@ -199,6 +204,7 @@ class Wizard(QtGui.QWizard): self.button(QtGui.QWizard.BackButton).setEnabled(False) else: self._username = None + self._password = None error_msg = self.tr("Unknown error") try: error_msg = req.json().get("errors").get("login")[0] -- cgit v1.2.3 From 060984df444dbf407e9078de638ef7486ef7b0a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 16:42:23 -0300 Subject: Notify if openvpn is not found --- src/leap/gui/mainwindow.py | 24 ++++++++++++++---------- src/leap/services/eip/vpnlaunchers.py | 9 ++++++++- 2 files changed, 22 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 78ff9101..8464d313 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -29,6 +29,7 @@ from ui_mainwindow import Ui_MainWindow from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth from leap.services.eip.vpn import VPN +from leap.services.eip.vpnlaunchers import VPNLauncherException from leap.services.eip.providerbootstrapper import ProviderBootstrapper from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig @@ -546,17 +547,20 @@ class MainWindow(QtGui.QMainWindow): self._download_eip_config() def _start_eip(self): - self._vpn.start(eipconfig=self._eip_config, - providerconfig=self._provider_config, - socket_host="localhost", - socket_port=str(random.randint(1000, 9999))) - self._vpn_systray.setVisible(True) + try: + self._vpn.start(eipconfig=self._eip_config, + providerconfig=self._provider_config, + socket_host="localhost", + socket_port=str(random.randint(1000, 9999))) + self._vpn_systray.setVisible(True) + self.ui.btnEipStartStop.setText(self.tr("Stop EIP")) + self.ui.btnEipStartStop.clicked.disconnect( + self._start_eip) + self.ui.btnEipStartStop.clicked.connect( + self._stop_eip) + except VPNLauncherException as e: + self._set_eip_status("%s" % (e,)) self.ui.btnEipStartStop.setEnabled(True) - self.ui.btnEipStartStop.setText(self.tr("Stop EIP")) - self.ui.btnEipStartStop.clicked.disconnect( - self._start_eip) - self.ui.btnEipStartStop.clicked.connect( - self._stop_eip) def _stop_eip(self): self._vpn.set_should_quit() diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index cf817321..9f2967aa 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -34,6 +34,10 @@ from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) +class VPNLauncherException(Exception): + pass + + class VPNLauncher: """ Abstract launcher class @@ -153,6 +157,8 @@ class LinuxVPNLauncher(VPNLauncher): """ Returns the platform dependant vpn launching command + Might raise VPNException. + @param eipconfig: eip configuration object @type eipconfig: EIPConfig @param providerconfig: provider specific configuration @@ -174,7 +180,8 @@ class LinuxVPNLauncher(VPNLauncher): leap_assert(socket_port, "We need a socket port!") openvpn_possibilities = which(self.OPENVPN_BIN) - leap_assert(len(openvpn_possibilities) > 0, "We couldn't find openvpn") + if len(openvpn_possibilities) == 0: + raise VPNLauncherException("We couldn't find openvpn") openvpn = openvpn_possibilities[0] args = [] -- cgit v1.2.3 From 815315bdfb26aaee16a8482f1708bf3868019fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 17:03:00 -0300 Subject: Add user regex validator --- src/leap/gui/wizard.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index f8a1f833..ef4eafdb 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -49,6 +49,8 @@ class Wizard(QtGui.QWizard): WEAK_PASSWORDS = ("123456", "qweasd", "qwerty", "password") + BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" + def __init__(self, checker): QtGui.QWizard.__init__(self) @@ -110,6 +112,10 @@ class Wizard(QtGui.QWizard): self.ui.btnRegister.clicked.connect( self._register) + usernameRe = QtCore.QRegExp(self.BARE_USERNAME_REGEX) + self.ui.lblUser.setValidator( + QtGui.QRegExpValidator(usernameRe, self)) + self._username = None self._password = None -- cgit v1.2.3 From 1371b450a3e4cd8bc1d8b50133eb4b7a8f1d9aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 17:23:08 -0300 Subject: First run checks for a complete valid run of the wizard Not just the files on disk --- src/leap/gui/mainwindow.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 8464d313..cf795a8e 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -54,6 +54,7 @@ class MainWindow(QtGui.QMainWindow): WINDOWSTATE_KEY = "WindowState" USER_KEY = "User" AUTOLOGIN_KEY = "AutoLogin" + PROPER_PROVIDER = "ProperProvider" # Keyring KEYRING_KEY = "leap_client" @@ -183,6 +184,8 @@ class MainWindow(QtGui.QMainWindow): def _rejected_wizard(self): if self._wizard_firstrun: + settings = QtCore.QSettings() + settings.setValue(self.PROPER_PROVIDER, False) self.quit() else: self._finish_init() @@ -194,9 +197,11 @@ class MainWindow(QtGui.QMainWindow): self._wizard = None def _finish_init(self): + settings = QtCore.QSettings() self.ui.cmbProviders.addItems(self._configured_providers()) self._show_systray() self.show() + if self._wizard: possible_username = self._wizard.get_username() possible_password = self._wizard.get_password() @@ -208,8 +213,8 @@ class MainWindow(QtGui.QMainWindow): self.ui.chkRemember.setChecked(True) self._login() self._wizard = None + settings.setValue(self.PROPER_PROVIDER, True) else: - settings = QtCore.QSettings() saved_user = settings.value(self.USER_KEY, None) auto_login = settings.value(self.AUTOLOGIN_KEY, "false") != "false" @@ -305,7 +310,7 @@ class MainWindow(QtGui.QMainWindow): def quit(self): self._really_quit = True if self._wizard: - self._wizard.accept() + self._wizard.close() self.close() def changeEvent(self, e): @@ -359,7 +364,11 @@ class MainWindow(QtGui.QMainWindow): @rtype: bool """ - return len(self._configured_providers()) == 0 + settings = QtCore.QSettings() + has_provider_on_disk = len(self._configured_providers()) != 0 + is_proper_provider = settings.value(self.PROPER_PROVIDER, + "false") != "false" + return not (has_provider_on_disk and is_proper_provider) def _focus_password(self): """ -- cgit v1.2.3 From b7325ef4527b90ed007a3f3ba7c61ff680076957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 17:36:28 -0300 Subject: Provide more meaningful messages in the provider select page --- src/leap/gui/ui/wizard.ui | 47 +++++++++++++++++++++++++++-------------------- src/leap/gui/wizard.py | 15 +++++++++++++++ 2 files changed, 42 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui index a7198c5f..160d628d 100644 --- a/src/leap/gui/ui/wizard.ui +++ b/src/leap/gui/ui/wizard.ui @@ -122,6 +122,29 @@ + + + + + + + Check + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -132,10 +155,7 @@ - - - - + Checking provider @@ -226,26 +246,13 @@ - - + + - Check + - - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index ef4eafdb..82deab0d 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -283,6 +283,11 @@ class Wizard(QtGui.QWizard): Sets the status for the name resolution check """ self._complete_task(data, self.ui.lblNameResolution) + status = "" + if not data[self._provider_bootstrapper.PASSED_KEY]: + status = self.tr("Non-existent " + "provider") + self.ui.lblProviderSelectStatus.setText(status) def _https_connection(self, data): """ @@ -292,6 +297,11 @@ class Wizard(QtGui.QWizard): Sets the status for the https connection check """ self._complete_task(data, self.ui.lblHTTPS) + status = "" + if not data[self._provider_bootstrapper.PASSED_KEY]: + status = self.tr("Provider does not " + "support HTTPS") + self.ui.lblProviderSelectStatus.setText(status) def _download_provider_info(self, data): """ @@ -316,6 +326,11 @@ class Wizard(QtGui.QWizard): } self._complete_task(new_data, self.ui.lblProviderInfo) + status = "" + if not data[self._provider_bootstrapper.PASSED_KEY]: + status = self.tr("Not a valid provider" + "") + self.ui.lblProviderSelectStatus.setText(status) self.ui.btnCheck.setEnabled(True) def _download_ca_cert(self, data): -- cgit v1.2.3 From 77c7e96e2aad4db223e4c7af53ec300d5472829b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 18:13:55 -0300 Subject: Instantiate provider name in the wizard once its set --- src/leap/gui/wizard.py | 37 ++++++++++++++++++++++++++++++++++--- src/leap/gui/wizardpage.py | 7 ++++--- 2 files changed, 38 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 82deab0d..70028581 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -129,6 +129,7 @@ class Wizard(QtGui.QWizard): def _enable_check(self, text): self.ui.btnCheck.setEnabled(len(self.ui.lnProvider.text()) != 0) + self._reset_provider_check() def _focus_password(self): """ @@ -228,6 +229,14 @@ class Wizard(QtGui.QWizard): """ self.ui.lblRegisterStatus.setText(status) + def _reset_provider_check(self): + self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) + self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON) + self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) + self._domain = None + self.button(QtGui.QWizard.NextButton).setEnabled(False) + self.page(self.SELECT_PROVIDER_PAGE).set_completed(False) + def _check_provider(self): """ SLOT @@ -242,6 +251,8 @@ class Wizard(QtGui.QWizard): self.ui.grpCheckProvider.setVisible(True) self.ui.btnCheck.setEnabled(False) + self.ui.lnProvider.setEnabled(False) + self.button(QtGui.QWizard.BackButton).clearFocus() self._domain = self.ui.lnProvider.text() self._provider_bootstrapper.run_provider_select_checks( @@ -284,10 +295,13 @@ class Wizard(QtGui.QWizard): """ self._complete_task(data, self.ui.lblNameResolution) status = "" - if not data[self._provider_bootstrapper.PASSED_KEY]: + passed = data[self._provider_bootstrapper.PASSED_KEY] + if not passed: status = self.tr("Non-existent " "provider") self.ui.lblProviderSelectStatus.setText(status) + self.ui.btnCheck.setEnabled(not passed) + self.ui.lnProvider.setEnabled(not passed) def _https_connection(self, data): """ @@ -298,10 +312,13 @@ class Wizard(QtGui.QWizard): """ self._complete_task(data, self.ui.lblHTTPS) status = "" - if not data[self._provider_bootstrapper.PASSED_KEY]: + passed = data[self._provider_bootstrapper.PASSED_KEY] + if not passed: status = self.tr("Provider does not " "support HTTPS") self.ui.lblProviderSelectStatus.setText(status) + self.ui.btnCheck.setEnabled(not passed) + self.ui.lnProvider.setEnabled(not passed) def _download_provider_info(self, data): """ @@ -332,6 +349,7 @@ class Wizard(QtGui.QWizard): "") self.ui.lblProviderSelectStatus.setText(status) self.ui.btnCheck.setEnabled(True) + self.ui.lnProvider.setEnabled(True) def _download_ca_cert(self, data): """ @@ -400,6 +418,10 @@ class Wizard(QtGui.QWizard): self._enable_check("") if pageId == self.SETUP_PROVIDER_PAGE: + self.page(pageId).setSubTitle(self.tr("Gathering configuration " + "options for %s") % + (self._provider_config + .get_name(),)) self._provider_bootstrapper.\ run_provider_setup_checks(self._checker_thread, self._provider_config) @@ -409,7 +431,10 @@ class Wizard(QtGui.QWizard): self._provider_config) if pageId == self.PRESENT_PROVIDER_PAGE: - # TODO: get the right lang for these + self.page(pageId).setSubTitle(self.tr("Services offered by %s") % + (self._provider_config + .get_name(),)) + lang = QtCore.QLocale.system().name() self.ui.lblProviderName.setText( "%s" % @@ -421,6 +446,12 @@ class Wizard(QtGui.QWizard): self.ui.lblProviderPolicy.setText(self._provider_config .get_enrollment_policy()) + if pageId == self.REGISTER_USER_PAGE: + self.page(pageId).setSubTitle(self.tr("Register a new user with " + "%s") % + (self._provider_config + .get_name(),)) + def nextId(self): """ Sets the next page id for the wizard based on wether the user diff --git a/src/leap/gui/wizardpage.py b/src/leap/gui/wizardpage.py index 2138ac7b..b2a00028 100644 --- a/src/leap/gui/wizardpage.py +++ b/src/leap/gui/wizardpage.py @@ -27,9 +27,10 @@ class WizardPage(QtGui.QWizardPage): QtGui.QWizardPage.__init__(self) self._completed = False - def set_completed(self): - self._completed = True - self.completeChanged.emit() + def set_completed(self, val=True): + self._completed = val + if val: + self.completeChanged.emit() def isComplete(self): return self._completed -- cgit v1.2.3 From 453a4abc87f957a96fcf68fa0ad625d27c5e73e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 18:22:19 -0300 Subject: Prepend username if the error message doesn't start with uppercase --- src/leap/gui/wizard.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 70028581..86e0dcfd 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -210,14 +210,17 @@ class Wizard(QtGui.QWizard): self.page(self.REGISTER_USER_PAGE).set_completed() self.button(QtGui.QWizard.BackButton).setEnabled(False) else: + old_username = self._username self._username = None self._password = None error_msg = self.tr("Unknown error") try: error_msg = req.json().get("errors").get("login")[0] + if not error_msg.istitle(): + error_msg = "%s %s" % (old_username, error_msg) + self._set_register_status(error_msg) except: logger.error("Unknown error: %r" % (req.content,)) - self._set_register_status(error_msg) self.ui.btnRegister.setEnabled(True) def _set_register_status(self, status): -- cgit v1.2.3 From 0a295be5fb396e67d7ce6800a647adbc69acee8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 18:29:49 -0300 Subject: Set register page as commit page Also, document reset_provider_checks method, and add a reset for provider setup --- src/leap/gui/wizard.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 86e0dcfd..90ff7f1b 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -116,6 +116,8 @@ class Wizard(QtGui.QWizard): self.ui.lblUser.setValidator( QtGui.QRegExpValidator(usernameRe, self)) + self.page(self.REGISTER_USER_PAGE).setCommitPage(True) + self._username = None self._password = None @@ -233,6 +235,10 @@ class Wizard(QtGui.QWizard): self.ui.lblRegisterStatus.setText(status) def _reset_provider_check(self): + """ + Resets the UI for checking a provider. Also resets the domain + in this object. + """ self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON) self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) @@ -240,6 +246,14 @@ class Wizard(QtGui.QWizard): self.button(QtGui.QWizard.NextButton).setEnabled(False) self.page(self.SELECT_PROVIDER_PAGE).set_completed(False) + def _reset_provider_setup(self): + """ + Resets the UI for setting up a provider. + """ + self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON) + self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON) + self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON) + def _check_provider(self): """ SLOT @@ -421,6 +435,7 @@ class Wizard(QtGui.QWizard): self._enable_check("") if pageId == self.SETUP_PROVIDER_PAGE: + self._reset_provider_setup() self.page(pageId).setSubTitle(self.tr("Gathering configuration " "options for %s") % (self._provider_config -- cgit v1.2.3 From 461721850ee668d1a5e612de6af4cf1d2eeeca74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 18:43:02 -0300 Subject: Disable remember checkbox if no keyring is available Also set properly the enabledness of chkAutoLogin --- src/leap/gui/mainwindow.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index cf795a8e..7b03eabb 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -139,6 +139,10 @@ class MainWindow(QtGui.QMainWindow): QtCore.SIGNAL("aboutToQuit()"), self._checker_thread.set_should_quit) + self.ui.chkRemember.stateChanged.connect( + self._remember_state_changed) + self.ui.chkRemember.setEnabled(keyring.get_keyring() is not None) + self.ui.action_sign_out.setEnabled(False) self.ui.action_sign_out.triggered.connect(self._logout) self.ui.action_about_leap.triggered.connect(self._about) @@ -196,6 +200,9 @@ class MainWindow(QtGui.QMainWindow): self._wizard.exec_() self._wizard = None + def _remember_state_changed(self, state): + self.ui.chkAutoLogin.setEnabled(state == QtCore.Qt.Checked) + def _finish_init(self): settings = QtCore.QSettings() self.ui.cmbProviders.addItems(self._configured_providers()) @@ -221,7 +228,8 @@ class MainWindow(QtGui.QMainWindow): if saved_user is not None: self.ui.lnUser.setText(saved_user) self.ui.chkRemember.setChecked(True) - self.ui.chkAutoLogin.setEnabled(True) + self.ui.chkAutoLogin.setEnabled(self.ui.chkRemember + .isEnabled()) saved_password = keyring.get_password(self.KEYRING_KEY, saved_user .encode("utf8")) @@ -405,7 +413,8 @@ class MainWindow(QtGui.QMainWindow): self.ui.lnPassword.setEnabled(enabled) self.ui.btnLogin.setEnabled(enabled) self.ui.chkRemember.setEnabled(enabled) - self.ui.chkAutoLogin.setEnabled(enabled) + if not enabled: + self.ui.chkAutoLogin.setEnabled(False) self.ui.cmbProviders.setEnabled(enabled) def _download_provider_config(self): -- cgit v1.2.3 From d28912e0a364ba5ed4201e87614ec94b29bd68bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 18:48:08 -0300 Subject: Explicitly disable watermark pixmap in wizard --- src/leap/gui/wizard.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 90ff7f1b..3a604157 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -60,6 +60,8 @@ class Wizard(QtGui.QWizard): self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/images/leap-color-small.png")) + self.setPixmap(QtGui.QWizard.WatermarkPixmap, None) + self.QUESTION_ICON = QtGui.QPixmap(":/images/Emblem-question.png") self.ERROR_ICON = QtGui.QPixmap(":/images/Dialog-error.png") self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png") -- cgit v1.2.3 From c0e411564a2e7c3af75e2f7c894fa46c3ca8da00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 18:51:50 -0300 Subject: Display error messages in red and bold --- src/leap/gui/mainwindow.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 7b03eabb..08b8f08b 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -384,13 +384,15 @@ class MainWindow(QtGui.QMainWindow): """ self.ui.lnPassword.setFocus() - def _set_status(self, status): + def _set_status(self, status, error=True): """ Sets the status label at the login stage to status @param status: status message @type status: str """ + if error: + status = "%s" % (status,) self.ui.lblStatus.setText(status) def _set_eip_status(self, status): @@ -492,7 +494,7 @@ class MainWindow(QtGui.QMainWindow): self._set_status(self.tr("Please provide a valid Password")) return - self._set_status(self.tr("Logging in...")) + self._set_status(self.tr("Logging in..."), error=False) self._login_set_enabled(False) settings = QtCore.QSettings() @@ -545,7 +547,7 @@ class MainWindow(QtGui.QMainWindow): Once the user is properly authenticated, try starting the EIP service """ - self._set_status(message) + self._set_status(message, error=not ok) if ok: self.ui.action_sign_out.setEnabled(True) # We leave a bit of room for the user to see the @@ -708,7 +710,6 @@ class MainWindow(QtGui.QMainWindow): Switches the stackedWidget back to the login stage after logging out """ - self._set_status(message) self._vpn_systray.setIcon(self.LOGGED_OUT_ICON) self.ui.action_sign_out.setEnabled(False) self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) -- cgit v1.2.3 From 7752eb5fc11d6f5973c4aa7d575cabd0dbc979b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 11 Mar 2013 18:54:47 -0300 Subject: Don't display a ? icon when we are checking things in the wizard --- src/leap/gui/ui/wizard.ui | 48 +++++++++++++++++++++++++++++++++++++++++++++++ src/leap/gui/wizard.py | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui index 160d628d..d44e8224 100644 --- a/src/leap/gui/ui/wizard.ui +++ b/src/leap/gui/ui/wizard.ui @@ -183,6 +183,12 @@ 0 + + + 24 + 24 + + @@ -199,6 +205,12 @@ 0 + + + 24 + 24 + + @@ -215,6 +227,12 @@ 0 + + + 24 + 24 + + @@ -383,6 +401,12 @@ 0 + + + 24 + 24 + + @@ -399,6 +423,12 @@ 0 + + + 24 + 24 + + @@ -436,6 +466,12 @@ 0 + + + 24 + 24 + + @@ -638,6 +674,12 @@ 0 + + + 24 + 24 + + @@ -654,6 +696,12 @@ 0 + + + 24 + 24 + + diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 3a604157..1aa0738b 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -62,7 +62,7 @@ class Wizard(QtGui.QWizard): self.setPixmap(QtGui.QWizard.WatermarkPixmap, None) - self.QUESTION_ICON = QtGui.QPixmap(":/images/Emblem-question.png") + self.QUESTION_ICON = None self.ERROR_ICON = QtGui.QPixmap(":/images/Dialog-error.png") self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png") -- cgit v1.2.3 From 908a726f0f10722ff8a875f24325ea1be6f293a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 07:49:57 -0300 Subject: Fix passwords are equal check for the register page --- src/leap/gui/wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 1aa0738b..ff257601 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -163,7 +163,7 @@ class Wizard(QtGui.QWizard): """ message = None - if message is not None and password != password2: + if message is None and password != password2: message = self.tr("Passwords don't match") if message is None and len(password) < 6: -- cgit v1.2.3 From cf83b3ffe300058a4ed30d255481f0c781b0b942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 09:02:08 -0300 Subject: Extend path to sbin by default --- src/leap/services/eip/vpnlaunchers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 9f2967aa..6f277481 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -76,7 +76,7 @@ def get_platform_launcher(): # Twisted implementation of which -def which(name, flags=os.X_OK): +def which(name, flags=os.X_OK, path_extension="/usr/sbin:/sbin"): """ Search PATH for executable files with the given name. @@ -99,14 +99,14 @@ def which(name, flags=os.X_OK): order in which they were found. """ - # TODO: make sure sbin is in path - result = [] exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep)) path = os.environ.get('PATH', None) + path += ":" + path_extension if path is None: return [] - for p in os.environ.get('PATH', '').split(os.pathsep): + parts = path.split(os.pathsep) + for p in parts: p = os.path.join(p, name) if os.access(p, flags): result.append(p) -- cgit v1.2.3 From 2da60cd0f78378fdcb8f6364a798720281b34b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 09:56:05 -0300 Subject: Check and try to fix certificate permissions --- src/leap/gui/mainwindow.py | 31 +++++++++++++++++---------- src/leap/services/eip/eipbootstrapper.py | 12 +++++++++-- src/leap/services/eip/providerbootstrapper.py | 14 ++++++++++-- src/leap/util/files.py | 27 +++++++++++++++++++++++ 4 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 src/leap/util/files.py (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 08b8f08b..aa9b4ffd 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -395,13 +395,15 @@ class MainWindow(QtGui.QMainWindow): status = "%s" % (status,) self.ui.lblStatus.setText(status) - def _set_eip_status(self, status): + def _set_eip_status(self, status, error=False): """ Sets the status label at the VPN stage to status @param status: status message @type status: str """ + if error: + status = "%s" % (status,) self.ui.lblEIPStatus.setText(status) def _login_set_enabled(self, enabled=False): @@ -579,7 +581,7 @@ class MainWindow(QtGui.QMainWindow): self.ui.btnEipStartStop.clicked.connect( self._stop_eip) except VPNLauncherException as e: - self._set_eip_status("%s" % (e,)) + self._set_eip_status("%s" % (e,), error=True) self.ui.btnEipStartStop.setEnabled(True) def _stop_eip(self): @@ -609,7 +611,8 @@ class MainWindow(QtGui.QMainWindow): download_if_needed=True) else: self._set_eip_status(self.tr("%s does not support EIP") % - (self._provider_config.get_domain(),)) + (self._provider_config.get_domain(),), + error=True) def _set_eip_status_icon(self, status): """ @@ -681,15 +684,21 @@ class MainWindow(QtGui.QMainWindow): leap_assert(self._eip_config, "We need an eip config!") leap_assert(self._provider_config, "We need a provider config!") - if self._eip_config.loaded() or \ - self._eip_config.load(os.path.join("leap", - "providers", - self._provider_config - .get_domain(), - "eip-service.json")): + if data[self._eip_bootstrapper.PASSED_KEY] and \ + (self._eip_config.loaded() or + self._eip_config.load(os.path.join("leap", + "providers", + self._provider_config + .get_domain(), + "eip-service.json"))): self._start_eip() - # TODO: display a message if the EIP configuration cannot be - # loaded + else: + if data[self._eip_bootstrapper.PASSED_KEY]: + self._set_eip_status(self.tr("Could not load EIP " + "Configuration"), error=True) + else: + self._set_eip_status(data[self._eip_bootstrapper.ERROR_KEY], + error=True) def _logout(self): """ diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index bd6ab715..79ff28d6 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -30,6 +30,7 @@ from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread +from leap.util.files import check_and_fix_urw_only logger = logging.getLogger(__name__) @@ -147,9 +148,14 @@ class EIPBootstrapper(QtCore.QObject): if self._download_if_needed and \ os.path.exists(client_cert_path): - download_cert[self.PASSED_KEY] = True + try: + check_and_fix_urw_only(client_cert_path) + download_cert[self.PASSED_KEY] = True + except Exception as e: + download_cert[self.PASSED_KEY] = False + download_cert[self.ERROR_KEY] = "%s" % (e,) self.download_client_certificate.emit(download_cert) - return True + return download_cert[self.PASSED_KEY] try: res = self._session.get("%s/%s/%s/" % @@ -176,6 +182,8 @@ class EIPBootstrapper(QtCore.QObject): with open(client_cert_path, "w") as f: f.write(client_cert) + check_and_fix_urw_only(client_cert_path) + download_cert[self.PASSED_KEY] = True except Exception as e: download_cert[self.ERROR_KEY] = "%s" % (e,) diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index ecdc4e07..0e9f8563 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -31,6 +31,7 @@ from PySide import QtGui, QtCore from leap.config.providerconfig import ProviderConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread +from leap.util.files import check_and_fix_urw_only logger = logging.getLogger(__name__) @@ -258,9 +259,16 @@ class ProviderBootstrapper(QtCore.QObject): } if not self._should_proceed_cert(): - download_ca_cert_data[self.PASSED_KEY] = True + try: + check_and_fix_urw_only( + self._provider_config + .get_ca_cert_path(about_to_download=True)) + download_ca_cert_data[self.PASSED_KEY] = True + except Exception as e: + download_ca_cert_data[self.PASSED_KEY] = False + download_ca_cert_data[self.ERROR_KEY] = "%s" % (e,) self.download_ca_cert.emit(download_ca_cert_data) - return True + return download_ca_cert_data[self.PASSED_KEY] try: res = self._session.get(self._provider_config.get_ca_cert_uri()) @@ -282,6 +290,8 @@ class ProviderBootstrapper(QtCore.QObject): with open(cert_path, "w") as f: f.write(res.content) + check_and_fix_urw_only(cert_path) + download_ca_cert_data[self.PASSED_KEY] = True except Exception as e: download_ca_cert_data[self.ERROR_KEY] = "%s" % (e,) diff --git a/src/leap/util/files.py b/src/leap/util/files.py new file mode 100644 index 00000000..f7fda39e --- /dev/null +++ b/src/leap/util/files.py @@ -0,0 +1,27 @@ +import os +import stat +import logging + +logger = logging.getLogger(__name__) + + +def check_and_fix_urw_only(cert): + """ + Test for 600 mode and try to set it if anything different found + + Might raise OSError + + @param cert: Certificate path + @type cert: str + """ + mode = stat.S_IMODE(os.stat(cert).st_mode) + + if mode != int('600', 8): + try: + logger.warning('Bad permission on %s attempting to set 600' % + (cert,)) + os.chmod(cert, stat.S_IRUSR | stat.S_IWUSR) + except OSError: + logger.error('Error while trying to chmod 600 %s' % + cert) + raise -- cgit v1.2.3 From 295b7ed9982094b6a4aa1951561e34bfb16e16c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 10:08:58 -0300 Subject: Gray out the systray icon after logging out --- src/leap/gui/mainwindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index aa9b4ffd..7209861f 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -719,7 +719,7 @@ class MainWindow(QtGui.QMainWindow): Switches the stackedWidget back to the login stage after logging out """ - self._vpn_systray.setIcon(self.LOGGED_OUT_ICON) + self._systray.setIcon(self.LOGGED_OUT_ICON) self.ui.action_sign_out.setEnabled(False) self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) self.ui.lnPassword.setText("") -- cgit v1.2.3 From f3d8bf14688227cfae1efcbec606639991885c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 10:34:40 -0300 Subject: Set vpn systray tooltip to the latest status --- src/leap/gui/mainwindow.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 7209861f..65ab6632 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -402,6 +402,7 @@ class MainWindow(QtGui.QMainWindow): @param status: status message @type status: str """ + self._vpn_systray.setToolTip(status) if error: status = "%s" % (status,) self.ui.lblEIPStatus.setText(status) -- cgit v1.2.3 From 731ecfaf60d7bd0c6a52f3fd1676cfbd71696493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 10:34:57 -0300 Subject: Fix disconnect problem with start/stop EIP button --- src/leap/gui/mainwindow.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 65ab6632..2a72d4d6 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -82,7 +82,7 @@ class MainWindow(QtGui.QMainWindow): self.ui.btnEipStartStop.setEnabled(False) self.ui.btnEipStartStop.clicked.connect( - self._stop_eip) + self._start_eip) # This is loaded only once, there's a bug when doing that more # than once @@ -577,8 +577,7 @@ class MainWindow(QtGui.QMainWindow): socket_port=str(random.randint(1000, 9999))) self._vpn_systray.setVisible(True) self.ui.btnEipStartStop.setText(self.tr("Stop EIP")) - self.ui.btnEipStartStop.clicked.disconnect( - self._start_eip) + self.ui.btnEipStartStop.disconnect(self) self.ui.btnEipStartStop.clicked.connect( self._stop_eip) except VPNLauncherException as e: @@ -591,8 +590,7 @@ class MainWindow(QtGui.QMainWindow): self._set_eip_status(self.tr("EIP has stopped")) self._set_eip_status_icon("error") self.ui.btnEipStartStop.setText(self.tr("Start EIP")) - self.ui.btnEipStartStop.clicked.disconnect( - self._stop_eip) + self.ui.btnEipStartStop.disconnect(self) self.ui.btnEipStartStop.clicked.connect( self._start_eip) -- cgit v1.2.3 From e18eb96f09069afe538508c30acd3d95e8359a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 11:53:11 -0300 Subject: Add version to about --- src/leap/gui/mainwindow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 2a72d4d6..882a5a99 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -36,6 +36,7 @@ from leap.services.eip.eipconfig import EIPConfig from leap.gui.wizard import Wizard from leap.util.check import leap_assert from leap.util.checkerthread import CheckerThread +from leap import __version__ as VERSION logger = logging.getLogger(__name__) @@ -306,7 +307,7 @@ class MainWindow(QtGui.QMainWindow): Display the About LEAP dialog """ QtGui.QMessageBox.about( - self, self.tr("About LEAP"), + self, self.tr("About LEAP - %s") % (VERSION,), self.tr("LEAP is a non-profit dedicated to giving " "all internet users access to secure " "communication. Our focus is on adapting " -- cgit v1.2.3 From f838e880b7d694eee09c360ab737103a68e613aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 11:55:04 -0300 Subject: Display ? icon for the test being run at the time Also, change button texts --- src/leap/gui/wizard.py | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index ff257601..5a66adbc 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -44,7 +44,7 @@ class Wizard(QtGui.QWizard): SETUP_PROVIDER_PAGE = 3 REGISTER_USER_PAGE = 4 SETUP_EIP_PAGE = 5 - FINISH_PATH = 6 + FINISH_PAGE = 6 WEAK_PASSWORDS = ("123456", "qweasd", "qwerty", "password") @@ -62,7 +62,7 @@ class Wizard(QtGui.QWizard): self.setPixmap(QtGui.QWizard.WatermarkPixmap, None) - self.QUESTION_ICON = None + self.QUESTION_ICON = QtGui.QPixmap(":/images/Emblem-question.png") self.ERROR_ICON = QtGui.QPixmap(":/images/Dialog-error.png") self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png") @@ -125,6 +125,11 @@ class Wizard(QtGui.QWizard): self._checker_thread = checker + self.page(self.REGISTER_USER_PAGE).setButtonText( + QtGui.QWizard.CommitButton, self.tr("&Next >")) + self.page(self.FINISH_PAGE).setButtonText( + QtGui.QWizard.FinishButton, self.tr("Connect")) + def get_username(self): return self._username @@ -241,9 +246,9 @@ class Wizard(QtGui.QWizard): Resets the UI for checking a provider. Also resets the domain in this object. """ - self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) - self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON) - self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) + self.ui.lblNameResolution.setPixmap(None) + self.ui.lblHTTPS.setPixmap(None) + self.ui.lblProviderInfo.setPixmap(None) self._domain = None self.button(QtGui.QWizard.NextButton).setEnabled(False) self.page(self.SELECT_PROVIDER_PAGE).set_completed(False) @@ -252,9 +257,16 @@ class Wizard(QtGui.QWizard): """ Resets the UI for setting up a provider. """ - self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON) - self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON) - self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON) + self.ui.lblDownloadCaCert.setPixmap(None) + self.ui.lblCheckCaFpr.setPixmap(None) + self.ui.lblCheckApiCert.setPixmap(None) + + def _reset_eip_check(self): + """ + Resets the UI for the EIP check + """ + self.ui.lblDownloadEIPConfig.setPixmap(None) + self.ui.lblDownloadClientCert.setPixmap(None) def _check_provider(self): """ @@ -274,6 +286,7 @@ class Wizard(QtGui.QWizard): self.button(QtGui.QWizard.BackButton).clearFocus() self._domain = self.ui.lnProvider.text() + self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) self._provider_bootstrapper.run_provider_select_checks( self._checker_thread, self._domain) @@ -321,6 +334,7 @@ class Wizard(QtGui.QWizard): self.ui.lblProviderSelectStatus.setText(status) self.ui.btnCheck.setEnabled(not passed) self.ui.lnProvider.setEnabled(not passed) + self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON) def _https_connection(self, data): """ @@ -338,6 +352,7 @@ class Wizard(QtGui.QWizard): self.ui.lblProviderSelectStatus.setText(status) self.ui.btnCheck.setEnabled(not passed) self.ui.lnProvider.setEnabled(not passed) + self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) def _download_provider_info(self, data): """ @@ -378,6 +393,7 @@ class Wizard(QtGui.QWizard): Sets the status for the download of the CA certificate check """ self._complete_task(data, self.ui.lblDownloadCaCert) + self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON) def _check_ca_fingerprint(self, data): """ @@ -387,6 +403,7 @@ class Wizard(QtGui.QWizard): Sets the status for the CA fingerprint check """ self._complete_task(data, self.ui.lblCheckCaFpr) + self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON) def _check_api_certificate(self, data): """ @@ -408,6 +425,7 @@ class Wizard(QtGui.QWizard): Sets the status for the EIP config downloading check """ self._complete_task(data, self.ui.lblDownloadEIPConfig) + self.ui.lblDownloadClientCert.setPixmap(self.QUESTION_ICON) def _download_client_certificate(self, data): """ @@ -430,10 +448,7 @@ class Wizard(QtGui.QWizard): Prepares the pages when they appear """ if pageId == self.SELECT_PROVIDER_PAGE: - self.ui.grpCheckProvider.setVisible(False) - self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) - self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON) - self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) + self._reset_provider_check() self._enable_check("") if pageId == self.SETUP_PROVIDER_PAGE: @@ -442,11 +457,14 @@ class Wizard(QtGui.QWizard): "options for %s") % (self._provider_config .get_name(),)) + self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON) self._provider_bootstrapper.\ run_provider_setup_checks(self._checker_thread, self._provider_config) if pageId == self.SETUP_EIP_PAGE: + self._reset_eip_check() + self.ui.lblDownloadEIPConfig.setPixmap(self.QUESTION_ICON) self._eip_bootstrapper.run_eip_setup_checks(self._checker_thread, self._provider_config) -- cgit v1.2.3 From 68c18135659351aac1c10aefadce431240ee6366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 12:06:56 -0300 Subject: Reset error message when the provider changes --- src/leap/gui/wizard.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 5a66adbc..08e9cc6b 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -249,6 +249,7 @@ class Wizard(QtGui.QWizard): self.ui.lblNameResolution.setPixmap(None) self.ui.lblHTTPS.setPixmap(None) self.ui.lblProviderInfo.setPixmap(None) + self.ui.lblProviderSelectStatus.setText("") self._domain = None self.button(QtGui.QWizard.NextButton).setEnabled(False) self.page(self.SELECT_PROVIDER_PAGE).set_completed(False) -- cgit v1.2.3 From 605a456ffdb196211d239b6bdb56914b260c468f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 12:11:34 -0300 Subject: Add watermark image --- src/leap/gui/wizard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 08e9cc6b..202c6dc2 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -60,7 +60,8 @@ class Wizard(QtGui.QWizard): self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/images/leap-color-small.png")) - self.setPixmap(QtGui.QWizard.WatermarkPixmap, None) + self.setPixmap(QtGui.QWizard.WatermarkPixmap, + QtGui.QPixmap(":/images/watermark.png")) self.QUESTION_ICON = QtGui.QPixmap(":/images/Emblem-question.png") self.ERROR_ICON = QtGui.QPixmap(":/images/Dialog-error.png") -- cgit v1.2.3 From 70ab05982ab778b6d2e949b186b9932b90115a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 12:54:49 -0300 Subject: Prepend https:// to the provider domain --- src/leap/gui/wizard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 202c6dc2..de988153 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -479,7 +479,8 @@ class Wizard(QtGui.QWizard): self.ui.lblProviderName.setText( "%s" % (self._provider_config.get_name(lang=lang),)) - self.ui.lblProviderURL.setText(self._provider_config.get_domain()) + self.ui.lblProviderURL.setText( + "https://%s" % (self._provider_config.get_domain(),)) self.ui.lblProviderDesc.setText( "%s" % (self._provider_config.get_description(lang=lang),)) -- cgit v1.2.3 From cabfa92593d0c21f63f218842af8ed2bfba77cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 14:08:47 -0300 Subject: Make login in parallel --- src/leap/gui/mainwindow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 882a5a99..3af87366 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -24,6 +24,7 @@ import random import keyring from PySide import QtCore, QtGui +from functools import partial from ui_mainwindow import Ui_MainWindow from leap.config.providerconfig import ProviderConfig @@ -538,7 +539,10 @@ class MainWindow(QtGui.QMainWindow): self._srp_auth.logout_finished.connect( self._done_logging_out) - self._srp_auth.authenticate(username, password) + auth_partial = partial(self._srp_auth.authenticate, + username, + password) + self._checker_thread.add_checks([auth_partial]) else: self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) self._login_set_enabled(True) @@ -760,7 +764,6 @@ class MainWindow(QtGui.QMainWindow): if __name__ == "__main__": import signal - from functools import partial def sigint_handler(*args, **kwargs): logger.debug('SIGINT catched. shutting down...') -- cgit v1.2.3 From 66464e6a2ea48d3dbafaa7c11b14478e7d34ab4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 14:11:56 -0300 Subject: Login after register Also, mark error in red and bold in the register form --- src/leap/gui/ui/wizard.ui | 84 ++++++++++++++++++++++++++--------------------- src/leap/gui/wizard.py | 44 ++++++++++++++++++++----- 2 files changed, 83 insertions(+), 45 deletions(-) (limited to 'src') diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui index d44e8224..ad4c0500 100644 --- a/src/leap/gui/ui/wizard.ui +++ b/src/leap/gui/ui/wizard.ui @@ -535,42 +535,6 @@ 4 - - - - - - - Qt::AutoText - - - Qt::AlignCenter - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - <b>User:</b> - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - @@ -600,7 +564,7 @@ - + Register @@ -620,6 +584,52 @@ + + + + + + + Qt::AutoText + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <b>User:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + false + + + Remember my username and password + + + diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index de988153..1f0bc877 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -27,6 +27,7 @@ from functools import partial from ui_wizard import Ui_Wizard from leap.config.providerconfig import ProviderConfig from leap.crypto.srpregister import SRPRegister +from leap.crypto.srpauth import SRPAuth from leap.services.eip.providerbootstrapper import ProviderBootstrapper from leap.services.eip.eipbootstrapper import EIPBootstrapper @@ -60,9 +61,6 @@ class Wizard(QtGui.QWizard): self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/images/leap-color-small.png")) - self.setPixmap(QtGui.QWizard.WatermarkPixmap, - QtGui.QPixmap(":/images/watermark.png")) - self.QUESTION_ICON = QtGui.QPixmap(":/images/Emblem-question.png") self.ERROR_ICON = QtGui.QPixmap(":/images/Dialog-error.png") self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png") @@ -182,7 +180,7 @@ class Wizard(QtGui.QWizard): message = self.tr("Password equal to username") if message is not None: - self._set_register_status(message) + self._set_register_status(message, error=True) self._focus_password() return False @@ -215,10 +213,20 @@ class Wizard(QtGui.QWizard): def _registration_finished(self, ok, req): if ok: self._set_register_status(self.tr("" - "User registration OK")) + "User registration OK. " + "Logging in...")) self.ui.lblPassword2.clearFocus() - self.page(self.REGISTER_USER_PAGE).set_completed() - self.button(QtGui.QWizard.BackButton).setEnabled(False) + + srp_auth = SRPAuth(self._provider_config) + srp_auth.authentication_finished.connect( + self._authentication_finished) + + auth_partial = partial(srp_auth.authenticate, + self._username, + self._password) + self._checker_thread.add_checks([auth_partial]) + + self.ui.chkRemember.setEnabled(True) else: old_username = self._username self._username = None @@ -233,13 +241,33 @@ class Wizard(QtGui.QWizard): logger.error("Unknown error: %r" % (req.content,)) self.ui.btnRegister.setEnabled(True) - def _set_register_status(self, status): + def _authentication_finished(self, ok, message): + """ + SLOT + TRIGGER: srp_auth.authentication_finished + + Finish the authentication process as it comes from the + register form + """ + if ok: + self._set_register_status(self.tr("" + "Login succeeded!" + "")) + self.page(self.REGISTER_USER_PAGE).set_completed() + else: + self._set_register_status(message) + self.ui.btnRegister.setEnabled(True) + self.ui.chkRemember.setEnabled(False) + + def _set_register_status(self, status, error=False): """ Sets the status label in the registration page to status @param status: status message to display, can be HTML @type status: str """ + if error: + status = "%s" % (status,) self.ui.lblRegisterStatus.setText(status) def _reset_provider_check(self): -- cgit v1.2.3 From a120904b512394346b286bb417adf34fc622e739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 14:26:38 -0300 Subject: Get eip cert with session_id when possible --- src/leap/crypto/srpauth.py | 3 +++ src/leap/services/eip/eipbootstrapper.py | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index 2877efab..c1964514 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -390,6 +390,9 @@ class SRPAuth(QtCore.QObject): self.authentication_finished.emit(False, "%s" % (e,)) return False + def get_session_id(self): + return self.__instance.get_session_id() + def logout(self): """ Logs out the current session. diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 79ff28d6..6fbb98b9 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -26,6 +26,7 @@ import errno from PySide import QtGui, QtCore +from leap.crypto.srpauth import SRPAuth from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig from leap.util.check import leap_assert, leap_assert_type @@ -158,12 +159,18 @@ class EIPBootstrapper(QtCore.QObject): return download_cert[self.PASSED_KEY] try: + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} res = self._session.get("%s/%s/%s/" % (self._provider_config.get_api_uri(), self._provider_config.get_api_version(), "cert"), verify=self._provider_config - .get_ca_cert_path()) + .get_ca_cert_path(), + cookies=cookies) res.raise_for_status() client_cert = res.content -- cgit v1.2.3 From 364c6d5e9882f47ff5938f731641bc7509e32fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 12 Mar 2013 17:12:20 -0300 Subject: Make the wizard provider setup only Also: - Add a way to enable/disable services for a provider - Run EIP only if it is enabled --- src/leap/gui/mainwindow.py | 25 +++++++-- src/leap/gui/ui/mainwindow.ui | 3 +- src/leap/gui/ui/wizard.ui | 106 ++-------------------------------- src/leap/gui/wizard.py | 128 +++++++++++++++++++----------------------- 4 files changed, 88 insertions(+), 174 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 3af87366..f8ca4c35 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -175,6 +175,8 @@ class MainWindow(QtGui.QMainWindow): self._action_visible = QtGui.QAction(self.tr("Hide"), self) self._action_visible.triggered.connect(self._toggle_visible) + self._enabled_services = [] + self._center_window() self._wizard = None self._wizard_firstrun = False @@ -214,6 +216,11 @@ class MainWindow(QtGui.QMainWindow): if self._wizard: possible_username = self._wizard.get_username() possible_password = self._wizard.get_password() + self.ui.chkRemember.setChecked(self._wizard.get_remember()) + self._enabled_services = list(self._wizard.get_services()) + settings.setValue("%s/Services" % + (self.ui.cmbProviders.currentText(),), + self._enabled_services) if possible_username is not None: self.ui.lnUser.setText(possible_username) self._focus_password() @@ -487,6 +494,11 @@ class MainWindow(QtGui.QMainWindow): password = self.ui.lnPassword.text() provider = self.ui.cmbProviders.currentText() + settings = QtCore.QSettings() + self._enabled_services = settings.value( + "%s/Services" % + (self.ui.cmbProviders.currentText(),), "").split(",") + if len(provider) == 0: self._set_status(self.tr("Please select a valid provider")) return @@ -608,15 +620,20 @@ class MainWindow(QtGui.QMainWindow): self._set_eip_status(self.tr("Checking configuration, please wait...")) - if self._provider_config.provides_eip(): + if self._provider_config.provides_eip() and \ + self._enabled_services.count("openvpn") > 0: self._eip_bootstrapper.run_eip_setup_checks( self._checker_thread, self._provider_config, download_if_needed=True) else: - self._set_eip_status(self.tr("%s does not support EIP") % - (self._provider_config.get_domain(),), - error=True) + if self._enabled_services.count("openvpn") > 0: + self._set_eip_status(self.tr("%s does not support EIP") % + (self._provider_config.get_domain(),), + error=True) + else: + self._set_eip_status(self.tr("EIP is disabled")) + self.ui.btnEipStartStop.setEnabled(False) def _set_eip_status_icon(self, status): """ diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui index d723114c..a8a8aa59 100644 --- a/src/leap/gui/ui/mainwindow.ui +++ b/src/leap/gui/ui/mainwindow.ui @@ -70,7 +70,7 @@ - 0 + 1 @@ -389,6 +389,7 @@ + diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui index ad4c0500..b8fed183 100644 --- a/src/leap/gui/ui/wizard.ui +++ b/src/leap/gui/ui/wizard.ui @@ -632,123 +632,29 @@ - + - EIP Setup + Service selection - Setting up Encrypted Internet + Please select the services you would like to have 5 - - - Qt::Vertical - - - - 20 - 40 - - - - - - + - Checking EIP + Services by PROVIDER - - - - Download EIP configuration - - - - - - - Download client certificate - - - - - - - - 0 - 0 - - - - - 24 - 24 - - - - - - - :/images/Emblem-question.png - - - - - - - - 0 - 0 - - - - - 24 - 24 - - - - - - - :/images/Emblem-question.png - - - - - - Qt::Horizontal - - - - 40 - 0 - - - + - - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 1f0bc877..83f64020 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -27,9 +27,7 @@ from functools import partial from ui_wizard import Ui_Wizard from leap.config.providerconfig import ProviderConfig from leap.crypto.srpregister import SRPRegister -from leap.crypto.srpauth import SRPAuth from leap.services.eip.providerbootstrapper import ProviderBootstrapper -from leap.services.eip.eipbootstrapper import EIPBootstrapper logger = logging.getLogger(__name__) @@ -44,7 +42,7 @@ class Wizard(QtGui.QWizard): PRESENT_PROVIDER_PAGE = 2 SETUP_PROVIDER_PAGE = 3 REGISTER_USER_PAGE = 4 - SETUP_EIP_PAGE = 5 + SERVICES_PAGE = 5 FINISH_PAGE = 6 WEAK_PASSWORDS = ("123456", "qweasd", "qwerty", @@ -65,6 +63,16 @@ class Wizard(QtGui.QWizard): self.ERROR_ICON = QtGui.QPixmap(":/images/Dialog-error.png") self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png") + # Correspondence for services and their name to display + self.SERVICE_DISPLAY = [ + self.tr("Encrypted Internet") + ] + self.SERVICE_CONFIG = [ + "openvpn" + ] + + self._selected_services = set() + self._show_register = False self.ui.grpCheckProvider.setVisible(False) @@ -86,13 +94,6 @@ class Wizard(QtGui.QWizard): self._provider_bootstrapper.check_api_certificate.connect( self._check_api_certificate) - self._eip_bootstrapper = EIPBootstrapper() - - self._eip_bootstrapper.download_config.connect( - self._download_eip_config) - self._eip_bootstrapper.download_client_certificate.connect( - self._download_client_certificate) - self._domain = None self._provider_config = ProviderConfig() @@ -135,6 +136,12 @@ class Wizard(QtGui.QWizard): def get_password(self): return self._password + def get_remember(self): + return self.ui.chkRemember.isChecked() + + def get_services(self): + return self._selected_services + def _enable_check(self, text): self.ui.btnCheck.setEnabled(len(self.ui.lnProvider.text()) != 0) self._reset_provider_check() @@ -214,19 +221,11 @@ class Wizard(QtGui.QWizard): if ok: self._set_register_status(self.tr("" "User registration OK. " - "Logging in...")) + "")) self.ui.lblPassword2.clearFocus() - - srp_auth = SRPAuth(self._provider_config) - srp_auth.authentication_finished.connect( - self._authentication_finished) - - auth_partial = partial(srp_auth.authenticate, - self._username, - self._password) - self._checker_thread.add_checks([auth_partial]) - self.ui.chkRemember.setEnabled(True) + + self.page(self.REGISTER_USER_PAGE).set_completed() else: old_username = self._username self._username = None @@ -241,24 +240,6 @@ class Wizard(QtGui.QWizard): logger.error("Unknown error: %r" % (req.content,)) self.ui.btnRegister.setEnabled(True) - def _authentication_finished(self, ok, message): - """ - SLOT - TRIGGER: srp_auth.authentication_finished - - Finish the authentication process as it comes from the - register form - """ - if ok: - self._set_register_status(self.tr("" - "Login succeeded!" - "")) - self.page(self.REGISTER_USER_PAGE).set_completed() - else: - self._set_register_status(message) - self.ui.btnRegister.setEnabled(True) - self.ui.chkRemember.setEnabled(False) - def _set_register_status(self, status, error=False): """ Sets the status label in the registration page to status @@ -291,13 +272,6 @@ class Wizard(QtGui.QWizard): self.ui.lblCheckCaFpr.setPixmap(None) self.ui.lblCheckApiCert.setPixmap(None) - def _reset_eip_check(self): - """ - Resets the UI for the EIP check - """ - self.ui.lblDownloadEIPConfig.setPixmap(None) - self.ui.lblDownloadClientCert.setPixmap(None) - def _check_provider(self): """ SLOT @@ -447,28 +421,47 @@ class Wizard(QtGui.QWizard): self._complete_task(data, self.ui.lblCheckApiCert, True, self.SETUP_PROVIDER_PAGE) - def _download_eip_config(self, data): + def _service_selection_changed(self, service, state): """ SLOT - TRIGGER: self._eip_bootstrapper.download_config + TRIGGER: service_checkbox.stateChanged + Adds the service to the state if the state is checked, removes + it otherwise + + @param service: service to handle + @type service: str + @param state: state of the checkbox + @type state: int + """ + if state == QtCore.Qt.Checked: + self._selected_services = \ + self._selected_services.union({service}) + else: + self._selected_services = \ + self._selected_services.difference({service}) - Sets the status for the EIP config downloading check + def _populate_services(self): """ - self._complete_task(data, self.ui.lblDownloadEIPConfig) - self.ui.lblDownloadClientCert.setPixmap(self.QUESTION_ICON) - - def _download_client_certificate(self, data): + Loads the services that the provider provides into the UI for + the user to enable or disable """ - SLOT - TRIGGER: self._provider_bootstrapper.download_client_certificate + self.ui.grpServices.setTitle( + self.tr("Services by %s") % + (self._provider_config.get_name(),)) - Sets the status for the download client certificate check and - completes the page if passed. Also stops the eip bootstrapper - thread since it's not needed from this point on unless the - check chain is restarted - """ - self._complete_task(data, self.ui.lblDownloadClientCert, - True, self.SETUP_EIP_PAGE) + for service in self._provider_config.get_services(): + try: + checkbox = QtGui.QCheckBox(self) + service_index = self.SERVICE_CONFIG.index(service) + checkbox.setText(self.SERVICE_DISPLAY[service_index]) + self.ui.serviceListLayout.addWidget(checkbox) + checkbox.stateChanged.connect( + partial(self._service_selection_changed, service)) + checkbox.setChecked(True) + except ValueError: + logger.error( + self.tr("Something went wrong while trying to " + "load service %s" % (service,))) def _current_id_changed(self, pageId): """ @@ -492,12 +485,6 @@ class Wizard(QtGui.QWizard): run_provider_setup_checks(self._checker_thread, self._provider_config) - if pageId == self.SETUP_EIP_PAGE: - self._reset_eip_check() - self.ui.lblDownloadEIPConfig.setPixmap(self.QUESTION_ICON) - self._eip_bootstrapper.run_eip_setup_checks(self._checker_thread, - self._provider_config) - if pageId == self.PRESENT_PROVIDER_PAGE: self.page(pageId).setSubTitle(self.tr("Services offered by %s") % (self._provider_config @@ -521,6 +508,9 @@ class Wizard(QtGui.QWizard): (self._provider_config .get_name(),)) + if pageId == self.SERVICES_PAGE: + self._populate_services() + def nextId(self): """ Sets the next page id for the wizard based on wether the user @@ -533,6 +523,6 @@ class Wizard(QtGui.QWizard): if self._show_register: return self.REGISTER_USER_PAGE else: - return self.SETUP_EIP_PAGE + return self.SERVICES_PAGE return QtGui.QWizard.nextId(self) -- cgit v1.2.3 From 70c402fe170ca4e01159b03739b7cacda7b0dfd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 09:43:13 -0300 Subject: Add mtime check for existing json definitions before download Also, wait for threads to finish when quitting --- src/leap/gui/mainwindow.py | 8 ++++ src/leap/services/eip/eipbootstrapper.py | 44 +++++++++++--------- src/leap/services/eip/providerbootstrapper.py | 59 ++++++++++++--------------- src/leap/util/checkerthread.py | 1 - src/leap/util/files.py | 18 ++++++++ 5 files changed, 76 insertions(+), 54 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index f8ca4c35..6f7f646e 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -136,10 +136,18 @@ class MainWindow(QtGui.QMainWindow): QtCore.QCoreApplication.instance(), QtCore.SIGNAL("aboutToQuit()"), self._vpn.set_should_quit) + QtCore.QCoreApplication.instance().connect( + QtCore.QCoreApplication.instance(), + QtCore.SIGNAL("aboutToQuit()"), + self._vpn.wait) QtCore.QCoreApplication.instance().connect( QtCore.QCoreApplication.instance(), QtCore.SIGNAL("aboutToQuit()"), self._checker_thread.set_should_quit) + QtCore.QCoreApplication.instance().connect( + QtCore.QCoreApplication.instance(), + QtCore.SIGNAL("aboutToQuit()"), + self._checker_thread.wait) self.ui.chkRemember.stateChanged.connect( self._remember_state_changed) diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 6fbb98b9..fdf54bbb 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -31,7 +31,7 @@ from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread -from leap.util.files import check_and_fix_urw_only +from leap.util.files import check_and_fix_urw_only, get_mtime logger = logging.getLogger(__name__) @@ -88,33 +88,39 @@ class EIPBootstrapper(QtCore.QObject): self._eip_config = EIPConfig() - if self._download_if_needed and \ - os.path.exists(os.path.join(self._eip_config.get_path_prefix(), - "leap", - "providers", - self._provider_config.get_domain(), - "eip-service.json")): - download_config_data[self.PASSED_KEY] = True - self.download_config.emit(download_config_data) - return True - try: + headers = {} + mtime = get_mtime(os.path.join(self._eip_config + .get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "eip-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + res = self._session.get("%s/%s/%s/%s" % (self._provider_config.get_api_uri(), self._provider_config.get_api_version(), "config", "eip-service.json"), verify=self._provider_config - .get_ca_cert_path()) + .get_ca_cert_path(), + headers=headers) res.raise_for_status() - eip_definition = res.content - - self._eip_config.load(data=eip_definition) - self._eip_config.save(["leap", - "providers", - self._provider_config.get_domain(), - "eip-service.json"]) + # Not modified + if res.status_code == 304: + logger.debug("EIP definition has not been modified") + else: + eip_definition = res.content + + self._eip_config.load(data=eip_definition) + self._eip_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "eip-service.json"]) download_config_data[self.PASSED_KEY] = True except Exception as e: diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 0e9f8563..f1a917f0 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -31,7 +31,7 @@ from PySide import QtGui, QtCore from leap.config.providerconfig import ProviderConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread -from leap.util.files import check_and_fix_urw_only +from leap.util.files import check_and_fix_urw_only, get_mtime logger = logging.getLogger(__name__) @@ -72,25 +72,6 @@ class ProviderBootstrapper(QtCore.QObject): self._provider_config = None self._download_if_needed = False - def _should_proceed_provider(self): - """ - Returns False if provider.json already exists for the given - domain. True otherwise - - @rtype: bool - """ - if not self._download_if_needed: - return True - - # We don't really need a provider config at this stage, just - # the path prefix - return not os.path.exists(os.path.join(ProviderConfig() - .get_path_prefix(), - "leap", - "providers", - self._domain, - "provider.json")) - def _check_name_resolution(self): """ Checks that the name resolution for the provider name works @@ -171,24 +152,34 @@ class ProviderBootstrapper(QtCore.QObject): self.ERROR_KEY: "" } - if not self._should_proceed_provider(): - download_data[self.PASSED_KEY] = True - self.download_provider_info.emit(download_data) - return True - try: + headers = {} + mtime = get_mtime(os.path.join(ProviderConfig() + .get_path_prefix(), + "leap", + "providers", + self._domain, + "provider.json")) + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + res = self._session.get("https://%s/%s" % (self._domain, - "provider.json")) + "provider.json"), + headers=headers) res.raise_for_status() - provider_definition = res.content - - provider_config = ProviderConfig() - provider_config.load(data=provider_definition) - provider_config.save(["leap", - "providers", - self._domain, - "provider.json"]) + # Not modified + if res.status_code == 304: + logger.debug("Provider definition has not been modified") + else: + provider_definition = res.content + + provider_config = ProviderConfig() + provider_config.load(data=provider_definition) + provider_config.save(["leap", + "providers", + self._domain, + "provider.json"]) download_data[self.PASSED_KEY] = True except Exception as e: diff --git a/src/leap/util/checkerthread.py b/src/leap/util/checkerthread.py index 681c33e1..3430a450 100644 --- a/src/leap/util/checkerthread.py +++ b/src/leap/util/checkerthread.py @@ -64,7 +64,6 @@ class CheckerThread(QtCore.QThread): """ QtCore.QMutexLocker(self._should_quit_lock) self._should_quit = True - self.wait() def start(self): """ diff --git a/src/leap/util/files.py b/src/leap/util/files.py index f7fda39e..8c7a5af3 100644 --- a/src/leap/util/files.py +++ b/src/leap/util/files.py @@ -1,6 +1,7 @@ import os import stat import logging +import time logger = logging.getLogger(__name__) @@ -25,3 +26,20 @@ def check_and_fix_urw_only(cert): logger.error('Error while trying to chmod 600 %s' % cert) raise + + +def get_mtime(filename): + """ + Returns the modified time or None if the file doesn't exist + + @param filename: path to check + @type filename: str + + @rtype: str + """ + try: + _mtime = os.stat(filename)[8] + mtime = time.strftime("%c GMT", time.gmtime(_mtime)) + return mtime + except OSError: + return None -- cgit v1.2.3 From 0ff122cf9fd0a76871093b595910fb7c0d3bfe85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 10:05:58 -0300 Subject: Pass mtime to pluggableconfig's load Also add a request_helpers file to util where all the helper methods for handling requests should go --- src/leap/config/baseconfig.py | 6 +-- src/leap/services/eip/eipbootstrapper.py | 5 ++- src/leap/services/eip/providerbootstrapper.py | 5 ++- src/leap/util/request_helpers.py | 55 +++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 src/leap/util/request_helpers.py (limited to 'src') diff --git a/src/leap/config/baseconfig.py b/src/leap/config/baseconfig.py index b6890d09..f04d8b35 100644 --- a/src/leap/config/baseconfig.py +++ b/src/leap/config/baseconfig.py @@ -107,7 +107,7 @@ class BaseConfig: raise return True - def load(self, path="", data=None): + def load(self, path="", data=None, mtime=None): """ Loads the configuration from disk @@ -127,9 +127,9 @@ class BaseConfig: try: if data is None: - self._config_checker.load(fromfile=config_path) + self._config_checker.load(fromfile=config_path, mtime=mtime) else: - self._config_checker.load(data) + self._config_checker.load(data, mtime=mtime) except Exception as e: logger.warning("Something went wrong while loading " + "the config from %s\n%s" % (config_path, e)) diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index fdf54bbb..84a309cb 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -32,6 +32,7 @@ from leap.services.eip.eipconfig import EIPConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread from leap.util.files import check_and_fix_urw_only, get_mtime +from leap.util.request_helpers import get_content logger = logging.getLogger(__name__) @@ -114,9 +115,9 @@ class EIPBootstrapper(QtCore.QObject): if res.status_code == 304: logger.debug("EIP definition has not been modified") else: - eip_definition = res.content + eip_definition, mtime = get_content(res) - self._eip_config.load(data=eip_definition) + self._eip_config.load(data=eip_definition, mtime=mtime) self._eip_config.save(["leap", "providers", self._provider_config.get_domain(), diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index f1a917f0..4fdd9b8d 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -32,6 +32,7 @@ from leap.config.providerconfig import ProviderConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread from leap.util.files import check_and_fix_urw_only, get_mtime +from leap.util.request_helpers import get_content logger = logging.getLogger(__name__) @@ -172,10 +173,10 @@ class ProviderBootstrapper(QtCore.QObject): if res.status_code == 304: logger.debug("Provider definition has not been modified") else: - provider_definition = res.content + provider_definition, mtime = get_content(res) provider_config = ProviderConfig() - provider_config.load(data=provider_definition) + provider_config.load(data=provider_definition, mtime=mtime) provider_config.save(["leap", "providers", self._domain, diff --git a/src/leap/util/request_helpers.py b/src/leap/util/request_helpers.py new file mode 100644 index 00000000..c5d0f3f5 --- /dev/null +++ b/src/leap/util/request_helpers.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# request_helpers.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Request helpers for backward compatible "parsing" of requests +""" + +import json + +from dateutil import parser as dateparser + + +def get_content(request): + """ + Returns the content by trying to get it from the json + property/function or from content, in that order. + Also returns the mtime for that content if available + + @param request: request as it is given by requests + @type request: Response + + @rtype: tuple (contents, mtime) + """ + + contents = "" + mtime = None + + if request.json: + if callable(request.json): + contents = json.dumps(request.json()) + else: + contents = json.dumps(request.json) + else: + contents = request.content + + mtime = None + last_modified = request.headers.get('last-modified', None) + if last_modified: + mtime = int(dateparser.parse(last_modified).strftime("%s")) + + return contents, mtime -- cgit v1.2.3 From fc434146593060c1fc454776b1feaaa80a700823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 10:11:35 -0300 Subject: Revert jsonschema version to <=0.8 --- src/leap/services/eip/eipspec.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/eipspec.py b/src/leap/services/eip/eipspec.py index d5c73056..69d4d927 100644 --- a/src/leap/services/eip/eipspec.py +++ b/src/leap/services/eip/eipspec.py @@ -18,18 +18,16 @@ eipservice_config_spec = { 'description': 'sample eip service config', 'type': 'object', - 'required': [ - 'serial', - 'version' - ], 'properties': { 'serial': { 'type': int, - 'default': 1 + 'default': 1, + 'required': True }, 'version': { 'type': int, - 'default': 1 + 'default': 1, + 'required': True }, 'clusters': { 'type': list, -- cgit v1.2.3 From ab1c68b5a85af10b4c646d56ee2395e89793b8ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 10:12:45 -0300 Subject: Fix comment --- src/leap/config/baseconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/config/baseconfig.py b/src/leap/config/baseconfig.py index f04d8b35..9e47f685 100644 --- a/src/leap/config/baseconfig.py +++ b/src/leap/config/baseconfig.py @@ -115,7 +115,7 @@ class BaseConfig: @param path: relative path to configuration. The absolute path will be calculated depending on the platform - @return: True if loaded to disk correctly, False otherwise + @return: True if loaded from disk correctly, False otherwise """ # TODO: retrieve standalone option from app-level config -- cgit v1.2.3 From b15f28b73afc31fd4176bee1a615e4095b0f4479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 10:13:59 -0300 Subject: Fix comment on baseconfig::save --- src/leap/config/baseconfig.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/config/baseconfig.py b/src/leap/config/baseconfig.py index 9e47f685..0386c294 100644 --- a/src/leap/config/baseconfig.py +++ b/src/leap/config/baseconfig.py @@ -84,9 +84,10 @@ class BaseConfig: """ Saves the current configuration to disk - @param path: relative path to configuration. The absolute path - will be calculated depending on the platform. - @type path: list + @param path_list: list of components that form the relative + path to configuration. The absolute path will be calculated + depending on the platform. + @type path_list: list @return: True if saved to disk correctly, False otherwise """ -- cgit v1.2.3 From 12d2835c7d1f3c3d11eaa587b2196c104e6859e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 10:21:15 -0300 Subject: Add mkdir_p method to util.files --- src/leap/config/baseconfig.py | 10 ++----- src/leap/services/eip/eipbootstrapper.py | 12 ++------ src/leap/services/eip/providerbootstrapper.py | 10 ++----- src/leap/util/files.py | 41 +++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/leap/config/baseconfig.py b/src/leap/config/baseconfig.py index 0386c294..538a47f0 100644 --- a/src/leap/config/baseconfig.py +++ b/src/leap/config/baseconfig.py @@ -22,7 +22,6 @@ Implements the abstract base class for configuration import logging import functools import os -import errno import copy from abc import ABCMeta, abstractmethod @@ -30,6 +29,7 @@ from abc import ABCMeta, abstractmethod from leap.config.prefixers import get_platform_prefixer from leap.config.pluggableconfig import PluggableConfig from leap.util.check import leap_assert +from leap.util.files import mkdir_p logger = logging.getLogger(__name__) @@ -92,13 +92,7 @@ class BaseConfig: @return: True if saved to disk correctly, False otherwise """ config_path = os.path.join(self.get_path_prefix(), *(path_list[:-1])) - try: - os.makedirs(config_path) - except OSError as e: - if e.errno == errno.EEXIST and os.path.isdir(config_path): - pass - else: - raise + mkdir_p(config_path) try: self._config_checker.serialize(os.path.join(config_path, diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 84a309cb..3e4e2063 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -22,7 +22,6 @@ EIP bootstrapping import requests import logging import os -import errno from PySide import QtGui, QtCore @@ -31,7 +30,7 @@ from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread -from leap.util.files import check_and_fix_urw_only, get_mtime +from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content logger = logging.getLogger(__name__) @@ -184,14 +183,7 @@ class EIPBootstrapper(QtCore.QObject): # TODO: check certificate validity - try: - os.makedirs(os.path.dirname(client_cert_path)) - except OSError as e: - if e.errno == errno.EEXIST and \ - os.path.isdir(os.path.dirname(client_cert_path)): - pass - else: - raise + mkdir_p(os.path.dirname(client_cert_path)) with open(client_cert_path, "w") as f: f.write(client_cert) diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 4fdd9b8d..df56110e 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -31,7 +31,7 @@ from PySide import QtGui, QtCore from leap.config.providerconfig import ProviderConfig from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread -from leap.util.files import check_and_fix_urw_only, get_mtime +from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content logger = logging.getLogger(__name__) @@ -271,13 +271,7 @@ class ProviderBootstrapper(QtCore.QObject): cert_dir = os.path.dirname(cert_path) - try: - os.makedirs(cert_dir) - except OSError as e: - if e.errno == errno.EEXIST and os.path.isdir(cert_dir): - pass - else: - raise + mkdir_p(cert_dir) with open(cert_path, "w") as f: f.write(res.content) diff --git a/src/leap/util/files.py b/src/leap/util/files.py index 8c7a5af3..97741433 100644 --- a/src/leap/util/files.py +++ b/src/leap/util/files.py @@ -1,7 +1,29 @@ +# -*- coding: utf-8 -*- +# files.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Implements file helper methods +""" + import os import stat import logging import time +import errno logger = logging.getLogger(__name__) @@ -43,3 +65,22 @@ def get_mtime(filename): return mtime except OSError: return None + + +def mkdir_p(path): + """ + Creates the path and all the intermediate directories that don't + exist + + Might raise OSError + + @param path: path to create + @type path: str + """ + try: + os.makedirs(path) + except OSError as exc: + if exc.errno == errno.EEXIST and os.path.isdir(path): + pass + else: + raise -- cgit v1.2.3 From 5e3c49e57cd87ba6cdd11cb9ef59333fbfe4d49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 10:23:37 -0300 Subject: Fix standalone type comments --- src/leap/config/baseconfig.py | 4 ++-- src/leap/config/prefixers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/config/baseconfig.py b/src/leap/config/baseconfig.py index 538a47f0..90529042 100644 --- a/src/leap/config/baseconfig.py +++ b/src/leap/config/baseconfig.py @@ -69,7 +69,7 @@ class BaseConfig: @param standalone: if True it will return the prefix for a standalone application. Otherwise, it will return the system default for configuration storage. - @param type: bool + @type standalone: bool """ return get_platform_prefixer().get_path_prefix(standalone=standalone) @@ -147,7 +147,7 @@ class LocalizedKey(object): informs the problem and returns an empty string @param lang: language code - @param type: str + @type lang: str @return: localized value from the possible values returned by self._func diff --git a/src/leap/config/prefixers.py b/src/leap/config/prefixers.py index 20d59b2d..64c36908 100644 --- a/src/leap/config/prefixers.py +++ b/src/leap/config/prefixers.py @@ -42,7 +42,7 @@ class Prefixer: @param standalone: if True it will return the prefix for a standalone application. Otherwise, it will return the system default for configuration storage. - @param type: bool + @type standalone: bool """ return "" @@ -68,7 +68,7 @@ class LinuxPrefixer(Prefixer): @param standalone: if True it will return the prefix for a standalone application. Otherwise, it will return the system default for configuration storage. - @param type: bool + @type standalone: bool """ config_dir = BaseDirectory.xdg_config_home if not standalone: -- cgit v1.2.3 From a12906958e4d117daaf45bd42e7383d2344ea463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 10:38:25 -0300 Subject: Add util.certs and abstract digest there --- src/leap/services/eip/providerbootstrapper.py | 6 ++--- src/leap/util/certs.py | 39 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 src/leap/util/certs.py (limited to 'src') diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index df56110e..dc87a1bd 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -23,9 +23,7 @@ import requests import logging import socket import os -import errno -from OpenSSL import crypto from PySide import QtGui, QtCore from leap.config.providerconfig import ProviderConfig @@ -33,6 +31,7 @@ from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content +from leap.util.certs import get_digest logger = logging.getLogger(__name__) @@ -324,8 +323,7 @@ class ProviderBootstrapper(QtCore.QObject): leap_assert(len(cert_data) > 0, "Could not read certificate data") - x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_data) - digest = x509.digest(method).replace(":", "").lower() + digest = get_digest(cert_data, method) leap_assert(digest == fingerprint, "Downloaded certificate has a different fingerprint!") diff --git a/src/leap/util/certs.py b/src/leap/util/certs.py new file mode 100644 index 00000000..7cbd7519 --- /dev/null +++ b/src/leap/util/certs.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# certs.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Implements cert checks and helpers +""" + +from OpenSSL import crypto + + +def get_digest(cert_data, method): + """ + Returns the digest for the cert_data using the method specified + + @param cert_data: certificate data in string form + @type cert_data: str + @param method: method to be used for digest + @type method: str + + @rtype: str + """ + x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_data) + digest = x509.digest(method).replace(":", "").lower() + + return digest -- cgit v1.2.3 From 60bcc7b27aa934a0d62033e7152b87d5af638491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 11:09:38 -0300 Subject: Add valid pemfile check before saving the downloaded client cert --- src/leap/services/eip/eipbootstrapper.py | 5 ++++ src/leap/util/certs.py | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) (limited to 'src') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 3e4e2063..ec3dfe7b 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -32,6 +32,7 @@ from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content +from leap.util.certs import is_valid_pemfile logger = logging.getLogger(__name__) @@ -183,6 +184,10 @@ class EIPBootstrapper(QtCore.QObject): # TODO: check certificate validity + if not is_valid_pemfile(client_cert): + raise Exception(self.tr("The downloaded certificate is not a " + "valid PEM file")) + mkdir_p(os.path.dirname(client_cert_path)) with open(client_cert_path, "w") as f: diff --git a/src/leap/util/certs.py b/src/leap/util/certs.py index 7cbd7519..d6065474 100644 --- a/src/leap/util/certs.py +++ b/src/leap/util/certs.py @@ -19,8 +19,14 @@ Implements cert checks and helpers """ +import logging + from OpenSSL import crypto +from leap.util.check import leap_assert + +logger = logging.getLogger(__name__) + def get_digest(cert_data, method): """ @@ -37,3 +43,44 @@ def get_digest(cert_data, method): digest = x509.digest(method).replace(":", "").lower() return digest + + +def can_load_cert_and_pkey(string): + """ + Loads certificate and private key from a buffer, returns True if + everything went well, False otherwise + + @param string: buffer containing the cert and private key + @type string: str or any kind of buffer + + @rtype: bool + """ + + can_load = True + + try: + cert = crypto.load_certificate(crypto.FILETYPE_PEM, string) + key = crypto.load_privatekey(crypto.FILETYPE_PEM, string) + + leap_assert(cert, 'The certificate could not be loaded') + leap_assert(key, 'The private key could not be loaded') + except Exception as e: + can_load = False + logger.error("Something went wrong while trying to load " + "the certificate: %r" % (e,)) + + return can_load + + +def is_valid_pemfile(cert): + """ + Checks that the passed string is a valid pem certificate + + @param cert: String containing pem content + @type cert: str + + @rtype: bool + """ + leap_assert(cert, "We need a cert to load") + + return can_load_cert_and_pkey(cert) -- cgit v1.2.3 From 240d6b7762f7cc8f4c6fd229e4538aa9aa2262a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 11:33:42 -0300 Subject: Check validity for downloaded certs and re-download if needed --- src/leap/services/eip/eipbootstrapper.py | 6 +- src/leap/util/certs.py | 99 ++++++++++++++++++++++++++++++-- 2 files changed, 100 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index ec3dfe7b..c83cb1b5 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -32,7 +32,7 @@ from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content -from leap.util.certs import is_valid_pemfile +from leap.util.certs import is_valid_pemfile, should_redownload logger = logging.getLogger(__name__) @@ -154,6 +154,10 @@ class EIPBootstrapper(QtCore.QObject): get_client_cert_path(self._provider_config, about_to_download=True) + # For re-download if something is wrong with the cert + self._download_if_needed = self._download_if_needed and \ + not should_redownload(client_cert_path) + if self._download_if_needed and \ os.path.exists(client_cert_path): try: diff --git a/src/leap/util/certs.py b/src/leap/util/certs.py index d6065474..a8bcd65e 100644 --- a/src/leap/util/certs.py +++ b/src/leap/util/certs.py @@ -19,15 +19,58 @@ Implements cert checks and helpers """ +import os +import time import logging from OpenSSL import crypto +from dateutil.parser import parse as dateparse from leap.util.check import leap_assert logger = logging.getLogger(__name__) +def get_cert_from_string(string): + """ + Returns the x509 from the contents of this string + + @param string: certificate contents as downloaded + @type string: str + + @return: x509 or None + """ + leap_assert(string, "We need something to load") + + x509 = None + try: + x509 = crypto.load_certificate(crypto.FILETYPE_PEM, string) + except Exception as e: + logger.error("Something went wrong while loading the certificate: %r" + % (e,)) + return x509 + + +def get_privatekey_from_string(string): + """ + Returns the private key from the contents of this string + + @param string: private key contents as downloaded + @type string: str + + @return: private key or None + """ + leap_assert(string, "We need something to load") + + pkey = None + try: + pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, string) + except Exception as e: + logger.error("Something went wrong while loading the certificate: %r" + % (e,)) + return pkey + + def get_digest(cert_data, method): """ Returns the digest for the cert_data using the method specified @@ -39,7 +82,7 @@ def get_digest(cert_data, method): @rtype: str """ - x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_data) + x509 = get_cert_from_string(cert_data) digest = x509.digest(method).replace(":", "").lower() return digest @@ -55,12 +98,11 @@ def can_load_cert_and_pkey(string): @rtype: bool """ - can_load = True try: - cert = crypto.load_certificate(crypto.FILETYPE_PEM, string) - key = crypto.load_privatekey(crypto.FILETYPE_PEM, string) + cert = get_cert_from_string(string) + key = get_privatekey_from_string(string) leap_assert(cert, 'The certificate could not be loaded') leap_assert(key, 'The private key could not be loaded') @@ -84,3 +126,52 @@ def is_valid_pemfile(cert): leap_assert(cert, "We need a cert to load") return can_load_cert_and_pkey(cert) + + +def get_cert_time_boundaries(certfile): + """ + Returns the time boundaries for the certificate saved in certfile + + @param certfile: path to certificate + @type certfile: str + + @rtype: tuple (from, to) + """ + cert = get_cert_from_string(certfile) + leap_assert(cert, 'There was a problem loading the certificate') + + fromts, tots = (cert.get_notBefore(), cert.get_notAfter()) + from_, to_ = map( + lambda ts: time.gmtime(time.mktime(dateparse(ts).timetuple())), + (fromts, tots)) + return from_, to_ + + +def should_redownload(certfile, now=time.gmtime): + """ + Returns True if any of the checks don't pass, False otherwise + + @param certfile: path to certificate + @type certfile: str + @param now: current date function, ONLY USED FOR TESTING + + @rtype: bool + """ + exists = os.path.isfile(certfile) + + if not exists: + return True + + try: + with open(certfile, "r") as f: + if not is_valid_pemfile(f.read()): + return True + except: + return True + + valid_from, valid_to = get_cert_time_boundaries(certfile) + + if not (valid_from < now() < valid_to): + return True + + return False -- cgit v1.2.3 From df01ec4406134d9abbda3c3e1ae1933633827a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 11:37:16 -0300 Subject: Update name in file header --- src/leap/util/checkerthread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/util/checkerthread.py b/src/leap/util/checkerthread.py index 3430a450..0e69eca3 100644 --- a/src/leap/util/checkerthread.py +++ b/src/leap/util/checkerthread.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# providerbootstrapper.py +# checkerthread.py # Copyright (C) 2013 LEAP # # This program is free software: you can redistribute it and/or modify -- cgit v1.2.3 From 98699d1c1c9d4698faa6bd7b1c7cf5b576372381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 11:39:06 -0300 Subject: Separate stdlibs from non-std in imports --- src/leap/crypto/srpauth.py | 5 +++-- src/leap/crypto/srpregister.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index c1964514..e9c72408 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -15,11 +15,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import requests -import srp import binascii import logging +import requests +import srp + from PySide import QtCore, QtGui from leap.config.providerconfig import ProviderConfig diff --git a/src/leap/crypto/srpregister.py b/src/leap/crypto/srpregister.py index cf673e35..471ef28f 100644 --- a/src/leap/crypto/srpregister.py +++ b/src/leap/crypto/srpregister.py @@ -15,11 +15,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import requests import binascii -import srp import logging +import requests +import srp + from PySide import QtCore from urlparse import urlparse -- cgit v1.2.3 From fc80cfd6d393534d71bfd0489557d3b4203cf4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 11:45:34 -0300 Subject: Default to port 443 if no port is specified --- src/leap/crypto/srpregister.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/leap/crypto/srpregister.py b/src/leap/crypto/srpregister.py index 471ef28f..c99f79ab 100644 --- a/src/leap/crypto/srpregister.py +++ b/src/leap/crypto/srpregister.py @@ -72,6 +72,8 @@ class SRPRegister(QtCore.QObject): parsed_url = urlparse(provider_config.get_api_uri()) self._provider = parsed_url.hostname self._port = parsed_url.port + if self._port is None: + self._port = "443" self._register_path = register_path @@ -85,17 +87,11 @@ class SRPRegister(QtCore.QObject): @rtype: str """ - if self._port: - uri = "https://%s:%s/%s/%s" % ( - self._provider, - self._port, - self._provider_config.get_api_version(), - self._register_path) - else: - uri = "https://%s/%s/%s" % ( - self._provider, - self._provider_config.get_api_version(), - self._register_path) + uri = "https://%s:%s/%s/%s" % ( + self._provider, + self._port, + self._provider_config.get_api_version(), + self._register_path) return uri -- cgit v1.2.3 From ebb2d8c1a01e5273d84273fd5dc3bf735be34fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 11:50:54 -0300 Subject: Use expanduser instead of hardcoded string --- src/leap/services/eip/vpn.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 3ec32f6f..b9988117 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -318,10 +318,11 @@ class VPN(QtCore.QThread): if __name__ == "__main__": - app = QtGui.QApplication(sys.argv) - + import os import signal + app = QtGui.QApplication(sys.argv) + def sigint_handler(*args, **kwargs): logger.debug('SIGINT catched. shutting down...') vpn_thread = args[0] @@ -352,7 +353,7 @@ if __name__ == "__main__": if provider.load("leap/providers/bitmask.net/provider.json"): vpn_thread.start(eipconfig=eipconfig, providerconfig=provider, - socket_host="/home/chiiph/vpnsock", + socket_host=os.path.expanduser("~/vpnsock"), socket_port="unix") timer = QtCore.QTimer() -- cgit v1.2.3 From 4359515dafe572398262ce91bf88d4f122042981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 13:39:00 -0300 Subject: Add vpn already running checks --- src/leap/gui/mainwindow.py | 8 +++++ src/leap/services/eip/vpn.py | 76 ++++++++++++++++++++++++++++++++++++++++++++ src/leap/util/certs.py | 6 ++-- 3 files changed, 88 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 6f7f646e..9b4a70a5 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -680,6 +680,14 @@ class MainWindow(QtGui.QMainWindow): self._set_eip_status(self.tr("VPN: Connected!")) elif status == "WAIT": self._set_eip_status(self.tr("VPN: Waiting to start...")) + elif status == "ALREADYRUNNING": + # Put the following calls in Qt's event queue, otherwise + # the UI won't update properly + QtCore.QTimer.singleShot(0, self._stop_eip) + QtCore.QTimer.singleShot(0, partial(self._set_eip_status, + self.tr("Unable to start VPN, " + "it's already " + "running."))) else: self._set_eip_status(status) diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index b9988117..dd42cd13 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -18,8 +18,10 @@ """ VPN launcher and watcher thread """ + import logging import sys +import psutil from PySide import QtCore, QtGui from functools import partial @@ -61,6 +63,8 @@ class VPN(QtCore.QThread): TCPUDP_WRITE_KEY = "tcp_udp_write" AUTH_READ_KEY = "auth_read" + ALREADY_RUNNING_STEP = "ALREADYRUNNING" + def __init__(self): QtCore.QThread.__init__(self) @@ -134,6 +138,20 @@ class VPN(QtCore.QThread): with QtCore.QMutexLocker(self._should_quit_lock): self._should_quit = False + if not self._stop_if_already_running(): + # We send a fake state + state_dict = { + self.TS_KEY: "", + self.STATUS_STEP_KEY: self.ALREADY_RUNNING_STEP, + self.OK_KEY: "", + self.IP_KEY: "", + self.REMOTE_KEY: "" + } + + self.state_changed.emit(state_dict) + # And just return, don't start the process + return + command = self._launcher.get_vpn_command(eipconfig=eipconfig, providerconfig=providerconfig, socket_host=socket_host, @@ -156,6 +174,64 @@ class VPN(QtCore.QThread): logger.warning("Something went wrong while starting OpenVPN: %r" % (e,)) + def _get_openvpn_process(self): + """ + Looks for openvpn instances running + + @rtype: process + """ + openvpn_process = None + for p in psutil.process_iter(): + try: + # XXX Not exact! + # Will give false positives. + # we should check that cmdline BEGINS + # with openvpn or with our wrapper + # (pkexec / osascript / whatever) + if self._launcher.OPENVPN_BIN in ' '.join(p.cmdline): + openvpn_process = p + break + except psutil.error.AccessDenied: + pass + return openvpn_process + + def _stop_if_already_running(self): + """ + Checks if VPN is already running and tries to stop it + + @return: True if stopped, False otherwise + """ + + process = self._get_openvpn_process() + if process: + logger.debug("OpenVPN is already running, trying to stop it") + cmdline = process.cmdline + + manag_flag = "--management" + if isinstance(cmdline, list) and manag_flag in cmdline: + try: + index = cmdline.index(manag_flag) + host = cmdline[index + 1] + port = cmdline[index + 2] + logger.debug("Trying to connect to %s:%s" + % (host, port)) + self._connect(host, port) + self._send_command("signal SIGTERM") + self._tn.close() + self._tn = None + except Exception as e: + logger.warning("Problem trying to terminate OpenVPN: %r" + % (e,)) + + process = self._get_openvpn_process() + if process is None: + logger.warning("Unabled to terminate OpenVPN") + return True + else: + return False + + return True + def _connect(self, socket_host, socket_port): """ Connects to the specified socket_host socket_port diff --git a/src/leap/util/certs.py b/src/leap/util/certs.py index a8bcd65e..63c60c3d 100644 --- a/src/leap/util/certs.py +++ b/src/leap/util/certs.py @@ -162,14 +162,16 @@ def should_redownload(certfile, now=time.gmtime): if not exists: return True + certdata = None try: with open(certfile, "r") as f: - if not is_valid_pemfile(f.read()): + certdata = f.read() + if not is_valid_pemfile(certdata): return True except: return True - valid_from, valid_to = get_cert_time_boundaries(certfile) + valid_from, valid_to = get_cert_time_boundaries(certdata) if not (valid_from < now() < valid_to): return True -- cgit v1.2.3 From ef43dff37c6db915757184ad51bc017d45e70c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 14:02:13 -0300 Subject: Notify the user of pkexec/polkit/openvpn checks --- src/leap/gui/mainwindow.py | 22 +++++++++++++++++++++- src/leap/gui/ui/mainwindow.ui | 3 +++ src/leap/services/eip/vpnlaunchers.py | 21 +++++++++++++++++---- 3 files changed, 41 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 9b4a70a5..9589bea0 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -30,7 +30,10 @@ from ui_mainwindow import Ui_MainWindow from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth from leap.services.eip.vpn import VPN -from leap.services.eip.vpnlaunchers import VPNLauncherException +from leap.services.eip.vpnlaunchers import (VPNLauncherException, + OpenVPNNotFoundException, + EIPNoPkexecAvailable, + EIPNoPolkitAuthAgentAvailable) from leap.services.eip.providerbootstrapper import ProviderBootstrapper from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig @@ -605,8 +608,25 @@ class MainWindow(QtGui.QMainWindow): self.ui.btnEipStartStop.disconnect(self) self.ui.btnEipStartStop.clicked.connect( self._stop_eip) + except EIPNoPolkitAuthAgentAvailable: + self._set_eip_status(self.tr("We could not find any " + "authentication " + "agent in your system.
" + "Make sure you have " + "polkit-gnome-authentication-" + "agent-1 " + "running and try again."), + error=True) + except EIPNoPkexecAvailable: + self._set_eip_status(self.tr("We could not find pkexec " + "in your system."), + error=True) + except OpenVPNNotFoundException: + self._set_eip_status(self.tr("We couldn't find openvpn"), + error=True) except VPNLauncherException as e: self._set_eip_status("%s" % (e,), error=True) + self.ui.btnEipStartStop.setEnabled(True) def _stop_eip(self): diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui index a8a8aa59..ca3db95c 100644 --- a/src/leap/gui/ui/mainwindow.ui +++ b/src/leap/gui/ui/mainwindow.ui @@ -274,6 +274,9 @@ Qt::AlignCenter + + true +
diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 6f277481..9345c0b3 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -38,6 +38,18 @@ class VPNLauncherException(Exception): pass +class OpenVPNNotFoundException(VPNLauncherException): + pass + + +class EIPNoPolkitAuthAgentAvailable(VPNLauncherException): + pass + + +class EIPNoPkexecAvailable(VPNLauncherException): + pass + + class VPNLauncher: """ Abstract launcher class @@ -45,7 +57,6 @@ class VPNLauncher: __metaclass__ = ABCMeta - # TODO: document parameters @abstractmethod def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port=None): @@ -137,9 +148,9 @@ def _has_updown_scripts(path): def _is_auth_agent_running(): - return bool( + return len( commands.getoutput( - 'ps aux | grep polkit-[g]nome-authentication-agent-1')) + 'ps aux | grep polkit-[g]nome-authentication-agent-1')) > 0 class LinuxVPNLauncher(VPNLauncher): @@ -181,7 +192,7 @@ class LinuxVPNLauncher(VPNLauncher): openvpn_possibilities = which(self.OPENVPN_BIN) if len(openvpn_possibilities) == 0: - raise VPNLauncherException("We couldn't find openvpn") + raise OpenVPNNotFoundException() openvpn = openvpn_possibilities[0] args = [] @@ -196,8 +207,10 @@ class LinuxVPNLauncher(VPNLauncher): else: logger.warning("No polkit auth agent found. pkexec " + "will use its own auth agent.") + raise EIPNoPolkitAuthAgentAvailable() else: logger.warning("System has no pkexec") + raise EIPNoPkexecAvailable() # TODO: handle verbosity -- cgit v1.2.3 From fdc1d749a859cefd325e1de712f90eba79d3f678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 14:15:02 -0300 Subject: Display a more related error message for https wizard checks Differentiate between SSLError and other type of errors, so to its clear when the certificate fails and when the connection fails or other kind of problems --- src/leap/gui/wizard.py | 4 ++-- src/leap/services/eip/providerbootstrapper.py | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 83f64020..4e811fb9 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -351,8 +351,8 @@ class Wizard(QtGui.QWizard): status = "" passed = data[self._provider_bootstrapper.PASSED_KEY] if not passed: - status = self.tr("Provider does not " - "support HTTPS") + status = self.tr("%s") \ + % (data[self._provider_bootstrapper.ERROR_KEY]) self.ui.lblProviderSelectStatus.setText(status) self.ui.btnCheck.setEnabled(not passed) self.ui.lnProvider.setEnabled(not passed) diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index dc87a1bd..82c62f90 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -127,8 +127,14 @@ class ProviderBootstrapper(QtCore.QObject): res = self._session.get("https://%s" % (self._domain,)) res.raise_for_status() https_data[self.PASSED_KEY] = True + except requests.exceptions.SSLError as e: + logger.error("%s" % (e,)) + https_data[self.ERROR_KEY] = self.tr("Provider certificate could " + "not verify") except Exception as e: - https_data[self.ERROR_KEY] = "%s" % (e,) + logger.error("%s" % (e,)) + https_data[self.ERROR_KEY] = self.tr("Provider does not support " + "HTTPS") logger.debug("Emitting https_connection %s" % (https_data,)) self.https_connection.emit(https_data) -- cgit v1.2.3 From 4459619c0d0597447119c67fb7267caae028103f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 14:17:08 -0300 Subject: Document checker parameter in providerbootstrapper --- src/leap/services/eip/providerbootstrapper.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 82c62f90..40ec55c5 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -201,6 +201,8 @@ class ProviderBootstrapper(QtCore.QObject): """ Populates the check queue + @param checker: checker thread to be used to run this check + @type checker: CheckerThread @param domain: domain to check @type domain: str @param download_if_needed: if True, makes the checks do not -- cgit v1.2.3 From 715b50b872736952d01b2c8cac9003da1e634c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 14:24:05 -0300 Subject: Make sure we always end up with the proper list of services --- src/leap/gui/mainwindow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 9589bea0..af905fc0 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -508,7 +508,10 @@ class MainWindow(QtGui.QMainWindow): settings = QtCore.QSettings() self._enabled_services = settings.value( "%s/Services" % - (self.ui.cmbProviders.currentText(),), "").split(",") + (self.ui.cmbProviders.currentText(),), "") + + if isinstance(self._enabled_services, (str, unicode)): + self._enabled_services = self._enabled_services.split(",") if len(provider) == 0: self._set_status(self.tr("Please select a valid provider")) -- cgit v1.2.3 From 3ff6a5ebadca8cf6066e9db39fcbfcb3d8283430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 14:42:52 -0300 Subject: Add a TODO comment to not forget improve the configured providers check --- src/leap/gui/mainwindow.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index af905fc0..6aaf0a38 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -374,6 +374,9 @@ class MainWindow(QtGui.QMainWindow): @rtype: list """ + + # TODO: check which providers have a valid certificate among + # other things, not just the directories providers = [] try: providers = os.listdir( -- cgit v1.2.3 From 452d1b3a0ab64a6d1e742a74c369336dcdf91897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 15:37:04 -0300 Subject: Make the no up/down log an error instead of a warning --- src/leap/services/eip/vpnlaunchers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 9345c0b3..c646da7d 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -142,8 +142,8 @@ def _has_updown_scripts(path): # XXX should check permissions too is_file = os.path.isfile(path) if not is_file: - logger.warning("Could not find up/down scripts. " + - "Might produce DNS leaks.") + logger.error("Could not find up/down scripts. " + + "Might produce DNS leaks.") return is_file -- cgit v1.2.3 From 4e3b2f09f0e5fba35aef3edf8ad2b23c64a5d995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 15:46:50 -0300 Subject: Use a unix socket every time for now Will need a special case for windows --- src/leap/gui/mainwindow.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 6aaf0a38..c2c37485 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -20,7 +20,7 @@ Main window for the leap client """ import os import logging -import random +import tempfile import keyring from PySide import QtCore, QtGui @@ -603,12 +603,27 @@ class MainWindow(QtGui.QMainWindow): self._systray.setIcon(self.LOGGED_IN_ICON) self._download_eip_config() + def _get_socket_host(self): + """ + Returns the socket and port to be used for VPN + + @rtype: tuple (str, str) (host, port) + """ + + # TODO: make this properly multiplatform + host = os.path.join(tempfile.mkdtemp(prefix="leap-tmp"), + 'openvpn.socket') + port = "unix" + + return host, port + def _start_eip(self): try: + host, port = self._get_socket_host() self._vpn.start(eipconfig=self._eip_config, providerconfig=self._provider_config, - socket_host="localhost", - socket_port=str(random.randint(1000, 9999))) + socket_host=host, + socket_port=port) self._vpn_systray.setVisible(True) self.ui.btnEipStartStop.setText(self.tr("Stop EIP")) self.ui.btnEipStartStop.disconnect(self) -- cgit v1.2.3 From 9bd4a1394cfbdd555b9cb42e2aeba3dc4957ae88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 16:08:42 -0300 Subject: Always show the eip icon if the service is available and enabled --- src/leap/gui/mainwindow.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index c2c37485..4db74c2d 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -104,8 +104,6 @@ class MainWindow(QtGui.QMainWindow): # configuration and certificate. self._provider_bootstrapper = ProviderBootstrapper() - # TODO: add sigint handler - # Intermediate stages, only do something if there was an error self._provider_bootstrapper.name_resolution.connect( self._intermediate_stage) @@ -171,8 +169,8 @@ class MainWindow(QtGui.QMainWindow): self._action_eip_status = QtGui.QAction(self.tr("Encryption is OFF"), self) self._action_eip_status.setEnabled(False) - self._action_eip_stop = QtGui.QAction(self.tr("Stop"), self) - self._action_eip_stop.triggered.connect( + self._action_eip_startstop = QtGui.QAction(self.tr("Stop"), self) + self._action_eip_startstop.triggered.connect( self._stop_eip) self._action_eip_write = QtGui.QAction( QtGui.QIcon(":/images/Arrow-Up-32.png"), @@ -278,7 +276,7 @@ class MainWindow(QtGui.QMainWindow): vpn_systrayMenu = QtGui.QMenu(self) vpn_systrayMenu.addAction(self._action_eip_status) - vpn_systrayMenu.addAction(self._action_eip_stop) + vpn_systrayMenu.addAction(self._action_eip_startstop) vpn_systrayMenu.addAction(self._action_eip_read) vpn_systrayMenu.addAction(self._action_eip_write) self._vpn_systray = QtGui.QSystemTrayIcon(self) @@ -624,11 +622,14 @@ class MainWindow(QtGui.QMainWindow): providerconfig=self._provider_config, socket_host=host, socket_port=port) - self._vpn_systray.setVisible(True) self.ui.btnEipStartStop.setText(self.tr("Stop EIP")) self.ui.btnEipStartStop.disconnect(self) self.ui.btnEipStartStop.clicked.connect( self._stop_eip) + self._action_eip_startstop.setText(self.tr("Stop")) + self._action_eip_startstop.disconnect(self) + self._action_eip_startstop.triggered.connect( + self._stop_eip) except EIPNoPolkitAuthAgentAvailable: self._set_eip_status(self.tr("We could not find any " "authentication " @@ -652,13 +653,16 @@ class MainWindow(QtGui.QMainWindow): def _stop_eip(self): self._vpn.set_should_quit() - self._vpn_systray.setVisible(False) self._set_eip_status(self.tr("EIP has stopped")) self._set_eip_status_icon("error") self.ui.btnEipStartStop.setText(self.tr("Start EIP")) self.ui.btnEipStartStop.disconnect(self) self.ui.btnEipStartStop.clicked.connect( self._start_eip) + self._action_eip_startstop.setText(self.tr("Start")) + self._action_eip_startstop.disconnect(self) + self._action_eip_startstop.triggered.connect( + self._start_eip) def _download_eip_config(self): """ @@ -671,6 +675,7 @@ class MainWindow(QtGui.QMainWindow): if self._provider_config.provides_eip() and \ self._enabled_services.count("openvpn") > 0: + self._vpn_systray.setVisible(True) self._eip_bootstrapper.run_eip_setup_checks( self._checker_thread, self._provider_config, @@ -804,7 +809,6 @@ class MainWindow(QtGui.QMainWindow): self._login_set_enabled(True) self._set_status("") self._vpn.set_should_quit() - self._vpn_systray.setVisible(False) def _intermediate_stage(self, data): """ -- cgit v1.2.3 From 6502ea97ca6dfe5341047341ecbe166276d75c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 13 Mar 2013 17:06:55 -0300 Subject: Fix timestamp for file creation --- src/leap/util/files.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/util/files.py b/src/leap/util/files.py index 97741433..7c878e1d 100644 --- a/src/leap/util/files.py +++ b/src/leap/util/files.py @@ -60,8 +60,7 @@ def get_mtime(filename): @rtype: str """ try: - _mtime = os.stat(filename)[8] - mtime = time.strftime("%c GMT", time.gmtime(_mtime)) + mtime = time.ctime(os.path.getmtime(filename)) + " GMT" return mtime except OSError: return None -- cgit v1.2.3 From d0dfad6ac2af360de6421ce74a6831b5b81ad019 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 14 Mar 2013 07:08:31 +0900 Subject: namespace leap + leap.common split leap is a namespace package from here on. common folder will be deleted and moved to leap_pycommon repository. --- src/leap/__init__.py | 31 +---- src/leap/app.py | 3 +- src/leap/common/README | 5 + src/leap/common/__init__.py | 9 ++ src/leap/common/certs.py | 179 ++++++++++++++++++++++++++ src/leap/common/check.py | 61 +++++++++ src/leap/common/files.py | 85 ++++++++++++ src/leap/common/testing/__init__.py | 0 src/leap/common/testing/basetest.py | 100 ++++++++++++++ src/leap/common/testing/cacert.pem | 23 ++++ src/leap/common/testing/https_server.py | 87 +++++++++++++ src/leap/common/testing/leaptestscert.pem | 84 ++++++++++++ src/leap/common/testing/leaptestskey.pem | 27 ++++ src/leap/common/testing/test_basetest.py | 109 ++++++++++++++++ src/leap/config/baseconfig.py | 8 +- src/leap/config/pluggableconfig.py | 2 +- src/leap/config/prefixers.py | 4 +- src/leap/config/providerconfig.py | 2 +- src/leap/crypto/srpauth.py | 2 +- src/leap/crypto/srpregister.py | 2 +- src/leap/gui/mainwindow.py | 9 +- src/leap/services/eip/eipbootstrapper.py | 11 +- src/leap/services/eip/eipconfig.py | 2 +- src/leap/services/eip/providerbootstrapper.py | 10 +- src/leap/services/eip/vpn.py | 2 +- src/leap/services/eip/vpnlaunchers.py | 51 +------- src/leap/util/__init__.py | 29 +++++ src/leap/util/certs.py | 179 -------------------------- src/leap/util/check.py | 61 --------- src/leap/util/checkerthread.py | 2 +- src/leap/util/files.py | 85 ------------ 31 files changed, 837 insertions(+), 427 deletions(-) create mode 100644 src/leap/common/README create mode 100644 src/leap/common/__init__.py create mode 100644 src/leap/common/certs.py create mode 100644 src/leap/common/check.py create mode 100644 src/leap/common/files.py create mode 100644 src/leap/common/testing/__init__.py create mode 100644 src/leap/common/testing/basetest.py create mode 100644 src/leap/common/testing/cacert.pem create mode 100644 src/leap/common/testing/https_server.py create mode 100644 src/leap/common/testing/leaptestscert.pem create mode 100644 src/leap/common/testing/leaptestskey.pem create mode 100644 src/leap/common/testing/test_basetest.py delete mode 100644 src/leap/util/certs.py delete mode 100644 src/leap/util/check.py delete mode 100644 src/leap/util/files.py (limited to 'src') diff --git a/src/leap/__init__.py b/src/leap/__init__.py index 44235c7c..f48ad105 100644 --- a/src/leap/__init__.py +++ b/src/leap/__init__.py @@ -1,29 +1,6 @@ -""" -LEAP Encryption Access Project -website: U{https://leap.se/} -""" - -__version__ = "unknown" +# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: - from ._version import get_versions - __version__ = get_versions()['version'] - del get_versions + __import__('pkg_resources').declare_namespace(__name__) except ImportError: - #running on a tree that has not run - #the setup.py setver - pass - -__appname__ = "unknown" -try: - from leap._appname import __appname__ -except ImportError: - #running on a tree that has not run - #the setup.py setver - pass - -__full_version__ = __appname__ + '/' + str(__version__) - -# try: -# from leap._branding import BRANDING as __branding -# except ImportError: -# __branding = {} + from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) diff --git a/src/leap/app.py b/src/leap/app.py index 246f2d14..9f4d4614 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -22,8 +22,7 @@ import sys from functools import partial from PySide import QtCore, QtGui -from leap import __version__ as VERSION - +from leap.util import __version__ as VERSION from leap.util import leap_argparse from leap.gui import locale_rc from leap.gui.mainwindow import MainWindow diff --git a/src/leap/common/README b/src/leap/common/README new file mode 100644 index 00000000..040fa2b6 --- /dev/null +++ b/src/leap/common/README @@ -0,0 +1,5 @@ +for the record, I'm gathering here, on this feature branch, +all what I want to have on the new splitted package "leap.common" + +This is going to be deleted at the end of this feature branch, +and will move to leap_common repo. diff --git a/src/leap/common/__init__.py b/src/leap/common/__init__.py new file mode 100644 index 00000000..a70a9a8b --- /dev/null +++ b/src/leap/common/__init__.py @@ -0,0 +1,9 @@ +import logging +logger = logging.getLogger(__name__) + +try: + import pygeoip + HAS_GEOIP = True +except ImportError: + logger.debug('PyGeoIP not found. Disabled Geo support.') + HAS_GEOIP = False diff --git a/src/leap/common/certs.py b/src/leap/common/certs.py new file mode 100644 index 00000000..4cb70ddf --- /dev/null +++ b/src/leap/common/certs.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# certs.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Implements cert checks and helpers +""" + +import os +import time +import logging + +from OpenSSL import crypto +from dateutil.parser import parse as dateparse + +from leap.common.check import leap_assert + +logger = logging.getLogger(__name__) + + +def get_cert_from_string(string): + """ + Returns the x509 from the contents of this string + + @param string: certificate contents as downloaded + @type string: str + + @return: x509 or None + """ + leap_assert(string, "We need something to load") + + x509 = None + try: + x509 = crypto.load_certificate(crypto.FILETYPE_PEM, string) + except Exception as e: + logger.error("Something went wrong while loading the certificate: %r" + % (e,)) + return x509 + + +def get_privatekey_from_string(string): + """ + Returns the private key from the contents of this string + + @param string: private key contents as downloaded + @type string: str + + @return: private key or None + """ + leap_assert(string, "We need something to load") + + pkey = None + try: + pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, string) + except Exception as e: + logger.error("Something went wrong while loading the certificate: %r" + % (e,)) + return pkey + + +def get_digest(cert_data, method): + """ + Returns the digest for the cert_data using the method specified + + @param cert_data: certificate data in string form + @type cert_data: str + @param method: method to be used for digest + @type method: str + + @rtype: str + """ + x509 = get_cert_from_string(cert_data) + digest = x509.digest(method).replace(":", "").lower() + + return digest + + +def can_load_cert_and_pkey(string): + """ + Loads certificate and private key from a buffer, returns True if + everything went well, False otherwise + + @param string: buffer containing the cert and private key + @type string: str or any kind of buffer + + @rtype: bool + """ + can_load = True + + try: + cert = get_cert_from_string(string) + key = get_privatekey_from_string(string) + + leap_assert(cert, 'The certificate could not be loaded') + leap_assert(key, 'The private key could not be loaded') + except Exception as e: + can_load = False + logger.error("Something went wrong while trying to load " + "the certificate: %r" % (e,)) + + return can_load + + +def is_valid_pemfile(cert): + """ + Checks that the passed string is a valid pem certificate + + @param cert: String containing pem content + @type cert: str + + @rtype: bool + """ + leap_assert(cert, "We need a cert to load") + + return can_load_cert_and_pkey(cert) + + +def get_cert_time_boundaries(certfile): + """ + Returns the time boundaries for the certificate saved in certfile + + @param certfile: path to certificate + @type certfile: str + + @rtype: tuple (from, to) + """ + cert = get_cert_from_string(certfile) + leap_assert(cert, 'There was a problem loading the certificate') + + fromts, tots = (cert.get_notBefore(), cert.get_notAfter()) + from_, to_ = map( + lambda ts: time.gmtime(time.mktime(dateparse(ts).timetuple())), + (fromts, tots)) + return from_, to_ + + +def should_redownload(certfile, now=time.gmtime): + """ + Returns True if any of the checks don't pass, False otherwise + + @param certfile: path to certificate + @type certfile: str + @param now: current date function, ONLY USED FOR TESTING + + @rtype: bool + """ + exists = os.path.isfile(certfile) + + if not exists: + return True + + certdata = None + try: + with open(certfile, "r") as f: + certdata = f.read() + if not is_valid_pemfile(certdata): + return True + except: + return True + + valid_from, valid_to = get_cert_time_boundaries(certdata) + + if not (valid_from < now() < valid_to): + return True + + return False diff --git a/src/leap/common/check.py b/src/leap/common/check.py new file mode 100644 index 00000000..9787341a --- /dev/null +++ b/src/leap/common/check.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# check.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Set of functions to help checking situations +""" +import logging +import inspect +import traceback + + +logger = logging.getLogger(__name__) + + +def leap_assert(condition, message=""): + """ + Asserts the condition and displays the message if that's not + met. It also logs the error and its backtrace. + + @param condition: condition to check + @type condition: bool + @param message: message to display if the condition isn't met + @type message: str + """ + if not condition: + logger.error("Bug: %s" % (message,)) + try: + frame = inspect.currentframe() + stack_trace = traceback.format_stack(frame) + logger.error(''.join(stack_trace)) + except Exception as e: + logger.error("Bug in leap_assert: %r" % (e,)) + assert condition, message + + +def leap_assert_type(var, expectedType): + """ + Helper assert check for a variable's expected type + + @param var: variable to check + @type var: any + @param expectedType: type to check agains + @type expectedType: type + """ + leap_assert(isinstance(var, expectedType), + "Expected type %r instead of %r" % + (expectedType, type(var))) diff --git a/src/leap/common/files.py b/src/leap/common/files.py new file mode 100644 index 00000000..7c878e1d --- /dev/null +++ b/src/leap/common/files.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# files.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Implements file helper methods +""" + +import os +import stat +import logging +import time +import errno + +logger = logging.getLogger(__name__) + + +def check_and_fix_urw_only(cert): + """ + Test for 600 mode and try to set it if anything different found + + Might raise OSError + + @param cert: Certificate path + @type cert: str + """ + mode = stat.S_IMODE(os.stat(cert).st_mode) + + if mode != int('600', 8): + try: + logger.warning('Bad permission on %s attempting to set 600' % + (cert,)) + os.chmod(cert, stat.S_IRUSR | stat.S_IWUSR) + except OSError: + logger.error('Error while trying to chmod 600 %s' % + cert) + raise + + +def get_mtime(filename): + """ + Returns the modified time or None if the file doesn't exist + + @param filename: path to check + @type filename: str + + @rtype: str + """ + try: + mtime = time.ctime(os.path.getmtime(filename)) + " GMT" + return mtime + except OSError: + return None + + +def mkdir_p(path): + """ + Creates the path and all the intermediate directories that don't + exist + + Might raise OSError + + @param path: path to create + @type path: str + """ + try: + os.makedirs(path) + except OSError as exc: + if exc.errno == errno.EEXIST and os.path.isdir(path): + pass + else: + raise diff --git a/src/leap/common/testing/__init__.py b/src/leap/common/testing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/common/testing/basetest.py b/src/leap/common/testing/basetest.py new file mode 100644 index 00000000..2359754f --- /dev/null +++ b/src/leap/common/testing/basetest.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# leap.common.testing.basetest.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Common testing facilities +""" +import os +import platform +import shutil +import tempfile + +try: + import unittest2 as unittest +except ImportError: + import unittest + +from leap.common.files import mkdir_p, check_and_fix_urw_only + +_system = platform.system() + + +class BaseLeapTest(unittest.TestCase): + """ + Base Leap TestCase + """ + + __name__ = "leap_test" + + @classmethod + def setUpClass(cls): + cls.old_path = os.environ['PATH'] + cls.old_home = os.environ['HOME'] + cls.tempdir = tempfile.mkdtemp(prefix="leap_tests-") + cls.home = cls.tempdir + bin_tdir = os.path.join( + cls.tempdir, + 'bin') + os.environ["PATH"] = bin_tdir + os.environ["HOME"] = cls.tempdir + + @classmethod + def tearDownClass(cls): + os.environ["PATH"] = cls.old_path + os.environ["HOME"] = cls.old_home + # safety check + assert cls.tempdir.startswith('/tmp/leap_tests-') + shutil.rmtree(cls.tempdir) + + # you have to override these methods + # this way we ensure we did not put anything + # here that you can forget to call. + + def setUp(self): + raise NotImplementedError("abstract base class") + + def tearDown(self): + raise NotImplementedError("abstract base class") + + # + # helper methods + # + + def get_tempfile(self, filename): + return os.path.join(self.tempdir, filename) + + def _missing_test_for_plat(self, do_raise=False): + if do_raise: + raise NotImplementedError( + "This test is not implemented " + "for the running platform: %s" % + _system) + + def touch(self, filepath): + folder, filename = os.path.split(filepath) + if not os.path.isdir(folder): + mkdir_p(folder) + # XXX should move to test_basetest + self.assertTrue(os.path.isdir(folder)) + + with open(filepath, 'w') as fp: + fp.write(' ') + + # XXX should move to test_basetest + self.assertTrue(os.path.isfile(filepath)) + + def chmod600(self, filepath): + check_and_fix_urw_only(filepath) diff --git a/src/leap/common/testing/cacert.pem b/src/leap/common/testing/cacert.pem new file mode 100644 index 00000000..6989c480 --- /dev/null +++ b/src/leap/common/testing/cacert.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID1TCCAr2gAwIBAgIJAOv0BS09D8byMA0GCSqGSIb3DQEBBQUAMIGAMQswCQYD +VQQGEwJVUzETMBEGA1UECAwKY3liZXJzcGFjZTEnMCUGA1UECgweTEVBUCBFbmNy +eXB0aW9uIEFjY2VzcyBQcm9qZWN0MRYwFAYDVQQDDA10ZXN0cy1sZWFwLnNlMRsw +GQYJKoZIhvcNAQkBFgxpbmZvQGxlYXAuc2UwHhcNMTIwODMxMTYyNjMwWhcNMTUw +ODMxMTYyNjMwWjCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCmN5YmVyc3BhY2Ux +JzAlBgNVBAoMHkxFQVAgRW5jcnlwdGlvbiBBY2Nlc3MgUHJvamVjdDEWMBQGA1UE +AwwNdGVzdHMtbGVhcC5zZTEbMBkGCSqGSIb3DQEJARYMaW5mb0BsZWFwLnNlMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1pU7OU+abrUXFZwp6X0LlF0f +xQvC1Nmr5sFH7N9RTu3bdwY2t57ECP2TPkH6+x7oOvCTgAMxIE1scWEEkfgKViqW +FH/Om1UW1PMaiDYGtFuqEuxM95FvaYxp2K6rzA37WNsedA28sCYzhRD+/5HqbCNT +3rRS2cPaVO8kXI/5bgd8bUk3009pWTg4SvTtOW/9MWJbBH5f5JWmMn7Ayt6hIdT/ +E6npofEK/UCqAlEscARYFXSB/F8nK1whjo9mGFjMUd7d/25UbFHqOk4K7ishD4DH +F7LaS84rS+Sjwn3YtDdDQblGghJfz8X1AfPSGivGnvLVdkmMF9Y2hJlSQ7+C5wID +AQABo1AwTjAdBgNVHQ4EFgQUnpJEv4FnlqKbfm7mprudKdrnOAowHwYDVR0jBBgw +FoAUnpJEv4FnlqKbfm7mprudKdrnOAowDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B +AQUFAAOCAQEAGW66qwdK/ATRVZkTpI2sgi+2dWD5tY4VyZuJIrRwfXsGPeVvmdsa +zDmwW5dMkth1Of5yO6o7ijvUvfnw/UCLNLNICKZhH5G0DHstfBeFc0jnP2MqOZCp +puRGPBlO2nxUCvoGcPRUKGQK9XSYmxcmaSFyzKVDMLnmH+Lakj5vaY9a8ZAcZTz7 +T5qePxKAxg+RIlH8Ftc485QP3fhqPYPrRsL3g6peiqCvIRshoP1MSoh19boI+1uX +wHQ/NyDkL5ErKC5JCSpaeF8VG1ek570kKWQLuQAbnlXZw+Sqfu35CIdizHaYGEcx +xA8oXH4L2JaT2x9GKDSpCmB2xXy/NVamUg== +-----END CERTIFICATE----- diff --git a/src/leap/common/testing/https_server.py b/src/leap/common/testing/https_server.py new file mode 100644 index 00000000..08d5089d --- /dev/null +++ b/src/leap/common/testing/https_server.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# leap.common.testing.https_server.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +A simple HTTPS server to be used in tests +""" +from BaseHTTPServer import HTTPServer +import os +import ssl +import SocketServer +import threading +import unittest + +_where = os.path.split(__file__)[0] + + +def where(filename): + return os.path.join(_where, filename) + + +class HTTPSServer(HTTPServer): + def server_bind(self): + SocketServer.TCPServer.server_bind(self) + self.socket = ssl.wrap_socket( + self.socket, server_side=True, + certfile=where("leaptestscert.pem"), + keyfile=where("leaptestskey.pem"), + ca_certs=where("cacert.pem"), + ssl_version=ssl.PROTOCOL_SSLv23) + + +class TestServerThread(threading.Thread): + def __init__(self, test_object, request_handler): + threading.Thread.__init__(self) + self.request_handler = request_handler + self.test_object = test_object + + def run(self): + self.server = HTTPSServer(('localhost', 0), self.request_handler) + host, port = self.server.socket.getsockname() + self.test_object.HOST, self.test_object.PORT = host, port + self.test_object.server_started.set() + self.test_object = None + try: + self.server.serve_forever(0.05) + finally: + self.server.server_close() + + def stop(self): + self.server.shutdown() + + +class BaseHTTPSServerTestCase(unittest.TestCase): + """ + derived classes need to implement a request_handler + """ + def setUp(self): + self.server_started = threading.Event() + self.thread = TestServerThread(self, self.request_handler) + self.thread.start() + self.server_started.wait() + + def tearDown(self): + self.thread.stop() + + def get_server(self): + host, port = self.HOST, self.PORT + if host == "127.0.0.1": + host = "localhost" + return "%s:%s" % (host, port) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/common/testing/leaptestscert.pem b/src/leap/common/testing/leaptestscert.pem new file mode 100644 index 00000000..65596b1a --- /dev/null +++ b/src/leap/common/testing/leaptestscert.pem @@ -0,0 +1,84 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + eb:f4:05:2d:3d:0f:c6:f3 + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, ST=cyberspace, O=LEAP Encryption Access Project, CN=tests-leap.se/emailAddress=info@leap.se + Validity + Not Before: Aug 31 16:30:17 2012 GMT + Not After : Aug 31 16:30:17 2013 GMT + Subject: C=US, ST=cyberspace, L=net, O=LEAP Encryption Access Project, CN=localhost/emailAddress=info@leap.se + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:bc:f1:c4:05:ce:4b:d5:9b:9a:fa:c1:a5:0c:89: + 15:7e:05:69:b6:a4:62:38:3a:d6:14:4a:36:aa:3c: + 31:70:54:2e:bf:7d:05:19:ad:7b:0c:a9:a6:7d:46: + be:83:62:cb:ea:b9:48:6c:7d:78:a0:10:0b:ad:8a: + 74:7a:b8:ff:32:85:64:36:90:dc:38:dd:90:6e:07: + 82:70:ae:5f:4e:1f:f4:46:98:f3:98:b4:fa:08:65: + bf:d6:ec:a9:ba:7e:a8:f0:40:a2:d0:1a:cb:e6:fc: + 95:c5:54:63:92:5b:b8:0a:36:cc:26:d3:2b:ad:16: + ff:49:53:f4:65:7c:64:27:9a:f5:12:75:11:a5:0c: + 5a:ea:1e:e4:31:f3:a6:2b:db:0e:4a:5d:aa:47:3a: + f0:5e:2a:d5:6f:74:b6:f8:bc:9a:73:d0:fa:8a:be: + a8:69:47:9b:07:45:d9:b5:cd:1c:9b:c5:41:9a:65: + cc:99:a0:bd:bf:b5:e8:9f:66:5f:69:c9:6d:c8:68: + 50:68:74:ae:8e:12:7e:9c:24:4f:dc:05:61:b7:8a: + 6d:2a:95:43:d9:3f:fe:d8:c9:a7:ae:63:cd:30:d5: + 95:84:18:2d:12:b5:2d:a6:fe:37:dd:74:b8:f8:a5: + 59:18:8f:ca:f7:ae:63:0d:9d:66:51:7d:9c:40:48: + 9b:a1 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + B2:50:B4:C6:38:8F:BA:C4:3B:69:4C:6B:45:7C:CF:08:48:36:02:E0 + X509v3 Authority Key Identifier: + keyid:9E:92:44:BF:81:67:96:A2:9B:7E:6E:E6:A6:BB:9D:29:DA:E7:38:0A + + Signature Algorithm: sha1WithRSAEncryption + aa:ab:d4:27:e3:cb:42:05:55:fd:24:b3:e5:55:7d:fb:ce:6c: + ff:c7:96:f0:7d:30:a1:53:4a:04:eb:a4:24:5e:96:ee:65:ef: + e5:aa:08:47:9d:aa:95:2a:bb:6a:28:9f:51:62:63:d9:7d:1a: + 81:a0:72:f7:9f:33:6b:3b:f4:dc:85:cd:2a:ee:83:a9:93:3d: + 75:53:91:fa:0b:1b:10:83:11:2c:03:4e:ac:bf:c3:e6:25:74: + 9f:14:13:4a:43:66:c2:d7:1c:6c:94:3e:a6:f3:a5:bd:01:2c: + 9f:20:29:2e:62:82:12:d8:8b:70:1b:88:2b:18:68:5a:45:80: + 46:2a:6a:d5:df:1f:d3:e8:57:39:0a:be:1a:d8:b0:3e:e5:b6: + c3:69:b7:5e:c0:7b:b3:a8:a6:78:ee:0a:3d:a0:74:40:fb:42: + 9f:f4:98:7f:47:cc:15:28:eb:b1:95:77:82:a8:65:9b:46:c3: + 4f:f9:f4:72:be:bd:24:28:5c:0d:b3:89:e4:13:71:c8:a7:54: + 1b:26:15:f3:c1:b2:a9:13:77:54:c2:b9:b0:c7:24:39:00:4c: + 1a:a7:9b:e7:ad:4a:3a:32:c2:81:0d:13:2d:27:ea:98:00:a9: + 0e:9e:38:3b:8f:80:34:17:17:3d:49:7e:f4:a5:19:05:28:08: + 7d:de:d3:1f +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAOv0BS09D8bzMA0GCSqGSIb3DQEBBQUAMIGAMQswCQYD +VQQGEwJVUzETMBEGA1UECAwKY3liZXJzcGFjZTEnMCUGA1UECgweTEVBUCBFbmNy +eXB0aW9uIEFjY2VzcyBQcm9qZWN0MRYwFAYDVQQDDA10ZXN0cy1sZWFwLnNlMRsw +GQYJKoZIhvcNAQkBFgxpbmZvQGxlYXAuc2UwHhcNMTIwODMxMTYzMDE3WhcNMTMw +ODMxMTYzMDE3WjCBijELMAkGA1UEBhMCVVMxEzARBgNVBAgMCmN5YmVyc3BhY2Ux +DDAKBgNVBAcMA25ldDEnMCUGA1UECgweTEVBUCBFbmNyeXB0aW9uIEFjY2VzcyBQ +cm9qZWN0MRIwEAYDVQQDDAlsb2NhbGhvc3QxGzAZBgkqhkiG9w0BCQEWDGluZm9A +bGVhcC5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALzxxAXOS9Wb +mvrBpQyJFX4FabakYjg61hRKNqo8MXBULr99BRmtewyppn1GvoNiy+q5SGx9eKAQ +C62KdHq4/zKFZDaQ3DjdkG4HgnCuX04f9EaY85i0+ghlv9bsqbp+qPBAotAay+b8 +lcVUY5JbuAo2zCbTK60W/0lT9GV8ZCea9RJ1EaUMWuoe5DHzpivbDkpdqkc68F4q +1W90tvi8mnPQ+oq+qGlHmwdF2bXNHJvFQZplzJmgvb+16J9mX2nJbchoUGh0ro4S +fpwkT9wFYbeKbSqVQ9k//tjJp65jzTDVlYQYLRK1Lab+N910uPilWRiPyveuYw2d +ZlF9nEBIm6ECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3Bl +blNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFLJQtMY4j7rEO2lM +a0V8zwhINgLgMB8GA1UdIwQYMBaAFJ6SRL+BZ5aim35u5qa7nSna5zgKMA0GCSqG +SIb3DQEBBQUAA4IBAQCqq9Qn48tCBVX9JLPlVX37zmz/x5bwfTChU0oE66QkXpbu +Ze/lqghHnaqVKrtqKJ9RYmPZfRqBoHL3nzNrO/Tchc0q7oOpkz11U5H6CxsQgxEs +A06sv8PmJXSfFBNKQ2bC1xxslD6m86W9ASyfICkuYoIS2ItwG4grGGhaRYBGKmrV +3x/T6Fc5Cr4a2LA+5bbDabdewHuzqKZ47go9oHRA+0Kf9Jh/R8wVKOuxlXeCqGWb +RsNP+fRyvr0kKFwNs4nkE3HIp1QbJhXzwbKpE3dUwrmwxyQ5AEwap5vnrUo6MsKB +DRMtJ+qYAKkOnjg7j4A0Fxc9SX70pRkFKAh93tMf +-----END CERTIFICATE----- diff --git a/src/leap/common/testing/leaptestskey.pem b/src/leap/common/testing/leaptestskey.pem new file mode 100644 index 00000000..fe6291a1 --- /dev/null +++ b/src/leap/common/testing/leaptestskey.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAvPHEBc5L1Zua+sGlDIkVfgVptqRiODrWFEo2qjwxcFQuv30F +Ga17DKmmfUa+g2LL6rlIbH14oBALrYp0erj/MoVkNpDcON2QbgeCcK5fTh/0Rpjz +mLT6CGW/1uypun6o8ECi0BrL5vyVxVRjklu4CjbMJtMrrRb/SVP0ZXxkJ5r1EnUR +pQxa6h7kMfOmK9sOSl2qRzrwXirVb3S2+Lyac9D6ir6oaUebB0XZtc0cm8VBmmXM +maC9v7Xon2ZfacltyGhQaHSujhJ+nCRP3AVht4ptKpVD2T/+2MmnrmPNMNWVhBgt +ErUtpv433XS4+KVZGI/K965jDZ1mUX2cQEiboQIDAQABAoIBAQCh/+yhSbrtoCgm +PegEsnix/3QfPBxWt+Obq/HozglZlWQrnMbFuF+bgM4V9ZUdU5UhYNF+66mEG53X +orGyE3IDYCmHO3cGbroKDPhDIs7mTjGEYlniIbGLh6oPXgU8uKKis9ik84TGPOUx +NuTUtT07zLYHx+FX3DLwLUKLzTaWWSRgA7nxNwCY8aPqDxCkXEyZHvSlm9KYZnhe +nVevycoHR+chxL6X/ebbBt2FKR7tl4328mlDXvMXr0vahPH94CuXEvfTj+f6ZxZF +OctdikyRfd8O3ebrUw0XjafPYyTsDMH0/rQovEBVlecEHqh6Z9dBFlogRq5DSun9 +jem4bBXRAoGBAPGPi4g21pTQPqTFxpqea8TsPqIfo3csfMDPdzT246MxzALHqCfG +yZi4g2JYJrReSWHulZDORO5skSKNEb5VTA/3xFhKLt8CULZOakKBDLkzRXlnDFXg +Jsu9vtjDWjQcJsdsRx1tc5V6s+hmel70aaUu/maUlEYZnyIXaTe+1SB1AoGBAMg9 +EMEO5YN52pOI5qPH8j7uyVKtZWKRiR6jb5KA5TxWqZalSdPV6YwDqV/e+HjWrZNw +kSEFONY0seKpIHwXchx91aym7rDHUgOoBQfCWufRMYvRXLhfOTBu4X+U52++i8wt +FvKgh6eSmc7VayAaDfHp7yfrIfS03IiN0T35mGj9AoGAPCoXg7a83VW8tId5/trE +VsjMlM6yhSU0cUV7GFsBuYzWlj6qODX/0iTqvFzeTwBI4LZu1CE78/Jgd62RJMnT +5wo8Ag1//RVziuSe/K9tvtbxT9qFrQHmR8qbtRt65Q257uOeFstDBZEJLDIR+oJ/ +qZ+5x0zsXUVWaERSdYr3RF0CgYEApKDgN3oB5Ti4Jnh1984aMver+heptYKmU9RX +lQH4dsVhpQO8UTgcTgtso+/0JZWLHB9+ksFyW1rzrcETfjLglOA4XzzYHeuiWHM5 +v4lhqBpsO+Ij80oHAPUI3RYVud/VnEauCUlGftWfM1hwPPJu6KhHAnDleAWDE5pV +oDinwBkCgYEAnn/OceaqA2fNYp1IRegbFzpewjUlHLq3bXiCIVhO7W/HqsdfUxjE +VVdjEno/pAG7ZCO5j8u+rLkG2ZIVY3qsUENUiXz52Q08qEltgM8nfirK7vIQkfd9 +YISRE3QHYJd+ArY4v+7rNeF1O5eIEyzPAbvG5raeZFcZ6POxy66uWKo= +-----END RSA PRIVATE KEY----- diff --git a/src/leap/common/testing/test_basetest.py b/src/leap/common/testing/test_basetest.py new file mode 100644 index 00000000..3674185a --- /dev/null +++ b/src/leap/common/testing/test_basetest.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# leap.common.testing.test_basetest +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Unittests for base test +...becase it's oh so meta""" +try: + import unittest2 as unittest +except ImportError: + import unittest + +import os +import StringIO + +from leap.testing.basetest import BaseLeapTest + +# global for tempdir checking +_tempdir = None + + +class _TestCaseRunner(object): + def run_testcase(self, testcase=None): + if not testcase: + return None + loader = unittest.TestLoader() + suite = loader.loadTestsFromTestCase(testcase) + + # Create runner, and run testcase + io = StringIO.StringIO() + runner = unittest.TextTestRunner(stream=io) + results = runner.run(suite) + return results + + +class TestAbstractBaseLeapTest(unittest.TestCase, _TestCaseRunner): + + def test_abstract_base_class(self): + class _BaseTest(BaseLeapTest): + def test_dummy_method(self): + pass + + def test_tautology(self): + assert True + + results = self.run_testcase(_BaseTest) + + # should be 2 errors: NotImplemented + # raised for setUp/tearDown + self.assertEquals(results.testsRun, 2) + self.assertEquals(len(results.failures), 0) + self.assertEquals(len(results.errors), 2) + + +class TestInitBaseLeapTest(BaseLeapTest): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_path_is_changed(self): + os_path = os.environ['PATH'] + self.assertTrue(os_path.startswith(self.tempdir)) + + def test_old_path_is_saved(self): + self.assertTrue(len(self.old_path) > 1) + + +class TestCleanedBaseLeapTest(unittest.TestCase, _TestCaseRunner): + + def test_tempdir_is_cleaned_after_tests(self): + class _BaseTest(BaseLeapTest): + def setUp(self): + global _tempdir + _tempdir = self.tempdir + + def tearDown(self): + pass + + def test_tempdir_created(self): + self.assertTrue(os.path.isdir(self.tempdir)) + + def test_tempdir_created_on_setupclass(self): + self.assertEqual(_tempdir, self.tempdir) + + results = self.run_testcase(_BaseTest) + self.assertEquals(results.testsRun, 2) + self.assertEquals(len(results.failures), 0) + self.assertEquals(len(results.errors), 0) + + # did we cleaned the tempdir? + self.assertFalse(os.path.isdir(_tempdir)) + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/config/baseconfig.py b/src/leap/config/baseconfig.py index 90529042..c497d156 100644 --- a/src/leap/config/baseconfig.py +++ b/src/leap/config/baseconfig.py @@ -19,17 +19,17 @@ Implements the abstract base class for configuration """ +import copy import logging import functools import os -import copy from abc import ABCMeta, abstractmethod -from leap.config.prefixers import get_platform_prefixer +from leap.common.check import leap_assert +from leap.common.files import mkdir_p from leap.config.pluggableconfig import PluggableConfig -from leap.util.check import leap_assert -from leap.util.files import mkdir_p +from leap.config.prefixers import get_platform_prefixer logger = logging.getLogger(__name__) diff --git a/src/leap/config/pluggableconfig.py b/src/leap/config/pluggableconfig.py index 5ed83b3f..4a742da4 100644 --- a/src/leap/config/pluggableconfig.py +++ b/src/leap/config/pluggableconfig.py @@ -28,7 +28,7 @@ import urlparse import jsonschema #from leap.base.util.translations import LEAPTranslatable -from leap.util.check import leap_assert +from leap.common.check import leap_assert logger = logging.getLogger(__name__) diff --git a/src/leap/config/prefixers.py b/src/leap/config/prefixers.py index 64c36908..dc00b5b6 100644 --- a/src/leap/config/prefixers.py +++ b/src/leap/config/prefixers.py @@ -18,13 +18,13 @@ """ Platform dependant configuration path prefixers """ +from abc import ABCMeta, abstractmethod import os import platform -from abc import ABCMeta, abstractmethod from xdg import BaseDirectory -from leap.util.check import leap_assert +from leap.common.check import leap_assert class Prefixer: diff --git a/src/leap/config/providerconfig.py b/src/leap/config/providerconfig.py index 55b33b98..71b2856f 100644 --- a/src/leap/config/providerconfig.py +++ b/src/leap/config/providerconfig.py @@ -21,9 +21,9 @@ Provider configuration import logging import os +from leap.common.check import leap_assert from leap.config.baseconfig import BaseConfig, LocalizedKey from leap.config.provider_spec import leap_provider_spec -from leap.util.check import leap_assert logger = logging.getLogger(__name__) diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index e9c72408..152d77b5 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -23,8 +23,8 @@ import srp from PySide import QtCore, QtGui +from leap.common.check import leap_assert from leap.config.providerconfig import ProviderConfig -from leap.util.check import leap_assert from leap.util.checkerthread import CheckerThread logger = logging.getLogger(__name__) diff --git a/src/leap/crypto/srpregister.py b/src/leap/crypto/srpregister.py index c99f79ab..9a9cac76 100644 --- a/src/leap/crypto/srpregister.py +++ b/src/leap/crypto/srpregister.py @@ -26,7 +26,7 @@ from urlparse import urlparse from leap.config.providerconfig import ProviderConfig from leap.crypto.constants import SIGNUP_TIMEOUT -from leap.util.check import leap_assert, leap_assert_type +from leap.common.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 4db74c2d..a93b2e3c 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -27,6 +27,7 @@ from PySide import QtCore, QtGui from functools import partial from ui_mainwindow import Ui_MainWindow +from leap.common.check import leap_assert from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth from leap.services.eip.vpn import VPN @@ -38,9 +39,8 @@ from leap.services.eip.providerbootstrapper import ProviderBootstrapper from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig from leap.gui.wizard import Wizard -from leap.util.check import leap_assert from leap.util.checkerthread import CheckerThread -from leap import __version__ as VERSION +from leap.util import __version__ as VERSION logger = logging.getLogger(__name__) @@ -325,13 +325,14 @@ class MainWindow(QtGui.QMainWindow): """ QtGui.QMessageBox.about( self, self.tr("About LEAP - %s") % (VERSION,), - self.tr("LEAP is a non-profit dedicated to giving " + self.tr("version: %s
" + "LEAP is a non-profit dedicated to giving " "all internet users access to secure " "communication. Our focus is on adapting " "encryption technology to make it easy to use " "and widely available. " "More about LEAP" - "")) + "") % (VERSION,)) def quit(self): self._really_quit = True diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index c83cb1b5..19b74856 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -19,20 +19,21 @@ EIP bootstrapping """ -import requests import logging import os +import requests + from PySide import QtGui, QtCore -from leap.crypto.srpauth import SRPAuth +from leap.common.check import leap_assert, leap_assert_type +from leap.common.certs import is_valid_pemfile, should_redownload +from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpauth import SRPAuth from leap.services.eip.eipconfig import EIPConfig -from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread -from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content -from leap.util.certs import is_valid_pemfile, should_redownload logger = logging.getLogger(__name__) diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index eab5bfd4..3f873878 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -21,10 +21,10 @@ Provider configuration import os import logging +from leap.common.check import leap_assert, leap_assert_type from leap.config.baseconfig import BaseConfig from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipspec import eipservice_config_spec -from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 40ec55c5..778d5149 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -18,20 +18,20 @@ """ Provider bootstrapping """ - -import requests import logging import socket import os +import requests + from PySide import QtGui, QtCore +from leap.common.certs import get_digest +from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p +from leap.common.check import leap_assert, leap_assert_type from leap.config.providerconfig import ProviderConfig -from leap.util.check import leap_assert, leap_assert_type from leap.util.checkerthread import CheckerThread -from leap.util.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.util.request_helpers import get_content -from leap.util.certs import get_digest logger = logging.getLogger(__name__) diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index dd42cd13..66b39dd9 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -26,11 +26,11 @@ import psutil from PySide import QtCore, QtGui from functools import partial +from leap.common.check import leap_assert, leap_assert_type from leap.config.providerconfig import ProviderConfig from leap.services.eip.vpnlaunchers import get_platform_launcher from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.udstelnet import UDSTelnet -from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index c646da7d..f9e8e366 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -18,18 +18,19 @@ """ Platform dependant VPN launchers """ -import os -import platform -import logging import commands +import logging import getpass import grp +import os +import platform from abc import ABCMeta, abstractmethod +from leap.common.check import leap_assert, leap_assert_type +from leap.common.files import which from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig -from leap.util.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -86,48 +87,6 @@ def get_platform_launcher(): return launcher() -# Twisted implementation of which -def which(name, flags=os.X_OK, path_extension="/usr/sbin:/sbin"): - """ - Search PATH for executable files with the given name. - - On newer versions of MS-Windows, the PATHEXT environment variable will be - set to the list of file extensions for files considered executable. This - will normally include things like ".EXE". This fuction will also find files - with the given name ending with any of these extensions. - - On MS-Windows the only flag that has any meaning is os.F_OK. Any other - flags will be ignored. - - @type name: C{str} - @param name: The name for which to search. - - @type flags: C{int} - @param flags: Arguments to L{os.access}. - - @rtype: C{list} - @param: A list of the full paths to files found, in the - order in which they were found. - """ - - result = [] - exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep)) - path = os.environ.get('PATH', None) - path += ":" + path_extension - if path is None: - return [] - parts = path.split(os.pathsep) - for p in parts: - p = os.path.join(p, name) - if os.access(p, flags): - result.append(p) - for e in exts: - pext = p + e - if os.access(pext, flags): - result.append(pext) - return result - - def _is_pkexec_in_system(): pkexec_path = which('pkexec') if len(pkexec_path) == 0: diff --git a/src/leap/util/__init__.py b/src/leap/util/__init__.py index e69de29b..a4e49ae5 100644 --- a/src/leap/util/__init__.py +++ b/src/leap/util/__init__.py @@ -0,0 +1,29 @@ +""" +LEAP Encryption Access Project +website: U{https://leap.se/} +""" + +__version__ = "unknown" +try: + from leap._version import get_versions + __version__ = get_versions()['version'] + del get_versions +except ImportError: + #running on a tree that has not run + #the setup.py setver + pass + +__appname__ = "unknown" +try: + from leap._appname import __appname__ +except ImportError: + #running on a tree that has not run + #the setup.py setver + pass + +__full_version__ = __appname__ + '/' + str(__version__) + +# try: +# from leap._branding import BRANDING as __branding +# except ImportError: +# __branding = {} diff --git a/src/leap/util/certs.py b/src/leap/util/certs.py deleted file mode 100644 index 63c60c3d..00000000 --- a/src/leap/util/certs.py +++ /dev/null @@ -1,179 +0,0 @@ -# -*- coding: utf-8 -*- -# certs.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Implements cert checks and helpers -""" - -import os -import time -import logging - -from OpenSSL import crypto -from dateutil.parser import parse as dateparse - -from leap.util.check import leap_assert - -logger = logging.getLogger(__name__) - - -def get_cert_from_string(string): - """ - Returns the x509 from the contents of this string - - @param string: certificate contents as downloaded - @type string: str - - @return: x509 or None - """ - leap_assert(string, "We need something to load") - - x509 = None - try: - x509 = crypto.load_certificate(crypto.FILETYPE_PEM, string) - except Exception as e: - logger.error("Something went wrong while loading the certificate: %r" - % (e,)) - return x509 - - -def get_privatekey_from_string(string): - """ - Returns the private key from the contents of this string - - @param string: private key contents as downloaded - @type string: str - - @return: private key or None - """ - leap_assert(string, "We need something to load") - - pkey = None - try: - pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, string) - except Exception as e: - logger.error("Something went wrong while loading the certificate: %r" - % (e,)) - return pkey - - -def get_digest(cert_data, method): - """ - Returns the digest for the cert_data using the method specified - - @param cert_data: certificate data in string form - @type cert_data: str - @param method: method to be used for digest - @type method: str - - @rtype: str - """ - x509 = get_cert_from_string(cert_data) - digest = x509.digest(method).replace(":", "").lower() - - return digest - - -def can_load_cert_and_pkey(string): - """ - Loads certificate and private key from a buffer, returns True if - everything went well, False otherwise - - @param string: buffer containing the cert and private key - @type string: str or any kind of buffer - - @rtype: bool - """ - can_load = True - - try: - cert = get_cert_from_string(string) - key = get_privatekey_from_string(string) - - leap_assert(cert, 'The certificate could not be loaded') - leap_assert(key, 'The private key could not be loaded') - except Exception as e: - can_load = False - logger.error("Something went wrong while trying to load " - "the certificate: %r" % (e,)) - - return can_load - - -def is_valid_pemfile(cert): - """ - Checks that the passed string is a valid pem certificate - - @param cert: String containing pem content - @type cert: str - - @rtype: bool - """ - leap_assert(cert, "We need a cert to load") - - return can_load_cert_and_pkey(cert) - - -def get_cert_time_boundaries(certfile): - """ - Returns the time boundaries for the certificate saved in certfile - - @param certfile: path to certificate - @type certfile: str - - @rtype: tuple (from, to) - """ - cert = get_cert_from_string(certfile) - leap_assert(cert, 'There was a problem loading the certificate') - - fromts, tots = (cert.get_notBefore(), cert.get_notAfter()) - from_, to_ = map( - lambda ts: time.gmtime(time.mktime(dateparse(ts).timetuple())), - (fromts, tots)) - return from_, to_ - - -def should_redownload(certfile, now=time.gmtime): - """ - Returns True if any of the checks don't pass, False otherwise - - @param certfile: path to certificate - @type certfile: str - @param now: current date function, ONLY USED FOR TESTING - - @rtype: bool - """ - exists = os.path.isfile(certfile) - - if not exists: - return True - - certdata = None - try: - with open(certfile, "r") as f: - certdata = f.read() - if not is_valid_pemfile(certdata): - return True - except: - return True - - valid_from, valid_to = get_cert_time_boundaries(certdata) - - if not (valid_from < now() < valid_to): - return True - - return False diff --git a/src/leap/util/check.py b/src/leap/util/check.py deleted file mode 100644 index 9787341a..00000000 --- a/src/leap/util/check.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -# check.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Set of functions to help checking situations -""" -import logging -import inspect -import traceback - - -logger = logging.getLogger(__name__) - - -def leap_assert(condition, message=""): - """ - Asserts the condition and displays the message if that's not - met. It also logs the error and its backtrace. - - @param condition: condition to check - @type condition: bool - @param message: message to display if the condition isn't met - @type message: str - """ - if not condition: - logger.error("Bug: %s" % (message,)) - try: - frame = inspect.currentframe() - stack_trace = traceback.format_stack(frame) - logger.error(''.join(stack_trace)) - except Exception as e: - logger.error("Bug in leap_assert: %r" % (e,)) - assert condition, message - - -def leap_assert_type(var, expectedType): - """ - Helper assert check for a variable's expected type - - @param var: variable to check - @type var: any - @param expectedType: type to check agains - @type expectedType: type - """ - leap_assert(isinstance(var, expectedType), - "Expected type %r instead of %r" % - (expectedType, type(var))) diff --git a/src/leap/util/checkerthread.py b/src/leap/util/checkerthread.py index 0e69eca3..47a96ec5 100644 --- a/src/leap/util/checkerthread.py +++ b/src/leap/util/checkerthread.py @@ -23,7 +23,7 @@ import logging from PySide import QtCore -from leap.util.check import leap_assert_type +from leap.common.check import leap_assert_type logger = logging.getLogger(__name__) diff --git a/src/leap/util/files.py b/src/leap/util/files.py deleted file mode 100644 index 7c878e1d..00000000 --- a/src/leap/util/files.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -# files.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Implements file helper methods -""" - -import os -import stat -import logging -import time -import errno - -logger = logging.getLogger(__name__) - - -def check_and_fix_urw_only(cert): - """ - Test for 600 mode and try to set it if anything different found - - Might raise OSError - - @param cert: Certificate path - @type cert: str - """ - mode = stat.S_IMODE(os.stat(cert).st_mode) - - if mode != int('600', 8): - try: - logger.warning('Bad permission on %s attempting to set 600' % - (cert,)) - os.chmod(cert, stat.S_IRUSR | stat.S_IWUSR) - except OSError: - logger.error('Error while trying to chmod 600 %s' % - cert) - raise - - -def get_mtime(filename): - """ - Returns the modified time or None if the file doesn't exist - - @param filename: path to check - @type filename: str - - @rtype: str - """ - try: - mtime = time.ctime(os.path.getmtime(filename)) + " GMT" - return mtime - except OSError: - return None - - -def mkdir_p(path): - """ - Creates the path and all the intermediate directories that don't - exist - - Might raise OSError - - @param path: path to create - @type path: str - """ - try: - os.makedirs(path) - except OSError as exc: - if exc.errno == errno.EEXIST and os.path.isdir(path): - pass - else: - raise -- cgit v1.2.3 From 5aea858b422b8034dfe77bdcfa8649862b567381 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 14 Mar 2013 09:59:11 +0900 Subject: remove common module! from now on this will live in a common repo, leap_common. We still need to add it to requirements --- src/leap/common/README | 5 - src/leap/common/__init__.py | 9 -- src/leap/common/certs.py | 179 ------------------------------ src/leap/common/check.py | 61 ---------- src/leap/common/files.py | 85 -------------- src/leap/common/testing/__init__.py | 0 src/leap/common/testing/basetest.py | 100 ----------------- src/leap/common/testing/cacert.pem | 23 ---- src/leap/common/testing/https_server.py | 87 --------------- src/leap/common/testing/leaptestscert.pem | 84 -------------- src/leap/common/testing/leaptestskey.pem | 27 ----- src/leap/common/testing/test_basetest.py | 109 ------------------ 12 files changed, 769 deletions(-) delete mode 100644 src/leap/common/README delete mode 100644 src/leap/common/__init__.py delete mode 100644 src/leap/common/certs.py delete mode 100644 src/leap/common/check.py delete mode 100644 src/leap/common/files.py delete mode 100644 src/leap/common/testing/__init__.py delete mode 100644 src/leap/common/testing/basetest.py delete mode 100644 src/leap/common/testing/cacert.pem delete mode 100644 src/leap/common/testing/https_server.py delete mode 100644 src/leap/common/testing/leaptestscert.pem delete mode 100644 src/leap/common/testing/leaptestskey.pem delete mode 100644 src/leap/common/testing/test_basetest.py (limited to 'src') diff --git a/src/leap/common/README b/src/leap/common/README deleted file mode 100644 index 040fa2b6..00000000 --- a/src/leap/common/README +++ /dev/null @@ -1,5 +0,0 @@ -for the record, I'm gathering here, on this feature branch, -all what I want to have on the new splitted package "leap.common" - -This is going to be deleted at the end of this feature branch, -and will move to leap_common repo. diff --git a/src/leap/common/__init__.py b/src/leap/common/__init__.py deleted file mode 100644 index a70a9a8b..00000000 --- a/src/leap/common/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -import logging -logger = logging.getLogger(__name__) - -try: - import pygeoip - HAS_GEOIP = True -except ImportError: - logger.debug('PyGeoIP not found. Disabled Geo support.') - HAS_GEOIP = False diff --git a/src/leap/common/certs.py b/src/leap/common/certs.py deleted file mode 100644 index 4cb70ddf..00000000 --- a/src/leap/common/certs.py +++ /dev/null @@ -1,179 +0,0 @@ -# -*- coding: utf-8 -*- -# certs.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Implements cert checks and helpers -""" - -import os -import time -import logging - -from OpenSSL import crypto -from dateutil.parser import parse as dateparse - -from leap.common.check import leap_assert - -logger = logging.getLogger(__name__) - - -def get_cert_from_string(string): - """ - Returns the x509 from the contents of this string - - @param string: certificate contents as downloaded - @type string: str - - @return: x509 or None - """ - leap_assert(string, "We need something to load") - - x509 = None - try: - x509 = crypto.load_certificate(crypto.FILETYPE_PEM, string) - except Exception as e: - logger.error("Something went wrong while loading the certificate: %r" - % (e,)) - return x509 - - -def get_privatekey_from_string(string): - """ - Returns the private key from the contents of this string - - @param string: private key contents as downloaded - @type string: str - - @return: private key or None - """ - leap_assert(string, "We need something to load") - - pkey = None - try: - pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, string) - except Exception as e: - logger.error("Something went wrong while loading the certificate: %r" - % (e,)) - return pkey - - -def get_digest(cert_data, method): - """ - Returns the digest for the cert_data using the method specified - - @param cert_data: certificate data in string form - @type cert_data: str - @param method: method to be used for digest - @type method: str - - @rtype: str - """ - x509 = get_cert_from_string(cert_data) - digest = x509.digest(method).replace(":", "").lower() - - return digest - - -def can_load_cert_and_pkey(string): - """ - Loads certificate and private key from a buffer, returns True if - everything went well, False otherwise - - @param string: buffer containing the cert and private key - @type string: str or any kind of buffer - - @rtype: bool - """ - can_load = True - - try: - cert = get_cert_from_string(string) - key = get_privatekey_from_string(string) - - leap_assert(cert, 'The certificate could not be loaded') - leap_assert(key, 'The private key could not be loaded') - except Exception as e: - can_load = False - logger.error("Something went wrong while trying to load " - "the certificate: %r" % (e,)) - - return can_load - - -def is_valid_pemfile(cert): - """ - Checks that the passed string is a valid pem certificate - - @param cert: String containing pem content - @type cert: str - - @rtype: bool - """ - leap_assert(cert, "We need a cert to load") - - return can_load_cert_and_pkey(cert) - - -def get_cert_time_boundaries(certfile): - """ - Returns the time boundaries for the certificate saved in certfile - - @param certfile: path to certificate - @type certfile: str - - @rtype: tuple (from, to) - """ - cert = get_cert_from_string(certfile) - leap_assert(cert, 'There was a problem loading the certificate') - - fromts, tots = (cert.get_notBefore(), cert.get_notAfter()) - from_, to_ = map( - lambda ts: time.gmtime(time.mktime(dateparse(ts).timetuple())), - (fromts, tots)) - return from_, to_ - - -def should_redownload(certfile, now=time.gmtime): - """ - Returns True if any of the checks don't pass, False otherwise - - @param certfile: path to certificate - @type certfile: str - @param now: current date function, ONLY USED FOR TESTING - - @rtype: bool - """ - exists = os.path.isfile(certfile) - - if not exists: - return True - - certdata = None - try: - with open(certfile, "r") as f: - certdata = f.read() - if not is_valid_pemfile(certdata): - return True - except: - return True - - valid_from, valid_to = get_cert_time_boundaries(certdata) - - if not (valid_from < now() < valid_to): - return True - - return False diff --git a/src/leap/common/check.py b/src/leap/common/check.py deleted file mode 100644 index 9787341a..00000000 --- a/src/leap/common/check.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -# check.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Set of functions to help checking situations -""" -import logging -import inspect -import traceback - - -logger = logging.getLogger(__name__) - - -def leap_assert(condition, message=""): - """ - Asserts the condition and displays the message if that's not - met. It also logs the error and its backtrace. - - @param condition: condition to check - @type condition: bool - @param message: message to display if the condition isn't met - @type message: str - """ - if not condition: - logger.error("Bug: %s" % (message,)) - try: - frame = inspect.currentframe() - stack_trace = traceback.format_stack(frame) - logger.error(''.join(stack_trace)) - except Exception as e: - logger.error("Bug in leap_assert: %r" % (e,)) - assert condition, message - - -def leap_assert_type(var, expectedType): - """ - Helper assert check for a variable's expected type - - @param var: variable to check - @type var: any - @param expectedType: type to check agains - @type expectedType: type - """ - leap_assert(isinstance(var, expectedType), - "Expected type %r instead of %r" % - (expectedType, type(var))) diff --git a/src/leap/common/files.py b/src/leap/common/files.py deleted file mode 100644 index 7c878e1d..00000000 --- a/src/leap/common/files.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -# files.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Implements file helper methods -""" - -import os -import stat -import logging -import time -import errno - -logger = logging.getLogger(__name__) - - -def check_and_fix_urw_only(cert): - """ - Test for 600 mode and try to set it if anything different found - - Might raise OSError - - @param cert: Certificate path - @type cert: str - """ - mode = stat.S_IMODE(os.stat(cert).st_mode) - - if mode != int('600', 8): - try: - logger.warning('Bad permission on %s attempting to set 600' % - (cert,)) - os.chmod(cert, stat.S_IRUSR | stat.S_IWUSR) - except OSError: - logger.error('Error while trying to chmod 600 %s' % - cert) - raise - - -def get_mtime(filename): - """ - Returns the modified time or None if the file doesn't exist - - @param filename: path to check - @type filename: str - - @rtype: str - """ - try: - mtime = time.ctime(os.path.getmtime(filename)) + " GMT" - return mtime - except OSError: - return None - - -def mkdir_p(path): - """ - Creates the path and all the intermediate directories that don't - exist - - Might raise OSError - - @param path: path to create - @type path: str - """ - try: - os.makedirs(path) - except OSError as exc: - if exc.errno == errno.EEXIST and os.path.isdir(path): - pass - else: - raise diff --git a/src/leap/common/testing/__init__.py b/src/leap/common/testing/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/leap/common/testing/basetest.py b/src/leap/common/testing/basetest.py deleted file mode 100644 index 2359754f..00000000 --- a/src/leap/common/testing/basetest.py +++ /dev/null @@ -1,100 +0,0 @@ -# -*- coding: utf-8 -*- -# leap.common.testing.basetest.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -Common testing facilities -""" -import os -import platform -import shutil -import tempfile - -try: - import unittest2 as unittest -except ImportError: - import unittest - -from leap.common.files import mkdir_p, check_and_fix_urw_only - -_system = platform.system() - - -class BaseLeapTest(unittest.TestCase): - """ - Base Leap TestCase - """ - - __name__ = "leap_test" - - @classmethod - def setUpClass(cls): - cls.old_path = os.environ['PATH'] - cls.old_home = os.environ['HOME'] - cls.tempdir = tempfile.mkdtemp(prefix="leap_tests-") - cls.home = cls.tempdir - bin_tdir = os.path.join( - cls.tempdir, - 'bin') - os.environ["PATH"] = bin_tdir - os.environ["HOME"] = cls.tempdir - - @classmethod - def tearDownClass(cls): - os.environ["PATH"] = cls.old_path - os.environ["HOME"] = cls.old_home - # safety check - assert cls.tempdir.startswith('/tmp/leap_tests-') - shutil.rmtree(cls.tempdir) - - # you have to override these methods - # this way we ensure we did not put anything - # here that you can forget to call. - - def setUp(self): - raise NotImplementedError("abstract base class") - - def tearDown(self): - raise NotImplementedError("abstract base class") - - # - # helper methods - # - - def get_tempfile(self, filename): - return os.path.join(self.tempdir, filename) - - def _missing_test_for_plat(self, do_raise=False): - if do_raise: - raise NotImplementedError( - "This test is not implemented " - "for the running platform: %s" % - _system) - - def touch(self, filepath): - folder, filename = os.path.split(filepath) - if not os.path.isdir(folder): - mkdir_p(folder) - # XXX should move to test_basetest - self.assertTrue(os.path.isdir(folder)) - - with open(filepath, 'w') as fp: - fp.write(' ') - - # XXX should move to test_basetest - self.assertTrue(os.path.isfile(filepath)) - - def chmod600(self, filepath): - check_and_fix_urw_only(filepath) diff --git a/src/leap/common/testing/cacert.pem b/src/leap/common/testing/cacert.pem deleted file mode 100644 index 6989c480..00000000 --- a/src/leap/common/testing/cacert.pem +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID1TCCAr2gAwIBAgIJAOv0BS09D8byMA0GCSqGSIb3DQEBBQUAMIGAMQswCQYD -VQQGEwJVUzETMBEGA1UECAwKY3liZXJzcGFjZTEnMCUGA1UECgweTEVBUCBFbmNy -eXB0aW9uIEFjY2VzcyBQcm9qZWN0MRYwFAYDVQQDDA10ZXN0cy1sZWFwLnNlMRsw -GQYJKoZIhvcNAQkBFgxpbmZvQGxlYXAuc2UwHhcNMTIwODMxMTYyNjMwWhcNMTUw -ODMxMTYyNjMwWjCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCmN5YmVyc3BhY2Ux -JzAlBgNVBAoMHkxFQVAgRW5jcnlwdGlvbiBBY2Nlc3MgUHJvamVjdDEWMBQGA1UE -AwwNdGVzdHMtbGVhcC5zZTEbMBkGCSqGSIb3DQEJARYMaW5mb0BsZWFwLnNlMIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1pU7OU+abrUXFZwp6X0LlF0f -xQvC1Nmr5sFH7N9RTu3bdwY2t57ECP2TPkH6+x7oOvCTgAMxIE1scWEEkfgKViqW -FH/Om1UW1PMaiDYGtFuqEuxM95FvaYxp2K6rzA37WNsedA28sCYzhRD+/5HqbCNT -3rRS2cPaVO8kXI/5bgd8bUk3009pWTg4SvTtOW/9MWJbBH5f5JWmMn7Ayt6hIdT/ -E6npofEK/UCqAlEscARYFXSB/F8nK1whjo9mGFjMUd7d/25UbFHqOk4K7ishD4DH -F7LaS84rS+Sjwn3YtDdDQblGghJfz8X1AfPSGivGnvLVdkmMF9Y2hJlSQ7+C5wID -AQABo1AwTjAdBgNVHQ4EFgQUnpJEv4FnlqKbfm7mprudKdrnOAowHwYDVR0jBBgw -FoAUnpJEv4FnlqKbfm7mprudKdrnOAowDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B -AQUFAAOCAQEAGW66qwdK/ATRVZkTpI2sgi+2dWD5tY4VyZuJIrRwfXsGPeVvmdsa -zDmwW5dMkth1Of5yO6o7ijvUvfnw/UCLNLNICKZhH5G0DHstfBeFc0jnP2MqOZCp -puRGPBlO2nxUCvoGcPRUKGQK9XSYmxcmaSFyzKVDMLnmH+Lakj5vaY9a8ZAcZTz7 -T5qePxKAxg+RIlH8Ftc485QP3fhqPYPrRsL3g6peiqCvIRshoP1MSoh19boI+1uX -wHQ/NyDkL5ErKC5JCSpaeF8VG1ek570kKWQLuQAbnlXZw+Sqfu35CIdizHaYGEcx -xA8oXH4L2JaT2x9GKDSpCmB2xXy/NVamUg== ------END CERTIFICATE----- diff --git a/src/leap/common/testing/https_server.py b/src/leap/common/testing/https_server.py deleted file mode 100644 index 08d5089d..00000000 --- a/src/leap/common/testing/https_server.py +++ /dev/null @@ -1,87 +0,0 @@ -# -*- coding: utf-8 -*- -# leap.common.testing.https_server.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -A simple HTTPS server to be used in tests -""" -from BaseHTTPServer import HTTPServer -import os -import ssl -import SocketServer -import threading -import unittest - -_where = os.path.split(__file__)[0] - - -def where(filename): - return os.path.join(_where, filename) - - -class HTTPSServer(HTTPServer): - def server_bind(self): - SocketServer.TCPServer.server_bind(self) - self.socket = ssl.wrap_socket( - self.socket, server_side=True, - certfile=where("leaptestscert.pem"), - keyfile=where("leaptestskey.pem"), - ca_certs=where("cacert.pem"), - ssl_version=ssl.PROTOCOL_SSLv23) - - -class TestServerThread(threading.Thread): - def __init__(self, test_object, request_handler): - threading.Thread.__init__(self) - self.request_handler = request_handler - self.test_object = test_object - - def run(self): - self.server = HTTPSServer(('localhost', 0), self.request_handler) - host, port = self.server.socket.getsockname() - self.test_object.HOST, self.test_object.PORT = host, port - self.test_object.server_started.set() - self.test_object = None - try: - self.server.serve_forever(0.05) - finally: - self.server.server_close() - - def stop(self): - self.server.shutdown() - - -class BaseHTTPSServerTestCase(unittest.TestCase): - """ - derived classes need to implement a request_handler - """ - def setUp(self): - self.server_started = threading.Event() - self.thread = TestServerThread(self, self.request_handler) - self.thread.start() - self.server_started.wait() - - def tearDown(self): - self.thread.stop() - - def get_server(self): - host, port = self.HOST, self.PORT - if host == "127.0.0.1": - host = "localhost" - return "%s:%s" % (host, port) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/common/testing/leaptestscert.pem b/src/leap/common/testing/leaptestscert.pem deleted file mode 100644 index 65596b1a..00000000 --- a/src/leap/common/testing/leaptestscert.pem +++ /dev/null @@ -1,84 +0,0 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: - eb:f4:05:2d:3d:0f:c6:f3 - Signature Algorithm: sha1WithRSAEncryption - Issuer: C=US, ST=cyberspace, O=LEAP Encryption Access Project, CN=tests-leap.se/emailAddress=info@leap.se - Validity - Not Before: Aug 31 16:30:17 2012 GMT - Not After : Aug 31 16:30:17 2013 GMT - Subject: C=US, ST=cyberspace, L=net, O=LEAP Encryption Access Project, CN=localhost/emailAddress=info@leap.se - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (2048 bit) - Modulus: - 00:bc:f1:c4:05:ce:4b:d5:9b:9a:fa:c1:a5:0c:89: - 15:7e:05:69:b6:a4:62:38:3a:d6:14:4a:36:aa:3c: - 31:70:54:2e:bf:7d:05:19:ad:7b:0c:a9:a6:7d:46: - be:83:62:cb:ea:b9:48:6c:7d:78:a0:10:0b:ad:8a: - 74:7a:b8:ff:32:85:64:36:90:dc:38:dd:90:6e:07: - 82:70:ae:5f:4e:1f:f4:46:98:f3:98:b4:fa:08:65: - bf:d6:ec:a9:ba:7e:a8:f0:40:a2:d0:1a:cb:e6:fc: - 95:c5:54:63:92:5b:b8:0a:36:cc:26:d3:2b:ad:16: - ff:49:53:f4:65:7c:64:27:9a:f5:12:75:11:a5:0c: - 5a:ea:1e:e4:31:f3:a6:2b:db:0e:4a:5d:aa:47:3a: - f0:5e:2a:d5:6f:74:b6:f8:bc:9a:73:d0:fa:8a:be: - a8:69:47:9b:07:45:d9:b5:cd:1c:9b:c5:41:9a:65: - cc:99:a0:bd:bf:b5:e8:9f:66:5f:69:c9:6d:c8:68: - 50:68:74:ae:8e:12:7e:9c:24:4f:dc:05:61:b7:8a: - 6d:2a:95:43:d9:3f:fe:d8:c9:a7:ae:63:cd:30:d5: - 95:84:18:2d:12:b5:2d:a6:fe:37:dd:74:b8:f8:a5: - 59:18:8f:ca:f7:ae:63:0d:9d:66:51:7d:9c:40:48: - 9b:a1 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Basic Constraints: - CA:FALSE - Netscape Comment: - OpenSSL Generated Certificate - X509v3 Subject Key Identifier: - B2:50:B4:C6:38:8F:BA:C4:3B:69:4C:6B:45:7C:CF:08:48:36:02:E0 - X509v3 Authority Key Identifier: - keyid:9E:92:44:BF:81:67:96:A2:9B:7E:6E:E6:A6:BB:9D:29:DA:E7:38:0A - - Signature Algorithm: sha1WithRSAEncryption - aa:ab:d4:27:e3:cb:42:05:55:fd:24:b3:e5:55:7d:fb:ce:6c: - ff:c7:96:f0:7d:30:a1:53:4a:04:eb:a4:24:5e:96:ee:65:ef: - e5:aa:08:47:9d:aa:95:2a:bb:6a:28:9f:51:62:63:d9:7d:1a: - 81:a0:72:f7:9f:33:6b:3b:f4:dc:85:cd:2a:ee:83:a9:93:3d: - 75:53:91:fa:0b:1b:10:83:11:2c:03:4e:ac:bf:c3:e6:25:74: - 9f:14:13:4a:43:66:c2:d7:1c:6c:94:3e:a6:f3:a5:bd:01:2c: - 9f:20:29:2e:62:82:12:d8:8b:70:1b:88:2b:18:68:5a:45:80: - 46:2a:6a:d5:df:1f:d3:e8:57:39:0a:be:1a:d8:b0:3e:e5:b6: - c3:69:b7:5e:c0:7b:b3:a8:a6:78:ee:0a:3d:a0:74:40:fb:42: - 9f:f4:98:7f:47:cc:15:28:eb:b1:95:77:82:a8:65:9b:46:c3: - 4f:f9:f4:72:be:bd:24:28:5c:0d:b3:89:e4:13:71:c8:a7:54: - 1b:26:15:f3:c1:b2:a9:13:77:54:c2:b9:b0:c7:24:39:00:4c: - 1a:a7:9b:e7:ad:4a:3a:32:c2:81:0d:13:2d:27:ea:98:00:a9: - 0e:9e:38:3b:8f:80:34:17:17:3d:49:7e:f4:a5:19:05:28:08: - 7d:de:d3:1f ------BEGIN CERTIFICATE----- -MIIECjCCAvKgAwIBAgIJAOv0BS09D8bzMA0GCSqGSIb3DQEBBQUAMIGAMQswCQYD -VQQGEwJVUzETMBEGA1UECAwKY3liZXJzcGFjZTEnMCUGA1UECgweTEVBUCBFbmNy -eXB0aW9uIEFjY2VzcyBQcm9qZWN0MRYwFAYDVQQDDA10ZXN0cy1sZWFwLnNlMRsw -GQYJKoZIhvcNAQkBFgxpbmZvQGxlYXAuc2UwHhcNMTIwODMxMTYzMDE3WhcNMTMw -ODMxMTYzMDE3WjCBijELMAkGA1UEBhMCVVMxEzARBgNVBAgMCmN5YmVyc3BhY2Ux -DDAKBgNVBAcMA25ldDEnMCUGA1UECgweTEVBUCBFbmNyeXB0aW9uIEFjY2VzcyBQ -cm9qZWN0MRIwEAYDVQQDDAlsb2NhbGhvc3QxGzAZBgkqhkiG9w0BCQEWDGluZm9A -bGVhcC5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALzxxAXOS9Wb -mvrBpQyJFX4FabakYjg61hRKNqo8MXBULr99BRmtewyppn1GvoNiy+q5SGx9eKAQ -C62KdHq4/zKFZDaQ3DjdkG4HgnCuX04f9EaY85i0+ghlv9bsqbp+qPBAotAay+b8 -lcVUY5JbuAo2zCbTK60W/0lT9GV8ZCea9RJ1EaUMWuoe5DHzpivbDkpdqkc68F4q -1W90tvi8mnPQ+oq+qGlHmwdF2bXNHJvFQZplzJmgvb+16J9mX2nJbchoUGh0ro4S -fpwkT9wFYbeKbSqVQ9k//tjJp65jzTDVlYQYLRK1Lab+N910uPilWRiPyveuYw2d -ZlF9nEBIm6ECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3Bl -blNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFLJQtMY4j7rEO2lM -a0V8zwhINgLgMB8GA1UdIwQYMBaAFJ6SRL+BZ5aim35u5qa7nSna5zgKMA0GCSqG -SIb3DQEBBQUAA4IBAQCqq9Qn48tCBVX9JLPlVX37zmz/x5bwfTChU0oE66QkXpbu -Ze/lqghHnaqVKrtqKJ9RYmPZfRqBoHL3nzNrO/Tchc0q7oOpkz11U5H6CxsQgxEs -A06sv8PmJXSfFBNKQ2bC1xxslD6m86W9ASyfICkuYoIS2ItwG4grGGhaRYBGKmrV -3x/T6Fc5Cr4a2LA+5bbDabdewHuzqKZ47go9oHRA+0Kf9Jh/R8wVKOuxlXeCqGWb -RsNP+fRyvr0kKFwNs4nkE3HIp1QbJhXzwbKpE3dUwrmwxyQ5AEwap5vnrUo6MsKB -DRMtJ+qYAKkOnjg7j4A0Fxc9SX70pRkFKAh93tMf ------END CERTIFICATE----- diff --git a/src/leap/common/testing/leaptestskey.pem b/src/leap/common/testing/leaptestskey.pem deleted file mode 100644 index fe6291a1..00000000 --- a/src/leap/common/testing/leaptestskey.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAvPHEBc5L1Zua+sGlDIkVfgVptqRiODrWFEo2qjwxcFQuv30F -Ga17DKmmfUa+g2LL6rlIbH14oBALrYp0erj/MoVkNpDcON2QbgeCcK5fTh/0Rpjz -mLT6CGW/1uypun6o8ECi0BrL5vyVxVRjklu4CjbMJtMrrRb/SVP0ZXxkJ5r1EnUR -pQxa6h7kMfOmK9sOSl2qRzrwXirVb3S2+Lyac9D6ir6oaUebB0XZtc0cm8VBmmXM -maC9v7Xon2ZfacltyGhQaHSujhJ+nCRP3AVht4ptKpVD2T/+2MmnrmPNMNWVhBgt -ErUtpv433XS4+KVZGI/K965jDZ1mUX2cQEiboQIDAQABAoIBAQCh/+yhSbrtoCgm -PegEsnix/3QfPBxWt+Obq/HozglZlWQrnMbFuF+bgM4V9ZUdU5UhYNF+66mEG53X -orGyE3IDYCmHO3cGbroKDPhDIs7mTjGEYlniIbGLh6oPXgU8uKKis9ik84TGPOUx -NuTUtT07zLYHx+FX3DLwLUKLzTaWWSRgA7nxNwCY8aPqDxCkXEyZHvSlm9KYZnhe -nVevycoHR+chxL6X/ebbBt2FKR7tl4328mlDXvMXr0vahPH94CuXEvfTj+f6ZxZF -OctdikyRfd8O3ebrUw0XjafPYyTsDMH0/rQovEBVlecEHqh6Z9dBFlogRq5DSun9 -jem4bBXRAoGBAPGPi4g21pTQPqTFxpqea8TsPqIfo3csfMDPdzT246MxzALHqCfG -yZi4g2JYJrReSWHulZDORO5skSKNEb5VTA/3xFhKLt8CULZOakKBDLkzRXlnDFXg -Jsu9vtjDWjQcJsdsRx1tc5V6s+hmel70aaUu/maUlEYZnyIXaTe+1SB1AoGBAMg9 -EMEO5YN52pOI5qPH8j7uyVKtZWKRiR6jb5KA5TxWqZalSdPV6YwDqV/e+HjWrZNw -kSEFONY0seKpIHwXchx91aym7rDHUgOoBQfCWufRMYvRXLhfOTBu4X+U52++i8wt -FvKgh6eSmc7VayAaDfHp7yfrIfS03IiN0T35mGj9AoGAPCoXg7a83VW8tId5/trE -VsjMlM6yhSU0cUV7GFsBuYzWlj6qODX/0iTqvFzeTwBI4LZu1CE78/Jgd62RJMnT -5wo8Ag1//RVziuSe/K9tvtbxT9qFrQHmR8qbtRt65Q257uOeFstDBZEJLDIR+oJ/ -qZ+5x0zsXUVWaERSdYr3RF0CgYEApKDgN3oB5Ti4Jnh1984aMver+heptYKmU9RX -lQH4dsVhpQO8UTgcTgtso+/0JZWLHB9+ksFyW1rzrcETfjLglOA4XzzYHeuiWHM5 -v4lhqBpsO+Ij80oHAPUI3RYVud/VnEauCUlGftWfM1hwPPJu6KhHAnDleAWDE5pV -oDinwBkCgYEAnn/OceaqA2fNYp1IRegbFzpewjUlHLq3bXiCIVhO7W/HqsdfUxjE -VVdjEno/pAG7ZCO5j8u+rLkG2ZIVY3qsUENUiXz52Q08qEltgM8nfirK7vIQkfd9 -YISRE3QHYJd+ArY4v+7rNeF1O5eIEyzPAbvG5raeZFcZ6POxy66uWKo= ------END RSA PRIVATE KEY----- diff --git a/src/leap/common/testing/test_basetest.py b/src/leap/common/testing/test_basetest.py deleted file mode 100644 index 3674185a..00000000 --- a/src/leap/common/testing/test_basetest.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -# leap.common.testing.test_basetest -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -Unittests for base test -...becase it's oh so meta""" -try: - import unittest2 as unittest -except ImportError: - import unittest - -import os -import StringIO - -from leap.testing.basetest import BaseLeapTest - -# global for tempdir checking -_tempdir = None - - -class _TestCaseRunner(object): - def run_testcase(self, testcase=None): - if not testcase: - return None - loader = unittest.TestLoader() - suite = loader.loadTestsFromTestCase(testcase) - - # Create runner, and run testcase - io = StringIO.StringIO() - runner = unittest.TextTestRunner(stream=io) - results = runner.run(suite) - return results - - -class TestAbstractBaseLeapTest(unittest.TestCase, _TestCaseRunner): - - def test_abstract_base_class(self): - class _BaseTest(BaseLeapTest): - def test_dummy_method(self): - pass - - def test_tautology(self): - assert True - - results = self.run_testcase(_BaseTest) - - # should be 2 errors: NotImplemented - # raised for setUp/tearDown - self.assertEquals(results.testsRun, 2) - self.assertEquals(len(results.failures), 0) - self.assertEquals(len(results.errors), 2) - - -class TestInitBaseLeapTest(BaseLeapTest): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_path_is_changed(self): - os_path = os.environ['PATH'] - self.assertTrue(os_path.startswith(self.tempdir)) - - def test_old_path_is_saved(self): - self.assertTrue(len(self.old_path) > 1) - - -class TestCleanedBaseLeapTest(unittest.TestCase, _TestCaseRunner): - - def test_tempdir_is_cleaned_after_tests(self): - class _BaseTest(BaseLeapTest): - def setUp(self): - global _tempdir - _tempdir = self.tempdir - - def tearDown(self): - pass - - def test_tempdir_created(self): - self.assertTrue(os.path.isdir(self.tempdir)) - - def test_tempdir_created_on_setupclass(self): - self.assertEqual(_tempdir, self.tempdir) - - results = self.run_testcase(_BaseTest) - self.assertEquals(results.testsRun, 2) - self.assertEquals(len(results.failures), 0) - self.assertEquals(len(results.errors), 0) - - # did we cleaned the tempdir? - self.assertFalse(os.path.isdir(_tempdir)) - -if __name__ == "__main__": - unittest.main() -- cgit v1.2.3 From 1f4841079a72df874d0624f7908115ded295a82f Mon Sep 17 00:00:00 2001 From: kali Date: Fri, 15 Mar 2013 01:13:30 +0900 Subject: remove branding remnant, add missing header --- src/leap/util/__init__.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/leap/util/__init__.py b/src/leap/util/__init__.py index a4e49ae5..41358d38 100644 --- a/src/leap/util/__init__.py +++ b/src/leap/util/__init__.py @@ -1,6 +1,21 @@ +# -*- coding: utf-8 -*- +# __init__.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . """ -LEAP Encryption Access Project -website: U{https://leap.se/} +Initializes version and app info """ __version__ = "unknown" @@ -22,8 +37,3 @@ except ImportError: pass __full_version__ = __appname__ + '/' + str(__version__) - -# try: -# from leap._branding import BRANDING as __branding -# except ImportError: -# __branding = {} -- cgit v1.2.3 From 5be6aba659b8e7db486e985ac1bb98bdae53233f Mon Sep 17 00:00:00 2001 From: kali Date: Tue, 19 Mar 2013 00:49:54 +0900 Subject: move abc import to its place --- src/leap/config/prefixers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/config/prefixers.py b/src/leap/config/prefixers.py index dc00b5b6..c65d8f53 100644 --- a/src/leap/config/prefixers.py +++ b/src/leap/config/prefixers.py @@ -18,10 +18,11 @@ """ Platform dependant configuration path prefixers """ -from abc import ABCMeta, abstractmethod import os import platform +from abc import ABCMeta, abstractmethod + from xdg import BaseDirectory from leap.common.check import leap_assert -- cgit v1.2.3 From ddbad58fc2b3f44a293eeac7790a06f13b56944a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 20 Mar 2013 16:44:00 -0300 Subject: Display ASSIGN_IP state correctly in the mainwindow --- src/leap/gui/mainwindow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index a93b2e3c..eabdfe57 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -699,7 +699,8 @@ class MainWindow(QtGui.QMainWindow): """ selected_pixmap = self.ERROR_ICON tray_message = self.tr("Encryption is OFF") - if status in ("WAIT", "AUTH", "GET_CONFIG", "RECONNECTING"): + if status in ("WAIT", "AUTH", "GET_CONFIG", + "RECONNECTING", "ASSIGN_IP"): selected_pixmap = self.CONNECTING_ICON elif status in ("CONNECTED"): tray_message = self.tr("Encryption is ON") @@ -727,6 +728,8 @@ class MainWindow(QtGui.QMainWindow): self._set_eip_status(self.tr("VPN: Connected!")) elif status == "WAIT": self._set_eip_status(self.tr("VPN: Waiting to start...")) + elif status == "ASSIGN_IP": + self._set_eip_status(self.tr("VPN: Assigning IP")) elif status == "ALREADYRUNNING": # Put the following calls in Qt's event queue, otherwise # the UI won't update properly -- cgit v1.2.3 From e33081871affdbca197ea77c461b1379b9039117 Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 20 Mar 2013 19:31:24 +0900 Subject: add darwin prefixer and launcher --- src/leap/config/prefixers.py | 22 +++++++ src/leap/services/eip/vpnlaunchers.py | 110 ++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) (limited to 'src') diff --git a/src/leap/config/prefixers.py b/src/leap/config/prefixers.py index c65d8f53..ebcd49e7 100644 --- a/src/leap/config/prefixers.py +++ b/src/leap/config/prefixers.py @@ -77,6 +77,28 @@ class LinuxPrefixer(Prefixer): return os.getenv("LEAP_CLIENT_PATH", config_dir) +class DarwinPrefixer(Prefixer): + """ + Config prefixer for the Darwin platform + """ + + def get_path_prefix(self, standalone=False): + """ + Returns the platform dependant path prefixer. + This method expects an env variable named LEAP_CLIENT_PATH if + standalone is used. + + @param standalone: if True it will return the prefix for a + standalone application. Otherwise, it will return the system + default for configuration storage. + @type standalone: bool + """ + config_dir = BaseDirectory.xdg_config_home + if not standalone: + return config_dir + return os.getenv("LEAP_CLIENT_PATH", config_dir) + + if __name__ == "__main__": try: abs_prefixer = Prefixer() diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index f9e8e366..44dff4d0 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -188,6 +188,115 @@ class LinuxVPNLauncher(VPNLauncher): 'server' ] + openvpn_configuration = eipconfig.get_openvpn_configuration() + + # FIXME: sanitize this! -- + for key, value in openvpn_configuration.items(): + args += ['--%s' % (key,), value] + + args += [ + '--user', getpass.getuser(), + '--group', grp.getgrgid(os.getgroups()[-1]).gr_name + ] + + if socket_port == "unix": + args += [ + '--management-client-user', getpass.getuser() + ] + + args += [ + '--management-signal', + '--management', socket_host, socket_port, + '--script-security', '2' + ] + + if _has_updown_scripts(self.UP_DOWN_SCRIPT): + args += [ + '--up', self.UP_DOWN_SCRIPT, + '--down', self.UP_DOWN_SCRIPT, + '--plugin', self.OPENVPN_DOWN_ROOT, + '\'script_type=down %s\'' % self.UP_DOWN_SCRIPT + ] + + args += [ + '--cert', eipconfig.get_client_cert_path(providerconfig), + '--key', eipconfig.get_client_cert_path(providerconfig), + '--ca', providerconfig.get_ca_cert_path() + ] + + logger.debug("Running VPN with command:") + logger.debug("%s %s" % (openvpn, " ".join(args))) + + return [openvpn] + args + + +class DarwinVPNLauncher(VPNLauncher): + """ + VPN launcher for the Darwin Platform + """ + + OSASCRIPT_BIN = 'osascript' + OSX_ASADMIN = 'do shell script "%s" with administrator privileges' + OPENVPN_BIN = 'openvpn.leap' + INSTALL_PATH = "/Applications/LEAPClient.app/" + # OPENVPN_BIN = "/%s/Contents/Resources/openvpn.leap" % ( + # self.INSTALL_PATH,) + UP_DOWN_SCRIPT = "/etc/leap/resolv-update" + OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" + + def get_vpn_command(self, eipconfig=None, providerconfig=None, + socket_host=None, socket_port="unix"): + """ + Returns the platform dependant vpn launching command + + Might raise VPNException. + + @param eipconfig: eip configuration object + @type eipconfig: EIPConfig + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + @param socket_host: either socket path (unix) or socket IP + @type socket_host: str + @param socket_port: either string "unix" if it's a unix + socket, or port otherwise + @type socket_port: str + + @return: A VPN command ready to be launched + @rtype: list + """ + leap_assert(eipconfig, "We need an eip config") + leap_assert_type(eipconfig, EIPConfig) + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + leap_assert(socket_host, "We need a socket host!") + leap_assert(socket_port, "We need a socket port!") + + openvpn_possibilities = which(self.OPENVPN_BIN) + if len(openvpn_possibilities) == 0: + raise OpenVPNNotFoundException() + + openvpn = openvpn_possibilities[0] + args = [] + + # TODO: handle verbosity + + gateway_ip = str(eipconfig.get_gateway_ip(0)) + + logger.debug("Using gateway ip %s" % (gateway_ip,)) + + args += [ + '--client', + '--dev', 'tun', + '--persist-tun', + '--persist-key', + '--remote', gateway_ip, '1194', 'udp', + '--tls-client', + '--remote-cert-tls', + 'server' + ] + + # FIXME: sanitize this! -- + openvpn_configuration = eipconfig.get_openvpn_configuration() for key, value in openvpn_configuration.items(): args += ['--%s' % (key,), value] @@ -225,6 +334,7 @@ class LinuxVPNLauncher(VPNLauncher): logger.debug("Running VPN with command:") logger.debug("%s %s" % (openvpn, " ".join(args))) + # return [self.OSASCRIPT_BIN, ["-e", self.OSX_ASADMIN % ' '.join(args)]] return [openvpn] + args -- cgit v1.2.3 From 74600d1c73cfc2f958f2c64d57ea8fb85d348424 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 20 Mar 2013 21:06:59 +0900 Subject: syntax fix for python 2.6 --- src/leap/gui/wizard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 4e811fb9..dee3b230 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -435,10 +435,10 @@ class Wizard(QtGui.QWizard): """ if state == QtCore.Qt.Checked: self._selected_services = \ - self._selected_services.union({service}) + self._selected_services.union(set([service])) else: self._selected_services = \ - self._selected_services.difference({service}) + self._selected_services.difference(set([service])) def _populate_services(self): """ -- cgit v1.2.3 From 85166f4d12fbaafa5ab1cd5fca5ad37a4ba94774 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 20 Mar 2013 23:48:07 +0900 Subject: fix osascript invocation --- src/leap/services/eip/vpn.py | 11 +++++++++++ src/leap/services/eip/vpnlaunchers.py | 36 +++++++++++++++++++++-------------- 2 files changed, 33 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 66b39dd9..9a60cf73 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -159,6 +159,7 @@ class VPN(QtCore.QThread): try: self._subp = QtCore.QProcess() self._subp.finished.connect(self.process_finished) + self._subp.finished.connect(self._dump_exitinfo) self._subp.start(command[:1][0], command[1:]) logger.debug("Waiting for started...") self._subp.waitForStarted() @@ -174,6 +175,16 @@ class VPN(QtCore.QThread): logger.warning("Something went wrong while starting OpenVPN: %r" % (e,)) + def _dump_exitinfo(self): + """ + SLOT + Prints debug info when quitting the process + + TRIGGER: self._subp.finished + """ + logger.debug("stdout: %s", self._subp.readAllStandardOutput()) + logger.debug("stderr: %s", self._subp.readAllStandardError()) + def _get_openvpn_process(self): """ Looks for openvpn instances running diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 44dff4d0..773b1f92 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -235,14 +235,17 @@ class DarwinVPNLauncher(VPNLauncher): VPN launcher for the Darwin Platform """ - OSASCRIPT_BIN = 'osascript' - OSX_ASADMIN = 'do shell script "%s" with administrator privileges' + OSASCRIPT_BIN = '/usr/bin/osascript' + OSX_ASADMIN = "do shell script \"%s\" with administrator privileges" OPENVPN_BIN = 'openvpn.leap' INSTALL_PATH = "/Applications/LEAPClient.app/" # OPENVPN_BIN = "/%s/Contents/Resources/openvpn.leap" % ( # self.INSTALL_PATH,) - UP_DOWN_SCRIPT = "/etc/leap/resolv-update" - OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" + UP_SCRIPT = "/%s/client.up.sh" % (INSTALL_PATH,) + DOWN_SCRIPT = "/%s/client.down.sh" % (INSTALL_PATH,) + + # TODO: Add + # OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="unix"): @@ -276,12 +279,11 @@ class DarwinVPNLauncher(VPNLauncher): raise OpenVPNNotFoundException() openvpn = openvpn_possibilities[0] - args = [] + args = [openvpn] # TODO: handle verbosity gateway_ip = str(eipconfig.get_gateway_ip(0)) - logger.debug("Using gateway ip %s" % (gateway_ip,)) args += [ @@ -317,12 +319,16 @@ class DarwinVPNLauncher(VPNLauncher): '--script-security', '2' ] - if _has_updown_scripts(self.UP_DOWN_SCRIPT): + if _has_updown_scripts(self.UP_SCRIPT): args += [ - '--up', self.UP_DOWN_SCRIPT, - '--down', self.UP_DOWN_SCRIPT, - '--plugin', self.OPENVPN_DOWN_ROOT, - '\'script_type=down %s\'' % self.UP_DOWN_SCRIPT + '--up', self.UP_SCRIPT, + ] + if _has_updown_scripts(self.DOWN_SCRIPT): + args += [ + '--down', self.DOWN_SCRIPT, + # FIXME add down-plugin + # '--plugin', self.OPENVPN_DOWN_ROOT, + # '\'script_type=down %s\'' % self.DOWN_SCRIPT ] args += [ @@ -331,11 +337,13 @@ class DarwinVPNLauncher(VPNLauncher): '--ca', providerconfig.get_ca_cert_path() ] + command = self.OSASCRIPT_BIN + cmd_args = ["-e", self.OSX_ASADMIN % (' '.join(args),)] + logger.debug("Running VPN with command:") - logger.debug("%s %s" % (openvpn, " ".join(args))) + logger.debug("%s %s" % (command, " ".join(cmd_args))) - # return [self.OSASCRIPT_BIN, ["-e", self.OSX_ASADMIN % ' '.join(args)]] - return [openvpn] + args + return [command] + cmd_args if __name__ == "__main__": -- cgit v1.2.3 From 8f54774f6c3f779527718a0158ebd0efc4aab588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 15 Mar 2013 13:30:01 -0300 Subject: Handle configuration and paths in a standalone way Also, abstracts QSettings under LeapSettings and adds a way to define the VPN env in a platform dependant way. --- src/leap/app.py | 3 +- src/leap/config/baseconfig.py | 20 ++-- src/leap/config/leapsettings.py | 186 ++++++++++++++++++++++++++++++++++ src/leap/config/prefixers.py | 2 +- src/leap/gui/mainwindow.py | 69 ++++++------- src/leap/services/eip/vpn.py | 7 ++ src/leap/services/eip/vpnlaunchers.py | 38 ++++++- src/leap/util/leap_argparse.py | 4 + 8 files changed, 281 insertions(+), 48 deletions(-) create mode 100644 src/leap/config/leapsettings.py (limited to 'src') diff --git a/src/leap/app.py b/src/leap/app.py index 9f4d4614..14d3c69c 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -42,6 +42,7 @@ def main(): _, opts = leap_argparse.init_leapc_args() debug = opts.debug + standalone = opts.standalone # TODO: get severity from command line args if debug: @@ -93,7 +94,7 @@ def main(): # TODO: check if the leap-client is already running and quit # gracefully in that case. - window = MainWindow() + window = MainWindow(standalone) window.show() # This dummy timer ensures that control is given to the outside diff --git a/src/leap/config/baseconfig.py b/src/leap/config/baseconfig.py index c497d156..f5c07184 100644 --- a/src/leap/config/baseconfig.py +++ b/src/leap/config/baseconfig.py @@ -41,6 +41,16 @@ class BaseConfig: __metaclass__ = ABCMeta + """ + Standalone is a class wide parameter + + @param standalone: if True it will return the prefix for a + standalone application. Otherwise, it will return the system + default for configuration storage. + @type standalone: bool + """ + standalone = False + def __init__(self): self._data = {} self._config_checker = None @@ -62,16 +72,13 @@ class BaseConfig: leap_assert(self._config_checker, "Load the config first") return self._config_checker.config[key] - def get_path_prefix(self, standalone=False): + def get_path_prefix(self): """ Returns the platform dependant path prefixer - @param standalone: if True it will return the prefix for a - standalone application. Otherwise, it will return the system - default for configuration storage. - @type standalone: bool """ - return get_platform_prefixer().get_path_prefix(standalone=standalone) + return get_platform_prefixer().get_path_prefix( + standalone=self.standalone) def loaded(self): """ @@ -113,7 +120,6 @@ class BaseConfig: @return: True if loaded from disk correctly, False otherwise """ - # TODO: retrieve standalone option from app-level config config_path = os.path.join(self.get_path_prefix(), path) diff --git a/src/leap/config/leapsettings.py b/src/leap/config/leapsettings.py new file mode 100644 index 00000000..4f12b4f8 --- /dev/null +++ b/src/leap/config/leapsettings.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +# leapsettings.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +QSettings abstraction +""" +import os +import logging + +from PySide import QtCore + +from leap.config.prefixers import get_platform_prefixer +from leap.common.check import leap_assert, leap_assert_type + +logger = logging.getLogger(__name__) + + +class LeapSettings(object): + """ + Leap client QSettings wrapper + """ + + CONFIG_NAME = "leap.conf" + + # keys + GEOMETRY_KEY = "Geometry" + WINDOWSTATE_KEY = "WindowState" + USER_KEY = "User" + AUTOLOGIN_KEY = "AutoLogin" + PROPERPROVIDER_KEY = "ProperProvider" + + def __init__(self, standalone=False): + """ + Constructor + + @param standalone: parameter used to define the location of + the config + @type standalone: bool + """ + + settings_path = os.path.join(get_platform_prefixer() + .get_path_prefix(standalone=standalone), + self.CONFIG_NAME) + self._settings = QtCore.QSettings(settings_path, + QtCore.QSettings.IniFormat) + + def get_geometry(self): + """ + Returns the saved geometry or None if it wasn't saved + + @rtype: bytearray or None + """ + return self._settings.value(self.GEOMETRY_KEY, None) + + def set_geometry(self, geometry): + """ + Saves the geometry to the settings + + @param geometry: bytearray representing the geometry + @type geometry: bytearray + """ + leap_assert(geometry, "We need a geometry") + self._settings.setValue(self.GEOMETRY_KEY, geometry) + + def get_windowstate(self): + """ + Returns the window state or None if it wasn't saved + + @rtype: bytearray or None + """ + return self._settings.value(self.WINDOWSTATE_KEY, None) + + def set_windowstate(self, windowstate): + """ + Saves the window state to the settings + + @param windowstate: bytearray representing the window state + @type windowstate: bytearray + """ + leap_assert(windowstate, "We need a window state") + self._settings.setValue(self.WINDOWSTATE_KEY, windowstate) + + def get_enabled_services(self, provider): + """ + Returns a list of enabled services for the given provider + + @param provider: provider domain + @type provider: str + + @rtype: list of str + """ + + leap_assert(len(provider) > 0, "We need a nonempty provider") + enabled_services = self._settings.value("%s/Services" % (provider,), + []) + if isinstance(enabled_services, (str, unicode)): + enabled_services = enabled_services.split(",") + + return enabled_services + + def set_enabled_services(self, provider, services): + """ + Saves the list of enabled services for the given provider + + @param provider: provider domain + @type provider: str + @param services: list of services to save + @type services: list of str + """ + + leap_assert(len(provider) > 0, "We need a nonempty provider") + leap_assert_type(services, list) + + self._settings.setValue("%s/Services" % (provider,), + services) + + def get_user(self): + """ + Returns the configured user to remember, None if there isn't one + + @rtype: str or None + """ + return self._settings.value(self.USER_KEY, None) + + def set_user(self, user): + """ + Saves the user to remember + + @param user: user name to remember + @type user: str + """ + leap_assert(len(user) > 0, "We cannot save an empty user") + self._settings.setValue(self.USER_KEY, user) + + def get_autologin(self): + """ + Returns True if the app should automatically login, False otherwise + + @rtype: bool + """ + return self._settings.value(self.AUTOLOGIN_KEY, "false") != "false" + + def set_autologin(self, autologin): + """ + Sets wether the app should automatically login + + @param autologin: True if the app should autologin, False otherwise + @type autologin: bool + """ + leap_assert_type(autologin, bool) + self._settings.setValue(self.AUTOLOGIN_KEY, autologin) + + # TODO: make this scale with multiple providers, we are assuming + # just one for now + def get_properprovider(self): + """ + Returns True if there is a properly configured provider + + @rtype: bool + """ + return self._settings.value(self.PROPERPROVIDER_KEY, + "false") != "false" + + def set_properprovider(self, properprovider): + """ + Sets wether the app should automatically login + + @param autologin: True if the app should autologin, False otherwise + @type autologin: bool + """ + leap_assert_type(properprovider, bool) + self._settings.setValue(self.PROPERPROVIDER_KEY, properprovider) diff --git a/src/leap/config/prefixers.py b/src/leap/config/prefixers.py index c65d8f53..557a77ac 100644 --- a/src/leap/config/prefixers.py +++ b/src/leap/config/prefixers.py @@ -74,7 +74,7 @@ class LinuxPrefixer(Prefixer): config_dir = BaseDirectory.xdg_config_home if not standalone: return config_dir - return os.getenv("LEAP_CLIENT_PATH", config_dir) + return os.path.join(os.getcwd(), "config") if __name__ == "__main__": diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index eabdfe57..703d1e26 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -29,6 +29,7 @@ from functools import partial from ui_mainwindow import Ui_MainWindow from leap.common.check import leap_assert from leap.config.providerconfig import ProviderConfig +from leap.config.leapsettings import LeapSettings from leap.crypto.srpauth import SRPAuth from leap.services.eip.vpn import VPN from leap.services.eip.vpnlaunchers import (VPNLauncherException, @@ -54,17 +55,17 @@ class MainWindow(QtGui.QMainWindow): LOGIN_INDEX = 0 EIP_STATUS_INDEX = 1 - # Settings - GEOMETRY_KEY = "Geometry" - WINDOWSTATE_KEY = "WindowState" - USER_KEY = "User" - AUTOLOGIN_KEY = "AutoLogin" - PROPER_PROVIDER = "ProperProvider" - # Keyring KEYRING_KEY = "leap_client" - def __init__(self): + def __init__(self, standalone=False): + """ + Constructor for the client main window + + @param standalone: Set to true if the app should use configs + inside its pwd + @type standalone: bool + """ QtGui.QMainWindow.__init__(self) self.CONNECTING_ICON = QtGui.QPixmap(":/images/conn_connecting.png") @@ -91,6 +92,9 @@ class MainWindow(QtGui.QMainWindow): # This is loaded only once, there's a bug when doing that more # than once + ProviderConfig.standalone = standalone + EIPConfig.standalone = standalone + self._standalone = standalone self._provider_config = ProviderConfig() self._eip_config = EIPConfig() # This is created once we have a valid provider config @@ -185,6 +189,7 @@ class MainWindow(QtGui.QMainWindow): self._action_visible.triggered.connect(self._toggle_visible) self._enabled_services = [] + self._settings = LeapSettings(standalone) self._center_window() self._wizard = None @@ -201,8 +206,7 @@ class MainWindow(QtGui.QMainWindow): def _rejected_wizard(self): if self._wizard_firstrun: - settings = QtCore.QSettings() - settings.setValue(self.PROPER_PROVIDER, False) + self._settings.set_properprovider(False) self.quit() else: self._finish_init() @@ -217,7 +221,6 @@ class MainWindow(QtGui.QMainWindow): self.ui.chkAutoLogin.setEnabled(state == QtCore.Qt.Checked) def _finish_init(self): - settings = QtCore.QSettings() self.ui.cmbProviders.addItems(self._configured_providers()) self._show_systray() self.show() @@ -227,9 +230,9 @@ class MainWindow(QtGui.QMainWindow): possible_password = self._wizard.get_password() self.ui.chkRemember.setChecked(self._wizard.get_remember()) self._enabled_services = list(self._wizard.get_services()) - settings.setValue("%s/Services" % - (self.ui.cmbProviders.currentText(),), - self._enabled_services) + self._settings.set_enabled_services( + self.ui.cmbProviders.currentText(), + self._enabled_services) if possible_username is not None: self.ui.lnUser.setText(possible_username) self._focus_password() @@ -238,10 +241,10 @@ class MainWindow(QtGui.QMainWindow): self.ui.chkRemember.setChecked(True) self._login() self._wizard = None - settings.setValue(self.PROPER_PROVIDER, True) + self._settings.set_properprovider(True) else: - saved_user = settings.value(self.USER_KEY, None) - auto_login = settings.value(self.AUTOLOGIN_KEY, "false") != "false" + saved_user = self._settings.get_user() + auto_login = self._settings.get_autologin() if saved_user is not None: self.ui.lnUser.setText(saved_user) @@ -301,9 +304,9 @@ class MainWindow(QtGui.QMainWindow): """ Centers the mainwindow based on the desktop geometry """ - settings = QtCore.QSettings() - geometry = settings.value(self.GEOMETRY_KEY, None) - state = settings.value(self.WINDOWSTATE_KEY, None) + geometry = self._settings.get_geometry() + state = self._settings.get_windowstate() + if geometry is None: app = QtGui.QApplication.instance() width = app.desktop().width() @@ -361,10 +364,11 @@ class MainWindow(QtGui.QMainWindow): self._toggle_visible() e.ignore() return - settings = QtCore.QSettings() - settings.setValue(self.GEOMETRY_KEY, self.saveGeometry()) - settings.setValue(self.WINDOWSTATE_KEY, self.saveState()) - settings.setValue(self.AUTOLOGIN_KEY, self.ui.chkAutoLogin.isChecked()) + + self._settings.set_geometry(self.saveGeometry()) + self._settings.set_windowstate(self.saveState()) + self._settings.set_autologin(self.ui.chkAutoLogin.isChecked()) + QtGui.QMainWindow.closeEvent(self, e) def _configured_providers(self): @@ -394,10 +398,8 @@ class MainWindow(QtGui.QMainWindow): @rtype: bool """ - settings = QtCore.QSettings() has_provider_on_disk = len(self._configured_providers()) != 0 - is_proper_provider = settings.value(self.PROPER_PROVIDER, - "false") != "false" + is_proper_provider = self._settings.get_properprovider() return not (has_provider_on_disk and is_proper_provider) def _focus_password(self): @@ -507,13 +509,8 @@ class MainWindow(QtGui.QMainWindow): password = self.ui.lnPassword.text() provider = self.ui.cmbProviders.currentText() - settings = QtCore.QSettings() - self._enabled_services = settings.value( - "%s/Services" % - (self.ui.cmbProviders.currentText(),), "") - - if isinstance(self._enabled_services, (str, unicode)): - self._enabled_services = self._enabled_services.split(",") + self._enabled_services = self._settings.get_enabled_services( + self.ui.cmbProviders.currentText()) if len(provider) == 0: self._set_status(self.tr("Please select a valid provider")) @@ -530,8 +527,6 @@ class MainWindow(QtGui.QMainWindow): self._set_status(self.tr("Logging in..."), error=False) self._login_set_enabled(False) - settings = QtCore.QSettings() - if self.ui.chkRemember.isChecked(): try: keyring.set_password(self.KEYRING_KEY, @@ -539,7 +534,7 @@ class MainWindow(QtGui.QMainWindow): password.encode("utf8")) # Only save the username if it was saved correctly in # the keyring - settings.setValue(self.USER_KEY, username) + self._settings.set_user(username) except Exception as e: logger.error("Problem saving data to keyring. %r" % (e,)) diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 66b39dd9..4ac7f8a2 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -157,7 +157,14 @@ class VPN(QtCore.QThread): socket_host=socket_host, socket_port=socket_port) try: + env = QtCore.QProcessEnvironment.systemEnvironment() + for key, val in self._launcher.get_vpn_env(providerconfig).items(): + env.insert(key, val) + self._subp = QtCore.QProcess() + + self._subp.setProcessEnvironment(env) + self._subp.finished.connect(self.process_finished) self._subp.start(command[:1][0], command[1:]) logger.debug("Waiting for started...") diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index f9e8e366..c58649b9 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -79,6 +79,20 @@ class VPNLauncher: """ return [] + @abstractmethod + def get_vpn_env(self, providerconfig): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + + @rtype: dict + """ + return {} + def get_platform_launcher(): launcher = globals()[platform.system() + "VPNLauncher"] @@ -125,7 +139,9 @@ class LinuxVPNLauncher(VPNLauncher): def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="unix"): """ - Returns the platform dependant vpn launching command + Returns the platform dependant vpn launching command. It will + look for openvpn in the regular paths and algo in + path_prefix/apps/eip/ (in case standalone is set) Might raise VPNException. @@ -149,7 +165,11 @@ class LinuxVPNLauncher(VPNLauncher): leap_assert(socket_host, "We need a socket host!") leap_assert(socket_port, "We need a socket port!") - openvpn_possibilities = which(self.OPENVPN_BIN) + openvpn_possibilities = which( + self.OPENVPN_BIN, + path_extension=os.path.join(providerconfig.get_path_prefix(), + "..", "apps", "eip")) + if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() @@ -227,6 +247,20 @@ class LinuxVPNLauncher(VPNLauncher): return [openvpn] + args + def get_vpn_env(self, providerconfig): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + @rtype: dict + """ + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + + return {"LD_LIBRARY_PATH": os.path.join( + providerconfig.get_path_prefix(), + "..", "lib")} if __name__ == "__main__": logger = logging.getLogger(name='leap') diff --git a/src/leap/util/leap_argparse.py b/src/leap/util/leap_argparse.py index 83272a3d..66b1a2a5 100644 --- a/src/leap/util/leap_argparse.py +++ b/src/leap/util/leap_argparse.py @@ -37,6 +37,10 @@ Launches the LEAP Client""", epilog=epilog) type=int, action="store", dest="openvpn_verb", help='verbosity level for openvpn logs [1-6]') + parser.add_argument('--standalone', action="store_true", + help='Makes the client use standalone' + 'directories for configuration and binary' + 'searching') # Not in use, we might want to reintroduce them. #parser.add_argument('-i', '--no-provider-checks', -- cgit v1.2.3 From bdc3e1b840c383f0d9236ad8d4df66633e87baf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 21 Mar 2013 10:38:48 -0300 Subject: Fixes as per review --- src/leap/config/leapsettings.py | 10 ++++++---- src/leap/gui/mainwindow.py | 2 +- src/leap/services/eip/vpnlaunchers.py | 12 ++++++++++-- src/leap/util/leap_argparse.py | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/leap/config/leapsettings.py b/src/leap/config/leapsettings.py index 4f12b4f8..b728ce15 100644 --- a/src/leap/config/leapsettings.py +++ b/src/leap/config/leapsettings.py @@ -23,8 +23,8 @@ import logging from PySide import QtCore -from leap.config.prefixers import get_platform_prefixer from leap.common.check import leap_assert, leap_assert_type +from leap.config.prefixers import get_platform_prefixer logger = logging.getLogger(__name__) @@ -54,6 +54,7 @@ class LeapSettings(object): settings_path = os.path.join(get_platform_prefixer() .get_path_prefix(standalone=standalone), + "leap", self.CONFIG_NAME) self._settings = QtCore.QSettings(settings_path, QtCore.QSettings.IniFormat) @@ -156,7 +157,7 @@ class LeapSettings(object): def set_autologin(self, autologin): """ - Sets wether the app should automatically login + Sets whether the app should automatically login @param autologin: True if the app should autologin, False otherwise @type autologin: bool @@ -179,8 +180,9 @@ class LeapSettings(object): """ Sets wether the app should automatically login - @param autologin: True if the app should autologin, False otherwise - @type autologin: bool + @param properprovider: True if the provider is properly + configured, False otherwise + @type properprovider: bool """ leap_assert_type(properprovider, bool) self._settings.setValue(self.PROPERPROVIDER_KEY, properprovider) diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 703d1e26..3f29f957 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -28,8 +28,8 @@ from functools import partial from ui_mainwindow import Ui_MainWindow from leap.common.check import leap_assert -from leap.config.providerconfig import ProviderConfig from leap.config.leapsettings import LeapSettings +from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth from leap.services.eip.vpn import VPN from leap.services.eip.vpnlaunchers import (VPNLauncherException, diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index c58649b9..e6502813 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -165,10 +165,15 @@ class LinuxVPNLauncher(VPNLauncher): leap_assert(socket_host, "We need a socket host!") leap_assert(socket_port, "We need a socket port!") + kwargs = {} + if ProviderConfig.standalone: + kwargs['path_extension'] = os.path.join( + providerconfig.get_path_prefix(), + "..", "apps", "eip") + openvpn_possibilities = which( self.OPENVPN_BIN, - path_extension=os.path.join(providerconfig.get_path_prefix(), - "..", "apps", "eip")) + **kwargs) if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() @@ -253,6 +258,9 @@ class LinuxVPNLauncher(VPNLauncher): This is mainly used for setting LD_LIBRARY_PATH to the correct path when distributing a standalone client + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + @rtype: dict """ leap_assert(providerconfig, "We need a provider config") diff --git a/src/leap/util/leap_argparse.py b/src/leap/util/leap_argparse.py index 66b1a2a5..66268f6f 100644 --- a/src/leap/util/leap_argparse.py +++ b/src/leap/util/leap_argparse.py @@ -37,7 +37,7 @@ Launches the LEAP Client""", epilog=epilog) type=int, action="store", dest="openvpn_verb", help='verbosity level for openvpn logs [1-6]') - parser.add_argument('--standalone', action="store_true", + parser.add_argument('-s', '--standalone', action="store_true", help='Makes the client use standalone' 'directories for configuration and binary' 'searching') -- cgit v1.2.3 From 344abd42c6b480a783ee05b6e92532a1113a86d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 21 Mar 2013 10:47:37 -0300 Subject: Wrap bool setting casting in a method --- src/leap/config/leapsettings.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/config/leapsettings.py b/src/leap/config/leapsettings.py index b728ce15..35c9fef6 100644 --- a/src/leap/config/leapsettings.py +++ b/src/leap/config/leapsettings.py @@ -29,6 +29,28 @@ from leap.config.prefixers import get_platform_prefixer logger = logging.getLogger(__name__) +def to_bool(val): + """ + Returns the boolean value corresponding to val. Will return False + in case val is not a string or something that behaves like one. + + @param val: value to cast + @type val: either bool already or str + + @rtype: bool + """ + if isinstance(val, bool): + return val + + bool_val = False + try: + bool_val = val.lower() == "true" + except: + pass + + return bool_val + + class LeapSettings(object): """ Leap client QSettings wrapper @@ -153,7 +175,7 @@ class LeapSettings(object): @rtype: bool """ - return self._settings.value(self.AUTOLOGIN_KEY, "false") != "false" + return to_bool(self._settings.value(self.AUTOLOGIN_KEY, False)) def set_autologin(self, autologin): """ @@ -173,8 +195,7 @@ class LeapSettings(object): @rtype: bool """ - return self._settings.value(self.PROPERPROVIDER_KEY, - "false") != "false" + return to_bool(self._settings.value(self.PROPERPROVIDER_KEY, False)) def set_properprovider(self, properprovider): """ -- cgit v1.2.3 From 9dace17a4b162c3fbef9909b6f8226903b0ad445 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 21 Mar 2013 23:30:02 +0900 Subject: fix docstring --- src/leap/services/eip/vpn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 9a60cf73..55ace23b 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -178,9 +178,9 @@ class VPN(QtCore.QThread): def _dump_exitinfo(self): """ SLOT - Prints debug info when quitting the process - TRIGGER: self._subp.finished + + Prints debug info when quitting the process """ logger.debug("stdout: %s", self._subp.readAllStandardOutput()) logger.debug("stderr: %s", self._subp.readAllStandardError()) -- cgit v1.2.3 From d1dbe61039cb318efcf239dd9bf47fdbd82922d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 21 Mar 2013 13:39:09 -0300 Subject: Add Windows prefixer --- src/leap/config/prefixers.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'src') diff --git a/src/leap/config/prefixers.py b/src/leap/config/prefixers.py index 5a9b2112..460e5b46 100644 --- a/src/leap/config/prefixers.py +++ b/src/leap/config/prefixers.py @@ -99,6 +99,28 @@ class DarwinPrefixer(Prefixer): return os.getenv("LEAP_CLIENT_PATH", config_dir) +class WindowsPrefixer(Prefixer): + """ + Config prefixer for the Windows platform + """ + + def get_path_prefix(self, standalone=False): + """ + Returns the platform dependant path prefixer. + This method expects an env variable named LEAP_CLIENT_PATH if + standalone is used. + + @param standalone: if True it will return the prefix for a + standalone application. Otherwise, it will return the system + default for configuration storage. + @type standalone: bool + """ + config_dir = BaseDirectory.xdg_config_home + + if not standalone: + return config_dir + return os.path.join(os.getcwd(), "config") + if __name__ == "__main__": try: abs_prefixer = Prefixer() -- cgit v1.2.3 From 5488aa1c4f01be567248c2a8ccd0d49d4161877b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 21 Mar 2013 13:39:35 -0300 Subject: Add Windows VPN Launcher --- src/leap/services/eip/vpnlaunchers.py | 107 ++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) (limited to 'src') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 78db0176..1b7f11f5 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -272,6 +272,7 @@ class LinuxVPNLauncher(VPNLauncher): providerconfig.get_path_prefix(), "..", "lib")} + class DarwinVPNLauncher(VPNLauncher): """ VPN launcher for the Darwin Platform @@ -388,6 +389,112 @@ class DarwinVPNLauncher(VPNLauncher): return [command] + cmd_args +class WindowsVPNLauncher(VPNLauncher): + """ + VPN launcher for the Windows platform + """ + + OPENVPN_BIN = 'openvpn.exe' + + def get_vpn_command(self, eipconfig=None, providerconfig=None, + socket_host=None, socket_port="9876"): + """ + Returns the platform dependant vpn launching command. It will + look for openvpn in the regular paths and algo in + path_prefix/apps/eip/ (in case standalone is set) + + Might raise VPNException. + + @param eipconfig: eip configuration object + @type eipconfig: EIPConfig + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + @param socket_host: either socket path (unix) or socket IP + @type socket_host: str + @param socket_port: either string "unix" if it's a unix + socket, or port otherwise + @type socket_port: str + + @return: A VPN command ready to be launched + @rtype: list + """ + leap_assert(eipconfig, "We need an eip config") + leap_assert_type(eipconfig, EIPConfig) + leap_assert(providerconfig, "We need a provider config") + leap_assert_type(providerconfig, ProviderConfig) + leap_assert(socket_host, "We need a socket host!") + leap_assert(socket_port, "We need a socket port!") + leap_assert(socket_port != "unix", + "We cannot use unix sockets in windows!") + + openvpn_possibilities = which( + self.OPENVPN_BIN, + path_extension=os.path.join(providerconfig.get_path_prefix(), + "..", "apps", "eip")) + + if len(openvpn_possibilities) == 0: + raise OpenVPNNotFoundException() + + openvpn = openvpn_possibilities[0] + args = [] + + # TODO: handle verbosity + + gateway_ip = str(eipconfig.get_gateway_ip(0)) + + logger.debug("Using gateway ip %s" % (gateway_ip,)) + + args += [ + '--client', + '--dev', 'tun', + '--persist-tun', + '--persist-key', + '--remote', gateway_ip, '1194', 'udp', + '--tls-client', + '--remote-cert-tls', + 'server' + ] + + openvpn_configuration = eipconfig.get_openvpn_configuration() + for key, value in openvpn_configuration.items(): + args += ['--%s' % (key,), value] + + args += [ + '--user', getpass.getuser(), + #'--group', grp.getgrgid(os.getgroups()[-1]).gr_name + ] + + args += [ + '--management-signal', + '--management', socket_host, socket_port, + '--script-security', '2' + ] + + args += [ + '--cert', eipconfig.get_client_cert_path(providerconfig), + '--key', eipconfig.get_client_cert_path(providerconfig), + '--ca', providerconfig.get_ca_cert_path() + ] + + logger.debug("Running VPN with command:") + logger.debug("%s %s" % (openvpn, " ".join(args))) + + return [openvpn] + args + + def get_vpn_env(self, providerconfig): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + + @rtype: dict + """ + return {} + + if __name__ == "__main__": logger = logging.getLogger(name='leap') logger.setLevel(logging.DEBUG) -- cgit v1.2.3 From 031f84229bb34a62816a9f0fa5d80ae89c285801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 21 Mar 2013 13:39:53 -0300 Subject: Alias cp65001 to utf-8 for Windows --- src/leap/app.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/leap/app.py b/src/leap/app.py index 14d3c69c..7cf78dc9 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -27,6 +27,10 @@ from leap.util import leap_argparse from leap.gui import locale_rc from leap.gui.mainwindow import MainWindow +import codecs +codecs.register(lambda name: codecs.lookup('utf-8') + if name == 'cp65001' else None) + def sigint_handler(*args, **kwargs): logger = kwargs.get('logger', None) -- cgit v1.2.3 From 30912fed29fd652f69de5a518cf70e6fe04b04f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 21 Mar 2013 13:40:18 -0300 Subject: Make Windows use a regular socket --- src/leap/gui/mainwindow.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 3f29f957..974476a3 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -20,6 +20,7 @@ Main window for the leap client """ import os import logging +import platform import tempfile import keyring @@ -605,9 +606,14 @@ class MainWindow(QtGui.QMainWindow): """ # TODO: make this properly multiplatform - host = os.path.join(tempfile.mkdtemp(prefix="leap-tmp"), - 'openvpn.socket') - port = "unix" + + if platform.system() == "Windows": + host = "localhost" + port = "9876" + else: + host = os.path.join(tempfile.mkdtemp(prefix="leap-tmp"), + 'openvpn.socket') + port = "unix" return host, port -- cgit v1.2.3 From 9bd35a826b398bd3bdd98824f17a14538d14abb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 21 Mar 2013 13:40:37 -0300 Subject: Calculate mtime in a more multiplatform way --- src/leap/util/request_helpers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/util/request_helpers.py b/src/leap/util/request_helpers.py index c5d0f3f5..019ff353 100644 --- a/src/leap/util/request_helpers.py +++ b/src/leap/util/request_helpers.py @@ -19,6 +19,8 @@ Request helpers for backward compatible "parsing" of requests """ +import time + import json from dateutil import parser as dateparser @@ -50,6 +52,7 @@ def get_content(request): mtime = None last_modified = request.headers.get('last-modified', None) if last_modified: - mtime = int(dateparser.parse(last_modified).strftime("%s")) + dt = dateparser.parse(unicode(last_modified)) + mtime = int(time.mktime(dt.timetuple()) + dt.microsecond / 1000000.0) return contents, mtime -- cgit v1.2.3 From e267ebd09c57c869dc0ab8d57baaed6d63fb2f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 22 Mar 2013 16:12:26 -0300 Subject: Improve systray behavior with context menu and activation --- src/leap/gui/mainwindow.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 3f29f957..e039fdf1 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -287,18 +287,19 @@ class MainWindow(QtGui.QMainWindow): self._vpn_systray.setIcon(QtGui.QIcon(self.ERROR_ICON)) self._vpn_systray.setVisible(False) - def _toggle_visible(self): + def _toggle_visible(self, reason=None): """ SLOT TRIGGER: self._systray.activated Toggles the window visibility """ - self.setVisible(not self.isVisible()) - action_visible_text = self.tr("Hide") - if not self.isVisible(): - action_visible_text = self.tr("Show") - self._action_visible.setText(action_visible_text) + if reason != QtGui.QSystemTrayIcon.Context: + self.setVisible(not self.isVisible()) + action_visible_text = self.tr("Hide") + if not self.isVisible(): + action_visible_text = self.tr("Show") + self._action_visible.setText(action_visible_text) def _center_window(self): """ -- cgit v1.2.3 From 2c30ffe4ab8a12712735b7f8fef27cd7700eaaae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 25 Mar 2013 12:00:49 -0300 Subject: Add windows platform initializer --- src/leap/gui/mainwindow.py | 4 ++ src/leap/platform_init/__init__.py | 0 src/leap/platform_init/initializers.py | 92 ++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/leap/platform_init/__init__.py create mode 100644 src/leap/platform_init/initializers.py (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 3f29f957..ca6f1a8b 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -31,6 +31,7 @@ from leap.common.check import leap_assert from leap.config.leapsettings import LeapSettings from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth +from leap.platform_init.initializers import init_platform from leap.services.eip.vpn import VPN from leap.services.eip.vpnlaunchers import (VPNLauncherException, OpenVPNNotFoundException, @@ -192,6 +193,9 @@ class MainWindow(QtGui.QMainWindow): self._settings = LeapSettings(standalone) self._center_window() + + init_platform() + self._wizard = None self._wizard_firstrun = False if self._first_run(): diff --git a/src/leap/platform_init/__init__.py b/src/leap/platform_init/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py new file mode 100644 index 00000000..60421d62 --- /dev/null +++ b/src/leap/platform_init/initializers.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# initializers.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Platform dependant initializing code +""" + +import logging +import os +import platform +import subprocess + +from PySide import QtGui + +logger = logging.getLogger(__name__) + + +def init_platform(): + initializer = globals()[platform.system() + "Initializer"] + if initializer: + logger.debug("Running initializer for %s" % (platform.system(),)) + initializer() + else: + logger.debug("Initializer not found for %s" % (platform.system(),)) + + +def _windows_has_tap_device(): + import _winreg as reg + + adapter_key = 'SYSTEM\CurrentControlSet\Control\Class' \ + '\{4D36E972-E325-11CE-BFC1-08002BE10318}' + with reg.OpenKey(reg.HKEY_LOCAL_MACHINE, adapter_key) as adapters: + try: + for i in xrange(10000): + key_name = reg.EnumKey(adapters, i) + with reg.OpenKey(adapters, key_name) as adapter: + try: + component_id = reg.QueryValueEx(adapter, + 'ComponentId')[0] + if component_id.startswith("tap0901"): + return True + except WindowsError: + pass + except WindowsError: + pass + return False + + +def WindowsInitializer(): + if not _windows_has_tap_device(): + msg = QtGui.QMessageBox() + msg.setWindowTitle(msg.tr("TAP Driver")) + msg.setText(msg.tr("LEAPClient needs to install the necessary drivers " + "for Encrypted Internet to work. Would you like to " + "proceed?")) + msg.setInformativeText(msg.tr("Encrypted Internet uses VPN, which " + "needs a TAP device installed and none " + "have been found")) + msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) + msg.setDefaultButton(QtGui.QMessageBox.Yes) + ret = msg.exec_() + + if ret == QtGui.QMessageBox.Yes: + driver_path = os.path.join(os.getcwd(), + "apps", + "eip", + "tap_driver") + dev_installer = os.path.join(driver_path, + "devcon.exe") + if os.path.isfile(dev_installer) and \ + os.access(dev_installer, os.X_OK): + inf_path = os.path.join(driver_path, + "OemWin2k.inf") + cmd = [dev_installer, "install", inf_path, "tap0901"] + ret = subprocess.call(cmd, stdout=subprocess.PIPE, shell=True) + else: + logger.error("Tried to install TAP driver, but the installer " + "is not found or not executable") -- cgit v1.2.3 From 01ebc3a165de4f84313b298ab6da78c11f181e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 25 Mar 2013 12:01:03 -0300 Subject: grp doesn't exist in windows --- src/leap/services/eip/vpnlaunchers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 78db0176..9761c225 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -21,9 +21,12 @@ Platform dependant VPN launchers import commands import logging import getpass -import grp import os import platform +try: + import grp +except ImportError: + pass # ignore, probably windows from abc import ABCMeta, abstractmethod -- cgit v1.2.3 From f6fa3ad1f1c7c4ed9d069da031bfb6dfc6511590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 25 Mar 2013 12:55:06 -0300 Subject: Reorder imports based on rope's recommendations --- src/leap/gui/mainwindow.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 974476a3..aa29e843 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -18,31 +18,32 @@ """ Main window for the leap client """ -import os import logging +import os import platform import tempfile -import keyring - -from PySide import QtCore, QtGui from functools import partial -from ui_mainwindow import Ui_MainWindow +import keyring +from PySide import QtCore, QtGui from leap.common.check import leap_assert from leap.config.leapsettings import LeapSettings from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth +from leap.gui.wizard import Wizard +from leap.services.eip.eipbootstrapper import EIPBootstrapper +from leap.services.eip.eipconfig import EIPConfig +from leap.services.eip.providerbootstrapper import ProviderBootstrapper from leap.services.eip.vpn import VPN from leap.services.eip.vpnlaunchers import (VPNLauncherException, OpenVPNNotFoundException, EIPNoPkexecAvailable, EIPNoPolkitAuthAgentAvailable) -from leap.services.eip.providerbootstrapper import ProviderBootstrapper -from leap.services.eip.eipbootstrapper import EIPBootstrapper -from leap.services.eip.eipconfig import EIPConfig -from leap.gui.wizard import Wizard -from leap.util.checkerthread import CheckerThread from leap.util import __version__ as VERSION +from leap.util.checkerthread import CheckerThread + +from ui_mainwindow import Ui_MainWindow + logger = logging.getLogger(__name__) -- cgit v1.2.3 From a72a8e0eca6d070b51673129fddb2192066ab1af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 25 Mar 2013 13:07:48 -0300 Subject: Rename the openvpn exe to something more specific --- src/leap/services/eip/vpnlaunchers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 1b7f11f5..b5a6cc94 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -394,7 +394,7 @@ class WindowsVPNLauncher(VPNLauncher): VPN launcher for the Windows platform """ - OPENVPN_BIN = 'openvpn.exe' + OPENVPN_BIN = 'openvpn_leap.exe' def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="9876"): -- cgit v1.2.3 From bd883c86aec15673a56b954cc6046b43e759e562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 25 Mar 2013 13:09:15 -0300 Subject: Mark sanitation point clearly for future reference --- src/leap/services/eip/vpnlaunchers.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index b5a6cc94..5267d918 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -456,6 +456,7 @@ class WindowsVPNLauncher(VPNLauncher): ] openvpn_configuration = eipconfig.get_openvpn_configuration() + # XXX sanitize this for key, value in openvpn_configuration.items(): args += ['--%s' % (key,), value] -- cgit v1.2.3 From 18b806806fcf508126b86fe84dce9ecaae98fc47 Mon Sep 17 00:00:00 2001 From: kali Date: Tue, 26 Mar 2013 01:12:35 +0900 Subject: pep8 --- src/leap/config/pluggableconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/config/pluggableconfig.py b/src/leap/config/pluggableconfig.py index 4a742da4..8535fa6b 100644 --- a/src/leap/config/pluggableconfig.py +++ b/src/leap/config/pluggableconfig.py @@ -160,7 +160,7 @@ class TranslatableType(object): def to_python(self, data): # TODO: add translatable - return data#LEAPTranslatable(data) + return data # LEAPTranslatable(data) # needed? we already have an extended dict... #def get_prep_value(self, data): -- cgit v1.2.3 From 310eff047bdc8f5c5cbd4890f91f9cf492c68922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 25 Mar 2013 13:14:29 -0300 Subject: Fix grammar error --- src/leap/platform_init/initializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index 60421d62..427b3da4 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -69,7 +69,7 @@ def WindowsInitializer(): "proceed?")) msg.setInformativeText(msg.tr("Encrypted Internet uses VPN, which " "needs a TAP device installed and none " - "have been found")) + "has been found")) msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) msg.setDefaultButton(QtGui.QMessageBox.Yes) ret = msg.exec_() -- cgit v1.2.3 From d193fee401d606f6120ac11819a0127e7ee92458 Mon Sep 17 00:00:00 2001 From: kali Date: Tue, 26 Mar 2013 01:15:44 +0900 Subject: tests for srpregister and srpauth in this commit too, the twisted fake_provider implementation --- src/leap/crypto/srpauth.py | 31 +-- src/leap/crypto/srpregister.py | 25 ++- src/leap/crypto/tests/__init__.py | 16 ++ src/leap/crypto/tests/fake_provider.py | 333 ++++++++++++++++++++++++++++++ src/leap/crypto/tests/test.txt | 1 + src/leap/crypto/tests/test_provider.json | 15 ++ src/leap/crypto/tests/test_srpauth.py | 136 ++++++++++++ src/leap/crypto/tests/test_srpregister.py | 142 +++++++++++++ 8 files changed, 677 insertions(+), 22 deletions(-) create mode 100644 src/leap/crypto/tests/__init__.py create mode 100755 src/leap/crypto/tests/fake_provider.py create mode 100644 src/leap/crypto/tests/test.txt create mode 100644 src/leap/crypto/tests/test_provider.json create mode 100644 src/leap/crypto/tests/test_srpauth.py create mode 100644 src/leap/crypto/tests/test_srpregister.py (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index 152d77b5..027ee0d7 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -272,7 +272,14 @@ class SRPAuth(QtCore.QObject): "failed")) logger.debug("Session verified.") - self.set_session_id(self._session.cookies["_session_id"]) + SESSION_ID_KEY = "_session_id" + session_id = self._session.cookies.get(SESSION_ID_KEY, None) + if not session_id: + logger.error("Bad cookie from server (missing _session_id)") + raise SRPAuthenticationError(self.tr("Session cookie " + "verification " + "failed")) + self.set_session_id(session_id) def authenticate(self, username, password): """ @@ -409,11 +416,18 @@ class SRPAuth(QtCore.QObject): if __name__ == "__main__": + import signal import sys + from functools import partial app = QtGui.QApplication(sys.argv) - import signal + if not len(sys.argv) == 3: + print 'Usage: srpauth.py ' + sys.exit(0) + + _user = sys.argv[1] + _pass = sys.argv[2] def sigint_handler(*args, **kwargs): logger.debug('SIGINT catched. shutting down...') @@ -452,20 +466,9 @@ if __name__ == "__main__": provider = ProviderConfig() if provider.load("leap/providers/bitmask.net/provider.json"): - # url = "%s/tickets" % (provider.get_api_uri(),) - # print url - # res = requests.session().get(url, verify=provider.get_ca_cert_path()) - # print res.content - # res.raise_for_status() auth = SRPAuth(provider) - auth_instantiated = partial(auth.authenticate, "test2", "sarasaaaa") + auth_instantiated = partial(auth.authenticate, _user, _pass) checker.add_checks([auth_instantiated, auth.logout]) - #auth.authenticate("test2", "sarasaaaa") - #res = requests.session().get("%s/cert" % (provider.get_api_uri(),), - #verify=provider.get_ca_cert_path()) - #print res.content - #auth.logout() - sys.exit(app.exec_()) diff --git a/src/leap/crypto/srpregister.py b/src/leap/crypto/srpregister.py index 9a9cac76..dc137aeb 100644 --- a/src/leap/crypto/srpregister.py +++ b/src/leap/crypto/srpregister.py @@ -55,7 +55,7 @@ class SRPRegister(QtCore.QObject): @type register_path; str """ QtCore.QObject.__init__(self) - leap_assert(provider_config, "Please provider a provider") + leap_assert(provider_config, "Please provide a provider") leap_assert_type(provider_config, ProviderConfig) self._provider_config = provider_config @@ -125,15 +125,24 @@ class SRPRegister(QtCore.QObject): logger.debug("Will try to register user = %s" % (username,)) logger.debug("user_data => %r" % (user_data,)) - req = self._session.post(uri, - data=user_data, - timeout=SIGNUP_TIMEOUT, - verify=self._provider_config. - get_ca_cert_path()) + try: + req = self._session.post(uri, + data=user_data, + timeout=SIGNUP_TIMEOUT, + verify=self._provider_config. + get_ca_cert_path()) - self.registration_finished.emit(req.ok, req) + except requests.exceptions.SSLError as exc: + logger.error("SSLError: %s" % exc.message) + _ok = False + req = None - return req.ok + else: + _ok = req.ok + + self.registration_finished.emit(_ok, req) + + return _ok if __name__ == "__main__": diff --git a/src/leap/crypto/tests/__init__.py b/src/leap/crypto/tests/__init__.py new file mode 100644 index 00000000..7f118735 --- /dev/null +++ b/src/leap/crypto/tests/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# __init__.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/src/leap/crypto/tests/fake_provider.py b/src/leap/crypto/tests/fake_provider.py new file mode 100755 index 00000000..4b05bbff --- /dev/null +++ b/src/leap/crypto/tests/fake_provider.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# fake_provider.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""A server faking some of the provider resources and apis, +used for testing Leap Client requests + +It needs that you create a subfolder named 'certs', +and that you place the following files: + +XXX check if in use + +[ ] test-openvpn.pem +[ ] test-provider.json +[ ] test-eip-service.json +""" +import binascii +import json +import os +import sys + +import srp + +from OpenSSL import SSL + +from zope.interface import Interface, Attribute, implements + +from twisted.web.server import Site, Request +from twisted.web.static import File +from twisted.web.resource import Resource +from twisted.internet import reactor + +from leap.common.testing.https_server import where + +# See +# http://twistedmatrix.com/documents/current/web/howto/web-in-60/index.html +# for more examples + +""" +Testing the FAKE_API: +##################### + + 1) register an user + >> curl -d "user[login]=me" -d "user[password_salt]=foo" \ + -d "user[password_verifier]=beef" http://localhost:8000/1/users + << {"errors": null} + + 2) check that if you try to register again, it will fail: + >> curl -d "user[login]=me" -d "user[password_salt]=foo" \ + -d "user[password_verifier]=beef" http://localhost:8000/1/users + << {"errors": {"login": "already taken!"}} + +""" + +# Globals to mock user/sessiondb + +_USERDB = {} +_SESSIONDB = {} + +_here = os.path.split(__file__)[0] + + +safe_unhexlify = lambda x: binascii.unhexlify(x) \ + if (len(x) % 2 == 0) else binascii.unhexlify('0' + x) + + +class IUser(Interface): + 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 '[server]', login, verifier, salt + user = User(login, salt, verifier) + _USERDB[login] = user + return json.dumps({'errors': None}) + + +def getSession(self, sessionInterface=None): + """ + we overwrite twisted.web.server.Request.getSession method to + put the right cookie name in place + """ + if not self.session: + #cookiename = b"_".join([b'TWISTED_SESSION'] + self.sitepath) + cookiename = b"_".join([b'_session_id'] + self.sitepath) + sessionCookie = self.getCookie(cookiename) + if sessionCookie: + try: + self.session = self.site.getSession(sessionCookie) + except KeyError: + pass + # if it still hasn't been set, fix it up. + if not self.session: + self.session = self.site.makeSession() + self.addCookie(cookiename, self.session.uid, path=b'/') + self.session.touch() + if sessionInterface: + return self.session.getComponent(sessionInterface) + return self.session + + +def get_user(request): + """ + Returns user from the session dict + """ + login = request.args.get('login') + if login: + user = _USERDB.get(login[0], None) + if user: + return user + + request.getSession = getSession.__get__(request, Request) + session = request.getSession() + + user = _SESSIONDB.get(session, None) + return user + + +class FakeSession(Resource): + def __init__(self, name): + """ + Initializes session + """ + self.name = name + + def render_GET(self, request): + """ + Handles GET requests. + """ + return "%s\n" % json.dumps({'errors': None}) + + def render_POST(self, request): + """ + Handles POST requests. + """ + user = get_user(request) + + if not user: + # XXX get real error from demo provider + return json.dumps({'errors': 'no such user'}) + + A = request.args['A'][0] + + _A = safe_unhexlify(A) + _salt = safe_unhexlify(user.salt) + _verifier = safe_unhexlify(user.verifier) + + svr = srp.Verifier( + user.login, + _salt, + _verifier, + _A, + hash_alg=srp.SHA256, + ng_type=srp.NG_1024) + + s, B = svr.get_challenge() + + _B = binascii.hexlify(B) + + print '[server] login = %s' % user.login + print '[server] salt = %s' % user.salt + print '[server] len(_salt) = %s' % len(_salt) + print '[server] vkey = %s' % user.verifier + print '[server] len(vkey) = %s' % len(_verifier) + print '[server] s = %s' % binascii.hexlify(s) + print '[server] B = %s' % _B + print '[server] len(B) = %s' % len(_B) + + # override Request.getSession + request.getSession = getSession.__get__(request, Request) + session = request.getSession() + + user.set_session(session) + user.set_server_verifier(svr) + + # yep, this is tricky. + # some things are *already* unhexlified. + data = { + 'salt': user.salt, + 'B': _B, + 'errors': None} + + return json.dumps(data) + + def render_PUT(self, request): + """ + Handles PUT requests. + """ + # XXX check session??? + user = get_user(request) + + if not user: + print '[server] NO USER' + return json.dumps({'errors': 'no such user'}) + + data = request.content.read() + auth = data.split("client_auth=") + M = auth[1] if len(auth) > 1 else None + # if not H, return + if not M: + return json.dumps({'errors': 'no M proof passed by client'}) + + svr = user.svr + HAMK = svr.verify_session(binascii.unhexlify(M)) + if HAMK is None: + print '[server] verification failed!!!' + raise Exception("Authentication failed!") + #import ipdb;ipdb.set_trace() + + assert svr.authenticated() + print "***" + print '[server] User successfully authenticated using SRP!' + print "***" + + return json.dumps( + {'M2': binascii.hexlify(HAMK), + 'id': '9c943eb9d96a6ff1b7a7030bdeadbeef', + 'errors': None}) + + +class API_Sessions(Resource): + def getChild(self, name, request): + return FakeSession(name) + + +class OpenSSLServerContextFactory: + + def getContext(self): + """ + Create an SSL context. + """ + ctx = SSL.Context(SSL.SSLv23_METHOD) + #ctx = SSL.Context(SSL.TLSv1_METHOD) + ctx.use_certificate_file(where('leaptestscert.pem')) + ctx.use_privatekey_file(where('leaptestskey.pem')) + + return ctx + + +def get_provider_factory(): + """ + Instantiates a Site that serves the resources + that we expect from a valid provider. + Listens on: + * port 8000 for http connections + * port 8443 for https connections + + @rparam: factory for a site + @rtype: Site instance + """ + root = Resource() + root.putChild("provider.json", File( + os.path.join(_here, + "test_provider.json"))) + config = Resource() + config.putChild( + "eip-service.json", + File("./eip-service.json")) + apiv1 = Resource() + apiv1.putChild("config", config) + apiv1.putChild("sessions", API_Sessions()) + apiv1.putChild("users", FakeUsers(None)) + apiv1.putChild("cert", File( + os.path.join(_here, + 'openvpn.pem'))) + root.putChild("1", apiv1) + + factory = Site(root) + return factory + + +if __name__ == "__main__": + + from twisted.python import log + log.startLogging(sys.stdout) + + factory = get_provider_factory() + + # regular http (for debugging with curl) + reactor.listenTCP(8000, factory) + reactor.listenSSL(8443, factory, OpenSSLServerContextFactory()) + reactor.run() + + diff --git a/src/leap/crypto/tests/test.txt b/src/leap/crypto/tests/test.txt new file mode 100644 index 00000000..d6406617 --- /dev/null +++ b/src/leap/crypto/tests/test.txt @@ -0,0 +1 @@ +OK! diff --git a/src/leap/crypto/tests/test_provider.json b/src/leap/crypto/tests/test_provider.json new file mode 100644 index 00000000..c37bef8f --- /dev/null +++ b/src/leap/crypto/tests/test_provider.json @@ -0,0 +1,15 @@ +{ + "api_uri": "https://localhost:8443", + "api_version": "1", + "ca_cert_fingerprint": "SHA256: 0f17c033115f6b76ff67871872303ff65034efe7dd1b910062ca323eb4da5c7e", + "ca_cert_uri": "https://bitmask.net/ca.crt", + "default_language": "en", + "domain": "example.com", + "enrollment_policy": "open", + "name": { + "en": "Bitmask" + }, + "services": [ + "openvpn" + ] +} diff --git a/src/leap/crypto/tests/test_srpauth.py b/src/leap/crypto/tests/test_srpauth.py new file mode 100644 index 00000000..ce9403c7 --- /dev/null +++ b/src/leap/crypto/tests/test_srpauth.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# test_srpauth.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Tests for leap/crypto/srpauth.py +""" +try: + import unittest +except ImportError: + import unittest +import os +import sys + +from mock import MagicMock +from nose.twistedtools import reactor, threaded_reactor, stop_reactor +from twisted.python import log + +from leap.common.testing.https_server import where +from leap.config.providerconfig import ProviderConfig +from leap.crypto import srpauth +from leap.crypto import srpregister +from leap.crypto.tests import fake_provider + +log.startLogging(sys.stdout) + + +def _get_capath(): + return where("cacert.pem") + +_here = os.path.split(__file__)[0] + + +class ImproperlyConfiguredError(Exception): + """ + Raised if the test provider is missing configuration + """ + + +class SRPRegisterTestCase(unittest.TestCase): + """ + Tests for the SRP Authentication class + """ + __name__ = "SRPAuth tests" + + @classmethod + def setUpClass(cls): + """ + Sets up this TestCase with a simple and faked provider instance: + + * runs a threaded reactor + * loads a mocked ProviderConfig that points to the certs in the + leap.common.testing module. + """ + factory = fake_provider.get_provider_factory() + reactor.listenTCP(8000, factory) + reactor.listenSSL( + 8443, factory, + fake_provider.OpenSSLServerContextFactory()) + threaded_reactor() + + provider = ProviderConfig() + provider.get_ca_cert_path = MagicMock() + provider.get_ca_cert_path.return_value = _get_capath() + loaded = provider.load(path=os.path.join( + _here, "test_provider.json")) + if not loaded: + raise ImproperlyConfiguredError( + "Could not load test provider config") + cls.provider = provider + cls.register = srpregister.SRPRegister(provider_config=provider) + cls.auth = srpauth.SRPAuth(provider) + cls._auth_instance = cls.auth.__dict__['_SRPAuth__instance'] + cls.authenticate = cls._auth_instance.authenticate + cls.logout = cls._auth_instance.logout + + @classmethod + def tearDownClass(cls): + """ + Stops reactor when tearing down the class + """ + stop_reactor() + + def test_auth(self): + """ + Checks whether a pair of valid credentials is able to be authenticated. + """ + TEST_USER = "register_test_auth" + TEST_PASS = "pass" + + # pristine registration, should go well + ok = self.register.register_user(TEST_USER, TEST_PASS) + self.assertTrue(ok) + + self.authenticate(TEST_USER, TEST_PASS) + with self.assertRaises(AssertionError): + # AssertionError: already logged in + # We probably could take this as its own exception + self.authenticate(TEST_USER, TEST_PASS) + + self.logout() + + # cannot log out two times in a row (there's no session) + with self.assertRaises(AssertionError): + self.logout() + + def test_auth_with_bad_credentials(self): + """ + Checks that auth does not succeed with bad credentials. + """ + TEST_USER = "register_test_auth" + TEST_PASS = "pass" + + # non-existent credentials, should fail + with self.assertRaises(srpauth.SRPAuthenticationError): + self.authenticate("baduser_1", "passwrong") + + # good user, bad password, should fail + with self.assertRaises(srpauth.SRPAuthenticationError): + self.authenticate(TEST_USER, "passwrong") + + # bad user, good password, should fail too :) + with self.assertRaises(srpauth.SRPAuthenticationError): + self.authenticate("myunclejoe", TEST_PASS) diff --git a/src/leap/crypto/tests/test_srpregister.py b/src/leap/crypto/tests/test_srpregister.py new file mode 100644 index 00000000..b065958d --- /dev/null +++ b/src/leap/crypto/tests/test_srpregister.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# test_srpregister.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Tests for leap/crypto/srpregister.py +""" +try: + import unittest +except ImportError: + import unittest +import os +import sys + +from mock import MagicMock +from nose.twistedtools import reactor, threaded_reactor, stop_reactor +from twisted.python import log + +from leap.common.testing.https_server import where +from leap.config.providerconfig import ProviderConfig +from leap.crypto import srpregister +from leap.crypto.tests import fake_provider + +log.startLogging(sys.stdout) + + +def _get_capath(): + return where("cacert.pem") + +_here = os.path.split(__file__)[0] + + +class ImproperlyConfiguredError(Exception): + """ + Raised if the test provider is missing configuration + """ + + +class SRPRegisterTestCase(unittest.TestCase): + """ + Tests for the SRP Register class + """ + __name__ = "SRPRegister tests" + + @classmethod + def setUpClass(cls): + """ + Sets up this TestCase with a simple and faked provider instance: + + * runs a threaded reactor + """ + factory = fake_provider.get_provider_factory() + reactor.listenTCP(8000, factory) + reactor.listenSSL( + 8443, factory, + fake_provider.OpenSSLServerContextFactory()) + threaded_reactor() + + def setUp(self): + """ + Sets up common parameters for each test: + + * loads a mocked ProviderConfig that points to the certs in the + leap.common.testing module. + """ + provider = ProviderConfig() + provider.get_ca_cert_path = MagicMock() + provider.get_ca_cert_path.return_value = _get_capath() + loaded = provider.load(path=os.path.join( + _here, "test_provider.json")) + if not loaded: + raise ImproperlyConfiguredError( + "Could not load test provider config") + self.register = srpregister.SRPRegister(provider_config=provider) + + @classmethod + def tearDownClass(cls): + """ + Stops reactor when tearing down the class + """ + stop_reactor() + + def test_register_user(self): + """ + Checks if the registration of an unused name works as expected when + it is the first time that we attempt to register that user, as well as + when we request a user that is taken. + """ + # pristine registration + ok = self.register.register_user("foouser_firsttime", "barpass") + self.assertTrue(ok) + + # second registration attempt with the same user should return errors + ok = self.register.register_user("foouser_second", "barpass") + self.assertTrue(ok) + + # FIXME currently we are catching this in an upper layer, + # we could bring the error validation to the SRPRegister class + ok = self.register.register_user("foouser_second", "barpass") + # XXX + #self.assertFalse(ok) + + def test_correct_http_uri(self): + """ + Checks that registration autocorrect http uris to https ones. + """ + HTTP_URI = "http://localhost:8443" + HTTPS_URI = "https://localhost:8443/1/users" + provider = ProviderConfig() + provider.get_ca_cert_path = MagicMock() + provider.get_ca_cert_path.return_value = _get_capath() + provider.get_api_uri = MagicMock() + + # we introduce a http uri in the config file... + provider.get_api_uri.return_value = HTTP_URI + loaded = provider.load(path=os.path.join( + _here, "test_provider.json")) + if not loaded: + raise ImproperlyConfiguredError( + "Could not load test provider config") + self.register = srpregister.SRPRegister(provider_config=provider) + + # ... and we check that we're correctly taking the HTTPS protocol + # instead + self.assertEquals(self.register._get_registration_uri(), + HTTPS_URI) + ok = self.register.register_user("test_failhttp", "barpass") + self.assertTrue(ok) + + # XXX need to assert that _get_registration_uri was called too -- cgit v1.2.3 From ee2ea741883aa6fa3b168431d588f20a5e90f5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 25 Mar 2013 13:31:53 -0300 Subject: Make it not fail in any other platform --- src/leap/platform_init/initializers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index 427b3da4..ac08e23f 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -30,7 +30,11 @@ logger = logging.getLogger(__name__) def init_platform(): - initializer = globals()[platform.system() + "Initializer"] + initializer = None + try: + initializer = globals()[platform.system() + "Initializer"] + except: + pass if initializer: logger.debug("Running initializer for %s" % (platform.system(),)) initializer() -- cgit v1.2.3 From 05fe7f44a899288a8a69b9a46793513b87f8d228 Mon Sep 17 00:00:00 2001 From: kali Date: Tue, 26 Mar 2013 02:55:55 +0900 Subject: workaround for srp server timing out on consecutive runs --- src/leap/crypto/tests/fake_provider.py | 2 - src/leap/crypto/tests/test_srpauth.py | 136 ------------------------------ src/leap/crypto/tests/test_srpregister.py | 107 ++++++++++++++++++----- 3 files changed, 86 insertions(+), 159 deletions(-) delete mode 100644 src/leap/crypto/tests/test_srpauth.py (limited to 'src') diff --git a/src/leap/crypto/tests/fake_provider.py b/src/leap/crypto/tests/fake_provider.py index 4b05bbff..d3e05812 100755 --- a/src/leap/crypto/tests/fake_provider.py +++ b/src/leap/crypto/tests/fake_provider.py @@ -329,5 +329,3 @@ if __name__ == "__main__": reactor.listenTCP(8000, factory) reactor.listenSSL(8443, factory, OpenSSLServerContextFactory()) reactor.run() - - diff --git a/src/leap/crypto/tests/test_srpauth.py b/src/leap/crypto/tests/test_srpauth.py deleted file mode 100644 index ce9403c7..00000000 --- a/src/leap/crypto/tests/test_srpauth.py +++ /dev/null @@ -1,136 +0,0 @@ -# -*- coding: utf-8 -*- -# test_srpauth.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -""" -Tests for leap/crypto/srpauth.py -""" -try: - import unittest -except ImportError: - import unittest -import os -import sys - -from mock import MagicMock -from nose.twistedtools import reactor, threaded_reactor, stop_reactor -from twisted.python import log - -from leap.common.testing.https_server import where -from leap.config.providerconfig import ProviderConfig -from leap.crypto import srpauth -from leap.crypto import srpregister -from leap.crypto.tests import fake_provider - -log.startLogging(sys.stdout) - - -def _get_capath(): - return where("cacert.pem") - -_here = os.path.split(__file__)[0] - - -class ImproperlyConfiguredError(Exception): - """ - Raised if the test provider is missing configuration - """ - - -class SRPRegisterTestCase(unittest.TestCase): - """ - Tests for the SRP Authentication class - """ - __name__ = "SRPAuth tests" - - @classmethod - def setUpClass(cls): - """ - Sets up this TestCase with a simple and faked provider instance: - - * runs a threaded reactor - * loads a mocked ProviderConfig that points to the certs in the - leap.common.testing module. - """ - factory = fake_provider.get_provider_factory() - reactor.listenTCP(8000, factory) - reactor.listenSSL( - 8443, factory, - fake_provider.OpenSSLServerContextFactory()) - threaded_reactor() - - provider = ProviderConfig() - provider.get_ca_cert_path = MagicMock() - provider.get_ca_cert_path.return_value = _get_capath() - loaded = provider.load(path=os.path.join( - _here, "test_provider.json")) - if not loaded: - raise ImproperlyConfiguredError( - "Could not load test provider config") - cls.provider = provider - cls.register = srpregister.SRPRegister(provider_config=provider) - cls.auth = srpauth.SRPAuth(provider) - cls._auth_instance = cls.auth.__dict__['_SRPAuth__instance'] - cls.authenticate = cls._auth_instance.authenticate - cls.logout = cls._auth_instance.logout - - @classmethod - def tearDownClass(cls): - """ - Stops reactor when tearing down the class - """ - stop_reactor() - - def test_auth(self): - """ - Checks whether a pair of valid credentials is able to be authenticated. - """ - TEST_USER = "register_test_auth" - TEST_PASS = "pass" - - # pristine registration, should go well - ok = self.register.register_user(TEST_USER, TEST_PASS) - self.assertTrue(ok) - - self.authenticate(TEST_USER, TEST_PASS) - with self.assertRaises(AssertionError): - # AssertionError: already logged in - # We probably could take this as its own exception - self.authenticate(TEST_USER, TEST_PASS) - - self.logout() - - # cannot log out two times in a row (there's no session) - with self.assertRaises(AssertionError): - self.logout() - - def test_auth_with_bad_credentials(self): - """ - Checks that auth does not succeed with bad credentials. - """ - TEST_USER = "register_test_auth" - TEST_PASS = "pass" - - # non-existent credentials, should fail - with self.assertRaises(srpauth.SRPAuthenticationError): - self.authenticate("baduser_1", "passwrong") - - # good user, bad password, should fail - with self.assertRaises(srpauth.SRPAuthenticationError): - self.authenticate(TEST_USER, "passwrong") - - # bad user, good password, should fail too :) - with self.assertRaises(srpauth.SRPAuthenticationError): - self.authenticate("myunclejoe", TEST_PASS) diff --git a/src/leap/crypto/tests/test_srpregister.py b/src/leap/crypto/tests/test_srpregister.py index b065958d..a59f71cb 100644 --- a/src/leap/crypto/tests/test_srpregister.py +++ b/src/leap/crypto/tests/test_srpregister.py @@ -15,7 +15,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ -Tests for leap/crypto/srpregister.py +Tests for: + * leap/crypto/srpregister.py + * leap/crypto/srpauth.py """ try: import unittest @@ -30,7 +32,7 @@ from twisted.python import log from leap.common.testing.https_server import where from leap.config.providerconfig import ProviderConfig -from leap.crypto import srpregister +from leap.crypto import srpregister, srpauth from leap.crypto.tests import fake_provider log.startLogging(sys.stdout) @@ -48,11 +50,11 @@ class ImproperlyConfiguredError(Exception): """ -class SRPRegisterTestCase(unittest.TestCase): +class SRPTestCase(unittest.TestCase): """ - Tests for the SRP Register class + Tests for the SRP Register and Auth classes """ - __name__ = "SRPRegister tests" + __name__ = "SRPRegister and SRPAuth tests" @classmethod def setUpClass(cls): @@ -60,30 +62,39 @@ class SRPRegisterTestCase(unittest.TestCase): Sets up this TestCase with a simple and faked provider instance: * runs a threaded reactor + * loads a mocked ProviderConfig that points to the certs in the + leap.common.testing module. """ factory = fake_provider.get_provider_factory() - reactor.listenTCP(8000, factory) - reactor.listenSSL( - 8443, factory, + http = reactor.listenTCP(8001, factory) + https = reactor.listenSSL( + 0, factory, fake_provider.OpenSSLServerContextFactory()) - threaded_reactor() - - def setUp(self): - """ - Sets up common parameters for each test: + get_port = lambda p: p.getHost().port + cls.http_port = get_port(http) + cls.https_port = get_port(https) - * loads a mocked ProviderConfig that points to the certs in the - leap.common.testing module. - """ provider = ProviderConfig() provider.get_ca_cert_path = MagicMock() provider.get_ca_cert_path.return_value = _get_capath() + + provider.get_api_uri = MagicMock() + provider.get_api_uri.return_value = cls._get_https_uri() + loaded = provider.load(path=os.path.join( _here, "test_provider.json")) if not loaded: raise ImproperlyConfiguredError( "Could not load test provider config") - self.register = srpregister.SRPRegister(provider_config=provider) + cls.register = srpregister.SRPRegister(provider_config=provider) + + cls.auth = srpauth.SRPAuth(provider) + cls._auth_instance = cls.auth.__dict__['_SRPAuth__instance'] + cls.authenticate = cls._auth_instance.authenticate + cls.logout = cls._auth_instance.logout + + # run! + threaded_reactor() @classmethod def tearDownClass(cls): @@ -92,6 +103,17 @@ class SRPRegisterTestCase(unittest.TestCase): """ stop_reactor() + # helper methods + + @classmethod + def _get_https_uri(cls): + """ + Returns a https uri with the right https port initialized + """ + return "https://localhost:%s" % (cls.https_port,) + + # Register tests + def test_register_user(self): """ Checks if the registration of an unused name works as expected when @@ -109,15 +131,13 @@ class SRPRegisterTestCase(unittest.TestCase): # FIXME currently we are catching this in an upper layer, # we could bring the error validation to the SRPRegister class ok = self.register.register_user("foouser_second", "barpass") - # XXX - #self.assertFalse(ok) def test_correct_http_uri(self): """ Checks that registration autocorrect http uris to https ones. """ - HTTP_URI = "http://localhost:8443" - HTTPS_URI = "https://localhost:8443/1/users" + HTTP_URI = "http://localhost:%s" % (self.https_port, ) + HTTPS_URI = "https://localhost:%s/1/users" % (self.https_port, ) provider = ProviderConfig() provider.get_ca_cert_path = MagicMock() provider.get_ca_cert_path.return_value = _get_capath() @@ -130,6 +150,7 @@ class SRPRegisterTestCase(unittest.TestCase): if not loaded: raise ImproperlyConfiguredError( "Could not load test provider config") + self.register = srpregister.SRPRegister(provider_config=provider) # ... and we check that we're correctly taking the HTTPS protocol @@ -140,3 +161,47 @@ class SRPRegisterTestCase(unittest.TestCase): self.assertTrue(ok) # XXX need to assert that _get_registration_uri was called too + + # Auth tests + + def test_auth(self): + """ + Checks whether a pair of valid credentials is able to be authenticated. + """ + TEST_USER = "register_test_auth" + TEST_PASS = "pass" + + # pristine registration, should go well + ok = self.register.register_user(TEST_USER, TEST_PASS) + self.assertTrue(ok) + + self.authenticate(TEST_USER, TEST_PASS) + with self.assertRaises(AssertionError): + # AssertionError: already logged in + # We probably could take this as its own exception + self.authenticate(TEST_USER, TEST_PASS) + + self.logout() + + # cannot log out two times in a row (there's no session) + with self.assertRaises(AssertionError): + self.logout() + + def test_auth_with_bad_credentials(self): + """ + Checks that auth does not succeed with bad credentials. + """ + TEST_USER = "register_test_auth" + TEST_PASS = "pass" + + # non-existent credentials, should fail + with self.assertRaises(srpauth.SRPAuthenticationError): + self.authenticate("baduser_1", "passwrong") + + # good user, bad password, should fail + with self.assertRaises(srpauth.SRPAuthenticationError): + self.authenticate(TEST_USER, "passwrong") + + # bad user, good password, should fail too :) + with self.assertRaises(srpauth.SRPAuthenticationError): + self.authenticate("myunclejoe", TEST_PASS) -- cgit v1.2.3 From 59c6d949611abd867bb9d3b6bf712f199a7f39d5 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 27 Mar 2013 03:56:57 +0900 Subject: fix osx prefixer and launcher --- src/leap/config/prefixers.py | 2 +- src/leap/services/eip/vpnlaunchers.py | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/config/prefixers.py b/src/leap/config/prefixers.py index 460e5b46..72211790 100644 --- a/src/leap/config/prefixers.py +++ b/src/leap/config/prefixers.py @@ -96,7 +96,7 @@ class DarwinPrefixer(Prefixer): config_dir = BaseDirectory.xdg_config_home if not standalone: return config_dir - return os.getenv("LEAP_CLIENT_PATH", config_dir) + return os.getenv(os.getcwd(), "config") class WindowsPrefixer(Prefixer): diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 57a8092e..37c6256e 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -320,7 +320,15 @@ class DarwinVPNLauncher(VPNLauncher): leap_assert(socket_host, "We need a socket host!") leap_assert(socket_port, "We need a socket port!") - openvpn_possibilities = which(self.OPENVPN_BIN) + kwargs = {} + if ProviderConfig.standalone: + kwargs['path_extension'] = os.path.join( + providerconfig.get_path_prefix(), + "..", "apps", "eip") + + openvpn_possibilities = which( + self.OPENVPN_BIN, + **kwargs) if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() @@ -391,6 +399,21 @@ class DarwinVPNLauncher(VPNLauncher): return [command] + cmd_args + def get_vpn_env(self, providerconfig): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + + @rtype: dict + """ + return {"LD_LIBRARY_PATH": os.path.join( + providerconfig.get_path_prefix(), + "..", "lib")} + class WindowsVPNLauncher(VPNLauncher): """ -- cgit v1.2.3 From 2c3593b803d88b67e8d98f6227a687a6737916ec Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 27 Mar 2013 03:56:57 +0900 Subject: fix osx prefixer and launcher --- src/leap/config/prefixers.py | 2 +- src/leap/services/eip/vpnlaunchers.py | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/config/prefixers.py b/src/leap/config/prefixers.py index 460e5b46..72211790 100644 --- a/src/leap/config/prefixers.py +++ b/src/leap/config/prefixers.py @@ -96,7 +96,7 @@ class DarwinPrefixer(Prefixer): config_dir = BaseDirectory.xdg_config_home if not standalone: return config_dir - return os.getenv("LEAP_CLIENT_PATH", config_dir) + return os.getenv(os.getcwd(), "config") class WindowsPrefixer(Prefixer): diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 57a8092e..37c6256e 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -320,7 +320,15 @@ class DarwinVPNLauncher(VPNLauncher): leap_assert(socket_host, "We need a socket host!") leap_assert(socket_port, "We need a socket port!") - openvpn_possibilities = which(self.OPENVPN_BIN) + kwargs = {} + if ProviderConfig.standalone: + kwargs['path_extension'] = os.path.join( + providerconfig.get_path_prefix(), + "..", "apps", "eip") + + openvpn_possibilities = which( + self.OPENVPN_BIN, + **kwargs) if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() @@ -391,6 +399,21 @@ class DarwinVPNLauncher(VPNLauncher): return [command] + cmd_args + def get_vpn_env(self, providerconfig): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + @param providerconfig: provider specific configuration + @type providerconfig: ProviderConfig + + @rtype: dict + """ + return {"LD_LIBRARY_PATH": os.path.join( + providerconfig.get_path_prefix(), + "..", "lib")} + class WindowsVPNLauncher(VPNLauncher): """ -- cgit v1.2.3 From 399b80dbc2a48806fe54fa4d84d513dc83269c3d Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 2 Apr 2013 23:58:34 +0900 Subject: add osx initializer --- src/leap/platform_init/initializers.py | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) (limited to 'src') diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index ac08e23f..6392a3c5 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -30,6 +30,10 @@ logger = logging.getLogger(__name__) def init_platform(): + """ + Returns the right initializer for the platform we are running in, or + None if no proper initializer is found + """ initializer = None try: initializer = globals()[platform.system() + "Initializer"] @@ -43,6 +47,10 @@ def init_platform(): def _windows_has_tap_device(): + """ + Loops over the windows registry trying to find if the tap0901 tap driver + has been installed on this machine. + """ import _winreg as reg adapter_key = 'SYSTEM\CurrentControlSet\Control\Class' \ @@ -65,6 +73,10 @@ def _windows_has_tap_device(): def WindowsInitializer(): + """ + Raises a dialog in case that the windows tap driver has not been found + in the registry, asking the user for permission to install the driver + """ if not _windows_has_tap_device(): msg = QtGui.QMessageBox() msg.setWindowTitle(msg.tr("TAP Driver")) @@ -94,3 +106,46 @@ def WindowsInitializer(): else: logger.error("Tried to install TAP driver, but the installer " "is not found or not executable") + + +def _darwin_has_tun_kext(): + """ + Returns True only if we found a directory under the system kext folder + containing a kext named tun.kext, AND we found a startup item named 'tun' + """ + # XXX we should be smarter here and use kextstats output. + has_kext = lambda: os.path.isdir("/System/Library/Extensions/tun.kext") + has_startup = lambda: os.path.isdir("/System/Library/StartupItems/tun") + return has_kext() and has_startup() + +def DarwinInitializer(): + """ + Raises a dialog in case that the osx tuntap driver has not been found + in the registry, asking the user for permission to install the driver + """ + if not _darwin_has_tun_kext(): + msg = QtGui.QMessageBox() + msg.setWindowTitle(msg.tr("TUN Driver")) + msg.setText(msg.tr("LEAPClient needs to install the necessary drivers " + "for Encrypted Internet to work. Would you like to " + "proceed?")) + msg.setInformativeText(msg.tr("Encrypted Internet uses VPN, which " + "needs a kernel extension for a TUN " + "device installed and none " + "has been found")) + msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) + msg.setDefaultButton(QtGui.QMessageBox.Yes) + ret = msg.exec_() + + if ret == QtGui.QMessageBox.Yes: + installer_path = os.path.join(os.getcwd(), + "..", + "Resources", + "tuntap-installer.app") + if os.path.isfile(installer_path) and \ + os.access(installer_path, os.X_OK): + cmd = ["open", installer_path] + ret = subprocess.call(cmd, stdout=subprocess.PIPE, shell=True) + else: + logger.error("Tried to install tuntaposx kext, but the installer " + "is not found inside this bundle, or it is not executable") -- cgit v1.2.3 From 91a60870e89e565a6136bf6d21b155dc055f4827 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 3 Apr 2013 00:42:29 +0900 Subject: fix tuntaposx invocation --- src/leap/platform_init/initializers.py | 35 ++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index 6392a3c5..3cb19fc6 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -85,7 +85,8 @@ def WindowsInitializer(): "proceed?")) msg.setInformativeText(msg.tr("Encrypted Internet uses VPN, which " "needs a TAP device installed and none " - "has been found")) + "has been found. This will ask for " + "administrative privileges.")) msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) msg.setDefaultButton(QtGui.QMessageBox.Yes) ret = msg.exec_() @@ -102,6 +103,7 @@ def WindowsInitializer(): inf_path = os.path.join(driver_path, "OemWin2k.inf") cmd = [dev_installer, "install", inf_path, "tap0901"] + # XXX should avoid shell expansion. ret = subprocess.call(cmd, stdout=subprocess.PIPE, shell=True) else: logger.error("Tried to install TAP driver, but the installer " @@ -114,15 +116,23 @@ def _darwin_has_tun_kext(): containing a kext named tun.kext, AND we found a startup item named 'tun' """ # XXX we should be smarter here and use kextstats output. + has_kext = lambda: os.path.isdir("/System/Library/Extensions/tun.kext") has_startup = lambda: os.path.isdir("/System/Library/StartupItems/tun") - return has_kext() and has_startup() + has_tun_and_startup = has_kext() and has_startup() + logger.debug('platform initializer check: has tun_and_startup = %s' % + (has_tun_and_startup,)) + return has_tun_and_startup def DarwinInitializer(): """ Raises a dialog in case that the osx tuntap driver has not been found in the registry, asking the user for permission to install the driver """ + NOTFOUND_MSG = ("Tried to install tuntaposx kext, but the installer " + "is not found inside this bundle.") + BADEXEC_MSG = ("Tried to install tuntaposx kext, but the installer " + "failed to be launched.") if not _darwin_has_tun_kext(): msg = QtGui.QMessageBox() msg.setWindowTitle(msg.tr("TUN Driver")) @@ -131,8 +141,9 @@ def DarwinInitializer(): "proceed?")) msg.setInformativeText(msg.tr("Encrypted Internet uses VPN, which " "needs a kernel extension for a TUN " - "device installed and none " - "has been found")) + "device installed, and none " + "has been found. This will ask for " + "administrative privileges.")) msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) msg.setDefaultButton(QtGui.QMessageBox.Yes) ret = msg.exec_() @@ -142,10 +153,14 @@ def DarwinInitializer(): "..", "Resources", "tuntap-installer.app") - if os.path.isfile(installer_path) and \ - os.access(installer_path, os.X_OK): - cmd = ["open", installer_path] - ret = subprocess.call(cmd, stdout=subprocess.PIPE, shell=True) + if os.path.isdir(installer_path): + cmd = ["open %s" % (installer_path,)] + try: + # XXX should avoid shell expansion + ret = subprocess.call( + cmd, stdout=subprocess.PIPE, + shell=True) + except: + logger.error(BADEXEC_MSG) else: - logger.error("Tried to install tuntaposx kext, but the installer " - "is not found inside this bundle, or it is not executable") + logger.error(NOTFOUND_MSG) -- cgit v1.2.3 From 466ccbafb38cf8fd767ed2b88d93c547a4eca9a2 Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 3 Apr 2013 03:22:08 +0900 Subject: Do not populate user/pass when deselecting "remember" Closes: #2059 --- src/leap/config/leapsettings.py | 20 ++++++++++++++++++++ src/leap/gui/mainwindow.py | 5 +++++ 2 files changed, 25 insertions(+) (limited to 'src') diff --git a/src/leap/config/leapsettings.py b/src/leap/config/leapsettings.py index 35c9fef6..19ec4a9a 100644 --- a/src/leap/config/leapsettings.py +++ b/src/leap/config/leapsettings.py @@ -64,6 +64,7 @@ class LeapSettings(object): USER_KEY = "User" AUTOLOGIN_KEY = "AutoLogin" PROPERPROVIDER_KEY = "ProperProvider" + REMEMBER_KEY = "RememberUserAndPass" def __init__(self, standalone=False): """ @@ -169,6 +170,25 @@ class LeapSettings(object): leap_assert(len(user) > 0, "We cannot save an empty user") self._settings.setValue(self.USER_KEY, user) + def get_remember(self): + """ + Returns the value of the remember selection. + + @rtype: bool + """ + return to_bool(self._settings.value(self.REMEMBER_KEY, False)) + + def set_remember(self, remember): + """ + Sets wheter the app should remember username and password + + @param remember: True if the app should remember username and + password, False otherwise + @rtype: bool + """ + leap_assert_type(remember, bool) + self._settings.setValue(self.REMEMBER_KEY, remember) + def get_autologin(self): """ Returns True if the app should automatically login, False otherwise diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 863640ef..e5efdbed 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -225,6 +225,7 @@ class MainWindow(QtGui.QMainWindow): def _remember_state_changed(self, state): self.ui.chkAutoLogin.setEnabled(state == QtCore.Qt.Checked) + self._settings.set_remember(bool(state)) def _finish_init(self): self.ui.cmbProviders.addItems(self._configured_providers()) @@ -249,6 +250,10 @@ class MainWindow(QtGui.QMainWindow): self._wizard = None self._settings.set_properprovider(True) else: + if not self._settings.get_remember(): + # nothing to do here + return + saved_user = self._settings.get_user() auto_login = self._settings.get_autologin() -- cgit v1.2.3 From 005b6ca29c75e0e0f583327b8afea601a90ed6e7 Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 3 Apr 2013 04:37:11 +0900 Subject: Makes traffic indicators display fixed precision. Closes:#2114 --- src/leap/gui/mainwindow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 863640ef..3fa3aad3 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -181,11 +181,11 @@ class MainWindow(QtGui.QMainWindow): self._stop_eip) self._action_eip_write = QtGui.QAction( QtGui.QIcon(":/images/Arrow-Up-32.png"), - "0.0 Kb", self) + "%12.2f Kb" % (0.0,), self) self._action_eip_write.setEnabled(False) self._action_eip_read = QtGui.QAction( QtGui.QIcon(":/images/Arrow-Down-32.png"), - "0.0 Kb", self) + "%12.2f Kb" % (0.0,), self) self._action_eip_read.setEnabled(False) self._action_visible = QtGui.QAction(self.tr("Hide"), self) @@ -758,12 +758,12 @@ class MainWindow(QtGui.QMainWindow): """ upload = float(data[self._vpn.TUNTAP_WRITE_KEY]) upload = upload / 1000.0 - upload_str = "%s Kb" % (upload,) + upload_str = "%12.2f Kb" % (upload,) self.ui.lblUpload.setText(upload_str) self._action_eip_write.setText(upload_str) download = float(data[self._vpn.TUNTAP_READ_KEY]) download = download / 1000.0 - download_str = "%s Kb" % (download,) + download_str = "%12.2f Kb" % (download,) self.ui.lblDownload.setText(download_str) self._action_eip_read.setText(download_str) -- cgit v1.2.3 From 22342664951ac32756ceb7ade59ada90f92c8793 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 4 Apr 2013 01:47:12 +0900 Subject: Several fixes in wizard Closes:#2061 o Rewording of setup steps in wizard, to make them more meaningful to the non-technical user. Closes: #2061 o Fix typo in wizard o Fix multiple drawing of services if going back o Make registration errors show in red o Add a warning if EIP service needs admin password. Addresses part of #2062 --- src/leap/config/providerconfig.py | 18 +++ src/leap/gui/mainwindow.py | 2 +- src/leap/gui/ui/wizard.ui | 265 ++++++++++++++++++++++++++++-------- src/leap/gui/wizard.py | 49 +++++-- src/leap/util/privilege_policies.py | 78 +++++++++++ 5 files changed, 344 insertions(+), 68 deletions(-) create mode 100644 src/leap/util/privilege_policies.py (limited to 'src') diff --git a/src/leap/config/providerconfig.py b/src/leap/config/providerconfig.py index 71b2856f..7651863b 100644 --- a/src/leap/config/providerconfig.py +++ b/src/leap/config/providerconfig.py @@ -65,6 +65,11 @@ class ProviderConfig(BaseConfig): return self._safe_get_value("domain") def get_enrollment_policy(self): + """ + Returns the enrollment policy + + @rtype: string + """ return self._safe_get_value("enrollment_policy") def get_languages(self): @@ -75,8 +80,21 @@ class ProviderConfig(BaseConfig): return self._safe_get_value("name") def get_services(self): + """ + Returns a list with the services supported by the + current provider + + @rtype: list + """ return self._safe_get_value("services") + def get_services_string(self): + """ + Returns a string with the services supported by the current provider, + ready to be shown to the user + """ + return ", ".join(self.get_services()) + def get_ca_cert_path(self, about_to_download=False): """ Returns the path to the certificate for the current provider diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 863640ef..7f529e2f 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -202,7 +202,7 @@ class MainWindow(QtGui.QMainWindow): self._wizard_firstrun = False if self._first_run(): self._wizard_firstrun = True - self._wizard = Wizard(self._checker_thread) + self._wizard = Wizard(self._checker_thread, standalone=standalone) # Give this window time to finish init and then show the wizard QtCore.QTimer.singleShot(1, self._launch_wizard) self._wizard.accepted.connect(self._finish_init) diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui index b8fed183..141418db 100644 --- a/src/leap/gui/ui/wizard.ui +++ b/src/leap/gui/ui/wizard.ui @@ -100,7 +100,7 @@ Provider selection - Please enter the domain of the provider you want to user for your connection + Please enter the domain of the provider you want to use for your connection 1 @@ -158,20 +158,20 @@ - Checking provider + Checking for a valid provider - Download provider information + Getting provider information - HTTPS Connection + Can we stablish a secure connection? @@ -244,7 +244,7 @@ - Name resolution + Can we reach this provider? @@ -278,39 +278,117 @@ Provider Information - Services offered by this provider + Description of services offered by this provider 2 - - + + - Qt::Horizontal + Qt::Vertical - 40 - 0 + 20 + 40 - + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + <b>Enrollment policy:</b> - - + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + - URL + policy + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -318,20 +396,27 @@ - - + + - Qt::Horizontal + Qt::Vertical - 40 - 0 + 20 + 40 - + + + + <b>Services offered:</b> + + + + Qt::Vertical @@ -344,36 +429,81 @@ - - + + + + Qt::Vertical + + + + 20 + 40 + + + + + + - policy + URL - - - - Desc + + + + Qt::Horizontal - - true + + + 40 + 20 + + + + + + + + services - - + + - Qt::Vertical + Qt::Horizontal - 20 - 40 + 40 + 20 + + + + + 0 + 0 + + + + + 200 + 0 + + + + Desc + + + true + + + @@ -386,11 +516,47 @@ 3 - - + + + + + Qt::Vertical + + + + 20 + 60 + + + + + + + + We are downloading some bits that we need to stablish a secure connection with the provider for the first time. + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + - Checking provider + Setting up provider @@ -440,21 +606,21 @@ - Download CA Certificate + Getting info from the Certificate Authority - Check CA Certificate Fingerprint + Do we trust this Certificate Authority? - Check API Certificate + Establishing a trust relationship with this provider @@ -496,20 +662,7 @@ - - - - Qt::Vertical - - - - 20 - 60 - - - - - + Qt::Vertical @@ -662,7 +815,7 @@ Congratulations! - You have successfully configured the LEAP client. + You have successfully configured the LEAP Client. 6 diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index dee3b230..5742dfef 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -27,6 +27,7 @@ from functools import partial from ui_wizard import Ui_Wizard from leap.config.providerconfig import ProviderConfig from leap.crypto.srpregister import SRPRegister +from leap.util.privilege_policies import is_missing_policy_permissions from leap.services.eip.providerbootstrapper import ProviderBootstrapper logger = logging.getLogger(__name__) @@ -50,9 +51,11 @@ class Wizard(QtGui.QWizard): BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" - def __init__(self, checker): + def __init__(self, checker, standalone=False): QtGui.QWizard.__init__(self) + self.standalone = standalone + self.ui = Ui_Wizard() self.ui.setupUi(self) @@ -64,14 +67,23 @@ class Wizard(QtGui.QWizard): self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png") # Correspondence for services and their name to display + # XXX need to add a note about "requires admin pass" if + # no polkit found. + EIP_LABEL = self.tr("Encrypted Internet") + + if self._is_need_eip_password_warning(): + EIP_LABEL += " " + self.tr( + "(will need admin pass to start)") + self.SERVICE_DISPLAY = [ - self.tr("Encrypted Internet") + EIP_LABEL ] self.SERVICE_CONFIG = [ "openvpn" ] self._selected_services = set() + self._shown_services = set() self._show_register = False @@ -235,7 +247,7 @@ class Wizard(QtGui.QWizard): error_msg = req.json().get("errors").get("login")[0] if not error_msg.istitle(): error_msg = "%s %s" % (old_username, error_msg) - self._set_register_status(error_msg) + self._set_register_status(error_msg, error=True) except: logger.error("Unknown error: %r" % (req.content,)) self.ui.btnRegister.setEnabled(True) @@ -451,13 +463,15 @@ class Wizard(QtGui.QWizard): for service in self._provider_config.get_services(): try: - checkbox = QtGui.QCheckBox(self) - service_index = self.SERVICE_CONFIG.index(service) - checkbox.setText(self.SERVICE_DISPLAY[service_index]) - self.ui.serviceListLayout.addWidget(checkbox) - checkbox.stateChanged.connect( - partial(self._service_selection_changed, service)) - checkbox.setChecked(True) + if service not in self._shown_services: + checkbox = QtGui.QCheckBox(self) + service_index = self.SERVICE_CONFIG.index(service) + checkbox.setText(self.SERVICE_DISPLAY[service_index]) + self.ui.serviceListLayout.addWidget(checkbox) + checkbox.stateChanged.connect( + partial(self._service_selection_changed, service)) + checkbox.setChecked(True) + self._shown_services.add(service) except ValueError: logger.error( self.tr("Something went wrong while trying to " @@ -486,7 +500,8 @@ class Wizard(QtGui.QWizard): self._provider_config) if pageId == self.PRESENT_PROVIDER_PAGE: - self.page(pageId).setSubTitle(self.tr("Services offered by %s") % + self.page(pageId).setSubTitle(self.tr("Description of services " + "offered by %s") % (self._provider_config .get_name(),)) @@ -499,6 +514,9 @@ class Wizard(QtGui.QWizard): self.ui.lblProviderDesc.setText( "%s" % (self._provider_config.get_description(lang=lang),)) + + self.ui.lblServicesOffered.setText(self._provider_config + .get_services_string()) self.ui.lblProviderPolicy.setText(self._provider_config .get_enrollment_policy()) @@ -511,6 +529,15 @@ class Wizard(QtGui.QWizard): if pageId == self.SERVICES_PAGE: self._populate_services() + def _is_need_eip_password_warning(self): + """ + Returns True if we need to add a warning about eip needing + administrative permissions to start. That can be either + because we are running in standalone mode, or because we could + not find the needed privilege escalation mechanisms being operative. + """ + return self.standalone or is_missing_policy_permissions() + def nextId(self): """ Sets the next page id for the wizard based on wether the user diff --git a/src/leap/util/privilege_policies.py b/src/leap/util/privilege_policies.py new file mode 100644 index 00000000..5bf1b476 --- /dev/null +++ b/src/leap/util/privilege_policies.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# privilege_policies.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Helpers to determine if the needed policies for privilege escalation +are operative under this client run. +""" +import logging +import os +import platform + +from abc import ABCMeta, abstractmethod + +logger = logging.getLogger(__name__) + + +def is_missing_policy_permissions(): + """ + Returns True if we do not have implemented a policy checker for this + platform, or if the policy checker exists but it cannot find the + appropriate policy mechanisms in place. + """ + _system = platform.system() + platform_checker = _system + "PolicyChecker" + policy_checker = globals().get(platform_checker, None) + if not policy_checker: + # it is true that we miss permission to escalate + # privileges without asking for password each time. + logger.debug("we could not find a policy checker implementation " + "for %s" % (_system,)) + return True + return policy_checker().is_missing_policy_permissions() + + +class PolicyChecker: + """ + Abstract PolicyChecker class + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def is_missing_policy_permissions(self): + """ + Returns True if we could not find any policy mechanisms that + are defined to be in used for this particular platform. + + @rtype: bool + """ + return True + + +class LinuxPolicyChecker(PolicyChecker): + """ + PolicyChecker for Linux + """ + LINUX_POLKIT_FILE = ("/usr/share/polkit-1/actions/" + "net.openvpn.gui.leap.policy") + + def is_missing_policy_permissions(self): + """ + Returns True if we could not find the appropriate policykit file + in place + """ + return not os.path.isfile(self.LINUX_POLKIT_FILE) -- cgit v1.2.3 From 42593d4c6bda51a544a72abc0f935633939dad49 Mon Sep 17 00:00:00 2001 From: kali Date: Mon, 8 Apr 2013 23:44:22 +0900 Subject: Several fixes as per review --- src/leap/crypto/srpauth.py | 4 ++-- src/leap/crypto/srpregister.py | 12 +++++------- src/leap/crypto/tests/fake_provider.py | 29 ++++++++++++++++++++++++++++- src/leap/crypto/tests/test.txt | 1 - src/leap/crypto/tests/test_srpregister.py | 2 +- 5 files changed, 36 insertions(+), 12 deletions(-) delete mode 100644 src/leap/crypto/tests/test.txt (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index 027ee0d7..8028a6dc 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -50,6 +50,7 @@ class SRPAuth(QtCore.QObject): LOGIN_KEY = "login" A_KEY = "A" CLIENT_AUTH_KEY = "client_auth" + SESSION_ID_KEY = "_session_id" def __init__(self, provider_config): """ @@ -272,8 +273,7 @@ class SRPAuth(QtCore.QObject): "failed")) logger.debug("Session verified.") - SESSION_ID_KEY = "_session_id" - session_id = self._session.cookies.get(SESSION_ID_KEY, None) + session_id = self._session.cookies.get(self.SESSION_ID_KEY, None) if not session_id: logger.error("Bad cookie from server (missing _session_id)") raise SRPAuthenticationError(self.tr("Session cookie " diff --git a/src/leap/crypto/srpregister.py b/src/leap/crypto/srpregister.py index dc137aeb..59aaf257 100644 --- a/src/leap/crypto/srpregister.py +++ b/src/leap/crypto/srpregister.py @@ -125,6 +125,7 @@ class SRPRegister(QtCore.QObject): logger.debug("Will try to register user = %s" % (username,)) logger.debug("user_data => %r" % (user_data,)) + ok = None try: req = self._session.post(uri, data=user_data, @@ -134,15 +135,12 @@ class SRPRegister(QtCore.QObject): except requests.exceptions.SSLError as exc: logger.error("SSLError: %s" % exc.message) - _ok = False req = None - + ok = False else: - _ok = req.ok - - self.registration_finished.emit(_ok, req) - - return _ok + ok = req.ok + self.registration_finished.emit(ok, req) + return ok if __name__ == "__main__": diff --git a/src/leap/crypto/tests/fake_provider.py b/src/leap/crypto/tests/fake_provider.py index d3e05812..d533b82b 100755 --- a/src/leap/crypto/tests/fake_provider.py +++ b/src/leap/crypto/tests/fake_provider.py @@ -78,6 +78,9 @@ safe_unhexlify = lambda x: binascii.unhexlify(x) \ class IUser(Interface): + """ + Defines the User Interface + """ login = Attribute("User login.") salt = Attribute("Password salt.") verifier = Attribute("Password verifier.") @@ -86,6 +89,10 @@ class IUser(Interface): class User(object): + """ + User object. + We store it in our simple session mocks + """ implements(IUser) @@ -94,20 +101,37 @@ class User(object): self.salt = salt self.verifier = verifier self.session = None + self.svr = None def set_server_verifier(self, svr): + """ + Adds a svr verifier object to this + User instance + """ self.svr = svr def set_session(self, session): + """ + Adds this instance of User to the + global session dict + """ _SESSIONDB[session] = self self.session = session class FakeUsers(Resource): + """ + Resource that handles user registration. + """ + def __init__(self, name): self.name = name def render_POST(self, request): + """ + Handles POST to the users api resource + Simulates a login. + """ args = request.args login = args['user[login]'][0] @@ -268,11 +292,14 @@ class FakeSession(Resource): class API_Sessions(Resource): + """ + Top resource for the API v1 + """ def getChild(self, name, request): return FakeSession(name) -class OpenSSLServerContextFactory: +class OpenSSLServerContextFactory(object): def getContext(self): """ diff --git a/src/leap/crypto/tests/test.txt b/src/leap/crypto/tests/test.txt deleted file mode 100644 index d6406617..00000000 --- a/src/leap/crypto/tests/test.txt +++ /dev/null @@ -1 +0,0 @@ -OK! diff --git a/src/leap/crypto/tests/test_srpregister.py b/src/leap/crypto/tests/test_srpregister.py index a59f71cb..5ba7306f 100644 --- a/src/leap/crypto/tests/test_srpregister.py +++ b/src/leap/crypto/tests/test_srpregister.py @@ -20,7 +20,7 @@ Tests for: * leap/crypto/srpauth.py """ try: - import unittest + import unittest2 as unittest except ImportError: import unittest import os -- cgit v1.2.3 From 2c4cbe8f0e77e0b7cb08fd2dec3cd43bab6ac62e Mon Sep 17 00:00:00 2001 From: kali Date: Mon, 8 Apr 2013 23:54:42 +0900 Subject: fix dyld_library_path for osx --- src/leap/services/eip/vpnlaunchers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 37c6256e..3d36736d 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -410,7 +410,7 @@ class DarwinVPNLauncher(VPNLauncher): @rtype: dict """ - return {"LD_LIBRARY_PATH": os.path.join( + return {"DYLD_LIBRARY_PATH": os.path.join( providerconfig.get_path_prefix(), "..", "lib")} -- cgit v1.2.3 From 3b253461e79f286f29b890cd0e4adb94c2695393 Mon Sep 17 00:00:00 2001 From: kali Date: Tue, 9 Apr 2013 00:06:27 +0900 Subject: add TODO about refactor install dialog --- src/leap/platform_init/initializers.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index 3cb19fc6..055c90a2 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -124,6 +124,7 @@ def _darwin_has_tun_kext(): (has_tun_and_startup,)) return has_tun_and_startup + def DarwinInitializer(): """ Raises a dialog in case that the osx tuntap driver has not been found @@ -133,6 +134,11 @@ def DarwinInitializer(): "is not found inside this bundle.") BADEXEC_MSG = ("Tried to install tuntaposx kext, but the installer " "failed to be launched.") + + # TODO DRY this with other cases, and + # factor out to _should_install() function. + # Leave the dialog as a more generic thing. + if not _darwin_has_tun_kext(): msg = QtGui.QMessageBox() msg.setWindowTitle(msg.tr("TUN Driver")) -- cgit v1.2.3 From e75fe6ffd58b067b3fc32196ad245d26a1287e99 Mon Sep 17 00:00:00 2001 From: kali Date: Tue, 9 Apr 2013 00:43:13 +0900 Subject: fixes as per review --- src/leap/gui/ui/wizard.ui | 184 ++++++++++++++++++------------------ src/leap/gui/wizard.py | 13 ++- src/leap/util/privilege_policies.py | 4 + 3 files changed, 106 insertions(+), 95 deletions(-) (limited to 'src') diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui index 141418db..ed7fe37c 100644 --- a/src/leap/gui/ui/wizard.ui +++ b/src/leap/gui/ui/wizard.ui @@ -284,8 +284,8 @@ 2 - - + + Qt::Vertical @@ -297,8 +297,8 @@ - - + + Qt::Vertical @@ -310,8 +310,8 @@ - - + + Qt::Vertical @@ -323,15 +323,8 @@ - - - - <b>Enrollment policy:</b> - - - - - + + Qt::Vertical @@ -343,8 +336,8 @@ - - + + Qt::Vertical @@ -356,15 +349,8 @@ - - - - policy - - - - - + + Qt::Vertical @@ -376,7 +362,21 @@ - + + + + Name + + + + + + + URL + + + + Qt::Horizontal @@ -390,66 +390,55 @@ - - - Name - - - - - + - Qt::Vertical + Qt::Horizontal - 20 - 40 + 40 + 20 - - - - <b>Services offered:</b> - - - - - - - Qt::Vertical - - + + + - 20 - 40 + 0 + 0 - - - - - - Qt::Vertical - - + - 20 - 40 + 200 + 0 - + + Desc + + + true + + - - + + - URL + <b>Services offered:</b> + + + + + + + services - + Qt::Horizontal @@ -462,47 +451,58 @@ - - + + - services + <b>Enrollment policy:</b> - - + + + + policy + + + + + - Qt::Horizontal + Qt::Vertical - 40 - 20 + 20 + 40 - - - - - 0 - 0 - + + + + Qt::Vertical - + - 200 - 0 + 20 + 40 - - Desc + + + + + + Qt::Vertical - - true + + + 20 + 40 + - + @@ -533,7 +533,7 @@ - We are downloading some bits that we need to stablish a secure connection with the provider for the first time. + We are downloading some bits that we need to establish a secure connection with the provider for the first time. true diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 5742dfef..cc00039b 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -52,6 +52,15 @@ class Wizard(QtGui.QWizard): BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" def __init__(self, checker, standalone=False): + """ + Constructor for the main Wizard. + + @param checker: Checker thread that the wizard should use. + @type checker: CheckerThread + @param standalone: If True, the application is running as standalone + and the wizard should display some messages according to this. + @type standalone: bool + """ QtGui.QWizard.__init__(self) self.standalone = standalone @@ -67,13 +76,11 @@ class Wizard(QtGui.QWizard): self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png") # Correspondence for services and their name to display - # XXX need to add a note about "requires admin pass" if - # no polkit found. EIP_LABEL = self.tr("Encrypted Internet") if self._is_need_eip_password_warning(): EIP_LABEL += " " + self.tr( - "(will need admin pass to start)") + "(
will need admin password to start
)") self.SERVICE_DISPLAY = [ EIP_LABEL diff --git a/src/leap/util/privilege_policies.py b/src/leap/util/privilege_policies.py index 5bf1b476..e74c4d33 100644 --- a/src/leap/util/privilege_policies.py +++ b/src/leap/util/privilege_policies.py @@ -32,6 +32,8 @@ def is_missing_policy_permissions(): Returns True if we do not have implemented a policy checker for this platform, or if the policy checker exists but it cannot find the appropriate policy mechanisms in place. + + @rtype: bool """ _system = platform.system() platform_checker = _system + "PolicyChecker" @@ -74,5 +76,7 @@ class LinuxPolicyChecker(PolicyChecker): """ Returns True if we could not find the appropriate policykit file in place + + @rtype: bool """ return not os.path.isfile(self.LINUX_POLKIT_FILE) -- cgit v1.2.3 From a0497a6e1a4ef556e55299c967441e237a5e7bce Mon Sep 17 00:00:00 2001 From: kali Date: Tue, 9 Apr 2013 20:03:54 +0900 Subject: remove lambdas --- src/leap/platform_init/initializers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index 3cb19fc6..049d32a2 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -117,13 +117,14 @@ def _darwin_has_tun_kext(): """ # XXX we should be smarter here and use kextstats output. - has_kext = lambda: os.path.isdir("/System/Library/Extensions/tun.kext") - has_startup = lambda: os.path.isdir("/System/Library/StartupItems/tun") - has_tun_and_startup = has_kext() and has_startup() + has_kext = os.path.isdir("/System/Library/Extensions/tun.kext") + has_startup = os.path.isdir("/System/Library/StartupItems/tun") + has_tun_and_startup = has_kext and has_startup logger.debug('platform initializer check: has tun_and_startup = %s' % (has_tun_and_startup,)) return has_tun_and_startup + def DarwinInitializer(): """ Raises a dialog in case that the osx tuntap driver has not been found -- cgit v1.2.3 From 785bdfaceb9f3d392f162dd818fea9098d9375a4 Mon Sep 17 00:00:00 2001 From: kali Date: Tue, 9 Apr 2013 20:17:03 +0900 Subject: fix markup typo s/br/b --- src/leap/gui/wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index cc00039b..7759b98d 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -80,7 +80,7 @@ class Wizard(QtGui.QWizard): if self._is_need_eip_password_warning(): EIP_LABEL += " " + self.tr( - "(
will need admin password to start
)") + "(will need admin password to start)") self.SERVICE_DISPLAY = [ EIP_LABEL -- cgit v1.2.3 From bcfdc13f5450e3df695c61c44bf637b18386f31a Mon Sep 17 00:00:00 2001 From: kali Date: Tue, 9 Apr 2013 20:48:29 +0900 Subject: make state switch more readable --- src/leap/gui/mainwindow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index e5efdbed..3c256bea 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -224,8 +224,9 @@ class MainWindow(QtGui.QMainWindow): self._wizard = None def _remember_state_changed(self, state): - self.ui.chkAutoLogin.setEnabled(state == QtCore.Qt.Checked) - self._settings.set_remember(bool(state)) + enable = True if state == QtCore.Qt.Checked else False + self.ui.chkAutoLogin.setEnabled(enable) + self._settings.set_remember(enable) def _finish_init(self): self.ui.cmbProviders.addItems(self._configured_providers()) -- cgit v1.2.3 From 3dc9110df56c2919acacb0622915823bfde51d5f Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 10 Apr 2013 00:12:20 +0900 Subject: baseconfig moved to leap.common.config --- src/leap/config/baseconfig.py | 186 --------------- src/leap/config/leapsettings.py | 2 +- src/leap/config/pluggableconfig.py | 475 ------------------------------------- src/leap/config/prefixers.py | 133 ----------- src/leap/config/providerconfig.py | 2 +- src/leap/services/eip/eipconfig.py | 2 +- 6 files changed, 3 insertions(+), 797 deletions(-) delete mode 100644 src/leap/config/baseconfig.py delete mode 100644 src/leap/config/pluggableconfig.py delete mode 100644 src/leap/config/prefixers.py (limited to 'src') diff --git a/src/leap/config/baseconfig.py b/src/leap/config/baseconfig.py deleted file mode 100644 index f5c07184..00000000 --- a/src/leap/config/baseconfig.py +++ /dev/null @@ -1,186 +0,0 @@ -# -*- coding: utf-8 -*- -# baseconfig.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Implements the abstract base class for configuration -""" - -import copy -import logging -import functools -import os - -from abc import ABCMeta, abstractmethod - -from leap.common.check import leap_assert -from leap.common.files import mkdir_p -from leap.config.pluggableconfig import PluggableConfig -from leap.config.prefixers import get_platform_prefixer - -logger = logging.getLogger(__name__) - - -class BaseConfig: - """ - Abstract base class for any JSON based configuration - """ - - __metaclass__ = ABCMeta - - """ - Standalone is a class wide parameter - - @param standalone: if True it will return the prefix for a - standalone application. Otherwise, it will return the system - default for configuration storage. - @type standalone: bool - """ - standalone = False - - def __init__(self): - self._data = {} - self._config_checker = None - - @abstractmethod - def _get_spec(self): - """ - Returns the spec object for the specific configuration - """ - return None - - def _safe_get_value(self, key): - """ - Tries to return a value only if the config has already been loaded - - @rtype: depends on the config structure, dict, str, array, int - @return: returns the value for the specified key in the config - """ - leap_assert(self._config_checker, "Load the config first") - return self._config_checker.config[key] - - def get_path_prefix(self): - """ - Returns the platform dependant path prefixer - - """ - return get_platform_prefixer().get_path_prefix( - standalone=self.standalone) - - def loaded(self): - """ - Returns True if the configuration has been already - loaded. False otherwise - """ - return self._config_checker is not None - - def save(self, path_list): - """ - Saves the current configuration to disk - - @param path_list: list of components that form the relative - path to configuration. The absolute path will be calculated - depending on the platform. - @type path_list: list - - @return: True if saved to disk correctly, False otherwise - """ - config_path = os.path.join(self.get_path_prefix(), *(path_list[:-1])) - mkdir_p(config_path) - - try: - self._config_checker.serialize(os.path.join(config_path, - path_list[-1])) - except Exception as e: - logger.warning("%s" % (e,)) - raise - return True - - def load(self, path="", data=None, mtime=None): - """ - Loads the configuration from disk - - @type path: str - @param path: relative path to configuration. The absolute path - will be calculated depending on the platform - - @return: True if loaded from disk correctly, False otherwise - """ - - config_path = os.path.join(self.get_path_prefix(), - path) - - self._config_checker = PluggableConfig(format="json") - self._config_checker.options = copy.deepcopy(self._get_spec()) - - try: - if data is None: - self._config_checker.load(fromfile=config_path, mtime=mtime) - else: - self._config_checker.load(data, mtime=mtime) - except Exception as e: - logger.warning("Something went wrong while loading " + - "the config from %s\n%s" % (config_path, e)) - self._config_checker = None - return False - return True - - -class LocalizedKey(object): - """ - Decorator used for keys that are localized in a configuration - """ - - def __init__(self, func, **kwargs): - self._func = func - - def __call__(self, instance, lang="en"): - """ - Tries to return the string for the specified language, otherwise - informs the problem and returns an empty string - - @param lang: language code - @type lang: str - - @return: localized value from the possible values returned by - self._func - """ - descriptions = self._func(instance) - description_lang = "" - config_lang = "en" - for key in descriptions.keys(): - if lang.startswith(key): - config_lang = key - break - - description_lang = descriptions[config_lang] - return description_lang - - def __get__(self, instance, instancetype): - """ - Implement the descriptor protocol to make decorating instance - method possible. - """ - # Return a partial function with the first argument is the instance - # of the class decorated. - return functools.partial(self.__call__, instance) - -if __name__ == "__main__": - try: - config = BaseConfig() # should throw TypeError for _get_spec - except Exception as e: - assert isinstance(e, TypeError), "Something went wrong" - print "Abstract BaseConfig class is working as expected" diff --git a/src/leap/config/leapsettings.py b/src/leap/config/leapsettings.py index 19ec4a9a..df9c9f11 100644 --- a/src/leap/config/leapsettings.py +++ b/src/leap/config/leapsettings.py @@ -24,7 +24,7 @@ import logging from PySide import QtCore from leap.common.check import leap_assert, leap_assert_type -from leap.config.prefixers import get_platform_prefixer +from leap.common.config.prefixers import get_platform_prefixer logger = logging.getLogger(__name__) diff --git a/src/leap/config/pluggableconfig.py b/src/leap/config/pluggableconfig.py deleted file mode 100644 index 8535fa6b..00000000 --- a/src/leap/config/pluggableconfig.py +++ /dev/null @@ -1,475 +0,0 @@ -# -*- coding: utf-8 -*- -# pluggableconfig.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -generic configuration handlers -""" -import copy -import json -import logging -import os -import time -import urlparse - -import jsonschema - -#from leap.base.util.translations import LEAPTranslatable -from leap.common.check import leap_assert - - -logger = logging.getLogger(__name__) - - -__all__ = ['PluggableConfig', - 'adaptors', - 'types', - 'UnknownOptionException', - 'MissingValueException', - 'ConfigurationProviderException', - 'TypeCastException'] - -# exceptions - - -class UnknownOptionException(Exception): - """exception raised when a non-configuration - value is present in the configuration""" - - -class MissingValueException(Exception): - """exception raised when a required value is missing""" - - -class ConfigurationProviderException(Exception): - """exception raised when a configuration provider is missing, etc""" - - -class TypeCastException(Exception): - """exception raised when a - configuration item cannot be coerced to a type""" - - -class ConfigAdaptor(object): - """ - abstract base class for config adaotors for - serialization/deserialization and custom validation - and type casting. - """ - def read(self, filename): - raise NotImplementedError("abstract base class") - - def write(self, config, filename): - with open(filename, 'w') as f: - self._write(f, config) - - def _write(self, fp, config): - raise NotImplementedError("abstract base class") - - def validate(self, config, schema): - raise NotImplementedError("abstract base class") - - -adaptors = {} - - -class JSONSchemaEncoder(json.JSONEncoder): - """ - custom default encoder that - casts python objects to json objects for - the schema validation - """ - def default(self, obj): - if obj is str: - return 'string' - if obj is unicode: - return 'string' - if obj is int: - return 'integer' - if obj is list: - return 'array' - if obj is dict: - return 'object' - if obj is bool: - return 'boolean' - - -class JSONAdaptor(ConfigAdaptor): - indent = 2 - extensions = ['json'] - - def read(self, _from): - if isinstance(_from, file): - _from_string = _from.read() - if isinstance(_from, str): - _from_string = _from - return json.loads(_from_string) - - def _write(self, fp, config): - fp.write(json.dumps(config, - indent=self.indent, - sort_keys=True)) - - def validate(self, config, schema_obj): - schema_json = JSONSchemaEncoder().encode(schema_obj) - schema = json.loads(schema_json) - jsonschema.validate(config, schema) - - -adaptors['json'] = JSONAdaptor() - -# -# Adaptors -# -# Allow to apply a predefined set of types to the -# specs, so it checks the validity of formats and cast it -# to proper python types. - -# TODO: -# - HTTPS uri - - -class DateType(object): - fmt = '%Y-%m-%d' - - def to_python(self, data): - return time.strptime(data, self.fmt) - - def get_prep_value(self, data): - return time.strftime(self.fmt, data) - - -class TranslatableType(object): - """ - a type that casts to LEAPTranslatable objects. - Used for labels we get from providers and stuff. - """ - - def to_python(self, data): - # TODO: add translatable - return data # LEAPTranslatable(data) - - # needed? we already have an extended dict... - #def get_prep_value(self, data): - #return dict(data) - - -class URIType(object): - - def to_python(self, data): - parsed = urlparse.urlparse(data) - if not parsed.scheme: - raise TypeCastException("uri %s has no schema" % data) - return parsed.geturl() - - def get_prep_value(self, data): - return data - - -class HTTPSURIType(object): - - def to_python(self, data): - parsed = urlparse.urlparse(data) - if not parsed.scheme: - raise TypeCastException("uri %s has no schema" % data) - if parsed.scheme != "https": - raise TypeCastException( - "uri %s does not has " - "https schema" % data) - return parsed.geturl() - - def get_prep_value(self, data): - return data - - -types = { - 'date': DateType(), - 'uri': URIType(), - 'https-uri': HTTPSURIType(), - 'translatable': TranslatableType(), -} - - -class PluggableConfig(object): - - options = {} - - def __init__(self, - adaptors=adaptors, - types=types, - format=None): - - self.config = {} - self.adaptors = adaptors - self.types = types - self._format = format - self.mtime = None - self.dirty = False - - @property - def option_dict(self): - if hasattr(self, 'options') and isinstance(self.options, dict): - return self.options.get('properties', None) - - def items(self): - """ - act like an iterator - """ - if isinstance(self.option_dict, dict): - return self.option_dict.items() - return self.options - - def validate(self, config, format=None): - """ - validate config - """ - schema = self.options - if format is None: - format = self._format - - if format: - adaptor = self.get_adaptor(self._format) - adaptor.validate(config, schema) - else: - # we really should make format mandatory... - logger.error('no format passed to validate') - - # first round of validation is ok. - # now we proceed to cast types if any specified. - self.to_python(config) - - def to_python(self, config): - """ - cast types following first type and then format indications. - """ - unseen_options = [i for i in config if i not in self.option_dict] - if unseen_options: - raise UnknownOptionException( - "Unknown options: %s" % ', '.join(unseen_options)) - - for key, value in config.items(): - _type = self.option_dict[key].get('type') - if _type is None and 'default' in self.option_dict[key]: - _type = type(self.option_dict[key]['default']) - if _type is not None: - tocast = True - if not callable(_type) and isinstance(value, _type): - tocast = False - if tocast: - try: - config[key] = _type(value) - except BaseException, e: - raise TypeCastException( - "Could not coerce %s, %s, " - "to type %s: %s" % (key, value, _type.__name__, e)) - _format = self.option_dict[key].get('format', None) - _ftype = self.types.get(_format, None) - if _ftype: - try: - config[key] = _ftype.to_python(value) - except BaseException, e: - raise TypeCastException( - "Could not coerce %s, %s, " - "to format %s: %s" % (key, value, - _ftype.__class__.__name__, - e)) - - return config - - def prep_value(self, config): - """ - the inverse of to_python method, - called just before serialization - """ - for key, value in config.items(): - _format = self.option_dict[key].get('format', None) - _ftype = self.types.get(_format, None) - if _ftype and hasattr(_ftype, 'get_prep_value'): - try: - config[key] = _ftype.get_prep_value(value) - except BaseException, e: - raise TypeCastException( - "Could not serialize %s, %s, " - "by format %s: %s" % (key, value, - _ftype.__class__.__name__, - e)) - else: - config[key] = value - return config - - # methods for adding configuration - - def get_default_values(self): - """ - return a config options from configuration defaults - """ - defaults = {} - for key, value in self.items(): - if 'default' in value: - defaults[key] = value['default'] - return copy.deepcopy(defaults) - - def get_adaptor(self, format): - """ - get specified format adaptor or - guess for a given filename - """ - adaptor = self.adaptors.get(format, None) - if adaptor: - return adaptor - - # not registered in adaptors dict, let's try all - for adaptor in self.adaptors.values(): - if format in adaptor.extensions: - return adaptor - - def filename2format(self, filename): - extension = os.path.splitext(filename)[-1] - return extension.lstrip('.') or None - - def serialize(self, filename, format=None, full=False): - if not format: - format = self._format - if not format: - format = self.filename2format(filename) - if not format: - raise Exception('Please specify a format') - # TODO: more specific exception type - - adaptor = self.get_adaptor(format) - if not adaptor: - raise Exception("Adaptor not found for format: %s" % format) - - config = copy.deepcopy(self.config) - serializable = self.prep_value(config) - adaptor.write(serializable, filename) - - if self.mtime: - self.touch_mtime(filename) - - def touch_mtime(self, filename): - mtime = self.mtime - os.utime(filename, (mtime, mtime)) - - def deserialize(self, string=None, fromfile=None, format=None): - """ - load configuration from a file or string - """ - - def _try_deserialize(): - if fromfile: - with open(fromfile, 'r') as f: - content = adaptor.read(f) - elif string: - content = adaptor.read(string) - return content - - # XXX cleanup this! - - if fromfile: - leap_assert(os.path.exists(fromfile)) - if not format: - format = self.filename2format(fromfile) - - if not format: - format = self._format - if format: - adaptor = self.get_adaptor(format) - else: - adaptor = None - - if adaptor: - content = _try_deserialize() - return content - - # no adaptor, let's try rest of adaptors - - adaptors = self.adaptors[:] - - if format: - adaptors.sort( - key=lambda x: int( - format in x.extensions), - reverse=True) - - for adaptor in adaptors: - content = _try_deserialize() - return content - - def set_dirty(self): - self.dirty = True - - def is_dirty(self): - return self.dirty - - def load(self, *args, **kwargs): - """ - load from string or file - if no string of fromfile option is given, - it will attempt to load from defaults - defined in the schema. - """ - string = args[0] if args else None - fromfile = kwargs.get("fromfile", None) - mtime = kwargs.pop("mtime", None) - self.mtime = mtime - content = None - - # start with defaults, so we can - # have partial values applied. - content = self.get_default_values() - if string and isinstance(string, str): - content = self.deserialize(string) - - if not string and fromfile is not None: - #import ipdb;ipdb.set_trace() - content = self.deserialize(fromfile=fromfile) - - if not content: - logger.error('no content could be loaded') - # XXX raise! - return - - # lazy evaluation until first level of nesting - # to allow lambdas with context-dependant info - # like os.path.expanduser - for k, v in content.iteritems(): - if callable(v): - content[k] = v() - - self.validate(content) - self.config = content - return True - - -def testmain(): # pragma: no cover - - from tests import test_validation as t - import pprint - - config = PluggableConfig(_format="json") - properties = copy.deepcopy(t.sample_spec) - - config.options = properties - config.load(fromfile='data.json') - - print 'config' - pprint.pprint(config.config) - - config.serialize('/tmp/testserial.json') - -if __name__ == "__main__": - testmain() diff --git a/src/leap/config/prefixers.py b/src/leap/config/prefixers.py deleted file mode 100644 index 72211790..00000000 --- a/src/leap/config/prefixers.py +++ /dev/null @@ -1,133 +0,0 @@ -# -*- coding: utf-8 -*- -# prefixers.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Platform dependant configuration path prefixers -""" -import os -import platform - -from abc import ABCMeta, abstractmethod - -from xdg import BaseDirectory - -from leap.common.check import leap_assert - - -class Prefixer: - """ - Abstract prefixer class - """ - - __metaclass__ = ABCMeta - - @abstractmethod - def get_path_prefix(self, standalone=False): - """ - Returns the platform dependant path prefixer - - @param standalone: if True it will return the prefix for a - standalone application. Otherwise, it will return the system - default for configuration storage. - @type standalone: bool - """ - return "" - - -def get_platform_prefixer(): - prefixer = globals()[platform.system() + "Prefixer"] - leap_assert(prefixer, "Unimplemented platform prefixer: %s" % - (platform.system(),)) - return prefixer() - - -class LinuxPrefixer(Prefixer): - """ - Config prefixer for the Linux platform - """ - - def get_path_prefix(self, standalone=False): - """ - Returns the platform dependant path prefixer. - This method expects an env variable named LEAP_CLIENT_PATH if - standalone is used. - - @param standalone: if True it will return the prefix for a - standalone application. Otherwise, it will return the system - default for configuration storage. - @type standalone: bool - """ - config_dir = BaseDirectory.xdg_config_home - if not standalone: - return config_dir - return os.path.join(os.getcwd(), "config") - - -class DarwinPrefixer(Prefixer): - """ - Config prefixer for the Darwin platform - """ - - def get_path_prefix(self, standalone=False): - """ - Returns the platform dependant path prefixer. - This method expects an env variable named LEAP_CLIENT_PATH if - standalone is used. - - @param standalone: if True it will return the prefix for a - standalone application. Otherwise, it will return the system - default for configuration storage. - @type standalone: bool - """ - config_dir = BaseDirectory.xdg_config_home - if not standalone: - return config_dir - return os.getenv(os.getcwd(), "config") - - -class WindowsPrefixer(Prefixer): - """ - Config prefixer for the Windows platform - """ - - def get_path_prefix(self, standalone=False): - """ - Returns the platform dependant path prefixer. - This method expects an env variable named LEAP_CLIENT_PATH if - standalone is used. - - @param standalone: if True it will return the prefix for a - standalone application. Otherwise, it will return the system - default for configuration storage. - @type standalone: bool - """ - config_dir = BaseDirectory.xdg_config_home - - if not standalone: - return config_dir - return os.path.join(os.getcwd(), "config") - -if __name__ == "__main__": - try: - abs_prefixer = Prefixer() - except Exception as e: - assert isinstance(e, TypeError), "Something went wrong" - print "Abstract Prefixer class is working as expected" - - linux_prefixer = LinuxPrefixer() - print linux_prefixer.get_path_prefix(standalone=True) - print linux_prefixer.get_path_prefix() diff --git a/src/leap/config/providerconfig.py b/src/leap/config/providerconfig.py index 7651863b..5aa0cc6e 100644 --- a/src/leap/config/providerconfig.py +++ b/src/leap/config/providerconfig.py @@ -22,7 +22,7 @@ import logging import os from leap.common.check import leap_assert -from leap.config.baseconfig import BaseConfig, LocalizedKey +from leap.common.config.baseconfig import BaseConfig, LocalizedKey from leap.config.provider_spec import leap_provider_spec logger = logging.getLogger(__name__) diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index 3f873878..4e74687a 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -22,7 +22,7 @@ import os import logging from leap.common.check import leap_assert, leap_assert_type -from leap.config.baseconfig import BaseConfig +from leap.common.config.baseconfig import BaseConfig from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipspec import eipservice_config_spec -- cgit v1.2.3 From e1d8032f4a0c4e8e8d46c017af3ffb91ea98946c Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Tue, 9 Apr 2013 10:27:42 -0300 Subject: Display a simple message when updates are available --- src/leap/gui/mainwindow.py | 65 +++++++++++++++++++++++ src/leap/gui/ui/mainwindow.ui | 119 +++++++++++++++++++++++++++++++++--------- 2 files changed, 159 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 6a73fb76..71dff1c5 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -42,6 +42,11 @@ from leap.services.eip.vpnlaunchers import (VPNLauncherException, EIPNoPolkitAuthAgentAvailable) from leap.util import __version__ as VERSION from leap.util.checkerthread import CheckerThread +from leap.common.events import server +from leap.common.events import ( + register, + events_pb2 as proto, +) from ui_mainwindow import Ui_MainWindow @@ -61,6 +66,8 @@ class MainWindow(QtGui.QMainWindow): # Keyring KEYRING_KEY = "leap_client" + new_updates = QtCore.Signal(object) + def __init__(self, standalone=False): """ Constructor for the client main window @@ -71,6 +78,12 @@ class MainWindow(QtGui.QMainWindow): """ QtGui.QMainWindow.__init__(self) + server.ensure_server(port=8090) + + register(signal=proto.UPDATER_NEW_UPDATES, + callback=self._new_updates_available) + self._updates_content = "" + self.CONNECTING_ICON = QtGui.QPixmap(":/images/conn_connecting.png") self.CONNECTED_ICON = QtGui.QPixmap(":/images/conn_connected.png") self.ERROR_ICON = QtGui.QPixmap(":/images/conn_error.png") @@ -196,6 +209,11 @@ class MainWindow(QtGui.QMainWindow): self._center_window() + self.ui.lblNewUpdates.setVisible(False) + self.ui.btnMore.setVisible(False) + self.ui.btnMore.clicked.connect(self._updates_details) + self.new_updates.connect(self._react_to_new_updates) + init_platform() self._wizard = None @@ -228,6 +246,53 @@ class MainWindow(QtGui.QMainWindow): self.ui.chkAutoLogin.setEnabled(enable) self._settings.set_remember(enable) + def _new_updates_available(self, req): + """ + Callback for the new updates event + + @param req: Request type + @type req: leap.common.events.events_pb2.SignalRequest + """ + self.new_updates.emit(req) + + def _react_to_new_updates(self, req): + """ + SLOT + TRIGGER: self._new_updates_available + + Displays the new updates label and sets the updates_content + """ + self.moveToThread(QtCore.QCoreApplication.instance().thread()) + self.ui.lblNewUpdates.setVisible(True) + self.ui.btnMore.setVisible(True) + self._updates_content = req.content + + def _updates_details(self): + """ + Parses and displays the updates details + """ + msg = "" + if len(self._updates_content) == 0: + # We assume that if there is nothing in the contents, then + # the LEAPClient bundle is what needs updating. + msg = self.tr("The LEAPClient app is ready to update, please" + " restart the application.") + else: + files = self._updates_content.split(", ") + files_str = "" + for f in files: + final_name = f.replace("/data/", "") + final_name = final_name.replace(".thp", "") + files_str += final_name + files_str += "\n" + msg = self.tr("The LEAPClient app is ready to update, please" + " restart the application so the following " + "components get updated:\n%s") % (files_str,) + + QtGui.QMessageBox.information(self, + self.tr("Updates available"), + msg) + def _finish_init(self): self.ui.cmbProviders.addItems(self._configured_providers()) self._show_systray() diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui index ca3db95c..b42ff180 100644 --- a/src/leap/gui/ui/mainwindow.ui +++ b/src/leap/gui/ui/mainwindow.ui @@ -6,7 +6,7 @@ 0 0 - 405 + 415 579
@@ -28,7 +28,20 @@ - + + + + Qt::Vertical + + + + 20 + 40 + + + + + Qt::Vertical @@ -41,8 +54,8 @@ - - + + Qt::Horizontal @@ -54,20 +67,20 @@ - - + + - Qt::Vertical + Qt::Horizontal - 20 - 40 + 40 + 20 - + 1 @@ -290,20 +303,7 @@
- - - - Qt::Horizontal - - - - 40 - 20 - - - - - + false @@ -319,6 +319,75 @@ + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + There are new updates available, please restart. + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + More... + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + +
@@ -326,7 +395,7 @@ 0 0 - 405 + 415 25 -- cgit v1.2.3 From 0cbff90910fa35a9489a5542d093211c61d2267f Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Wed, 10 Apr 2013 09:27:14 -0300 Subject: Move ensure server to the very beginning Also, fix documentation for a slot and mini-refactor a method for improved translations. --- src/leap/app.py | 3 +++ src/leap/gui/mainwindow.py | 24 +++++++++++------------- 2 files changed, 14 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/leap/app.py b/src/leap/app.py index 7cf78dc9..4112b404 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -22,6 +22,7 @@ import sys from functools import partial from PySide import QtCore, QtGui +from leap.common.events import server from leap.util import __version__ as VERSION from leap.util import leap_argparse from leap.gui import locale_rc @@ -44,6 +45,8 @@ def main(): Launches the main event loop """ + server.ensure_server(port=8090) + _, opts = leap_argparse.init_leapc_args() debug = opts.debug standalone = opts.standalone diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 71dff1c5..0d59e24b 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -42,7 +42,6 @@ from leap.services.eip.vpnlaunchers import (VPNLauncherException, EIPNoPolkitAuthAgentAvailable) from leap.util import __version__ as VERSION from leap.util.checkerthread import CheckerThread -from leap.common.events import server from leap.common.events import ( register, events_pb2 as proto, @@ -78,8 +77,6 @@ class MainWindow(QtGui.QMainWindow): """ QtGui.QMainWindow.__init__(self) - server.ensure_server(port=8090) - register(signal=proto.UPDATER_NEW_UPDATES, callback=self._new_updates_available) self._updates_content = "" @@ -269,15 +266,17 @@ class MainWindow(QtGui.QMainWindow): def _updates_details(self): """ + SLOT + TRIGGER: self.ui.btnMore.clicked + Parses and displays the updates details """ - msg = "" - if len(self._updates_content) == 0: - # We assume that if there is nothing in the contents, then - # the LEAPClient bundle is what needs updating. - msg = self.tr("The LEAPClient app is ready to update, please" - " restart the application.") - else: + msg = self.tr("The LEAPClient app is ready to update, please" + " restart the application.") + + # We assume that if there is nothing in the contents, then + # the LEAPClient bundle is what needs updating. + if len(self._updates_content) > 0: files = self._updates_content.split(", ") files_str = "" for f in files: @@ -285,9 +284,8 @@ class MainWindow(QtGui.QMainWindow): final_name = final_name.replace(".thp", "") files_str += final_name files_str += "\n" - msg = self.tr("The LEAPClient app is ready to update, please" - " restart the application so the following " - "components get updated:\n%s") % (files_str,) + msg += self.tr(" The following components will be updated:\n%s") \ + % (files_str,) QtGui.QMessageBox.information(self, self.tr("Updates available"), -- cgit v1.2.3 From 816104f2983e0f6dcded621c02c4c520e2e36da6 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 4 Apr 2013 05:16:36 +0900 Subject: detection of multiple instances using flock. sending RAISE_WINDOW leap event if we are not the main instance. --- src/leap/app.py | 35 +++-- src/leap/gui/mainwindow.py | 54 ++++++- src/leap/platform_init/__init__.py | 28 ++++ src/leap/platform_init/locks.py | 312 +++++++++++++++++++++++++++++++++++++ 4 files changed, 413 insertions(+), 16 deletions(-) create mode 100644 src/leap/platform_init/locks.py (limited to 'src') diff --git a/src/leap/app.py b/src/leap/app.py index 4112b404..c9f39e98 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -17,16 +17,20 @@ import logging import signal +import socket import sys from functools import partial + from PySide import QtCore, QtGui -from leap.common.events import server +from leap.common.events import server as event_server from leap.util import __version__ as VERSION from leap.util import leap_argparse from leap.gui import locale_rc from leap.gui.mainwindow import MainWindow +from leap.platform_init import IS_MAC +from leap.platform_init.locks import we_are_the_one_and_only import codecs codecs.register(lambda name: codecs.lookup('utf-8') @@ -34,8 +38,12 @@ codecs.register(lambda name: codecs.lookup('utf-8') def sigint_handler(*args, **kwargs): + """ + Signal handler for SIGINT + """ logger = kwargs.get('logger', None) - logger.debug('SIGINT catched. shutting down...') + if logger: + logger.debug("SIGINT catched. shutting down...") mainwindow = args[0] mainwindow.quit() @@ -44,8 +52,7 @@ def main(): """ Launches the main event loop """ - - server.ensure_server(port=8090) + event_server.ensure_server(event_server.SERVER_PORT) _, opts = leap_argparse.init_leapc_args() debug = opts.debug @@ -67,6 +74,15 @@ def main(): console.setFormatter(formatter) logger.addHandler(console) + if we_are_the_one_and_only(): + event_server.ensure_server(event_server.SERVER_PORT) + else: + # leap-client is already running + logger.warning("Tried to launch more than one instance " + "of leap-client. Raising the existing " + "one instead.") + sys.exit(1) + logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') logger.info('LEAP client version %s', VERSION) logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') @@ -98,22 +114,19 @@ def main(): app.setApplicationName("leap") app.setOrganizationDomain("leap.se") - # TODO: check if the leap-client is already running and quit - # gracefully in that case. - - window = MainWindow(standalone) - window.show() - # This dummy timer ensures that control is given to the outside # loop, so we can hook our sigint handler. timer = QtCore.QTimer() timer.start(500) timer.timeout.connect(lambda: None) + window = MainWindow(standalone) + window.show() + sigint_window = partial(sigint_handler, window, logger=logger) signal.signal(signal.SIGINT, sigint_window) - if sys.platform == "darwin": + if IS_MAC: window.raise_() # Run main loop diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 0d59e24b..e362663a 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -26,7 +26,10 @@ from functools import partial import keyring from PySide import QtCore, QtGui + from leap.common.check import leap_assert +from leap.common.events import register +from leap.common.events import events_pb2 as proto from leap.config.leapsettings import LeapSettings from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth @@ -34,6 +37,7 @@ from leap.gui.wizard import Wizard from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.providerbootstrapper import ProviderBootstrapper +from leap.platform_init import IS_WIN from leap.platform_init.initializers import init_platform from leap.services.eip.vpn import VPN from leap.services.eip.vpnlaunchers import (VPNLauncherException, @@ -42,14 +46,9 @@ from leap.services.eip.vpnlaunchers import (VPNLauncherException, EIPNoPolkitAuthAgentAvailable) from leap.util import __version__ as VERSION from leap.util.checkerthread import CheckerThread -from leap.common.events import ( - register, - events_pb2 as proto, -) from ui_mainwindow import Ui_MainWindow - logger = logging.getLogger(__name__) @@ -65,7 +64,9 @@ class MainWindow(QtGui.QMainWindow): # Keyring KEYRING_KEY = "leap_client" + # Signals new_updates = QtCore.Signal(object) + raise_window = QtCore.Signal([]) def __init__(self, standalone=False): """ @@ -77,8 +78,12 @@ class MainWindow(QtGui.QMainWindow): """ QtGui.QMainWindow.__init__(self) + # register leap events register(signal=proto.UPDATER_NEW_UPDATES, callback=self._new_updates_available) + register(signal=proto.RAISE_WINDOW, + callback=self._on_raise_window_event) + self._updates_content = "" self.CONNECTING_ICON = QtGui.QPixmap(":/images/conn_connecting.png") @@ -110,6 +115,7 @@ class MainWindow(QtGui.QMainWindow): self._standalone = standalone self._provider_config = ProviderConfig() self._eip_config = EIPConfig() + # This is created once we have a valid provider config self._srp_auth = None @@ -166,6 +172,10 @@ class MainWindow(QtGui.QMainWindow): QtCore.QCoreApplication.instance(), QtCore.SIGNAL("aboutToQuit()"), self._checker_thread.wait) + QtCore.QCoreApplication.instance().connect( + QtCore.QCoreApplication.instance(), + QtCore.SIGNAL("aboutToQuit()"), + self._cleanup_pidfiles) self.ui.chkRemember.stateChanged.connect( self._remember_state_changed) @@ -176,6 +186,7 @@ class MainWindow(QtGui.QMainWindow): self.ui.action_about_leap.triggered.connect(self._about) self.ui.action_quit.triggered.connect(self.quit) self.ui.action_wizard.triggered.connect(self._launch_wizard) + self.raise_window.connect(self._do_raise_mainwindow) # Used to differentiate between real quits and close to tray self._really_quit = False @@ -920,6 +931,39 @@ class MainWindow(QtGui.QMainWindow): logger.debug("Finished VPN with exitCode %s" % (exitCode,)) self._stop_eip() + def _on_raise_window_event(self, req): + """ + Callback for the raise window event + """ + self.raise_window.emit() + + def _do_raise_mainwindow(self): + """ + SLOT + TRIGGERS: + self._on_raise_window_event + + Triggered when we receive a RAISE_WINDOW event. + """ + TOPFLAG = QtCore.Qt.WindowStaysOnTopHint + self.setWindowFlags(self.windowFlags() | TOPFLAG) + self.show() + self.setWindowFlags(self.windowFlags() & ~TOPFLAG) + self.show() + + def _cleanup_pidfiles(self): + """ + SLOT + TRIGGERS: + self.aboutToQuit + + Triggered on about to quit signal, removes lockfiles on a clean + shutdown + """ + if IS_WIN: + lockfile = WindowsLock() + lockfile.release_lock() + if __name__ == "__main__": import signal diff --git a/src/leap/platform_init/__init__.py b/src/leap/platform_init/__init__.py index e69de29b..2a262a30 100644 --- a/src/leap/platform_init/__init__.py +++ b/src/leap/platform_init/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# __init__.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +System constants +""" +import platform + +_system = platform.system() + +IS_WIN = True if _system == "Windows" else False +IS_MAC = True if _system == "Darwin" else False +IS_LINUX = True if _system == "Linux" else False +IS_UNIX = IS_MAC or IS_LINUX diff --git a/src/leap/platform_init/locks.py b/src/leap/platform_init/locks.py new file mode 100644 index 00000000..2cdee3d9 --- /dev/null +++ b/src/leap/platform_init/locks.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- +# locks.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Utilities for handling multi-platform file locking mechanisms +""" +import commands +import logging +import os +import platform + +from leap.common.events import signal as signal_event +from leap.common.events import events_pb2 as proto +from leap import platform_init + +if platform_init.IS_UNIX: + from fcntl import flock, LOCK_EX, LOCK_NB +else: + import errno + import glob + import shutil + import socket + + from tempfile import gettempdir + +logger = logging.getLogger(__name__) + +if platform_init.IS_UNIX: + + class UnixLock(object): + """ + Uses flock to get an exclusive lock over a file. + See man 2 flock + """ + + def __init__(self, path): + """ + iniializes t he UnixLock with the path of the + desired lockfile + """ + + self._fd = None + self.path = path + + def get_lock(self): + """ + Tries to get a lock, and writes the running pid there if successful + """ + gotit, pid = self._get_lock_and_pid() + return gotit + + def get_pid(self): + """ + Returns the pid of the locking process + """ + gotit, pid = self._get_lock_and_pid() + return pid + + def _get_lock(self): + """ + Tries to get a lock, returning True if successful + + @rtype: bool + """ + self._fd = os.open(self.path, os.O_CREAT | os.O_RDWR) + + try: + flock(self._fd, LOCK_EX | LOCK_NB) + except IOError as exc: + # could not get the lock + if exc.args[0] == 11: + # Resource temporarily unavailable + return False + else: + raise + return True + + @property + def locked_by_us(self): + """ + Returns True if the pid in the pidfile + is ours. + + @rtype: bool + """ + gotit, pid = self._get_lock_and_pid() + return pid == os.getpid() + + def _get_lock_and_pid(self): + """ + Tries to get a lock over the file. + Returns (locked, pid) tuple. + + @rtype: tuple + """ + + if self._get_lock(): + self._write_to_pidfile() + return True, None + + return False, self._read_from_pidfile() + + def _read_from_pidfile(self): + """ + Tries to read pid from the pidfile, + returns False if no content found. + """ + + pidfile = os.read( + self._fd, 16) + if not pidfile: + return False + + try: + return int(pidfile.strip()) + except Exception as exc: + exc.args += (pidfile, self.lock_file) + raise + + def _write_to_pidfile(self): + """ + Writes the pid of the running process + to the pidfile + """ + fd = self._fd + os.ftruncate(fd, 0) + os.write(fd, '%d\n' % os.getpid()) + os.fsync(fd) + + +if platform_init.IS_WIN: + + class WindowsLock(object): + """ + Creates a lock based on the atomic nature of mkdir on Windows + system calls. + """ + LOCKBASE = os.path.join(gettempdir(), "leap-client-lock") + + def __init__(self): + """ + Initializes the lock. + Sets the lock name to basename plus the process pid. + """ + self._fd = None + pid = os.getpid() + self.name = "%s-%s" % (self.LOCKBASE, pid) + self.pid = pid + + def get_lock(self): + """ + Tries to get a lock, and writes the running pid there if successful + """ + gotit = self._get_lock() + return gotit + + def _get_lock(self): + """ + Tries to write to a file with the current pid as part of the name + """ + try: + self._fd = os.makedirs(self.name) + except WindowsError as exc: + # could not create the dir + if exc.args[0] == 183: + logger.debug('cannot create dir') + # cannot create dir with existing name + return False + else: + raise + return self._is_one_pidfile()[0] + + def _is_one_pidfile(self): + """ + Returns True, pid if there is only one pidfile with the expected + base path + + @rtype: tuple + """ + pidfiles = glob.glob(self.LOCKBASE + '-*') + if len(pidfiles) == 1: + pid = pidfiles[0].split('-')[-1] + return True, int(pid) + else: + return False, None + + def get_pid(self): + """ + Returns the pid of the locking process + + @rtype: int + """ + # XXX assert there is only one? + _, pid = self._is_one_pidfile() + return pid + + def release_lock(self): + """ + Releases the pidfile dir for this process, by removing it. + """ + try: + shutil.rmtree(self.name) + return True + + except WindowsError as exc: + if exc.errno in (errno.EPIPE, errno.ENOENT, + errno.ESRCH, errno.EACCES): + logger.warning( + 'exception while trying to remove the lockfile dir') + logger.warning('errno %s: %s' % (exc.errno, exc.args[1])) + # path does not exist + return False + else: + logger.debug('errno = %s' % (exc.errno,)) + # we did not foresee this error, better add it explicitely + raise + + @property + def locked_by_us(self): + """ + Returns True if the pid in the pidfile + is ours. + + @rtype: bool + """ + _, pid = self._is_one_pidfile() + return pid == self.pid + + def write_port(self, port): + """ + Writes the port for windows control to the pidfile folder + Returns True if successful. + + @rtype: bool + """ + if not self.locked_by_us: + logger.warning("Tried to write control port to a " + "non-unique pidfile folder") + return False + port_file = os.path.join(self.name, "port") + with open(port_file, 'w') as f: + f.write("%s" % port) + return True + + def get_control_port(self): + """ + Reads control port of the main instance from the port file + in the pidfile dir + + @rtype: int + """ + pid = self.get_pid() + port_file = os.path.join(self.LOCKBASE + "-%s" % pid, "port") + port = None + try: + with open(port_file) as f: + port_str = f.read() + port = int(port_str.strip()) + except IOError as exc: + if exc.errno == errno.ENOENT: + logger.error("Tried to read port from non-existent file") + else: + # we did not know explicitely about this error + raise + return port + + +def we_are_the_one_and_only(): + """ + Returns True if we are the only instance running, False otherwise. + If we came later, send a raise signal to the main instance of the + application + + @rtype: bool + """ + _sys = platform.system() + + if _sys in ("Linux", "Darwin"): + locker = UnixLock('/tmp/leap-client.lock') + locker.get_lock() + we_are_the_one = locker.locked_by_us + if not we_are_the_one: + signal_event(proto.RAISE_WINDOW) + return we_are_the_one + + elif _sys == "Windows": + locker = WindowsLock() + locker.get_lock() + we_are_the_one = locker.locked_by_us + if not we_are_the_one: + locker.release_lock() + signal_event(proto.RAISE_WINDOW) + return we_are_the_one + + else: + logger.warning("Multi-instance checker " + "not implemented for %s" % (_sys)) + # lies, lies, lies... + return True -- cgit v1.2.3 From cf6c30f6f046401dead7a990f56bf9ad2c7cfa01 Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 10 Apr 2013 22:52:32 +0900 Subject: remove duplicated ensure_server --- src/leap/app.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/app.py b/src/leap/app.py index c9f39e98..c4a3156e 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -74,9 +74,7 @@ def main(): console.setFormatter(formatter) logger.addHandler(console) - if we_are_the_one_and_only(): - event_server.ensure_server(event_server.SERVER_PORT) - else: + if not we_are_the_one_and_only(): # leap-client is already running logger.warning("Tried to launch more than one instance " "of leap-client. Raising the existing " -- cgit v1.2.3 From 92a2fb893b02d1880aac3509b0550fb04d5bf876 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 11 Apr 2013 20:36:56 +0900 Subject: Use dark icons (light theme) for OSX Closes:#2130 --- src/leap/gui/mainwindow.py | 18 +++++++++++++++--- src/leap/platform_init/__init__.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 0d59e24b..d9e8e3f6 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -34,6 +34,7 @@ from leap.gui.wizard import Wizard from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.providerbootstrapper import ProviderBootstrapper +from leap.platform_init import IS_MAC from leap.platform_init.initializers import init_platform from leap.services.eip.vpn import VPN from leap.services.eip.vpnlaunchers import (VPNLauncherException, @@ -81,9 +82,20 @@ class MainWindow(QtGui.QMainWindow): callback=self._new_updates_available) self._updates_content = "" - self.CONNECTING_ICON = QtGui.QPixmap(":/images/conn_connecting.png") - self.CONNECTED_ICON = QtGui.QPixmap(":/images/conn_connected.png") - self.ERROR_ICON = QtGui.QPixmap(":/images/conn_error.png") + if IS_MAC: + EIP_ICONS = ( + ":/images/conn_connecting-light.png" + ":/images/conn_connected-light.png", + ":/images/conn_error-light.png") + else: + EIP_ICONS = ( + ":/images/conn_connecting.png" + ":/images/conn_connected.png", + ":/images/conn_error.png") + + self.CONNECTING_ICON = QtGui.QPixmap(EIP_ICONS[0]) + self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1]) + self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2]) self.LOGGED_OUT_ICON = QtGui.QPixmap(":/images/leap-gray-big.png") self.LOGGED_IN_ICON = QtGui.QPixmap(":/images/leap-color-big.png") diff --git a/src/leap/platform_init/__init__.py b/src/leap/platform_init/__init__.py index e69de29b..2a262a30 100644 --- a/src/leap/platform_init/__init__.py +++ b/src/leap/platform_init/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# __init__.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +System constants +""" +import platform + +_system = platform.system() + +IS_WIN = True if _system == "Windows" else False +IS_MAC = True if _system == "Darwin" else False +IS_LINUX = True if _system == "Linux" else False +IS_UNIX = IS_MAC or IS_LINUX -- cgit v1.2.3 From b585cefa15a1d7a03dd8a6c5651220ac3fd94fd1 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 11 Apr 2013 22:04:00 +0900 Subject: missing comma in tuple --- src/leap/gui/mainwindow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index d9e8e3f6..f84cb00c 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -84,12 +84,12 @@ class MainWindow(QtGui.QMainWindow): if IS_MAC: EIP_ICONS = ( - ":/images/conn_connecting-light.png" + ":/images/conn_connecting-light.png", ":/images/conn_connected-light.png", ":/images/conn_error-light.png") else: EIP_ICONS = ( - ":/images/conn_connecting.png" + ":/images/conn_connecting.png", ":/images/conn_connected.png", ":/images/conn_error.png") -- cgit v1.2.3 From fd17db8aaeda0c5997a608fd2d2e0392eb0c68ae Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Wed, 10 Apr 2013 09:56:36 -0300 Subject: Emit session_id and uid through events --- src/leap/crypto/srpauth.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index 8028a6dc..ba8ac3f5 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -26,6 +26,8 @@ from PySide import QtCore, QtGui from leap.common.check import leap_assert from leap.config.providerconfig import ProviderConfig from leap.util.checkerthread import CheckerThread +from leap.common.events import signal as events_signal +from leap.common.events import events_pb2 as proto logger = logging.getLogger(__name__) @@ -237,7 +239,11 @@ class SRPAuth(QtCore.QObject): (auth_result.status_code,)) M2 = auth_result.json().get("M2", None) - self.set_uid(auth_result.json().get("id", None)) + uid = auth_result.json().get("id", None) + + events_signal(proto.CLIENT_UID, content=uid) + + self.set_uid(uid) if M2 is None or self.get_uid() is None: logger.error("Something went wrong. Content = %r" % @@ -279,6 +285,9 @@ class SRPAuth(QtCore.QObject): raise SRPAuthenticationError(self.tr("Session cookie " "verification " "failed")) + + events_signal(proto.CLIENT_SESSION_ID, content=session_id) + self.set_session_id(session_id) def authenticate(self, username, password): -- cgit v1.2.3 From d84e2a7b30b23f20ed832c447f1bcfa325f9510d Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Fri, 12 Apr 2013 13:51:29 -0300 Subject: Add service levels to the config spec --- src/leap/config/provider_spec.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'src') diff --git a/src/leap/config/provider_spec.py b/src/leap/config/provider_spec.py index 958f7846..cf942c7b 100644 --- a/src/leap/config/provider_spec.py +++ b/src/leap/config/provider_spec.py @@ -70,6 +70,36 @@ leap_provider_spec = { 'languages': { 'type': list, 'default': ['en'] + }, + 'service': { + 'levels': { + 'type': list + }, + 'default_service_level': { + 'type': int, + 'default': 1 + }, + 'allow_free': { + 'type': unicode + }, + 'allow_paid': { + 'type': unicode + }, + 'allow_anonymous': { + 'type': unicode + }, + 'allow_registration': { + 'type': unicode + }, + 'bandwidth_limit': { + 'type': int + }, + 'allow_limited_bandwidth': { + 'type': unicode + }, + 'allow_unlimited_bandwidth': { + 'type': unicode + } } } } -- cgit v1.2.3 From 4e4268af42e6d0bbf38e6027ca36d1b9f1bdda68 Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Fri, 12 Apr 2013 14:05:29 -0300 Subject: Fix UI which was missbehaving - Remove the wrapping on checkboxes, since they don't understand HTML - Change the display of provider since providers with big names like dev.bitmask.net make the UI look bad. --- src/leap/gui/ui/wizard.ui | 173 +++++++++++----------------------------------- src/leap/gui/wizard.py | 2 +- 2 files changed, 42 insertions(+), 133 deletions(-) (limited to 'src') diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui index ed7fe37c..87d04194 100644 --- a/src/leap/gui/ui/wizard.ui +++ b/src/leap/gui/ui/wizard.ui @@ -284,125 +284,27 @@ 2
- - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - + Name - - - - URL - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - + + - Qt::Horizontal + Qt::Vertical - 40 - 20 + 20 + 40 - + @@ -424,21 +326,24 @@ - + <b>Services offered:</b> + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + - + services - + Qt::Horizontal @@ -451,22 +356,25 @@ - + <b>Enrollment policy:</b> + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + - + policy - - + + Qt::Vertical @@ -478,31 +386,32 @@ - - - - Qt::Vertical + + + + <b>URL:</b> - - - 20 - 40 - + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + - - - - Qt::Vertical + + + + URL - - - 20 - 40 - + + + + + + <b>Description:</b> - + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 7759b98d..4da4c815 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -80,7 +80,7 @@ class Wizard(QtGui.QWizard): if self._is_need_eip_password_warning(): EIP_LABEL += " " + self.tr( - "(will need admin password to start)") + "(will need admin password to start)") self.SERVICE_DISPLAY = [ EIP_LABEL -- cgit v1.2.3 From 1fbf6db1276c5bca41c4cfbcc90818d9605c1938 Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Fri, 12 Apr 2013 14:07:15 -0300 Subject: Add --danger option to not validate the first hop of certificates This is intended to be used while testing, not in production --- src/leap/app.py | 3 ++- src/leap/gui/mainwindow.py | 17 +++++++++++++---- src/leap/gui/wizard.py | 7 +++++-- src/leap/services/eip/providerbootstrapper.py | 19 +++++++++++++++---- src/leap/util/leap_argparse.py | 2 ++ 5 files changed, 37 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/leap/app.py b/src/leap/app.py index c4a3156e..bb8add0d 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -57,6 +57,7 @@ def main(): _, opts = leap_argparse.init_leapc_args() debug = opts.debug standalone = opts.standalone + bypass_checks = opts.danger # TODO: get severity from command line args if debug: @@ -118,7 +119,7 @@ def main(): timer.start(500) timer.timeout.connect(lambda: None) - window = MainWindow(standalone) + window = MainWindow(standalone, bypass_checks) window.show() sigint_window = partial(sigint_handler, window, logger=logger) diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index bf8491d0..c9743f95 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -68,13 +68,16 @@ class MainWindow(QtGui.QMainWindow): new_updates = QtCore.Signal(object) raise_window = QtCore.Signal([]) - def __init__(self, standalone=False): + def __init__(self, standalone=False, bypass_checks=False): """ Constructor for the client main window @param standalone: Set to true if the app should use configs inside its pwd @type standalone: bool + @param bypass_checks: Set to true if the app should bypass + first round of checks for CA certificates at bootstrap + @type bypass_checks: bool """ QtGui.QMainWindow.__init__(self) @@ -136,7 +139,7 @@ class MainWindow(QtGui.QMainWindow): # This thread is always running, although it's quite # lightweight when it's done setting up provider # configuration and certificate. - self._provider_bootstrapper = ProviderBootstrapper() + self._provider_bootstrapper = ProviderBootstrapper(bypass_checks) # Intermediate stages, only do something if there was an error self._provider_bootstrapper.name_resolution.connect( @@ -237,9 +240,14 @@ class MainWindow(QtGui.QMainWindow): self._wizard = None self._wizard_firstrun = False + + self._bypass_checks = bypass_checks + if self._first_run(): self._wizard_firstrun = True - self._wizard = Wizard(self._checker_thread, standalone=standalone) + self._wizard = Wizard(self._checker_thread, + standalone=standalone, + bypass_checks=bypass_checks) # Give this window time to finish init and then show the wizard QtCore.QTimer.singleShot(1, self._launch_wizard) self._wizard.accepted.connect(self._finish_init) @@ -256,7 +264,8 @@ class MainWindow(QtGui.QMainWindow): def _launch_wizard(self): if self._wizard is None: - self._wizard = Wizard(self._checker_thread) + self._wizard = Wizard(self._checker_thread, + bypass_checks=self._bypass_checks) self._wizard.exec_() self._wizard = None diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 4da4c815..33c3ed0c 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -51,7 +51,7 @@ class Wizard(QtGui.QWizard): BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" - def __init__(self, checker, standalone=False): + def __init__(self, checker, standalone=False, bypass_checks=False): """ Constructor for the main Wizard. @@ -60,6 +60,9 @@ class Wizard(QtGui.QWizard): @param standalone: If True, the application is running as standalone and the wizard should display some messages according to this. @type standalone: bool + @param bypass_checks: Set to true if the app should bypass + first round of checks for CA certificates at bootstrap + @type bypass_checks: bool """ QtGui.QWizard.__init__(self) @@ -98,7 +101,7 @@ class Wizard(QtGui.QWizard): self.ui.btnCheck.clicked.connect(self._check_provider) self.ui.lnProvider.returnPressed.connect(self._check_provider) - self._provider_bootstrapper = ProviderBootstrapper() + self._provider_bootstrapper = ProviderBootstrapper(bypass_checks) self._provider_bootstrapper.name_resolution.connect( self._name_resolution) self._provider_bootstrapper.https_connection.connect( diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 778d5149..f5559143 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -58,7 +58,14 @@ class ProviderBootstrapper(QtCore.QObject): check_ca_fingerprint = QtCore.Signal(dict) check_api_certificate = QtCore.Signal(dict) - def __init__(self): + def __init__(self, bypass_checks=False): + """ + Constructor for provider bootstrapper object + + @param bypass_checks: Set to true if the app should bypass + first round of checks for CA certificates at bootstrap + @type bypass_checks: bool + """ QtCore.QObject.__init__(self) # **************************************************** # @@ -71,6 +78,7 @@ class ProviderBootstrapper(QtCore.QObject): self._domain = None self._provider_config = None self._download_if_needed = False + self._bypass_checks = bypass_checks def _check_name_resolution(self): """ @@ -124,7 +132,8 @@ class ProviderBootstrapper(QtCore.QObject): # system to work try: - res = self._session.get("https://%s" % (self._domain,)) + res = self._session.get("https://%s" % (self._domain,), + verify=not self._bypass_checks) res.raise_for_status() https_data[self.PASSED_KEY] = True except requests.exceptions.SSLError as e: @@ -171,7 +180,8 @@ class ProviderBootstrapper(QtCore.QObject): res = self._session.get("https://%s/%s" % (self._domain, "provider.json"), - headers=headers) + headers=headers, + verify=not self._bypass_checks) res.raise_for_status() # Not modified @@ -270,7 +280,8 @@ class ProviderBootstrapper(QtCore.QObject): return download_ca_cert_data[self.PASSED_KEY] try: - res = self._session.get(self._provider_config.get_ca_cert_uri()) + res = self._session.get(self._provider_config.get_ca_cert_uri(), + verify=not self._bypass_checks) res.raise_for_status() cert_path = self._provider_config.get_ca_cert_path( diff --git a/src/leap/util/leap_argparse.py b/src/leap/util/leap_argparse.py index 66268f6f..8300e4d8 100644 --- a/src/leap/util/leap_argparse.py +++ b/src/leap/util/leap_argparse.py @@ -29,6 +29,8 @@ Launches the LEAP Client""", epilog=epilog) parser.add_argument('-d', '--debug', action="store_true", help=("Launches client in debug mode, writing debug" "info to stdout")) + parser.add_argument('--danger', action="store_true", + help=("Bypasses the certificate check for bootstrap")) parser.add_argument('-l', '--logfile', metavar="LOG FILE", nargs='?', action="store", dest="log_file", #type=argparse.FileType('w'), -- cgit v1.2.3 From 922505a562474b9951bde02f93abbea0a4df5f10 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 16 Apr 2013 22:29:29 +0900 Subject: add locations to spec --- src/leap/services/eip/eipspec.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/leap/services/eip/eipspec.py b/src/leap/services/eip/eipspec.py index 69d4d927..7fa782c7 100644 --- a/src/leap/services/eip/eipspec.py +++ b/src/leap/services/eip/eipspec.py @@ -50,6 +50,10 @@ eipservice_config_spec = { "host": "location.example.org", "ip_address": "127.0.0.1"}] }, + 'locations': { + 'type': dict, + 'default': {} + }, 'openvpn_configuration': { 'type': dict, 'default': { -- cgit v1.2.3 From ff02409fdd40adc1611a11e0be3c1dcf9e36af3e Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 16 Apr 2013 21:13:21 +0900 Subject: fix flock errno under osx --- src/leap/platform_init/locks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/platform_init/locks.py b/src/leap/platform_init/locks.py index 2cdee3d9..f1672d8e 100644 --- a/src/leap/platform_init/locks.py +++ b/src/leap/platform_init/locks.py @@ -19,6 +19,7 @@ Utilities for handling multi-platform file locking mechanisms """ import commands import logging +import errno import os import platform @@ -81,7 +82,10 @@ if platform_init.IS_UNIX: flock(self._fd, LOCK_EX | LOCK_NB) except IOError as exc: # could not get the lock - if exc.args[0] == 11: + #import ipdb; ipdb.set_trace() + + if exc.args[0] in (errno.EDEADLK, errno.EAGAIN): + # errno 11 or 35 # Resource temporarily unavailable return False else: -- cgit v1.2.3 From 3d883f79cfe5f8efecd8cbab512eae65101a8c5a Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 1 May 2013 01:48:57 +0900 Subject: add debug to config and cert uris --- src/leap/services/eip/eipbootstrapper.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 19b74856..83f0a0c2 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -102,11 +102,14 @@ class EIPBootstrapper(QtCore.QObject): if self._download_if_needed and mtime: headers['if-modified-since'] = mtime - res = self._session.get("%s/%s/%s/%s" % - (self._provider_config.get_api_uri(), - self._provider_config.get_api_version(), - "config", - "eip-service.json"), + # there is some confusion with this uri, + # it's in 1/config/eip, config/eip and config/1/eip... + config_uri = "%s/%s/config/eip-service.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('Downloading eip config from: %s' % config_uri) + + res = self._session.get(config_uri, verify=self._provider_config .get_ca_cert_path(), headers=headers) @@ -176,15 +179,15 @@ class EIPBootstrapper(QtCore.QObject): cookies = None if session_id: cookies = {"_session_id": session_id} - res = self._session.get("%s/%s/%s/" % - (self._provider_config.get_api_uri(), - self._provider_config.get_api_version(), - "cert"), + cert_uri = "%s/%s/cert" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('getting cert from uri: %s' % cert_uri) + res = self._session.get(cert_uri, verify=self._provider_config .get_ca_cert_path(), cookies=cookies) res.raise_for_status() - client_cert = res.content # TODO: check certificate validity -- cgit v1.2.3 From 21b57bfd059ff32201c3403bd5ecc00d4b7d3aed Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 1 May 2013 04:11:26 +0900 Subject: whitelist openvpn cipher parameters --- src/leap/services/eip/eipconfig.py | 27 +++- src/leap/services/eip/tests/__init__.py | 0 src/leap/services/eip/tests/test_eipconfig.py | 174 ++++++++++++++++++++++++++ 3 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 src/leap/services/eip/tests/__init__.py create mode 100644 src/leap/services/eip/tests/test_eipconfig.py (limited to 'src') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index 4e74687a..baf26bca 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -18,8 +18,9 @@ """ Provider configuration """ -import os import logging +import os +import re from leap.common.check import leap_assert, leap_assert_type from leap.common.config.baseconfig import BaseConfig @@ -33,6 +34,8 @@ class EIPConfig(BaseConfig): """ Provider configuration abstraction class """ + OPENVPN_ALLOWED_KEYS = ("auth", "cipher", "tls-cipher") + OPENVPN_CIPHERS_REGEX = re.compile("[A-Z0-9\-]+") def __init__(self): BaseConfig.__init__(self) @@ -52,7 +55,24 @@ class EIPConfig(BaseConfig): return self._safe_get_value("gateways") def get_openvpn_configuration(self): - return self._safe_get_value("openvpn_configuration") + """ + Returns a dictionary containing the openvpn configuration + parameters. + + These are sanitized with alphanumeric whitelist. + + @returns: openvpn configuration dict + @rtype: C{dict} + """ + ovpncfg = self._safe_get_value("openvpn_configuration") + config = {} + for key, value in ovpncfg.items(): + if key in self.OPENVPN_ALLOWED_KEYS and value is not None: + sanitized_val = self.OPENVPN_CIPHERS_REGEX.findall(value) + if len(sanitized_val) != 0: + _val = sanitized_val[0] + config[str(key)] = str(_val) + return config def get_serial(self): return self._safe_get_value("serial") @@ -61,6 +81,9 @@ class EIPConfig(BaseConfig): return self._safe_get_value("version") def get_gateway_ip(self, index=0): + """ + Returns the ip of the gateway + """ gateways = self.get_gateways() leap_assert(len(gateways) > 0, "We don't have any gateway!") if index > len(gateways): diff --git a/src/leap/services/eip/tests/__init__.py b/src/leap/services/eip/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/services/eip/tests/test_eipconfig.py b/src/leap/services/eip/tests/test_eipconfig.py new file mode 100644 index 00000000..1675472f --- /dev/null +++ b/src/leap/services/eip/tests/test_eipconfig.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# test_eipconfig.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +tests for eipconfig +""" +import copy +import json +import os +import unittest + +from leap.common.testing.basetest import BaseLeapTest +from leap.services.eip.eipconfig import EIPConfig + + +sample_config = { + "gateways": [ + { + "capabilities": { + "adblock": False, + "filter_dns": True, + "limited": True, + "ports": [ + "1194", + "443", + "53", + "80" + ], + "protocols": [ + "tcp", + "udp"], + "transport": [ + "openvpn"], + "user_ips": False}, + "host": "host.dev.example.org", + "ip_address": "11.22.33.44", + "location": "cyberspace" + }], + "locations": { + "ankara": { + "country_code": "XX", + "hemisphere": "S", + "name": "Antarctica", + "timezone": "+2" + } + }, + "openvpn_configuration": { + "auth": "SHA1", + "cipher": "AES-128-CBC", + "tls-cipher": "DHE-RSA-AES128-SHA" + }, + "serial": 1, + "version": 1 +} + + +class EIPConfigTest(BaseLeapTest): + + __name__ = "eip_config_tests" + #provider = "testprovider.example.org" + + maxDiff = None + + def setUp(self): + pass + + def tearDown(self): + pass + + # + # helpers + # + + def write_config(self, data): + self.configfile = os.path.join( + self.tempdir, "eipconfig.json") + conf = open(self.configfile, "w") + conf.write(json.dumps(data)) + conf.close() + + def test_load_valid_config(self): + """ + load a sample config + """ + self.write_config(sample_config) + config = EIPConfig() + self.assertRaises( + AssertionError, + config.get_clusters) + self.assertTrue(config.load(self.configfile)) + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + self.assertEqual( + config.get_gateway_ip(), + "11.22.33.44") + self.assertEqual(config.get_version(), 1) + self.assertEqual(config.get_serial(), 1) + self.assertEqual(config.get_gateways(), + sample_config["gateways"]) + self.assertEqual( + config.get_clusters(), None) + + def test_openvpnoptions(self): + """ + check the sanitization of openvpn options + """ + # extra parameters + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["extra_param"] = "FOO" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + # non allowed chars + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["auth"] = "SHA1;" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + # non allowed chars + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["auth"] = "SHA1>`&|" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + # lowercase + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["auth"] = "shaSHA1" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) + + # all characters invalid -> null value + data = copy.deepcopy(sample_config) + data['openvpn_configuration']["auth"] = "sha&*!@#;" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_openvpn_configuration(), + {'cipher': 'AES-128-CBC', + 'tls-cipher': 'DHE-RSA-AES128-SHA'}) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From f74849f4c926a83190169cae570e9ec826fd46da Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 1 May 2013 04:14:15 +0900 Subject: pep8 --- src/leap/crypto/srpregister.py | 8 ++++---- src/leap/platform_init/initializers.py | 14 ++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/leap/crypto/srpregister.py b/src/leap/crypto/srpregister.py index 59aaf257..749b6f8c 100644 --- a/src/leap/crypto/srpregister.py +++ b/src/leap/crypto/srpregister.py @@ -128,10 +128,10 @@ class SRPRegister(QtCore.QObject): ok = None try: req = self._session.post(uri, - data=user_data, - timeout=SIGNUP_TIMEOUT, - verify=self._provider_config. - get_ca_cert_path()) + data=user_data, + timeout=SIGNUP_TIMEOUT, + verify=self._provider_config. + get_ca_cert_path()) except requests.exceptions.SSLError as exc: logger.error("SSLError: %s" % exc.message) diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index cf7e71b8..7e184d8a 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -120,8 +120,9 @@ def _darwin_has_tun_kext(): has_kext = os.path.isdir("/System/Library/Extensions/tun.kext") has_startup = os.path.isdir("/System/Library/StartupItems/tun") has_tun_and_startup = has_kext and has_startup - logger.debug('platform initializer check: has tun_and_startup = %s' % - (has_tun_and_startup,)) + logger.debug( + 'platform initializer check: has tun_and_startup = %s' % + (has_tun_and_startup,)) return has_tun_and_startup @@ -155,10 +156,11 @@ def DarwinInitializer(): ret = msg.exec_() if ret == QtGui.QMessageBox.Yes: - installer_path = os.path.join(os.getcwd(), - "..", - "Resources", - "tuntap-installer.app") + installer_path = os.path.join( + os.getcwd(), + "..", + "Resources", + "tuntap-installer.app") if os.path.isdir(installer_path): cmd = ["open %s" % (installer_path,)] try: -- cgit v1.2.3 From 544717da3e95a553fa2af8555df6b4e06d9e5af2 Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 1 May 2013 04:41:11 +0900 Subject: sanitize ip address --- src/leap/services/eip/eipconfig.py | 5 ++++- src/leap/services/eip/tests/test_eipconfig.py | 22 ++++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index baf26bca..e6b93647 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -36,6 +36,7 @@ class EIPConfig(BaseConfig): """ OPENVPN_ALLOWED_KEYS = ("auth", "cipher", "tls-cipher") OPENVPN_CIPHERS_REGEX = re.compile("[A-Z0-9\-]+") + IP_REGEX = re.compile("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$") def __init__(self): BaseConfig.__init__(self) @@ -90,7 +91,9 @@ class EIPConfig(BaseConfig): index = 0 logger.warning("Provided an unknown gateway index %s, " + "defaulting to 0") - return gateways[0]["ip_address"] + ip_addr = gateways[0]["ip_address"] + if self.IP_REGEX.search(ip_addr): + return ip_addr def get_client_cert_path(self, providerconfig=None, diff --git a/src/leap/services/eip/tests/test_eipconfig.py b/src/leap/services/eip/tests/test_eipconfig.py index 1675472f..ce04c2fc 100644 --- a/src/leap/services/eip/tests/test_eipconfig.py +++ b/src/leap/services/eip/tests/test_eipconfig.py @@ -114,9 +114,9 @@ class EIPConfigTest(BaseLeapTest): self.assertEqual( config.get_clusters(), None) - def test_openvpnoptions(self): + def test_sanitize_config(self): """ - check the sanitization of openvpn options + check the sanitization of options """ # extra parameters data = copy.deepcopy(sample_config) @@ -169,6 +169,24 @@ class EIPConfigTest(BaseLeapTest): {'cipher': 'AES-128-CBC', 'tls-cipher': 'DHE-RSA-AES128-SHA'}) + # bad_ip + data = copy.deepcopy(sample_config) + data['gateways'][0]["ip_address"] = "11.22.33.44;" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_gateway_ip(), + None) + + data = copy.deepcopy(sample_config) + data['gateways'][0]["ip_address"] = "11.22.33.44`" + self.write_config(data) + config = EIPConfig() + config.load(self.configfile) + self.assertEqual( + config.get_gateway_ip(), + None) if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From c85894efdbd6f65eb2b0c2edfc216827c192c1d1 Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 1 May 2013 04:45:05 +0900 Subject: remove comment about shell expansion --- src/leap/platform_init/initializers.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'src') diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index 7e184d8a..91c7086b 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -103,7 +103,6 @@ def WindowsInitializer(): inf_path = os.path.join(driver_path, "OemWin2k.inf") cmd = [dev_installer, "install", inf_path, "tap0901"] - # XXX should avoid shell expansion. ret = subprocess.call(cmd, stdout=subprocess.PIPE, shell=True) else: logger.error("Tried to install TAP driver, but the installer " @@ -164,7 +163,6 @@ def DarwinInitializer(): if os.path.isdir(installer_path): cmd = ["open %s" % (installer_path,)] try: - # XXX should avoid shell expansion ret = subprocess.call( cmd, stdout=subprocess.PIPE, shell=True) -- cgit v1.2.3 From 2dae2703fb8c2ae7e721ce83020c0dd10ff9ca33 Mon Sep 17 00:00:00 2001 From: kali Date: Fri, 3 May 2013 02:59:22 +0900 Subject: updated documentation * documentation reviewed after rewrite, ready for 0.2.1 * updated docstrings format to fit sphinx autodoc --- src/leap/config/leapsettings.py | 69 ++++++++-------- src/leap/config/providerconfig.py | 27 +++--- src/leap/crypto/srpauth.py | 68 +++++++-------- src/leap/crypto/srpregister.py | 22 ++--- src/leap/crypto/tests/fake_provider.py | 4 +- src/leap/gui/mainwindow.py | 38 ++++----- src/leap/gui/wizard.py | 56 ++++++------- src/leap/platform_init/locks.py | 18 ++-- src/leap/services/eip/eipbootstrapper.py | 12 +-- src/leap/services/eip/providerbootstrapper.py | 59 +++++++------ src/leap/services/eip/udstelnet.py | 1 - src/leap/services/eip/vpn.py | 52 ++++++------ src/leap/services/eip/vpnlaunchers.py | 114 ++++++++++++++------------ src/leap/util/checkerthread.py | 10 +-- src/leap/util/privilege_policies.py | 6 +- src/leap/util/request_helpers.py | 6 +- 16 files changed, 286 insertions(+), 276 deletions(-) (limited to 'src') diff --git a/src/leap/config/leapsettings.py b/src/leap/config/leapsettings.py index df9c9f11..59a0a16d 100644 --- a/src/leap/config/leapsettings.py +++ b/src/leap/config/leapsettings.py @@ -34,10 +34,10 @@ def to_bool(val): Returns the boolean value corresponding to val. Will return False in case val is not a string or something that behaves like one. - @param val: value to cast - @type val: either bool already or str + :param val: value to cast + :type val: either bool already or str - @rtype: bool + :rtype: bool """ if isinstance(val, bool): return val @@ -70,9 +70,9 @@ class LeapSettings(object): """ Constructor - @param standalone: parameter used to define the location of + :param standalone: parameter used to define the location of the config - @type standalone: bool + :type standalone: bool """ settings_path = os.path.join(get_platform_prefixer() @@ -86,7 +86,7 @@ class LeapSettings(object): """ Returns the saved geometry or None if it wasn't saved - @rtype: bytearray or None + :rtype: bytearray or None """ return self._settings.value(self.GEOMETRY_KEY, None) @@ -94,8 +94,8 @@ class LeapSettings(object): """ Saves the geometry to the settings - @param geometry: bytearray representing the geometry - @type geometry: bytearray + :param geometry: bytearray representing the geometry + :type geometry: bytearray """ leap_assert(geometry, "We need a geometry") self._settings.setValue(self.GEOMETRY_KEY, geometry) @@ -104,7 +104,7 @@ class LeapSettings(object): """ Returns the window state or None if it wasn't saved - @rtype: bytearray or None + :rtype: bytearray or None """ return self._settings.value(self.WINDOWSTATE_KEY, None) @@ -112,8 +112,8 @@ class LeapSettings(object): """ Saves the window state to the settings - @param windowstate: bytearray representing the window state - @type windowstate: bytearray + :param windowstate: bytearray representing the window state + :type windowstate: bytearray """ leap_assert(windowstate, "We need a window state") self._settings.setValue(self.WINDOWSTATE_KEY, windowstate) @@ -122,10 +122,10 @@ class LeapSettings(object): """ Returns a list of enabled services for the given provider - @param provider: provider domain - @type provider: str + :param provider: provider domain + :type provider: str - @rtype: list of str + :rtype: list of str """ leap_assert(len(provider) > 0, "We need a nonempty provider") @@ -140,10 +140,11 @@ class LeapSettings(object): """ Saves the list of enabled services for the given provider - @param provider: provider domain - @type provider: str - @param services: list of services to save - @type services: list of str + :param provider: provider domain + :type provider: str + + :param services: list of services to save + :type services: list of str """ leap_assert(len(provider) > 0, "We need a nonempty provider") @@ -156,7 +157,7 @@ class LeapSettings(object): """ Returns the configured user to remember, None if there isn't one - @rtype: str or None + :rtype: str or None """ return self._settings.value(self.USER_KEY, None) @@ -164,8 +165,8 @@ class LeapSettings(object): """ Saves the user to remember - @param user: user name to remember - @type user: str + :param user: user name to remember + :type user: str """ leap_assert(len(user) > 0, "We cannot save an empty user") self._settings.setValue(self.USER_KEY, user) @@ -174,7 +175,7 @@ class LeapSettings(object): """ Returns the value of the remember selection. - @rtype: bool + :rtype: bool """ return to_bool(self._settings.value(self.REMEMBER_KEY, False)) @@ -182,9 +183,9 @@ class LeapSettings(object): """ Sets wheter the app should remember username and password - @param remember: True if the app should remember username and + :param remember: True if the app should remember username and password, False otherwise - @rtype: bool + :rtype: bool """ leap_assert_type(remember, bool) self._settings.setValue(self.REMEMBER_KEY, remember) @@ -193,7 +194,7 @@ class LeapSettings(object): """ Returns True if the app should automatically login, False otherwise - @rtype: bool + :rtype: bool """ return to_bool(self._settings.value(self.AUTOLOGIN_KEY, False)) @@ -201,8 +202,8 @@ class LeapSettings(object): """ Sets whether the app should automatically login - @param autologin: True if the app should autologin, False otherwise - @type autologin: bool + :param autologin: True if the app should autologin, False otherwise + :type autologin: bool """ leap_assert_type(autologin, bool) self._settings.setValue(self.AUTOLOGIN_KEY, autologin) @@ -211,19 +212,21 @@ class LeapSettings(object): # just one for now def get_properprovider(self): """ - Returns True if there is a properly configured provider + Returns True if there is a properly configured provider. + + .. note:: this assumes only one provider for now. - @rtype: bool + :rtype: bool """ return to_bool(self._settings.value(self.PROPERPROVIDER_KEY, False)) def set_properprovider(self, properprovider): """ - Sets wether the app should automatically login + Sets whether the app should automatically login. - @param properprovider: True if the provider is properly - configured, False otherwise - @type properprovider: bool + :param properprovider: True if the provider is properly configured, + False otherwise. + :type properprovider: bool """ leap_assert_type(properprovider, bool) self._settings.setValue(self.PROPERPROVIDER_KEY, properprovider) diff --git a/src/leap/config/providerconfig.py b/src/leap/config/providerconfig.py index 5aa0cc6e..8f75d4fe 100644 --- a/src/leap/config/providerconfig.py +++ b/src/leap/config/providerconfig.py @@ -68,7 +68,7 @@ class ProviderConfig(BaseConfig): """ Returns the enrollment policy - @rtype: string + :rtype: string """ return self._safe_get_value("enrollment_policy") @@ -82,27 +82,28 @@ class ProviderConfig(BaseConfig): def get_services(self): """ Returns a list with the services supported by the - current provider + current provider. - @rtype: list + :rtype: list """ return self._safe_get_value("services") def get_services_string(self): """ - Returns a string with the services supported by the current provider, - ready to be shown to the user + Returns a string with the services supported by the current + provider, ready to be shown to the user. """ return ", ".join(self.get_services()) def get_ca_cert_path(self, about_to_download=False): """ - Returns the path to the certificate for the current provider + Returns the path to the certificate for the current provider. - @param about_to_download: defines wether we want the path to - download the cert or not. This helps avoid checking if the - cert exists because we are about to write it. - @type about_to_download: bool + :param about_to_download: defines wether we want the path to + download the cert or not. This helps avoid + checking if the cert exists because we + are about to write it. + :type about_to_download: bool """ cert_path = os.path.join(self.get_path_prefix(), @@ -122,8 +123,10 @@ class ProviderConfig(BaseConfig): def provides_eip(self): """ - Returns True if this particular provider has the EIP - service. False otherwise + Returns True if this particular provider has the EIP service, + False otherwise. + + :rtype: bool """ return "openvpn" in self.get_services() diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index ba8ac3f5..9446cee8 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -58,8 +58,8 @@ class SRPAuth(QtCore.QObject): """ Constructor for SRPAuth implementation - @param server: Server to which we will authenticate - @type server: str + :param server: Server to which we will authenticate + :type server: str """ QtCore.QObject.__init__(self) @@ -91,11 +91,11 @@ class SRPAuth(QtCore.QObject): Rounds the val to a multiple of 2 and returns the unhexlified value - @param val: hexlified value - @type val: str + :param val: hexlified value + :type val: str - @rtype: binary hex data - @return: unhexlified val + :rtype: binary hex data + :return: unhexlified val """ return binascii.unhexlify(val) \ if (len(val) % 2 == 0) else binascii.unhexlify('0' + val) @@ -104,10 +104,10 @@ class SRPAuth(QtCore.QObject): """ Generates the SRP.User to get the A SRP parameter - @param username: username to login - @type username: str - @param password: password for the username - @type password: str + :param username: username to login + :type username: str + :param password: password for the username + :type password: str """ logger.debug("Authentication preprocessing...") self._srp_user = self._srp.User(username, @@ -125,13 +125,13 @@ class SRPAuth(QtCore.QObject): Might raise SRPAuthenticationError - @param username: username to login - @type username: str - @param password: password for the username - @type password: str + :param username: username to login + :type username: str + :param password: password for the username + :type password: str - @return: salt and B parameters - @rtype: tuple + :return: salt and B parameters + :rtype: tuple """ logger.debug("Starting authentication process...") try: @@ -184,15 +184,15 @@ class SRPAuth(QtCore.QObject): Might throw SRPAuthenticationError - @param salt: salt for the username - @type salt: str - @param B: B SRP parameter - @type B: str - @param username: username for this session - @type username: str + :param salt: salt for the username + :type salt: str + :param B: B SRP parameter + :type B: str + :param username: username for this session + :type username: str - @return: the M2 SRP parameter - @rtype: str + :return: the M2 SRP parameter + :rtype: str """ logger.debug("Processing challenge...") try: @@ -261,8 +261,8 @@ class SRPAuth(QtCore.QObject): Might throw SRPAuthenticationError - @param M2: M2 SRP parameter - @type M2: str + :param M2: M2 SRP parameter + :type M2: str """ logger.debug("Verifying session...") try: @@ -296,10 +296,10 @@ class SRPAuth(QtCore.QObject): Might raise SRPAuthenticationError - @param username: username for this session - @type username: str - @param password: password for this user - @type password: str + :param username: username for this session + :type username: str + :param password: password for this user + :type password: str """ leap_assert(self.get_session_id() is None, "Already logged in") @@ -390,10 +390,10 @@ class SRPAuth(QtCore.QObject): Might raise SRPAuthenticationError - @param username: username for this session - @type username: str - @param password: password for this user - @type password: str + :param username: username for this session + :type username: str + :param password: password for this user + :type password: str """ try: diff --git a/src/leap/crypto/srpregister.py b/src/leap/crypto/srpregister.py index 749b6f8c..07b3c917 100644 --- a/src/leap/crypto/srpregister.py +++ b/src/leap/crypto/srpregister.py @@ -48,11 +48,11 @@ class SRPRegister(QtCore.QObject): """ Constructor - @param provider_config: provider configuration instance, + :param provider_config: provider configuration instance, properly loaded - @type privider_config: ProviderConfig - @param register_path: webapp path for registering users - @type register_path; str + :type privider_config: ProviderConfig + :param register_path: webapp path for registering users + :type register_path; str """ QtCore.QObject.__init__(self) leap_assert(provider_config, "Please provide a provider") @@ -84,7 +84,7 @@ class SRPRegister(QtCore.QObject): Returns the URI where the register request should be made for the provider - @rtype: str + :rtype: str """ uri = "https://%s:%s/%s/%s" % ( @@ -99,13 +99,13 @@ class SRPRegister(QtCore.QObject): """ Registers a user with the validator based on the password provider - @param username: username to register - @type username: str - @param password: password for this username - @type password: str + :param username: username to register + :type username: str + :param password: password for this username + :type password: str - @rtype: tuple - @rparam: (ok, request) + :rtype: tuple + :rparam: (ok, request) """ salt, verifier = self._srp.create_salted_verification_key( username, diff --git a/src/leap/crypto/tests/fake_provider.py b/src/leap/crypto/tests/fake_provider.py index d533b82b..74a735ff 100755 --- a/src/leap/crypto/tests/fake_provider.py +++ b/src/leap/crypto/tests/fake_provider.py @@ -321,8 +321,8 @@ def get_provider_factory(): * port 8000 for http connections * port 8443 for https connections - @rparam: factory for a site - @rtype: Site instance + :rparam: factory for a site + :rtype: Site instance """ root = Resource() root.putChild("provider.json", File( diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index c9743f95..ccf97672 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -72,12 +72,12 @@ class MainWindow(QtGui.QMainWindow): """ Constructor for the client main window - @param standalone: Set to true if the app should use configs + :param standalone: Set to true if the app should use configs inside its pwd - @type standalone: bool - @param bypass_checks: Set to true if the app should bypass + :type standalone: bool + :param bypass_checks: Set to true if the app should bypass first round of checks for CA certificates at bootstrap - @type bypass_checks: bool + :type bypass_checks: bool """ QtGui.QMainWindow.__init__(self) @@ -278,8 +278,8 @@ class MainWindow(QtGui.QMainWindow): """ Callback for the new updates event - @param req: Request type - @type req: leap.common.events.events_pb2.SignalRequest + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest """ self.new_updates.emit(req) @@ -482,7 +482,7 @@ class MainWindow(QtGui.QMainWindow): """ Returns the available providers based on the file structure - @rtype: list + :rtype: list """ # TODO: check which providers have a valid certificate among @@ -503,7 +503,7 @@ class MainWindow(QtGui.QMainWindow): """ Returns True if there are no configured providers. False otherwise - @rtype: bool + :rtype: bool """ has_provider_on_disk = len(self._configured_providers()) != 0 is_proper_provider = self._settings.get_properprovider() @@ -519,8 +519,8 @@ class MainWindow(QtGui.QMainWindow): """ Sets the status label at the login stage to status - @param status: status message - @type status: str + :param status: status message + :type status: str """ if error: status = "%s" % (status,) @@ -530,8 +530,8 @@ class MainWindow(QtGui.QMainWindow): """ Sets the status label at the VPN stage to status - @param status: status message - @type status: str + :param status: status message + :type status: str """ self._vpn_systray.setToolTip(status) if error: @@ -542,8 +542,8 @@ class MainWindow(QtGui.QMainWindow): """ Enables or disables all the login widgets - @param enabled: wether they should be enabled or not - @type enabled: bool + :param enabled: wether they should be enabled or not + :type enabled: bool """ self.ui.lnUser.setEnabled(enabled) self.ui.lnPassword.setEnabled(enabled) @@ -575,9 +575,9 @@ class MainWindow(QtGui.QMainWindow): self._provider_config instance with it and starts the second part of the bootstrapping sequence - @param data: result from the last stage of the + :param data: result from the last stage of the run_provider_select_checks - @type data: dict + :type data: dict """ if data[self._provider_bootstrapper.PASSED_KEY]: provider = self.ui.cmbProviders.currentText() @@ -708,7 +708,7 @@ class MainWindow(QtGui.QMainWindow): """ Returns the socket and port to be used for VPN - @rtype: tuple (str, str) (host, port) + :rtype: tuple (str, str) (host, port) """ # TODO: make this properly multiplatform @@ -801,8 +801,8 @@ class MainWindow(QtGui.QMainWindow): """ Given a status step from the VPN thread, set the icon properly - @param status: status step - @type status: str + :param status: status step + :type status: str """ selected_pixmap = self.ERROR_ICON tray_message = self.tr("Encryption is OFF") diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 33c3ed0c..ad45dd8c 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -55,14 +55,14 @@ class Wizard(QtGui.QWizard): """ Constructor for the main Wizard. - @param checker: Checker thread that the wizard should use. - @type checker: CheckerThread - @param standalone: If True, the application is running as standalone + :param checker: Checker thread that the wizard should use. + :type checker: CheckerThread + :param standalone: If True, the application is running as standalone and the wizard should display some messages according to this. - @type standalone: bool - @param bypass_checks: Set to true if the app should bypass + :type standalone: bool + :param bypass_checks: Set to true if the app should bypass first round of checks for CA certificates at bootstrap - @type bypass_checks: bool + :type bypass_checks: bool """ QtGui.QWizard.__init__(self) @@ -184,15 +184,15 @@ class Wizard(QtGui.QWizard): """ Performs basic password checks to avoid really easy passwords. - @param username: username provided at the registrarion form - @type username: str - @param password: password from the registration form - @type password: str - @param password2: second password from the registration form - @type password: str + :param username: username provided at the registrarion form + :type username: str + :param password: password from the registration form + :type password: str + :param password2: second password from the registration form + :type password: str - @return: returns True if all the checks pass, False otherwise - @rtype: bool + :return: returns True if all the checks pass, False otherwise + :rtype: bool """ message = None @@ -266,8 +266,8 @@ class Wizard(QtGui.QWizard): """ Sets the status label in the registration page to status - @param status: status message to display, can be HTML - @type status: str + :param status: status message to display, can be HTML + :type status: str """ if error: status = "%s" % (status,) @@ -321,17 +321,17 @@ class Wizard(QtGui.QWizard): """ Checks a task and completes a page if specified - @param data: data as it comes from the bootstrapper thread for + :param data: data as it comes from the bootstrapper thread for a specific check - @type data: dict - @param label: label that displays the status icon for a + :type data: dict + :param label: label that displays the status icon for a specific check that corresponds to the data - @type label: QtGui.QLabel - @param complete: if True, it completes the page specified, + :type label: QtGui.QLabel + :param complete: if True, it completes the page specified, which must be of type WizardPage - @type complete: bool - @param complete_page: page id to complete - @type complete_page: int + :type complete: bool + :param complete_page: page id to complete + :type complete_page: int """ passed = data[self._provider_bootstrapper.PASSED_KEY] error = data[self._provider_bootstrapper.ERROR_KEY] @@ -450,10 +450,10 @@ class Wizard(QtGui.QWizard): Adds the service to the state if the state is checked, removes it otherwise - @param service: service to handle - @type service: str - @param state: state of the checkbox - @type state: int + :param service: service to handle + :type service: str + :param state: state of the checkbox + :type state: int """ if state == QtCore.Qt.Checked: self._selected_services = \ diff --git a/src/leap/platform_init/locks.py b/src/leap/platform_init/locks.py index f1672d8e..e5b392a3 100644 --- a/src/leap/platform_init/locks.py +++ b/src/leap/platform_init/locks.py @@ -74,7 +74,7 @@ if platform_init.IS_UNIX: """ Tries to get a lock, returning True if successful - @rtype: bool + :rtype: bool """ self._fd = os.open(self.path, os.O_CREAT | os.O_RDWR) @@ -98,7 +98,7 @@ if platform_init.IS_UNIX: Returns True if the pid in the pidfile is ours. - @rtype: bool + :rtype: bool """ gotit, pid = self._get_lock_and_pid() return pid == os.getpid() @@ -108,7 +108,7 @@ if platform_init.IS_UNIX: Tries to get a lock over the file. Returns (locked, pid) tuple. - @rtype: tuple + :rtype: tuple """ if self._get_lock(): @@ -192,7 +192,7 @@ if platform_init.IS_WIN: Returns True, pid if there is only one pidfile with the expected base path - @rtype: tuple + :rtype: tuple """ pidfiles = glob.glob(self.LOCKBASE + '-*') if len(pidfiles) == 1: @@ -205,7 +205,7 @@ if platform_init.IS_WIN: """ Returns the pid of the locking process - @rtype: int + :rtype: int """ # XXX assert there is only one? _, pid = self._is_one_pidfile() @@ -238,7 +238,7 @@ if platform_init.IS_WIN: Returns True if the pid in the pidfile is ours. - @rtype: bool + :rtype: bool """ _, pid = self._is_one_pidfile() return pid == self.pid @@ -248,7 +248,7 @@ if platform_init.IS_WIN: Writes the port for windows control to the pidfile folder Returns True if successful. - @rtype: bool + :rtype: bool """ if not self.locked_by_us: logger.warning("Tried to write control port to a " @@ -264,7 +264,7 @@ if platform_init.IS_WIN: Reads control port of the main instance from the port file in the pidfile dir - @rtype: int + :rtype: int """ pid = self.get_pid() port_file = os.path.join(self.LOCKBASE + "-%s" % pid, "port") @@ -288,7 +288,7 @@ def we_are_the_one_and_only(): If we came later, send a raise signal to the main instance of the application - @rtype: bool + :rtype: bool """ _sys = platform.system() diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 83f0a0c2..a881f235 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -73,8 +73,8 @@ class EIPBootstrapper(QtCore.QObject): """ Downloads the EIP config for the given provider - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._provider_config, @@ -140,8 +140,8 @@ class EIPBootstrapper(QtCore.QObject): """ Downloads the EIP client certificate for the given provider - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._provider_config, "We need a provider configuration!") leap_assert(self._eip_config, "We need an eip configuration!") @@ -219,8 +219,8 @@ class EIPBootstrapper(QtCore.QObject): """ Starts the checks needed for a new eip setup - @param provider_config: Provider configuration - @type provider_config: ProviderConfig + :param provider_config: Provider configuration + :type provider_config: ProviderConfig """ leap_assert(provider_config, "We need a provider config!") leap_assert_type(provider_config, ProviderConfig) diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index f5559143..734d3867 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -62,9 +62,9 @@ class ProviderBootstrapper(QtCore.QObject): """ Constructor for provider bootstrapper object - @param bypass_checks: Set to true if the app should bypass + :param bypass_checks: Set to true if the app should bypass first round of checks for CA certificates at bootstrap - @type bypass_checks: bool + :type bypass_checks: bool """ QtCore.QObject.__init__(self) @@ -84,8 +84,8 @@ class ProviderBootstrapper(QtCore.QObject): """ Checks that the name resolution for the provider name works - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._domain, "Cannot check DNS without a domain") @@ -115,8 +115,8 @@ class ProviderBootstrapper(QtCore.QObject): Checks that https is working and that the provided certificate checks out - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._domain, "Cannot check HTTPS without a domain") @@ -154,8 +154,8 @@ class ProviderBootstrapper(QtCore.QObject): """ Downloads the provider.json defition - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._domain, "Cannot download provider info without a domain") @@ -211,16 +211,15 @@ class ProviderBootstrapper(QtCore.QObject): """ Populates the check queue - @param checker: checker thread to be used to run this check - @type checker: CheckerThread - @param domain: domain to check - @type domain: str - @param download_if_needed: if True, makes the checks do not - overwrite already downloaded data - @type download_if_needed: bool + :param checker: checker thread to be used to run this check + :type checker: CheckerThread + :param domain: domain to check + :type domain: str + :param download_if_needed: if True, makes the checks do not overwrite already downloaded data + :type download_if_needed: bool - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(domain and len(domain) > 0, "We need a domain!") @@ -238,7 +237,7 @@ class ProviderBootstrapper(QtCore.QObject): Returns False if the certificate already exists for the given provider. True otherwise - @rtype: bool + :rtype: bool """ leap_assert(self._provider_config, "We need a provider config!") @@ -252,8 +251,8 @@ class ProviderBootstrapper(QtCore.QObject): """ Downloads the CA cert that is going to be used for the api URL - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._provider_config, "Cannot download the ca cert " @@ -310,8 +309,8 @@ class ProviderBootstrapper(QtCore.QObject): Checks the CA cert fingerprint against the one provided in the json definition - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._provider_config, "Cannot check the ca cert " "without a provider config!") @@ -362,8 +361,8 @@ class ProviderBootstrapper(QtCore.QObject): Tries to make an API call with the downloaded cert and checks if it validates against it - @return: True if the checks passed, False otherwise - @rtype: bool + :return: True if the checks passed, False otherwise + :rtype: bool """ leap_assert(self._provider_config, "Cannot check the ca cert " "without a provider config!") @@ -403,13 +402,13 @@ class ProviderBootstrapper(QtCore.QObject): provider_config, download_if_needed=False): """ - Starts the checks needed for a new provider setup + Starts the checks needed for a new provider setup. - @param provider_config: Provider configuration - @type provider_config: ProviderConfig - @param download_if_needed: if True, makes the checks do not - overwrite already downloaded data - @type download_if_needed: bool + :param provider_config: Provider configuration + :type provider_config: ProviderConfig + + :param download_if_needed: if True, makes the checks do not overwrite already downloaded data. + :type download_if_needed: bool """ leap_assert(provider_config, "We need a provider config!") leap_assert_type(provider_config, ProviderConfig) diff --git a/src/leap/services/eip/udstelnet.py b/src/leap/services/eip/udstelnet.py index a47c24f4..e6c82350 100644 --- a/src/leap/services/eip/udstelnet.py +++ b/src/leap/services/eip/udstelnet.py @@ -40,7 +40,6 @@ class UDSTelnet(telnetlib.Telnet): The optional second argument is the port number, which defaults to the standard telnet port (23). - Don't try to reopen an already connected instance. """ self.eof = 0 diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py index 9d838609..af1febe6 100644 --- a/src/leap/services/eip/vpn.py +++ b/src/leap/services/eip/vpn.py @@ -85,8 +85,8 @@ class VPN(QtCore.QThread): """ Returns wether this thread should quit - @rtype: bool - @return: True if the thread should terminate itself, Flase otherwise + :rtype: bool + :return: True if the thread should terminate itself, Flase otherwise """ QtCore.QMutexLocker(self._should_quit_lock) return self._should_quit @@ -117,15 +117,15 @@ class VPN(QtCore.QThread): """ Launches OpenVPN and starts the thread to watch its output - @param eipconfig: eip configuration object - @type eipconfig: EIPConfig - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig - @param socket_host: either socket path (unix) or socket IP - @type socket_host: str - @param socket_port: either string "unix" if it's a unix + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix socket, or port otherwise - @type socket_port: str + :type socket_port: str """ leap_assert(eipconfig, "We need an eip config") leap_assert_type(eipconfig, EIPConfig) @@ -196,7 +196,7 @@ class VPN(QtCore.QThread): """ Looks for openvpn instances running - @rtype: process + :rtype: process """ openvpn_process = None for p in psutil.process_iter(): @@ -217,7 +217,7 @@ class VPN(QtCore.QThread): """ Checks if VPN is already running and tries to stop it - @return: True if stopped, False otherwise + :return: True if stopped, False otherwise """ process = self._get_openvpn_process() @@ -253,11 +253,11 @@ class VPN(QtCore.QThread): def _connect(self, socket_host, socket_port): """ Connects to the specified socket_host socket_port - @param socket_host: either socket path (unix) or socket IP - @type socket_host: str - @param socket_port: either string "unix" if it's a unix + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix socket, or port otherwise - @type socket_port: str + :type socket_port: str """ try: self._tn = UDSTelnet(socket_host, socket_port) @@ -291,12 +291,12 @@ class VPN(QtCore.QThread): Sends a command to the telnet connection and reads until END is reached - @param command: command to send - @type command: str - @param until: byte delimiter string for reading command output - @type until: byte str - @return: response read - @rtype: list + :param command: command to send + :type command: str + :param until: byte delimiter string for reading command output + :type until: byte str + :return: response read + :rtype: list """ leap_assert(self._tn, "We need a tn connection!") try: @@ -315,9 +315,9 @@ class VPN(QtCore.QThread): Parses the output of the state command and emits state_changed signal when the state changes - @param output: list of lines that the state command printed as + :param output: list of lines that the state command printed as its output - @type output: list + :type output: list """ for line in output: stripped = line.strip() @@ -345,9 +345,9 @@ class VPN(QtCore.QThread): Parses the output of the status command and emits status_changed signal when the status changes - @param output: list of lines that the status command printed + :param output: list of lines that the status command printed as its output - @type output: list + :type output: list """ tun_tap_read = "" tun_tap_write = "" diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 3d36736d..540bc45e 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -67,18 +67,18 @@ class VPNLauncher: """ Returns the platform dependant vpn launching command - @param eipconfig: eip configuration object - @type eipconfig: EIPConfig - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig - @param socket_host: either socket path (unix) or socket IP - @type socket_host: str - @param socket_port: either string "unix" if it's a unix + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix socket, or port otherwise - @type socket_port: str + :type socket_port: str - @return: A VPN command ready to be launched - @rtype: list + :return: A VPN command ready to be launched + :rtype: list """ return [] @@ -89,10 +89,10 @@ class VPNLauncher: This is mainly used for setting LD_LIBRARY_PATH to the correct path when distributing a standalone client - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig - @rtype: dict + :rtype: dict """ return {} @@ -148,18 +148,21 @@ class LinuxVPNLauncher(VPNLauncher): Might raise VPNException. - @param eipconfig: eip configuration object - @type eipconfig: EIPConfig - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig - @param socket_host: either socket path (unix) or socket IP - @type socket_host: str - @param socket_port: either string "unix" if it's a unix - socket, or port otherwise - @type socket_port: str + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str - @return: A VPN command ready to be launched - @rtype: list + :return: A VPN command ready to be launched + :rtype: list """ leap_assert(eipconfig, "We need an eip config") leap_assert_type(eipconfig, EIPConfig) @@ -263,10 +266,10 @@ class LinuxVPNLauncher(VPNLauncher): This is mainly used for setting LD_LIBRARY_PATH to the correct path when distributing a standalone client - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig - @rtype: dict + :rtype: dict """ leap_assert(providerconfig, "We need a provider config") leap_assert_type(providerconfig, ProviderConfig) @@ -300,18 +303,21 @@ class DarwinVPNLauncher(VPNLauncher): Might raise VPNException. - @param eipconfig: eip configuration object - @type eipconfig: EIPConfig - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig - @param socket_host: either socket path (unix) or socket IP - @type socket_host: str - @param socket_port: either string "unix" if it's a unix - socket, or port otherwise - @type socket_port: str + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str - @return: A VPN command ready to be launched - @rtype: list + :return: A VPN command ready to be launched + :rtype: list """ leap_assert(eipconfig, "We need an eip config") leap_assert_type(eipconfig, EIPConfig) @@ -405,10 +411,10 @@ class DarwinVPNLauncher(VPNLauncher): This is mainly used for setting LD_LIBRARY_PATH to the correct path when distributing a standalone client - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig - @rtype: dict + :rtype: dict """ return {"DYLD_LIBRARY_PATH": os.path.join( providerconfig.get_path_prefix(), @@ -431,18 +437,18 @@ class WindowsVPNLauncher(VPNLauncher): Might raise VPNException. - @param eipconfig: eip configuration object - @type eipconfig: EIPConfig - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig - @param socket_host: either socket path (unix) or socket IP - @type socket_host: str - @param socket_port: either string "unix" if it's a unix + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix socket, or port otherwise - @type socket_port: str + :type socket_port: str - @return: A VPN command ready to be launched - @rtype: list + :return: A VPN command ready to be launched + :rtype: list """ leap_assert(eipconfig, "We need an eip config") leap_assert_type(eipconfig, EIPConfig) @@ -514,10 +520,10 @@ class WindowsVPNLauncher(VPNLauncher): This is mainly used for setting LD_LIBRARY_PATH to the correct path when distributing a standalone client - @param providerconfig: provider specific configuration - @type providerconfig: ProviderConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig - @rtype: dict + :rtype: dict """ return {} diff --git a/src/leap/util/checkerthread.py b/src/leap/util/checkerthread.py index 47a96ec5..02aa333f 100644 --- a/src/leap/util/checkerthread.py +++ b/src/leap/util/checkerthread.py @@ -48,10 +48,10 @@ class CheckerThread(QtCore.QThread): def get_should_quit(self): """ - Returns wether this thread should quit + Returns whether this thread should quit - @rtype: bool - @return: True if the thread should terminate itself, Flase otherwise + :return: True if the thread should terminate itself, Flase otherwise + :rtype: bool """ QtCore.QMutexLocker(self._should_quit_lock) @@ -78,8 +78,8 @@ class CheckerThread(QtCore.QThread): """ Adds a list of checks to the ones being executed - @param checks: check functions to perform - @type checkes: list + :param checks: check functions to perform + :type checkes: list """ with QtCore.QMutexLocker(self._checks_lock): self._checks += checks diff --git a/src/leap/util/privilege_policies.py b/src/leap/util/privilege_policies.py index e74c4d33..10224bcd 100644 --- a/src/leap/util/privilege_policies.py +++ b/src/leap/util/privilege_policies.py @@ -33,7 +33,7 @@ def is_missing_policy_permissions(): platform, or if the policy checker exists but it cannot find the appropriate policy mechanisms in place. - @rtype: bool + :rtype: bool """ _system = platform.system() platform_checker = _system + "PolicyChecker" @@ -60,7 +60,7 @@ class PolicyChecker: Returns True if we could not find any policy mechanisms that are defined to be in used for this particular platform. - @rtype: bool + :rtype: bool """ return True @@ -77,6 +77,6 @@ class LinuxPolicyChecker(PolicyChecker): Returns True if we could not find the appropriate policykit file in place - @rtype: bool + :rtype: bool """ return not os.path.isfile(self.LINUX_POLKIT_FILE) diff --git a/src/leap/util/request_helpers.py b/src/leap/util/request_helpers.py index 019ff353..e06dabb8 100644 --- a/src/leap/util/request_helpers.py +++ b/src/leap/util/request_helpers.py @@ -32,10 +32,10 @@ def get_content(request): property/function or from content, in that order. Also returns the mtime for that content if available - @param request: request as it is given by requests - @type request: Response + :param request: request as it is given by requests + :type request: Response - @rtype: tuple (contents, mtime) + :rtype: tuple (contents, mtime) """ contents = "" -- cgit v1.2.3 From 7126aad25c3dd45bfe026ba1ad383bf5476ffb15 Mon Sep 17 00:00:00 2001 From: kali Date: Tue, 7 May 2013 22:51:08 +0900 Subject: use ipaddr to get ipv6 support --- src/leap/services/eip/eipconfig.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index e6b93647..0a7d2b23 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -22,6 +22,8 @@ import logging import os import re +import ipaddr + from leap.common.check import leap_assert, leap_assert_type from leap.common.config.baseconfig import BaseConfig from leap.config.providerconfig import ProviderConfig @@ -36,7 +38,6 @@ class EIPConfig(BaseConfig): """ OPENVPN_ALLOWED_KEYS = ("auth", "cipher", "tls-cipher") OPENVPN_CIPHERS_REGEX = re.compile("[A-Z0-9\-]+") - IP_REGEX = re.compile("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$") def __init__(self): BaseConfig.__init__(self) @@ -91,9 +92,14 @@ class EIPConfig(BaseConfig): index = 0 logger.warning("Provided an unknown gateway index %s, " + "defaulting to 0") - ip_addr = gateways[0]["ip_address"] - if self.IP_REGEX.search(ip_addr): - return ip_addr + ip_addr_str = gateways[0]["ip_address"] + + try: + ipaddr.IPAddress(ip_addr_str) + return ip_addr_str + except ValueError: + logger.error("Invalid ip address in config: %s" % (ip_addr_str,)) + return None def get_client_cert_path(self, providerconfig=None, -- cgit v1.2.3 From c533900a43f5006e6b4cb9d070b4bd30fb67f0b5 Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Fri, 10 May 2013 16:41:42 -0300 Subject: Save auth token --- src/leap/crypto/srpauth.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index 9446cee8..26bd0295 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -82,6 +82,8 @@ class SRPAuth(QtCore.QObject): self._session_id_lock = QtCore.QMutex() self._uid = None self._uid_lock = QtCore.QMutex() + self._token = None + self._token_lock = QtCore.QMutex() self._srp_user = None self._srp_a = None @@ -240,10 +242,12 @@ class SRPAuth(QtCore.QObject): M2 = auth_result.json().get("M2", None) uid = auth_result.json().get("id", None) + token = auth_result.json().get("token", None) events_signal(proto.CLIENT_UID, content=uid) self.set_uid(uid) + self.set_token(token) if M2 is None or self.get_uid() is None: logger.error("Something went wrong. Content = %r" % @@ -356,6 +360,14 @@ class SRPAuth(QtCore.QObject): QtCore.QMutexLocker(self._uid_lock) return self._uid + def set_token(self, token): + QtCore.QMutexLocker(self._token_lock) + self._token = token + + def get_token(self, token): + QtCore.QMutexLocker(self._token_lock) + return self._token + __instance = None authentication_finished = QtCore.Signal(bool, str) -- cgit v1.2.3 From 4e201329042d43c8d281c5737d3d5f6f8e2bf67f Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Fri, 10 May 2013 17:01:11 -0300 Subject: Add support for requests<1.0.0 --- src/leap/crypto/srpauth.py | 25 +++++++++++++++++-------- src/leap/gui/wizard.py | 6 +++++- 2 files changed, 22 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index 26bd0295..dbaac01b 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -20,12 +20,14 @@ import logging import requests import srp +import json from PySide import QtCore, QtGui from leap.common.check import leap_assert from leap.config.providerconfig import ProviderConfig from leap.util.checkerthread import CheckerThread +from leap.util.request_helpers import get_content from leap.common.events import signal as events_signal from leap.common.events import events_pb2 as proto @@ -159,14 +161,18 @@ class SRPAuth(QtCore.QObject): raise SRPAuthenticationError("Unknown error: %r" % (e,)) + content, mtime = get_content(init_session) + if init_session.status_code not in (200,): logger.error("No valid response (salt): " "Status code = %r. Content: %r" % - (init_session.status_code, init_session.content)) + (init_session.status_code, content)) if init_session.status_code == 422: raise SRPAuthenticationError(self.tr("Unknown user")) - salt = init_session.json().get("salt", None) - B = init_session.json().get("B", None) + + json_content = json.loads(content) + salt = json_content.get("salt", None) + B = json_content.get("B", None) if salt is None: logger.error("No salt parameter sent") @@ -226,22 +232,25 @@ class SRPAuth(QtCore.QObject): raise SRPAuthenticationError(self.tr("Could not connect to " "the server")) + content, mtime = get_content(auth_result) + if auth_result.status_code == 422: logger.error("[%s] Wrong password (HAMK): [%s]" % (auth_result.status_code, - auth_result.json(). + content. get("errors", ""))) raise SRPAuthenticationError(self.tr("Wrong password")) if auth_result.status_code not in (200,): logger.error("No valid response (HAMK): " "Status code = %s. Content = %r" % - (auth_result.status_code, auth_result.content)) + (auth_result.status_code, content)) raise SRPAuthenticationError(self.tr("Unknown error (%s)") % (auth_result.status_code,)) - M2 = auth_result.json().get("M2", None) - uid = auth_result.json().get("id", None) + json_content = json.loads(content) + M2 = json_content.get("M2", None) + uid = json_content.get("id", None) token = auth_result.json().get("token", None) events_signal(proto.CLIENT_UID, content=uid) @@ -251,7 +260,7 @@ class SRPAuth(QtCore.QObject): if M2 is None or self.get_uid() is None: logger.error("Something went wrong. Content = %r" % - (auth_result.content,)) + (content,)) raise SRPAuthenticationError(self.tr("Problem getting data " "from server")) diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index ad45dd8c..713383a6 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -20,6 +20,7 @@ First run wizard """ import os import logging +import json from PySide import QtCore, QtGui from functools import partial @@ -28,6 +29,7 @@ from ui_wizard import Ui_Wizard from leap.config.providerconfig import ProviderConfig from leap.crypto.srpregister import SRPRegister from leap.util.privilege_policies import is_missing_policy_permissions +from leap.util.request_helpers import get_content from leap.services.eip.providerbootstrapper import ProviderBootstrapper logger = logging.getLogger(__name__) @@ -254,7 +256,9 @@ class Wizard(QtGui.QWizard): self._password = None error_msg = self.tr("Unknown error") try: - error_msg = req.json().get("errors").get("login")[0] + content, _ = get_content(req) + json_content = json.loads(content) + error_msg = json_content.get("errors").get("login")[0] if not error_msg.istitle(): error_msg = "%s %s" % (old_username, error_msg) self._set_register_status(error_msg, error=True) -- cgit v1.2.3 From 9a1a657593c3ee18d35936a85077ae2f7e51c392 Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Fri, 10 May 2013 12:09:02 -0300 Subject: Fixup one instance on Windows --- src/leap/gui/mainwindow.py | 1 + src/leap/platform_init/locks.py | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index ccf97672..358b7a41 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -38,6 +38,7 @@ from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.providerbootstrapper import ProviderBootstrapper from leap.platform_init import IS_MAC, IS_WIN +from leap.platform_init.locks import WindowsLock from leap.platform_init.initializers import init_platform from leap.services.eip.vpn import VPN from leap.services.eip.vpnlaunchers import (VPNLauncherException, diff --git a/src/leap/platform_init/locks.py b/src/leap/platform_init/locks.py index e5b392a3..fefc209b 100644 --- a/src/leap/platform_init/locks.py +++ b/src/leap/platform_init/locks.py @@ -17,7 +17,6 @@ """ Utilities for handling multi-platform file locking mechanisms """ -import commands import logging import errno import os @@ -30,10 +29,8 @@ from leap import platform_init if platform_init.IS_UNIX: from fcntl import flock, LOCK_EX, LOCK_NB else: - import errno import glob import shutil - import socket from tempfile import gettempdir @@ -177,7 +174,7 @@ if platform_init.IS_WIN: """ try: self._fd = os.makedirs(self.name) - except WindowsError as exc: + except OSError as exc: # could not create the dir if exc.args[0] == 183: logger.debug('cannot create dir') @@ -218,8 +215,7 @@ if platform_init.IS_WIN: try: shutil.rmtree(self.name) return True - - except WindowsError as exc: + except shutil.WindowsError as exc: if exc.errno in (errno.EPIPE, errno.ENOENT, errno.ESRCH, errno.EACCES): logger.warning( -- cgit v1.2.3 From 6c66167ddce1682e88fae0ae106f44befb121c4d Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Fri, 10 May 2013 16:32:48 -0300 Subject: Make the import of WindowsLock conditional --- src/leap/gui/mainwindow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 358b7a41..b3ab56d3 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -38,7 +38,6 @@ from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.providerbootstrapper import ProviderBootstrapper from leap.platform_init import IS_MAC, IS_WIN -from leap.platform_init.locks import WindowsLock from leap.platform_init.initializers import init_platform from leap.services.eip.vpn import VPN from leap.services.eip.vpnlaunchers import (VPNLauncherException, @@ -48,6 +47,9 @@ from leap.services.eip.vpnlaunchers import (VPNLauncherException, from leap.util import __version__ as VERSION from leap.util.checkerthread import CheckerThread +if IS_WIN: + from leap.platform_init.locks import WindowsLock + from ui_mainwindow import Ui_MainWindow logger = logging.getLogger(__name__) -- cgit v1.2.3 From 8781a893aeaa62286633021e9d3eb8502bd129ee Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Sat, 11 May 2013 11:49:17 -0300 Subject: Support requests<1.0.0 for the token saving too --- src/leap/crypto/srpauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index dbaac01b..ce6c28f4 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -251,7 +251,7 @@ class SRPAuth(QtCore.QObject): json_content = json.loads(content) M2 = json_content.get("M2", None) uid = json_content.get("id", None) - token = auth_result.json().get("token", None) + token = json_content.get("token", None) events_signal(proto.CLIENT_UID, content=uid) -- cgit v1.2.3 From 1cb931e83522746da668f9a8bb5943aca1882086 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 16 May 2013 04:26:00 +0900 Subject: use qtreactor so twisted is driven by qt main loop aboutToQuit signal is not raised anymore with the qt4reactor. So we are calling all cleanup callbacks from the quit function. --- src/leap/app.py | 27 ++++++++++-- src/leap/gui/mainwindow.py | 76 ++++++++++++++++++---------------- src/leap/gui/twisted_main.py | 49 ++++++++++++++++++++++ src/leap/platform_init/initializers.py | 3 ++ src/leap/services/tx.py | 46 ++++++++++++++++++++ 5 files changed, 162 insertions(+), 39 deletions(-) create mode 100644 src/leap/gui/twisted_main.py create mode 100644 src/leap/services/tx.py (limited to 'src') diff --git a/src/leap/app.py b/src/leap/app.py index bb8add0d..797cea8a 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -17,7 +17,6 @@ import logging import signal -import socket import sys from functools import partial @@ -28,14 +27,19 @@ from leap.common.events import server as event_server from leap.util import __version__ as VERSION from leap.util import leap_argparse from leap.gui import locale_rc +from leap.gui import twisted_main from leap.gui.mainwindow import MainWindow from leap.platform_init import IS_MAC from leap.platform_init.locks import we_are_the_one_and_only +from leap.services.tx import leap_services import codecs codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None) +# pylint: avoid unused import +assert(locale_rc) + def sigint_handler(*args, **kwargs): """ @@ -48,9 +52,15 @@ def sigint_handler(*args, **kwargs): mainwindow.quit() +def install_qtreactor(logger): + import qt4reactor + qt4reactor.install() + logger.debug("Qt4 reactor installed") + + def main(): """ - Launches the main event loop + Starts the main event loop and launches the main window. """ event_server.ensure_server(event_server.SERVER_PORT) @@ -96,6 +106,9 @@ def main(): logger.info('Starting app') app = QtGui.QApplication(sys.argv) + # install the qt4reactor. + install_qtreactor(logger) + # To test: # $ LANG=es ./app.py locale = QtCore.QLocale.system().name() @@ -119,7 +132,10 @@ def main(): timer.start(500) timer.timeout.connect(lambda: None) - window = MainWindow(standalone, bypass_checks) + window = MainWindow( + lambda: twisted_main.quit(app), + standalone=standalone, + bypass_checks=bypass_checks) window.show() sigint_window = partial(sigint_handler, window, logger=logger) @@ -128,8 +144,11 @@ def main(): if IS_MAC: window.raise_() + tx_app = leap_services() + assert(tx_app) + # Run main loop - sys.exit(app.exec_()) + twisted_main.start(app) if __name__ == "__main__": main() diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index b3ab56d3..fdf84766 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -71,15 +71,22 @@ class MainWindow(QtGui.QMainWindow): new_updates = QtCore.Signal(object) raise_window = QtCore.Signal([]) - def __init__(self, standalone=False, bypass_checks=False): + def __init__(self, quit_callback, + standalone=False, bypass_checks=False): """ Constructor for the client main window + :param quit_callback: Function to be called when closing + the application. + :type quit_callback: callable + :param standalone: Set to true if the app should use configs - inside its pwd + inside its pwd :type standalone: bool + :param bypass_checks: Set to true if the app should bypass - first round of checks for CA certificates at bootstrap + first round of checks for CA + certificates at bootstrap :type bypass_checks: bool """ QtGui.QMainWindow.__init__(self) @@ -89,6 +96,7 @@ class MainWindow(QtGui.QMainWindow): callback=self._new_updates_available) register(signal=proto.RAISE_WINDOW, callback=self._on_raise_window_event) + self._quit_callback = quit_callback self._updates_content = "" @@ -173,27 +181,6 @@ class MainWindow(QtGui.QMainWindow): self._vpn.process_finished.connect( self._eip_finished) - QtCore.QCoreApplication.instance().connect( - QtCore.QCoreApplication.instance(), - QtCore.SIGNAL("aboutToQuit()"), - self._vpn.set_should_quit) - QtCore.QCoreApplication.instance().connect( - QtCore.QCoreApplication.instance(), - QtCore.SIGNAL("aboutToQuit()"), - self._vpn.wait) - QtCore.QCoreApplication.instance().connect( - QtCore.QCoreApplication.instance(), - QtCore.SIGNAL("aboutToQuit()"), - self._checker_thread.set_should_quit) - QtCore.QCoreApplication.instance().connect( - QtCore.QCoreApplication.instance(), - QtCore.SIGNAL("aboutToQuit()"), - self._checker_thread.wait) - QtCore.QCoreApplication.instance().connect( - QtCore.QCoreApplication.instance(), - QtCore.SIGNAL("aboutToQuit()"), - self._cleanup_pidfiles) - self.ui.chkRemember.stateChanged.connect( self._remember_state_changed) self.ui.chkRemember.setEnabled(keyring.get_keyring() is not None) @@ -447,12 +434,6 @@ class MainWindow(QtGui.QMainWindow): "More about LEAP" "") % (VERSION,)) - def quit(self): - self._really_quit = True - if self._wizard: - self._wizard.close() - self.close() - def changeEvent(self, e): """ Reimplements the changeEvent method to minimize to tray @@ -976,17 +957,42 @@ class MainWindow(QtGui.QMainWindow): def _cleanup_pidfiles(self): """ - SLOT - TRIGGERS: - self.aboutToQuit + Removes lockfiles on a clean shutdown. - Triggered on about to quit signal, removes lockfiles on a clean - shutdown + Triggered after aboutToQuit signal. """ if IS_WIN: lockfile = WindowsLock() lockfile.release_lock() + def _cleanup_and_quit(self): + """ + Call all the cleanup actions in a serialized way. + Should be called from the quit function. + """ + logger.debug('About to quit, doing cleanup...') + self._vpn.set_should_quit() + self._vpn.wait() + self._checker_thread.set_should_quit() + self._checker_thread.wait() + self._cleanup_pidfiles() + + def quit(self): + """ + Cleanup and tidely close the main window before quitting. + """ + self._cleanup_and_quit() + + self._really_quit = True + if self._wizard: + self._wizard.close() + self.close() + + if self._quit_callback: + self._quit_callback() + logger.debug('Bye.') + + if __name__ == "__main__": import signal diff --git a/src/leap/gui/twisted_main.py b/src/leap/gui/twisted_main.py new file mode 100644 index 00000000..44f532a4 --- /dev/null +++ b/src/leap/gui/twisted_main.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# twisted_main.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Main functions for integration of twisted reactor +""" +import logging + +# Resist the temptation of putting the import reactor here, +# it will raise an "reactor already imported" error. + +logger = logging.getLogger(__name__) + + +def start(app): + """ + Start the mainloop. + + :param app: the main qt QApplication instance. + :type app: QtCore.QApplication + """ + from twisted.internet import reactor + logger.debug('starting twisted reactor') + reactor.run() + + +def quit(app): + """ + Stop the mainloop. + + :param app: the main qt QApplication instance. + :type app: QtCore.QApplication + """ + from twisted.internet import reactor + logger.debug('stopping twisted reactor') + reactor.stop() diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index 91c7086b..2e8cbe95 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -28,6 +28,9 @@ from PySide import QtGui logger = logging.getLogger(__name__) +# NOTE we could use a deferToThread here, but should +# be aware of this bug: http://www.themacaque.com/?p=1067 + def init_platform(): """ diff --git a/src/leap/services/tx.py b/src/leap/services/tx.py new file mode 100644 index 00000000..ef08fcc6 --- /dev/null +++ b/src/leap/services/tx.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# twisted.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Twisted services launched by the client +""" +import logging + +from twisted.application.service import Application +from twisted.internet.task import LoopingCall + +logger = logging.getLogger(__name__) + + +def task(): + """ + stub periodic task, mainly for tests. + DELETE-ME when there's real meat here :) + """ + from datetime import datetime + logger.debug("hi there %s", datetime.now()) + + +def leap_services(): + """ + Check which twisted services are enabled and + register them. + """ + logger.debug('starting leap services') + application = Application("LEAP Client Local Services") + #lc = LoopingCall(task) + #lc.start(5) + return application -- cgit v1.2.3 From 212102f05bbc09b0b6cc6fa250eaafc8f80b6824 Mon Sep 17 00:00:00 2001 From: kali Date: Fri, 17 May 2013 00:45:06 +0900 Subject: catch reactor not running error --- src/leap/gui/twisted_main.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/twisted_main.py b/src/leap/gui/twisted_main.py index 44f532a4..871af577 100644 --- a/src/leap/gui/twisted_main.py +++ b/src/leap/gui/twisted_main.py @@ -19,6 +19,8 @@ Main functions for integration of twisted reactor """ import logging +from twisted.internet import error + # Resist the temptation of putting the import reactor here, # it will raise an "reactor already imported" error. @@ -46,4 +48,7 @@ def quit(app): """ from twisted.internet import reactor logger.debug('stopping twisted reactor') - reactor.stop() + try: + reactor.stop() + except error.ReactorNotRunning: + logger.debug('reactor not running') -- cgit v1.2.3 From 319e1d55a2f8e9c521450f60c571f24a907553ee Mon Sep 17 00:00:00 2001 From: kali Date: Fri, 17 May 2013 02:00:35 +0900 Subject: fix a segfault when stopping the reactor this particular way of invoking the run method in the reactor was giving trouble under some conditions. switching to runReturn makes it work. --- src/leap/gui/twisted_main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/twisted_main.py b/src/leap/gui/twisted_main.py index 871af577..c7add3ee 100644 --- a/src/leap/gui/twisted_main.py +++ b/src/leap/gui/twisted_main.py @@ -36,7 +36,13 @@ def start(app): """ from twisted.internet import reactor logger.debug('starting twisted reactor') - reactor.run() + + # this seems to be troublesome under some + # unidentified settings. + #reactor.run() + + reactor.runReturn() + app.exec_() def quit(app): -- cgit v1.2.3 From b0abf507bb8eb570328172b659ab072bc4b08634 Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Wed, 15 May 2013 16:16:32 -0300 Subject: Integrate soledad and keymanager in the client --- src/leap/crypto/srpauth.py | 8 +- src/leap/gui/mainwindow.py | 34 +++ src/leap/services/soledad/__init__.py | 0 src/leap/services/soledad/soledadbootstrapper.py | 279 +++++++++++++++++++++++ src/leap/services/soledad/soledadconfig.py | 48 ++++ src/leap/services/soledad/soledadspec.py | 57 +++++ 6 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 src/leap/services/soledad/__init__.py create mode 100644 src/leap/services/soledad/soledadbootstrapper.py create mode 100644 src/leap/services/soledad/soledadconfig.py create mode 100644 src/leap/services/soledad/soledadspec.py (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index ce6c28f4..2f3cbd1c 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -373,7 +373,7 @@ class SRPAuth(QtCore.QObject): QtCore.QMutexLocker(self._token_lock) self._token = token - def get_token(self, token): + def get_token(self): QtCore.QMutexLocker(self._token_lock) return self._token @@ -431,6 +431,12 @@ class SRPAuth(QtCore.QObject): def get_session_id(self): return self.__instance.get_session_id() + def get_uid(self): + return self.__instance.get_uid() + + def get_token(self): + return self.__instance.get_token() + def logout(self): """ Logs out the current session. diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index fdf84766..7eb956cb 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -37,6 +37,7 @@ from leap.gui.wizard import Wizard from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.providerbootstrapper import ProviderBootstrapper +from leap.services.soledad.soledadbootstrapper import SoledadBootstrapper from leap.platform_init import IS_MAC, IS_WIN from leap.platform_init.initializers import init_platform from leap.services.eip.vpn import VPN @@ -175,6 +176,10 @@ class MainWindow(QtGui.QMainWindow): self._eip_bootstrapper.download_client_certificate.connect( self._finish_eip_bootstrap) + self._soledad_bootstrapper = SoledadBootstrapper() + self._soledad_bootstrapper.download_config.connect( + self._soledad_bootstrapped_stage) + self._vpn = VPN() self._vpn.state_changed.connect(self._update_vpn_state) self._vpn.status_changed.connect(self._update_vpn_status) @@ -233,6 +238,8 @@ class MainWindow(QtGui.QMainWindow): self._bypass_checks = bypass_checks + self._soledad = None + if self._first_run(): self._wizard_firstrun = True self._wizard = Wizard(self._checker_thread, @@ -686,8 +693,35 @@ class MainWindow(QtGui.QMainWindow): """ self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) self._systray.setIcon(self.LOGGED_IN_ICON) + + self._soledad_bootstrapper.run_soledad_setup_checks( + self._checker_thread, + self._provider_config, + self.ui.lnUser.text(), + self.ui.lnPassword.text(), + download_if_needed=True) + self._download_eip_config() + def _soledad_bootstrapped_stage(self, data): + """ + SLOT + TRIGGERS: + self._soledad_bootstrapper.download_config + + If there was a problem, displays it, otherwise it does nothing. + This is used for intermediate bootstrapping stages, in case + they fail. + + :param data: result from the bootstrapping stage for Soledad + :type data: dict + """ + passed = data[self._soledad_bootstrapper.PASSED_KEY] + if not passed: + logger.error(data[self._soledad_bootstrapper.ERROR_KEY]) + else: + logger.debug("Done bootstrapping Soledad") + def _get_socket_host(self): """ Returns the socket and port to be used for VPN diff --git a/src/leap/services/soledad/__init__.py b/src/leap/services/soledad/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/services/soledad/soledadbootstrapper.py b/src/leap/services/soledad/soledadbootstrapper.py new file mode 100644 index 00000000..51c53a6e --- /dev/null +++ b/src/leap/services/soledad/soledadbootstrapper.py @@ -0,0 +1,279 @@ +# -*- coding: utf-8 -*- +# soledadbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Soledad bootstrapping +""" + +import logging +import os + +import requests + +from PySide import QtCore +from mock import Mock + +from leap.common.check import leap_assert, leap_assert_type +from leap.common.files import get_mtime +from leap.common.keymanager import KeyManager, openpgp +from leap.common.keymanager.errors import KeyNotFound +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpauth import SRPAuth +from leap.services.soledad.soledadconfig import SoledadConfig +from leap.util.request_helpers import get_content +from leap.soledad import Soledad + +logger = logging.getLogger(__name__) + + +class SoledadBootstrapper(QtCore.QObject): + """ + Soledad init procedure + """ + + PASSED_KEY = "passed" + ERROR_KEY = "error" + + PUBKEY_KEY = "user[public_key]" + + IDLE_SLEEP_INTERVAL = 100 + + # All dicts returned are of the form + # {"passed": bool, "error": str} + download_config = QtCore.Signal(dict) + gen_key = QtCore.Signal(dict) + + def __init__(self): + QtCore.QObject.__init__(self) + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + # **************************************************** # + + self._session = self._fetcher.session() + self._provider_config = None + self._soledad_config = None + self._keymanager = None + self._download_if_needed = False + self._user = "" + self._password = "" + + def _load_and_sync_soledad(self, srp_auth): + """ + Once everthing is in the right place, we instantiate and sync + Soledad + + :param srp_auth: SRPAuth object used + :type srp_auth: SRPAuth + """ + uuid = srp_auth.get_uid() + + prefix = os.path.join(self._soledad_config.get_path_prefix(), + "leap", "soledad") + secrets_path = "%s/%s.secret" % (prefix, uuid) + local_db_path = "%s/%s.db" % (prefix, uuid) + + # TODO: use the proper URL + server_url = 'https://mole.dev.bitmask.net:2424/user-%s' % (uuid,) + # server_url = self._soledad_config.get_hosts(...) + + cert_file = self._provider_config.get_ca_cert_path() + + self._soledad = Soledad(uuid, + self._password.encode("utf-8"), + secrets_path, + local_db_path, + server_url, + cert_file, + srp_auth.get_token()) + self._soledad.sync() + + def _download_config(self): + """ + Downloads the Soledad config for the given provider + + :return: True if everything went as expected, False otherwise + :rtype: bool + """ + + leap_assert(self._provider_config, + "We need a provider configuration!") + + logger.debug("Downloading Soledad config for %s" % + (self._provider_config.get_domain(),)) + + download_config_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + self._soledad_config = SoledadConfig() + + try: + headers = {} + mtime = get_mtime(os.path.join(self._soledad_config + .get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "soledad-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + # there is some confusion with this uri, + config_uri = "%s/%s/config/soledad-service.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('Downloading soledad config from: %s' % config_uri) + + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} + + res = self._session.get(config_uri, + verify=self._provider_config + .get_ca_cert_path(), + headers=headers, + cookies=cookies) + res.raise_for_status() + + # Not modified + if res.status_code == 304: + logger.debug("Soledad definition has not been modified") + else: + soledad_definition, mtime = get_content(res) + + self._soledad_config.load(data=soledad_definition, mtime=mtime) + self._soledad_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "soledad-service.json"]) + + self._load_and_sync_soledad(srp_auth) + + download_config_data[self.PASSED_KEY] = True + except Exception as e: + download_config_data[self.PASSED_KEY] = False + download_config_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting download_config %s" % (download_config_data,)) + self.download_config.emit(download_config_data) + + return download_config_data[self.PASSED_KEY] + + def _gen_key(self): + """ + Generates the key pair if needed, uploads it to the webapp and + nickserver + + :return: True if everything is done successfully, False + otherwise + :rtype: bool + """ + leap_assert(self._provider_config, + "We need a provider configuration!") + + # XXX Sanitize this + address = "%s@%s" % (self._user, self._provider_config.get_domain()) + + logger.debug("Retrieving key for %s" % (address,)) + + genkey_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + try: + srp_auth = SRPAuth(self._provider_config) + self._keymanager = KeyManager( + address, + "https://nickserver", # TODO: nickserver url, none for now + self._soledad, + token=srp_auth.get_token()) + self._keymanager._fetcher.put = Mock() + try: + self._keymanager.get_key(address, openpgp.OpenPGPKey, + private=True, fetch_remote=False) + except KeyNotFound: + logger.debug("Key not found. Generating key for %s" % (address,)) + self._keymanager.gen_key(openpgp.OpenPGPKey) + + logger.debug("Key generated successfully.") + + cookies = None + session_id = srp_auth.get_session_id() + if session_id: + cookies = {"_session_id": session_id} + + key_uri = "%s/%s/users/%s.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version(), + srp_auth.get_uid()) + + logger.debug("Uploading public key to %s" % (key_uri,)) + + pubkey = self._keymanager.get_key(address, openpgp.OpenPGPKey, + private=False, fetch_remote=False) + key_data = { + self.PUBKEY_KEY: pubkey.key_data, + } + + # TODO: check if uploaded before uploading it + key_result = self._session.put(key_uri, + data=key_data, + verify=self._provider_config + .get_ca_cert_path(), + cookies=cookies) + key_result.raise_for_status() + genkey_data[self.PASSED_KEY] = True + except Exception as e: + genkey_data[self.PASSED_KEY] = False + genkey_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting gen_key %s" % (genkey_data,)) + self.gen_key.emit(genkey_data) + + return genkey_data[self.PASSED_KEY] + + def run_soledad_setup_checks(self, + checker, + provider_config, + user, + password, + download_if_needed=False): + """ + Starts the checks needed for a new soledad setup + + :param provider_config: Provider configuration + :type provider_config: ProviderConfig + """ + leap_assert_type(provider_config, ProviderConfig) + + self._provider_config = provider_config + self._download_if_needed = download_if_needed + self._user = user + self._password = password + + checker.add_checks([ + self._download_config, + self._gen_key + ]) diff --git a/src/leap/services/soledad/soledadconfig.py b/src/leap/services/soledad/soledadconfig.py new file mode 100644 index 00000000..836265f3 --- /dev/null +++ b/src/leap/services/soledad/soledadconfig.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# soledadconfig.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Soledad configuration +""" +import logging + +from leap.common.config.baseconfig import BaseConfig +from leap.services.soledad.soledadspec import soledad_config_spec + +logger = logging.getLogger(__name__) + + +class SoledadConfig(BaseConfig): + """ + Soledad configuration abstraction class + """ + + def __init__(self): + BaseConfig.__init__(self) + + def _get_spec(self): + """ + Returns the spec object for the specific configuration + """ + return soledad_config_spec + + def get_hosts(self): + return self._safe_get_value("hosts") + + def get_locations(self): + return self._safe_get_value("locations") + diff --git a/src/leap/services/soledad/soledadspec.py b/src/leap/services/soledad/soledadspec.py new file mode 100644 index 00000000..d5a437cc --- /dev/null +++ b/src/leap/services/soledad/soledadspec.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# soledadspec.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +soledad_config_spec = { + 'description': 'sample soledad service config', + 'type': 'object', + 'properties': { + 'serial': { + 'type': int, + 'default': 1, + 'required': True + }, + 'version': { + 'type': int, + 'default': 1, + 'required': True + }, + 'hosts': { + 'type': dict, + 'default': { + "python": { + "hostname": "someprovider", + "ip_address": "1.1.1.1", + "location": "loc", + "port": 1111 + }, + }, + }, + 'locations': { + 'type': dict, + 'default': { + "locations": { + "ankara": { + "country_code": "TR", + "hemisphere": "N", + "name": "loc", + "timezone": "+0" + } + } + } + } + } +} -- cgit v1.2.3 From c2a0900868eecda3958185bf355a15b046d4e4af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Sat, 18 May 2013 12:38:29 -0300 Subject: Integrate SMTP --- src/leap/gui/mainwindow.py | 69 ++++++++- src/leap/services/mail/__init__.py | 0 src/leap/services/mail/smtpbootstrapper.py | 169 +++++++++++++++++++++++ src/leap/services/mail/smtpconfig.py | 48 +++++++ src/leap/services/mail/smtpspec.py | 51 +++++++ src/leap/services/soledad/soledadbootstrapper.py | 8 +- 6 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 src/leap/services/mail/__init__.py create mode 100644 src/leap/services/mail/smtpbootstrapper.py create mode 100644 src/leap/services/mail/smtpconfig.py create mode 100644 src/leap/services/mail/smtpspec.py (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 7eb956cb..d51ec3c8 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -25,7 +25,9 @@ import tempfile from functools import partial import keyring + from PySide import QtCore, QtGui +from mock import Mock from leap.common.check import leap_assert from leap.common.events import register @@ -38,6 +40,7 @@ from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.providerbootstrapper import ProviderBootstrapper from leap.services.soledad.soledadbootstrapper import SoledadBootstrapper +from leap.services.mail.smtpbootstrapper import SMTPBootstrapper from leap.platform_init import IS_MAC, IS_WIN from leap.platform_init.initializers import init_platform from leap.services.eip.vpn import VPN @@ -48,6 +51,8 @@ from leap.services.eip.vpnlaunchers import (VPNLauncherException, from leap.util import __version__ as VERSION from leap.util.checkerthread import CheckerThread +from leap.services.mail.smtpconfig import SMTPConfig + if IS_WIN: from leap.platform_init.locks import WindowsLock @@ -68,6 +73,10 @@ class MainWindow(QtGui.QMainWindow): # Keyring KEYRING_KEY = "leap_client" + # SMTP + PORT_KEY = "port" + IP_KEY = "ip_address" + # Signals new_updates = QtCore.Signal(object) raise_window = QtCore.Signal([]) @@ -177,9 +186,13 @@ class MainWindow(QtGui.QMainWindow): self._finish_eip_bootstrap) self._soledad_bootstrapper = SoledadBootstrapper() - self._soledad_bootstrapper.download_config.connect( + self._soledad_bootstrapper.gen_key.connect( self._soledad_bootstrapped_stage) + self._smtp_bootstrapper = SMTPBootstrapper() + self._smtp_bootstrapper.download_config.connect( + self._smtp_bootstrapped_stage) + self._vpn = VPN() self._vpn.state_changed.connect(self._update_vpn_state) self._vpn.status_changed.connect(self._update_vpn_status) @@ -239,6 +252,9 @@ class MainWindow(QtGui.QMainWindow): self._bypass_checks = bypass_checks self._soledad = None + self._keymanager = None + + self._smtp_config = SMTPConfig() if self._first_run(): self._wizard_firstrun = True @@ -707,7 +723,7 @@ class MainWindow(QtGui.QMainWindow): """ SLOT TRIGGERS: - self._soledad_bootstrapper.download_config + self._soledad_bootstrapper.gen_key If there was a problem, displays it, otherwise it does nothing. This is used for intermediate bootstrapping stages, in case @@ -722,6 +738,55 @@ class MainWindow(QtGui.QMainWindow): else: logger.debug("Done bootstrapping Soledad") + self._soledad = data[self._soledad_bootstrapper.SOLEDAD_KEY] + self._keymanager = data[self._soledad_bootstrapper.KEYMANAGER_KEY] + + self._smtp_bootstrapper.run_smtp_setup_checks( + self._checker_thread, + self._provider_config, + self._smtp_config, + True) + + def _smtp_bootstrapped_stage(self, data): + """ + SLOT + TRIGGERS: + self._smtp_bootstrapper.download_config + + If there was a problem, displays it, otherwise it does nothing. + This is used for intermediate bootstrapping stages, in case + they fail. + + :param data: result from the bootstrapping stage for Soledad + :type data: dict + """ + passed = data[self._smtp_bootstrapper.PASSED_KEY] + if not passed: + logger.error(data[self._smtp_bootstrapper.ERROR_KEY]) + else: + logger.debug("Done bootstrapping SMTP") + + hosts = self._smtp_config.get_hosts() + # TODO: handle more than one host and define how to choose + if len(hosts) > 0: + hostname = hosts.keys()[0] + logger.debug("Using hostname %s for SMTP" % (hostname,)) + host = hosts[hostname][self.IP_KEY].encode("utf-8") + port = hosts[hostname][self.PORT_KEY] + # TODO: pick local smtp port in a better way + # TODO: Make the encrypted_only configurable + + # TODO: Remove mocking!!! + self._keymanager.fetch_keys_from_server = Mock(return_value=[]) + from leap.mail.smtp import setup_smtp_relay + setup_smtp_relay(port=1234, + keymanager=self._keymanager, + smtp_host=host, + smtp_port=port, + smtp_username=".", + smtp_password=".", + encrypted_only=False) + def _get_socket_host(self): """ Returns the socket and port to be used for VPN diff --git a/src/leap/services/mail/__init__.py b/src/leap/services/mail/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py new file mode 100644 index 00000000..7e0f10de --- /dev/null +++ b/src/leap/services/mail/smtpbootstrapper.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# smtpbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +SMTP bootstrapping +""" + +import logging +import os + +import requests + +from PySide import QtCore + +from leap.common.check import leap_assert, leap_assert_type +from leap.common.files import get_mtime +from leap.config.providerconfig import ProviderConfig +from leap.crypto.srpauth import SRPAuth +from leap.util.request_helpers import get_content + +logger = logging.getLogger(__name__) + + +class SMTPBootstrapper(QtCore.QObject): + """ + SMTP init procedure + """ + + PASSED_KEY = "passed" + ERROR_KEY = "error" + + IDLE_SLEEP_INTERVAL = 100 + + # All dicts returned are of the form + # {"passed": bool, "error": str} + download_config = QtCore.Signal(dict) + + def __init__(self): + QtCore.QObject.__init__(self) + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + # **************************************************** # + + self._session = self._fetcher.session() + self._provider_config = None + self._smtp_config = None + self._download_if_needed = False + + def _download_config(self): + """ + Downloads the SMTP config for the given provider + + :return: True if everything went as expected, False otherwise + :rtype: bool + """ + + leap_assert(self._provider_config, + "We need a provider configuration!") + + logger.debug("Downloading SMTP config for %s" % + (self._provider_config.get_domain(),)) + + download_config_data = { + self.PASSED_KEY: False, + self.ERROR_KEY: "" + } + + try: + headers = {} + mtime = get_mtime(os.path.join(self._smtp_config + .get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + # there is some confusion with this uri, + config_uri = "%s/%s/config/smtp-service.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('Downloading SMTP config from: %s' % config_uri) + + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} + + res = self._session.get(config_uri, + verify=self._provider_config + .get_ca_cert_path(), + headers=headers, + cookies=cookies) + res.raise_for_status() + + # Not modified + if res.status_code == 304: + logger.debug("SMTP definition has not been modified") + self._smtp_config.load(os.path.join("leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json")) + else: + smtp_definition, mtime = get_content(res) + + self._smtp_config.load(data=smtp_definition, mtime=mtime) + self._smtp_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json"]) + + download_config_data[self.PASSED_KEY] = True + except Exception as e: + download_config_data[self.PASSED_KEY] = False + download_config_data[self.ERROR_KEY] = "%s" % (e,) + + logger.debug("Emitting download_config %s" % (download_config_data,)) + self.download_config.emit(download_config_data) + + return download_config_data[self.PASSED_KEY] + + def run_smtp_setup_checks(self, + checker, + provider_config, + smtp_config, + download_if_needed=False): + """ + Starts the checks needed for a new smtp setup + + :param checker: Object that executes actions in a different + thread + :type checker: leap.util.checkerthread.CheckerThread + :param provider_config: Provider configuration + :type provider_config: ProviderConfig + :param smtp_config: SMTP configuration to populate + :type smtp_config: SMTPConfig + :param download_if_needed: True if it should check for mtime + for the file + :type download_if_needed: bool + """ + leap_assert_type(provider_config, ProviderConfig) + + self._provider_config = provider_config + self._smtp_config = smtp_config + self._download_if_needed = download_if_needed + + checker.add_checks([ + self._download_config + ]) diff --git a/src/leap/services/mail/smtpconfig.py b/src/leap/services/mail/smtpconfig.py new file mode 100644 index 00000000..e7e2895a --- /dev/null +++ b/src/leap/services/mail/smtpconfig.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# smtpconfig.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +SMTP configuration +""" +import logging + +from leap.common.config.baseconfig import BaseConfig +from leap.services.mail.smtpspec import smtp_config_spec + +logger = logging.getLogger(__name__) + + +class SMTPConfig(BaseConfig): + """ + SMTP configuration abstraction class + """ + + def __init__(self): + BaseConfig.__init__(self) + + def _get_spec(self): + """ + Returns the spec object for the specific configuration + """ + return smtp_config_spec + + def get_hosts(self): + return self._safe_get_value("hosts") + + def get_locations(self): + return self._safe_get_value("locations") + diff --git a/src/leap/services/mail/smtpspec.py b/src/leap/services/mail/smtpspec.py new file mode 100644 index 00000000..b455b196 --- /dev/null +++ b/src/leap/services/mail/smtpspec.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# smtpspec.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +smtp_config_spec = { + 'description': 'sample smtp service config', + 'type': 'object', + 'properties': { + 'serial': { + 'type': int, + 'default': 1, + 'required': True + }, + 'version': { + 'type': int, + 'default': 1, + 'required': True + }, + 'hosts': { + 'type': dict, + 'default': { + "walrus": { + "hostname": "someprovider", + "ip_address": "1.1.1.1", + "port": 1111 + }, + }, + }, + 'locations': { + 'type': dict, + 'default': { + "locations": { + + } + } + } + } +} diff --git a/src/leap/services/soledad/soledadbootstrapper.py b/src/leap/services/soledad/soledadbootstrapper.py index 51c53a6e..bae933de 100644 --- a/src/leap/services/soledad/soledadbootstrapper.py +++ b/src/leap/services/soledad/soledadbootstrapper.py @@ -47,6 +47,8 @@ class SoledadBootstrapper(QtCore.QObject): PASSED_KEY = "passed" ERROR_KEY = "error" + SOLEDAD_KEY = "soledad" + KEYMANAGER_KEY = "keymanager" PUBKEY_KEY = "user[public_key]" @@ -199,7 +201,9 @@ class SoledadBootstrapper(QtCore.QObject): genkey_data = { self.PASSED_KEY: False, - self.ERROR_KEY: "" + self.ERROR_KEY: "", + self.SOLEDAD_KEY: None, + self.KEYMANAGER_KEY: None } try: @@ -245,6 +249,8 @@ class SoledadBootstrapper(QtCore.QObject): cookies=cookies) key_result.raise_for_status() genkey_data[self.PASSED_KEY] = True + genkey_data[self.SOLEDAD_KEY] = self._soledad + genkey_data[self.KEYMANAGER_KEY] = self._keymanager except Exception as e: genkey_data[self.PASSED_KEY] = False genkey_data[self.ERROR_KEY] = "%s" % (e,) -- cgit v1.2.3 From 372a9aad068e16c0ed957fac5c081ce409dbf7e7 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 21 May 2013 10:20:43 -0300 Subject: Add support for kde polkit, closes #2630 Close issue #2630 Add changes file for #2630 Add documentation to '_is_auth_agent_running' method. --- src/leap/services/eip/vpnlaunchers.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 540bc45e..addad959 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -124,9 +124,16 @@ def _has_updown_scripts(path): def _is_auth_agent_running(): - return len( - commands.getoutput( - 'ps aux | grep polkit-[g]nome-authentication-agent-1')) > 0 + """ + Checks if a polkit daemon is running. + + :return: True if it's running, False if it's not. + :rtype: boolean + """ + polkit_gnome = 'ps aux | grep polkit-[g]nome-authentication-agent-1' + polkit_kde = 'ps aux | grep polkit-[k]de-authentication-agent-1' + return (len(commands.getoutput(polkit_gnome) > 0) or + len(commands.getoutput(polkit_kde)) > 0) class LinuxVPNLauncher(VPNLauncher): -- cgit v1.2.3 From 8c54836c146ac1870996b0d00e116332b9b3d0a5 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 22 May 2013 11:13:31 -0300 Subject: Handle 'Incorrect Password' exception. closes issue #2640. don not automatic login if there is no password add changes file --- src/leap/gui/mainwindow.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 7eb956cb..dad88362 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -354,15 +354,22 @@ class MainWindow(QtGui.QMainWindow): self.ui.chkRemember.setChecked(True) self.ui.chkAutoLogin.setEnabled(self.ui.chkRemember .isEnabled()) - saved_password = keyring.get_password(self.KEYRING_KEY, - saved_user - .encode("utf8")) + + saved_password = None + try: + saved_password = keyring.get_password(self.KEYRING_KEY, + saved_user + .encode("utf8")) + except ValueError, e: + logger.debug("Incorrect Password. %r." % (e,)) + if saved_password is not None: self.ui.lnPassword.setText(saved_password.decode("utf8")) # Only automatically login if there is a saved user + # and the password was retrieved right self.ui.chkAutoLogin.setChecked(auto_login) - if auto_login: + if auto_login and saved_password: self._login() def _show_systray(self): -- cgit v1.2.3 From 9516f7e34cf265f87abb67e40b9fa76682012c84 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 23 May 2013 22:47:18 +0900 Subject: fix restore from minimized. Closes #2574 --- src/leap/gui/mainwindow.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 3e8a1c83..a8aa1b1c 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -420,12 +420,21 @@ class MainWindow(QtGui.QMainWindow): Toggles the window visibility """ + get_action = lambda visible: ( + self.tr("Show"), + self.tr("Hide"))[int(visible)] + + minimized = self.isMinimized() + if reason != QtGui.QSystemTrayIcon.Context: + # do show + if minimized: + self.showNormal() self.setVisible(not self.isVisible()) - action_visible_text = self.tr("Hide") - if not self.isVisible(): - action_visible_text = self.tr("Show") - self._action_visible.setText(action_visible_text) + + # set labels + visible = self.isVisible() + self._action_visible.setText(get_action(visible)) def _center_window(self): """ -- cgit v1.2.3 From 116f559198e0a9c26e9291209e81190bb27ef5cf Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 28 May 2013 20:51:58 -0300 Subject: Move KeyManager submodule from leap.common to here. --- src/leap/keymanager/__init__.py | 341 +++++++++++ src/leap/keymanager/errors.py | 86 +++ src/leap/keymanager/gpg.py | 397 +++++++++++++ src/leap/keymanager/keys.py | 285 ++++++++++ src/leap/keymanager/openpgp.py | 636 +++++++++++++++++++++ src/leap/keymanager/tests/__init__.py | 0 src/leap/keymanager/tests/test_keymanager.py | 686 +++++++++++++++++++++++ src/leap/services/soledad/soledadbootstrapper.py | 4 +- 8 files changed, 2433 insertions(+), 2 deletions(-) create mode 100644 src/leap/keymanager/__init__.py create mode 100644 src/leap/keymanager/errors.py create mode 100644 src/leap/keymanager/gpg.py create mode 100644 src/leap/keymanager/keys.py create mode 100644 src/leap/keymanager/openpgp.py create mode 100644 src/leap/keymanager/tests/__init__.py create mode 100644 src/leap/keymanager/tests/test_keymanager.py (limited to 'src') diff --git a/src/leap/keymanager/__init__.py b/src/leap/keymanager/__init__.py new file mode 100644 index 00000000..38e23d0e --- /dev/null +++ b/src/leap/keymanager/__init__.py @@ -0,0 +1,341 @@ +# -*- coding: utf-8 -*- +# __init__.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +Key Manager is a Nicknym agent for LEAP client. +""" + +import requests + +try: + import simplejson as json +except ImportError: + import json # noqa + +from leap.common.check import leap_assert +from leap.keymanager.errors import ( + KeyNotFound, + NoPasswordGiven, +) +from leap.keymanager.keys import ( + build_key_from_dict, + KEYMANAGER_KEY_TAG, + TAGS_PRIVATE_INDEX, +) +from leap.keymanager.openpgp import ( + OpenPGPKey, + OpenPGPScheme, +) + + +# +# The Key Manager +# + +class KeyManager(object): + + # + # server's key storage constants + # + + OPENPGP_KEY = 'openpgp' + PUBKEY_KEY = "user[public_key]" + + def __init__(self, address, nickserver_uri, soledad, session_id=None, + ca_cert_path=None, api_uri=None, api_version=None, uid=None): + """ + Initialize a Key Manager for user's C{address} with provider's + nickserver reachable in C{url}. + + :param address: The address of the user of this Key Manager. + :type address: str + :param url: The URL of the nickserver. + :type url: str + :param soledad: A Soledad instance for local storage of keys. + :type soledad: leap.soledad.Soledad + :param session_id: The session ID for interacting with the webapp API. + :type session_id: str + :param ca_cert_path: The path to the CA certificate. + :type ca_cert_path: str + :param api_uri: The URI of the webapp API. + :type api_uri: str + :param api_version: The version of the webapp API. + :type api_version: str + :param uid: The users' UID. + :type uid: str + """ + self._address = address + self._nickserver_uri = nickserver_uri + self._soledad = soledad + self._session_id = session_id + self.ca_cert_path = ca_cert_path + self.api_uri = api_uri + self.api_version = api_version + self.uid = uid + # a dict to map key types to their handlers + self._wrapper_map = { + OpenPGPKey: OpenPGPScheme(soledad), + # other types of key will be added to this mapper. + } + # the following are used to perform https requests + self._fetcher = requests + self._session = self._fetcher.session() + + # + # utilities + # + + def _key_class_from_type(self, ktype): + """ + Return key class from string representation of key type. + """ + return filter( + lambda klass: str(klass) == ktype, + self._wrapper_map).pop() + + def _get(self, uri, data=None): + """ + Send a GET request to C{uri} containing C{data}. + + :param uri: The URI of the request. + :type uri: str + :param data: The body of the request. + :type data: dict, str or file + + :return: The response to the request. + :rtype: requests.Response + """ + leap_assert( + self._ca_cert_path is not None, + 'We need the CA certificate path!') + res = self._fetcher.get(uri, data=data, verify=self._ca_cert_path) + # assert that the response is valid + res.raise_for_status() + leap_assert( + res.headers['content-type'].startswith('application/json'), + 'Content-type is not JSON.') + return res + + def _put(self, uri, data=None): + """ + Send a PUT request to C{uri} containing C{data}. + + The request will be sent using the configured CA certificate path to + verify the server certificate and the configured session id for + authentication. + + :param uri: The URI of the request. + :type uri: str + :param data: The body of the request. + :type data: dict, str or file + + :return: The response to the request. + :rtype: requests.Response + """ + leap_assert( + self._ca_cert_path is not None, + 'We need the CA certificate path!') + leap_assert( + self._session_id is not None, + 'We need a session_id to interact with webapp!') + res = self._fetcher.put( + uri, data=data, verify=self._ca_cert_path, + cookies={'_session_id': self._session_id}) + # assert that the response is valid + res.raise_for_status() + return res + + def _fetch_keys_from_server(self, address): + """ + Fetch keys bound to C{address} from nickserver and insert them in + local database. + + :param address: The address bound to the keys. + :type address: str + + @raise KeyNotFound: If the key was not found on nickserver. + """ + # request keys from the nickserver + server_keys = self._get( + self._nickserver_uri, {'address': address}).json() + # insert keys in local database + if self.OPENPGP_KEY in server_keys: + self._wrapper_map[OpenPGPKey].put_ascii_key( + server_keys['openpgp']) + + # + # key management + # + + def send_key(self, ktype): + """ + Send user's key of type C{ktype} to provider. + + Public key bound to user's is sent to provider, which will sign it and + replace any prior keys for the same address in its database. + + If C{send_private} is True, then the private key is encrypted with + C{password} and sent to server in the same request, together with a + hash string of user's address and password. The encrypted private key + will be saved in the server in a way it is publicly retrievable + through the hash string. + + :param ktype: The type of the key. + :type ktype: KeyType + + @raise KeyNotFound: If the key was not found in local database. + """ + leap_assert( + ktype is OpenPGPKey, + 'For now we only know how to send OpenPGP public keys.') + # prepare the public key bound to address + pubkey = self.get_key( + self._address, ktype, private=False, fetch_remote=False) + data = { + self.PUBKEY_KEY: pubkey.key_data + } + uri = "%s/%s/users/%s.json" % ( + self._api_uri, + self._api_version, + self._uid) + self._put(uri, data) + + def get_key(self, address, ktype, private=False, fetch_remote=True): + """ + Return a key of type C{ktype} bound to C{address}. + + First, search for the key in local storage. If it is not available, + then try to fetch from nickserver. + + :param address: The address bound to the key. + :type address: str + :param ktype: The type of the key. + :type ktype: KeyType + :param private: Look for a private key instead of a public one? + :type private: bool + + :return: A key of type C{ktype} bound to C{address}. + :rtype: EncryptionKey + @raise KeyNotFound: If the key was not found both locally and in + keyserver. + """ + leap_assert( + ktype in self._wrapper_map, + 'Unkown key type: %s.' % str(ktype)) + try: + # return key if it exists in local database + return self._wrapper_map[ktype].get_key(address, private=private) + except KeyNotFound: + # we will only try to fetch a key from nickserver if fetch_remote + # is True and the key is not private. + if fetch_remote is False or private is True: + raise + self._fetch_keys_from_server(address) + return self._wrapper_map[ktype].get_key(address, private=False) + + def get_all_keys_in_local_db(self, private=False): + """ + Return all keys stored in local database. + + :return: A list with all keys in local db. + :rtype: list + """ + return map( + lambda doc: build_key_from_dict( + self._key_class_from_type(doc.content['type']), + doc.content['address'], + doc.content), + self._soledad.get_from_index( + TAGS_PRIVATE_INDEX, + KEYMANAGER_KEY_TAG, + '1' if private else '0')) + + def refresh_keys(self): + """ + Fetch keys from nickserver and update them locally. + """ + addresses = set(map( + lambda doc: doc.address, + self.get_all_keys_in_local_db(private=False))) + for address in addresses: + # do not attempt to refresh our own key + if address == self._address: + continue + self._fetch_keys_from_server(address) + + def gen_key(self, ktype): + """ + Generate a key of type C{ktype} bound to the user's address. + + :param ktype: The type of the key. + :type ktype: KeyType + + :return: The generated key. + :rtype: EncryptionKey + """ + return self._wrapper_map[ktype].gen_key(self._address) + + # + # Setters/getters + # + + def _get_session_id(self): + return self._session_id + + def _set_session_id(self, session_id): + self._session_id = session_id + + session_id = property( + _get_session_id, _set_session_id, doc='The session id.') + + def _get_ca_cert_path(self): + return self._ca_cert_path + + def _set_ca_cert_path(self, ca_cert_path): + self._ca_cert_path = ca_cert_path + + ca_cert_path = property( + _get_ca_cert_path, _set_ca_cert_path, + doc='The path to the CA certificate.') + + def _get_api_uri(self): + return self._api_uri + + def _set_api_uri(self, api_uri): + self._api_uri = api_uri + + api_uri = property( + _get_api_uri, _set_api_uri, doc='The webapp API URI.') + + def _get_api_version(self): + return self._api_version + + def _set_api_version(self, api_version): + self._api_version = api_version + + api_version = property( + _get_api_version, _set_api_version, doc='The webapp API version.') + + def _get_uid(self): + return self._uid + + def _set_uid(self, uid): + self._uid = uid + + uid = property( + _get_uid, _set_uid, doc='The uid of the user.') diff --git a/src/leap/keymanager/errors.py b/src/leap/keymanager/errors.py new file mode 100644 index 00000000..89949d29 --- /dev/null +++ b/src/leap/keymanager/errors.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# errors.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +Errors and exceptions used by the Key Manager. +""" + + +class KeyNotFound(Exception): + """ + Raised when key was no found on keyserver. + """ + pass + + +class KeyAlreadyExists(Exception): + """ + Raised when attempted to create a key that already exists. + """ + pass + + +class KeyAttributesDiffer(Exception): + """ + Raised when trying to delete a key but the stored key differs from the key + passed to the delete_key() method. + """ + pass + + +class NoPasswordGiven(Exception): + """ + Raised when trying to perform some action that needs a password without + providing one. + """ + pass + + +class InvalidSignature(Exception): + """ + Raised when signature could not be verified. + """ + pass + + +class EncryptionFailed(Exception): + """ + Raised upon failures of encryption. + """ + pass + + +class DecryptionFailed(Exception): + """ + Raised upon failures of decryption. + """ + pass + + +class EncryptionDecryptionFailed(Exception): + """ + Raised upon failures of encryption/decryption. + """ + pass + + +class SignFailed(Exception): + """ + Raised when failed to sign. + """ + pass diff --git a/src/leap/keymanager/gpg.py b/src/leap/keymanager/gpg.py new file mode 100644 index 00000000..15c1d9f6 --- /dev/null +++ b/src/leap/keymanager/gpg.py @@ -0,0 +1,397 @@ +# -*- coding: utf-8 -*- +# gpgwrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +A GPG wrapper used to handle OpenPGP keys. + +This is a temporary class that will be superseded by the a revised version of +python-gnupg. +""" + + +import os +import gnupg +import re +from gnupg import ( + logger, + _is_sequence, + _make_binary_stream, +) + + +class ListPackets(): + """ + Handle status messages for --list-packets. + """ + + def __init__(self, gpg): + """ + Initialize the packet listing handling class. + + :param gpg: GPG object instance. + :type gpg: gnupg.GPG + """ + self.gpg = gpg + self.nodata = None + self.key = None + self.need_passphrase = None + self.need_passphrase_sym = None + self.userid_hint = None + + def handle_status(self, key, value): + """ + Handle one line of the --list-packets status message. + + :param key: The status message key. + :type key: str + :param value: The status message value. + :type value: str + """ + # TODO: write tests for handle_status + if key == 'NODATA': + self.nodata = True + if key == 'ENC_TO': + # This will only capture keys in our keyring. In the future we + # may want to include multiple unknown keys in this list. + self.key, _, _ = value.split() + if key == 'NEED_PASSPHRASE': + self.need_passphrase = True + if key == 'NEED_PASSPHRASE_SYM': + self.need_passphrase_sym = True + if key == 'USERID_HINT': + self.userid_hint = value.strip().split() + + +class GPGWrapper(gnupg.GPG): + """ + This is a temporary class for handling GPG requests, and should be + replaced by a more general class used throughout the project. + """ + + GNUPG_HOME = os.environ['HOME'] + "/.config/leap/gnupg" + GNUPG_BINARY = "/usr/bin/gpg" # this has to be changed based on OS + + def __init__(self, gpgbinary=GNUPG_BINARY, gnupghome=GNUPG_HOME, + verbose=False, use_agent=False, keyring=None, options=None): + """ + Initialize a GnuPG process wrapper. + + :param gpgbinary: Name for GnuPG binary executable. + :type gpgbinary: C{str} + :param gpghome: Full pathname to directory containing the public and + private keyrings. + :type gpghome: C{str} + :param keyring: Name of alternative keyring file to use. If specified, + the default keyring is not used. + :param verbose: Should some verbose info be output? + :type verbose: bool + :param use_agent: Should pass `--use-agent` to GPG binary? + :type use_agent: bool + :param keyring: Path for the keyring to use. + :type keyring: str + @options: A list of additional options to pass to the GPG binary. + :type options: list + + @raise: RuntimeError with explanation message if there is a problem + invoking gpg. + """ + gnupg.GPG.__init__(self, gnupghome=gnupghome, gpgbinary=gpgbinary, + verbose=verbose, use_agent=use_agent, + keyring=keyring, options=options) + self.result_map['list-packets'] = ListPackets + + def find_key_by_email(self, email, secret=False): + """ + Find user's key based on their email. + + :param email: Email address of key being searched for. + :type email: str + :param secret: Should we search for a secret key? + :type secret: bool + + :return: The fingerprint of the found key. + :rtype: str + """ + for key in self.list_keys(secret=secret): + for uid in key['uids']: + if re.search(email, uid): + return key + raise LookupError("GnuPG public key for email %s not found!" % email) + + def find_key_by_subkey(self, subkey, secret=False): + """ + Find user's key based on a subkey fingerprint. + + :param email: Subkey fingerprint of the key being searched for. + :type email: str + :param secret: Should we search for a secret key? + :type secret: bool + + :return: The fingerprint of the found key. + :rtype: str + """ + for key in self.list_keys(secret=secret): + for sub in key['subkeys']: + if sub[0] == subkey: + return key + raise LookupError( + "GnuPG public key for subkey %s not found!" % subkey) + + def find_key_by_keyid(self, keyid, secret=False): + """ + Find user's key based on the key ID. + + :param email: The key ID of the key being searched for. + :type email: str + :param secret: Should we search for a secret key? + :type secret: bool + + :return: The fingerprint of the found key. + :rtype: str + """ + for key in self.list_keys(secret=secret): + if keyid == key['keyid']: + return key + raise LookupError( + "GnuPG public key for keyid %s not found!" % keyid) + + def find_key_by_fingerprint(self, fingerprint, secret=False): + """ + Find user's key based on the key fingerprint. + + :param email: The fingerprint of the key being searched for. + :type email: str + :param secret: Should we search for a secret key? + :type secret: bool + + :return: The fingerprint of the found key. + :rtype: str + """ + for key in self.list_keys(secret=secret): + if fingerprint == key['fingerprint']: + return key + raise LookupError( + "GnuPG public key for fingerprint %s not found!" % fingerprint) + + def encrypt(self, data, recipient, sign=None, always_trust=True, + passphrase=None, symmetric=False): + """ + Encrypt data using GPG. + + :param data: The data to be encrypted. + :type data: str + :param recipient: The address of the public key to be used. + :type recipient: str + :param sign: Should the encrypted content be signed? + :type sign: bool + :param always_trust: Skip key validation and assume that used keys + are always fully trusted? + :type always_trust: bool + :param passphrase: The passphrase to be used if symmetric encryption + is desired. + :type passphrase: str + :param symmetric: Should we encrypt to a password? + :type symmetric: bool + + :return: An object with encrypted result in the `data` field. + :rtype: gnupg.Crypt + """ + # TODO: devise a way so we don't need to "always trust". + return gnupg.GPG.encrypt(self, data, recipient, sign=sign, + always_trust=always_trust, + passphrase=passphrase, + symmetric=symmetric, + cipher_algo='AES256') + + def decrypt(self, data, always_trust=True, passphrase=None): + """ + Decrypt data using GPG. + + :param data: The data to be decrypted. + :type data: str + :param always_trust: Skip key validation and assume that used keys + are always fully trusted? + :type always_trust: bool + :param passphrase: The passphrase to be used if symmetric encryption + is desired. + :type passphrase: str + + :return: An object with decrypted result in the `data` field. + :rtype: gnupg.Crypt + """ + # TODO: devise a way so we don't need to "always trust". + return gnupg.GPG.decrypt(self, data, always_trust=always_trust, + passphrase=passphrase) + + def send_keys(self, keyserver, *keyids): + """ + Send keys to a keyserver + + :param keyserver: The keyserver to send the keys to. + :type keyserver: str + :param keyids: The key ids to send. + :type keyids: list + + :return: A list of keys sent to server. + :rtype: gnupg.ListKeys + """ + # TODO: write tests for this. + # TODO: write a SendKeys class to handle status for this. + result = self.result_map['list'](self) + gnupg.logger.debug('send_keys: %r', keyids) + data = gnupg._make_binary_stream("", self.encoding) + args = ['--keyserver', keyserver, '--send-keys'] + args.extend(keyids) + self._handle_io(args, data, result, binary=True) + gnupg.logger.debug('send_keys result: %r', result.__dict__) + data.close() + return result + + def encrypt_file(self, file, recipients, sign=None, + always_trust=False, passphrase=None, + armor=True, output=None, symmetric=False, + cipher_algo=None): + """ + Encrypt the message read from the file-like object 'file'. + + :param file: The file to be encrypted. + :type data: file + :param recipient: The address of the public key to be used. + :type recipient: str + :param sign: Should the encrypted content be signed? + :type sign: bool + :param always_trust: Skip key validation and assume that used keys + are always fully trusted? + :type always_trust: bool + :param passphrase: The passphrase to be used if symmetric encryption + is desired. + :type passphrase: str + :param armor: Create ASCII armored output? + :type armor: bool + :param output: Path of file to write results in. + :type output: str + :param symmetric: Should we encrypt to a password? + :type symmetric: bool + :param cipher_algo: Algorithm to use. + :type cipher_algo: str + + :return: An object with encrypted result in the `data` field. + :rtype: gnupg.Crypt + """ + args = ['--encrypt'] + if symmetric: + args = ['--symmetric'] + if cipher_algo: + args.append('--cipher-algo %s' % cipher_algo) + else: + args = ['--encrypt'] + if not _is_sequence(recipients): + recipients = (recipients,) + for recipient in recipients: + args.append('--recipient "%s"' % recipient) + if armor: # create ascii-armored output - set to False for binary + args.append('--armor') + if output: # write the output to a file with the specified name + if os.path.exists(output): + os.remove(output) # to avoid overwrite confirmation message + args.append('--output "%s"' % output) + if sign: + args.append('--sign --default-key "%s"' % sign) + if always_trust: + args.append("--always-trust") + result = self.result_map['crypt'](self) + self._handle_io(args, file, result, passphrase=passphrase, binary=True) + logger.debug('encrypt result: %r', result.data) + return result + + def list_packets(self, data): + """ + List the sequence of packets. + + :param data: The data to extract packets from. + :type data: str + + :return: An object with packet info. + :rtype ListPackets + """ + args = ["--list-packets"] + result = self.result_map['list-packets'](self) + self._handle_io( + args, + _make_binary_stream(data, self.encoding), + result, + ) + return result + + def encrypted_to(self, data): + """ + Return the key to which data is encrypted to. + + :param data: The data to be examined. + :type data: str + + :return: The fingerprint of the key to which data is encrypted to. + :rtype: str + """ + # TODO: make this support multiple keys. + result = self.list_packets(data) + if not result.key: + raise LookupError( + "Content is not encrypted to a GnuPG key!") + try: + return self.find_key_by_keyid(result.key) + except: + return self.find_key_by_subkey(result.key) + + def is_encrypted_sym(self, data): + """ + Say whether some chunk of data is encrypted to a symmetric key. + + :param data: The data to be examined. + :type data: str + + :return: Whether data is encrypted to a symmetric key. + :rtype: bool + """ + result = self.list_packets(data) + return bool(result.need_passphrase_sym) + + def is_encrypted_asym(self, data): + """ + Say whether some chunk of data is encrypted to a private key. + + :param data: The data to be examined. + :type data: str + + :return: Whether data is encrypted to a private key. + :rtype: bool + """ + result = self.list_packets(data) + return bool(result.key) + + def is_encrypted(self, data): + """ + Say whether some chunk of data is encrypted to a key. + + :param data: The data to be examined. + :type data: str + + :return: Whether data is encrypted to a key. + :rtype: bool + """ + return self.is_encrypted_asym(data) or self.is_encrypted_sym(data) diff --git a/src/leap/keymanager/keys.py b/src/leap/keymanager/keys.py new file mode 100644 index 00000000..44bd587b --- /dev/null +++ b/src/leap/keymanager/keys.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- +# keys.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +Abstact key type and encryption scheme representations. +""" + + +try: + import simplejson as json +except ImportError: + import json # noqa +import re + + +from abc import ABCMeta, abstractmethod +from leap.common.check import leap_assert + + +# +# Dictionary keys used for storing cryptographic keys. +# + +KEY_ADDRESS_KEY = 'address' +KEY_TYPE_KEY = 'type' +KEY_ID_KEY = 'key_id' +KEY_FINGERPRINT_KEY = 'fingerprint' +KEY_DATA_KEY = 'key_data' +KEY_PRIVATE_KEY = 'private' +KEY_LENGTH_KEY = 'length' +KEY_EXPIRY_DATE_KEY = 'expiry_date' +KEY_FIRST_SEEN_AT_KEY = 'first_seen_at' +KEY_LAST_AUDITED_AT_KEY = 'last_audited_at' +KEY_VALIDATION_KEY = 'validation' +KEY_TAGS_KEY = 'tags' + + +# +# Key storage constants +# + +KEYMANAGER_KEY_TAG = 'keymanager-key' + + +# +# key indexing constants. +# + +TAGS_PRIVATE_INDEX = 'by-tags-private' +TAGS_ADDRESS_PRIVATE_INDEX = 'by-tags-address-private' +INDEXES = { + TAGS_PRIVATE_INDEX: [ + KEY_TAGS_KEY, + 'bool(%s)' % KEY_PRIVATE_KEY, + ], + TAGS_ADDRESS_PRIVATE_INDEX: [ + KEY_TAGS_KEY, + KEY_ADDRESS_KEY, + 'bool(%s)' % KEY_PRIVATE_KEY, + ] +} + + +# +# Key handling utilities +# + +def is_address(address): + """ + Return whether the given C{address} is in the form user@provider. + + :param address: The address to be tested. + :type address: str + :return: Whether C{address} is in the form user@provider. + :rtype: bool + """ + return bool(re.match('[\w.-]+@[\w.-]+', address)) + + +def build_key_from_dict(kClass, address, kdict): + """ + Build an C{kClass} key bound to C{address} based on info in C{kdict}. + + :param address: The address bound to the key. + :type address: str + :param kdict: Dictionary with key data. + :type kdict: dict + :return: An instance of the key. + :rtype: C{kClass} + """ + leap_assert( + address == kdict[KEY_ADDRESS_KEY], + 'Wrong address in key data.') + return kClass( + address, + key_id=kdict[KEY_ID_KEY], + fingerprint=kdict[KEY_FINGERPRINT_KEY], + key_data=kdict[KEY_DATA_KEY], + private=kdict[KEY_PRIVATE_KEY], + length=kdict[KEY_LENGTH_KEY], + expiry_date=kdict[KEY_EXPIRY_DATE_KEY], + first_seen_at=kdict[KEY_FIRST_SEEN_AT_KEY], + last_audited_at=kdict[KEY_LAST_AUDITED_AT_KEY], + validation=kdict[KEY_VALIDATION_KEY], # TODO: verify for validation. + ) + + +# +# Abstraction for encryption keys +# + +class EncryptionKey(object): + """ + Abstract class for encryption keys. + + A key is "validated" if the nicknym agent has bound the user address to a + public key. Nicknym supports three different levels of key validation: + + * Level 3 - path trusted: A path of cryptographic signatures can be traced + from a trusted key to the key under evaluation. By default, only the + provider key from the user's provider is a "trusted key". + * level 2 - provider signed: The key has been signed by a provider key for + the same domain, but the provider key is not validated using a trust + path (i.e. it is only registered) + * level 1 - registered: The key has been encountered and saved, it has no + signatures (that are meaningful to the nicknym agent). + """ + + __metaclass__ = ABCMeta + + def __init__(self, address, key_id=None, fingerprint=None, + key_data=None, private=None, length=None, expiry_date=None, + validation=None, first_seen_at=None, last_audited_at=None): + self.address = address + self.key_id = key_id + self.fingerprint = fingerprint + self.key_data = key_data + self.private = private + self.length = length + self.expiry_date = expiry_date + self.validation = validation + self.first_seen_at = first_seen_at + self.last_audited_at = last_audited_at + + def get_json(self): + """ + Return a JSON string describing this key. + + :return: The JSON string describing this key. + :rtype: str + """ + return json.dumps({ + KEY_ADDRESS_KEY: self.address, + KEY_TYPE_KEY: str(self.__class__), + KEY_ID_KEY: self.key_id, + KEY_FINGERPRINT_KEY: self.fingerprint, + KEY_DATA_KEY: self.key_data, + KEY_PRIVATE_KEY: self.private, + KEY_LENGTH_KEY: self.length, + KEY_EXPIRY_DATE_KEY: self.expiry_date, + KEY_VALIDATION_KEY: self.validation, + KEY_FIRST_SEEN_AT_KEY: self.first_seen_at, + KEY_LAST_AUDITED_AT_KEY: self.last_audited_at, + KEY_TAGS_KEY: [KEYMANAGER_KEY_TAG], + }) + + def __repr__(self): + """ + Representation of this class + """ + return u"<%s 0x%s (%s - %s)>" % ( + self.__class__.__name__, + self.key_id, + self.address, + "priv" if self.private else "publ") + + +# +# Encryption schemes +# + +class EncryptionScheme(object): + """ + Abstract class for Encryption Schemes. + + A wrapper for a certain encryption schemes should know how to get and put + keys in local storage using Soledad, how to generate new keys and how to + find out about possibly encrypted content. + """ + + __metaclass__ = ABCMeta + + def __init__(self, soledad): + """ + Initialize this Encryption Scheme. + + :param soledad: A Soledad instance for local storage of keys. + :type soledad: leap.soledad.Soledad + """ + self._soledad = soledad + self._init_indexes() + + def _init_indexes(self): + """ + Initialize the database indexes. + """ + # Ask the database for currently existing indexes. + db_indexes = dict(self._soledad.list_indexes()) + # Loop through the indexes we expect to find. + for name, expression in INDEXES.items(): + if name not in db_indexes: + # The index does not yet exist. + self._soledad.create_index(name, *expression) + continue + if expression == db_indexes[name]: + # The index exists and is up to date. + continue + # The index exists but the definition is not what expected, so we + # delete it and add the proper index expression. + self._soledad.delete_index(name) + self._soledad.create_index(name, *expression) + + @abstractmethod + def get_key(self, address, private=False): + """ + Get key from local storage. + + :param address: The address bound to the key. + :type address: str + :param private: Look for a private key instead of a public one? + :type private: bool + + :return: The key bound to C{address}. + :rtype: EncryptionKey + @raise KeyNotFound: If the key was not found on local storage. + """ + pass + + @abstractmethod + def put_key(self, key): + """ + Put a key in local storage. + + :param key: The key to be stored. + :type key: EncryptionKey + """ + pass + + @abstractmethod + def gen_key(self, address): + """ + Generate a new key. + + :param address: The address bound to the key. + :type address: str + + :return: The key bound to C{address}. + :rtype: EncryptionKey + """ + pass + + @abstractmethod + def delete_key(self, key): + """ + Remove C{key} from storage. + + :param key: The key to be removed. + :type key: EncryptionKey + """ + pass diff --git a/src/leap/keymanager/openpgp.py b/src/leap/keymanager/openpgp.py new file mode 100644 index 00000000..89323117 --- /dev/null +++ b/src/leap/keymanager/openpgp.py @@ -0,0 +1,636 @@ +# -*- coding: utf-8 -*- +# openpgp.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Infrastructure for using OpenPGP keys in Key Manager. +""" +import logging +import os +import re +import shutil +import tempfile + +from leap.common.check import leap_assert, leap_assert_type +from leap.keymanager import errors +from leap.keymanager.keys import ( + EncryptionKey, + EncryptionScheme, + is_address, + build_key_from_dict, + KEYMANAGER_KEY_TAG, + TAGS_ADDRESS_PRIVATE_INDEX, +) +from leap.keymanager.gpg import GPGWrapper + +logger = logging.getLogger(__name__) + + +# +# gpg wrapper and decorator +# + +def temporary_gpgwrapper(keys=None): + """ + Returns a unitary gpg wrapper that implements context manager + protocol. + + :param key_data: ASCII armored key data. + :type key_data: str + + :return: a GPGWrapper instance + :rtype: GPGWrapper + """ + # TODO do here checks on key_data + return TempGPGWrapper(keys=keys) + + +def with_temporary_gpg(fun): + """ + Decorator to add a temporary gpg wrapper as context + to gpg related functions. + + Decorated functions are expected to return a function whose only + argument is a gpgwrapper instance. + """ + def wrapped(*args, **kwargs): + """ + We extract the arguments passed to the wrapped function, + run the function and do validations. + We expect that the positional arguments are `data`, + and an optional `key`. + All the rest of arguments should be passed as named arguments + to allow for a correct unpacking. + """ + if len(args) == 2: + keys = args[1] if isinstance(args[1], OpenPGPKey) else None + else: + keys = None + + # sign/verify keys passed as arguments + sign = kwargs.get('sign', None) + if sign: + keys = [keys, sign] + + verify = kwargs.get('verify', None) + if verify: + keys = [keys, verify] + + # is the wrapped function sign or verify? + fun_name = fun.__name__ + is_sign_function = True if fun_name == "sign" else False + is_verify_function = True if fun_name == "verify" else False + + result = None + + with temporary_gpgwrapper(keys) as gpg: + result = fun(*args, **kwargs)(gpg) + + # TODO: cleanup a little bit the + # validation. maybe delegate to other + # auxiliary functions for clarity. + + ok = getattr(result, 'ok', None) + + stderr = getattr(result, 'stderr', None) + if stderr: + logger.debug("%s" % (stderr,)) + + if ok is False: + raise errors.EncryptionDecryptionFailed( + 'Failed to encrypt/decrypt in %s: %s' % ( + fun.__name__, + stderr)) + + if verify is not None: + # A verify key has been passed + if result.valid is False or \ + verify.fingerprint != result.pubkey_fingerprint: + raise errors.InvalidSignature( + 'Failed to verify signature with key %s: %s' % + (verify.key_id, stderr)) + + if is_sign_function: + # Specific validation for sign function + privkey = gpg.list_keys(secret=True).pop() + rfprint = result.fingerprint + kfprint = privkey['fingerprint'] + if result.fingerprint is None: + raise errors.SignFailed( + 'Failed to sign with key %s: %s' % + (privkey['keyid'], stderr)) + leap_assert( + result.fingerprint == kfprint, + 'Signature and private key fingerprints mismatch: ' + '%s != %s' % + (rfprint, kfprint)) + + if is_verify_function: + # Specific validation for verify function + pubkey = gpg.list_keys().pop() + valid = result.valid + rfprint = result.fingerprint + kfprint = pubkey['fingerprint'] + if valid is False or rfprint != kfprint: + raise errors.InvalidSignature( + 'Failed to verify signature ' + 'with key %s.' % pubkey['keyid']) + result = result.valid + + # ok, enough checks. let's return data if available + if hasattr(result, 'data'): + result = result.data + return result + return wrapped + + +class TempGPGWrapper(object): + """ + A context manager returning a temporary GPG wrapper keyring, which + contains exactly zero or one pubkeys, and zero or one privkeys. + + Temporary unitary keyrings allow the to use GPG's facilities for exactly + one key. This function creates an empty temporary keyring and imports + C{keys} if it is not None. + """ + def __init__(self, keys=None): + """ + :param keys: OpenPGP key, or list of. + :type keys: OpenPGPKey or list of OpenPGPKeys + """ + self._gpg = None + if not keys: + keys = list() + if not isinstance(keys, list): + keys = [keys] + self._keys = keys + for key in filter(None, keys): + leap_assert_type(key, OpenPGPKey) + + def __enter__(self): + """ + Calls the unitary gpgwrapper initializer + + :return: A GPG wrapper with a unitary keyring. + :rtype: gnupg.GPG + """ + self._build_keyring() + return self._gpg + + def __exit__(self, exc_type, exc_value, traceback): + """ + Ensures the gpgwrapper is properly destroyed. + """ + # TODO handle exceptions and log here + self._destroy_keyring() + + def _build_keyring(self): + """ + Create an empty GPG keyring and import C{keys} into it. + + :param keys: List of keys to add to the keyring. + :type keys: list of OpenPGPKey + + :return: A GPG wrapper with a unitary keyring. + :rtype: gnupg.GPG + """ + privkeys = [key for key in self._keys if key and key.private is True] + publkeys = [key for key in self._keys if key and key.private is False] + # here we filter out public keys that have a correspondent + # private key in the list because the private key_data by + # itself is enough to also have the public key in the keyring, + # and we want to count the keys afterwards. + + privaddrs = map(lambda privkey: privkey.address, privkeys) + publkeys = filter( + lambda pubkey: pubkey.address not in privaddrs, publkeys) + + listkeys = lambda: self._gpg.list_keys() + listsecretkeys = lambda: self._gpg.list_keys(secret=True) + + self._gpg = GPGWrapper(gnupghome=tempfile.mkdtemp()) + leap_assert(len(listkeys()) is 0, 'Keyring not empty.') + + # import keys into the keyring: + # concatenating ascii-armored keys, which is correctly + # understood by the GPGWrapper. + + self._gpg.import_keys("".join( + [x.key_data for x in publkeys + privkeys])) + + # assert the number of keys in the keyring + leap_assert( + len(listkeys()) == len(publkeys) + len(privkeys), + 'Wrong number of public keys in keyring: %d, should be %d)' % + (len(listkeys()), len(publkeys) + len(privkeys))) + leap_assert( + len(listsecretkeys()) == len(privkeys), + 'Wrong number of private keys in keyring: %d, should be %d)' % + (len(listsecretkeys()), len(privkeys))) + + def _destroy_keyring(self): + """ + Securely erase a unitary keyring. + """ + # TODO: implement some kind of wiping of data or a more + # secure way that + # does not write to disk. + + try: + for secret in [True, False]: + for key in self._gpg.list_keys(secret=secret): + self._gpg.delete_keys( + key['fingerprint'], + secret=secret) + leap_assert(len(self._gpg.list_keys()) is 0, 'Keyring not empty!') + + except: + raise + + finally: + leap_assert(self._gpg.gnupghome != os.path.expanduser('~/.gnupg'), + "watch out! Tried to remove default gnupg home!") + shutil.rmtree(self._gpg.gnupghome) + + +# +# API functions +# + +@with_temporary_gpg +def encrypt_asym(data, key, passphrase=None, sign=None): + """ + Encrypt C{data} using public @{key} and sign with C{sign} key. + + :param data: The data to be encrypted. + :type data: str + :param pubkey: The key used to encrypt. + :type pubkey: OpenPGPKey + :param sign: The key used for signing. + :type sign: OpenPGPKey + + :return: The encrypted data. + :rtype: str + """ + leap_assert_type(key, OpenPGPKey) + leap_assert(key.private is False, 'Key is not public.') + if sign is not None: + leap_assert_type(sign, OpenPGPKey) + leap_assert(sign.private is True) + + # Here we cannot assert for correctness of sig because the sig is in + # the ciphertext. + # result.ok - (bool) indicates if the operation succeeded + # result.data - (bool) contains the result of the operation + + return lambda gpg: gpg.encrypt( + data, key.fingerprint, + sign=sign.key_id if sign else None, + passphrase=passphrase, symmetric=False) + + +@with_temporary_gpg +def decrypt_asym(data, key, passphrase=None, verify=None): + """ + Decrypt C{data} using private @{key} and verify with C{verify} key. + + :param data: The data to be decrypted. + :type data: str + :param privkey: The key used to decrypt. + :type privkey: OpenPGPKey + :param verify: The key used to verify a signature. + :type verify: OpenPGPKey + + :return: The decrypted data. + :rtype: str + + @raise InvalidSignature: Raised if unable to verify the signature with + C{verify} key. + """ + leap_assert(key.private is True, 'Key is not private.') + if verify is not None: + leap_assert_type(verify, OpenPGPKey) + leap_assert(verify.private is False) + + return lambda gpg: gpg.decrypt( + data, passphrase=passphrase) + + +@with_temporary_gpg +def is_encrypted(data): + """ + Return whether C{data} was encrypted using OpenPGP. + + :param data: The data we want to know about. + :type data: str + + :return: Whether C{data} was encrypted using this wrapper. + :rtype: bool + """ + return lambda gpg: gpg.is_encrypted(data) + + +@with_temporary_gpg +def is_encrypted_asym(data): + """ + Return whether C{data} was asymmetrically encrypted using OpenPGP. + + :param data: The data we want to know about. + :type data: str + + :return: Whether C{data} was encrypted using this wrapper. + :rtype: bool + """ + return lambda gpg: gpg.is_encrypted_asym(data) + + +@with_temporary_gpg +def sign(data, privkey): + """ + Sign C{data} with C{privkey}. + + :param data: The data to be signed. + :type data: str + + :param privkey: The private key to be used to sign. + :type privkey: OpenPGPKey + + :return: The ascii-armored signed data. + :rtype: str + """ + leap_assert_type(privkey, OpenPGPKey) + leap_assert(privkey.private is True) + + # result.fingerprint - contains the fingerprint of the key used to + # sign. + return lambda gpg: gpg.sign(data, keyid=privkey.key_id) + + +@with_temporary_gpg +def verify(data, key): + """ + Verify signed C{data} with C{pubkey}. + + :param data: The data to be verified. + :type data: str + + :param pubkey: The public key to be used on verification. + :type pubkey: OpenPGPKey + + :return: The ascii-armored signed data. + :rtype: str + """ + leap_assert_type(key, OpenPGPKey) + leap_assert(key.private is False) + + return lambda gpg: gpg.verify(data) + + +# +# Helper functions +# + + +def _build_key_from_gpg(address, key, key_data): + """ + Build an OpenPGPKey for C{address} based on C{key} from + local gpg storage. + + ASCII armored GPG key data has to be queried independently in this + wrapper, so we receive it in C{key_data}. + + :param address: The address bound to the key. + :type address: str + :param key: Key obtained from GPG storage. + :type key: dict + :param key_data: Key data obtained from GPG storage. + :type key_data: str + :return: An instance of the key. + :rtype: OpenPGPKey + """ + return OpenPGPKey( + address, + key_id=key['keyid'], + fingerprint=key['fingerprint'], + key_data=key_data, + private=True if key['type'] == 'sec' else False, + length=key['length'], + expiry_date=key['expires'], + validation=None, # TODO: verify for validation. + ) + + +# +# The OpenPGP wrapper +# + +class OpenPGPKey(EncryptionKey): + """ + Base class for OpenPGP keys. + """ + + +class OpenPGPScheme(EncryptionScheme): + """ + A wrapper for OpenPGP keys. + """ + + def __init__(self, soledad): + """ + Initialize the OpenPGP wrapper. + + :param soledad: A Soledad instance for key storage. + :type soledad: leap.soledad.Soledad + """ + EncryptionScheme.__init__(self, soledad) + + def gen_key(self, address): + """ + Generate an OpenPGP keypair bound to C{address}. + + :param address: The address bound to the key. + :type address: str + :return: The key bound to C{address}. + :rtype: OpenPGPKey + @raise KeyAlreadyExists: If key already exists in local database. + """ + # make sure the key does not already exist + leap_assert(is_address(address), 'Not an user address: %s' % address) + try: + self.get_key(address) + raise errors.KeyAlreadyExists(address) + except errors.KeyNotFound: + pass + + def _gen_key(gpg): + params = gpg.gen_key_input( + key_type='RSA', + key_length=4096, + name_real=address, + name_email=address, + name_comment='Generated by LEAP Key Manager.') + gpg.gen_key(params) + pubkeys = gpg.list_keys() + # assert for new key characteristics + leap_assert( + len(pubkeys) is 1, # a unitary keyring! + 'Keyring has wrong number of keys: %d.' % len(pubkeys)) + key = gpg.list_keys(secret=True).pop() + leap_assert( + len(key['uids']) is 1, # with just one uid! + 'Wrong number of uids for key: %d.' % len(key['uids'])) + leap_assert( + re.match('.*<%s>$' % address, key['uids'][0]) is not None, + 'Key not correctly bound to address.') + # insert both public and private keys in storage + for secret in [True, False]: + key = gpg.list_keys(secret=secret).pop() + openpgp_key = _build_key_from_gpg( + address, key, + gpg.export_keys(key['fingerprint'], secret=secret)) + self.put_key(openpgp_key) + + with temporary_gpgwrapper() as gpg: + # TODO: inspect result, or use decorator + _gen_key(gpg) + + return self.get_key(address, private=True) + + def get_key(self, address, private=False): + """ + Get key bound to C{address} from local storage. + + :param address: The address bound to the key. + :type address: str + :param private: Look for a private key instead of a public one? + :type private: bool + + :return: The key bound to C{address}. + :rtype: OpenPGPKey + @raise KeyNotFound: If the key was not found on local storage. + """ + leap_assert(is_address(address), 'Not an user address: %s' % address) + doc = self._get_key_doc(address, private) + if doc is None: + raise errors.KeyNotFound(address) + return build_key_from_dict(OpenPGPKey, address, doc.content) + + def put_ascii_key(self, key_data): + """ + Put key contained in ascii-armored C{key_data} in local storage. + + :param key_data: The key data to be stored. + :type key_data: str + """ + leap_assert_type(key_data, str) + # TODO: add more checks for correct key data. + leap_assert(key_data is not None, 'Data does not represent a key.') + + def _put_ascii_key(gpg): + gpg.import_keys(key_data) + privkey = None + pubkey = None + + try: + privkey = gpg.list_keys(secret=True).pop() + except IndexError: + pass + pubkey = gpg.list_keys(secret=False).pop() # unitary keyring + # extract adress from first uid on key + match = re.match('.*<([\w.-]+@[\w.-]+)>.*', pubkey['uids'].pop()) + leap_assert(match is not None, 'No user address in key data.') + address = match.group(1) + if privkey is not None: + match = re.match( + '.*<([\w.-]+@[\w.-]+)>.*', privkey['uids'].pop()) + leap_assert(match is not None, 'No user address in key data.') + privaddress = match.group(1) + leap_assert( + address == privaddress, + 'Addresses in pub and priv key differ.') + leap_assert( + pubkey['fingerprint'] == privkey['fingerprint'], + 'Fingerprints for pub and priv key differ.') + # insert private key in storage + openpgp_privkey = _build_key_from_gpg( + address, privkey, + gpg.export_keys(privkey['fingerprint'], secret=True)) + self.put_key(openpgp_privkey) + # insert public key in storage + openpgp_pubkey = _build_key_from_gpg( + address, pubkey, + gpg.export_keys(pubkey['fingerprint'], secret=False)) + self.put_key(openpgp_pubkey) + + with temporary_gpgwrapper() as gpg: + # TODO: inspect result, or use decorator + _put_ascii_key(gpg) + + def put_key(self, key): + """ + Put C{key} in local storage. + + :param key: The key to be stored. + :type key: OpenPGPKey + """ + doc = self._get_key_doc(key.address, private=key.private) + if doc is None: + self._soledad.create_doc_from_json(key.get_json()) + else: + doc.set_json(key.get_json()) + self._soledad.put_doc(doc) + + def _get_key_doc(self, address, private=False): + """ + Get the document with a key (public, by default) bound to C{address}. + + If C{private} is True, looks for a private key instead of a public. + + :param address: The address bound to the key. + :type address: str + :param private: Whether to look for a private key. + :type private: bool + :return: The document with the key or None if it does not exist. + :rtype: leap.soledad.backends.leap_backend.LeapDocument + """ + doclist = self._soledad.get_from_index( + TAGS_ADDRESS_PRIVATE_INDEX, + KEYMANAGER_KEY_TAG, + address, + '1' if private else '0') + if len(doclist) is 0: + return None + leap_assert( + len(doclist) is 1, + 'Found more than one %s key for address!' % + 'private' if private else 'public') + return doclist.pop() + + def delete_key(self, key): + """ + Remove C{key} from storage. + + :param key: The key to be removed. + :type key: EncryptionKey + """ + leap_assert(key.__class__ is OpenPGPKey, 'Wrong key type.') + stored_key = self.get_key(key.address, private=key.private) + if stored_key is None: + raise errors.KeyNotFound(key) + if stored_key.__dict__ != key.__dict__: + raise errors.KeyAttributesDiffer(key) + doc = self._get_key_doc(key.address, key.private) + self._soledad.delete_doc(doc) diff --git a/src/leap/keymanager/tests/__init__.py b/src/leap/keymanager/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/keymanager/tests/test_keymanager.py b/src/leap/keymanager/tests/test_keymanager.py new file mode 100644 index 00000000..9b94d504 --- /dev/null +++ b/src/leap/keymanager/tests/test_keymanager.py @@ -0,0 +1,686 @@ +## -*- coding: utf-8 -*- +# test_keymanager.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +Tests for the Key Manager. +""" + + +from mock import Mock +try: + import simplejson as json +except ImportError: + import json # noqa + + +from leap.common.testing.basetest import BaseLeapTest +from leap.soledad import Soledad +#from leap.soledad.crypto import SoledadCrypto + +from leap.keymanager import ( + KeyManager, + openpgp, + KeyNotFound, + NoPasswordGiven, + #TAGS_INDEX, + #TAGS_AND_PRIVATE_INDEX, +) +from leap.keymanager.openpgp import OpenPGPKey +from leap.keymanager.keys import ( + is_address, + build_key_from_dict, +) +from leap.keymanager import errors + + +ADDRESS = 'leap@leap.se' +ADDRESS_2 = 'anotheruser@leap.se' + + +class KeyManagerUtilTestCase(BaseLeapTest): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_is_address(self): + self.assertTrue( + is_address('user@leap.se'), + 'Incorrect address detection.') + self.assertFalse( + is_address('userleap.se'), + 'Incorrect address detection.') + self.assertFalse( + is_address('user@'), + 'Incorrect address detection.') + self.assertFalse( + is_address('@leap.se'), + 'Incorrect address detection.') + + def test_build_key_from_dict(self): + kdict = { + 'address': ADDRESS, + 'key_id': 'key_id', + 'fingerprint': 'fingerprint', + 'key_data': 'key_data', + 'private': 'private', + 'length': 'length', + 'expiry_date': 'expiry_date', + 'first_seen_at': 'first_seen_at', + 'last_audited_at': 'last_audited_at', + 'validation': 'validation', + } + key = build_key_from_dict(OpenPGPKey, ADDRESS, kdict) + self.assertEqual( + kdict['address'], key.address, + 'Wrong data in key.') + self.assertEqual( + kdict['key_id'], key.key_id, + 'Wrong data in key.') + self.assertEqual( + kdict['fingerprint'], key.fingerprint, + 'Wrong data in key.') + self.assertEqual( + kdict['key_data'], key.key_data, + 'Wrong data in key.') + self.assertEqual( + kdict['private'], key.private, + 'Wrong data in key.') + self.assertEqual( + kdict['length'], key.length, + 'Wrong data in key.') + self.assertEqual( + kdict['expiry_date'], key.expiry_date, + 'Wrong data in key.') + self.assertEqual( + kdict['first_seen_at'], key.first_seen_at, + 'Wrong data in key.') + self.assertEqual( + kdict['last_audited_at'], key.last_audited_at, + 'Wrong data in key.') + self.assertEqual( + kdict['validation'], key.validation, + 'Wrong data in key.') + + +class KeyManagerWithSoledadTestCase(BaseLeapTest): + + def setUp(self): + # mock key fetching and storing so Soledad doesn't fail when trying to + # reach the server. + Soledad._get_secrets_from_shared_db = Mock(return_value=None) + Soledad._put_secrets_in_shared_db = Mock(return_value=None) + + self._soledad = Soledad( + "leap@leap.se", + "123456", + self.tempdir + "/secret.gpg", + self.tempdir + "/soledad.u1db", + '', + None, + auth_token=None, + ) + + def tearDown(self): + km = self._key_manager() + for key in km.get_all_keys_in_local_db(): + km._wrapper_map[key.__class__].delete_key(key) + for key in km.get_all_keys_in_local_db(private=True): + km._wrapper_map[key.__class__].delete_key(key) + + def _key_manager(self, user=ADDRESS, url=''): + return KeyManager(user, url, self._soledad) + + +class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): + + def _test_openpgp_gen_key(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + self.assertRaises(KeyNotFound, pgp.get_key, 'user@leap.se') + key = pgp.gen_key('user@leap.se') + self.assertIsInstance(key, openpgp.OpenPGPKey) + self.assertEqual( + 'user@leap.se', key.address, 'Wrong address bound to key.') + self.assertEqual( + '4096', key.length, 'Wrong key length.') + + def test_openpgp_put_delete_key(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) + pgp.put_ascii_key(PUBLIC_KEY) + key = pgp.get_key(ADDRESS, private=False) + pgp.delete_key(key) + self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) + + def test_openpgp_put_ascii_key(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) + pgp.put_ascii_key(PUBLIC_KEY) + key = pgp.get_key(ADDRESS, private=False) + self.assertIsInstance(key, openpgp.OpenPGPKey) + self.assertEqual( + ADDRESS, key.address, 'Wrong address bound to key.') + self.assertEqual( + '4096', key.length, 'Wrong key length.') + pgp.delete_key(key) + self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) + + def test_get_public_key(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) + pgp.put_ascii_key(PUBLIC_KEY) + self.assertRaises( + KeyNotFound, pgp.get_key, ADDRESS, private=True) + key = pgp.get_key(ADDRESS, private=False) + self.assertEqual(ADDRESS, key.address) + self.assertFalse(key.private) + self.assertEqual(KEY_FINGERPRINT, key.fingerprint) + pgp.delete_key(key) + self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) + + def test_openpgp_encrypt_decrypt_asym(self): + # encrypt + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PUBLIC_KEY) + pubkey = pgp.get_key(ADDRESS, private=False) + cyphertext = openpgp.encrypt_asym('data', pubkey) + # assert + self.assertTrue(cyphertext is not None) + self.assertTrue(cyphertext != '') + self.assertTrue(cyphertext != 'data') + self.assertTrue(openpgp.is_encrypted_asym(cyphertext)) + self.assertTrue(openpgp.is_encrypted(cyphertext)) + # decrypt + self.assertRaises( + KeyNotFound, pgp.get_key, ADDRESS, private=True) + pgp.put_ascii_key(PRIVATE_KEY) + privkey = pgp.get_key(ADDRESS, private=True) + plaintext = openpgp.decrypt_asym(cyphertext, privkey) + pgp.delete_key(pubkey) + pgp.delete_key(privkey) + self.assertRaises( + KeyNotFound, pgp.get_key, ADDRESS, private=False) + self.assertRaises( + KeyNotFound, pgp.get_key, ADDRESS, private=True) + + def test_verify_with_private_raises(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + data = 'data' + privkey = pgp.get_key(ADDRESS, private=True) + signed = openpgp.sign(data, privkey) + self.assertRaises( + AssertionError, + openpgp.verify, signed, privkey) + + def test_sign_with_public_raises(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PUBLIC_KEY) + data = 'data' + pubkey = pgp.get_key(ADDRESS, private=False) + self.assertRaises( + AssertionError, + openpgp.sign, data, pubkey) + + def test_verify_with_wrong_key_raises(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + data = 'data' + privkey = pgp.get_key(ADDRESS, private=True) + signed = openpgp.sign(data, privkey) + pgp.put_ascii_key(PUBLIC_KEY_2) + wrongkey = pgp.get_key(ADDRESS_2) + self.assertRaises( + errors.InvalidSignature, + openpgp.verify, signed, wrongkey) + + def test_encrypt_asym_sign_with_public_raises(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + data = 'data' + privkey = pgp.get_key(ADDRESS, private=True) + pubkey = pgp.get_key(ADDRESS, private=False) + self.assertRaises( + AssertionError, + openpgp.encrypt_asym, data, privkey, sign=pubkey) + + def test_decrypt_asym_verify_with_private_raises(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + data = 'data' + privkey = pgp.get_key(ADDRESS, private=True) + pubkey = pgp.get_key(ADDRESS, private=False) + encrypted_and_signed = openpgp.encrypt_asym( + data, pubkey, sign=privkey) + self.assertRaises( + AssertionError, + openpgp.decrypt_asym, + encrypted_and_signed, privkey, verify=privkey) + + def test_decrypt_asym_verify_with_wrong_key_raises(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + data = 'data' + privkey = pgp.get_key(ADDRESS, private=True) + pubkey = pgp.get_key(ADDRESS, private=False) + encrypted_and_signed = openpgp.encrypt_asym(data, pubkey, sign=privkey) + pgp.put_ascii_key(PUBLIC_KEY_2) + wrongkey = pgp.get_key(ADDRESS_2) + self.assertRaises( + errors.InvalidSignature, + openpgp.verify, encrypted_and_signed, wrongkey) + + def test_sign_verify(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + data = 'data' + privkey = pgp.get_key(ADDRESS, private=True) + signed = openpgp.sign(data, privkey) + pubkey = pgp.get_key(ADDRESS, private=False) + self.assertTrue(openpgp.verify(signed, pubkey)) + + def test_encrypt_asym_sign_decrypt_verify(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + pubkey = pgp.get_key(ADDRESS, private=False) + privkey = pgp.get_key(ADDRESS, private=True) + pgp.put_ascii_key(PRIVATE_KEY_2) + pubkey2 = pgp.get_key(ADDRESS_2, private=False) + privkey2 = pgp.get_key(ADDRESS_2, private=True) + data = 'data' + encrypted_and_signed = openpgp.encrypt_asym( + data, pubkey2, sign=privkey) + res = openpgp.decrypt_asym( + encrypted_and_signed, privkey2, verify=pubkey) + self.assertTrue(data, res) + + +class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): + + def test_get_all_keys_in_db(self): + km = self._key_manager() + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) + # get public keys + keys = km.get_all_keys_in_local_db(False) + self.assertEqual(len(keys), 1, 'Wrong number of keys') + self.assertEqual(ADDRESS, keys[0].address) + self.assertFalse(keys[0].private) + # get private keys + keys = km.get_all_keys_in_local_db(True) + self.assertEqual(len(keys), 1, 'Wrong number of keys') + self.assertEqual(ADDRESS, keys[0].address) + self.assertTrue(keys[0].private) + + def test_get_public_key(self): + km = self._key_manager() + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) + # get the key + key = km.get_key(ADDRESS, OpenPGPKey, private=False, + fetch_remote=False) + self.assertTrue(key is not None) + self.assertEqual(key.address, ADDRESS) + self.assertEqual( + key.fingerprint.lower(), KEY_FINGERPRINT.lower()) + self.assertFalse(key.private) + + def test_get_private_key(self): + km = self._key_manager() + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) + # get the key + key = km.get_key(ADDRESS, OpenPGPKey, private=True, + fetch_remote=False) + self.assertTrue(key is not None) + self.assertEqual(key.address, ADDRESS) + self.assertEqual( + key.fingerprint.lower(), KEY_FINGERPRINT.lower()) + self.assertTrue(key.private) + + def test_send_key_raises_key_not_found(self): + km = self._key_manager() + self.assertRaises( + KeyNotFound, + km.send_key, OpenPGPKey) + + def test_send_key(self): + """ + Test that request is well formed when sending keys to server. + """ + km = self._key_manager() + km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY) + km._fetcher.put = Mock() + # the following data will be used on the send + km.ca_cert_path = 'capath' + km.session_id = 'sessionid' + km.uid = 'myuid' + km.api_uri = 'apiuri' + km.api_version = 'apiver' + km.send_key(OpenPGPKey) + # setup expected args + data = { + km.PUBKEY_KEY: km.get_key(km._address, OpenPGPKey).key_data, + } + url = '%s/%s/users/%s.json' % ('apiuri', 'apiver', 'myuid') + km._fetcher.put.assert_called_once_with( + url, data=data, verify='capath', + cookies={'_session_id': 'sessionid'}, + ) + + def test__fetch_keys_from_server(self): + """ + Test that the request is well formed when fetching keys from server. + """ + km = self._key_manager(url='http://nickserver.domain') + + class Response(object): + status_code = 200 + headers = {'content-type': 'application/json'} + + def json(self): + return {'address': ADDRESS_2, 'openpgp': PUBLIC_KEY_2} + + def raise_for_status(self): + pass + + # mock the fetcher so it returns the key for ADDRESS_2 + km._fetcher.get = Mock( + return_value=Response()) + km.ca_cert_path = 'cacertpath' + # do the fetch + km._fetch_keys_from_server(ADDRESS_2) + # and verify the call + km._fetcher.get.assert_called_once_with( + 'http://nickserver.domain', + data={'address': ADDRESS_2}, + verify='cacertpath', + ) + + def test_refresh_keys_does_not_refresh_own_key(self): + """ + Test that refreshing keys will not attempt to refresh our own key. + """ + km = self._key_manager() + # we add 2 keys but we expect it to only refresh the second one. + km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY) + km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY_2) + # mock the key fetching + km._fetch_keys_from_server = Mock(return_value=[]) + km.ca_cert_path = '' # some bogus path so the km does not complain. + # do the refreshing + km.refresh_keys() + km._fetch_keys_from_server.assert_called_once_with( + ADDRESS_2 + ) + + def test_get_key_fetches_from_server(self): + """ + Test that getting a key successfuly fetches from server. + """ + km = self._key_manager(url='http://nickserver.domain') + + class Response(object): + status_code = 200 + headers = {'content-type': 'application/json'} + + def json(self): + return {'address': ADDRESS_2, 'openpgp': PUBLIC_KEY_2} + + def raise_for_status(self): + pass + + # mock the fetcher so it returns the key for ADDRESS_2 + km._fetcher.get = Mock(return_value=Response()) + km.ca_cert_path = 'cacertpath' + # try to key get without fetching from server + self.assertRaises( + KeyNotFound, km.get_key, ADDRESS_2, OpenPGPKey, + fetch_remote=False + ) + # try to get key fetching from server. + key = km.get_key(ADDRESS_2, OpenPGPKey) + self.assertIsInstance(key, OpenPGPKey) + self.assertEqual(ADDRESS_2, key.address) + + +# Key material for testing + +# key 24D18DDF: public key "Leap Test Key " +KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF" +PUBLIC_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +mQINBFC9+dkBEADNRfwV23TWEoGc/x0wWH1P7PlXt8MnC2Z1kKaKKmfnglVrpOiz +iLWoiU58sfZ0L5vHkzXHXCBf6Eiy/EtUIvdiWAn+yASJ1mk5jZTBKO/WMAHD8wTO +zpMsFmWyg3xc4DkmFa9KQ5EVU0o/nqPeyQxNMQN7px5pPwrJtJFmPxnxm+aDkPYx +irDmz/4DeDNqXliazGJKw7efqBdlwTHkl9Akw2gwy178pmsKwHHEMOBOFFvX61AT +huKqHYmlCGSliwbrJppTG7jc1/ls3itrK+CWTg4txREkSpEVmfcASvw/ZqLbjgfs +d/INMwXnR9U81O8+7LT6yw/ca4ppcFoJD7/XJbkRiML6+bJ4Dakiy6i727BzV17g +wI1zqNvm5rAhtALKfACha6YO43aJzairO4II1wxVHvRDHZn2IuKDDephQ3Ii7/vb +hUOf6XCSmchkAcpKXUOvbxm1yfB1LRa64mMc2RcZxf4mW7KQkulBsdV5QG2276lv +U2UUy2IutXcGP5nXC+f6sJJGJeEToKJ57yiO/VWJFjKN8SvP+7AYsQSqINUuEf6H +T5gCPCraGMkTUTPXrREvu7NOohU78q6zZNaL3GW8ai7eSeANSuQ8Vzffx7Wd8Y7i +Pw9sYj0SMFs1UgjbuL6pO5ueHh+qyumbtAq2K0Bci0kqOcU4E9fNtdiovQARAQAB +tBxMZWFwIFRlc3QgS2V5IDxsZWFwQGxlYXAuc2U+iQI3BBMBCAAhBQJQvfnZAhsD +BQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEC9FXigk0Y3fT7EQAKH3IuRniOpb +T/DDIgwwjz3oxB/W0DDMyPXowlhSOuM0rgGfntBpBb3boezEXwL86NPQxNGGruF5 +hkmecSiuPSvOmQlqlS95NGQp6hNG0YaKColh+Q5NTspFXCAkFch9oqUje0LdxfSP +QfV9UpeEvGyPmk1I9EJV/YDmZ4+Djge1d7qhVZInz4Rx1NrSyF/Tc2EC0VpjQFsU +Y9Kb2YBBR7ivG6DBc8ty0jJXi7B4WjkFcUEJviQpMF2dCLdonCehYs1PqsN1N7j+ +eFjQd+hqVMJgYuSGKjvuAEfClM6MQw7+FmFwMyLgK/Ew/DttHEDCri77SPSkOGSI +txCzhTg6798f6mJr7WcXmHX1w1Vcib5FfZ8vTDFVhz/XgAgArdhPo9V6/1dgSSiB +KPQ/spsco6u5imdOhckERE0lnAYvVT6KE81TKuhF/b23u7x+Wdew6kK0EQhYA7wy +7LmlaNXc7rMBQJ9Z60CJ4JDtatBWZ0kNrt2VfdDHVdqBTOpl0CraNUjWE5YMDasr +K2dF5IX8D3uuYtpZnxqg0KzyLg0tzL0tvOL1C2iudgZUISZNPKbS0z0v+afuAAnx +2pTC3uezbh2Jt8SWTLhll4i0P4Ps5kZ6HQUO56O+/Z1cWovX+mQekYFmERySDR9n +3k1uAwLilJmRmepGmvYbB8HloV8HqwgguQINBFC9+dkBEAC0I/xn1uborMgDvBtf +H0sEhwnXBC849/32zic6udB6/3Efk9nzbSpL3FSOuXITZsZgCHPkKarnoQ2ztMcS +sh1ke1C5gQGms75UVmM/nS+2YI4vY8OX/GC/on2vUyncqdH+bR6xH5hx4NbWpfTs +iQHmz5C6zzS/kuabGdZyKRaZHt23WQ7JX/4zpjqbC99DjHcP9BSk7tJ8wI4bkMYD +uFVQdT9O6HwyKGYwUU4sAQRAj7XCTGvVbT0dpgJwH4RmrEtJoHAx4Whg8mJ710E0 +GCmzf2jqkNuOw76ivgk27Kge+Hw00jmJjQhHY0yVbiaoJwcRrPKzaSjEVNgrpgP3 +lXPRGQArgESsIOTeVVHQ8fhK2YtTeCY9rIiO+L0OX2xo9HK7hfHZZWL6rqymXdyS +fhzh/f6IPyHFWnvj7Brl7DR8heMikygcJqv+ed2yx7iLyCUJ10g12I48+aEj1aLe +dP7lna32iY8/Z0SHQLNH6PXO9SlPcq2aFUgKqE75A/0FMk7CunzU1OWr2ZtTLNO1 +WT/13LfOhhuEq9jTyTosn0WxBjJKq18lnhzCXlaw6EAtbA7CUwsD3CTPR56aAXFK +3I7KXOVAqggrvMe5Tpdg5drfYpI8hZovL5aAgb+7Y5ta10TcJdUhS5K3kFAWe/td +U0cmWUMDP1UMSQ5Jg6JIQVWhSwARAQABiQIfBBgBCAAJBQJQvfnZAhsMAAoJEC9F +Xigk0Y3fRwsP/i0ElYCyxeLpWJTwo1iCLkMKz2yX1lFVa9nT1BVTPOQwr/IAc5OX +NdtbJ14fUsKL5pWgW8OmrXtwZm1y4euI1RPWWubG01ouzwnGzv26UcuHeqC5orZj +cOnKtL40y8VGMm8LoicVkRJH8blPORCnaLjdOtmA3rx/v2EXrJpSa3AhOy0ZSRXk +ZSrK68AVNwamHRoBSYyo0AtaXnkPX4+tmO8X8BPfj125IljubvwZPIW9VWR9UqCE +VPfDR1XKegVb6VStIywF7kmrknM1C5qUY28rdZYWgKorw01hBGV4jTW0cqde3N51 +XT1jnIAa+NoXUM9uQoGYMiwrL7vNsLlyyiW5ayDyV92H/rIuiqhFgbJsHTlsm7I8 +oGheR784BagAA1NIKD1qEO9T6Kz9lzlDaeWS5AUKeXrb7ZJLI1TTCIZx5/DxjLqM +Tt/RFBpVo9geZQrvLUqLAMwdaUvDXC2c6DaCPXTh65oCZj/hqzlJHH+RoTWWzKI+ +BjXxgUWF9EmZUBrg68DSmI+9wuDFsjZ51BcqvJwxyfxtTaWhdoYqH/UQS+D1FP3/ +diZHHlzwVwPICzM9ooNTgbrcDzyxRkIVqsVwBq7EtzcvgYUyX53yG25Giy6YQaQ2 +ZtQ/VymwFL3XdUWV6B/hU4PVAFvO3qlOtdJ6TpE+nEWgcWjCv5g7RjXX +=MuOY +-----END PGP PUBLIC KEY BLOCK----- +""" +PRIVATE_KEY = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +lQcYBFC9+dkBEADNRfwV23TWEoGc/x0wWH1P7PlXt8MnC2Z1kKaKKmfnglVrpOiz +iLWoiU58sfZ0L5vHkzXHXCBf6Eiy/EtUIvdiWAn+yASJ1mk5jZTBKO/WMAHD8wTO +zpMsFmWyg3xc4DkmFa9KQ5EVU0o/nqPeyQxNMQN7px5pPwrJtJFmPxnxm+aDkPYx +irDmz/4DeDNqXliazGJKw7efqBdlwTHkl9Akw2gwy178pmsKwHHEMOBOFFvX61AT +huKqHYmlCGSliwbrJppTG7jc1/ls3itrK+CWTg4txREkSpEVmfcASvw/ZqLbjgfs +d/INMwXnR9U81O8+7LT6yw/ca4ppcFoJD7/XJbkRiML6+bJ4Dakiy6i727BzV17g +wI1zqNvm5rAhtALKfACha6YO43aJzairO4II1wxVHvRDHZn2IuKDDephQ3Ii7/vb +hUOf6XCSmchkAcpKXUOvbxm1yfB1LRa64mMc2RcZxf4mW7KQkulBsdV5QG2276lv +U2UUy2IutXcGP5nXC+f6sJJGJeEToKJ57yiO/VWJFjKN8SvP+7AYsQSqINUuEf6H +T5gCPCraGMkTUTPXrREvu7NOohU78q6zZNaL3GW8ai7eSeANSuQ8Vzffx7Wd8Y7i +Pw9sYj0SMFs1UgjbuL6pO5ueHh+qyumbtAq2K0Bci0kqOcU4E9fNtdiovQARAQAB +AA/+JHtlL39G1wsH9R6UEfUQJGXR9MiIiwZoKcnRB2o8+DS+OLjg0JOh8XehtuCs +E/8oGQKtQqa5bEIstX7IZoYmYFiUQi9LOzIblmp2vxOm+HKkxa4JszWci2/ZmC3t +KtaA4adl9XVnshoQ7pijuCMUKB3naBEOAxd8s9d/JeReGIYkJErdrnVfNk5N71Ds +FmH5Ll3XtEDvgBUQP3nkA6QFjpsaB94FHjL3gDwum/cxzj6pCglcvHOzEhfY0Ddb +J967FozQTaf2JW3O+w3LOqtcKWpq87B7+O61tVidQPSSuzPjCtFF0D2LC9R/Hpky +KTMQ6CaKja4MPhjwywd4QPcHGYSqjMpflvJqi+kYIt8psUK/YswWjnr3r4fbuqVY +VhtiHvnBHQjz135lUqWvEz4hM3Xpnxydx7aRlv5NlevK8+YIO5oFbWbGNTWsPZI5 +jpoFBpSsnR1Q5tnvtNHauvoWV+XN2qAOBTG+/nEbDYH6Ak3aaE9jrpTdYh0CotYF +q7csANsDy3JvkAzeU6WnYpsHHaAjqOGyiZGsLej1UcXPFMosE/aUo4WQhiS8Zx2c +zOVKOi/X5vQ2GdNT9Qolz8AriwzsvFR+bxPzyd8V6ALwDsoXvwEYinYBKK8j0OPv +OOihSR6HVsuP9NUZNU9ewiGzte/+/r6pNXHvR7wTQ8EWLcEIAN6Zyrb0bHZTIlxt +VWur/Ht2mIZrBaO50qmM5RD3T5oXzWXi/pjLrIpBMfeZR9DWfwQwjYzwqi7pxtYx +nJvbMuY505rfnMoYxb4J+cpRXV8MS7Dr1vjjLVUC9KiwSbM3gg6emfd2yuA93ihv +Pe3mffzLIiQa4mRE3wtGcioC43nWuV2K2e1KjxeFg07JhrezA/1Cak505ab/tmvP +4YmjR5c44+yL/YcQ3HdFgs4mV+nVbptRXvRcPpolJsgxPccGNdvHhsoR4gwXMS3F +RRPD2z6x8xeN73Q4KH3bm01swQdwFBZbWVfmUGLxvN7leCdfs9+iFJyqHiCIB6Iv +mQfp8F0IAOwSo8JhWN+V1dwML4EkIrM8wUb4yecNLkyR6TpPH/qXx4PxVMC+vy6x +sCtjeHIwKE+9vqnlhd5zOYh7qYXEJtYwdeDDmDbL8oks1LFfd+FyAuZXY33DLwn0 +cRYsr2OEZmaajqUB3NVmj3H4uJBN9+paFHyFSXrH68K1Fk2o3n+RSf2EiX+eICwI +L6rqoF5sSVUghBWdNegV7qfy4anwTQwrIMGjgU5S6PKW0Dr/3iO5z3qQpGPAj5OW +ATqPWkDICLbObPxD5cJlyyNE2wCA9VVc6/1d6w4EVwSq9h3/WTpATEreXXxTGptd +LNiTA1nmakBYNO2Iyo3djhaqBdWjk+EIAKtVEnJH9FAVwWOvaj1RoZMA5DnDMo7e +SnhrCXl8AL7Z1WInEaybasTJXn1uQ8xY52Ua4b8cbuEKRKzw/70NesFRoMLYoHTO +dyeszvhoDHberpGRTciVmpMu7Hyi33rM31K9epA4ib6QbbCHnxkWOZB+Bhgj1hJ8 +xb4RBYWiWpAYcg0+DAC3w9gfxQhtUlZPIbmbrBmrVkO2GVGUj8kH6k4UV6kUHEGY +HQWQR0HcbKcXW81ZXCCD0l7ROuEWQtTe5Jw7dJ4/QFuqZnPutXVRNOZqpl6eRShw +7X2/a29VXBpmHA95a88rSQsL+qm7Fb3prqRmuMCtrUZgFz7HLSTuUMR867QcTGVh +cCBUZXN0IEtleSA8bGVhcEBsZWFwLnNlPokCNwQTAQgAIQUCUL352QIbAwULCQgH +AwUVCgkICwUWAgMBAAIeAQIXgAAKCRAvRV4oJNGN30+xEACh9yLkZ4jqW0/wwyIM +MI896MQf1tAwzMj16MJYUjrjNK4Bn57QaQW926HsxF8C/OjT0MTRhq7heYZJnnEo +rj0rzpkJapUveTRkKeoTRtGGigqJYfkOTU7KRVwgJBXIfaKlI3tC3cX0j0H1fVKX +hLxsj5pNSPRCVf2A5mePg44HtXe6oVWSJ8+EcdTa0shf03NhAtFaY0BbFGPSm9mA +QUe4rxugwXPLctIyV4uweFo5BXFBCb4kKTBdnQi3aJwnoWLNT6rDdTe4/nhY0Hfo +alTCYGLkhio77gBHwpTOjEMO/hZhcDMi4CvxMPw7bRxAwq4u+0j0pDhkiLcQs4U4 +Ou/fH+pia+1nF5h19cNVXIm+RX2fL0wxVYc/14AIAK3YT6PVev9XYEkogSj0P7Kb +HKOruYpnToXJBERNJZwGL1U+ihPNUyroRf29t7u8flnXsOpCtBEIWAO8Muy5pWjV +3O6zAUCfWetAieCQ7WrQVmdJDa7dlX3Qx1XagUzqZdAq2jVI1hOWDA2rKytnReSF +/A97rmLaWZ8aoNCs8i4NLcy9Lbzi9QtornYGVCEmTTym0tM9L/mn7gAJ8dqUwt7n +s24dibfElky4ZZeItD+D7OZGeh0FDuejvv2dXFqL1/pkHpGBZhEckg0fZ95NbgMC +4pSZkZnqRpr2GwfB5aFfB6sIIJ0HGARQvfnZARAAtCP8Z9bm6KzIA7wbXx9LBIcJ +1wQvOPf99s4nOrnQev9xH5PZ820qS9xUjrlyE2bGYAhz5Cmq56ENs7THErIdZHtQ +uYEBprO+VFZjP50vtmCOL2PDl/xgv6J9r1Mp3KnR/m0esR+YceDW1qX07IkB5s+Q +us80v5LmmxnWcikWmR7dt1kOyV/+M6Y6mwvfQ4x3D/QUpO7SfMCOG5DGA7hVUHU/ +Tuh8MihmMFFOLAEEQI+1wkxr1W09HaYCcB+EZqxLSaBwMeFoYPJie9dBNBgps39o +6pDbjsO+or4JNuyoHvh8NNI5iY0IR2NMlW4mqCcHEazys2koxFTYK6YD95Vz0RkA +K4BErCDk3lVR0PH4StmLU3gmPayIjvi9Dl9saPRyu4Xx2WVi+q6spl3ckn4c4f3+ +iD8hxVp74+wa5ew0fIXjIpMoHCar/nndsse4i8glCddINdiOPPmhI9Wi3nT+5Z2t +9omPP2dEh0CzR+j1zvUpT3KtmhVICqhO+QP9BTJOwrp81NTlq9mbUyzTtVk/9dy3 +zoYbhKvY08k6LJ9FsQYySqtfJZ4cwl5WsOhALWwOwlMLA9wkz0eemgFxStyOylzl +QKoIK7zHuU6XYOXa32KSPIWaLy+WgIG/u2ObWtdE3CXVIUuSt5BQFnv7XVNHJllD +Az9VDEkOSYOiSEFVoUsAEQEAAQAP/1AagnZQZyzHDEgw4QELAspYHCWLXE5aZInX +wTUJhK31IgIXNn9bJ0hFiSpQR2xeMs9oYtRuPOu0P8oOFMn4/z374fkjZy8QVY3e +PlL+3EUeqYtkMwlGNmVw5a/NbNuNfm5Darb7pEfbYd1gPcni4MAYw7R2SG/57GbC +9gucvspHIfOSfBNLBthDzmK8xEKe1yD2eimfc2T7IRYb6hmkYfeds5GsqvGI6mwI +85h4uUHWRc5JOlhVM6yX8hSWx0L60Z3DZLChmc8maWnFXd7C8eQ6P1azJJbW71Ih +7CoK0XW4LE82vlQurSRFgTwfl7wFYszW2bOzCuhHDDtYnwH86Nsu0DC78ZVRnvxn +E8Ke/AJgrdhIOo4UAyR+aZD2+2mKd7/waOUTUrUtTzc7i8N3YXGi/EIaNReBXaq+ +ZNOp24BlFzRp+FCF/pptDW9HjPdiV09x0DgICmeZS4Gq/4vFFIahWctg52NGebT0 +Idxngjj+xDtLaZlLQoOz0n5ByjO/Wi0ANmMv1sMKCHhGvdaSws2/PbMR2r4caj8m +KXpIgdinM/wUzHJ5pZyF2U/qejsRj8Kw8KH/tfX4JCLhiaP/mgeTuWGDHeZQERAT +xPmRFHaLP9/ZhvGNh6okIYtrKjWTLGoXvKLHcrKNisBLSq+P2WeFrlme1vjvJMo/ +jPwLT5o9CADQmcbKZ+QQ1ZM9v99iDZol7SAMZX43JC019sx6GK0u6xouJBcLfeB4 +OXacTgmSYdTa9RM9fbfVpti01tJ84LV2SyL/VJq/enJF4XQPSynT/tFTn1PAor6o +tEAAd8fjKdJ6LnD5wb92SPHfQfXqI84rFEO8rUNIE/1ErT6DYifDzVCbfD2KZdoF +cOSp7TpD77sY1bs74ocBX5ejKtd+aH99D78bJSMM4pSDZsIEwnomkBHTziubPwJb +OwnATy0LmSMAWOw5rKbsh5nfwCiUTM20xp0t5JeXd+wPVWbpWqI2EnkCEN+RJr9i +7dp/ymDQ+Yt5wrsN3NwoyiexPOG91WQVCADdErHsnglVZZq9Z8Wx7KwecGCUurJ2 +H6lKudv5YOxPnAzqZS5HbpZd/nRTMZh2rdXCr5m2YOuewyYjvM757AkmUpM09zJX +MQ1S67/UX2y8/74TcRF97Ncx9HeELs92innBRXoFitnNguvcO6Esx4BTe1OdU6qR +ER3zAmVf22Le9ciXbu24DN4mleOH+OmBx7X2PqJSYW9GAMTsRB081R6EWKH7romQ +waxFrZ4DJzZ9ltyosEJn5F32StyLrFxpcrdLUoEaclZCv2qka7sZvi0EvovDVEBU +e10jOx9AOwf8Gj2ufhquQ6qgVYCzbP+YrodtkFrXRS3IsljIchj1M2ffB/0bfoUs +rtER9pLvYzCjBPg8IfGLw0o754Qbhh/ReplCRTusP/fQMybvCvfxreS3oyEriu/G +GufRomjewZ8EMHDIgUsLcYo2UHZsfF7tcazgxMGmMvazp4r8vpgrvW/8fIN/6Adu +tF+WjWDTvJLFJCe6O+BFJOWrssNrrra1zGtLC1s8s+Wfpe+bGPL5zpHeebGTwH1U +22eqgJArlEKxrfarz7W5+uHZJHSjF/K9ZvunLGD0n9GOPMpji3UO3zeM8IYoWn7E +/EWK1XbjnssNemeeTZ+sDh+qrD7BOi+vCX1IyBxbfqnQfJZvmcPWpruy1UsO+aIC +0GY8Jr3OL69dDQ21jueJAh8EGAEIAAkFAlC9+dkCGwwACgkQL0VeKCTRjd9HCw/+ +LQSVgLLF4ulYlPCjWIIuQwrPbJfWUVVr2dPUFVM85DCv8gBzk5c121snXh9Swovm +laBbw6ate3BmbXLh64jVE9Za5sbTWi7PCcbO/bpRy4d6oLmitmNw6cq0vjTLxUYy +bwuiJxWREkfxuU85EKdouN062YDevH+/YResmlJrcCE7LRlJFeRlKsrrwBU3BqYd +GgFJjKjQC1peeQ9fj62Y7xfwE9+PXbkiWO5u/Bk8hb1VZH1SoIRU98NHVcp6BVvp +VK0jLAXuSauSczULmpRjbyt1lhaAqivDTWEEZXiNNbRyp17c3nVdPWOcgBr42hdQ +z25CgZgyLCsvu82wuXLKJblrIPJX3Yf+si6KqEWBsmwdOWybsjygaF5HvzgFqAAD +U0goPWoQ71PorP2XOUNp5ZLkBQp5etvtkksjVNMIhnHn8PGMuoxO39EUGlWj2B5l +Cu8tSosAzB1pS8NcLZzoNoI9dOHrmgJmP+GrOUkcf5GhNZbMoj4GNfGBRYX0SZlQ +GuDrwNKYj73C4MWyNnnUFyq8nDHJ/G1NpaF2hiof9RBL4PUU/f92JkceXPBXA8gL +Mz2ig1OButwPPLFGQhWqxXAGrsS3Ny+BhTJfnfIbbkaLLphBpDZm1D9XKbAUvdd1 +RZXoH+FTg9UAW87eqU610npOkT6cRaBxaMK/mDtGNdc= +=JTFu +-----END PGP PRIVATE KEY BLOCK----- +""" + +# key 7FEE575A: public key "anotheruser " +PUBLIC_KEY_2 = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +mI0EUYwJXgEEAMbTKHuPJ5/Gk34l9Z06f+0WCXTDXdte1UBoDtZ1erAbudgC4MOR +gquKqoj3Hhw0/ILqJ88GcOJmKK/bEoIAuKaqlzDF7UAYpOsPZZYmtRfPC2pTCnXq +Z1vdeqLwTbUspqXflkCkFtfhGKMq5rH8GV5a3tXZkRWZhdNwhVXZagC3ABEBAAG0 +IWFub3RoZXJ1c2VyIDxhbm90aGVydXNlckBsZWFwLnNlPoi4BBMBAgAiBQJRjAle +AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRB/nfpof+5XWotuA/4tLN4E +gUr7IfLy2HkHAxzw7A4rqfMN92DIM9mZrDGaWRrOn3aVF7VU1UG7MDkHfPvp/cFw +ezoCw4s4IoHVc/pVlOkcHSyt4/Rfh248tYEJmFCJXGHpkK83VIKYJAithNccJ6Q4 +JE/o06Mtf4uh/cA1HUL4a4ceqUhtpLJULLeKo7iNBFGMCV4BBADsyQI7GR0wSAxz +VayLjuPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQt +Z/hwcLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63 +yuRe94WenT1RJd6xU1aaUff4rKizuQARAQABiJ8EGAECAAkFAlGMCV4CGwwACgkQ +f536aH/uV1rPZQQAqCzRysOlu8ez7PuiBD4SebgRqWlxa1TF1ujzfLmuPivROZ2X +Kw5aQstxgGSjoB7tac49s0huh4X8XK+BtJBfU84JS8Jc2satlfwoyZ35LH6sDZck +I+RS/3we6zpMfHs3vvp9xgca6ZupQxivGtxlJs294TpJorx+mFFqbV17AzQ= +=Thdu +-----END PGP PUBLIC KEY BLOCK----- +""" + +PRIVATE_KEY_2 = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +lQHYBFGMCV4BBADG0yh7jyefxpN+JfWdOn/tFgl0w13bXtVAaA7WdXqwG7nYAuDD +kYKriqqI9x4cNPyC6ifPBnDiZiiv2xKCALimqpcwxe1AGKTrD2WWJrUXzwtqUwp1 +6mdb3Xqi8E21LKal35ZApBbX4RijKuax/BleWt7V2ZEVmYXTcIVV2WoAtwARAQAB +AAP7BLuSAx7tOohnimEs74ks8l/L6dOcsFQZj2bqs4AoY3jFe7bV0tHr4llypb/8 +H3/DYvpf6DWnCjyUS1tTnXSW8JXtx01BUKaAufSmMNg9blKV6GGHlT/Whe9uVyks +7XHk/+9mebVMNJ/kNlqq2k+uWqJohzC8WWLRK+d1tBeqDsECANZmzltPaqUsGV5X +C3zszE3tUBgptV/mKnBtopKi+VH+t7K6fudGcG+bAcZDUoH/QVde52mIIjjIdLje +uajJuHUCAO1mqh+vPoGv4eBLV7iBo3XrunyGXiys4a39eomhxTy3YktQanjjx+ty +GltAGCs5PbWGO6/IRjjvd46wh53kzvsCAO0J97gsWhzLuFnkxFAJSPk7RRlyl7lI +1XS/x0Og6j9XHCyY1OYkfBm0to3UlCfkgirzCYlTYObCofzdKFIPDmSqHbQhYW5v +dGhlcnVzZXIgPGFub3RoZXJ1c2VyQGxlYXAuc2U+iLgEEwECACIFAlGMCV4CGwMG +CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEH+d+mh/7ldai24D/i0s3gSBSvsh +8vLYeQcDHPDsDiup8w33YMgz2ZmsMZpZGs6fdpUXtVTVQbswOQd8++n9wXB7OgLD +izgigdVz+lWU6RwdLK3j9F+Hbjy1gQmYUIlcYemQrzdUgpgkCK2E1xwnpDgkT+jT +oy1/i6H9wDUdQvhrhx6pSG2kslQst4qjnQHYBFGMCV4BBADsyQI7GR0wSAxzVayL +juPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQtZ/hw +cLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63yuRe +94WenT1RJd6xU1aaUff4rKizuQARAQABAAP9EyElqJ3dq3EErXwwT4mMnbd1SrVC +rUJrNWQZL59mm5oigS00uIyR0SvusOr+UzTtd8ysRuwHy5d/LAZsbjQStaOMBILx +77TJveOel0a1QK0YSMF2ywZMCKvquvjli4hAtWYz/EwfuzQN3t23jc5ny+GqmqD2 +3FUxLJosFUfLNmECAO9KhVmJi+L9dswIs+2Dkjd1eiRQzNOEVffvYkGYZyKxNiXF +UA5kvyZcB4iAN9sWCybE4WHZ9jd4myGB0MPDGxkCAP1RsXJbbuD6zS7BXe5gwunO +2q4q7ptdSl/sJYQuTe1KNP5d/uGsvlcFfsYjpsopasPjFBIncc/2QThMKlhoEaEB +/0mVAxpT6SrEvUbJ18z7kna24SgMPr3OnPMxPGfvNLJY/Xv/A17YfoqjmByCvsKE +JCDjopXtmbcrZyoEZbEht9mko4ifBBgBAgAJBQJRjAleAhsMAAoJEH+d+mh/7lda +z2UEAKgs0crDpbvHs+z7ogQ+Enm4EalpcWtUxdbo83y5rj4r0TmdlysOWkLLcYBk +o6Ae7WnOPbNIboeF/FyvgbSQX1POCUvCXNrGrZX8KMmd+Sx+rA2XJCPkUv98Hus6 +THx7N776fcYHGumbqUMYrxrcZSbNveE6SaK8fphRam1dewM0 +=a5gs +-----END PGP PRIVATE KEY BLOCK----- +""" +import unittest +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/services/soledad/soledadbootstrapper.py b/src/leap/services/soledad/soledadbootstrapper.py index bae933de..2869e098 100644 --- a/src/leap/services/soledad/soledadbootstrapper.py +++ b/src/leap/services/soledad/soledadbootstrapper.py @@ -29,8 +29,8 @@ from mock import Mock from leap.common.check import leap_assert, leap_assert_type from leap.common.files import get_mtime -from leap.common.keymanager import KeyManager, openpgp -from leap.common.keymanager.errors import KeyNotFound +from leap.keymanager import KeyManager, openpgp +from leap.keymanager.errors import KeyNotFound from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth from leap.services.soledad.soledadconfig import SoledadConfig -- cgit v1.2.3 From 92d43151bad2f2ac6e292d725555249082462f81 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 27 May 2013 15:05:08 -0300 Subject: Add logging handler with history and signal emision --- src/leap/util/leap_log_handler.py | 99 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/leap/util/leap_log_handler.py (limited to 'src') diff --git a/src/leap/util/leap_log_handler.py b/src/leap/util/leap_log_handler.py new file mode 100644 index 00000000..0e598032 --- /dev/null +++ b/src/leap/util/leap_log_handler.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# leap_log_handler.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Custom handler for the logger window. +""" +import logging + +from PySide import QtCore + + +class LeapLogHandler(logging.Handler, QtCore.QObject): + """ + Custom logging handler. It emits Qt signals so it can be plugged to a gui. + Also stores an history of logs that can be fetched after connect to a gui. + """ + # All dicts returned are of the form + # {'record': LogRecord, 'message': str} + new_log = QtCore.Signal(dict) + + MESSAGE_KEY = 'message' + RECORD_KEY = 'record' + + def __init__(self): + logging.Handler.__init__(self) + QtCore.QObject.__init__(self) + + self._log_history = [] + + def _set_format(self, logging_level): + """ + Sets the log format depending on the parameter. + It uses html and css to set the colors for the logs. + + :param logging_level: the debug level to define the color. + :type logging_level: str. + """ + html_style = { + 'DEBUG': "color: blue", + 'INFO': "color: black", + 'WARNING': "color: black; background: yellow;", + 'ERROR': "color: red", + 'CRITICAL': "color: red; font-weight: bold;" + } + + style_open = "" + style_close = "" + time = "%(asctime)s" + name = style_open + "%(name)s" + level = "%(levelname)s" + message = "%(message)s" + style_close + format_attrs = [time, name, level, message] + log_format = ' - '.join(format_attrs) + formatter = logging.Formatter(log_format) + self.setFormatter(formatter) + + def emit(self, logRecord): + """ + This method is fired every time that a record is logged by the + logging module. + This method reimplements logging.Handler.emit that is fired + in every logged message. + QObject.emit gets in the way on the PySide signal model but we + workarouded that issue. + + :param logRecord: the record emitted by the logging module. + :type logRecord: logging.LogRecord. + """ + self._set_format(logRecord.levelname) + log = self.format(logRecord) + log_item = {self.RECORD_KEY: logRecord, self.MESSAGE_KEY: log} + self._log_history.append(log_item) + + # WARNING: the new-style connection does NOT work because PySide + # translates the emit method to self.emit, and that collides with + # the emit method for logging.Handler + # self.new_log.emit(log_item) + QtCore.QObject.emit(self, QtCore.SIGNAL('new_log(PyObject)'), log_item) + + @property + def log_history(self): + """ + Returns the history of the logged messages. + """ + return self._log_history -- cgit v1.2.3 From ef00c4ec75446c60dea8f790ae86109e0d8b6ca3 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 27 May 2013 15:08:44 -0300 Subject: Add logger window connected with the leap_handler --- src/leap/gui/loggerwindow.py | 124 ++++++++++++++++++++++++++++++++ src/leap/gui/ui/loggerwindow.ui | 155 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 src/leap/gui/loggerwindow.py create mode 100644 src/leap/gui/ui/loggerwindow.ui (limited to 'src') diff --git a/src/leap/gui/loggerwindow.py b/src/leap/gui/loggerwindow.py new file mode 100644 index 00000000..dd724ac7 --- /dev/null +++ b/src/leap/gui/loggerwindow.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# loggerwindow.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +History log window +""" +import logging + +from PySide import QtGui +from ui_loggerwindow import Ui_LoggerWindow +from leap.common.check import leap_assert, leap_assert_type +from leap.util.leap_log_handler import LeapLogHandler + +logger = logging.getLogger(__name__) + + +class LoggerWindow(QtGui.QWidget): + """ + Window that displays a history of the logged messages in the app. + """ + def __init__(self, handler): + """ + Initialize the widget with the custom handler. + + :param handler: Custom handler that supports history and signal. + :type handler: LeapLogHandler. + """ + QtGui.QWidget.__init__(self) + leap_assert(handler, "We need a handler for the logger window") + leap_assert_type(handler, LeapLogHandler) + + # Load UI + self.ui = Ui_LoggerWindow() + self.ui.setupUi(self) + + # Make connections + self.ui.btnSave.clicked.connect(self._save_log_to_file) + self.ui.btnDebug.toggled.connect(self._load_history), + self.ui.btnInfo.toggled.connect(self._load_history), + self.ui.btnWarning.toggled.connect(self._load_history), + self.ui.btnError.toggled.connect(self._load_history), + self.ui.btnCritical.toggled.connect(self._load_history) + + # Load logging history and connect logger with the widget + self._logging_handler = handler + self._connect_to_handler() + self._load_history() + + def _connect_to_handler(self): + """ + This method connects the loggerwindow with the handler through a + signal communicate the logger events. + """ + self._logging_handler.new_log.connect(self._add_log_line) + + def _add_log_line(self, log): + """ + Adds a line to the history, only if it's in the desired levels to show. + + :param log: a log record to be inserted in the widget + :type log: a dict with RECORD_KEY and MESSAGE_KEY. + the record contains the LogRecord of the logging module, + the message contains the formatted message for the log. + """ + level = log[LeapLogHandler.RECORD_KEY].levelname + message = log[LeapLogHandler.MESSAGE_KEY] + + if self._logs_to_display[level]: + self.ui.txtLogHistory.append(message) + + def _load_history(self): + """ + Load the previous logged messages in the widget. + They are stored in the custom handler. + """ + self._set_logs_to_display() + self.ui.txtLogHistory.clear() + history = self._logging_handler.log_history + for line in history: + self._add_log_line(line) + + def _set_logs_to_display(self): + """ + Sets the logs_to_display dict getting the toggled options from the ui + """ + self._logs_to_display = { + 'DEBUG': self.ui.btnDebug.isChecked(), + 'INFO': self.ui.btnInfo.isChecked(), + 'WARNING': self.ui.btnWarning.isChecked(), + 'ERROR': self.ui.btnError.isChecked(), + 'CRITICAL': self.ui.btnCritical.isChecked() + } + + def _save_log_to_file(self): + """ + Lets the user save the current log to a file + """ + fileName, filtr = QtGui.QFileDialog.getSaveFileName( + self, self.tr("Save As")) + + if fileName: + try: + with open(fileName, 'w') as output: + output.write(self.ui.txtLogHistory.toPlainText()) + output.write('\n') + logger.debug('Log saved in %s' % (fileName, )) + except IOError, e: + logger.error("Error saving log file: %r" % (e, )) + else: + logger.debug('Log not saved!') diff --git a/src/leap/gui/ui/loggerwindow.ui b/src/leap/gui/ui/loggerwindow.ui new file mode 100644 index 00000000..28325cdf --- /dev/null +++ b/src/leap/gui/ui/loggerwindow.ui @@ -0,0 +1,155 @@ + + + LoggerWindow + + + + 0 + 0 + 648 + 469 + + + + Logs + + + + :/images/leap-color-big.png:/images/leap-color-big.png + + + + + + + + + + + Debug + + + + :/images/oxygen-icons/script-error.png:/images/oxygen-icons/script-error.png + + + true + + + true + + + true + + + + + + + Info + + + + :/images/oxygen-icons/dialog-information.png:/images/oxygen-icons/dialog-information.png + + + true + + + true + + + true + + + + + + + Warning + + + + :/images/oxygen-icons/dialog-warning.png:/images/oxygen-icons/dialog-warning.png + + + true + + + true + + + true + + + + + + + Error + + + + :/images/oxygen-icons/dialog-error.png:/images/oxygen-icons/dialog-error.png + + + true + + + true + + + true + + + + + + + Critical + + + + :/images/oxygen-icons/edit-bomb.png:/images/oxygen-icons/edit-bomb.png + + + true + + + true + + + true + + + + + + + Save to file + + + + :/images/oxygen-icons/document-save-as.png:/images/oxygen-icons/document-save-as.png + + + + + + + + + btnDebug + btnInfo + btnWarning + btnError + btnCritical + btnSave + txtLogHistory + + + + + + + -- cgit v1.2.3 From c928400ff02adf6c23c5341c2139e95f2c49976d Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 27 May 2013 15:10:49 -0300 Subject: Integrate LoggerWindow with client --- src/leap/app.py | 15 ++++++++++++--- src/leap/gui/mainwindow.py | 37 +++++++++++++++++++++++++++++++++++++ src/leap/gui/ui/mainwindow.ui | 10 ++++++++-- 3 files changed, 57 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/leap/app.py b/src/leap/app.py index 797cea8a..03552edb 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -26,6 +26,7 @@ from PySide import QtCore, QtGui from leap.common.events import server as event_server from leap.util import __version__ as VERSION from leap.util import leap_argparse +from leap.util.leap_log_handler import LeapLogHandler from leap.gui import locale_rc from leap.gui import twisted_main from leap.gui.mainwindow import MainWindow @@ -33,6 +34,7 @@ from leap.platform_init import IS_MAC from leap.platform_init.locks import we_are_the_one_and_only from leap.services.tx import leap_services + import codecs codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None) @@ -75,16 +77,23 @@ def main(): else: level = logging.WARNING + # Console logger logger = logging.getLogger(name='leap') logger.setLevel(level) console = logging.StreamHandler() console.setLevel(level) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') + log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + formatter = logging.Formatter(log_format) console.setFormatter(formatter) logger.addHandler(console) + # LEAP custom handler + leap_handler = LeapLogHandler() + leap_handler.setLevel(level) + logger.addHandler(leap_handler) + + logger.debug('Leap handler plugged!') + if not we_are_the_one_and_only(): # leap-client is already running logger.warning("Tried to launch more than one instance " diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index a8aa1b1c..12187f51 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -35,6 +35,7 @@ from leap.common.events import events_pb2 as proto from leap.config.leapsettings import LeapSettings from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth +from leap.gui.loggerwindow import LoggerWindow from leap.gui.wizard import Wizard from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig @@ -208,6 +209,7 @@ class MainWindow(QtGui.QMainWindow): self.ui.action_about_leap.triggered.connect(self._about) self.ui.action_quit.triggered.connect(self.quit) self.ui.action_wizard.triggered.connect(self._launch_wizard) + self.ui.action_show_logs.triggered.connect(self._show_logger_window) self.raise_window.connect(self._do_raise_mainwindow) # Used to differentiate between real quits and close to tray @@ -249,6 +251,8 @@ class MainWindow(QtGui.QMainWindow): self._wizard = None self._wizard_firstrun = False + self._logger_window = None + self._bypass_checks = bypass_checks self._soledad = None @@ -282,6 +286,35 @@ class MainWindow(QtGui.QMainWindow): self._wizard.exec_() self._wizard = None + def _get_leap_logging_handler(self): + """ + Gets the leap handler from the top level logger + + :return: a logging handler or None + :rtype: LeapLogHandler or None + """ + from leap.util.leap_log_handler import LeapLogHandler + leap_logger = logging.getLogger('leap') + for h in leap_logger.handlers: + if isinstance(h, LeapLogHandler): + return h + return None + + def _show_logger_window(self): + """ + Displays the window with the history of messages logged until now + and displays the new ones on arrival. + """ + if self._logger_window is None: + leap_log_handler = self._get_leap_logging_handler() + if leap_log_handler is None: + logger.error('Leap logger handler not found') + else: + self._logger_window = LoggerWindow(handler=leap_log_handler) + self._logger_window.show() + else: + self._logger_window.show() + def _remember_state_changed(self, state): enable = True if state == QtCore.Qt.Checked else False self.ui.chkAutoLogin.setEnabled(enable) @@ -1101,6 +1134,10 @@ class MainWindow(QtGui.QMainWindow): self._really_quit = True if self._wizard: self._wizard.close() + + if self._logger_window: + self._logger_window.close() + self.close() if self._quit_callback: diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui index b42ff180..fdf5c167 100644 --- a/src/leap/gui/ui/mainwindow.ui +++ b/src/leap/gui/ui/mainwindow.ui @@ -396,7 +396,7 @@ 0 0 415 - 25 + 21 @@ -417,9 +417,10 @@ - S&ettings + &Utils + @@ -451,6 +452,11 @@ &Wizard + + + Show &logs + + lnUser -- cgit v1.2.3 From c4ba840c7c0df81fa928bab2edd1f2a2e3379952 Mon Sep 17 00:00:00 2001 From: kali Date: Mon, 27 May 2013 21:42:10 +0900 Subject: adapt test suite to latest client code * fix run_tests * add tox.ini * cleanup/update requirements * fix coverage reports * add pypi badge --- src/leap/gui/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) (limited to 'src') diff --git a/src/leap/gui/__init__.py b/src/leap/gui/__init__.py index e69de29b..d31dac64 100644 --- a/src/leap/gui/__init__.py +++ b/src/leap/gui/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# __init__.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +init file for leap.gui +""" +from .. import app +__all__ = [app] -- cgit v1.2.3 From 884d0e0f4dbba34b6f6f5afe6e27390a7606a7fa Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 29 May 2013 04:02:43 +0900 Subject: make tests pass & fix pep8 --- src/leap/crypto/srpauth.py | 9 ++++++++- src/leap/services/eip/eipspec.py | 4 ++-- src/leap/services/eip/providerbootstrapper.py | 10 +++++++--- src/leap/services/eip/tests/test_eipconfig.py | 25 +++++++++++++----------- src/leap/services/mail/smtpbootstrapper.py | 8 ++++---- src/leap/services/mail/smtpconfig.py | 1 - src/leap/services/soledad/soledadbootstrapper.py | 8 +++++--- src/leap/services/soledad/soledadconfig.py | 1 - 8 files changed, 40 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index 2f3cbd1c..f1897e1d 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -22,6 +22,9 @@ import requests import srp import json +#this error is raised from requests +from simplejson.decoder import JSONDecodeError + from PySide import QtCore, QtGui from leap.common.check import leap_assert @@ -232,7 +235,10 @@ class SRPAuth(QtCore.QObject): raise SRPAuthenticationError(self.tr("Could not connect to " "the server")) - content, mtime = get_content(auth_result) + try: + content, mtime = get_content(auth_result) + except JSONDecodeError: + raise SRPAuthenticationError("Bad JSON content in auth result") if auth_result.status_code == 422: logger.error("[%s] Wrong password (HAMK): [%s]" % @@ -319,6 +325,7 @@ class SRPAuth(QtCore.QObject): self._authentication_preprocessing(username, password) salt, B = self._start_authentication(username, password) M2 = self._process_challenge(salt, B, username) + self._verify_session(M2) leap_assert(self.get_session_id(), "Something went wrong because" diff --git a/src/leap/services/eip/eipspec.py b/src/leap/services/eip/eipspec.py index 7fa782c7..94ba674f 100644 --- a/src/leap/services/eip/eipspec.py +++ b/src/leap/services/eip/eipspec.py @@ -22,12 +22,12 @@ eipservice_config_spec = { 'serial': { 'type': int, 'default': 1, - 'required': True + 'required': ["True"] }, 'version': { 'type': int, 'default': 1, - 'required': True + 'required': ["True"] }, 'clusters': { 'type': list, diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 734d3867..289d212b 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -209,13 +209,16 @@ class ProviderBootstrapper(QtCore.QObject): def run_provider_select_checks(self, checker, domain, download_if_needed=False): """ - Populates the check queue + Populates the check queue. :param checker: checker thread to be used to run this check :type checker: CheckerThread + :param domain: domain to check :type domain: str - :param download_if_needed: if True, makes the checks do not overwrite already downloaded data + + :param download_if_needed: if True, makes the checks do not + overwrite already downloaded data :type download_if_needed: bool :return: True if the checks passed, False otherwise @@ -407,7 +410,8 @@ class ProviderBootstrapper(QtCore.QObject): :param provider_config: Provider configuration :type provider_config: ProviderConfig - :param download_if_needed: if True, makes the checks do not overwrite already downloaded data. + :param download_if_needed: if True, makes the checks do not + overwrite already downloaded data. :type download_if_needed: bool """ leap_assert(provider_config, "We need a provider config!") diff --git a/src/leap/services/eip/tests/test_eipconfig.py b/src/leap/services/eip/tests/test_eipconfig.py index ce04c2fc..0bd19d5e 100644 --- a/src/leap/services/eip/tests/test_eipconfig.py +++ b/src/leap/services/eip/tests/test_eipconfig.py @@ -97,10 +97,12 @@ class EIPConfigTest(BaseLeapTest): """ self.write_config(sample_config) config = EIPConfig() - self.assertRaises( - AssertionError, - config.get_clusters) - self.assertTrue(config.load(self.configfile)) + #self.assertRaises( + #AssertionError, + #config.get_clusters) + + self.assertTrue(config.load( + self.configfile, relative=False)) self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) @@ -123,7 +125,8 @@ class EIPConfigTest(BaseLeapTest): data['openvpn_configuration']["extra_param"] = "FOO" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load( + self.configfile, relative=False) self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) @@ -133,7 +136,7 @@ class EIPConfigTest(BaseLeapTest): data['openvpn_configuration']["auth"] = "SHA1;" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load(self.configfile, relative=False) self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) @@ -143,7 +146,7 @@ class EIPConfigTest(BaseLeapTest): data['openvpn_configuration']["auth"] = "SHA1>`&|" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load(self.configfile, relative=False) self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) @@ -153,7 +156,7 @@ class EIPConfigTest(BaseLeapTest): data['openvpn_configuration']["auth"] = "shaSHA1" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load(self.configfile, relative=False) self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) @@ -163,7 +166,7 @@ class EIPConfigTest(BaseLeapTest): data['openvpn_configuration']["auth"] = "sha&*!@#;" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load(self.configfile, relative=False) self.assertEqual( config.get_openvpn_configuration(), {'cipher': 'AES-128-CBC', @@ -174,7 +177,7 @@ class EIPConfigTest(BaseLeapTest): data['gateways'][0]["ip_address"] = "11.22.33.44;" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load(self.configfile, relative=False) self.assertEqual( config.get_gateway_ip(), None) @@ -183,7 +186,7 @@ class EIPConfigTest(BaseLeapTest): data['gateways'][0]["ip_address"] = "11.22.33.44`" self.write_config(data) config = EIPConfig() - config.load(self.configfile) + config.load(self.configfile, relative=False) self.assertEqual( config.get_gateway_ip(), None) diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py index 7e0f10de..6e0a0a47 100644 --- a/src/leap/services/mail/smtpbootstrapper.py +++ b/src/leap/services/mail/smtpbootstrapper.py @@ -116,10 +116,10 @@ class SMTPBootstrapper(QtCore.QObject): # Not modified if res.status_code == 304: logger.debug("SMTP definition has not been modified") - self._smtp_config.load(os.path.join("leap", - "providers", - self._provider_config.get_domain(), - "smtp-service.json")) + self._smtp_config.load(os.path.join( + "leap", "providers", + self._provider_config.get_domain(), + "smtp-service.json")) else: smtp_definition, mtime = get_content(res) diff --git a/src/leap/services/mail/smtpconfig.py b/src/leap/services/mail/smtpconfig.py index e7e2895a..30371005 100644 --- a/src/leap/services/mail/smtpconfig.py +++ b/src/leap/services/mail/smtpconfig.py @@ -45,4 +45,3 @@ class SMTPConfig(BaseConfig): def get_locations(self): return self._safe_get_value("locations") - diff --git a/src/leap/services/soledad/soledadbootstrapper.py b/src/leap/services/soledad/soledadbootstrapper.py index bae933de..db019a87 100644 --- a/src/leap/services/soledad/soledadbootstrapper.py +++ b/src/leap/services/soledad/soledadbootstrapper.py @@ -218,7 +218,8 @@ class SoledadBootstrapper(QtCore.QObject): self._keymanager.get_key(address, openpgp.OpenPGPKey, private=True, fetch_remote=False) except KeyNotFound: - logger.debug("Key not found. Generating key for %s" % (address,)) + logger.debug( + "Key not found. Generating key for %s" % (address,)) self._keymanager.gen_key(openpgp.OpenPGPKey) logger.debug("Key generated successfully.") @@ -235,8 +236,9 @@ class SoledadBootstrapper(QtCore.QObject): logger.debug("Uploading public key to %s" % (key_uri,)) - pubkey = self._keymanager.get_key(address, openpgp.OpenPGPKey, - private=False, fetch_remote=False) + pubkey = self._keymanager.get_key( + address, openpgp.OpenPGPKey, + private=False, fetch_remote=False) key_data = { self.PUBKEY_KEY: pubkey.key_data, } diff --git a/src/leap/services/soledad/soledadconfig.py b/src/leap/services/soledad/soledadconfig.py index 836265f3..80a82d11 100644 --- a/src/leap/services/soledad/soledadconfig.py +++ b/src/leap/services/soledad/soledadconfig.py @@ -45,4 +45,3 @@ class SoledadConfig(BaseConfig): def get_locations(self): return self._safe_get_value("locations") - -- cgit v1.2.3 From 0327db2e9be9d4e58fe6e8cc262de4fe0ac0d81e Mon Sep 17 00:00:00 2001 From: kali Date: Fri, 31 May 2013 07:12:39 +0900 Subject: pep8 --- src/leap/keymanager/tests/test_keymanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/keymanager/tests/test_keymanager.py b/src/leap/keymanager/tests/test_keymanager.py index 9b94d504..a36406a6 100644 --- a/src/leap/keymanager/tests/test_keymanager.py +++ b/src/leap/keymanager/tests/test_keymanager.py @@ -337,7 +337,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): self.assertTrue(key is not None) self.assertEqual(key.address, ADDRESS) self.assertEqual( - key.fingerprint.lower(), KEY_FINGERPRINT.lower()) + key.fingerprint.lower(), KEY_FINGERPRINT.lower()) self.assertFalse(key.private) def test_get_private_key(self): -- cgit v1.2.3 From f59db5457489606a6e12843c9e71911a1dbc54ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 31 May 2013 16:32:27 -0300 Subject: Fix relative import --- src/leap/gui/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/__init__.py b/src/leap/gui/__init__.py index d31dac64..4b289442 100644 --- a/src/leap/gui/__init__.py +++ b/src/leap/gui/__init__.py @@ -17,5 +17,5 @@ """ init file for leap.gui """ -from .. import app +app = __import__("app", globals(), locals(), [], 2) __all__ = [app] -- cgit v1.2.3 From 8a92157bd4359c3c41bcbcfbbd9f164ef633f205 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 30 May 2013 16:00:30 -0300 Subject: Add runtime requirements checker --- src/leap/util/requirement_checker.py | 101 +++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/leap/util/requirement_checker.py (limited to 'src') diff --git a/src/leap/util/requirement_checker.py b/src/leap/util/requirement_checker.py new file mode 100644 index 00000000..3538f122 --- /dev/null +++ b/src/leap/util/requirement_checker.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# requirement_checker.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Utility to check the needed requirements. +""" + +import os +import logging + +from pkg_resources import (DistributionNotFound, + get_distribution, + Requirement, + resource_stream, + VersionConflict) + +logger = logging.getLogger(__name__) + + +def get_requirements(): + """ + This function returns a list with requirements. + It checks either if its running from the source or if its installed. + + :returns: a list with packages names, required for the app. + :return type: list of str. + """ + develop = True + requirements = [] + + try: + # if we are running from the source + from pkg import util + requirements = util.parse_requirements() + except ImportError: + develop = False + + # if we are running from the package + if not develop: + requires_file_name = os.path.join('leap', 'util', 'reqs.txt') + dist_name = Requirement.parse('leap-client') + + try: + with resource_stream(dist_name, requires_file_name) as stream: + requirements = [line.strip() for line in stream] + except Exception, e: + logger.error("Requirements file not found. %e", (e, )) + + return requirements + + +def check_requirements(): + """ + This function check the dependencies declared in the + requirement(s) file(s) and logs the results. + """ + logger.debug("Checking requirements...") + requirements = get_requirements() + + for package in requirements: + try: + get_distribution(package) + except VersionConflict: + required_package = Requirement.parse(package) + required_version = required_package.specs[0] + required_name = required_package.key + + installed_package = get_distribution(required_name) + installed_version = installed_package.version + installed_location = installed_package.location + + msg = "Error: version not satisfied. " + msg += "Expected %s, installed %s (path: %s)." % ( + required_version, installed_version, installed_location) + + result = "%s ... %s" % (package, msg) + logger.error(result) + except DistributionNotFound: + msg = "Error: package not found!" + result = "%s ... %s" % (package, msg) + logger.error(result) + else: + msg = "OK" + result = "%s ... %s" % (package, msg) + logger.debug(result) + + logger.debug('Done') -- cgit v1.2.3 From 4e43431086531370118e310f29d0730256537127 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 31 May 2013 10:30:07 -0300 Subject: Integrate checker with app --- src/leap/app.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/leap/app.py b/src/leap/app.py index 03552edb..05b54d1f 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -27,6 +27,7 @@ from leap.common.events import server as event_server from leap.util import __version__ as VERSION from leap.util import leap_argparse from leap.util.leap_log_handler import LeapLogHandler +from leap.util.requirement_checker import check_requirements from leap.gui import locale_rc from leap.gui import twisted_main from leap.gui.mainwindow import MainWindow @@ -101,6 +102,8 @@ def main(): "one instead.") sys.exit(1) + check_requirements() + logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') logger.info('LEAP client version %s', VERSION) logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') -- cgit v1.2.3 From 4c726c1531abfe288604eaa4c1d347e85bed81eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 3 Jun 2013 15:02:41 -0300 Subject: Use Twisted's deferToThread and Deferreds to handle parallel tasks This removes CheckerThread --- src/leap/config/providerconfig.py | 9 + src/leap/gui/mainwindow.py | 78 +++-- src/leap/gui/wizard.py | 28 +- src/leap/services/abstractbootstrapper.py | 155 +++++++++ src/leap/services/eip/eipbootstrapper.py | 264 +++++----------- src/leap/services/eip/providerbootstrapper.py | 384 ++++++----------------- src/leap/services/eip/vpnlaunchers.py | 2 +- src/leap/services/mail/smtpbootstrapper.py | 142 ++++----- src/leap/services/soledad/soledadbootstrapper.py | 233 +++++--------- src/leap/util/checkerthread.py | 109 ------- 10 files changed, 541 insertions(+), 863 deletions(-) create mode 100644 src/leap/services/abstractbootstrapper.py delete mode 100644 src/leap/util/checkerthread.py (limited to 'src') diff --git a/src/leap/config/providerconfig.py b/src/leap/config/providerconfig.py index 8f75d4fe..68099ad4 100644 --- a/src/leap/config/providerconfig.py +++ b/src/leap/config/providerconfig.py @@ -130,6 +130,15 @@ class ProviderConfig(BaseConfig): """ return "openvpn" in self.get_services() + def provides_mx(self): + """ + Returns True if this particular provider has the MX service, + False otherwise. + + :rtype: bool + """ + return "mx" in self.get_services() + if __name__ == "__main__": logger = logging.getLogger(name='leap') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 12187f51..25478aa1 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -27,7 +27,7 @@ from functools import partial import keyring from PySide import QtCore, QtGui -from mock import Mock +from twisted.internet import threads from leap.common.check import leap_assert from leap.common.events import register @@ -50,7 +50,6 @@ from leap.services.eip.vpnlaunchers import (VPNLauncherException, EIPNoPkexecAvailable, EIPNoPolkitAuthAgentAvailable) from leap.util import __version__ as VERSION -from leap.util.checkerthread import CheckerThread from leap.services.mail.smtpconfig import SMTPConfig @@ -78,6 +77,9 @@ class MainWindow(QtGui.QMainWindow): PORT_KEY = "port" IP_KEY = "ip_address" + OPENVPN_SERVICE = "openvpn" + MX_SERVICE = "mx" + # Signals new_updates = QtCore.Signal(object) raise_window = QtCore.Signal([]) @@ -155,9 +157,6 @@ class MainWindow(QtGui.QMainWindow): # This is created once we have a valid provider config self._srp_auth = None - self._checker_thread = CheckerThread() - self._checker_thread.start() - # This thread is always running, although it's quite # lightweight when it's done setting up provider # configuration and certificate. @@ -187,6 +186,8 @@ class MainWindow(QtGui.QMainWindow): self._finish_eip_bootstrap) self._soledad_bootstrapper = SoledadBootstrapper() + self._soledad_bootstrapper.download_config.connect( + self._soledad_intermediate_stage) self._soledad_bootstrapper.gen_key.connect( self._soledad_bootstrapped_stage) @@ -262,8 +263,7 @@ class MainWindow(QtGui.QMainWindow): if self._first_run(): self._wizard_firstrun = True - self._wizard = Wizard(self._checker_thread, - standalone=standalone, + self._wizard = Wizard(standalone=standalone, bypass_checks=bypass_checks) # Give this window time to finish init and then show the wizard QtCore.QTimer.singleShot(1, self._launch_wizard) @@ -281,8 +281,8 @@ class MainWindow(QtGui.QMainWindow): def _launch_wizard(self): if self._wizard is None: - self._wizard = Wizard(self._checker_thread, - bypass_checks=self._bypass_checks) + self._wizard = Wizard(bypass_checks=self._bypass_checks) + self._wizard.accepted.connect(self._finish_init) self._wizard.exec_() self._wizard = None @@ -369,6 +369,7 @@ class MainWindow(QtGui.QMainWindow): msg) def _finish_init(self): + self.ui.cmbProviders.clear() self.ui.cmbProviders.addItems(self._configured_providers()) self._show_systray() self.show() @@ -425,6 +426,9 @@ class MainWindow(QtGui.QMainWindow): """ Sets up the systray icon """ + if self._systray is not None: + self._systray.setVisible(True) + return systrayMenu = QtGui.QMenu(self) systrayMenu.addAction(self._action_visible) systrayMenu.addAction(self.ui.action_sign_out) @@ -618,7 +622,6 @@ class MainWindow(QtGui.QMainWindow): provider = self.ui.cmbProviders.currentText() self._provider_bootstrapper.run_provider_select_checks( - self._checker_thread, provider, download_if_needed=True) @@ -643,7 +646,6 @@ class MainWindow(QtGui.QMainWindow): provider, "provider.json")): self._provider_bootstrapper.run_provider_setup_checks( - self._checker_thread, self._provider_config, download_if_needed=True) else: @@ -728,7 +730,7 @@ class MainWindow(QtGui.QMainWindow): auth_partial = partial(self._srp_auth.authenticate, username, password) - self._checker_thread.add_checks([auth_partial]) + threads.deferToThread(auth_partial) else: self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) self._login_set_enabled(True) @@ -760,7 +762,6 @@ class MainWindow(QtGui.QMainWindow): self._systray.setIcon(self.LOGGED_IN_ICON) self._soledad_bootstrapper.run_soledad_setup_checks( - self._checker_thread, self._provider_config, self.ui.lnUser.text(), self.ui.lnPassword.text(), @@ -768,6 +769,22 @@ class MainWindow(QtGui.QMainWindow): self._download_eip_config() + def _soledad_intermediate_stage(self, data): + """ + SLOT + TRIGGERS: + self._soledad_bootstrapper.download_config + + If there was a problem, displays it, otherwise it does nothing. + This is used for intermediate bootstrapping stages, in case + they fail. + """ + passed = data[self._soledad_bootstrapper.PASSED_KEY] + if not passed: + # TODO: display in the GUI + logger.error("Soledad failed to start: %s" % + (data[self._soledad_bootstrapper.ERROR_KEY],)) + def _soledad_bootstrapped_stage(self, data): """ SLOT @@ -787,14 +804,24 @@ class MainWindow(QtGui.QMainWindow): else: logger.debug("Done bootstrapping Soledad") - self._soledad = data[self._soledad_bootstrapper.SOLEDAD_KEY] - self._keymanager = data[self._soledad_bootstrapper.KEYMANAGER_KEY] + self._soledad = self._soledad_bootstrapper.soledad + self._keymanager = self._soledad_bootstrapper.keymanager - self._smtp_bootstrapper.run_smtp_setup_checks( - self._checker_thread, - self._provider_config, - self._smtp_config, - True) + if self._provider_config.provides_mx() and \ + self._enabled_services.count(self.MX_SERVICE) > 0: + self._smtp_bootstrapper.run_smtp_setup_checks( + self._provider_config, + self._smtp_config, + True) + else: + if self._enabled_services.count(self.MX_SERVICE) > 0: + pass # TODO: show MX status + #self._set_eip_status(self.tr("%s does not support MX") % + # (self._provider_config.get_domain(),), + # error=True) + else: + pass # TODO: show MX status + #self._set_eip_status(self.tr("MX is disabled")) def _smtp_bootstrapped_stage(self, data): """ @@ -914,14 +941,13 @@ class MainWindow(QtGui.QMainWindow): self._set_eip_status(self.tr("Checking configuration, please wait...")) if self._provider_config.provides_eip() and \ - self._enabled_services.count("openvpn") > 0: + self._enabled_services.count(self.OPENVPN_SERVICE) > 0: self._vpn_systray.setVisible(True) self._eip_bootstrapper.run_eip_setup_checks( - self._checker_thread, self._provider_config, download_if_needed=True) else: - if self._enabled_services.count("openvpn") > 0: + if self._enabled_services.count(self.OPENVPN_SERVICE) > 0: self._set_eip_status(self.tr("%s does not support EIP") % (self._provider_config.get_domain(),), error=True) @@ -1035,7 +1061,9 @@ class MainWindow(QtGui.QMainWindow): """ self._set_eip_status_icon("error") self._set_eip_status(self.tr("Signing out...")) - self._checker_thread.add_checks([self._srp_auth.logout]) + # XXX: If other defers are doing authenticated stuff, this + # might conflict with those. CHECK! + threads.deferToThread(self._srp_auth.logout) def _done_logging_out(self, ok, message): """ @@ -1121,8 +1149,6 @@ class MainWindow(QtGui.QMainWindow): logger.debug('About to quit, doing cleanup...') self._vpn.set_should_quit() self._vpn.wait() - self._checker_thread.set_should_quit() - self._checker_thread.wait() self._cleanup_pidfiles() def quit(self): diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 713383a6..552ace50 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -24,6 +24,7 @@ import json from PySide import QtCore, QtGui from functools import partial +from twisted.internet import threads from ui_wizard import Ui_Wizard from leap.config.providerconfig import ProviderConfig @@ -53,12 +54,10 @@ class Wizard(QtGui.QWizard): BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" - def __init__(self, checker, standalone=False, bypass_checks=False): + def __init__(self, standalone=False, bypass_checks=False): """ Constructor for the main Wizard. - :param checker: Checker thread that the wizard should use. - :type checker: CheckerThread :param standalone: If True, the application is running as standalone and the wizard should display some messages according to this. :type standalone: bool @@ -82,16 +81,19 @@ class Wizard(QtGui.QWizard): # Correspondence for services and their name to display EIP_LABEL = self.tr("Encrypted Internet") + MX_LABEL = self.tr("Encrypted Mail") if self._is_need_eip_password_warning(): EIP_LABEL += " " + self.tr( "(will need admin password to start)") self.SERVICE_DISPLAY = [ - EIP_LABEL + EIP_LABEL, + MX_LABEL ] self.SERVICE_CONFIG = [ - "openvpn" + "openvpn", + "mx" ] self._selected_services = set() @@ -147,8 +149,6 @@ class Wizard(QtGui.QWizard): self._username = None self._password = None - self._checker_thread = checker - self.page(self.REGISTER_USER_PAGE).setButtonText( QtGui.QWizard.CommitButton, self.tr("&Next >")) self.page(self.FINISH_PAGE).setButtonText( @@ -231,10 +231,12 @@ class Wizard(QtGui.QWizard): register = SRPRegister(provider_config=self._provider_config) register.registration_finished.connect( self._registration_finished) - self._checker_thread.add_checks( - [partial(register.register_user, - username.encode("utf8"), - password.encode("utf8"))]) + + threads.deferToThread( + partial(register.register_user, + username.encode("utf8"), + password.encode("utf8"))) + self._username = username self._password = password self._set_register_status(self.tr("Starting registration...")) @@ -318,7 +320,6 @@ class Wizard(QtGui.QWizard): self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) self._provider_bootstrapper.run_provider_select_checks( - self._checker_thread, self._domain) def _complete_task(self, data, label, complete=False, complete_page=-1): @@ -510,8 +511,7 @@ class Wizard(QtGui.QWizard): .get_name(),)) self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON) self._provider_bootstrapper.\ - run_provider_setup_checks(self._checker_thread, - self._provider_config) + run_provider_setup_checks(self._provider_config) if pageId == self.PRESENT_PROVIDER_PAGE: self.page(pageId).setSubTitle(self.tr("Description of services " diff --git a/src/leap/services/abstractbootstrapper.py b/src/leap/services/abstractbootstrapper.py new file mode 100644 index 00000000..bce03e6b --- /dev/null +++ b/src/leap/services/abstractbootstrapper.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# abstractbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Abstract bootstrapper implementation +""" +import logging + +import requests + +from PySide import QtCore +from twisted.internet import threads +from leap.common.check import leap_assert, leap_assert_type + +logger = logging.getLogger(__name__) + + +class AbstractBootstrapper(QtCore.QObject): + """ + Abstract Bootstrapper that implements the needed deferred callbacks + """ + + PASSED_KEY = "passed" + ERROR_KEY = "error" + + def __init__(self, bypass_checks=False): + """ + Constructor for the abstract bootstrapper + + :param bypass_checks: Set to true if the app should bypass + first round of checks for CA + certificates at bootstrap + :type bypass_checks: bool + """ + QtCore.QObject.__init__(self) + + leap_assert(self._gui_errback.im_func == \ + AbstractBootstrapper._gui_errback.im_func, + "Cannot redefine _gui_errback") + leap_assert(self._errback.im_func == \ + AbstractBootstrapper._errback.im_func, + "Cannot redefine _errback") + leap_assert(self._gui_notify.im_func == \ + AbstractBootstrapper._gui_notify.im_func, + "Cannot redefine _gui_notify") + + # **************************************************** # + # Dependency injection helpers, override this for more + # granular testing + self._fetcher = requests + # **************************************************** # + + self._session = self._fetcher.session() + self._bypass_checks = bypass_checks + self._signal_to_emit = None + self._err_msg = None + + def _gui_errback(self, failure): + """ + Errback used to notify the GUI of a problem, it should be used + as the last errback of the whole chain. + + Traps all exceptions if a signal is defined, otherwise it just + lets it continue. + + NOTE: This method is final, it should not be redefined. + + :param failure: failure object that Twisted generates + :type failure: twisted.python.failure.Failure + """ + if self._signal_to_emit: + err_msg = self._err_msg \ + if self._err_msg is not None \ + else str(failure.value) + self._signal_to_emit.emit({ + self.PASSED_KEY: False, + self.ERROR_KEY: err_msg + }) + failure.trap(Exception) + + def _errback(self, failure, signal=None): + """ + Regular errback used for the middle of the chain. If it's + executed, the first one will set the signal to emit as + failure. + + NOTE: This method is final, it should not be redefined. + + :param failure: failure object that Twisted generates + :type failure: twisted.python.failure.Failure + :param signal: Signal to emit if it fails here first + :type signal: QtCore.SignalInstance + + :returns: failure object that Twisted generates + :rtype: twisted.python.failure.Failure + """ + if self._signal_to_emit is None: + self._signal_to_emit = signal + return failure + + def _gui_notify(self, _, signal=None): + """ + Callback used to notify the GUI of a success. Will emit signal + if specified + + NOTE: This method is final, it should not be redefined. + + :param _: IGNORED. Returned from the previous callback + :type _: IGNORED + :param signal: Signal to emit if it fails here first + :type signal: QtCore.SignalInstance + """ + if signal: + logger.debug("Emitting %s" % (signal,)) + signal.emit({self.PASSED_KEY: True, self.ERROR_KEY: ""}) + + def addCallbackChain(self, callbacks): + """ + Creates a callback/errback chain on another thread using + deferToThread and adds the _gui_errback to the end to notify + the GUI on an error. + + :param callbacks: List of tuples of callbacks and the signal + associated to that callback + :type callbacks: list(tuple(func, func)) + """ + leap_assert_type(callbacks, list) + + self._signal_to_emit = None + self._err_msg = None + + d = None + for cb, sig in callbacks: + if d is None: + d = threads.deferToThread(cb) + else: + d.addCallback(cb) + d.addErrback(self._errback, signal=sig) + d.addCallback(self._gui_notify, signal=sig) + d.addErrback(self._gui_errback) + diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index a881f235..7216bb80 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -22,9 +22,7 @@ EIP bootstrapping import logging import os -import requests - -from PySide import QtGui, QtCore +from PySide import QtCore from leap.common.check import leap_assert, leap_assert_type from leap.common.certs import is_valid_pemfile, should_redownload @@ -32,49 +30,34 @@ from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth from leap.services.eip.eipconfig import EIPConfig -from leap.util.checkerthread import CheckerThread from leap.util.request_helpers import get_content +from leap.services.abstractbootstrapper import AbstractBootstrapper logger = logging.getLogger(__name__) -class EIPBootstrapper(QtCore.QObject): +class EIPBootstrapper(AbstractBootstrapper): """ Sets up EIP for a provider a series of checks and emits signals after they are passed. If a check fails, the subsequent checks are not executed """ - PASSED_KEY = "passed" - ERROR_KEY = "error" - - IDLE_SLEEP_INTERVAL = 100 - # All dicts returned are of the form # {"passed": bool, "error": str} download_config = QtCore.Signal(dict) download_client_certificate = QtCore.Signal(dict) def __init__(self): - QtCore.QObject.__init__(self) + AbstractBootstrapper.__init__(self) - # **************************************************** # - # Dependency injection helpers, override this for more - # granular testing - self._fetcher = requests - # **************************************************** # - - self._session = self._fetcher.session() self._provider_config = None self._eip_config = None self._download_if_needed = False - def _download_config(self): + def _download_config(self, *args): """ Downloads the EIP config for the given provider - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._provider_config, @@ -83,65 +66,47 @@ class EIPBootstrapper(QtCore.QObject): logger.debug("Downloading EIP config for %s" % (self._provider_config.get_domain(),)) - download_config_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - self._eip_config = EIPConfig() - try: - headers = {} - mtime = get_mtime(os.path.join(self._eip_config - .get_path_prefix(), - "leap", - "providers", - self._provider_config.get_domain(), - "eip-service.json")) - - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - # there is some confusion with this uri, - # it's in 1/config/eip, config/eip and config/1/eip... - config_uri = "%s/%s/config/eip-service.json" % ( - self._provider_config.get_api_uri(), - self._provider_config.get_api_version()) - logger.debug('Downloading eip config from: %s' % config_uri) - - res = self._session.get(config_uri, - verify=self._provider_config - .get_ca_cert_path(), - headers=headers) - res.raise_for_status() - - # Not modified - if res.status_code == 304: - logger.debug("EIP definition has not been modified") - else: - eip_definition, mtime = get_content(res) - - self._eip_config.load(data=eip_definition, mtime=mtime) - self._eip_config.save(["leap", + headers = {} + mtime = get_mtime(os.path.join(self._eip_config + .get_path_prefix(), + "leap", "providers", self._provider_config.get_domain(), - "eip-service.json"]) - - download_config_data[self.PASSED_KEY] = True - except Exception as e: - download_config_data[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting download_config %s" % (download_config_data,)) - self.download_config.emit(download_config_data) - - return download_config_data[self.PASSED_KEY] - - def _download_client_certificates(self): + "eip-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + # there is some confusion with this uri, + # it's in 1/config/eip, config/eip and config/1/eip... + config_uri = "%s/%s/config/eip-service.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('Downloading eip config from: %s' % config_uri) + + res = self._session.get(config_uri, + verify=self._provider_config + .get_ca_cert_path(), + headers=headers) + res.raise_for_status() + + # Not modified + if res.status_code == 304: + logger.debug("EIP definition has not been modified") + else: + eip_definition, mtime = get_content(res) + + self._eip_config.load(data=eip_definition, mtime=mtime) + self._eip_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "eip-service.json"]) + + def _download_client_certificates(self, *args): """ Downloads the EIP client certificate for the given provider - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._provider_config, "We need a provider configuration!") leap_assert(self._eip_config, "We need an eip configuration!") @@ -149,11 +114,6 @@ class EIPBootstrapper(QtCore.QObject): logger.debug("Downloading EIP client certificate for %s" % (self._provider_config.get_domain(),)) - download_cert = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - client_cert_path = self._eip_config.\ get_client_cert_path(self._provider_config, about_to_download=True) @@ -164,56 +124,39 @@ class EIPBootstrapper(QtCore.QObject): if self._download_if_needed and \ os.path.exists(client_cert_path): - try: - check_and_fix_urw_only(client_cert_path) - download_cert[self.PASSED_KEY] = True - except Exception as e: - download_cert[self.PASSED_KEY] = False - download_cert[self.ERROR_KEY] = "%s" % (e,) - self.download_client_certificate.emit(download_cert) - return download_cert[self.PASSED_KEY] - - try: - srp_auth = SRPAuth(self._provider_config) - session_id = srp_auth.get_session_id() - cookies = None - if session_id: - cookies = {"_session_id": session_id} - cert_uri = "%s/%s/cert" % ( - self._provider_config.get_api_uri(), - self._provider_config.get_api_version()) - logger.debug('getting cert from uri: %s' % cert_uri) - res = self._session.get(cert_uri, - verify=self._provider_config - .get_ca_cert_path(), - cookies=cookies) - res.raise_for_status() - client_cert = res.content - - # TODO: check certificate validity - - if not is_valid_pemfile(client_cert): - raise Exception(self.tr("The downloaded certificate is not a " - "valid PEM file")) - - mkdir_p(os.path.dirname(client_cert_path)) - - with open(client_cert_path, "w") as f: - f.write(client_cert) - check_and_fix_urw_only(client_cert_path) - - download_cert[self.PASSED_KEY] = True - except Exception as e: - download_cert[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting download_client_certificates %s" % - (download_cert,)) - self.download_client_certificate.emit(download_cert) - - return download_cert[self.PASSED_KEY] - - def run_eip_setup_checks(self, checker, + return + + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} + cert_uri = "%s/%s/cert" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('getting cert from uri: %s' % cert_uri) + res = self._session.get(cert_uri, + verify=self._provider_config + .get_ca_cert_path(), + cookies=cookies) + res.raise_for_status() + client_cert = res.content + + # TODO: check certificate validity + + if not is_valid_pemfile(client_cert): + raise Exception(self.tr("The downloaded certificate is not a " + "valid PEM file")) + + mkdir_p(os.path.dirname(client_cert_path)) + + with open(client_cert_path, "w") as f: + f.write(client_cert) + + check_and_fix_urw_only(client_cert_path) + + def run_eip_setup_checks(self, provider_config, download_if_needed=False): """ @@ -228,60 +171,9 @@ class EIPBootstrapper(QtCore.QObject): self._provider_config = provider_config self._download_if_needed = download_if_needed - checker.add_checks([ - self._download_config, - self._download_client_certificates - ]) - - -if __name__ == "__main__": - import sys - from functools import partial - app = QtGui.QApplication(sys.argv) - - import signal - - def sigint_handler(*args, **kwargs): - logger.debug('SIGINT catched. shutting down...') - checker = args[0] - checker.set_should_quit() - QtGui.QApplication.quit() - - def signal_tester(d): - print d - - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - eip_checks = EIPBootstrapper() - checker = CheckerThread() - - sigint = partial(sigint_handler, checker) - signal.signal(signal.SIGINT, sigint) - - timer = QtCore.QTimer() - timer.start(500) - timer.timeout.connect(lambda: None) - app.connect(app, QtCore.SIGNAL("aboutToQuit()"), - checker.set_should_quit) - w = QtGui.QWidget() - w.resize(100, 100) - w.show() - - checker.start() - - provider_config = ProviderConfig() - if provider_config.load(os.path.join("leap", - "providers", - "bitmask.net", - "provider.json")): - eip_checks.run_eip_setup_checks(checker, provider_config) - - sys.exit(app.exec_()) + cb_chain = [ + (self._download_config, self.download_config), + (self._download_client_certificates, self.download_client_certificate) + ] + + self.addCallbackChain(cb_chain) diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 289d212b..1339e086 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -24,30 +24,25 @@ import os import requests -from PySide import QtGui, QtCore +from PySide import QtCore from leap.common.certs import get_digest from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.common.check import leap_assert, leap_assert_type from leap.config.providerconfig import ProviderConfig -from leap.util.checkerthread import CheckerThread from leap.util.request_helpers import get_content +from leap.services.abstractbootstrapper import AbstractBootstrapper logger = logging.getLogger(__name__) -class ProviderBootstrapper(QtCore.QObject): +class ProviderBootstrapper(AbstractBootstrapper): """ Given a provider URL performs a series of checks and emits signals after they are passed. If a check fails, the subsequent checks are not executed """ - PASSED_KEY = "passed" - ERROR_KEY = "error" - - IDLE_SLEEP_INTERVAL = 100 - # All dicts returned are of the form # {"passed": bool, "error": str} name_resolution = QtCore.Signal(dict) @@ -66,68 +61,34 @@ class ProviderBootstrapper(QtCore.QObject): first round of checks for CA certificates at bootstrap :type bypass_checks: bool """ - QtCore.QObject.__init__(self) + AbstractBootstrapper.__init__(self, bypass_checks) - # **************************************************** # - # Dependency injection helpers, override this for more - # granular testing - self._fetcher = requests - # **************************************************** # - - self._session = self._fetcher.session() self._domain = None self._provider_config = None self._download_if_needed = False - self._bypass_checks = bypass_checks def _check_name_resolution(self): """ Checks that the name resolution for the provider name works - - :return: True if the checks passed, False otherwise - :rtype: bool """ - leap_assert(self._domain, "Cannot check DNS without a domain") logger.debug("Checking name resolution for %s" % (self._domain)) - name_resolution_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - # We don't skip this check, since it's basic for the whole # system to work - try: - socket.gethostbyname(self._domain) - name_resolution_data[self.PASSED_KEY] = True - except socket.gaierror as e: - name_resolution_data[self.ERROR_KEY] = "%s" % (e,) + socket.gethostbyname(self._domain) - logger.debug("Emitting name_resolution %s" % (name_resolution_data,)) - self.name_resolution.emit(name_resolution_data) - - return name_resolution_data[self.PASSED_KEY] - - def _check_https(self): + def _check_https(self, *args): """ Checks that https is working and that the provided certificate checks out - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._domain, "Cannot check HTTPS without a domain") logger.debug("Checking https for %s" % (self._domain)) - https_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - # We don't skip this check, since it's basic for the whole # system to work @@ -135,105 +96,75 @@ class ProviderBootstrapper(QtCore.QObject): res = self._session.get("https://%s" % (self._domain,), verify=not self._bypass_checks) res.raise_for_status() - https_data[self.PASSED_KEY] = True - except requests.exceptions.SSLError as e: - logger.error("%s" % (e,)) - https_data[self.ERROR_KEY] = self.tr("Provider certificate could " - "not verify") - except Exception as e: - logger.error("%s" % (e,)) - https_data[self.ERROR_KEY] = self.tr("Provider does not support " - "HTTPS") - - logger.debug("Emitting https_connection %s" % (https_data,)) - self.https_connection.emit(https_data) - - return https_data[self.PASSED_KEY] - - def _download_provider_info(self): + except requests.exceptions.SSLError: + self._err_msg = self.tr("Provider certificate could " + "not be verified") + raise + except Exception: + self._err_msg = self.tr("Provider does not support HTTPS") + raise + + def _download_provider_info(self, *args): """ Downloads the provider.json defition - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._domain, "Cannot download provider info without a domain") logger.debug("Downloading provider info for %s" % (self._domain)) - download_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - - try: - headers = {} - mtime = get_mtime(os.path.join(ProviderConfig() - .get_path_prefix(), - "leap", - "providers", - self._domain, - "provider.json")) - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - res = self._session.get("https://%s/%s" % (self._domain, - "provider.json"), - headers=headers, - verify=not self._bypass_checks) - res.raise_for_status() - - # Not modified - if res.status_code == 304: - logger.debug("Provider definition has not been modified") - else: - provider_definition, mtime = get_content(res) - - provider_config = ProviderConfig() - provider_config.load(data=provider_definition, mtime=mtime) - provider_config.save(["leap", - "providers", - self._domain, - "provider.json"]) - - download_data[self.PASSED_KEY] = True - except Exception as e: - download_data[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting download_provider_info %s" % (download_data,)) - self.download_provider_info.emit(download_data) - - return download_data[self.PASSED_KEY] - - def run_provider_select_checks(self, checker, - domain, download_if_needed=False): + headers = {} + mtime = get_mtime(os.path.join(ProviderConfig() + .get_path_prefix(), + "leap", + "providers", + self._domain, + "provider.json")) + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + res = self._session.get("https://%s/%s" % (self._domain, + "provider.json"), + headers=headers, + verify=not self._bypass_checks) + res.raise_for_status() + + # Not modified + if res.status_code == 304: + logger.debug("Provider definition has not been modified") + else: + provider_definition, mtime = get_content(res) + + provider_config = ProviderConfig() + provider_config.load(data=provider_definition, mtime=mtime) + provider_config.save(["leap", + "providers", + self._domain, + "provider.json"]) + + def run_provider_select_checks(self, domain, download_if_needed=False): """ Populates the check queue. - :param checker: checker thread to be used to run this check - :type checker: CheckerThread - :param domain: domain to check :type domain: str :param download_if_needed: if True, makes the checks do not overwrite already downloaded data :type download_if_needed: bool - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(domain and len(domain) > 0, "We need a domain!") self._domain = domain self._download_if_needed = download_if_needed - checker.add_checks([ - self._check_name_resolution, - self._check_https, - self._download_provider_info - ]) + cb_chain = [ + (self._check_name_resolution, self.name_resolution), + (self._check_https, self.https_connection), + (self._download_provider_info, self.download_provider_info) + ] + + self.addCallbackChain(cb_chain) def _should_proceed_cert(self): """ @@ -250,12 +181,9 @@ class ProviderBootstrapper(QtCore.QObject): return not os.path.exists(self._provider_config .get_ca_cert_path(about_to_download=True)) - def _download_ca_cert(self): + def _download_ca_cert(self, *args): """ Downloads the CA cert that is going to be used for the api URL - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._provider_config, "Cannot download the ca cert " @@ -264,56 +192,28 @@ class ProviderBootstrapper(QtCore.QObject): logger.debug("Downloading ca cert for %s at %s" % (self._domain, self._provider_config.get_ca_cert_uri())) - download_ca_cert_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - if not self._should_proceed_cert(): - try: - check_and_fix_urw_only( - self._provider_config - .get_ca_cert_path(about_to_download=True)) - download_ca_cert_data[self.PASSED_KEY] = True - except Exception as e: - download_ca_cert_data[self.PASSED_KEY] = False - download_ca_cert_data[self.ERROR_KEY] = "%s" % (e,) - self.download_ca_cert.emit(download_ca_cert_data) - return download_ca_cert_data[self.PASSED_KEY] - - try: - res = self._session.get(self._provider_config.get_ca_cert_uri(), - verify=not self._bypass_checks) - res.raise_for_status() - - cert_path = self._provider_config.get_ca_cert_path( - about_to_download=True) - - cert_dir = os.path.dirname(cert_path) + check_and_fix_urw_only( + self._provider_config + .get_ca_cert_path(about_to_download=True)) - mkdir_p(cert_dir) + res = self._session.get(self._provider_config.get_ca_cert_uri(), + verify=not self._bypass_checks) + res.raise_for_status() - with open(cert_path, "w") as f: - f.write(res.content) + cert_path = self._provider_config.get_ca_cert_path( + about_to_download=True) + cert_dir = os.path.dirname(cert_path) + mkdir_p(cert_dir) + with open(cert_path, "w") as f: + f.write(res.content) - check_and_fix_urw_only(cert_path) + check_and_fix_urw_only(cert_path) - download_ca_cert_data[self.PASSED_KEY] = True - except Exception as e: - download_ca_cert_data[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting download_ca_cert %s" % (download_ca_cert_data,)) - self.download_ca_cert.emit(download_ca_cert_data) - - return download_ca_cert_data[self.PASSED_KEY] - - def _check_ca_fingerprint(self): + def _check_ca_fingerprint(self, *args): """ Checks the CA cert fingerprint against the one provided in the json definition - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._provider_config, "Cannot check the ca cert " "without a provider config!") @@ -322,50 +222,27 @@ class ProviderBootstrapper(QtCore.QObject): (self._domain, self._provider_config.get_ca_cert_path())) - check_ca_fingerprint_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - if not self._should_proceed_cert(): - check_ca_fingerprint_data[self.PASSED_KEY] = True - self.check_ca_fingerprint.emit(check_ca_fingerprint_data) - return True + return - try: - parts = self._provider_config.get_ca_cert_fingerprint().split(":") - leap_assert(len(parts) == 2, "Wrong fingerprint format") - - method = parts[0].strip() - fingerprint = parts[1].strip() - cert_data = None - with open(self._provider_config.get_ca_cert_path()) as f: - cert_data = f.read() - - leap_assert(len(cert_data) > 0, "Could not read certificate data") - - digest = get_digest(cert_data, method) + parts = self._provider_config.get_ca_cert_fingerprint().split(":") + leap_assert(len(parts) == 2, "Wrong fingerprint format") - leap_assert(digest == fingerprint, - "Downloaded certificate has a different fingerprint!") + method = parts[0].strip() + fingerprint = parts[1].strip() + cert_data = None + with open(self._provider_config.get_ca_cert_path()) as f: + cert_data = f.read() - check_ca_fingerprint_data[self.PASSED_KEY] = True - except Exception as e: - check_ca_fingerprint_data[self.ERROR_KEY] = "%s" % (e,) + leap_assert(len(cert_data) > 0, "Could not read certificate data") + digest = get_digest(cert_data, method) + leap_assert(digest == fingerprint, + "Downloaded certificate has a different fingerprint!") - logger.debug("Emitting check_ca_fingerprint %s" % - (check_ca_fingerprint_data,)) - self.check_ca_fingerprint.emit(check_ca_fingerprint_data) - - return check_ca_fingerprint_data[self.PASSED_KEY] - - def _check_api_certificate(self): + def _check_api_certificate(self, *args): """ Tries to make an API call with the downloaded cert and checks if it validates against it - - :return: True if the checks passed, False otherwise - :rtype: bool """ leap_assert(self._provider_config, "Cannot check the ca cert " "without a provider config!") @@ -374,34 +251,17 @@ class ProviderBootstrapper(QtCore.QObject): (self._provider_config.get_api_uri(), self._provider_config.get_ca_cert_path())) - check_api_certificate_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - if not self._should_proceed_cert(): - check_api_certificate_data[self.PASSED_KEY] = True - self.check_api_certificate.emit(check_api_certificate_data) - return True + return - try: - test_uri = "%s/%s/cert" % (self._provider_config.get_api_uri(), - self._provider_config.get_api_version()) - res = self._session.get(test_uri, - verify=self._provider_config - .get_ca_cert_path()) - res.raise_for_status() - check_api_certificate_data[self.PASSED_KEY] = True - except Exception as e: - check_api_certificate_data[self.ERROR_KEY] = "%s" % (e,) + test_uri = "%s/%s/cert" % (self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + res = self._session.get(test_uri, + verify=self._provider_config + .get_ca_cert_path()) + res.raise_for_status() - logger.debug("Emitting check_api_certificate %s" % - (check_api_certificate_data,)) - self.check_api_certificate.emit(check_api_certificate_data) - - return check_api_certificate_data[self.PASSED_KEY] - - def run_provider_setup_checks(self, checker, + def run_provider_setup_checks(self, provider_config, download_if_needed=False): """ @@ -420,64 +280,10 @@ class ProviderBootstrapper(QtCore.QObject): self._provider_config = provider_config self._download_if_needed = download_if_needed - checker.add_checks([ - self._download_ca_cert, - self._check_ca_fingerprint, - self._check_api_certificate - ]) - -if __name__ == "__main__": - import sys - from functools import partial - app = QtGui.QApplication(sys.argv) - - import signal - - def sigint_handler(*args, **kwargs): - logger.debug('SIGINT catched. shutting down...') - bootstrapper_checks = args[0] - bootstrapper_checks.set_should_quit() - QtGui.QApplication.quit() - - def signal_tester(d): - print d - - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - bootstrapper_checks = ProviderBootstrapper() - - checker = CheckerThread() - checker.start() - - sigint = partial(sigint_handler, checker) - signal.signal(signal.SIGINT, sigint) - - timer = QtCore.QTimer() - timer.start(500) - timer.timeout.connect(lambda: None) - app.connect(app, QtCore.SIGNAL("aboutToQuit()"), - checker.set_should_quit) - w = QtGui.QWidget() - w.resize(100, 100) - w.show() - - bootstrapper_checks.run_provider_select_checks(checker, - "bitmask.net") - - provider_config = ProviderConfig() - if provider_config.load(os.path.join("leap", - "providers", - "bitmask.net", - "provider.json")): - bootstrapper_checks.run_provider_setup_checks(checker, - provider_config) - - sys.exit(app.exec_()) + cb_chain = [ + (self._download_ca_cert, self.download_ca_cert), + (self._check_ca_fingerprint, self.check_ca_fingerprint), + (self._check_api_certificate, self.check_api_certificate) + ] + + self.addCallbackChain(cb_chain) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index addad959..0691e121 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -132,7 +132,7 @@ def _is_auth_agent_running(): """ polkit_gnome = 'ps aux | grep polkit-[g]nome-authentication-agent-1' polkit_kde = 'ps aux | grep polkit-[k]de-authentication-agent-1' - return (len(commands.getoutput(polkit_gnome) > 0) or + return (len(commands.getoutput(polkit_gnome)) > 0 or len(commands.getoutput(polkit_kde)) > 0) diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py index 6e0a0a47..64bf3153 100644 --- a/src/leap/services/mail/smtpbootstrapper.py +++ b/src/leap/services/mail/smtpbootstrapper.py @@ -22,8 +22,6 @@ SMTP bootstrapping import logging import os -import requests - from PySide import QtCore from leap.common.check import leap_assert, leap_assert_type @@ -31,44 +29,30 @@ from leap.common.files import get_mtime from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth from leap.util.request_helpers import get_content +from leap.services.abstractbootstrapper import AbstractBootstrapper logger = logging.getLogger(__name__) -class SMTPBootstrapper(QtCore.QObject): +class SMTPBootstrapper(AbstractBootstrapper): """ SMTP init procedure """ - PASSED_KEY = "passed" - ERROR_KEY = "error" - - IDLE_SLEEP_INTERVAL = 100 - # All dicts returned are of the form # {"passed": bool, "error": str} download_config = QtCore.Signal(dict) def __init__(self): - QtCore.QObject.__init__(self) + AbstractBootstrapper.__init__(self) - # **************************************************** # - # Dependency injection helpers, override this for more - # granular testing - self._fetcher = requests - # **************************************************** # - - self._session = self._fetcher.session() self._provider_config = None self._smtp_config = None self._download_if_needed = False - def _download_config(self): + def _download_config(self, *args): """ Downloads the SMTP config for the given provider - - :return: True if everything went as expected, False otherwise - :rtype: bool """ leap_assert(self._provider_config, @@ -77,79 +61,59 @@ class SMTPBootstrapper(QtCore.QObject): logger.debug("Downloading SMTP config for %s" % (self._provider_config.get_domain(),)) - download_config_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - - try: - headers = {} - mtime = get_mtime(os.path.join(self._smtp_config - .get_path_prefix(), - "leap", - "providers", - self._provider_config.get_domain(), - "smtp-service.json")) - - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - # there is some confusion with this uri, - config_uri = "%s/%s/config/smtp-service.json" % ( - self._provider_config.get_api_uri(), - self._provider_config.get_api_version()) - logger.debug('Downloading SMTP config from: %s' % config_uri) - - srp_auth = SRPAuth(self._provider_config) - session_id = srp_auth.get_session_id() - cookies = None - if session_id: - cookies = {"_session_id": session_id} - - res = self._session.get(config_uri, - verify=self._provider_config - .get_ca_cert_path(), - headers=headers, - cookies=cookies) - res.raise_for_status() - - # Not modified - if res.status_code == 304: - logger.debug("SMTP definition has not been modified") - self._smtp_config.load(os.path.join( - "leap", "providers", - self._provider_config.get_domain(), - "smtp-service.json")) - else: - smtp_definition, mtime = get_content(res) - - self._smtp_config.load(data=smtp_definition, mtime=mtime) - self._smtp_config.save(["leap", - "providers", - self._provider_config.get_domain(), - "smtp-service.json"]) - - download_config_data[self.PASSED_KEY] = True - except Exception as e: - download_config_data[self.PASSED_KEY] = False - download_config_data[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting download_config %s" % (download_config_data,)) - self.download_config.emit(download_config_data) - - return download_config_data[self.PASSED_KEY] + headers = {} + mtime = get_mtime(os.path.join(self._smtp_config + .get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + # there is some confusion with this uri, + config_uri = "%s/%s/config/smtp-service.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('Downloading SMTP config from: %s' % config_uri) + + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} + + res = self._session.get(config_uri, + verify=self._provider_config + .get_ca_cert_path(), + headers=headers, + cookies=cookies) + res.raise_for_status() + + # Not modified + if res.status_code == 304: + logger.debug("SMTP definition has not been modified") + self._smtp_config.load(os.path.join("leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json")) + else: + smtp_definition, mtime = get_content(res) + + self._smtp_config.load(data=smtp_definition, mtime=mtime) + self._smtp_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "smtp-service.json"]) def run_smtp_setup_checks(self, - checker, provider_config, smtp_config, download_if_needed=False): """ Starts the checks needed for a new smtp setup - :param checker: Object that executes actions in a different - thread - :type checker: leap.util.checkerthread.CheckerThread :param provider_config: Provider configuration :type provider_config: ProviderConfig :param smtp_config: SMTP configuration to populate @@ -164,6 +128,8 @@ class SMTPBootstrapper(QtCore.QObject): self._smtp_config = smtp_config self._download_if_needed = download_if_needed - checker.add_checks([ - self._download_config - ]) + cb_chain = [ + (self._download_config, self.download_config), + ] + + self.addCallbackChain(cb_chain) diff --git a/src/leap/services/soledad/soledadbootstrapper.py b/src/leap/services/soledad/soledadbootstrapper.py index eea9b0d5..2635a7e6 100644 --- a/src/leap/services/soledad/soledadbootstrapper.py +++ b/src/leap/services/soledad/soledadbootstrapper.py @@ -22,10 +22,7 @@ Soledad bootstrapping import logging import os -import requests - from PySide import QtCore -from mock import Mock from leap.common.check import leap_assert, leap_assert_type from leap.common.files import get_mtime @@ -36,39 +33,29 @@ from leap.crypto.srpauth import SRPAuth from leap.services.soledad.soledadconfig import SoledadConfig from leap.util.request_helpers import get_content from leap.soledad import Soledad +from leap.services.abstractbootstrapper import AbstractBootstrapper logger = logging.getLogger(__name__) -class SoledadBootstrapper(QtCore.QObject): +class SoledadBootstrapper(AbstractBootstrapper): """ Soledad init procedure """ - PASSED_KEY = "passed" - ERROR_KEY = "error" SOLEDAD_KEY = "soledad" KEYMANAGER_KEY = "keymanager" PUBKEY_KEY = "user[public_key]" - IDLE_SLEEP_INTERVAL = 100 - # All dicts returned are of the form # {"passed": bool, "error": str} download_config = QtCore.Signal(dict) gen_key = QtCore.Signal(dict) def __init__(self): - QtCore.QObject.__init__(self) - - # **************************************************** # - # Dependency injection helpers, override this for more - # granular testing - self._fetcher = requests - # **************************************************** # + AbstractBootstrapper.__init__(self) - self._session = self._fetcher.session() self._provider_config = None self._soledad_config = None self._keymanager = None @@ -76,6 +63,14 @@ class SoledadBootstrapper(QtCore.QObject): self._user = "" self._password = "" + @property + def keymanager(self): + return self._keymanager + + @property + def soledad(self): + return self._soledad + def _load_and_sync_soledad(self, srp_auth): """ Once everthing is in the right place, we instantiate and sync @@ -92,7 +87,8 @@ class SoledadBootstrapper(QtCore.QObject): local_db_path = "%s/%s.db" % (prefix, uuid) # TODO: use the proper URL - server_url = 'https://mole.dev.bitmask.net:2424/user-%s' % (uuid,) + #server_url = 'https://mole.dev.bitmask.net:2424/user-%s' % (uuid,) + server_url = 'https://gadwall.dev.bitmask.net:1111/user-%s' % (uuid,) # server_url = self._soledad_config.get_hosts(...) cert_file = self._provider_config.get_ca_cert_path() @@ -109,9 +105,6 @@ class SoledadBootstrapper(QtCore.QObject): def _download_config(self): """ Downloads the Soledad config for the given provider - - :return: True if everything went as expected, False otherwise - :rtype: bool """ leap_assert(self._provider_config, @@ -120,150 +113,84 @@ class SoledadBootstrapper(QtCore.QObject): logger.debug("Downloading Soledad config for %s" % (self._provider_config.get_domain(),)) - download_config_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "" - } - self._soledad_config = SoledadConfig() - try: - headers = {} - mtime = get_mtime(os.path.join(self._soledad_config - .get_path_prefix(), - "leap", - "providers", - self._provider_config.get_domain(), - "soledad-service.json")) - - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - # there is some confusion with this uri, - config_uri = "%s/%s/config/soledad-service.json" % ( - self._provider_config.get_api_uri(), - self._provider_config.get_api_version()) - logger.debug('Downloading soledad config from: %s' % config_uri) - - srp_auth = SRPAuth(self._provider_config) - session_id = srp_auth.get_session_id() - cookies = None - if session_id: - cookies = {"_session_id": session_id} - - res = self._session.get(config_uri, - verify=self._provider_config - .get_ca_cert_path(), - headers=headers, - cookies=cookies) - res.raise_for_status() - - # Not modified - if res.status_code == 304: - logger.debug("Soledad definition has not been modified") - else: - soledad_definition, mtime = get_content(res) - - self._soledad_config.load(data=soledad_definition, mtime=mtime) - self._soledad_config.save(["leap", - "providers", - self._provider_config.get_domain(), - "soledad-service.json"]) - - self._load_and_sync_soledad(srp_auth) - - download_config_data[self.PASSED_KEY] = True - except Exception as e: - download_config_data[self.PASSED_KEY] = False - download_config_data[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting download_config %s" % (download_config_data,)) - self.download_config.emit(download_config_data) - - return download_config_data[self.PASSED_KEY] + headers = {} + mtime = get_mtime(os.path.join(self._soledad_config + .get_path_prefix(), + "leap", + "providers", + self._provider_config.get_domain(), + "soledad-service.json")) + + if self._download_if_needed and mtime: + headers['if-modified-since'] = mtime + + # there is some confusion with this uri, + config_uri = "%s/%s/config/soledad-service.json" % ( + self._provider_config.get_api_uri(), + self._provider_config.get_api_version()) + logger.debug('Downloading soledad config from: %s' % config_uri) + + srp_auth = SRPAuth(self._provider_config) + session_id = srp_auth.get_session_id() + cookies = None + if session_id: + cookies = {"_session_id": session_id} + + res = self._session.get(config_uri, + verify=self._provider_config + .get_ca_cert_path(), + headers=headers, + cookies=cookies) + res.raise_for_status() + + # Not modified + if res.status_code == 304: + logger.debug("Soledad definition has not been modified") + else: + soledad_definition, mtime = get_content(res) + + self._soledad_config.load(data=soledad_definition, mtime=mtime) + self._soledad_config.save(["leap", + "providers", + self._provider_config.get_domain(), + "soledad-service.json"]) + + self._load_and_sync_soledad(srp_auth) def _gen_key(self): """ Generates the key pair if needed, uploads it to the webapp and nickserver - - :return: True if everything is done successfully, False - otherwise - :rtype: bool """ leap_assert(self._provider_config, "We need a provider configuration!") - # XXX Sanitize this address = "%s@%s" % (self._user, self._provider_config.get_domain()) logger.debug("Retrieving key for %s" % (address,)) - genkey_data = { - self.PASSED_KEY: False, - self.ERROR_KEY: "", - self.SOLEDAD_KEY: None, - self.KEYMANAGER_KEY: None - } - + srp_auth = SRPAuth(self._provider_config) + self._keymanager = KeyManager( + address, + "https://%s:6425" % (self._provider_config.get_domain()), + self._soledad, + #token=srp_auth.get_token(), # TODO: enable token usage + session_id=srp_auth.get_session_id(), + ca_cert_path=self._provider_config.get_ca_cert_path(), + api_uri=self._provider_config.get_api_uri(), + api_version=self._provider_config.get_api_version(), + uid=srp_auth.get_uid()) try: - srp_auth = SRPAuth(self._provider_config) - self._keymanager = KeyManager( - address, - "https://nickserver", # TODO: nickserver url, none for now - self._soledad, - token=srp_auth.get_token()) - self._keymanager._fetcher.put = Mock() - try: - self._keymanager.get_key(address, openpgp.OpenPGPKey, - private=True, fetch_remote=False) - except KeyNotFound: - logger.debug( - "Key not found. Generating key for %s" % (address,)) - self._keymanager.gen_key(openpgp.OpenPGPKey) - - logger.debug("Key generated successfully.") - - cookies = None - session_id = srp_auth.get_session_id() - if session_id: - cookies = {"_session_id": session_id} - - key_uri = "%s/%s/users/%s.json" % ( - self._provider_config.get_api_uri(), - self._provider_config.get_api_version(), - srp_auth.get_uid()) - - logger.debug("Uploading public key to %s" % (key_uri,)) - - pubkey = self._keymanager.get_key( - address, openpgp.OpenPGPKey, - private=False, fetch_remote=False) - key_data = { - self.PUBKEY_KEY: pubkey.key_data, - } - - # TODO: check if uploaded before uploading it - key_result = self._session.put(key_uri, - data=key_data, - verify=self._provider_config - .get_ca_cert_path(), - cookies=cookies) - key_result.raise_for_status() - genkey_data[self.PASSED_KEY] = True - genkey_data[self.SOLEDAD_KEY] = self._soledad - genkey_data[self.KEYMANAGER_KEY] = self._keymanager - except Exception as e: - genkey_data[self.PASSED_KEY] = False - genkey_data[self.ERROR_KEY] = "%s" % (e,) - - logger.debug("Emitting gen_key %s" % (genkey_data,)) - self.gen_key.emit(genkey_data) - - return genkey_data[self.PASSED_KEY] + self._keymanager.get_key(address, openpgp.OpenPGPKey, + private=True, fetch_remote=False) + except KeyNotFound: + logger.debug("Key not found. Generating key for %s" % (address,)) + self._keymanager.gen_key(openpgp.OpenPGPKey) + logger.debug("Key generated successfully.") def run_soledad_setup_checks(self, - checker, provider_config, user, password, @@ -273,6 +200,10 @@ class SoledadBootstrapper(QtCore.QObject): :param provider_config: Provider configuration :type provider_config: ProviderConfig + :param user: User's login + :type user: str + :param password: User's password + :type password: str """ leap_assert_type(provider_config, ProviderConfig) @@ -281,7 +212,9 @@ class SoledadBootstrapper(QtCore.QObject): self._user = user self._password = password - checker.add_checks([ - self._download_config, - self._gen_key - ]) + cb_chain = [ + (self._download_config, self.download_config), + (self._gen_key, self.gen_key) + ] + + self.addCallbackChain(cb_chain) diff --git a/src/leap/util/checkerthread.py b/src/leap/util/checkerthread.py deleted file mode 100644 index 02aa333f..00000000 --- a/src/leap/util/checkerthread.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -# checkerthread.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Checker thread -""" - -import logging - -from PySide import QtCore - -from leap.common.check import leap_assert_type - -logger = logging.getLogger(__name__) - - -class CheckerThread(QtCore.QThread): - """ - Generic checker thread that can perform any type of operation as - long as it returns a boolean value that identifies how the - execution went. - """ - - IDLE_SLEEP_INTERVAL = 1 - - def __init__(self): - QtCore.QThread.__init__(self) - - self._checks = [] - self._checks_lock = QtCore.QMutex() - - self._should_quit = False - self._should_quit_lock = QtCore.QMutex() - - def get_should_quit(self): - """ - Returns whether this thread should quit - - :return: True if the thread should terminate itself, Flase otherwise - :rtype: bool - """ - - QtCore.QMutexLocker(self._should_quit_lock) - return self._should_quit - - def set_should_quit(self): - """ - Sets the should_quit flag to True so that this thread - terminates the first chance it gets - """ - QtCore.QMutexLocker(self._should_quit_lock) - self._should_quit = True - - def start(self): - """ - Starts the thread and resets the should_quit flag - """ - with QtCore.QMutexLocker(self._should_quit_lock): - self._should_quit = False - - QtCore.QThread.start(self) - - def add_checks(self, checks): - """ - Adds a list of checks to the ones being executed - - :param checks: check functions to perform - :type checkes: list - """ - with QtCore.QMutexLocker(self._checks_lock): - self._checks += checks - - def run(self): - """ - Main run loop for this thread. Executes the checks. - """ - shouldContinue = False - while True: - if self.get_should_quit(): - logger.debug("Quitting checker thread") - return - checkSomething = False - with QtCore.QMutexLocker(self._checks_lock): - if len(self._checks) > 0: - check = self._checks.pop(0) - shouldContinue = check() - leap_assert_type(shouldContinue, bool) - checkSomething = True - if not shouldContinue: - logger.debug("Something went wrong with the checks, " - "clearing...") - self._checks = [] - checkSomething = False - if not checkSomething: - self.sleep(self.IDLE_SLEEP_INTERVAL) -- cgit v1.2.3 From 655cec1fec89eb30fc17bdc0a5f527e5a91ba5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 4 Jun 2013 12:56:17 -0300 Subject: Remove CheckerThread from SRPAuth Also, some pep8 fixes --- src/leap/crypto/srpauth.py | 60 ------------------------------ src/leap/gui/mainwindow.py | 6 +-- src/leap/services/abstractbootstrapper.py | 1 - src/leap/services/eip/eipbootstrapper.py | 3 +- src/leap/services/mail/smtpbootstrapper.py | 3 +- 5 files changed, 7 insertions(+), 66 deletions(-) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index f1897e1d..28086279 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -29,7 +29,6 @@ from PySide import QtCore, QtGui from leap.common.check import leap_assert from leap.config.providerconfig import ProviderConfig -from leap.util.checkerthread import CheckerThread from leap.util.request_helpers import get_content from leap.common.events import signal as events_signal from leap.common.events import events_pb2 as proto @@ -456,62 +455,3 @@ class SRPAuth(QtCore.QObject): except Exception as e: self.logout_finished.emit(False, "%s" % (e,)) return False - - -if __name__ == "__main__": - import signal - import sys - - from functools import partial - app = QtGui.QApplication(sys.argv) - - if not len(sys.argv) == 3: - print 'Usage: srpauth.py ' - sys.exit(0) - - _user = sys.argv[1] - _pass = sys.argv[2] - - def sigint_handler(*args, **kwargs): - logger.debug('SIGINT catched. shutting down...') - checker = args[0] - checker.set_should_quit() - QtGui.QApplication.quit() - - def signal_tester(d): - print d - - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - checker = CheckerThread() - - sigint = partial(sigint_handler, checker) - signal.signal(signal.SIGINT, sigint) - - timer = QtCore.QTimer() - timer.start(500) - timer.timeout.connect(lambda: None) - app.connect(app, QtCore.SIGNAL("aboutToQuit()"), - checker.set_should_quit) - w = QtGui.QWidget() - w.resize(100, 100) - w.show() - - checker.start() - - provider = ProviderConfig() - if provider.load("leap/providers/bitmask.net/provider.json"): - auth = SRPAuth(provider) - auth_instantiated = partial(auth.authenticate, _user, _pass) - - checker.add_checks([auth_instantiated, auth.logout]) - - sys.exit(app.exec_()) diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 25478aa1..89f06a1c 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -815,12 +815,12 @@ class MainWindow(QtGui.QMainWindow): True) else: if self._enabled_services.count(self.MX_SERVICE) > 0: - pass # TODO: show MX status + pass # TODO: show MX status #self._set_eip_status(self.tr("%s does not support MX") % - # (self._provider_config.get_domain(),), + # (self._provider_config.get_domain(),), # error=True) else: - pass # TODO: show MX status + pass # TODO: show MX status #self._set_eip_status(self.tr("MX is disabled")) def _smtp_bootstrapped_stage(self, data): diff --git a/src/leap/services/abstractbootstrapper.py b/src/leap/services/abstractbootstrapper.py index bce03e6b..7bebdc15 100644 --- a/src/leap/services/abstractbootstrapper.py +++ b/src/leap/services/abstractbootstrapper.py @@ -152,4 +152,3 @@ class AbstractBootstrapper(QtCore.QObject): d.addErrback(self._errback, signal=sig) d.addCallback(self._gui_notify, signal=sig) d.addErrback(self._gui_errback) - diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 7216bb80..51c3dab4 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -173,7 +173,8 @@ class EIPBootstrapper(AbstractBootstrapper): cb_chain = [ (self._download_config, self.download_config), - (self._download_client_certificates, self.download_client_certificate) + (self._download_client_certificates, \ + self.download_client_certificate) ] self.addCallbackChain(cb_chain) diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py index 64bf3153..ea480c6d 100644 --- a/src/leap/services/mail/smtpbootstrapper.py +++ b/src/leap/services/mail/smtpbootstrapper.py @@ -96,7 +96,8 @@ class SMTPBootstrapper(AbstractBootstrapper): logger.debug("SMTP definition has not been modified") self._smtp_config.load(os.path.join("leap", "providers", - self._provider_config.get_domain(), + self._provider_config.\ + get_domain(), "smtp-service.json")) else: smtp_definition, mtime = get_content(res) -- cgit v1.2.3 From cc247f63441a8d434bc509ea2764f7969ad47fec Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 5 Jun 2013 10:12:22 -0300 Subject: Improve wizard for registration process --- src/leap/gui/ui/wizard.ui | 32 +++++++++++++++++++------------- src/leap/gui/wizard.py | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui index 87d04194..96cf4621 100644 --- a/src/leap/gui/ui/wizard.ui +++ b/src/leap/gui/ui/wizard.ui @@ -597,6 +597,12 @@ 4 + + QLayout::SetDefaultConstraint + + + 4 + @@ -646,19 +652,6 @@ - - - - - - - Qt::AutoText - - - Qt::AlignCenter - - - @@ -692,6 +685,19 @@ + + + + + + + Qt::AutoText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 552ace50..405752ef 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -243,15 +243,42 @@ class Wizard(QtGui.QWizard): else: self.ui.btnRegister.setEnabled(True) + def _set_registration_fields_visibility(self, visible): + """ + This method hides the username and password labels and inputboxes. + + :param visible: sets the visibility of the widgets + True: widgets are visible or False: are not + :type visible: bool + """ + # username and password inputs + self.ui.lblUser.setVisible(visible) + self.ui.lblPassword.setVisible(visible) + self.ui.lblPassword2.setVisible(visible) + + # username and password labels + self.ui.label_15.setVisible(visible) + self.ui.label_16.setVisible(visible) + self.ui.label_17.setVisible(visible) + + # register button + self.ui.btnRegister.setVisible(visible) + def _registration_finished(self, ok, req): if ok: - self._set_register_status(self.tr("" - "User registration OK. " - "")) + user_domain = self._username + "@" + self._domain + message = "

" + message += self.tr("User %s successfully registered.") % ( + user_domain, ) + message += "

" + self._set_register_status(message) + self.ui.lblPassword2.clearFocus() + self._set_registration_fields_visibility(False) self.ui.chkRemember.setEnabled(True) self.page(self.REGISTER_USER_PAGE).set_completed() + self.button(QtGui.QWizard.BackButton).setEnabled(False) else: old_username = self._username self._username = None -- cgit v1.2.3 From e6b055fc2b054c1ab12e2554e9dbe73f47c647c0 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 5 Jun 2013 10:47:38 -0300 Subject: Bugfix str format in checker --- src/leap/util/requirement_checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/util/requirement_checker.py b/src/leap/util/requirement_checker.py index 3538f122..1d9b9923 100644 --- a/src/leap/util/requirement_checker.py +++ b/src/leap/util/requirement_checker.py @@ -58,7 +58,7 @@ def get_requirements(): with resource_stream(dist_name, requires_file_name) as stream: requirements = [line.strip() for line in stream] except Exception, e: - logger.error("Requirements file not found. %e", (e, )) + logger.error("Requirements file not found. %r" % (e, )) return requirements -- cgit v1.2.3 From 5b5eef87e8eb41916f2f5657287ac1ad8f0bbfeb Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 5 Jun 2013 11:52:37 -0300 Subject: Select the configured domain in the providers box --- src/leap/gui/mainwindow.py | 6 ++++++ src/leap/gui/wizard.py | 3 +++ 2 files changed, 9 insertions(+) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 89f06a1c..f9e088a7 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -377,6 +377,12 @@ class MainWindow(QtGui.QMainWindow): if self._wizard: possible_username = self._wizard.get_username() possible_password = self._wizard.get_password() + + # select the configured provider in the combo box + domain = self._wizard.get_domain() + provider_index = self.ui.cmbProviders.findText(domain) + self.ui.cmbProviders.setCurrentIndex(provider_index) + self.ui.chkRemember.setChecked(self._wizard.get_remember()) self._enabled_services = list(self._wizard.get_services()) self._settings.set_enabled_services( diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 405752ef..ea4474ae 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -154,6 +154,9 @@ class Wizard(QtGui.QWizard): self.page(self.FINISH_PAGE).setButtonText( QtGui.QWizard.FinishButton, self.tr("Connect")) + def get_domain(self): + return self._domain + def get_username(self): return self._username -- cgit v1.2.3 From 2456b456ae72c7c56c03aaf131aef7a2e7ad011b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 5 Jun 2013 11:58:17 -0300 Subject: Only show question mark when the previous check passed --- src/leap/gui/wizard.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 405752ef..3cf1765b 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -389,10 +389,11 @@ class Wizard(QtGui.QWizard): if not passed: status = self.tr("Non-existent " "provider") + else: + self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON) self.ui.lblProviderSelectStatus.setText(status) self.ui.btnCheck.setEnabled(not passed) self.ui.lnProvider.setEnabled(not passed) - self.ui.lblHTTPS.setPixmap(self.QUESTION_ICON) def _https_connection(self, data): """ @@ -408,9 +409,10 @@ class Wizard(QtGui.QWizard): status = self.tr("%s") \ % (data[self._provider_bootstrapper.ERROR_KEY]) self.ui.lblProviderSelectStatus.setText(status) + else: + self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) self.ui.btnCheck.setEnabled(not passed) self.ui.lnProvider.setEnabled(not passed) - self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) def _download_provider_info(self, data): """ @@ -451,7 +453,9 @@ class Wizard(QtGui.QWizard): Sets the status for the download of the CA certificate check """ self._complete_task(data, self.ui.lblDownloadCaCert) - self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON) + passed = data[self._provider_bootstrapper.PASSED_KEY] + if passed: + self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON) def _check_ca_fingerprint(self, data): """ @@ -461,7 +465,9 @@ class Wizard(QtGui.QWizard): Sets the status for the CA fingerprint check """ self._complete_task(data, self.ui.lblCheckCaFpr) - self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON) + passed = data[self._provider_bootstrapper.PASSED_KEY] + if passed: + self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON) def _check_api_certificate(self, data): """ -- cgit v1.2.3 From 40c1190ad556aee33d1b90a9c234b36ad0759861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 6 Jun 2013 11:39:57 -0300 Subject: Make the login process more granular with defers --- src/leap/crypto/srpauth.py | 73 ++++++++++++++++++++++++++++------------------ src/leap/gui/mainwindow.py | 11 ++++--- 2 files changed, 51 insertions(+), 33 deletions(-) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index 28086279..3e47f679 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -25,10 +25,10 @@ import json #this error is raised from requests from simplejson.decoder import JSONDecodeError -from PySide import QtCore, QtGui +from PySide import QtCore +from twisted.internet import threads from leap.common.check import leap_assert -from leap.config.providerconfig import ProviderConfig from leap.util.request_helpers import get_content from leap.common.events import signal as events_signal from leap.common.events import events_pb2 as proto @@ -124,13 +124,15 @@ class SRPAuth(QtCore.QObject): self._srp_a = A - def _start_authentication(self, username, password): + def _start_authentication(self, _, username, password): """ Sends the first request for authentication to retrieve the salt and B parameter Might raise SRPAuthenticationError + :param _: IGNORED, output from the previous callback (None) + :type _: IGNORED :param username: username to login :type username: str :param password: password for the username @@ -187,17 +189,15 @@ class SRPAuth(QtCore.QObject): return salt, B - def _process_challenge(self, salt, B, username): + def _process_challenge(self, salt_B, username): """ Given the salt and B processes the auth challenge and generates the M2 parameter Might throw SRPAuthenticationError - :param salt: salt for the username - :type salt: str - :param B: B SRP parameter - :type B: str + :param salt_B: salt and B parameters for the username + :type salt_B: tuple :param username: username for this session :type username: str @@ -206,6 +206,7 @@ class SRPAuth(QtCore.QObject): """ logger.debug("Processing challenge...") try: + salt, B = salt_B unhex_salt = self._safe_unhexlify(salt) unhex_B = self._safe_unhexlify(B) except TypeError as e: @@ -318,17 +319,22 @@ class SRPAuth(QtCore.QObject): :type username: str :param password: password for this user :type password: str + + :returns: A defer on a different thread + :rtype: twisted.internet.defer.Deferred """ leap_assert(self.get_session_id() is None, "Already logged in") - self._authentication_preprocessing(username, password) - salt, B = self._start_authentication(username, password) - M2 = self._process_challenge(salt, B, username) + d = threads.deferToThread(self._authentication_preprocessing, + username=username, + password=password) - self._verify_session(M2) + d.addCallback(self._start_authentication, username=username, + password=password) + d.addCallback(self._process_challenge, username=username) + d.addCallback(self._verify_session) - leap_assert(self.get_session_id(), "Something went wrong because" - " we don't have the auth cookie afterwards") + return d def logout(self): """ @@ -388,10 +394,6 @@ class SRPAuth(QtCore.QObject): authentication_finished = QtCore.Signal(bool, str) logout_finished = QtCore.Signal(bool, str) - DO_NOTHING = 0 - DO_LOGIN = 1 - DO_LOGOUT = 2 - def __init__(self, provider_config): """ Creates a singleton instance if needed @@ -406,8 +408,6 @@ class SRPAuth(QtCore.QObject): # Store instance reference as the only member in the handle self.__dict__['_SRPAuth__instance'] = SRPAuth.__instance - self._should_login = self.DO_NOTHING - self._should_login_lock = QtCore.QMutex() self._username = None self._password = None @@ -423,16 +423,31 @@ class SRPAuth(QtCore.QObject): :type password: str """ - try: - self.__instance.authenticate(username, password) + d = self.__instance.authenticate(username, password) + d.addCallback(self._gui_notify) + d.addErrback(self._errback) + return d - logger.debug("Successful login!") - self.authentication_finished.emit(True, self.tr("Succeeded")) - return True - except Exception as e: - logger.error("Error logging in %s" % (e,)) - self.authentication_finished.emit(False, "%s" % (e,)) - return False + def _gui_notify(self, _): + """ + Callback that notifies the UI with the proper signal. + + :param _: IGNORED, output from the previous callback (None) + :type _: IGNORED + """ + logger.debug("Successful login!") + self.authentication_finished.emit(True, self.tr("Succeeded")) + + def _errback(self, failure): + """ + General errback for the whole login process. Will notify the + UI with the proper signal. + + :param failure: Failure object captured from a callback. + :type failure: twisted.python.failure.Failure + """ + logger.error("Error logging in %s" % (failure,)) + self.authentication_finished.emit(False, "%s" % (failure,)) def get_session_id(self): return self.__instance.get_session_id() diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index f9e088a7..c892cadd 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -259,6 +259,8 @@ class MainWindow(QtGui.QMainWindow): self._soledad = None self._keymanager = None + self._login_defer = None + self._smtp_config = SMTPConfig() if self._first_run(): @@ -733,10 +735,7 @@ class MainWindow(QtGui.QMainWindow): self._srp_auth.logout_finished.connect( self._done_logging_out) - auth_partial = partial(self._srp_auth.authenticate, - username, - password) - threads.deferToThread(auth_partial) + self._login_defer = self._srp_auth.authenticate(username,password) else: self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) self._login_set_enabled(True) @@ -756,6 +755,7 @@ class MainWindow(QtGui.QMainWindow): # "Succeeded" message and then we switch to the EIP status # panel QtCore.QTimer.singleShot(1000, self._switch_to_status) + self._login_defer = None else: self._login_set_enabled(True) @@ -1172,6 +1172,9 @@ class MainWindow(QtGui.QMainWindow): self.close() + if self._login_defer: + self._login_defer.cancel() + if self._quit_callback: self._quit_callback() logger.debug('Bye.') -- cgit v1.2.3 From 3e4ccef05a02c7b717b392acbb6201b2f8858a91 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 7 Jun 2013 12:10:08 -0300 Subject: Remember provider along with username and password --- src/leap/gui/mainwindow.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index f9e088a7..96c50201 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -405,8 +405,21 @@ class MainWindow(QtGui.QMainWindow): saved_user = self._settings.get_user() auto_login = self._settings.get_autologin() + try: + username, domain = saved_user.split('@') + except (ValueError, AttributeError) as e: + # if the saved_user does not contain an '@' or its None + logger.error('Username@provider malformed. %r' % (e, )) + saved_user = None + if saved_user is not None: - self.ui.lnUser.setText(saved_user) + # fill the username + self.ui.lnUser.setText(username) + + # select the configured provider in the combo box + provider_index = self.ui.cmbProviders.findText(domain) + self.ui.cmbProviders.setCurrentIndex(provider_index) + self.ui.chkRemember.setChecked(True) self.ui.chkAutoLogin.setEnabled(self.ui.chkRemember .isEnabled()) @@ -699,13 +712,16 @@ class MainWindow(QtGui.QMainWindow): self._login_set_enabled(False) if self.ui.chkRemember.isChecked(): + # in the keyring and in the settings + # we store the value 'usename@provider' + username_domain = (username + '@' + provider).encode("utf8") try: keyring.set_password(self.KEYRING_KEY, - username.encode("utf8"), + username_domain, password.encode("utf8")) # Only save the username if it was saved correctly in # the keyring - self._settings.set_user(username) + self._settings.set_user(username_domain) except Exception as e: logger.error("Problem saving data to keyring. %r" % (e,)) -- cgit v1.2.3 From 6a01913bffc10a96c6c3acfa6b8b410d6eb27661 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 7 Jun 2013 15:05:01 -0300 Subject: Wizard ui improvements Hide 'remember' checkbox until the registration succeeds. Use word wrapping to the successful registration message, to be more long-username-friendly. --- src/leap/gui/ui/wizard.ui | 3 +++ src/leap/gui/wizard.py | 4 ++++ 2 files changed, 7 insertions(+) (limited to 'src') diff --git a/src/leap/gui/ui/wizard.ui b/src/leap/gui/ui/wizard.ui index 96cf4621..4b9cab1c 100644 --- a/src/leap/gui/ui/wizard.ui +++ b/src/leap/gui/ui/wizard.ui @@ -696,6 +696,9 @@ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + true +
diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 8c5ea2a0..d03427db 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -278,6 +278,9 @@ class Wizard(QtGui.QWizard): self.ui.lblPassword2.clearFocus() self._set_registration_fields_visibility(False) + + # Allow the user to remember his password + self.ui.chkRemember.setVisible(True) self.ui.chkRemember.setEnabled(True) self.page(self.REGISTER_USER_PAGE).set_completed() @@ -575,6 +578,7 @@ class Wizard(QtGui.QWizard): "%s") % (self._provider_config .get_name(),)) + self.ui.chkRemember.setVisible(False) if pageId == self.SERVICES_PAGE: self._populate_services() -- cgit v1.2.3 From 20c78001df4cabccd062004e1a6a4f323ca594b8 Mon Sep 17 00:00:00 2001 From: kali Date: Tue, 11 Jun 2013 01:17:13 +0900 Subject: pep8 --- src/leap/gui/mainwindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index c892cadd..f8175b7f 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -735,7 +735,7 @@ class MainWindow(QtGui.QMainWindow): self._srp_auth.logout_finished.connect( self._done_logging_out) - self._login_defer = self._srp_auth.authenticate(username,password) + self._login_defer = self._srp_auth.authenticate(username, password) else: self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) self._login_set_enabled(True) -- cgit v1.2.3 From 364d31999dbc488b5f99d81a0480c67ef248a515 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 10 Jun 2013 15:39:20 -0300 Subject: Check provider api version for compatibility --- src/leap/provider/__init__.py | 0 src/leap/provider/supportedapis.py | 38 +++++++++++++++++++++++++++ src/leap/services/eip/providerbootstrapper.py | 21 +++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 src/leap/provider/__init__.py create mode 100644 src/leap/provider/supportedapis.py (limited to 'src') diff --git a/src/leap/provider/__init__.py b/src/leap/provider/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/provider/supportedapis.py b/src/leap/provider/supportedapis.py new file mode 100644 index 00000000..3e650ba2 --- /dev/null +++ b/src/leap/provider/supportedapis.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# supportedapis.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +API Support check. +""" + + +class SupportedAPIs(object): + """ + Class responsible of checking for API compatibility. + """ + SUPPORTED_APIS = ["1"] + + @classmethod + def supports(self, api_version): + """ + :param api_version: the version number of the api that we need to check + :type api_version: str + + :returns: if that version is supported or not. + :return type: bool + """ + return api_version in self.SUPPORTED_APIS diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index 1339e086..e099eee7 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -32,10 +32,19 @@ from leap.common.check import leap_assert, leap_assert_type from leap.config.providerconfig import ProviderConfig from leap.util.request_helpers import get_content from leap.services.abstractbootstrapper import AbstractBootstrapper +from leap.provider.supportedapis import SupportedAPIs + logger = logging.getLogger(__name__) +class UnsupportedProviderAPI(Exception): + """ + Raised when attempting to use a provider with an incompatible API. + """ + pass + + class ProviderBootstrapper(AbstractBootstrapper): """ Given a provider URL performs a series of checks and emits signals @@ -142,6 +151,18 @@ class ProviderBootstrapper(AbstractBootstrapper): self._domain, "provider.json"]) + api_version = provider_config.get_api_version() + if SupportedAPIs.supports(api_version): + logger.debug("Provider definition has been modified") + else: + api_supported = ', '.join(self._supported_api_versions) + error = ('Unsupported provider API version. ' + 'Supported versions are: {}. ' + 'Found: {}.').format(api_supported, api_version) + + logger.error(error) + raise UnsupportedProviderAPI(error) + def run_provider_select_checks(self, domain, download_if_needed=False): """ Populates the check queue. -- cgit v1.2.3 From 029b4c2ac07fab41dbed9ab90e04e477938c3c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 11 Jun 2013 12:09:30 -0300 Subject: Merge systray icons Also, catch a possible problem with the login answer from the webapp and display a proper message --- src/leap/crypto/srpauth.py | 11 ++++++++--- src/leap/gui/mainwindow.py | 38 ++++++++++++++------------------------ 2 files changed, 22 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index 28086279..82525d7f 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -254,9 +254,14 @@ class SRPAuth(QtCore.QObject): (auth_result.status_code,)) json_content = json.loads(content) - M2 = json_content.get("M2", None) - uid = json_content.get("id", None) - token = json_content.get("token", None) + + try: + M2 = json_content.get("M2", None) + uid = json_content.get("id", None) + token = json_content.get("token", None) + except Exception as e: + logger.error(e) + raise Exception("Something went wrong with the login") events_signal(proto.CLIENT_UID, content=uid) diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index f9e088a7..fa49c55d 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -217,12 +217,11 @@ class MainWindow(QtGui.QMainWindow): self._really_quit = False self._systray = None - self._vpn_systray = None - self._action_eip_status = QtGui.QAction(self.tr("Encryption is OFF"), + self._action_eip_status = QtGui.QAction(self.tr("Encrypted internet is OFF"), self) self._action_eip_status.setEnabled(False) - self._action_eip_startstop = QtGui.QAction(self.tr("Stop"), self) + self._action_eip_startstop = QtGui.QAction(self.tr("Turn encryption ON"), self) self._action_eip_startstop.triggered.connect( self._stop_eip) self._action_eip_write = QtGui.QAction( @@ -234,7 +233,7 @@ class MainWindow(QtGui.QMainWindow): "%12.2f Kb" % (0.0,), self) self._action_eip_read.setEnabled(False) - self._action_visible = QtGui.QAction(self.tr("Hide"), self) + self._action_visible = QtGui.QAction(self.tr("Hide Main Window"), self) self._action_visible.triggered.connect(self._toggle_visible) self._enabled_services = [] @@ -440,22 +439,15 @@ class MainWindow(QtGui.QMainWindow): systrayMenu.addAction(self.ui.action_sign_out) systrayMenu.addSeparator() systrayMenu.addAction(self.ui.action_quit) + systrayMenu.addSeparator() + systrayMenu.addAction(self._action_eip_status) + systrayMenu.addAction(self._action_eip_startstop) self._systray = QtGui.QSystemTrayIcon(self) self._systray.setContextMenu(systrayMenu) - self._systray.setIcon(QtGui.QIcon(self.LOGGED_OUT_ICON)) + self._systray.setIcon(QtGui.QIcon(self.ERROR_ICON)) self._systray.setVisible(True) self._systray.activated.connect(self._toggle_visible) - vpn_systrayMenu = QtGui.QMenu(self) - vpn_systrayMenu.addAction(self._action_eip_status) - vpn_systrayMenu.addAction(self._action_eip_startstop) - vpn_systrayMenu.addAction(self._action_eip_read) - vpn_systrayMenu.addAction(self._action_eip_write) - self._vpn_systray = QtGui.QSystemTrayIcon(self) - self._vpn_systray.setContextMenu(vpn_systrayMenu) - self._vpn_systray.setIcon(QtGui.QIcon(self.ERROR_ICON)) - self._vpn_systray.setVisible(False) - def _toggle_visible(self, reason=None): """ SLOT @@ -599,7 +591,7 @@ class MainWindow(QtGui.QMainWindow): :param status: status message :type status: str """ - self._vpn_systray.setToolTip(status) + self._systray.setToolTip(status) if error: status = "%s" % (status,) self.ui.lblEIPStatus.setText(status) @@ -765,7 +757,6 @@ class MainWindow(QtGui.QMainWindow): triggers the eip bootstrapping """ self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) - self._systray.setIcon(self.LOGGED_IN_ICON) self._soledad_bootstrapper.run_soledad_setup_checks( self._provider_config, @@ -895,11 +886,11 @@ class MainWindow(QtGui.QMainWindow): providerconfig=self._provider_config, socket_host=host, socket_port=port) - self.ui.btnEipStartStop.setText(self.tr("Stop EIP")) + self.ui.btnEipStartStop.setText(self.tr("Turn Encryption OFF")) self.ui.btnEipStartStop.disconnect(self) self.ui.btnEipStartStop.clicked.connect( self._stop_eip) - self._action_eip_startstop.setText(self.tr("Stop")) + self._action_eip_startstop.setText(self.tr("Turn Encryption OFF")) self._action_eip_startstop.disconnect(self) self._action_eip_startstop.triggered.connect( self._stop_eip) @@ -928,11 +919,11 @@ class MainWindow(QtGui.QMainWindow): self._vpn.set_should_quit() self._set_eip_status(self.tr("EIP has stopped")) self._set_eip_status_icon("error") - self.ui.btnEipStartStop.setText(self.tr("Start EIP")) + self.ui.btnEipStartStop.setText(self.tr("Turn Encryption ON")) self.ui.btnEipStartStop.disconnect(self) self.ui.btnEipStartStop.clicked.connect( self._start_eip) - self._action_eip_startstop.setText(self.tr("Start")) + self._action_eip_startstop.setText(self.tr("Turn Encryption ON")) self._action_eip_startstop.disconnect(self) self._action_eip_startstop.triggered.connect( self._start_eip) @@ -948,7 +939,6 @@ class MainWindow(QtGui.QMainWindow): if self._provider_config.provides_eip() and \ self._enabled_services.count(self.OPENVPN_SERVICE) > 0: - self._vpn_systray.setVisible(True) self._eip_bootstrapper.run_eip_setup_checks( self._provider_config, download_if_needed=True) @@ -973,12 +963,13 @@ class MainWindow(QtGui.QMainWindow): if status in ("WAIT", "AUTH", "GET_CONFIG", "RECONNECTING", "ASSIGN_IP"): selected_pixmap = self.CONNECTING_ICON + tray_message = self.tr("Turning Encryption ON") elif status in ("CONNECTED"): tray_message = self.tr("Encryption is ON") selected_pixmap = self.CONNECTED_ICON self.ui.lblVPNStatusIcon.setPixmap(selected_pixmap) - self._vpn_systray.setIcon(QtGui.QIcon(selected_pixmap)) + self._systray.setIcon(QtGui.QIcon(selected_pixmap)) self._action_eip_status.setText(tray_message) def _update_vpn_state(self, data): @@ -1079,7 +1070,6 @@ class MainWindow(QtGui.QMainWindow): Switches the stackedWidget back to the login stage after logging out """ - self._systray.setIcon(self.LOGGED_OUT_ICON) self.ui.action_sign_out.setEnabled(False) self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) self.ui.lnPassword.setText("") -- cgit v1.2.3 From 56f90d03b40e2a138a8b653de408f207ad562a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 11 Jun 2013 12:37:29 -0300 Subject: Save the provider used for EIP as the default provider. --- src/leap/config/leapsettings.py | 19 +++++++++++++++++++ src/leap/gui/mainwindow.py | 12 ++++++++++++ 2 files changed, 31 insertions(+) (limited to 'src') diff --git a/src/leap/config/leapsettings.py b/src/leap/config/leapsettings.py index 59a0a16d..006be851 100644 --- a/src/leap/config/leapsettings.py +++ b/src/leap/config/leapsettings.py @@ -65,6 +65,7 @@ class LeapSettings(object): AUTOLOGIN_KEY = "AutoLogin" PROPERPROVIDER_KEY = "ProperProvider" REMEMBER_KEY = "RememberUserAndPass" + DEFAULTPROVIDER_KEY = "DefaultProvider" def __init__(self, standalone=False): """ @@ -230,3 +231,21 @@ class LeapSettings(object): """ leap_assert_type(properprovider, bool) self._settings.setValue(self.PROPERPROVIDER_KEY, properprovider) + + def get_defaultprovider(self): + """ + Returns the default provider to be used for autostarting EIP + + :rtype: str or None + """ + return self._settings.value(self.DEFAULTPROVIDER_KEY, None) + + def set_defaultprovider(self, provider): + """ + Sets the default provider to be used for autostarting EIP + + :param provider: provider to use + :type provider: str + """ + leap_assert(len(provider) > 0, "We cannot save an empty provider") + self._settings.setValue(self.DEFAULTPROVIDER_KEY, provider) diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index f9e088a7..fdcacb56 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -889,12 +889,24 @@ class MainWindow(QtGui.QMainWindow): return host, port def _start_eip(self): + """ + SLOT + TRIGGERS: + self.ui.btnEipStartStop.clicked + self._action_eip_startstop.triggered + or called from _finish_eip_bootstrap + + Starts EIP + """ try: host, port = self._get_socket_host() self._vpn.start(eipconfig=self._eip_config, providerconfig=self._provider_config, socket_host=host, socket_port=port) + + self._settings.set_defaultprovider(self._provider_config.get_domain()) + self.ui.btnEipStartStop.setText(self.tr("Stop EIP")) self.ui.btnEipStartStop.disconnect(self) self.ui.btnEipStartStop.clicked.connect( -- cgit v1.2.3 From dbb873016042b213dd9cd84a59aec0c0a2383691 Mon Sep 17 00:00:00 2001 From: kali Date: Wed, 5 Jun 2013 05:18:39 +0900 Subject: use twisted processProtocol instead of QProcess to drive openvpn --- src/leap/gui/mainwindow.py | 33 +- src/leap/services/eip/vpn.py | 465 ------------------------ src/leap/services/eip/vpnlaunchers.py | 1 + src/leap/services/eip/vpnprocess.py | 592 +++++++++++++++++++++++++++++++ src/leap/services/mail/smtpspec.py | 4 +- src/leap/services/soledad/soledadspec.py | 4 +- 6 files changed, 615 insertions(+), 484 deletions(-) delete mode 100644 src/leap/services/eip/vpn.py create mode 100644 src/leap/services/eip/vpnprocess.py (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 89f06a1c..2cad6df3 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -44,7 +44,8 @@ from leap.services.soledad.soledadbootstrapper import SoledadBootstrapper from leap.services.mail.smtpbootstrapper import SMTPBootstrapper from leap.platform_init import IS_MAC, IS_WIN from leap.platform_init.initializers import init_platform -from leap.services.eip.vpn import VPN +from leap.services.eip.vpnprocess import VPN, VPNManager + from leap.services.eip.vpnlaunchers import (VPNLauncherException, OpenVPNNotFoundException, EIPNoPkexecAvailable, @@ -196,9 +197,9 @@ class MainWindow(QtGui.QMainWindow): self._smtp_bootstrapped_stage) self._vpn = VPN() - self._vpn.state_changed.connect(self._update_vpn_state) - self._vpn.status_changed.connect(self._update_vpn_status) - self._vpn.process_finished.connect( + self._vpn.qtsigs.state_changed.connect(self._update_vpn_state) + self._vpn.qtsigs.status_changed.connect(self._update_vpn_status) + self._vpn.qtsigs.process_finished.connect( self._eip_finished) self.ui.chkRemember.stateChanged.connect( @@ -816,8 +817,9 @@ class MainWindow(QtGui.QMainWindow): else: if self._enabled_services.count(self.MX_SERVICE) > 0: pass # TODO: show MX status - #self._set_eip_status(self.tr("%s does not support MX") % - # (self._provider_config.get_domain(),), + #self._set_eip_status( + # self.tr("%s does not support MX") % + # (self._provider_config.get_domain(),), # error=True) else: pass # TODO: show MX status @@ -852,8 +854,6 @@ class MainWindow(QtGui.QMainWindow): # TODO: pick local smtp port in a better way # TODO: Make the encrypted_only configurable - # TODO: Remove mocking!!! - self._keymanager.fetch_keys_from_server = Mock(return_value=[]) from leap.mail.smtp import setup_smtp_relay setup_smtp_relay(port=1234, keymanager=self._keymanager, @@ -919,7 +919,7 @@ class MainWindow(QtGui.QMainWindow): self.ui.btnEipStartStop.setEnabled(True) def _stop_eip(self): - self._vpn.set_should_quit() + self._vpn.terminate() self._set_eip_status(self.tr("EIP has stopped")) self._set_eip_status_icon("error") self.ui.btnEipStartStop.setText(self.tr("Start EIP")) @@ -983,7 +983,7 @@ class MainWindow(QtGui.QMainWindow): Updates the displayed VPN state based on the data provided by the VPN thread """ - status = data[self._vpn.STATUS_STEP_KEY] + status = data[VPNManager.STATUS_STEP_KEY] self._set_eip_status_icon(status) if status == "AUTH": self._set_eip_status(self.tr("VPN: Authenticating...")) @@ -1014,12 +1014,12 @@ class MainWindow(QtGui.QMainWindow): Updates the download/upload labels based on the data provided by the VPN thread """ - upload = float(data[self._vpn.TUNTAP_WRITE_KEY]) + upload = float(data[VPNManager.TUNTAP_WRITE_KEY]) upload = upload / 1000.0 upload_str = "%12.2f Kb" % (upload,) self.ui.lblUpload.setText(upload_str) self._action_eip_write.setText(upload_str) - download = float(data[self._vpn.TUNTAP_READ_KEY]) + download = float(data[VPNManager.TUNTAP_READ_KEY]) download = download / 1000.0 download_str = "%12.2f Kb" % (download,) self.ui.lblDownload.setText(download_str) @@ -1079,7 +1079,7 @@ class MainWindow(QtGui.QMainWindow): self.ui.lnPassword.setText("") self._login_set_enabled(True) self._set_status("") - self._vpn.set_should_quit() + self._vpn.terminate() def _intermediate_stage(self, data): """ @@ -1147,8 +1147,11 @@ class MainWindow(QtGui.QMainWindow): Should be called from the quit function. """ logger.debug('About to quit, doing cleanup...') - self._vpn.set_should_quit() - self._vpn.wait() + + logger.debug('Killing vpn') + self._vpn.terminate() + + logger.debug('Cleaning pidfiles') self._cleanup_pidfiles() def quit(self): diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py deleted file mode 100644 index af1febe6..00000000 --- a/src/leap/services/eip/vpn.py +++ /dev/null @@ -1,465 +0,0 @@ -# -*- coding: utf-8 -*- -# vpn.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -VPN launcher and watcher thread -""" - -import logging -import sys -import psutil - -from PySide import QtCore, QtGui -from functools import partial - -from leap.common.check import leap_assert, leap_assert_type -from leap.config.providerconfig import ProviderConfig -from leap.services.eip.vpnlaunchers import get_platform_launcher -from leap.services.eip.eipconfig import EIPConfig -from leap.services.eip.udstelnet import UDSTelnet - -logger = logging.getLogger(__name__) - - -# TODO: abstract the thread that can be asked to quit to another -# generic class that Fetcher and VPN inherit from -class VPN(QtCore.QThread): - """ - VPN launcher and watcher thread. It will emit signals based on - different events caught by the management interface - """ - - state_changed = QtCore.Signal(dict) - status_changed = QtCore.Signal(dict) - - process_finished = QtCore.Signal(int) - - CONNECTION_RETRY_TIME = 1000 - POLL_TIME = 100 - - TS_KEY = "ts" - STATUS_STEP_KEY = "status_step" - OK_KEY = "ok" - IP_KEY = "ip" - REMOTE_KEY = "remote" - - TUNTAP_READ_KEY = "tun_tap_read" - TUNTAP_WRITE_KEY = "tun_tap_write" - TCPUDP_READ_KEY = "tcp_udp_read" - TCPUDP_WRITE_KEY = "tcp_udp_write" - AUTH_READ_KEY = "auth_read" - - ALREADY_RUNNING_STEP = "ALREADYRUNNING" - - def __init__(self): - QtCore.QThread.__init__(self) - - self._should_quit = False - self._should_quit_lock = QtCore.QMutex() - - self._launcher = get_platform_launcher() - self._subp = None - - self._tn = None - self._host = None - self._port = None - - self._last_state = None - self._last_status = None - - def get_should_quit(self): - """ - Returns wether this thread should quit - - :rtype: bool - :return: True if the thread should terminate itself, Flase otherwise - """ - QtCore.QMutexLocker(self._should_quit_lock) - return self._should_quit - - def set_should_quit(self): - """ - Sets the should_quit flag to True so that this thread - terminates the first chance it gets. - Also terminates the VPN process and the connection to it - """ - QtCore.QMutexLocker(self._should_quit_lock) - self._should_quit = True - if self._tn is None or self._subp is None: - return - - try: - self._send_command("signal SIGTERM") - self._tn.close() - self._subp.terminate() - self._subp.waitForFinished() - except Exception as e: - logger.debug("Could not terminate process, trying command " + - "signal SIGNINT: %r" % (e,)) - finally: - self._tn = None - - def start(self, eipconfig, providerconfig, socket_host, socket_port): - """ - Launches OpenVPN and starts the thread to watch its output - - :param eipconfig: eip configuration object - :type eipconfig: EIPConfig - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - """ - leap_assert(eipconfig, "We need an eip config") - leap_assert_type(eipconfig, EIPConfig) - leap_assert(providerconfig, "We need a provider config") - leap_assert_type(providerconfig, ProviderConfig) - leap_assert(not self.isRunning(), "Starting process more than once!") - - logger.debug("Starting VPN...") - - with QtCore.QMutexLocker(self._should_quit_lock): - self._should_quit = False - - if not self._stop_if_already_running(): - # We send a fake state - state_dict = { - self.TS_KEY: "", - self.STATUS_STEP_KEY: self.ALREADY_RUNNING_STEP, - self.OK_KEY: "", - self.IP_KEY: "", - self.REMOTE_KEY: "" - } - - self.state_changed.emit(state_dict) - # And just return, don't start the process - return - - command = self._launcher.get_vpn_command(eipconfig=eipconfig, - providerconfig=providerconfig, - socket_host=socket_host, - socket_port=socket_port) - try: - env = QtCore.QProcessEnvironment.systemEnvironment() - for key, val in self._launcher.get_vpn_env(providerconfig).items(): - env.insert(key, val) - - self._subp = QtCore.QProcess() - - self._subp.setProcessEnvironment(env) - - self._subp.finished.connect(self.process_finished) - self._subp.finished.connect(self._dump_exitinfo) - self._subp.start(command[:1][0], command[1:]) - logger.debug("Waiting for started...") - self._subp.waitForStarted() - logger.debug("Started!") - - self._host = socket_host - self._port = socket_port - - self._started = True - - QtCore.QThread.start(self) - except Exception as e: - logger.warning("Something went wrong while starting OpenVPN: %r" % - (e,)) - - def _dump_exitinfo(self): - """ - SLOT - TRIGGER: self._subp.finished - - Prints debug info when quitting the process - """ - logger.debug("stdout: %s", self._subp.readAllStandardOutput()) - logger.debug("stderr: %s", self._subp.readAllStandardError()) - - def _get_openvpn_process(self): - """ - Looks for openvpn instances running - - :rtype: process - """ - openvpn_process = None - for p in psutil.process_iter(): - try: - # XXX Not exact! - # Will give false positives. - # we should check that cmdline BEGINS - # with openvpn or with our wrapper - # (pkexec / osascript / whatever) - if self._launcher.OPENVPN_BIN in ' '.join(p.cmdline): - openvpn_process = p - break - except psutil.error.AccessDenied: - pass - return openvpn_process - - def _stop_if_already_running(self): - """ - Checks if VPN is already running and tries to stop it - - :return: True if stopped, False otherwise - """ - - process = self._get_openvpn_process() - if process: - logger.debug("OpenVPN is already running, trying to stop it") - cmdline = process.cmdline - - manag_flag = "--management" - if isinstance(cmdline, list) and manag_flag in cmdline: - try: - index = cmdline.index(manag_flag) - host = cmdline[index + 1] - port = cmdline[index + 2] - logger.debug("Trying to connect to %s:%s" - % (host, port)) - self._connect(host, port) - self._send_command("signal SIGTERM") - self._tn.close() - self._tn = None - except Exception as e: - logger.warning("Problem trying to terminate OpenVPN: %r" - % (e,)) - - process = self._get_openvpn_process() - if process is None: - logger.warning("Unabled to terminate OpenVPN") - return True - else: - return False - - return True - - def _connect(self, socket_host, socket_port): - """ - Connects to the specified socket_host socket_port - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - """ - try: - self._tn = UDSTelnet(socket_host, socket_port) - - # XXX make password optional - # specially for win. we should generate - # the pass on the fly when invoking manager - # from conductor - - # self.tn.read_until('ENTER PASSWORD:', 2) - # self.tn.write(self.password + '\n') - # self.tn.read_until('SUCCESS:', 2) - if self._tn: - self._tn.read_eager() - except Exception as e: - logger.warning("Could not connect to OpenVPN yet: %r" % (e,)) - self._tn = None - - def _disconnect(self): - """ - Disconnects the telnet connection to the openvpn process - """ - logger.debug('Closing socket') - self._tn.write("quit\n") - self._tn.read_all() - self._tn.close() - self._tn = None - - def _send_command(self, command, until=b"END"): - """ - Sends a command to the telnet connection and reads until END - is reached - - :param command: command to send - :type command: str - :param until: byte delimiter string for reading command output - :type until: byte str - :return: response read - :rtype: list - """ - leap_assert(self._tn, "We need a tn connection!") - try: - self._tn.write("%s\n" % (command,)) - buf = self._tn.read_until(until, 2) - self._tn.read_eager() - lines = buf.split("\n") - return lines - except Exception as e: - logger.warning("Error sending command %s: %r" % - (command, e)) - return [] - - def _parse_state_and_notify(self, output): - """ - Parses the output of the state command and emits state_changed - signal when the state changes - - :param output: list of lines that the state command printed as - its output - :type output: list - """ - for line in output: - stripped = line.strip() - if stripped == "END": - continue - parts = stripped.split(",") - if len(parts) < 5: - continue - ts, status_step, ok, ip, remote = parts - - state_dict = { - self.TS_KEY: ts, - self.STATUS_STEP_KEY: status_step, - self.OK_KEY: ok, - self.IP_KEY: ip, - self.REMOTE_KEY: remote - } - - if state_dict != self._last_state: - self.state_changed.emit(state_dict) - self._last_state = state_dict - - def _parse_status_and_notify(self, output): - """ - Parses the output of the status command and emits - status_changed signal when the status changes - - :param output: list of lines that the status command printed - as its output - :type output: list - """ - tun_tap_read = "" - tun_tap_write = "" - tcp_udp_read = "" - tcp_udp_write = "" - auth_read = "" - for line in output: - stripped = line.strip() - if stripped.endswith("STATISTICS") or stripped == "END": - continue - parts = stripped.split(",") - if len(parts) < 2: - continue - if parts[0].strip() == "TUN/TAP read bytes": - tun_tap_read = parts[1] - elif parts[0].strip() == "TUN/TAP write bytes": - tun_tap_write = parts[1] - elif parts[0].strip() == "TCP/UDP read bytes": - tcp_udp_read = parts[1] - elif parts[0].strip() == "TCP/UDP write bytes": - tcp_udp_write = parts[1] - elif parts[0].strip() == "Auth read bytes": - auth_read = parts[1] - - status_dict = { - self.TUNTAP_READ_KEY: tun_tap_read, - self.TUNTAP_WRITE_KEY: tun_tap_write, - self.TCPUDP_READ_KEY: tcp_udp_read, - self.TCPUDP_WRITE_KEY: tcp_udp_write, - self.AUTH_READ_KEY: auth_read - } - - if status_dict != self._last_status: - self.status_changed.emit(status_dict) - self._last_status = status_dict - - def run(self): - """ - Main run loop for this thread - """ - while True: - if self.get_should_quit(): - logger.debug("Quitting VPN thread") - return - - if self._subp and self._subp.state() != QtCore.QProcess.Running: - QtCore.QThread.msleep(self.CONNECTION_RETRY_TIME) - - if self._tn is None: - self._connect(self._host, self._port) - QtCore.QThread.msleep(self.CONNECTION_RETRY_TIME) - else: - self._parse_state_and_notify(self._send_command("state")) - self._parse_status_and_notify(self._send_command("status")) - output_sofar = self._subp.readAllStandardOutput() - if len(output_sofar) > 0: - logger.debug(output_sofar) - output_sofar = self._subp.readAllStandardError() - if len(output_sofar) > 0: - logger.debug(output_sofar) - QtCore.QThread.msleep(self.POLL_TIME) - - -if __name__ == "__main__": - import os - import signal - - app = QtGui.QApplication(sys.argv) - - def sigint_handler(*args, **kwargs): - logger.debug('SIGINT catched. shutting down...') - vpn_thread = args[0] - vpn_thread.set_should_quit() - QtGui.QApplication.quit() - - def signal_tester(d): - print d - - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - vpn_thread = VPN() - - sigint = partial(sigint_handler, vpn_thread) - signal.signal(signal.SIGINT, sigint) - - eipconfig = EIPConfig() - if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): - provider = ProviderConfig() - if provider.load("leap/providers/bitmask.net/provider.json"): - vpn_thread.start(eipconfig=eipconfig, - providerconfig=provider, - socket_host=os.path.expanduser("~/vpnsock"), - socket_port="unix") - - timer = QtCore.QTimer() - timer.start(500) - timer.timeout.connect(lambda: None) - app.connect(app, QtCore.SIGNAL("aboutToQuit()"), - vpn_thread.set_should_quit) - w = QtGui.QWidget() - w.resize(100, 100) - w.show() - - vpn_thread.state_changed.connect(signal_tester) - vpn_thread.status_changed.connect(signal_tester) - - sys.exit(app.exec_()) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 0691e121..952d3618 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -132,6 +132,7 @@ def _is_auth_agent_running(): """ polkit_gnome = 'ps aux | grep polkit-[g]nome-authentication-agent-1' polkit_kde = 'ps aux | grep polkit-[k]de-authentication-agent-1' + return (len(commands.getoutput(polkit_gnome)) > 0 or len(commands.getoutput(polkit_kde)) > 0) diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py new file mode 100644 index 00000000..eae8aadd --- /dev/null +++ b/src/leap/services/eip/vpnprocess.py @@ -0,0 +1,592 @@ +# -*- coding: utf-8 -*- +# vpnprocess.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +VPN Manager, spawned in a custom processProtocol. +""" +import logging +import os +import psutil + +from PySide import QtCore + +from leap.common.check import leap_assert, leap_assert_type +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.vpnlaunchers import get_platform_launcher +from leap.services.eip.eipconfig import EIPConfig +from leap.services.eip.udstelnet import UDSTelnet + +logger = logging.getLogger(__name__) +vpnlog = logging.getLogger('leap.openvpn') + +from twisted.internet import protocol +from twisted.internet import defer +from twisted.internet.task import LoopingCall +from twisted.internet import error as internet_error + + +class VPNSignals(QtCore.QObject): + """ + These are the signals that we use to let the UI know + about the events we are polling. + They are instantiated in the VPN object and passed along + till the VPNProcess. + """ + state_changed = QtCore.Signal(dict) + status_changed = QtCore.Signal(dict) + process_finished = QtCore.Signal(int) + + def __init__(self): + QtCore.QObject.__init__(self) + + +class VPN(object): + """ + This is the high-level object that the GUI is dealing with. + It exposes the start and terminate methods. + + On start, it spawns a VPNProcess instance that will use a vpnlauncher + suited for the running platform and connect to the management interface + opened by the openvpn process, executing commands over that interface on + demand. + """ + def __init__(self): + """ + Instantiate empty attributes and get a copy + of a QObject containing the QSignals that we will pass along + to the VPNManager. + """ + from twisted.internet import reactor + self._vpnproc = None + self._pollers = [] + self._reactor = reactor + self._qtsigs = VPNSignals() + + @property + def qtsigs(self): + return self._qtsigs + + def start(self, *args, **kwargs): + """ + Starts the openvpn subprocess. + + :param args: args to be passed to the VPNProcess + :type args: tuple + + :param kwargs: kwargs to be passed to the VPNProcess + :type kwargs: dict + """ + kwargs['qtsigs'] = self.qtsigs + + # start the main vpn subprocess + vpnproc = VPNProcess(*args, **kwargs) + + cmd = vpnproc.getCommand() + env = os.environ + for key, val in vpnproc.vpn_env.items(): + env[key] = val + + self._reactor.spawnProcess(vpnproc, cmd[0], cmd, env) + self._vpnproc = vpnproc + + # add pollers for status and state + # XXX this could be extended to a collection of + # generic watchers + + poll_list = [LoopingCall(vpnproc.pollStatus), + LoopingCall(vpnproc.pollState)] + self._pollers.extend(poll_list) + self._start_pollers() + + def terminate(self): + """ + Stops the openvpn subprocess. + """ + self._stop_pollers() + # XXX we should leave a KILL as a last resort. + # First we should try to send a SIGTERM + if self._vpnproc: + self._vpnproc.killProcess() + + def _start_pollers(self): + """ + Iterate through the registered observers + and start the looping call for them. + """ + for poller in self._pollers: + poller.start(VPNManager.POLL_TIME) + + def _stop_pollers(self): + """ + Iterate through the registered observers + and stop the looping calls if they are running. + """ + for poller in self._pollers: + if poller.running: + poller.stop() + self._pollers = [] + + +class VPNManager(object): + """ + This is a mixin that we use in the VPNProcess class. + Here we get together all methods related with the openvpn management + interface. + + A copy of a QObject containing signals as attributes is passed along + upon initialization, and we use that object to emit signals to qt-land. + """ + + # Timers, in secs + POLL_TIME = 0.5 + CONNECTION_RETRY_TIME = 1 + + TS_KEY = "ts" + STATUS_STEP_KEY = "status_step" + OK_KEY = "ok" + IP_KEY = "ip" + REMOTE_KEY = "remote" + + TUNTAP_READ_KEY = "tun_tap_read" + TUNTAP_WRITE_KEY = "tun_tap_write" + TCPUDP_READ_KEY = "tcp_udp_read" + TCPUDP_WRITE_KEY = "tcp_udp_write" + AUTH_READ_KEY = "auth_read" + + def __init__(self, qtsigs=None): + """ + Initializes the VPNManager. + + :param qtsigs: a QObject containing the Qt signals used by the UI + to give feedback about state changes. + :type qtsigs: QObject + """ + from twisted.internet import reactor + self._reactor = reactor + self._tn = None + self._qtsigs = qtsigs + + @property + def qtsigs(self): + return self._qtsigs + + def _disconnect(self): + """ + Disconnects the telnet connection to the openvpn process. + """ + logger.debug('Closing socket') + self._tn.write("quit\n") + self._tn.read_all() + self._tn.close() + self._tn = None + + def _send_command(self, command, until=b"END"): + """ + Sends a command to the telnet connection and reads until END + is reached. + + :param command: command to send + :type command: str + + :param until: byte delimiter string for reading command output + :type until: byte str + + :return: response read + :rtype: list + """ + leap_assert(self._tn, "We need a tn connection!") + try: + self._tn.write("%s\n" % (command,)) + buf = self._tn.read_until(until, 2) + self._tn.read_eager() + lines = buf.split("\n") + return lines + + # XXX should move this to a errBack! + except Exception as e: + logger.warning("Error sending command %s: %r" % + (command, e)) + return [] + + def _connect(self, socket_host, socket_port): + """ + Connects to the specified socket_host socket_port. + + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str + """ + try: + self._tn = UDSTelnet(socket_host, socket_port) + + # XXX make password optional + # specially for win. we should generate + # the pass on the fly when invoking manager + # from conductor + + # self.tn.read_until('ENTER PASSWORD:', 2) + # self.tn.write(self.password + '\n') + # self.tn.read_until('SUCCESS:', 2) + if self._tn: + self._tn.read_eager() + + # XXX move this to the Errback + except Exception as e: + logger.warning("Could not connect to OpenVPN yet: %r" % (e,)) + self._tn = None + + def _connectCb(self, *args): + """ + Callback for connection. + + :param args: not used + """ + if self._tn: + logger.info('connected to management') + + def _connectErr(self, failure): + """ + Errorback for connection. + + :param failure: Failure + """ + logger.warning(failure) + + def connect(self, host, port): + """ + Connect to a management interface. + + :param host: the host of the management interface + :type host: str + + :param port: the port of the management interface + :type port: str + + :returns: a deferred + """ + self.connectd = defer.maybeDeferred(self._connect, host, port) + self.connectd.addCallbacks(self._connectCb, self._connectErr) + return self.connectd + + def is_connected(self): + """ + Returns the status of the management interface. + + :returns: True if connected, False otherwise + :rtype: bool + """ + return True if self._tn else False + + def try_to_connect(self, retry=0): + """ + Attempts to connect to a management interface, and retries + after CONNECTION_RETRY_TIME if not successful. + + :param retry: number of the retry + :type retry: int + """ + # TODO decide about putting a max_lim to retries and signaling + # an error. + if not self.is_connected(): + self.connect(self._socket_host, self._socket_port) + self._reactor.callLater( + self.CONNECTION_RETRY_TIME, self.try_to_connect, retry + 1) + + def _parse_state_and_notify(self, output): + """ + Parses the output of the state command and emits state_changed + signal when the state changes. + + :param output: list of lines that the state command printed as + its output + :type output: list + """ + for line in output: + stripped = line.strip() + if stripped == "END": + continue + parts = stripped.split(",") + if len(parts) < 5: + continue + ts, status_step, ok, ip, remote = parts + + state_dict = { + self.TS_KEY: ts, + self.STATUS_STEP_KEY: status_step, + self.OK_KEY: ok, + self.IP_KEY: ip, + self.REMOTE_KEY: remote + } + + if state_dict != self._last_state: + self.qtsigs.state_changed.emit(state_dict) + self._last_state = state_dict + + def _parse_status_and_notify(self, output): + """ + Parses the output of the status command and emits + status_changed signal when the status changes. + + :param output: list of lines that the status command printed + as its output + :type output: list + """ + tun_tap_read = "" + tun_tap_write = "" + tcp_udp_read = "" + tcp_udp_write = "" + auth_read = "" + for line in output: + stripped = line.strip() + if stripped.endswith("STATISTICS") or stripped == "END": + continue + parts = stripped.split(",") + if len(parts) < 2: + continue + if parts[0].strip() == "TUN/TAP read bytes": + tun_tap_read = parts[1] + elif parts[0].strip() == "TUN/TAP write bytes": + tun_tap_write = parts[1] + elif parts[0].strip() == "TCP/UDP read bytes": + tcp_udp_read = parts[1] + elif parts[0].strip() == "TCP/UDP write bytes": + tcp_udp_write = parts[1] + elif parts[0].strip() == "Auth read bytes": + auth_read = parts[1] + + status_dict = { + self.TUNTAP_READ_KEY: tun_tap_read, + self.TUNTAP_WRITE_KEY: tun_tap_write, + self.TCPUDP_READ_KEY: tcp_udp_read, + self.TCPUDP_WRITE_KEY: tcp_udp_write, + self.AUTH_READ_KEY: auth_read + } + + if status_dict != self._last_status: + self.qtsigs.status_changed.emit(status_dict) + self._last_status = status_dict + + def get_state(self): + """ + Notifies the gui of the output of the state command over + the openvpn management interface. + """ + if self.is_connected(): + return self._parse_state_and_notify(self._send_command("state")) + + def get_status(self): + """ + Notifies the gui of the output of the status command over + the openvpn management interface. + """ + if self.is_connected(): + return self._parse_status_and_notify(self._send_command("status")) + + @property + def vpn_env(self): + """ + Return a dict containing the vpn environment to be used. + """ + return self._launcher.get_vpn_env(self._providerconfig) + + # XXX old methods, not adapted to twisted process yet + + def _get_openvpn_process(self): + """ + Looks for openvpn instances running. + + :rtype: process + """ + openvpn_process = None + for p in psutil.process_iter(): + try: + # XXX Not exact! + # Will give false positives. + # we should check that cmdline BEGINS + # with openvpn or with our wrapper + # (pkexec / osascript / whatever) + if self._launcher.OPENVPN_BIN in ' '.join(p.cmdline): + openvpn_process = p + break + except psutil.error.AccessDenied: + pass + return openvpn_process + + def _stop_if_already_running(self): + """ + Checks if VPN is already running and tries to stop it. + + :return: True if stopped, False otherwise + """ + + process = self._get_openvpn_process() + if process: + logger.debug("OpenVPN is already running, trying to stop it") + cmdline = process.cmdline + + manag_flag = "--management" + if isinstance(cmdline, list) and manag_flag in cmdline: + try: + index = cmdline.index(manag_flag) + host = cmdline[index + 1] + port = cmdline[index + 2] + logger.debug("Trying to connect to %s:%s" + % (host, port)) + self._connect(host, port) + self._send_command("signal SIGTERM") + self._tn.close() + self._tn = None + #self._disconnect() + except Exception as e: + logger.warning("Problem trying to terminate OpenVPN: %r" + % (e,)) + + process = self._get_openvpn_process() + if process is None: + logger.warning("Unabled to terminate OpenVPN") + return True + else: + return False + return True + + +class VPNProcess(protocol.ProcessProtocol, VPNManager): + """ + A ProcessProtocol class that can be used to spawn a process that will + launch openvpn and connect to its management interface to control it + programmatically. + """ + + def __init__(self, eipconfig, providerconfig, socket_host, socket_port, + qtsigs): + """ + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + + :param socket_port: either string "unix" if it's a unix + socket, or port otherwise + :type socket_port: str + + :param qtsigs: a QObject containing the Qt signals used to notify the + UI. + :type qtsigs: QObject + """ + VPNManager.__init__(self, qtsigs=qtsigs) + leap_assert_type(eipconfig, EIPConfig) + leap_assert_type(providerconfig, ProviderConfig) + leap_assert_type(qtsigs, QtCore.QObject) + + #leap_assert(not self.isRunning(), "Starting process more than once!") + + self._eipconfig = eipconfig + self._providerconfig = providerconfig + self._socket_host = socket_host + self._socket_port = socket_port + + self._launcher = get_platform_launcher() + + self._last_state = None + self._last_status = None + + # processProtocol methods + + def connectionMade(self): + """ + Called when the connection is made. + + .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa + """ + self.try_to_connect() + + def outReceived(self, data): + """ + Called when new data is available on stdout. + + :param data: the data read on stdout + + .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa + """ + # truncate the newline + # should send this to the logging window + vpnlog.info(data[:-1]) + + def processExited(self, reason): + """ + Called when the child process exits. + + .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa + """ + exit_code = reason.value.exitCode + if isinstance(exit_code, int): + logger.debug("processExited, status %d" % (exit_code,)) + + def processEnded(self, reason): + """ + Called when the child process exits and all file descriptors associated + with it have been closed. + + .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa + """ + exit_code = reason.value.exitCode + if isinstance(exit_code, int): + logger.debug("processEnded, status %d" % (exit_code,)) + + # polling + + def pollStatus(self): + """ + Polls connection status. + """ + self.get_status() + + def pollState(self): + """ + Polls connection state. + """ + self.get_state() + + # launcher + + def getCommand(self): + """ + Gets the vpn command from the aproppriate launcher. + """ + cmd = self._launcher.get_vpn_command( + eipconfig=self._eipconfig, + providerconfig=self._providerconfig, + socket_host=self._socket_host, + socket_port=self._socket_port) + return map(str, cmd) + + # shutdown + + def killProcess(self): + """ + Sends the KILL signal to the running process. + """ + try: + self.transport.signalProcess('KILL') + except internet_error.ProcessExitedAlready: + logger.debug('Process Exited Already') diff --git a/src/leap/services/mail/smtpspec.py b/src/leap/services/mail/smtpspec.py index b455b196..270dfb76 100644 --- a/src/leap/services/mail/smtpspec.py +++ b/src/leap/services/mail/smtpspec.py @@ -22,12 +22,12 @@ smtp_config_spec = { 'serial': { 'type': int, 'default': 1, - 'required': True + 'required': ["True"] }, 'version': { 'type': int, 'default': 1, - 'required': True + 'required': ["True"] }, 'hosts': { 'type': dict, diff --git a/src/leap/services/soledad/soledadspec.py b/src/leap/services/soledad/soledadspec.py index d5a437cc..8233d6a0 100644 --- a/src/leap/services/soledad/soledadspec.py +++ b/src/leap/services/soledad/soledadspec.py @@ -22,12 +22,12 @@ soledad_config_spec = { 'serial': { 'type': int, 'default': 1, - 'required': True + 'required': ["True"] }, 'version': { 'type': int, 'default': 1, - 'required': True + 'required': ["True"] }, 'hosts': { 'type': dict, -- cgit v1.2.3 From b2653dcf8f9619c73e4fc792a2d952dd2fdfd3b4 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 11 Jun 2013 17:00:22 -0300 Subject: Remove lines disconnecting vpn on logout --- src/leap/gui/mainwindow.py | 3 --- 1 file changed, 3 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index a7d88aee..afa7e947 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -1084,8 +1084,6 @@ class MainWindow(QtGui.QMainWindow): Starts the logout sequence """ - self._set_eip_status_icon("error") - self._set_eip_status(self.tr("Signing out...")) # XXX: If other defers are doing authenticated stuff, this # might conflict with those. CHECK! threads.deferToThread(self._srp_auth.logout) @@ -1103,7 +1101,6 @@ class MainWindow(QtGui.QMainWindow): self.ui.lnPassword.setText("") self._login_set_enabled(True) self._set_status("") - self._vpn.terminate() def _intermediate_stage(self, data): """ -- cgit v1.2.3 From da018dfadc4f3c6cbd1613b45b339279aebb0589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 12 Jun 2013 10:56:57 -0300 Subject: Autostart EIP whenever possible --- src/leap/gui/mainwindow.py | 94 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index afa7e947..65f5fe3d 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -153,8 +153,12 @@ class MainWindow(QtGui.QMainWindow): EIPConfig.standalone = standalone self._standalone = standalone self._provider_config = ProviderConfig() + # Used for automatic start of EIP + self._provisional_provider_config = ProviderConfig() self._eip_config = EIPConfig() + self._already_started_eip = False + # This is created once we have a valid provider config self._srp_auth = None @@ -219,10 +223,11 @@ class MainWindow(QtGui.QMainWindow): self._systray = None - self._action_eip_status = QtGui.QAction(self.tr("Encrypted internet is OFF"), - self) + self._action_eip_status = QtGui.QAction( + self.tr("Encrypted internet is OFF"), self) self._action_eip_status.setEnabled(False) - self._action_eip_startstop = QtGui.QAction(self.tr("Turn encryption ON"), self) + self._action_eip_startstop = QtGui.QAction( + self.tr("Turn encryption ON"), self) self._action_eip_startstop.triggered.connect( self._stop_eip) self._action_eip_write = QtGui.QAction( @@ -400,6 +405,7 @@ class MainWindow(QtGui.QMainWindow): self._wizard = None self._settings.set_properprovider(True) else: + self._try_autostart_eip() if not self._settings.get_remember(): # nothing to do here return @@ -443,6 +449,31 @@ class MainWindow(QtGui.QMainWindow): if auto_login and saved_password: self._login() + def _try_autostart_eip(self): + """ + Tries to autostart EIP + """ + default_provider = self._settings.get_defaultprovider() + + if default_provider is None: + logger.info("Cannot autostart EIP because there's no default " + "provider configured") + return + + self._enabled_services = self._settings.get_enabled_services( + default_provider) + + if self._provisional_provider_config.load( + os.path.join("leap", + "providers", + default_provider, + "provider.json")): + self._download_eip_config() + else: + # XXX: Display a proper message to the user + logger.error("Unable to load %s config, cannot autostart." % + (default_provider,)) + def _show_systray(self): """ Sets up the systray icon @@ -744,6 +775,7 @@ class MainWindow(QtGui.QMainWindow): self._srp_auth.logout_finished.connect( self._done_logging_out) + # TODO: Add errback! self._login_defer = self._srp_auth.authenticate(username, password) else: self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) @@ -905,15 +937,17 @@ class MainWindow(QtGui.QMainWindow): Starts EIP """ + provider_config = self._get_best_provider_config() + try: host, port = self._get_socket_host() self._vpn.start(eipconfig=self._eip_config, - providerconfig=self._provider_config, + providerconfig=provider_config, socket_host=host, socket_port=port) self._settings.set_defaultprovider( - self._provider_config.get_domain()) + provider_config.get_domain()) self.ui.btnEipStartStop.setText(self.tr("Turn Encryption OFF")) self.ui.btnEipStartStop.disconnect(self) self.ui.btnEipStartStop.clicked.connect( @@ -955,25 +989,53 @@ class MainWindow(QtGui.QMainWindow): self._action_eip_startstop.disconnect(self) self._action_eip_startstop.triggered.connect( self._start_eip) + self._already_started_eip = False + + def _get_best_provider_config(self): + """ + Returns the best ProviderConfig to use at a moment. We may + have to use self._provider_config or + self._provisional_provider_config depending on the start + status. + + :rtype: ProviderConfig + """ + leap_assert(self._provider_config is not None or + self._provisional_provider_config is not None, + "We need a provider config") + + provider_config = None + if self._provider_config.loaded(): + provider_config = self._provider_config + elif self._provisional_provider_config.loaded(): + provider_config = self._provisional_provider_config + else: + leap_assert(False, "We couldn't find any usable ProviderConfig") + + return provider_config def _download_eip_config(self): """ Starts the EIP bootstrapping sequence """ leap_assert(self._eip_bootstrapper, "We need an eip bootstrapper!") - leap_assert(self._provider_config, "We need a provider config") - self._set_eip_status(self.tr("Checking configuration, please wait...")) + provider_config = self._get_best_provider_config() - if self._provider_config.provides_eip() and \ - self._enabled_services.count(self.OPENVPN_SERVICE) > 0: + if provider_config.provides_eip() and \ + self._enabled_services.count(self.OPENVPN_SERVICE) > 0 and \ + not self._already_started_eip: + + self._set_eip_status( + self.tr("Checking configuration, please wait...")) self._eip_bootstrapper.run_eip_setup_checks( - self._provider_config, + provider_config, download_if_needed=True) - else: + self._already_started_eip = True + elif not self._already_started_eip: if self._enabled_services.count(self.OPENVPN_SERVICE) > 0: self._set_eip_status(self.tr("%s does not support EIP") % - (self._provider_config.get_domain(),), + (provider_config.get_domain(),), error=True) else: self._set_eip_status(self.tr("EIP is disabled")) @@ -1059,14 +1121,16 @@ class MainWindow(QtGui.QMainWindow): loaded """ leap_assert(self._eip_config, "We need an eip config!") - leap_assert(self._provider_config, "We need a provider config!") + + provider_config = self._get_best_provider_config() + + domain = provider_config.get_domain() if data[self._eip_bootstrapper.PASSED_KEY] and \ (self._eip_config.loaded() or self._eip_config.load(os.path.join("leap", "providers", - self._provider_config - .get_domain(), + domain, "eip-service.json"))): self._start_eip() else: -- cgit v1.2.3 From 1ede2af0afb6db2265d7e32428c197605e74589e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 12 Jun 2013 10:57:20 -0300 Subject: Fix SRPAuth error reporting --- src/leap/crypto/srpauth.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index 52267b3b..bcd24de3 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -241,10 +241,17 @@ class SRPAuth(QtCore.QObject): raise SRPAuthenticationError("Bad JSON content in auth result") if auth_result.status_code == 422: + error = "" + try: + error = json.loads(content).get("errors", "") + except ValueError: + logger.error("Problem parsing the received response: %s" + % (content,)) + except AttributeError: + logger.error("Expecting a dict but something else was " + "received: %s", (content,)) logger.error("[%s] Wrong password (HAMK): [%s]" % - (auth_result.status_code, - content. - get("errors", ""))) + (auth_result.status_code, error)) raise SRPAuthenticationError(self.tr("Wrong password")) if auth_result.status_code not in (200,): -- cgit v1.2.3 From 9b2c0d673a1a8a4641508188c87662c9eacfd0ce Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 12 Jun 2013 02:15:20 +0900 Subject: workaround for pyside multiple inheritance problem --- src/leap/util/leap_log_handler.py | 74 ++++++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/leap/util/leap_log_handler.py b/src/leap/util/leap_log_handler.py index 0e598032..3264e05c 100644 --- a/src/leap/util/leap_log_handler.py +++ b/src/leap/util/leap_log_handler.py @@ -14,32 +14,30 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - """ Custom handler for the logger window. """ import logging +from functools import partial from PySide import QtCore - -class LeapLogHandler(logging.Handler, QtCore.QObject): +class LogHandler(logging.Handler): """ - Custom logging handler. It emits Qt signals so it can be plugged to a gui. - Also stores an history of logs that can be fetched after connect to a gui. + This is the custom handler that implements our desired formatting + and also keeps a history of all the logged events. """ - # All dicts returned are of the form - # {'record': LogRecord, 'message': str} - new_log = QtCore.Signal(dict) MESSAGE_KEY = 'message' RECORD_KEY = 'record' - def __init__(self): - logging.Handler.__init__(self) - QtCore.QObject.__init__(self) + # TODO This is going to eat lots of memory after some time. + # Should be pruned at some moment. + _log_history = [] - self._log_history = [] + def __init__(self, qtsignal): + logging.Handler.__init__(self) + self._qtsignal = qtsignal def _set_format(self, logging_level): """ @@ -66,6 +64,7 @@ class LeapLogHandler(logging.Handler, QtCore.QObject): format_attrs = [time, name, level, message] log_format = ' - '.join(format_attrs) formatter = logging.Formatter(log_format) + self.setFormatter(formatter) def emit(self, logRecord): @@ -74,8 +73,6 @@ class LeapLogHandler(logging.Handler, QtCore.QObject): logging module. This method reimplements logging.Handler.emit that is fired in every logged message. - QObject.emit gets in the way on the PySide signal model but we - workarouded that issue. :param logRecord: the record emitted by the logging module. :type logRecord: logging.LogRecord. @@ -83,17 +80,60 @@ class LeapLogHandler(logging.Handler, QtCore.QObject): self._set_format(logRecord.levelname) log = self.format(logRecord) log_item = {self.RECORD_KEY: logRecord, self.MESSAGE_KEY: log} - self._log_history.append(log_item) + self._qtsignal(log_item) + + +class HandlerAdapter(object): + """ + New style class that accesses all attributes from the LogHandler. + + Used as a workaround for a problem with multiple inheritance with Pyside + that surfaced under OSX with pyside 1.1.0. + """ + MESSAGE_KEY = 'message' + RECORD_KEY = 'record' + + def __init__(self, qtsignal): + self._handler = LogHandler(qtsignal=qtsignal) + + def setLevel(self, *args, **kwargs): + return self._handler.setLevel(*args, **kwargs) + + def handle(self, *args, **kwargs): + return self._handler.handle(*args, **kwargs) + + @property + def level(self): + return self._handler.level + + +class LeapLogHandler(QtCore.QObject, HandlerAdapter): + """ + Custom logging handler. It emits Qt signals so it can be plugged to a gui. + + Its inner handler also stores an history of logs that can be fetched after + having been connected to a gui. + """ + # All dicts returned are of the form + # {'record': LogRecord, 'message': str} + new_log = QtCore.Signal(dict) + + def __init__(self): + QtCore.QObject.__init__(self) + HandlerAdapter.__init__(self, qtsignal=self.qtsignal) + def qtsignal(self, log_item): # WARNING: the new-style connection does NOT work because PySide # translates the emit method to self.emit, and that collides with # the emit method for logging.Handler # self.new_log.emit(log_item) - QtCore.QObject.emit(self, QtCore.SIGNAL('new_log(PyObject)'), log_item) + QtCore.QObject.emit( + self, + QtCore.SIGNAL('new_log(PyObject)'), log_item) @property def log_history(self): """ Returns the history of the logged messages. """ - return self._log_history + return self._handler._log_history -- cgit v1.2.3 From ec1c3fb89b7eaf79dde1b0a7cd4b2dcb277653ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 12 Jun 2013 21:11:19 -0300 Subject: Fix wording in mainwindow --- src/leap/gui/ui/mainwindow.ui | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui index fdf5c167..ae6fa5bd 100644 --- a/src/leap/gui/ui/mainwindow.ui +++ b/src/leap/gui/ui/mainwindow.ui @@ -6,7 +6,7 @@ 0 0 - 415 + 429 579 @@ -295,7 +295,7 @@ - Start EIP + Turn Encryption ON @@ -395,8 +395,8 @@ 0 0 - 415 - 21 + 429 + 25 -- cgit v1.2.3 From 521c4b41515fa9491f744274548e1714eb8695f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 12 Jun 2013 21:31:32 -0300 Subject: Display the default provider in the systray menu --- src/leap/gui/mainwindow.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 65f5fe3d..ddcf1a4c 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -223,6 +223,9 @@ class MainWindow(QtGui.QMainWindow): self._systray = None + self._action_eip_provider = QtGui.QAction( + self.tr("No default provider"), self) + self._action_eip_provider.setEnabled(False) self._action_eip_status = QtGui.QAction( self.tr("Encrypted internet is OFF"), self) self._action_eip_status.setEnabled(False) @@ -460,6 +463,8 @@ class MainWindow(QtGui.QMainWindow): "provider configured") return + self._action_eip_provider.setText(default_provider) + self._enabled_services = self._settings.get_enabled_services( default_provider) @@ -487,6 +492,7 @@ class MainWindow(QtGui.QMainWindow): systrayMenu.addSeparator() systrayMenu.addAction(self.ui.action_quit) systrayMenu.addSeparator() + systrayMenu.addAction(self._action_eip_provider) systrayMenu.addAction(self._action_eip_status) systrayMenu.addAction(self._action_eip_startstop) self._systray = QtGui.QSystemTrayIcon(self) @@ -948,6 +954,7 @@ class MainWindow(QtGui.QMainWindow): self._settings.set_defaultprovider( provider_config.get_domain()) + self._action_eip_provider.setText(provider_config.get_domain()) self.ui.btnEipStartStop.setText(self.tr("Turn Encryption OFF")) self.ui.btnEipStartStop.disconnect(self) self.ui.btnEipStartStop.clicked.connect( -- cgit v1.2.3 From ba27c14ba84c6869c187bdd09138bfae4424445d Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 13 Jun 2013 01:19:49 +0900 Subject: copy missing updown scripts if missing --- src/leap/config/leapsettings.py | 19 ++++++ src/leap/platform_init/initializers.py | 119 +++++++++++++++++++++++++++++++-- src/leap/services/eip/vpnlaunchers.py | 96 +++++++++++++++++++------- src/leap/util/leap_log_handler.py | 11 ++- 4 files changed, 215 insertions(+), 30 deletions(-) (limited to 'src') diff --git a/src/leap/config/leapsettings.py b/src/leap/config/leapsettings.py index 006be851..ab0c1860 100644 --- a/src/leap/config/leapsettings.py +++ b/src/leap/config/leapsettings.py @@ -66,6 +66,7 @@ class LeapSettings(object): PROPERPROVIDER_KEY = "ProperProvider" REMEMBER_KEY = "RememberUserAndPass" DEFAULTPROVIDER_KEY = "DefaultProvider" + ALERTMISSING_KEY = "AlertMissingScripts" def __init__(self, standalone=False): """ @@ -249,3 +250,21 @@ class LeapSettings(object): """ leap_assert(len(provider) > 0, "We cannot save an empty provider") self._settings.setValue(self.DEFAULTPROVIDER_KEY, provider) + + def get_alert_missing_scripts(self): + """ + Returns the setting for alerting of missing up/down scripts. + + :rtype: bool + """ + return to_bool(self._settings.value(self.ALERTMISSING_KEY, True)) + + def set_alert_missing_scripts(self, value): + """ + Sets the setting for alerting of missing up/down scripts. + + :param value: the value to set + :type value: bool + """ + leap_assert_type(value, bool) + self._settings.setValue(self.ALERTMISSING_KEY, value) diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index 2e8cbe95..9dd31a18 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -22,10 +22,15 @@ Platform dependant initializing code import logging import os import platform +import stat import subprocess +import tempfile from PySide import QtGui +from leap.config.leapsettings import LeapSettings +from leap.services.eip import vpnlaunchers + logger = logging.getLogger(__name__) # NOTE we could use a deferToThread here, but should @@ -74,6 +79,28 @@ def _windows_has_tap_device(): pass return False +def _get_missing_updown_dialog(): + """ + Creates a dialog for notifying of missing updown scripts. + Returns that dialog. + + :rtype: QtGui.QMessageBox instance + """ + msg = QtGui.QMessageBox() + msg.setWindowTitle(msg.tr("Missing up/down scripts")) + msg.setText(msg.tr( + "LEAPClient needs to install up/down scripts " + "for Encrypted Internet to work properly. " + "Would you like to proceed?")) + msg.setInformativeText(msg.tr( + "It looks like either you have not installed " + "LEAP Client in a permanent location or you have an " + "incomplete installation. This will ask for " + "administrative privileges.")) + msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) + msg.addButton("No, don't ask again", QtGui.QMessageBox.RejectRole) + msg.setDefaultButton(QtGui.QMessageBox.Yes) + return msg def WindowsInitializer(): """ @@ -128,15 +155,73 @@ def _darwin_has_tun_kext(): return has_tun_and_startup +def _darwin_install_missing_scripts(badexec, notfound): + """ + Tries to install the missing up/down scripts. + + :param badexec: error for notifying execution error during command. + :type badexec: str + :param notfound: error for notifying missing path. + :type notfound: str + """ + # We expect to execute this from some way of bundle, since + # the up/down scripts should be put in place by the installer. + installer_path = os.path.join( + os.getcwd(), + "..", + "Resources", + "openvpn") + launcher = vpnlaunchers.DarwinVPNLauncher + if os.path.isdir(installer_path): + tempscript = tempfile.mktemp() + try: + cmd = launcher.OSASCRIPT_BIN + scriptlines = launcher.cmd_for_missing_scripts(installer_path) + with open(tempscript, 'w') as f: + f.write(scriptlines) + st = os.stat(tempscript) + os.chmod(tempscript, st.st_mode | stat.S_IEXEC | stat.S_IXUSR | + stat.S_IXGRP | stat.S_IXOTH) + + osascript = launcher.OSX_ASADMIN % ("/bin/sh %s" % (tempscript,),) + cmdline = ["%s -e '%s'" % (cmd, osascript)] + ret = subprocess.call( + cmdline, stdout=subprocess.PIPE, + shell=True) + assert(ret) + except Exception as exc: + logger.error(badexec) + logger.error("Error was: %r" % (exc,)) + f.close() + finally: + # XXX remove file + pass + else: + logger.error(notfound) + logger.debug('path searched: %s' % (installer_path,)) + + def DarwinInitializer(): """ Raises a dialog in case that the osx tuntap driver has not been found in the registry, asking the user for permission to install the driver """ - NOTFOUND_MSG = ("Tried to install tuntaposx kext, but the installer " - "is not found inside this bundle.") - BADEXEC_MSG = ("Tried to install tuntaposx kext, but the installer " - "failed to be launched.") + # XXX split this function into several + + NOTFOUND_MSG = ("Tried to install %s, but %s " + "not found inside this bundle.") + BADEXEC_MSG = ("Tried to install %s, but %s " + "failed to %s.") + + TUNTAP_NOTFOUND_MSG = NOTFOUND_MSG % ( + "tuntaposx kext", "the installer") + TUNTAP_BADEXEC_MSG = BADEXEC_MSG % ( + "tuntaposx kext", "the installer", "be launched") + + UPDOWN_NOTFOUND_MSG = NOTFOUND_MSG % ( + "updown scripts", "those were") + UPDOWN_BADEXEC_MSG = BADEXEC_MSG % ( + "updown scripts", "they", "be copied") # TODO DRY this with other cases, and # factor out to _should_install() function. @@ -170,6 +255,28 @@ def DarwinInitializer(): cmd, stdout=subprocess.PIPE, shell=True) except: - logger.error(BADEXEC_MSG) + logger.error(TUNTAP_BADEXEC_MSG) else: - logger.error(NOTFOUND_MSG) + logger.error(TUNTAP_NOTFOUND_MSG) + + config = LeapSettings() + alert_missing = config.get_alert_missing_scripts() + missing_scripts = vpnlaunchers.DarwinVPNLauncher.missing_updown_scripts + if alert_missing and missing_scripts(): + msg = _get_missing_updown_dialog() + ret = msg.exec_() + + if ret == QtGui.QMessageBox.Yes: + _darwin_install_missing_scripts( + UPDOWN_BADEXEC_MSG, + UPDOWN_NOTFOUND_MSG) + + elif ret == QtGui.QMessageBox.No: + logger.debug("Not installing missing scripts, " + "user decided to ignore our warning.") + + elif ret == QtGui.QMessageBox.Rejected: + logger.debug( + "Setting alert_missing_scripts to False, we will not " + "ask again") + config.set_alert_missing_scripts(False) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 952d3618..d15aed82 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -29,6 +29,7 @@ except ImportError: pass # ignore, probably windows from abc import ABCMeta, abstractmethod +from functools import partial from leap.common.check import leap_assert, leap_assert_type from leap.common.files import which @@ -105,22 +106,37 @@ def get_platform_launcher(): def _is_pkexec_in_system(): + """ + Checks the existence of the pkexec binary in system. + """ pkexec_path = which('pkexec') if len(pkexec_path) == 0: return False return True -def _has_updown_scripts(path): +def _has_updown_scripts(path, warn=True): """ - Checks the existence of the up/down scripts + Checks the existence of the up/down scripts. + + :param path: the path to be checked + :type path: str + + :param warn: whether we should log the absence + :type warn: bool + + :rtype: bool """ - # XXX should check permissions too is_file = os.path.isfile(path) - if not is_file: - logger.error("Could not find up/down scripts. " + - "Might produce DNS leaks.") - return is_file + if warn and not is_file: + logger.error("Could not find up/down script %s. " + "Might produce DNS leaks." % (path,)) + + is_exe = os.access(path, os.X_OK) + if warn and not is_exe: + logger.error("Up/down script %s is not executable. " + "Might produce DNS leaks." % (path,)) + return is_file and is_exe def _is_auth_agent_running(): @@ -229,7 +245,6 @@ class LinuxVPNLauncher(VPNLauncher): openvpn_configuration = eipconfig.get_openvpn_configuration() - # FIXME: sanitize this! -- for key, value in openvpn_configuration.items(): args += ['--%s' % (key,), value] @@ -294,15 +309,40 @@ class DarwinVPNLauncher(VPNLauncher): OSASCRIPT_BIN = '/usr/bin/osascript' OSX_ASADMIN = "do shell script \"%s\" with administrator privileges" - OPENVPN_BIN = 'openvpn.leap' - INSTALL_PATH = "/Applications/LEAPClient.app/" + + INSTALL_PATH = "/Applications/LEAP\ Client.app" # OPENVPN_BIN = "/%s/Contents/Resources/openvpn.leap" % ( # self.INSTALL_PATH,) - UP_SCRIPT = "/%s/client.up.sh" % (INSTALL_PATH,) - DOWN_SCRIPT = "/%s/client.down.sh" % (INSTALL_PATH,) + OPENVPN_BIN = 'openvpn.leap' + OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,) + + UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,) + DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,) + OPENVPN_DOWN_PLUGIN = '%s/openvpn-down-root.so' % (OPENVPN_PATH,) + + UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN) - # TODO: Add - # OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" + @classmethod + def missing_updown_scripts(kls): + """ + Returns what updown scripts are missing. + :rtype: list + """ + file_exist = partial(_has_updown_scripts, warn=False) + zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) + missing = filter(lambda (path, exists): exists is False, zipped) + return [path for path, exists in missing] + + @classmethod + def cmd_for_missing_scripts(kls, frompath): + """ + Returns a command that can copy the missing scripts. + :rtype: str + """ + to = kls.OPENVPN_PATH + cmd = "#!/bin/sh\nmkdir -p %s\ncp \"%s/\"* %s" % (to, frompath, to) + #return kls.OSX_ASADMIN % cmd + return cmd def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="unix"): @@ -365,20 +405,19 @@ class DarwinVPNLauncher(VPNLauncher): 'server' ] - # FIXME: sanitize this! -- - openvpn_configuration = eipconfig.get_openvpn_configuration() for key, value in openvpn_configuration.items(): args += ['--%s' % (key,), value] + user = getpass.getuser() args += [ - '--user', getpass.getuser(), + '--user', user, '--group', grp.getgrgid(os.getgroups()[-1]).gr_name ] if socket_port == "unix": args += [ - '--management-client-user', getpass.getuser() + '--management-client-user', user ] args += [ @@ -391,13 +430,21 @@ class DarwinVPNLauncher(VPNLauncher): args += [ '--up', self.UP_SCRIPT, ] + if _has_updown_scripts(self.DOWN_SCRIPT): args += [ - '--down', self.DOWN_SCRIPT, - # FIXME add down-plugin - # '--plugin', self.OPENVPN_DOWN_ROOT, - # '\'script_type=down %s\'' % self.DOWN_SCRIPT - ] + '--down', self.DOWN_SCRIPT] + + # should have the down script too + if _has_updown_scripts(self.OPENVPN_DOWN_PLUGIN): + args += [ + '--plugin', self.OPENVPN_DOWN_PLUGIN, + '\'%s\'' % self.DOWN_SCRIPT + ] + + # we set user to be passed to the up/down scripts + args += [ + '--setenv', "LEAPUSER", "%s" % (user,)] args += [ '--cert', eipconfig.get_client_cert_path(providerconfig), @@ -405,6 +452,9 @@ class DarwinVPNLauncher(VPNLauncher): '--ca', providerconfig.get_ca_cert_path() ] + # We are using osascript until we can write a proper wrapper + # for privilege escalation. + command = self.OSASCRIPT_BIN cmd_args = ["-e", self.OSX_ASADMIN % (' '.join(args),)] diff --git a/src/leap/util/leap_log_handler.py b/src/leap/util/leap_log_handler.py index 3264e05c..5b8ae789 100644 --- a/src/leap/util/leap_log_handler.py +++ b/src/leap/util/leap_log_handler.py @@ -18,10 +18,10 @@ Custom handler for the logger window. """ import logging -from functools import partial from PySide import QtCore + class LogHandler(logging.Handler): """ This is the custom handler that implements our desired formatting @@ -36,6 +36,11 @@ class LogHandler(logging.Handler): _log_history = [] def __init__(self, qtsignal): + """ + LogHander initialization. + Calls parent method and keeps a reference to the qtsignal + that will be used to fire the gui update. + """ logging.Handler.__init__(self) self._qtsignal = qtsignal @@ -119,6 +124,10 @@ class LeapLogHandler(QtCore.QObject, HandlerAdapter): new_log = QtCore.Signal(dict) def __init__(self): + """ + LeapLogHandler initialization. + Initializes parent classes. + """ QtCore.QObject.__init__(self) HandlerAdapter.__init__(self, qtsignal=self.qtsignal) -- cgit v1.2.3 From cd11784b8fdf0cb45783e8d6a8e9b5288f34820d Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 13 Jun 2013 22:48:29 +0900 Subject: pep8 --- src/leap/gui/mainwindow.py | 8 +++++--- src/leap/platform_init/initializers.py | 2 ++ src/leap/services/abstractbootstrapper.py | 18 +++++++++--------- src/leap/services/eip/eipbootstrapper.py | 4 ++-- src/leap/services/eip/vpnlaunchers.py | 2 +- src/leap/services/mail/smtpbootstrapper.py | 9 ++++----- 6 files changed, 23 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index afa7e947..23084102 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -219,10 +219,12 @@ class MainWindow(QtGui.QMainWindow): self._systray = None - self._action_eip_status = QtGui.QAction(self.tr("Encrypted internet is OFF"), - self) + self._action_eip_status = QtGui.QAction( + self.tr("Encrypted internet is OFF"), + self) self._action_eip_status.setEnabled(False) - self._action_eip_startstop = QtGui.QAction(self.tr("Turn encryption ON"), self) + self._action_eip_startstop = QtGui.QAction( + self.tr("Turn encryption ON"), self) self._action_eip_startstop.triggered.connect( self._stop_eip) self._action_eip_write = QtGui.QAction( diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index 9dd31a18..d72dc61f 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -79,6 +79,7 @@ def _windows_has_tap_device(): pass return False + def _get_missing_updown_dialog(): """ Creates a dialog for notifying of missing updown scripts. @@ -102,6 +103,7 @@ def _get_missing_updown_dialog(): msg.setDefaultButton(QtGui.QMessageBox.Yes) return msg + def WindowsInitializer(): """ Raises a dialog in case that the windows tap driver has not been found diff --git a/src/leap/services/abstractbootstrapper.py b/src/leap/services/abstractbootstrapper.py index 7bebdc15..2cbd56bc 100644 --- a/src/leap/services/abstractbootstrapper.py +++ b/src/leap/services/abstractbootstrapper.py @@ -48,14 +48,14 @@ class AbstractBootstrapper(QtCore.QObject): """ QtCore.QObject.__init__(self) - leap_assert(self._gui_errback.im_func == \ - AbstractBootstrapper._gui_errback.im_func, + leap_assert(self._gui_errback.im_func == + AbstractBootstrapper._gui_errback.im_func, "Cannot redefine _gui_errback") - leap_assert(self._errback.im_func == \ - AbstractBootstrapper._errback.im_func, + leap_assert(self._errback.im_func == + AbstractBootstrapper._errback.im_func, "Cannot redefine _errback") - leap_assert(self._gui_notify.im_func == \ - AbstractBootstrapper._gui_notify.im_func, + leap_assert(self._gui_notify.im_func == + AbstractBootstrapper._gui_notify.im_func, "Cannot redefine _gui_notify") # **************************************************** # @@ -87,9 +87,9 @@ class AbstractBootstrapper(QtCore.QObject): if self._err_msg is not None \ else str(failure.value) self._signal_to_emit.emit({ - self.PASSED_KEY: False, - self.ERROR_KEY: err_msg - }) + self.PASSED_KEY: False, + self.ERROR_KEY: err_msg + }) failure.trap(Exception) def _errback(self, failure, signal=None): diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 51c3dab4..4da8f90f 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -173,8 +173,8 @@ class EIPBootstrapper(AbstractBootstrapper): cb_chain = [ (self._download_config, self.download_config), - (self._download_client_certificates, \ - self.download_client_certificate) + (self._download_client_certificates, + self.download_client_certificate) ] self.addCallbackChain(cb_chain) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index d15aed82..6c2ff006 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -319,7 +319,7 @@ class DarwinVPNLauncher(VPNLauncher): UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,) DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,) OPENVPN_DOWN_PLUGIN = '%s/openvpn-down-root.so' % (OPENVPN_PATH,) - + UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN) @classmethod diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py index ea480c6d..e8af5349 100644 --- a/src/leap/services/mail/smtpbootstrapper.py +++ b/src/leap/services/mail/smtpbootstrapper.py @@ -94,11 +94,10 @@ class SMTPBootstrapper(AbstractBootstrapper): # Not modified if res.status_code == 304: logger.debug("SMTP definition has not been modified") - self._smtp_config.load(os.path.join("leap", - "providers", - self._provider_config.\ - get_domain(), - "smtp-service.json")) + self._smtp_config.load(os.path.join( + "leap", "providers", + self._provider_config.get_domain(), + "smtp-service.json")) else: smtp_definition, mtime = get_content(res) -- cgit v1.2.3 From 09f1399db76a5c64cc38d45510b18cecd3a3d457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 13 Jun 2013 10:49:21 -0300 Subject: If eip fails to autostart, mark it as such --- src/leap/gui/mainwindow.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 65f5fe3d..752f5ef1 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -186,7 +186,7 @@ class MainWindow(QtGui.QMainWindow): self._eip_bootstrapper = EIPBootstrapper() self._eip_bootstrapper.download_config.connect( - self._intermediate_stage) + self._eip_intermediate_stage) self._eip_bootstrapper.download_client_certificate.connect( self._finish_eip_bootstrap) @@ -1140,6 +1140,7 @@ class MainWindow(QtGui.QMainWindow): else: self._set_eip_status(data[self._eip_bootstrapper.ERROR_KEY], error=True) + self._already_started_eip = False def _logout(self): """ @@ -1184,6 +1185,21 @@ class MainWindow(QtGui.QMainWindow): self._login_set_enabled(True) self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) + def _eip_intermediate_stage(self, data): + """ + SLOT + TRIGGERS: + self._eip_bootstrapper.download_config + + If there was a problem, displays it, otherwise it does nothing. + This is used for intermediate bootstrapping stages, in case + they fail. + """ + passed = data[self._provider_bootstrapper.PASSED_KEY] + if not passed: + self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) + self._already_started_eip = False + def _eip_finished(self, exitCode): """ SLOT -- cgit v1.2.3 From b2f2831d6fc090a508437a073267d5a9e2bd5e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 12 Jun 2013 14:22:16 -0300 Subject: Refactor login to its own widget and remove Utils menu --- src/leap/config/leapsettings.py | 19 ---- src/leap/gui/login.py | 213 +++++++++++++++++++++++++++++++++++++++ src/leap/gui/mainwindow.py | 199 ++++++++++++++++++------------------- src/leap/gui/ui/login.ui | 129 ++++++++++++++++++++++++ src/leap/gui/ui/mainwindow.ui | 214 +++++++++++++++------------------------- 5 files changed, 519 insertions(+), 255 deletions(-) create mode 100644 src/leap/gui/login.py create mode 100644 src/leap/gui/ui/login.ui (limited to 'src') diff --git a/src/leap/config/leapsettings.py b/src/leap/config/leapsettings.py index ab0c1860..88b7d8c9 100644 --- a/src/leap/config/leapsettings.py +++ b/src/leap/config/leapsettings.py @@ -62,7 +62,6 @@ class LeapSettings(object): GEOMETRY_KEY = "Geometry" WINDOWSTATE_KEY = "WindowState" USER_KEY = "User" - AUTOLOGIN_KEY = "AutoLogin" PROPERPROVIDER_KEY = "ProperProvider" REMEMBER_KEY = "RememberUserAndPass" DEFAULTPROVIDER_KEY = "DefaultProvider" @@ -192,24 +191,6 @@ class LeapSettings(object): leap_assert_type(remember, bool) self._settings.setValue(self.REMEMBER_KEY, remember) - def get_autologin(self): - """ - Returns True if the app should automatically login, False otherwise - - :rtype: bool - """ - return to_bool(self._settings.value(self.AUTOLOGIN_KEY, False)) - - def set_autologin(self, autologin): - """ - Sets whether the app should automatically login - - :param autologin: True if the app should autologin, False otherwise - :type autologin: bool - """ - leap_assert_type(autologin, bool) - self._settings.setValue(self.AUTOLOGIN_KEY, autologin) - # TODO: make this scale with multiple providers, we are assuming # just one for now def get_properprovider(self): diff --git a/src/leap/gui/login.py b/src/leap/gui/login.py new file mode 100644 index 00000000..cbd10fd0 --- /dev/null +++ b/src/leap/gui/login.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- +# login.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Login widget implementation +""" +import logging +import keyring + +from PySide import QtCore, QtGui +from ui_login import Ui_LoginWidget + +logger = logging.getLogger(__name__) + + +class LoginWidget(QtGui.QWidget): + """ + Login widget that emits signals to display the wizard or to + perform login. + """ + + # Emitted when the login button is clicked + login = QtCore.Signal() + # Emitted when the user selects "Other..." in the provider + # combobox or click "Create Account" + show_wizard = QtCore.Signal() + + def __init__(self, settings, parent=None): + """ + Constructs the LoginWidget. + + :param settings: client wide settings + :type settings: LeapSettings + :param parent: The parent widget for this widget + :type parent: QWidget or None + """ + QtGui.QWidget.__init__(self, parent) + + self._settings = settings + self._selected_provider_index = -1 + + self.ui = Ui_LoginWidget() + self.ui.setupUi(self) + + self.ui.chkRemember.stateChanged.connect( + self._remember_state_changed) + self.ui.chkRemember.setEnabled(keyring.get_keyring() is not None) + + self.ui.lnPassword.setEchoMode(QtGui.QLineEdit.Password) + + self.ui.btnLogin.clicked.connect(self.login) + self.ui.lnPassword.returnPressed.connect(self.login) + + self.ui.lnUser.returnPressed.connect(self._focus_password) + + self.ui.cmbProviders.currentIndexChanged.connect( + self._current_provider_changed) + self.ui.btnCreateAccount.clicked.connect( + self.show_wizard) + + def _remember_state_changed(self, state): + """ + Saves the remember state in the LeapSettings + + :param state: possible stats can be Checked, Unchecked and + PartiallyChecked + :type state: QtCore.Qt.CheckState + """ + enable = True if state == QtCore.Qt.Checked else False + self._settings.set_remember(enable) + + def set_providers(self, provider_list): + """ + Set the provider list to provider_list plus an "Other..." item + that triggers the wizard + + :param provider_list: list of providers + :type provider_list: list of str + """ + self.ui.cmbProviders.blockSignals(True) + self.ui.cmbProviders.clear() + self.ui.cmbProviders.addItems(provider_list + ["Other..."]) + self.ui.cmbProviders.blockSignals(False) + + def select_provider_by_name(self, name): + """ + Given a provider name/domain, it selects it in the combobox + + :param name: name or domain for the provider + :type name: str + """ + provider_index = self.ui.cmbProviders.findText(name) + self.ui.cmbProviders.setCurrentIndex(provider_index) + + def get_selected_provider(self): + """ + Returns the selected provider in the combobox + """ + return self.ui.cmbProviders.currentText() + + def set_remember(self, value): + """ + Checks the remember user and password checkbox + + :param value: True to mark it checked, False otherwise + :type value: bool + """ + self.ui.chkRemember.setChecked(value) + + def get_remember(self): + """ + Returns the remember checkbox state + + :rtype: bool + """ + return self.ui.chkRemember.isChecked() + + def set_user(self, user): + """ + Sets the user and focuses on the next field, password. + + :param user: user to set the field to + :type user: str + """ + self.ui.lnUser.setText(user) + self._focus_password() + + def get_user(self): + """ + Returns the user that appears in the widget + + :rtype: str + """ + return self.ui.lnUser.text() + + def set_password(self, password): + """ + Sets the password for the widget + + :param password: password to set + :type password: str + """ + self.ui.lnPassword.setText(password) + + def get_password(self): + """ + Returns the password that appears in the widget + + :rtype: str + """ + return self.ui.lnPassword.text() + + def set_status(self, status, error=True): + """ + Sets the status label at the login stage to status + + :param status: status message + :type status: str + """ + if error: + status = "%s" % (status,) + self.ui.lblStatus.setText(status) + + def set_enabled(self, enabled=False): + """ + Enables or disables all the login widgets + + :param enabled: wether they should be enabled or not + :type enabled: bool + """ + self.ui.lnUser.setEnabled(enabled) + self.ui.lnPassword.setEnabled(enabled) + self.ui.btnLogin.setEnabled(enabled) + self.ui.chkRemember.setEnabled(enabled) + self.ui.cmbProviders.setEnabled(enabled) + + def _focus_password(self): + """ + Focuses in the password lineedit + """ + self.ui.lnPassword.setFocus() + + def _current_provider_changed(self, param): + """ + SLOT + TRIGGERS: self.ui.cmbProviders.currentIndexChanged + """ + if param == (self.ui.cmbProviders.count() - 1): + self.show_wizard.emit() + # Leave the previously selected provider in the combobox + prev_provider = 0 + if self._selected_provider_index != -1: + prev_provider = self._selected_provider_index + self.ui.cmbProviders.blockSignals(True) + self.ui.cmbProviders.setCurrentIndex(prev_provider) + self.ui.cmbProviders.blockSignals(False) + else: + self._selected_provider_index = param + diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 0e388e64..61eb8ca4 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -37,6 +37,7 @@ from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth from leap.gui.loggerwindow import LoggerWindow from leap.gui.wizard import Wizard +from leap.gui.login import LoginWidget from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.providerbootstrapper import ProviderBootstrapper @@ -135,11 +136,18 @@ class MainWindow(QtGui.QMainWindow): self.ui = Ui_MainWindow() self.ui.setupUi(self) - self.ui.lnPassword.setEchoMode(QtGui.QLineEdit.Password) + self._settings = LeapSettings(standalone) + + self._login_widget = LoginWidget( + self._settings, + self.ui.stackedWidget.widget(self.LOGIN_INDEX)) + self.ui.loginLayout.addWidget(self._login_widget) + + self._login_widget.login.connect(self._login) + self._login_widget.show_wizard.connect( + self._launch_wizard) - self.ui.btnLogin.clicked.connect(self._login) - self.ui.lnUser.returnPressed.connect(self._focus_password) - self.ui.lnPassword.returnPressed.connect(self._login) + self.ui.btnShowLog.clicked.connect(self._show_logger_window) self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) @@ -206,10 +214,6 @@ class MainWindow(QtGui.QMainWindow): self._vpn.qtsigs.process_finished.connect( self._eip_finished) - self.ui.chkRemember.stateChanged.connect( - self._remember_state_changed) - self.ui.chkRemember.setEnabled(keyring.get_keyring() is not None) - self.ui.action_sign_out.setEnabled(False) self.ui.action_sign_out.triggered.connect(self._logout) self.ui.action_about_leap.triggered.connect(self._about) @@ -244,7 +248,6 @@ class MainWindow(QtGui.QMainWindow): self._action_visible.triggered.connect(self._toggle_visible) self._enabled_services = [] - self._settings = LeapSettings(standalone) self._center_window() @@ -281,6 +284,13 @@ class MainWindow(QtGui.QMainWindow): self._finish_init() def _rejected_wizard(self): + """ + SLOT + TRIGGERS: self._wizard.rejected + + Called if the wizard has been cancelled or closed before + finishing. + """ if self._wizard_firstrun: self._settings.set_properprovider(False) self.quit() @@ -288,11 +298,24 @@ class MainWindow(QtGui.QMainWindow): self._finish_init() def _launch_wizard(self): + """ + SLOT + TRIGGERS: + self._login_widget.show_wizard + self.ui.action_wizard.triggered + + Also called in first run. + + Launches the wizard, creating the object itself if not already + there. + """ if self._wizard is None: self._wizard = Wizard(bypass_checks=self._bypass_checks) self._wizard.accepted.connect(self._finish_init) + self.setVisible(False) self._wizard.exec_() self._wizard = None + self.setVisible(True) def _get_leap_logging_handler(self): """ @@ -310,6 +333,11 @@ class MainWindow(QtGui.QMainWindow): def _show_logger_window(self): """ + SLOT + TRIGGERS: + self.ui.action_show_logs.triggered + self.ui.btnShowLog.clicked + Displays the window with the history of messages logged until now and displays the new ones on arrival. """ @@ -319,14 +347,11 @@ class MainWindow(QtGui.QMainWindow): logger.error('Leap logger handler not found') else: self._logger_window = LoggerWindow(handler=leap_log_handler) - self._logger_window.show() + self._logger_window.setVisible(not self._logger_window.isVisible()) + self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) else: - self._logger_window.show() - - def _remember_state_changed(self, state): - enable = True if state == QtCore.Qt.Checked else False - self.ui.chkAutoLogin.setEnabled(enable) - self._settings.set_remember(enable) + self._logger_window.setVisible(not self._logger_window.isVisible()) + self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) def _new_updates_available(self, req): """ @@ -377,8 +402,21 @@ class MainWindow(QtGui.QMainWindow): msg) def _finish_init(self): - self.ui.cmbProviders.clear() - self.ui.cmbProviders.addItems(self._configured_providers()) + """ + SLOT + TRIGGERS: + self._wizard.accepted + + Also called at the end of the constructor if not first run, + and after _rejected_wizard if not first run. + + Implements the behavior after either constructing the + mainwindow object, loading the saved user/password, or after + the wizard has been executed. + """ + # XXX: May be this can be divided into two methods? + + self._login_widget.set_providers(self._configured_providers()) self._show_systray() self.show() @@ -388,20 +426,18 @@ class MainWindow(QtGui.QMainWindow): # select the configured provider in the combo box domain = self._wizard.get_domain() - provider_index = self.ui.cmbProviders.findText(domain) - self.ui.cmbProviders.setCurrentIndex(provider_index) + self._login_widget.select_provider_by_name(domain) - self.ui.chkRemember.setChecked(self._wizard.get_remember()) + self._login_widget.set_remember(self._wizard.get_remember()) self._enabled_services = list(self._wizard.get_services()) self._settings.set_enabled_services( - self.ui.cmbProviders.currentText(), + self._login_widget.get_selected_provider(), self._enabled_services) if possible_username is not None: - self.ui.lnUser.setText(possible_username) - self._focus_password() + self._login_widget.set_user(possible_username) if possible_password is not None: - self.ui.lnPassword.setText(possible_password) - self.ui.chkRemember.setChecked(True) + self._login_widget.set_password(possible_password) + self._login_widget.set_remember(True) self._login() self._wizard = None self._settings.set_properprovider(True) @@ -412,7 +448,6 @@ class MainWindow(QtGui.QMainWindow): return saved_user = self._settings.get_user() - auto_login = self._settings.get_autologin() try: username, domain = saved_user.split('@') @@ -423,15 +458,12 @@ class MainWindow(QtGui.QMainWindow): if saved_user is not None: # fill the username - self.ui.lnUser.setText(username) + self._login_widget.set_user(username) # select the configured provider in the combo box - provider_index = self.ui.cmbProviders.findText(domain) - self.ui.cmbProviders.setCurrentIndex(provider_index) + self._login_widget.select_provider_by_name(domain) - self.ui.chkRemember.setChecked(True) - self.ui.chkAutoLogin.setEnabled(self.ui.chkRemember - .isEnabled()) + self._login_widget.set_remember(True) saved_password = None try: @@ -442,12 +474,7 @@ class MainWindow(QtGui.QMainWindow): logger.debug("Incorrect Password. %r." % (e,)) if saved_password is not None: - self.ui.lnPassword.setText(saved_password.decode("utf8")) - - # Only automatically login if there is a saved user - # and the password was retrieved right - self.ui.chkAutoLogin.setChecked(auto_login) - if auto_login and saved_password: + self._login_widget.set_password(saved_password.decode("utf8")) self._login() def _try_autostart_eip(self): @@ -543,6 +570,9 @@ class MainWindow(QtGui.QMainWindow): def _about(self): """ + SLOT + TRIGGERS: self.ui.action_about_leap.triggered + Display the About LEAP dialog """ QtGui.QMessageBox.about( @@ -580,7 +610,6 @@ class MainWindow(QtGui.QMainWindow): self._settings.set_geometry(self.saveGeometry()) self._settings.set_windowstate(self.saveState()) - self._settings.set_autologin(self.ui.chkAutoLogin.isChecked()) QtGui.QMainWindow.closeEvent(self, e) @@ -615,23 +644,6 @@ class MainWindow(QtGui.QMainWindow): is_proper_provider = self._settings.get_properprovider() return not (has_provider_on_disk and is_proper_provider) - def _focus_password(self): - """ - Focuses in the password lineedit - """ - self.ui.lnPassword.setFocus() - - def _set_status(self, status, error=True): - """ - Sets the status label at the login stage to status - - :param status: status message - :type status: str - """ - if error: - status = "%s" % (status,) - self.ui.lblStatus.setText(status) - def _set_eip_status(self, status, error=False): """ Sets the status label at the VPN stage to status @@ -644,28 +656,13 @@ class MainWindow(QtGui.QMainWindow): status = "%s" % (status,) self.ui.lblEIPStatus.setText(status) - def _login_set_enabled(self, enabled=False): - """ - Enables or disables all the login widgets - - :param enabled: wether they should be enabled or not - :type enabled: bool - """ - self.ui.lnUser.setEnabled(enabled) - self.ui.lnPassword.setEnabled(enabled) - self.ui.btnLogin.setEnabled(enabled) - self.ui.chkRemember.setEnabled(enabled) - if not enabled: - self.ui.chkAutoLogin.setEnabled(False) - self.ui.cmbProviders.setEnabled(enabled) - def _download_provider_config(self): """ Starts the bootstrapping sequence. It will download the provider configuration if it's not present, otherwise will emit the corresponding signals inmediately """ - provider = self.ui.cmbProviders.currentText() + provider = self._login_widget.get_selected_provider() self._provider_bootstrapper.run_provider_select_checks( provider, @@ -685,7 +682,7 @@ class MainWindow(QtGui.QMainWindow): :type data: dict """ if data[self._provider_bootstrapper.PASSED_KEY]: - provider = self.ui.cmbProviders.currentText() + provider = self._login_widget.get_selected_provider() if self._provider_config.loaded() or \ self._provider_config.load(os.path.join("leap", "providers", @@ -695,12 +692,12 @@ class MainWindow(QtGui.QMainWindow): self._provider_config, download_if_needed=True) else: - self._set_status( + self._login_widget.set_status( self.tr("Could not load provider configuration")) - self._login_set_enabled(True) + self._login_widget.set_enabled(True) else: - self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) - self._login_set_enabled(True) + self._login_widget.set_status(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.set_enabled(True) def _login(self): """ @@ -716,29 +713,29 @@ class MainWindow(QtGui.QMainWindow): """ leap_assert(self._provider_config, "We need a provider config") - username = self.ui.lnUser.text() - password = self.ui.lnPassword.text() - provider = self.ui.cmbProviders.currentText() + username = self._login_widget.get_user() + password = self._login_widget.get_password() + provider = self._login_widget.get_selected_provider() self._enabled_services = self._settings.get_enabled_services( - self.ui.cmbProviders.currentText()) + self._login_widget.get_selected_provider()) if len(provider) == 0: - self._set_status(self.tr("Please select a valid provider")) + self._login_widget.set_status(self.tr("Please select a valid provider")) return if len(username) == 0: - self._set_status(self.tr("Please provide a valid username")) + self._login_widget.set_status(self.tr("Please provide a valid username")) return if len(password) == 0: - self._set_status(self.tr("Please provide a valid Password")) + self._login_widget.set_status(self.tr("Please provide a valid Password")) return - self._set_status(self.tr("Logging in..."), error=False) - self._login_set_enabled(False) + self._login_widget.set_status(self.tr("Logging in..."), error=False) + self._login_widget.set_enabled(False) - if self.ui.chkRemember.isChecked(): + if self._login_widget.get_remember(): # in the keyring and in the settings # we store the value 'usename@provider' username_domain = (username + '@' + provider).encode("utf8") @@ -766,8 +763,8 @@ class MainWindow(QtGui.QMainWindow): leap_assert(self._provider_config, "We need a provider config!") if data[self._provider_bootstrapper.PASSED_KEY]: - username = self.ui.lnUser.text().encode("utf8") - password = self.ui.lnPassword.text().encode("utf8") + username = self._login_widget.get_user().encode("utf8") + password = self._login_widget.get_password().encode("utf8") if self._srp_auth is None: self._srp_auth = SRPAuth(self._provider_config) @@ -779,8 +776,8 @@ class MainWindow(QtGui.QMainWindow): # TODO: Add errback! self._login_defer = self._srp_auth.authenticate(username, password) else: - self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) - self._login_set_enabled(True) + self._login_widget.set_status(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.set_enabled(True) def _authentication_finished(self, ok, message): """ @@ -790,7 +787,7 @@ class MainWindow(QtGui.QMainWindow): Once the user is properly authenticated, try starting the EIP service """ - self._set_status(message, error=not ok) + self._login_widget.set_status(message, error=not ok) if ok: self.ui.action_sign_out.setEnabled(True) # We leave a bit of room for the user to see the @@ -799,7 +796,7 @@ class MainWindow(QtGui.QMainWindow): QtCore.QTimer.singleShot(1000, self._switch_to_status) self._login_defer = None else: - self._login_set_enabled(True) + self._login_widget.set_enabled(True) def _switch_to_status(self): """ @@ -810,8 +807,8 @@ class MainWindow(QtGui.QMainWindow): self._soledad_bootstrapper.run_soledad_setup_checks( self._provider_config, - self.ui.lnUser.text(), - self.ui.lnPassword.text(), + self._login_widget.get_user(), + self._login_widget.get_password(), download_if_needed=True) self._download_eip_config() @@ -1163,9 +1160,9 @@ class MainWindow(QtGui.QMainWindow): """ self.ui.action_sign_out.setEnabled(False) self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) - self.ui.lnPassword.setText("") - self._login_set_enabled(True) - self._set_status("") + self._login_widget.set_password("") + self._login_widget.set_enabled(True) + self._login_widget.set_status("") def _intermediate_stage(self, data): """ @@ -1182,8 +1179,8 @@ class MainWindow(QtGui.QMainWindow): """ passed = data[self._provider_bootstrapper.PASSED_KEY] if not passed: - self._login_set_enabled(True) - self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.set_enabled(True) + self._login_widget.set_status(data[self._provider_bootstrapper.ERROR_KEY]) def _eip_finished(self, exitCode): """ diff --git a/src/leap/gui/ui/login.ui b/src/leap/gui/ui/login.ui new file mode 100644 index 00000000..88c9ef44 --- /dev/null +++ b/src/leap/gui/ui/login.ui @@ -0,0 +1,129 @@ + + + LoginWidget + + + + 0 + 0 + 356 + 219 + + + + Form + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Create a new account + + + + + + + <b>Provider:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + + Remember username and password + + + + + + + <b>Username:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + <b>Password:</b> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Log In + + + + + + + + + + Qt::AlignCenter + + + + + + + cmbProviders + lnUser + lnPassword + chkRemember + btnLogin + btnCreateAccount + + + + diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui index fdf5c167..ae895dce 100644 --- a/src/leap/gui/ui/mainwindow.ui +++ b/src/leap/gui/ui/mainwindow.ui @@ -6,7 +6,7 @@ 0 0 - 415 + 429 579 @@ -28,19 +28,6 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -54,18 +41,21 @@ - - - - Qt::Horizontal + + + + false - - - 40 - 20 - + + - + + :/images/leap-color-big.png + + + Qt::AlignCenter + +
@@ -80,25 +70,18 @@ - + - 1 + 0 - + - - - - Remember - - - - - + + - - + + Qt::Horizontal @@ -110,15 +93,8 @@ - - - - Login - - - - - + + Qt::Horizontal @@ -130,66 +106,6 @@ - - - - <b>Provider:</b> - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - <b>Password:</b> - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - - - - - <b>User:</b> - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - Qt::AlignCenter - - - - - - - false - - - Automatically login - - - @@ -303,21 +219,31 @@ - - - - false + + + + Qt::Horizontal - - + + + 40 + 20 + - - :/images/leap-color-big.png + + + + + + Qt::Vertical - - Qt::AlignCenter + + + 20 + 40 + - + @@ -388,6 +314,39 @@ + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Show Log + + + true + + + false + + + true + + + + + @@ -395,8 +354,8 @@ 0 0 - 415 - 21 + 429 + 25 @@ -415,15 +374,7 @@ - - - &Utils - - - - - @@ -458,13 +409,6 @@ - - lnUser - lnPassword - chkRemember - btnLogin - cmbProviders - -- cgit v1.2.3 From 5e8feb737d0a0fb8065cda583c88498385ff7b9f Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 13 Jun 2013 23:37:13 +0900 Subject: pep8 --- src/leap/gui/login.py | 1 - src/leap/gui/mainwindow.py | 24 ++++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/leap/gui/login.py b/src/leap/gui/login.py index cbd10fd0..c367b2fb 100644 --- a/src/leap/gui/login.py +++ b/src/leap/gui/login.py @@ -210,4 +210,3 @@ class LoginWidget(QtGui.QWidget): self.ui.cmbProviders.blockSignals(False) else: self._selected_provider_index = param - diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 61eb8ca4..01707554 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -347,7 +347,8 @@ class MainWindow(QtGui.QMainWindow): logger.error('Leap logger handler not found') else: self._logger_window = LoggerWindow(handler=leap_log_handler) - self._logger_window.setVisible(not self._logger_window.isVisible()) + self._logger_window.setVisible( + not self._logger_window.isVisible()) self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) else: self._logger_window.setVisible(not self._logger_window.isVisible()) @@ -474,7 +475,8 @@ class MainWindow(QtGui.QMainWindow): logger.debug("Incorrect Password. %r." % (e,)) if saved_password is not None: - self._login_widget.set_password(saved_password.decode("utf8")) + self._login_widget.set_password( + saved_password.decode("utf8")) self._login() def _try_autostart_eip(self): @@ -696,7 +698,8 @@ class MainWindow(QtGui.QMainWindow): self.tr("Could not load provider configuration")) self._login_widget.set_enabled(True) else: - self._login_widget.set_status(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.set_status( + data[self._provider_bootstrapper.ERROR_KEY]) self._login_widget.set_enabled(True) def _login(self): @@ -721,15 +724,18 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.get_selected_provider()) if len(provider) == 0: - self._login_widget.set_status(self.tr("Please select a valid provider")) + self._login_widget.set_status( + self.tr("Please select a valid provider")) return if len(username) == 0: - self._login_widget.set_status(self.tr("Please provide a valid username")) + self._login_widget.set_status( + self.tr("Please provide a valid username")) return if len(password) == 0: - self._login_widget.set_status(self.tr("Please provide a valid Password")) + self._login_widget.set_status( + self.tr("Please provide a valid Password")) return self._login_widget.set_status(self.tr("Logging in..."), error=False) @@ -776,7 +782,8 @@ class MainWindow(QtGui.QMainWindow): # TODO: Add errback! self._login_defer = self._srp_auth.authenticate(username, password) else: - self._login_widget.set_status(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.set_status( + data[self._provider_bootstrapper.ERROR_KEY]) self._login_widget.set_enabled(True) def _authentication_finished(self, ok, message): @@ -1180,7 +1187,8 @@ class MainWindow(QtGui.QMainWindow): passed = data[self._provider_bootstrapper.PASSED_KEY] if not passed: self._login_widget.set_enabled(True) - self._login_widget.set_status(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.set_status( + data[self._provider_bootstrapper.ERROR_KEY]) def _eip_finished(self, exitCode): """ -- cgit v1.2.3 From b8a83ca21a0a44487097dbf41735e3bad176d5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 12 Jun 2013 11:22:08 -0300 Subject: Always show context menu when clicking the systray --- src/leap/gui/mainwindow.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 01707554..3c6c579c 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -523,31 +523,41 @@ class MainWindow(QtGui.QMainWindow): self._systray.setContextMenu(systrayMenu) self._systray.setIcon(QtGui.QIcon(self.ERROR_ICON)) self._systray.setVisible(True) - self._systray.activated.connect(self._toggle_visible) + self._systray.activated.connect(self._tray_activated) - def _toggle_visible(self, reason=None): + def _tray_activated(self, reason=None): """ SLOT TRIGGER: self._systray.activated - Toggles the window visibility + Displays the context menu from the tray icon """ get_action = lambda visible: ( - self.tr("Show"), - self.tr("Hide"))[int(visible)] - - minimized = self.isMinimized() + self.tr("Show Main Window"), + self.tr("Hide Main Window"))[int(visible)] if reason != QtGui.QSystemTrayIcon.Context: - # do show - if minimized: - self.showNormal() - self.setVisible(not self.isVisible()) - # set labels visible = self.isVisible() self._action_visible.setText(get_action(visible)) + context_menu = self._systray.contextMenu() + # for some reason, context_menu.show() + # is failing in a way beyond my understanding. + # (not working the first time it's clicked). + # this works however. + # XXX in osx it shows some glitches. + context_menu.exec_(self._systray.geometry().center()) + + def _toggle_visible(self): + """ + SLOT + TRIGGER: self._action_visible.triggered + + Toggles the window visibility + """ + self.setVisible(not self.isVisible()) + def _center_window(self): """ Centers the mainwindow based on the desktop geometry -- cgit v1.2.3 From c3fa54bc8628ba1618aba7b09a0daf458e798d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 13 Jun 2013 15:39:21 -0300 Subject: Don't autostart EIP if the user explicitly stops the service --- src/leap/config/leapsettings.py | 8 +++++--- src/leap/gui/mainwindow.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/config/leapsettings.py b/src/leap/config/leapsettings.py index 88b7d8c9..35010280 100644 --- a/src/leap/config/leapsettings.py +++ b/src/leap/config/leapsettings.py @@ -227,10 +227,12 @@ class LeapSettings(object): Sets the default provider to be used for autostarting EIP :param provider: provider to use - :type provider: str + :type provider: str or None """ - leap_assert(len(provider) > 0, "We cannot save an empty provider") - self._settings.setValue(self.DEFAULTPROVIDER_KEY, provider) + if provider is None: + self._settings.remove(self.DEFAULTPROVIDER_KEY) + else: + self._settings.setValue(self.DEFAULTPROVIDER_KEY, provider) def get_alert_missing_scripts(self): """ diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index e135c798..381dbf19 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -1012,6 +1012,7 @@ class MainWindow(QtGui.QMainWindow): self._action_eip_startstop.triggered.connect( self._start_eip) self._already_started_eip = False + self._settings.set_defaultprovider(None) def _get_best_provider_config(self): """ -- cgit v1.2.3 From cfd60d6e47a351e1ce52709a497e5a0b03319dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 13 Jun 2013 15:14:15 -0300 Subject: Refactor the status bits out of the MainWindow to StatusPanelWidget --- src/leap/gui/mainwindow.py | 238 +++++++++++--------------------- src/leap/gui/statuspanel.py | 216 +++++++++++++++++++++++++++++ src/leap/gui/ui/mainwindow.ui | 304 ++++++++++++++--------------------------- src/leap/gui/ui/statuspanel.ui | 248 +++++++++++++++++++++++++++++++++ 4 files changed, 646 insertions(+), 360 deletions(-) create mode 100644 src/leap/gui/statuspanel.py create mode 100644 src/leap/gui/ui/statuspanel.ui (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index e135c798..67fe34e3 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -38,14 +38,15 @@ from leap.crypto.srpauth import SRPAuth from leap.gui.loggerwindow import LoggerWindow from leap.gui.wizard import Wizard from leap.gui.login import LoginWidget +from leap.gui.statuspanel import StatusPanelWidget from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.providerbootstrapper import ProviderBootstrapper from leap.services.soledad.soledadbootstrapper import SoledadBootstrapper from leap.services.mail.smtpbootstrapper import SMTPBootstrapper -from leap.platform_init import IS_MAC, IS_WIN +from leap.platform_init import IS_WIN from leap.platform_init.initializers import init_platform -from leap.services.eip.vpnprocess import VPN, VPNManager +from leap.services.eip.vpnprocess import VPN from leap.services.eip.vpnlaunchers import (VPNLauncherException, OpenVPNNotFoundException, @@ -115,24 +116,6 @@ class MainWindow(QtGui.QMainWindow): self._updates_content = "" - if IS_MAC: - EIP_ICONS = ( - ":/images/conn_connecting-light.png", - ":/images/conn_connected-light.png", - ":/images/conn_error-light.png") - else: - EIP_ICONS = ( - ":/images/conn_connecting.png", - ":/images/conn_connected.png", - ":/images/conn_error.png") - - self.CONNECTING_ICON = QtGui.QPixmap(EIP_ICONS[0]) - self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1]) - self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2]) - - self.LOGGED_OUT_ICON = QtGui.QPixmap(":/images/leap-gray-big.png") - self.LOGGED_IN_ICON = QtGui.QPixmap(":/images/leap-color-big.png") - self.ui = Ui_MainWindow() self.ui.setupUi(self) @@ -149,11 +132,14 @@ class MainWindow(QtGui.QMainWindow): self.ui.btnShowLog.clicked.connect(self._show_logger_window) + self._status_panel = StatusPanelWidget( + self.ui.stackedWidget.widget(self.EIP_STATUS_INDEX)) + self.ui.statusLayout.addWidget(self._status_panel) + self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) - self.ui.btnEipStartStop.setEnabled(False) - self.ui.btnEipStartStop.clicked.connect( - self._start_eip) + self._status_panel.start_eip.connect(self._start_eip) + self._status_panel.stop_eip.connect(self._stop_eip) # This is loaded only once, there's a bug when doing that more # than once @@ -169,6 +155,7 @@ class MainWindow(QtGui.QMainWindow): # This is created once we have a valid provider config self._srp_auth = None + self._logged_user = None # This thread is always running, although it's quite # lightweight when it's done setting up provider @@ -209,8 +196,10 @@ class MainWindow(QtGui.QMainWindow): self._smtp_bootstrapped_stage) self._vpn = VPN() - self._vpn.qtsigs.state_changed.connect(self._update_vpn_state) - self._vpn.qtsigs.status_changed.connect(self._update_vpn_status) + self._vpn.qtsigs.state_changed.connect( + self._status_panel.update_vpn_state) + self._vpn.qtsigs.status_changed.connect( + self._status_panel.update_vpn_status) self._vpn.qtsigs.process_finished.connect( self._eip_finished) @@ -234,18 +223,14 @@ class MainWindow(QtGui.QMainWindow): self.tr("Encrypted internet is OFF"), self) self._action_eip_status.setEnabled(False) + + self._status_panel.set_action_eip_status( + self._action_eip_status) + self._action_eip_startstop = QtGui.QAction( - self.tr("Turn encryption ON"), self) + self.tr("Turn ON"), self) self._action_eip_startstop.triggered.connect( self._stop_eip) - self._action_eip_write = QtGui.QAction( - QtGui.QIcon(":/images/Arrow-Up-32.png"), - "%12.2f Kb" % (0.0,), self) - self._action_eip_write.setEnabled(False) - self._action_eip_read = QtGui.QAction( - QtGui.QIcon(":/images/Arrow-Down-32.png"), - "%12.2f Kb" % (0.0,), self) - self._action_eip_read.setEnabled(False) self._action_visible = QtGui.QAction(self.tr("Hide Main Window"), self) self._action_visible.triggered.connect(self._toggle_visible) @@ -527,10 +512,12 @@ class MainWindow(QtGui.QMainWindow): systrayMenu.addAction(self._action_eip_startstop) self._systray = QtGui.QSystemTrayIcon(self) self._systray.setContextMenu(systrayMenu) - self._systray.setIcon(QtGui.QIcon(self.ERROR_ICON)) + self._systray.setIcon(self._status_panel.ERROR_ICON) self._systray.setVisible(True) self._systray.activated.connect(self._tray_activated) + self._status_panel.set_systray(self._systray) + def _tray_activated(self, reason=None): """ SLOT @@ -662,18 +649,6 @@ class MainWindow(QtGui.QMainWindow): is_proper_provider = self._settings.get_properprovider() return not (has_provider_on_disk and is_proper_provider) - def _set_eip_status(self, status, error=False): - """ - Sets the status label at the VPN stage to status - - :param status: status message - :type status: str - """ - self._systray.setToolTip(status) - if error: - status = "%s" % (status,) - self.ui.lblEIPStatus.setText(status) - def _download_provider_config(self): """ Starts the bootstrapping sequence. It will download the @@ -812,6 +787,7 @@ class MainWindow(QtGui.QMainWindow): """ self._login_widget.set_status(message, error=not ok) if ok: + self._logged_user = self._login_widget.get_user() self.ui.action_sign_out.setEnabled(True) # We leave a bit of room for the user to see the # "Succeeded" message and then we switch to the EIP status @@ -826,6 +802,11 @@ class MainWindow(QtGui.QMainWindow): Changes the stackedWidget index to the EIP status one and triggers the eip bootstrapping """ + if not self._already_started_eip: + self._status_panel.set_provider( + "%s@%s" % (self._login_widget.get_user(), + self._provider_config.get_domain())) + self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) self._soledad_bootstrapper.run_soledad_setup_checks( @@ -883,13 +864,14 @@ class MainWindow(QtGui.QMainWindow): else: if self._enabled_services.count(self.MX_SERVICE) > 0: pass # TODO: show MX status - #self._set_eip_status( + #self._status_panel.set_eip_status( # self.tr("%s does not support MX") % # (self._provider_config.get_domain(),), # error=True) else: pass # TODO: show MX status - #self._set_eip_status(self.tr("MX is disabled")) + #self._status_panel.set_eip_status( + # self.tr("MX is disabled")) def _smtp_bootstrapped_stage(self, data): """ @@ -969,45 +951,55 @@ class MainWindow(QtGui.QMainWindow): self._settings.set_defaultprovider( provider_config.get_domain()) + + provider = self._provider_config.get_domain() + if self._logged_user is not None: + provider = "%s@%s" % (self._logged_user, provider) + + self._status_panel.set_provider(provider) + self._action_eip_provider.setText(provider_config.get_domain()) - self.ui.btnEipStartStop.setText(self.tr("Turn Encryption OFF")) - self.ui.btnEipStartStop.disconnect(self) - self.ui.btnEipStartStop.clicked.connect( - self._stop_eip) - self._action_eip_startstop.setText(self.tr("Turn Encryption OFF")) + + self._status_panel.eip_started() + + self._action_eip_startstop.setText(self.tr("Turn OFF")) self._action_eip_startstop.disconnect(self) self._action_eip_startstop.triggered.connect( self._stop_eip) except EIPNoPolkitAuthAgentAvailable: - self._set_eip_status(self.tr("We could not find any " - "authentication " - "agent in your system.
" - "Make sure you have " - "polkit-gnome-authentication-" - "agent-1 " - "running and try again."), - error=True) + self._status_panel.set_eip_status( + self.tr("We could not find any " + "authentication " + "agent in your system.
" + "Make sure you have " + "polkit-gnome-authentication-" + "agent-1 " + "running and try again."), + error=True) except EIPNoPkexecAvailable: - self._set_eip_status(self.tr("We could not find pkexec " - "in your system."), - error=True) + self._status_panel.set_eip_status( + self.tr("We could not find pkexec " + "in your system."), + error=True) except OpenVPNNotFoundException: - self._set_eip_status(self.tr("We couldn't find openvpn"), - error=True) + self._status_panel.set_eip_status( + self.tr("We couldn't find openvpn"), + error=True) except VPNLauncherException as e: - self._set_eip_status("%s" % (e,), error=True) + self._status_panel.set_eip_status("%s" % (e,), error=True) + else: + self._already_started_eip = True - self.ui.btnEipStartStop.setEnabled(True) + self._status_panel.set_startstop_enabled(True) def _stop_eip(self): self._vpn.terminate() - self._set_eip_status(self.tr("EIP has stopped")) - self._set_eip_status_icon("error") - self.ui.btnEipStartStop.setText(self.tr("Turn Encryption ON")) - self.ui.btnEipStartStop.disconnect(self) - self.ui.btnEipStartStop.clicked.connect( - self._start_eip) - self._action_eip_startstop.setText(self.tr("Turn Encryption ON")) + self._status_panel.set_eip_status(self.tr("Off")) + self._status_panel.set_eip_status_icon("error") + + self._status_panel.eip_stopped() + + self._action_eip_startstop.setText(self.tr("Turn ON")) self._action_eip_startstop.disconnect(self) self._action_eip_startstop.triggered.connect( self._start_eip) @@ -1048,92 +1040,21 @@ class MainWindow(QtGui.QMainWindow): self._enabled_services.count(self.OPENVPN_SERVICE) > 0 and \ not self._already_started_eip: - self._set_eip_status( - self.tr("Checking configuration, please wait...")) + self._status_panel.set_eip_status( + self.tr("Starting...")) self._eip_bootstrapper.run_eip_setup_checks( provider_config, download_if_needed=True) self._already_started_eip = True elif not self._already_started_eip: if self._enabled_services.count(self.OPENVPN_SERVICE) > 0: - self._set_eip_status(self.tr("%s does not support EIP") % - (provider_config.get_domain(),), - error=True) + self._status_panel.set_eip_status( + self.tr("Not supported"), + error=True) else: - self._set_eip_status(self.tr("EIP is disabled")) + self._status_panel.set_eip_status(self.tr("Disabled")) self.ui.btnEipStartStop.setEnabled(False) - def _set_eip_status_icon(self, status): - """ - Given a status step from the VPN thread, set the icon properly - - :param status: status step - :type status: str - """ - selected_pixmap = self.ERROR_ICON - tray_message = self.tr("Encryption is OFF") - if status in ("WAIT", "AUTH", "GET_CONFIG", - "RECONNECTING", "ASSIGN_IP"): - selected_pixmap = self.CONNECTING_ICON - tray_message = self.tr("Turning Encryption ON") - elif status in ("CONNECTED"): - tray_message = self.tr("Encryption is ON") - selected_pixmap = self.CONNECTED_ICON - - self.ui.lblVPNStatusIcon.setPixmap(selected_pixmap) - self._systray.setIcon(QtGui.QIcon(selected_pixmap)) - self._action_eip_status.setText(tray_message) - - def _update_vpn_state(self, data): - """ - SLOT - TRIGGER: self._vpn.state_changed - - Updates the displayed VPN state based on the data provided by - the VPN thread - """ - status = data[VPNManager.STATUS_STEP_KEY] - self._set_eip_status_icon(status) - if status == "AUTH": - self._set_eip_status(self.tr("VPN: Authenticating...")) - elif status == "GET_CONFIG": - self._set_eip_status(self.tr("VPN: Retrieving configuration...")) - elif status == "CONNECTED": - self._set_eip_status(self.tr("VPN: Connected!")) - elif status == "WAIT": - self._set_eip_status(self.tr("VPN: Waiting to start...")) - elif status == "ASSIGN_IP": - self._set_eip_status(self.tr("VPN: Assigning IP")) - elif status == "ALREADYRUNNING": - # Put the following calls in Qt's event queue, otherwise - # the UI won't update properly - QtCore.QTimer.singleShot(0, self._stop_eip) - QtCore.QTimer.singleShot(0, partial(self._set_eip_status, - self.tr("Unable to start VPN, " - "it's already " - "running."))) - else: - self._set_eip_status(status) - - def _update_vpn_status(self, data): - """ - SLOT - TRIGGER: self._vpn.status_changed - - Updates the download/upload labels based on the data provided - by the VPN thread - """ - upload = float(data[VPNManager.TUNTAP_WRITE_KEY]) - upload = upload / 1000.0 - upload_str = "%12.2f Kb" % (upload,) - self.ui.lblUpload.setText(upload_str) - self._action_eip_write.setText(upload_str) - download = float(data[VPNManager.TUNTAP_READ_KEY]) - download = download / 1000.0 - download_str = "%12.2f Kb" % (download,) - self.ui.lblDownload.setText(download_str) - self._action_eip_read.setText(download_str) - def _finish_eip_bootstrap(self, data): """ SLOT @@ -1157,11 +1078,13 @@ class MainWindow(QtGui.QMainWindow): self._start_eip() else: if data[self._eip_bootstrapper.PASSED_KEY]: - self._set_eip_status(self.tr("Could not load EIP " - "Configuration"), error=True) + self._status_panel.set_eip_status( + self.tr("Could not load EIP Configuration"), + error=True) else: - self._set_eip_status(data[self._eip_bootstrapper.ERROR_KEY], - error=True) + self._status_panel.set_eip_status( + data[self._eip_bootstrapper.ERROR_KEY], + error=True) self._already_started_eip = False def _logout(self): @@ -1183,6 +1106,7 @@ class MainWindow(QtGui.QMainWindow): Switches the stackedWidget back to the login stage after logging out """ + self._logged_user = None self.ui.action_sign_out.setEnabled(False) self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) self._login_widget.set_password("") diff --git a/src/leap/gui/statuspanel.py b/src/leap/gui/statuspanel.py new file mode 100644 index 00000000..53c19e86 --- /dev/null +++ b/src/leap/gui/statuspanel.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +# statuspanel.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Status Panel widget implementation +""" +import logging + +from functools import partial +from PySide import QtCore, QtGui + +from ui_statuspanel import Ui_StatusPanel +from leap.services.eip.vpnprocess import VPNManager +from leap.platform_init import IS_MAC +from leap.common.check import leap_assert_type + +logger = logging.getLogger(__name__) + + +class StatusPanelWidget(QtGui.QWidget): + """ + Status widget that displays the current state of the LEAP services + """ + + start_eip = QtCore.Signal() + stop_eip = QtCore.Signal() + + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + + self._systray = None + self._action_eip_status = None + + self.ui = Ui_StatusPanel() + self.ui.setupUi(self) + + self.ui.btnEipStartStop.setEnabled(False) + self.ui.btnEipStartStop.clicked.connect( + self.start_eip) + + if IS_MAC: + EIP_ICONS = ( + ":/images/conn_connecting-light.png", + ":/images/conn_connected-light.png", + ":/images/conn_error-light.png") + else: + EIP_ICONS = ( + ":/images/conn_connecting.png", + ":/images/conn_connected.png", + ":/images/conn_error.png") + + self.CONNECTING_ICON = QtGui.QPixmap(EIP_ICONS[0]) + self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1]) + self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2]) + + def set_systray(self, systray): + """ + Sets the systray object to use + + :param systray: Systray object + :type systray: QtGui.QSystemTrayIcon + """ + leap_assert_type(systray, QtGui.QSystemTrayIcon) + self._systray = systray + + def set_action_eip_status(self, action_eip_status): + """ + Sets the action_eip_status to use + + :param action_eip_status: action_eip_status to be used + :type action_eip_status: QtGui.QAction + """ + leap_assert_type(action_eip_status, QtGui.QAction) + self._action_eip_status = action_eip_status + + def set_eip_status(self, status, error=False): + """ + Sets the status label at the VPN stage to status + + :param status: status message + :type status: str or unicode + :param error: if the status is an erroneous one, then set this + to True + :type error: bool + """ + leap_assert_type(error, bool) + + self._systray.setToolTip(status) + if error: + status = "%s" % (status,) + self.ui.lblEIPStatus.setText(status) + + def set_startstop_enabled(self, value): + """ + Enable or disable btnEipStartStop based on value + + :param value: True for enabled, False otherwise + :type value: bool + """ + leap_assert_type(value, bool) + self.ui.btnEipStartStop.setEnabled(value) + + def eip_started(self): + """ + Sets the state of the widget to how it should look after EIP + has started + """ + self.ui.btnEipStartStop.setText(self.tr("Turn OFF")) + self.ui.btnEipStartStop.disconnect(self) + self.ui.btnEipStartStop.clicked.connect( + self.stop_eip) + + def eip_stopped(self): + """ + Sets the state of the widget to how it should look after EIP + has stopped + """ + self.ui.btnEipStartStop.setText(self.tr("Turn ON")) + self.ui.btnEipStartStop.disconnect(self) + self.ui.btnEipStartStop.clicked.connect( + self.start_eip) + + def set_icon(self, icon): + """ + Sets the icon to display for EIP + + :param icon: icon to display + :type icon: QPixmap + """ + self.ui.lblVPNStatusIcon.setPixmap(icon) + + def update_vpn_status(self, data): + """ + SLOT + TRIGGER: VPN.status_changed + + Updates the download/upload labels based on the data provided + by the VPN thread + """ + upload = float(data[VPNManager.TUNTAP_WRITE_KEY]) + upload = upload / 1000.0 + upload_str = "%12.2f Kb" % (upload,) + self.ui.lblUpload.setText(upload_str) + download = float(data[VPNManager.TUNTAP_READ_KEY]) + download = download / 1000.0 + download_str = "%12.2f Kb" % (download,) + self.ui.lblDownload.setText(download_str) + + def update_vpn_state(self, data): + """ + SLOT + TRIGGER: VPN.state_changed + + Updates the displayed VPN state based on the data provided by + the VPN thread + """ + status = data[VPNManager.STATUS_STEP_KEY] + self.set_eip_status_icon(status) + if status == "AUTH": + self.set_eip_status(self.tr("Authenticating...")) + elif status == "GET_CONFIG": + self.set_eip_status(self.tr("Retrieving configuration...")) + elif status == "CONNECTED": + self.set_eip_status(self.tr("On")) + elif status == "WAIT": + self.set_eip_status(self.tr("Waiting to start...")) + elif status == "ASSIGN_IP": + self.set_eip_status(self.tr("Assigning IP")) + elif status == "ALREADYRUNNING": + # Put the following calls in Qt's event queue, otherwise + # the UI won't update properly + QtCore.QTimer.singleShot(0, self.stop_eip) + QtCore.QTimer.singleShot(0, partial(self.set_eip_status, + self.tr("Unable to start VPN, " + "it's already " + "running."))) + else: + self._set_eip_status(status) + + def set_eip_status_icon(self, status): + """ + Given a status step from the VPN thread, set the icon properly + + :param status: status step + :type status: str + """ + selected_pixmap = self.ERROR_ICON + tray_message = self.tr("Encryption is OFF") + if status in ("WAIT", "AUTH", "GET_CONFIG", + "RECONNECTING", "ASSIGN_IP"): + selected_pixmap = self.CONNECTING_ICON + tray_message = self.tr("Turning ON") + elif status in ("CONNECTED"): + tray_message = self.tr("Encryption is ON") + selected_pixmap = self.CONNECTED_ICON + + self.set_icon(selected_pixmap) + self._systray.setIcon(QtGui.QIcon(selected_pixmap)) + self._action_eip_status.setText(tray_message) + + def set_provider(self, provider): + self.ui.lblProvider.setText(provider) diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui index 4874a324..58827fe0 100644 --- a/src/leap/gui/ui/mainwindow.ui +++ b/src/leap/gui/ui/mainwindow.ui @@ -28,6 +28,75 @@ + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + There are new updates available, please restart. + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + More... + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -41,39 +110,10 @@ - - - - false - - - - - - :/images/leap-color-big.png - - - Qt::AlignCenter - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - 0 + 1 @@ -110,115 +150,29 @@ - - - - - - - :/images/conn_error.png - - - Qt::AlignCenter - - - - - - - - 70 - 0 - - - - 0.0 Kb - - - - - - - - 70 - 0 - - - - 0.0 Kb - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - :/images/Arrow-Up-32.png - - - - - - - - - - :/images/Arrow-Down-32.png - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Disconnected - - - Qt::AlignCenter - - - true - - - - - - - Turn Encryption ON - - + + + + + + false + + + + + + :/images/leap-color-big.png + + + Qt::AlignCenter + + + @@ -245,76 +199,20 @@ - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 0 - - - - - - - - There are new updates available, please restart. - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - More... - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + + Qt::Horizontal + + + + 40 + 20 + + + - + diff --git a/src/leap/gui/ui/statuspanel.ui b/src/leap/gui/ui/statuspanel.ui new file mode 100644 index 00000000..67f5f669 --- /dev/null +++ b/src/leap/gui/ui/statuspanel.ui @@ -0,0 +1,248 @@ + + + StatusPanel + + + + 0 + 0 + 506 + 403 + + + + Form + + + + + + font: bold; + + + user@domain.org + + + true + + + + + + + + + + + 12 + + + 16 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 0 + + + + + + 64 + 64 + + + + + + + :/images/light/64/network-eip-down.png + + + Qt::AlignCenter + + + + + + + 0 + + + + + + + Encrypted Internet: + + + + + + + font: bold; + + + Off + + + Qt::AutoText + + + Qt::AlignCenter + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Turn On + + + + + + + + + 4 + + + QLayout::SetDefaultConstraint + + + + + + + + :/images/light/16/down-arrow.png + + + + + + + 0.0 Kb + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + + + :/images/light/16/up-arrow.png + + + + + + + 0.0 Kb + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 0 + 11 + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + -- cgit v1.2.3 From c752f90f2e242b96a6a63aa56466d22d8db8ceb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 13 Jun 2013 16:16:47 -0300 Subject: Always use best provider config Also, set the expected user@provider after stopping EIP, in case it was automatically started with another provider --- src/leap/gui/mainwindow.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 67fe34e3..aa9154e2 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -805,7 +805,7 @@ class MainWindow(QtGui.QMainWindow): if not self._already_started_eip: self._status_panel.set_provider( "%s@%s" % (self._login_widget.get_user(), - self._provider_config.get_domain())) + self._get_best_provider_config().get_domain())) self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) @@ -952,7 +952,7 @@ class MainWindow(QtGui.QMainWindow): self._settings.set_defaultprovider( provider_config.get_domain()) - provider = self._provider_config.get_domain() + provider = provider_config.get_domain() if self._logged_user is not None: provider = "%s@%s" % (self._logged_user, provider) @@ -1005,6 +1005,11 @@ class MainWindow(QtGui.QMainWindow): self._start_eip) self._already_started_eip = False + if self._logged_user: + self._status_panel.set_provider( + "%s@%s" % (self._logged_user, + self._get_best_provider_config().get_domain())) + def _get_best_provider_config(self): """ Returns the best ProviderConfig to use at a moment. We may -- cgit v1.2.3 From a573f046165aa8f28498021939b40286ab96befe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 14 Jun 2013 10:06:46 -0300 Subject: Fix set_eip_status wrong call --- src/leap/gui/statuspanel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/statuspanel.py b/src/leap/gui/statuspanel.py index 53c19e86..554903d8 100644 --- a/src/leap/gui/statuspanel.py +++ b/src/leap/gui/statuspanel.py @@ -189,7 +189,7 @@ class StatusPanelWidget(QtGui.QWidget): "it's already " "running."))) else: - self._set_eip_status(status) + self.set_eip_status(status) def set_eip_status_icon(self, status): """ -- cgit v1.2.3 From d8ccb2845ec34d0d00ef5c1799822da7c1281bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 14 Jun 2013 10:49:41 -0300 Subject: Fix wrong set_status call after refactor --- src/leap/gui/mainwindow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 90e74823..a5905679 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -1150,7 +1150,8 @@ class MainWindow(QtGui.QMainWindow): """ passed = data[self._provider_bootstrapper.PASSED_KEY] if not passed: - self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.set_status( + data[self._provider_bootstrapper.ERROR_KEY]) self._already_started_eip = False def _eip_finished(self, exitCode): -- cgit v1.2.3 From 1b670d268ee26fc06115702aff055884327f85ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 14 Jun 2013 11:08:39 -0300 Subject: Improve error messages in login --- src/leap/crypto/srpauth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index bcd24de3..d089fa50 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -459,7 +459,8 @@ class SRPAuth(QtCore.QObject): :type failure: twisted.python.failure.Failure """ logger.error("Error logging in %s" % (failure,)) - self.authentication_finished.emit(False, "%s" % (failure,)) + self.authentication_finished.emit(False, "%s" % (failure.value,)) + failure.trap(Exception) def get_session_id(self): return self.__instance.get_session_id() -- cgit v1.2.3 From 699eb99b1092335ee681dacdc06e6bf9c89827ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 14 Jun 2013 11:19:50 -0300 Subject: Make both clicks behave the same in the systray --- src/leap/gui/mainwindow.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 90e74823..c58cd4e3 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -529,18 +529,17 @@ class MainWindow(QtGui.QMainWindow): self.tr("Show Main Window"), self.tr("Hide Main Window"))[int(visible)] - if reason != QtGui.QSystemTrayIcon.Context: - # set labels - visible = self.isVisible() - self._action_visible.setText(get_action(visible)) - - context_menu = self._systray.contextMenu() - # for some reason, context_menu.show() - # is failing in a way beyond my understanding. - # (not working the first time it's clicked). - # this works however. - # XXX in osx it shows some glitches. - context_menu.exec_(self._systray.geometry().center()) + # set labels + visible = self.isVisible() + self._action_visible.setText(get_action(visible)) + + context_menu = self._systray.contextMenu() + # for some reason, context_menu.show() + # is failing in a way beyond my understanding. + # (not working the first time it's clicked). + # this works however. + # XXX in osx it shows some glitches. + context_menu.exec_(self._systray.geometry().center()) def _toggle_visible(self): """ -- cgit v1.2.3 From 336c21f8f5691f30cdf43c025695c5476be7fcec Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 13 Jun 2013 18:04:38 -0300 Subject: Autoselect VPN gateway based on timezone. --- src/leap/services/eip/eipconfig.py | 91 +++++++++++++++++++++++++++++++++-- src/leap/services/eip/vpnlaunchers.py | 12 +++-- 2 files changed, 96 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index 0a7d2b23..f7d03963 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -21,6 +21,8 @@ Provider configuration import logging import os import re +import datetime +import time import ipaddr @@ -32,6 +34,79 @@ from leap.services.eip.eipspec import eipservice_config_spec logger = logging.getLogger(__name__) +class VPNGatewaySelector(object): + """ + VPN Gateway selector. + """ + + def __init__(self, eipconfig): + ''' + Constructor for VPNGatewaySelector. + + :param eipconfig: a valid EIP Configuration. + :type eipconfig: EIPConfig + ''' + leap_assert_type(eipconfig, EIPConfig) + self._local_offset = 0 # defaults to GMT + self._local_timezone = None + self._set_local_offset() + self._eipconfig = eipconfig + + def _get_best_gateway(self): + """ + Returns index of the closest gateway, using timezones offsets. + + :rtype: int + """ + best_gateway = (-1, 99) # gateway, distance + locations = self._eipconfig.get_locations() + gateways = self._eipconfig.get_gateways() + for idx, gateway in enumerate(gateways): + gateway_offset = int(locations[gateway['location']]['timezone']) + gateway_distance = self._get_timezone_distance(gateway_offset) + if gateway_distance < best_gateway[1]: + best_gateway = (idx, gateway_distance) + + return best_gateway[0] + + def get_best_gateway_ip(self): + """ + Returns the ip of the best possible gateway. + + :rtype: An IPv4Address or IPv6Address object. + """ + best_gateway = self._get_best_gateway() + gateway_ip = self._eipconfig.get_gateway_ip(best_gateway) + + return gateway_ip + + def _get_timezone_distance(self, offset): + ''' + Returns the distance between the local timezone and + the one with offset 'offset'. + + :param offset: the distance of a timezone to GMT. + :type offset: int + :returns: distance between local offset and param offset. + :rtype: int + ''' + delta1 = datetime.timedelta(hours=offset) + delta2 = self._local_offset + diff = abs(delta1 - delta2) + hours = diff.seconds / (60 * 60) + return hours + + def _set_local_offset(self): + ''' + Sets the distance between GMT and the local timezone. + ''' + local_offset = time.timezone + if time.daylight: + local_offset = time.altzone + + self._local_offset = datetime.timedelta(seconds=-local_offset) + + class EIPConfig(BaseConfig): """ Provider configuration abstraction class @@ -56,6 +131,14 @@ class EIPConfig(BaseConfig): # TODO: create an abstraction for gateways return self._safe_get_value("gateways") + def get_locations(self): + ''' + Returns a list of locations + + :rtype: dict + ''' + return self._safe_get_value("locations") + def get_openvpn_configuration(self): """ Returns a dictionary containing the openvpn configuration @@ -63,8 +146,8 @@ class EIPConfig(BaseConfig): These are sanitized with alphanumeric whitelist. - @returns: openvpn configuration dict - @rtype: C{dict} + :returns: openvpn configuration dict + :rtype: C{dict} """ ovpncfg = self._safe_get_value("openvpn_configuration") config = {} @@ -84,7 +167,9 @@ class EIPConfig(BaseConfig): def get_gateway_ip(self, index=0): """ - Returns the ip of the gateway + Returns the ip of the gateway. + + :rtype: An IPv4Address or IPv6Address object. """ gateways = self.get_gateways() leap_assert(len(gateways) > 0, "We don't have any gateway!") diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 6c2ff006..fa2989bc 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -34,7 +34,7 @@ from functools import partial from leap.common.check import leap_assert, leap_assert_type from leap.common.files import which from leap.config.providerconfig import ProviderConfig -from leap.services.eip.eipconfig import EIPConfig +from leap.services.eip.eipconfig import EIPConfig, VPNGatewaySelector logger = logging.getLogger(__name__) @@ -228,7 +228,8 @@ class LinuxVPNLauncher(VPNLauncher): # TODO: handle verbosity - gateway_ip = str(eipconfig.get_gateway_ip(0)) + gateway_selector = VPNGatewaySelector(eipconfig) + gateway_ip = gateway_selector.get_best_gateway_ip() logger.debug("Using gateway ip %s" % (gateway_ip,)) @@ -391,7 +392,9 @@ class DarwinVPNLauncher(VPNLauncher): # TODO: handle verbosity - gateway_ip = str(eipconfig.get_gateway_ip(0)) + gateway_selector = VPNGatewaySelector(eipconfig) + gateway_ip = gateway_selector.get_best_gateway_ip() + logger.debug("Using gateway ip %s" % (gateway_ip,)) args += [ @@ -530,7 +533,8 @@ class WindowsVPNLauncher(VPNLauncher): # TODO: handle verbosity - gateway_ip = str(eipconfig.get_gateway_ip(0)) + gateway_selector = VPNGatewaySelector(eipconfig) + gateway_ip = gateway_selector.get_best_gateway_ip() logger.debug("Using gateway ip %s" % (gateway_ip,)) -- cgit v1.2.3 From 8bee5f4e9a1bb0f7069fe41ab37dfec000487d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 14 Jun 2013 12:45:08 -0300 Subject: Actually deferToThread all the things we expect to do in parallel --- src/leap/crypto/srpauth.py | 19 +++++++++++++++---- src/leap/services/abstractbootstrapper.py | 7 ++++++- 2 files changed, 21 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/leap/crypto/srpauth.py b/src/leap/crypto/srpauth.py index d089fa50..0e95ae64 100644 --- a/src/leap/crypto/srpauth.py +++ b/src/leap/crypto/srpauth.py @@ -24,6 +24,7 @@ import json #this error is raised from requests from simplejson.decoder import JSONDecodeError +from functools import partial from PySide import QtCore from twisted.internet import threads @@ -321,6 +322,9 @@ class SRPAuth(QtCore.QObject): self.set_session_id(session_id) + def _threader(self, cb, res, *args, **kwargs): + return threads.deferToThread(cb, res, *args, **kwargs) + def authenticate(self, username, password): """ Executes the whole authentication process for a user @@ -341,10 +345,17 @@ class SRPAuth(QtCore.QObject): username=username, password=password) - d.addCallback(self._start_authentication, username=username, - password=password) - d.addCallback(self._process_challenge, username=username) - d.addCallback(self._verify_session) + d.addCallback( + partial(self._threader, + self._start_authentication), + username=username, + password=password) + d.addCallback( + partial(self._threader, + self._process_challenge), + username=username) + d.addCallback(partial(self._threader, + self._verify_session)) return d diff --git a/src/leap/services/abstractbootstrapper.py b/src/leap/services/abstractbootstrapper.py index 2cbd56bc..f0937197 100644 --- a/src/leap/services/abstractbootstrapper.py +++ b/src/leap/services/abstractbootstrapper.py @@ -22,6 +22,8 @@ import logging import requests +from functools import partial + from PySide import QtCore from twisted.internet import threads from leap.common.check import leap_assert, leap_assert_type @@ -128,6 +130,9 @@ class AbstractBootstrapper(QtCore.QObject): logger.debug("Emitting %s" % (signal,)) signal.emit({self.PASSED_KEY: True, self.ERROR_KEY: ""}) + def _callback_threader(self, cb, res, *args, **kwargs): + return threads.deferToThread(cb, res, *args, **kwargs) + def addCallbackChain(self, callbacks): """ Creates a callback/errback chain on another thread using @@ -148,7 +153,7 @@ class AbstractBootstrapper(QtCore.QObject): if d is None: d = threads.deferToThread(cb) else: - d.addCallback(cb) + d.addCallback(partial(self._callback_threader, cb)) d.addErrback(self._errback, signal=sig) d.addCallback(self._gui_notify, signal=sig) d.addErrback(self._gui_errback) -- cgit v1.2.3 From e6e88154f3b274ff97474a26a36dd6453f55de0b Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 14 Jun 2013 12:51:18 -0300 Subject: Bugfix: add logs to history. Closes #2871. --- src/leap/util/leap_log_handler.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/util/leap_log_handler.py b/src/leap/util/leap_log_handler.py index 5b8ae789..e5bc87e1 100644 --- a/src/leap/util/leap_log_handler.py +++ b/src/leap/util/leap_log_handler.py @@ -31,16 +31,16 @@ class LogHandler(logging.Handler): MESSAGE_KEY = 'message' RECORD_KEY = 'record' - # TODO This is going to eat lots of memory after some time. - # Should be pruned at some moment. - _log_history = [] - def __init__(self, qtsignal): """ LogHander initialization. Calls parent method and keeps a reference to the qtsignal that will be used to fire the gui update. """ + # TODO This is going to eat lots of memory after some time. + # Should be pruned at some moment. + self._log_history = [] + logging.Handler.__init__(self) self._qtsignal = qtsignal @@ -85,6 +85,7 @@ class LogHandler(logging.Handler): self._set_format(logRecord.levelname) log = self.format(logRecord) log_item = {self.RECORD_KEY: logRecord, self.MESSAGE_KEY: log} + self._log_history.append(log_item) self._qtsignal(log_item) -- cgit v1.2.3 From 4494b396385d1f9f84a43201cf25c3afd87a823d Mon Sep 17 00:00:00 2001 From: kali Date: Fri, 14 Jun 2013 04:06:43 +0900 Subject: disable soledad bootstrapper --- src/leap/gui/mainwindow.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index a5905679..42148836 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -185,11 +185,11 @@ class MainWindow(QtGui.QMainWindow): self._eip_bootstrapper.download_client_certificate.connect( self._finish_eip_bootstrap) - self._soledad_bootstrapper = SoledadBootstrapper() - self._soledad_bootstrapper.download_config.connect( - self._soledad_intermediate_stage) - self._soledad_bootstrapper.gen_key.connect( - self._soledad_bootstrapped_stage) + #self._soledad_bootstrapper = SoledadBootstrapper() + #self._soledad_bootstrapper.download_config.connect( + #self._soledad_intermediate_stage) + #self._soledad_bootstrapper.gen_key.connect( + #self._soledad_bootstrapped_stage) self._smtp_bootstrapper = SMTPBootstrapper() self._smtp_bootstrapper.download_config.connect( @@ -809,11 +809,12 @@ class MainWindow(QtGui.QMainWindow): self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) - self._soledad_bootstrapper.run_soledad_setup_checks( - self._provider_config, - self._login_widget.get_user(), - self._login_widget.get_password(), - download_if_needed=True) + # XXX disabling soledad for now + #self._soledad_bootstrapper.run_soledad_setup_checks( + #self._provider_config, + #self._login_widget.get_user(), + #self._login_widget.get_password(), + #download_if_needed=True) self._download_eip_config() -- cgit v1.2.3 From fdda95c92d22b4ede61bfb7587320ddb36da5cd7 Mon Sep 17 00:00:00 2001 From: kali Date: Sat, 15 Jun 2013 01:17:35 +0900 Subject: working openvpn termination: SIGTERM, then SIGKILL. Closes: #2753 --- src/leap/app.py | 9 ++- src/leap/gui/mainwindow.py | 19 +++-- src/leap/services/eip/vpnprocess.py | 141 +++++++++++++++++++++++++++++------- 3 files changed, 130 insertions(+), 39 deletions(-) (limited to 'src') diff --git a/src/leap/app.py b/src/leap/app.py index 05b54d1f..cb9951c1 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -138,11 +138,14 @@ def main(): app.setApplicationName("leap") app.setOrganizationDomain("leap.se") + # XXX --------------------------------------------------------- + # In quarantine, looks like we don't need it anymore. # This dummy timer ensures that control is given to the outside # loop, so we can hook our sigint handler. - timer = QtCore.QTimer() - timer.start(500) - timer.timeout.connect(lambda: None) + #timer = QtCore.QTimer() + #timer.start(500) + #timer.timeout.connect(lambda: None) + # XXX --------------------------------------------------------- window = MainWindow( lambda: twisted_main.quit(app), diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 42148836..dd9fb148 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -994,19 +994,21 @@ class MainWindow(QtGui.QMainWindow): self._status_panel.set_startstop_enabled(True) def _stop_eip(self): + """ + Stops vpn process and makes gui adjustments to reflect + the change of state. + """ self._vpn.terminate() + self._status_panel.set_eip_status(self.tr("Off")) self._status_panel.set_eip_status_icon("error") - self._status_panel.eip_stopped() - self._action_eip_startstop.setText(self.tr("Turn ON")) self._action_eip_startstop.disconnect(self) self._action_eip_startstop.triggered.connect( self._start_eip) self._already_started_eip = False self._settings.set_defaultprovider(None) - if self._logged_user: self._status_panel.set_provider( "%s@%s" % (self._logged_user, @@ -1204,12 +1206,12 @@ class MainWindow(QtGui.QMainWindow): """ logger.debug('About to quit, doing cleanup...') - logger.debug('Killing vpn') - self._vpn.terminate() - logger.debug('Cleaning pidfiles') self._cleanup_pidfiles() + logger.debug('Terminating vpn') + self._vpn.terminate() + def quit(self): """ Cleanup and tidely close the main window before quitting. @@ -1223,13 +1225,14 @@ class MainWindow(QtGui.QMainWindow): if self._logger_window: self._logger_window.close() - self.close() - if self._login_defer: self._login_defer.cancel() + self.close() + if self._quit_callback: self._quit_callback() + logger.debug('Bye.') diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py index eae8aadd..162dc7f0 100644 --- a/src/leap/services/eip/vpnprocess.py +++ b/src/leap/services/eip/vpnprocess.py @@ -20,6 +20,8 @@ VPN Manager, spawned in a custom processProtocol. import logging import os import psutil +import socket +import time from PySide import QtCore @@ -63,6 +65,9 @@ class VPN(object): opened by the openvpn process, executing commands over that interface on demand. """ + TERMINATE_MAXTRIES = 10 + TERMINATE_WAIT = 1 # secs + def __init__(self): """ Instantiate empty attributes and get a copy @@ -94,6 +99,10 @@ class VPN(object): # start the main vpn subprocess vpnproc = VPNProcess(*args, **kwargs) + # XXX Should stop if already running ------- + if vpnproc.get_openvpn_process(): + logger.warning("Another vpnprocess is running!") + cmd = vpnproc.getCommand() env = os.environ for key, val in vpnproc.vpn_env.items(): @@ -103,7 +112,7 @@ class VPN(object): self._vpnproc = vpnproc # add pollers for status and state - # XXX this could be extended to a collection of + # this could be extended to a collection of # generic watchers poll_list = [LoopingCall(vpnproc.pollStatus), @@ -111,15 +120,50 @@ class VPN(object): self._pollers.extend(poll_list) self._start_pollers() + def _kill_if_left_alive(self, tries=0): + """ + Check if the process is still alive, and sends a + SIGKILL after a timeout period. + + :param tries: counter of tries, used in recursion + :type tries: int + """ + from twisted.internet import reactor + while tries < self.TERMINATE_MAXTRIES: + if self._vpnproc.transport.pid is None: + logger.debug("Process has been happily terminated.") + return + else: + logger.debug("Process did not die, waiting...") + tries += 1 + reactor.callLater(self.TERMINATE_WAIT, + self._kill_if_left_alive, tries) + + # after running out of patience, we try a killProcess + logger.debug("Process did not died. Sending a SIGKILL.") + self._vpnproc.killProcess() + def terminate(self): """ Stops the openvpn subprocess. + + Attempts to send a SIGTERM first, and after a timeout + it sends a SIGKILL. """ + from twisted.internet import reactor self._stop_pollers() - # XXX we should leave a KILL as a last resort. - # First we should try to send a SIGTERM + + # First we try to be polite and send a SIGTERM... if self._vpnproc: - self._vpnproc.killProcess() + self._sentterm = True + self._vpnproc.terminate_openvpn() + + # ...but we also trigger a countdown to be unpolite + # if strictly needed. + reactor.callLater( + self.TERMINATE_WAIT, self._kill_if_left_alive) + + # TODO: should also cleanup tempfiles!!! def _start_pollers(self): """ @@ -148,6 +192,10 @@ class VPNManager(object): A copy of a QObject containing signals as attributes is passed along upon initialization, and we use that object to emit signals to qt-land. + + For more info about management methods:: + + zcat `dpkg -L openvpn | grep management` """ # Timers, in secs @@ -183,15 +231,15 @@ class VPNManager(object): def qtsigs(self): return self._qtsigs - def _disconnect(self): + def _seek_to_eof(self): """ - Disconnects the telnet connection to the openvpn process. + Read as much as available. Position seek pointer to end of stream """ - logger.debug('Closing socket') - self._tn.write("quit\n") - self._tn.read_all() - self._tn.close() - self._tn = None + try: + self._tn.read_eager() + except EOFError: + logger.debug("Could not read from socket. Assuming it died.") + return def _send_command(self, command, until=b"END"): """ @@ -208,12 +256,24 @@ class VPNManager(object): :rtype: list """ leap_assert(self._tn, "We need a tn connection!") + try: self._tn.write("%s\n" % (command,)) buf = self._tn.read_until(until, 2) - self._tn.read_eager() - lines = buf.split("\n") - return lines + self._seek_to_eof() + blist = buf.split('\r\n') + if blist[-1].startswith(until): + del blist[-1] + return blist + else: + return [] + + except socket.error: + # XXX should get a counter and repeat only + # after mod X times. + logger.warning('socket error') + self._close_management_socket(announce=False) + return [] # XXX should move this to a errBack! except Exception as e: @@ -221,9 +281,21 @@ class VPNManager(object): (command, e)) return [] - def _connect(self, socket_host, socket_port): + def _close_management_socket(self, announce=True): + """ + Close connection to openvpn management interface. + """ + logger.debug('closing socket') + if announce: + self._tn.write("quit\n") + self._tn.read_all() + self._tn.get_socket().close() + del self._tn + + def _connect_management(self, socket_host, socket_port): """ - Connects to the specified socket_host socket_port. + Connects to the management interface on the specified + socket_host socket_port. :param socket_host: either socket path (unix) or socket IP :type socket_host: str @@ -232,6 +304,9 @@ class VPNManager(object): socket, or port otherwise :type socket_port: str """ + if self.is_connected(): + self._close_management_socket() + try: self._tn = UDSTelnet(socket_host, socket_port) @@ -268,7 +343,7 @@ class VPNManager(object): """ logger.warning(failure) - def connect(self, host, port): + def connect_to_management(self, host, port): """ Connect to a management interface. @@ -280,7 +355,8 @@ class VPNManager(object): :returns: a deferred """ - self.connectd = defer.maybeDeferred(self._connect, host, port) + self.connectd = defer.maybeDeferred( + self._connect_management, host, port) self.connectd.addCallbacks(self._connectCb, self._connectErr) return self.connectd @@ -293,7 +369,7 @@ class VPNManager(object): """ return True if self._tn else False - def try_to_connect(self, retry=0): + def try_to_connect_to_management(self, retry=0): """ Attempts to connect to a management interface, and retries after CONNECTION_RETRY_TIME if not successful. @@ -304,9 +380,10 @@ class VPNManager(object): # TODO decide about putting a max_lim to retries and signaling # an error. if not self.is_connected(): - self.connect(self._socket_host, self._socket_port) + self.connect_to_management(self._socket_host, self._socket_port) self._reactor.callLater( - self.CONNECTION_RETRY_TIME, self.try_to_connect, retry + 1) + self.CONNECTION_RETRY_TIME, + self.try_to_connect_to_management, retry + 1) def _parse_state_and_notify(self, output): """ @@ -405,9 +482,17 @@ class VPNManager(object): """ return self._launcher.get_vpn_env(self._providerconfig) + def terminate_openvpn(self): + """ + Attempts to terminate openvpn by sending a SIGTERM. + """ + if self.is_connected(): + self._send_command("signal SIGTERM") + + # --------------------------------------------------- # XXX old methods, not adapted to twisted process yet - def _get_openvpn_process(self): + def get_openvpn_process(self): """ Looks for openvpn instances running. @@ -421,7 +506,7 @@ class VPNManager(object): # we should check that cmdline BEGINS # with openvpn or with our wrapper # (pkexec / osascript / whatever) - if self._launcher.OPENVPN_BIN in ' '.join(p.cmdline): + if "openvpn" in ' '.join(p.cmdline): openvpn_process = p break except psutil.error.AccessDenied: @@ -434,10 +519,10 @@ class VPNManager(object): :return: True if stopped, False otherwise """ - + # TODO cleanup this process = self._get_openvpn_process() if process: - logger.debug("OpenVPN is already running, trying to stop it") + logger.debug("OpenVPN is already running, trying to stop it...") cmdline = process.cmdline manag_flag = "--management" @@ -448,11 +533,11 @@ class VPNManager(object): port = cmdline[index + 2] logger.debug("Trying to connect to %s:%s" % (host, port)) - self._connect(host, port) + self._connect_to_management(host, port) self._send_command("signal SIGTERM") self._tn.close() self._tn = None - #self._disconnect() + #self._disconnect_management() except Exception as e: logger.warning("Problem trying to terminate OpenVPN: %r" % (e,)) @@ -518,7 +603,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa """ - self.try_to_connect() + self.try_to_connect_to_management() def outReceived(self, data): """ -- cgit v1.2.3 From 8fe9b31ba750ca6e0396b6330640efeccacb4ced Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 14 Jun 2013 16:24:01 -0300 Subject: Set EIP different status icons depending on OS. Closes bug #2643 --- src/leap/gui/statuspanel.py | 47 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/leap/gui/statuspanel.py b/src/leap/gui/statuspanel.py index 554903d8..19f84d4c 100644 --- a/src/leap/gui/statuspanel.py +++ b/src/leap/gui/statuspanel.py @@ -25,7 +25,7 @@ from PySide import QtCore, QtGui from ui_statuspanel import Ui_StatusPanel from leap.services.eip.vpnprocess import VPNManager -from leap.platform_init import IS_MAC +from leap.platform_init import IS_WIN, IS_LINUX from leap.common.check import leap_assert_type logger = logging.getLogger(__name__) @@ -52,13 +52,35 @@ class StatusPanelWidget(QtGui.QWidget): self.ui.btnEipStartStop.clicked.connect( self.start_eip) - if IS_MAC: - EIP_ICONS = ( - ":/images/conn_connecting-light.png", - ":/images/conn_connected-light.png", - ":/images/conn_error-light.png") - else: - EIP_ICONS = ( + # Set the EIP status icons + self.CONNECTING_ICON = None + self.CONNECTED_ICON = None + self.ERROR_ICON = None + self.CONNECTING_ICON_TRAY = None + self.CONNECTED_ICON_TRAY = None + self.ERROR_ICON_TRAY = None + self._set_eip_icons() + + def _set_eip_icons(self): + """ + Sets the EIP status icons for the main window and for the tray + + MAC : dark icons + LINUX : dark icons in window, light icons in tray + WIN : light icons + """ + EIP_ICONS = EIP_ICONS_TRAY = ( + ":/images/conn_connecting-light.png", + ":/images/conn_connected-light.png", + ":/images/conn_error-light.png") + + if IS_LINUX: + EIP_ICONS_TRAY = ( + ":/images/conn_connecting.png", + ":/images/conn_connected.png", + ":/images/conn_error.png") + elif IS_WIN: + EIP_ICONS = EIP_ICONS_TRAY = ( ":/images/conn_connecting.png", ":/images/conn_connected.png", ":/images/conn_error.png") @@ -67,6 +89,10 @@ class StatusPanelWidget(QtGui.QWidget): self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1]) self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2]) + self.CONNECTING_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[0]) + self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1]) + self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2]) + def set_systray(self, systray): """ Sets the systray object to use @@ -199,17 +225,20 @@ class StatusPanelWidget(QtGui.QWidget): :type status: str """ selected_pixmap = self.ERROR_ICON + selected_pixmap_tray = self.ERROR_ICON_TRAY tray_message = self.tr("Encryption is OFF") if status in ("WAIT", "AUTH", "GET_CONFIG", "RECONNECTING", "ASSIGN_IP"): selected_pixmap = self.CONNECTING_ICON + selected_pixmap_tray = self.CONNECTING_ICON_TRAY tray_message = self.tr("Turning ON") elif status in ("CONNECTED"): tray_message = self.tr("Encryption is ON") selected_pixmap = self.CONNECTED_ICON + selected_pixmap_tray = self.CONNECTED_ICON_TRAY self.set_icon(selected_pixmap) - self._systray.setIcon(QtGui.QIcon(selected_pixmap)) + self._systray.setIcon(QtGui.QIcon(selected_pixmap_tray)) self._action_eip_status.setText(tray_message) def set_provider(self, provider): -- cgit v1.2.3 From 0f43f3db34dd0262e298dd4431b1e971c77dfbdd Mon Sep 17 00:00:00 2001 From: kali Date: Mon, 17 Jun 2013 05:37:04 +0900 Subject: Remove temporal folder during shutdown Closes: #2891 This is the temporal folder that is used under osx and linux for the unix socket to connect to the management interface. --- src/leap/gui/mainwindow.py | 2 +- src/leap/services/eip/vpnprocess.py | 28 ++++++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 51b96463..9fd543bf 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -1209,7 +1209,7 @@ class MainWindow(QtGui.QMainWindow): self._cleanup_pidfiles() logger.debug('Terminating vpn') - self._vpn.terminate() + self._vpn.terminate(shutdown=True) def quit(self): """ diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py index 162dc7f0..f3443533 100644 --- a/src/leap/services/eip/vpnprocess.py +++ b/src/leap/services/eip/vpnprocess.py @@ -20,8 +20,8 @@ VPN Manager, spawned in a custom processProtocol. import logging import os import psutil +import shutil import socket -import time from PySide import QtCore @@ -143,7 +143,7 @@ class VPN(object): logger.debug("Process did not died. Sending a SIGKILL.") self._vpnproc.killProcess() - def terminate(self): + def terminate(self, shutdown=False): """ Stops the openvpn subprocess. @@ -156,15 +156,13 @@ class VPN(object): # First we try to be polite and send a SIGTERM... if self._vpnproc: self._sentterm = True - self._vpnproc.terminate_openvpn() + self._vpnproc.terminate_openvpn(shutdown=shutdown) # ...but we also trigger a countdown to be unpolite # if strictly needed. reactor.callLater( self.TERMINATE_WAIT, self._kill_if_left_alive) - # TODO: should also cleanup tempfiles!!! - def _start_pollers(self): """ Iterate through the registered observers @@ -482,12 +480,30 @@ class VPNManager(object): """ return self._launcher.get_vpn_env(self._providerconfig) - def terminate_openvpn(self): + def terminate_openvpn(self, shutdown=False): """ Attempts to terminate openvpn by sending a SIGTERM. """ if self.is_connected(): self._send_command("signal SIGTERM") + if shutdown: + self._cleanup_tempfiles() + + def _cleanup_tempfiles(self): + """ + Remove all temporal files we might have left behind. + + Iif self.port is 'unix', we have created a temporal socket path that, + under normal circumstances, we should be able to delete. + """ + if self._socket_port == "unix": + logger.debug('cleaning socket file temp folder') + tempfolder = os.path.split(self._socket_host)[0] # XXX use `first` + if os.path.isdir(tempfolder): + try: + shutil.rmtree(tempfolder) + except OSError: + logger.error('could not delete tmpfolder %s' % tempfolder) # --------------------------------------------------- # XXX old methods, not adapted to twisted process yet -- cgit v1.2.3 From 1d23bb2d8f5715031bc45f506ac32ce2afa5bfce Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 18 Jun 2013 13:03:10 -0300 Subject: Close the app on rejected wizard. Closes bug #2905 --- src/leap/gui/mainwindow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 51b96463..bf6aefe9 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -299,9 +299,12 @@ class MainWindow(QtGui.QMainWindow): """ if self._wizard is None: self._wizard = Wizard(bypass_checks=self._bypass_checks) - self._wizard.accepted.connect(self._finish_init) + self._wizard.accepted.connect(self._finish_init) + self.setVisible(False) self._wizard.exec_() + # We need this to process any wizard related event + QtCore.QCoreApplication.processEvents() self._wizard = None self.setVisible(True) -- cgit v1.2.3 From 6c309232c6b669a3f715913f5e172dc6a5e24078 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 18 Jun 2013 14:39:33 -0300 Subject: Improve timezone gateway selector. Closes #2894 It allows to use multiple gateways in openvpn for redundancy. --- src/leap/services/eip/eipconfig.py | 33 ++++++++++++++++----------------- src/leap/services/eip/vpnlaunchers.py | 24 +++++++++++++++--------- 2 files changed, 31 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index f7d03963..a85fe64a 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -52,33 +52,32 @@ class VPNGatewaySelector(object): self._set_local_offset() self._eipconfig = eipconfig - def _get_best_gateway(self): + def get_gateways(self): """ - Returns index of the closest gateway, using timezones offsets. + Returns the 4 best gateways, sorted by timezone proximity. - :rtype: int + :rtype: list of IPv4Address or IPv6Address object. """ - best_gateway = (-1, 99) # gateway, distance + gateways_timezones = [] locations = self._eipconfig.get_locations() gateways = self._eipconfig.get_gateways() + for idx, gateway in enumerate(gateways): - gateway_offset = int(locations[gateway['location']]['timezone']) - gateway_distance = self._get_timezone_distance(gateway_offset) - if gateway_distance < best_gateway[1]: - best_gateway = (idx, gateway_distance) + gateway_location = gateway.get('location') + gateway_distance = 99 # if hasn't location -> should go last - return best_gateway[0] + if gateway_location is not None: + gw_offset = int(locations[gateway['location']]['timezone']) + gateway_distance = self._get_timezone_distance(gw_offset) - def get_best_gateway_ip(self): - """ - Returns the ip of the best possible gateway. + ip = self._eipconfig.get_gateway_ip(idx) + gateways_timezones.append((ip, gateway_distance)) - :rtype: An IPv4Address or IPv6Address object. - """ - best_gateway = self._get_best_gateway() - gateway_ip = self._eipconfig.get_gateway_ip(best_gateway) + gateways_timezones = sorted(gateways_timezones, + key=lambda gw: gw[1])[:4] - return gateway_ip + gateways = [ip for ip, dist in gateways_timezones] + return gateways def _get_timezone_distance(self, offset): ''' diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index fa2989bc..fdc863c6 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -229,16 +229,18 @@ class LinuxVPNLauncher(VPNLauncher): # TODO: handle verbosity gateway_selector = VPNGatewaySelector(eipconfig) - gateway_ip = gateway_selector.get_best_gateway_ip() + gateways = gateway_selector.get_gateways() - logger.debug("Using gateway ip %s" % (gateway_ip,)) + logger.debug("Using gateways ips: {}".format(', '.join(gateways))) + + for gw in gateways: + args += ['--remote', gw, '1194', 'udp'] args += [ '--client', '--dev', 'tun', '--persist-tun', '--persist-key', - '--remote', gateway_ip, '1194', 'udp', '--tls-client', '--remote-cert-tls', 'server' @@ -393,16 +395,18 @@ class DarwinVPNLauncher(VPNLauncher): # TODO: handle verbosity gateway_selector = VPNGatewaySelector(eipconfig) - gateway_ip = gateway_selector.get_best_gateway_ip() + gateways = gateway_selector.get_gateways() + + logger.debug("Using gateways ips: {}".format(', '.join(gateways))) - logger.debug("Using gateway ip %s" % (gateway_ip,)) + for gw in gateways: + args += ['--remote', gw, '1194', 'udp'] args += [ '--client', '--dev', 'tun', '--persist-tun', '--persist-key', - '--remote', gateway_ip, '1194', 'udp', '--tls-client', '--remote-cert-tls', 'server' @@ -534,16 +538,18 @@ class WindowsVPNLauncher(VPNLauncher): # TODO: handle verbosity gateway_selector = VPNGatewaySelector(eipconfig) - gateway_ip = gateway_selector.get_best_gateway_ip() + gateways = gateway_selector.get_gateways() + + logger.debug("Using gateways ips: {}".format(', '.join(gateways))) - logger.debug("Using gateway ip %s" % (gateway_ip,)) + for gw in gateways: + args += ['--remote', gw, '1194', 'udp'] args += [ '--client', '--dev', 'tun', '--persist-tun', '--persist-key', - '--remote', gateway_ip, '1194', 'udp', '--tls-client', '--remote-cert-tls', 'server' -- cgit v1.2.3 From 8c9e05eb498f24f9e580d6dd13cb6834612f9530 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 19 Jun 2013 11:04:03 -0300 Subject: Temporary removal of enrollment policy in UI. Only hide the components. Closes issue #2922. --- src/leap/gui/wizard.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index d03427db..897bf94f 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -154,6 +154,11 @@ class Wizard(QtGui.QWizard): self.page(self.FINISH_PAGE).setButtonText( QtGui.QWizard.FinishButton, self.tr("Connect")) + # XXX: Temporary removal for enrollment policy + # https://leap.se/code/issues/2922 + self.ui.label_12.setVisible(False) + self.ui.lblProviderPolicy.setVisible(False) + def get_domain(self): return self._domain -- cgit v1.2.3 From b36fe9cf87bc1917abc0667756f01e6d4609cc4c Mon Sep 17 00:00:00 2001 From: kali Date: Mon, 17 Jun 2013 04:46:06 +0900 Subject: install missing files during linux initialization Closes: #2247, #2761 --- src/leap/platform_init/initializers.py | 217 ++++++++++++++++++++++++--------- src/leap/services/eip/vpnlaunchers.py | 164 ++++++++++++++++++------- src/leap/util/__init__.py | 10 ++ 3 files changed, 290 insertions(+), 101 deletions(-) (limited to 'src') diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index d72dc61f..3374e32e 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -30,12 +30,17 @@ from PySide import QtGui from leap.config.leapsettings import LeapSettings from leap.services.eip import vpnlaunchers +from leap.util import first logger = logging.getLogger(__name__) # NOTE we could use a deferToThread here, but should # be aware of this bug: http://www.themacaque.com/?p=1067 +__all__ = ["init_platform"] + +_system = platform.system() + def init_platform(): """ @@ -44,7 +49,7 @@ def init_platform(): """ initializer = None try: - initializer = globals()[platform.system() + "Initializer"] + initializer = globals()[_system + "Initializer"] except: pass if initializer: @@ -54,6 +59,86 @@ def init_platform(): logger.debug("Initializer not found for %s" % (platform.system(),)) +# +# common utils +# + +NOTFOUND_MSG = ("Tried to install %s, but %s " + "not found inside this bundle.") +BADEXEC_MSG = ("Tried to install %s, but %s " + "failed to %s.") + +UPDOWN_NOTFOUND_MSG = NOTFOUND_MSG % ( + "updown scripts", "those were") +UPDOWN_BADEXEC_MSG = BADEXEC_MSG % ( + "updown scripts", "they", "be copied") + + +def get_missing_updown_dialog(): + """ + Creates a dialog for notifying of missing updown scripts. + Returns that dialog. + + :rtype: QtGui.QMessageBox instance + """ + WE_NEED_POWERS = ("To better protect your privacy, " + "LEAP needs administrative privileges " + "to install helper files. " + "Do you want to proceed?") + msg = QtGui.QMessageBox() + msg.setWindowTitle(msg.tr("Missing up/down scripts")) + msg.setText(msg.tr(WE_NEED_POWERS)) + # but maybe the user really deserve to know more + #msg.setInformativeText(msg.tr(BECAUSE)) + msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) + msg.addButton("No, don't ask again", QtGui.QMessageBox.RejectRole) + msg.setDefaultButton(QtGui.QMessageBox.Yes) + return msg + + +def check_missing(): + """ + Checks for the need of installing missing scripts, and + raises a dialog to ask user for permission to do it. + """ + config = LeapSettings() + alert_missing = config.get_alert_missing_scripts() + + launcher = vpnlaunchers.get_platform_launcher() + missing_scripts = launcher.missing_updown_scripts + missing_other = launcher.missing_other_files + + if alert_missing and (missing_scripts() or missing_other()): + msg = get_missing_updown_dialog() + ret = msg.exec_() + + if ret == QtGui.QMessageBox.Yes: + install_missing_fun = globals().get( + "_%s_install_missing_scripts" % (_system.lower(),), + None) + if not install_missing_fun: + logger.warning( + "Installer not found for platform %s." % (_system,)) + return + install_missing_fun( + # XXX maybe move constants to fun + UPDOWN_BADEXEC_MSG, + UPDOWN_NOTFOUND_MSG) + + elif ret == QtGui.QMessageBox.No: + logger.debug("Not installing missing scripts, " + "user decided to ignore our warning.") + + elif ret == QtGui.QMessageBox.Rejected: + logger.debug( + "Setting alert_missing_scripts to False, we will not " + "ask again") + config.set_alert_missing_scripts(False) +# +# windows initializers +# + + def _windows_has_tap_device(): """ Loops over the windows registry trying to find if the tap0901 tap driver @@ -80,30 +165,6 @@ def _windows_has_tap_device(): return False -def _get_missing_updown_dialog(): - """ - Creates a dialog for notifying of missing updown scripts. - Returns that dialog. - - :rtype: QtGui.QMessageBox instance - """ - msg = QtGui.QMessageBox() - msg.setWindowTitle(msg.tr("Missing up/down scripts")) - msg.setText(msg.tr( - "LEAPClient needs to install up/down scripts " - "for Encrypted Internet to work properly. " - "Would you like to proceed?")) - msg.setInformativeText(msg.tr( - "It looks like either you have not installed " - "LEAP Client in a permanent location or you have an " - "incomplete installation. This will ask for " - "administrative privileges.")) - msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) - msg.addButton("No, don't ask again", QtGui.QMessageBox.RejectRole) - msg.setDefaultButton(QtGui.QMessageBox.Yes) - return msg - - def WindowsInitializer(): """ Raises a dialog in case that the windows tap driver has not been found @@ -124,6 +185,9 @@ def WindowsInitializer(): ret = msg.exec_() if ret == QtGui.QMessageBox.Yes: + # XXX should do this only if executed inside bundle. + # Let's assume it's the only way it's gonna be executed under win + # by now. driver_path = os.path.join(os.getcwd(), "apps", "eip", @@ -140,6 +204,10 @@ def WindowsInitializer(): logger.error("Tried to install TAP driver, but the installer " "is not found or not executable") +# +# Darwin initializer functions +# + def _darwin_has_tun_kext(): """ @@ -174,12 +242,15 @@ def _darwin_install_missing_scripts(badexec, notfound): "Resources", "openvpn") launcher = vpnlaunchers.DarwinVPNLauncher + + # TODO should change osascript by use of the proper + # os authorization api. if os.path.isdir(installer_path): - tempscript = tempfile.mktemp() + fd, tempscript = tempfile.mkstemp(prefix="leap_installer-") try: cmd = launcher.OSASCRIPT_BIN scriptlines = launcher.cmd_for_missing_scripts(installer_path) - with open(tempscript, 'w') as f: + with os.fdopen(fd, 'w') as f: f.write(scriptlines) st = os.stat(tempscript) os.chmod(tempscript, st.st_mode | stat.S_IEXEC | stat.S_IXUSR | @@ -190,14 +261,15 @@ def _darwin_install_missing_scripts(badexec, notfound): ret = subprocess.call( cmdline, stdout=subprocess.PIPE, shell=True) - assert(ret) + assert([ret]) # happy flakes except Exception as exc: logger.error(badexec) logger.error("Error was: %r" % (exc,)) - f.close() finally: - # XXX remove file - pass + try: + os.remove(tempscript) + except OSError as exc: + logger.error("%r" % (exc,)) else: logger.error(notfound) logger.debug('path searched: %s' % (installer_path,)) @@ -210,21 +282,11 @@ def DarwinInitializer(): """ # XXX split this function into several - NOTFOUND_MSG = ("Tried to install %s, but %s " - "not found inside this bundle.") - BADEXEC_MSG = ("Tried to install %s, but %s " - "failed to %s.") - TUNTAP_NOTFOUND_MSG = NOTFOUND_MSG % ( "tuntaposx kext", "the installer") TUNTAP_BADEXEC_MSG = BADEXEC_MSG % ( "tuntaposx kext", "the installer", "be launched") - UPDOWN_NOTFOUND_MSG = NOTFOUND_MSG % ( - "updown scripts", "those were") - UPDOWN_BADEXEC_MSG = BADEXEC_MSG % ( - "updown scripts", "they", "be copied") - # TODO DRY this with other cases, and # factor out to _should_install() function. # Leave the dialog as a more generic thing. @@ -261,24 +323,61 @@ def DarwinInitializer(): else: logger.error(TUNTAP_NOTFOUND_MSG) - config = LeapSettings() - alert_missing = config.get_alert_missing_scripts() - missing_scripts = vpnlaunchers.DarwinVPNLauncher.missing_updown_scripts - if alert_missing and missing_scripts(): - msg = _get_missing_updown_dialog() - ret = msg.exec_() + # Second check, for missing scripts. + check_missing() - if ret == QtGui.QMessageBox.Yes: - _darwin_install_missing_scripts( - UPDOWN_BADEXEC_MSG, - UPDOWN_NOTFOUND_MSG) - elif ret == QtGui.QMessageBox.No: - logger.debug("Not installing missing scripts, " - "user decided to ignore our warning.") +# +# Linux initializers +# - elif ret == QtGui.QMessageBox.Rejected: - logger.debug( - "Setting alert_missing_scripts to False, we will not " - "ask again") - config.set_alert_missing_scripts(False) +def _linux_install_missing_scripts(badexec, notfound): + """ + Tries to install the missing up/down scripts. + + :param badexec: error for notifying execution error during command. + :type badexec: str + :param notfound: error for notifying missing path. + :type notfound: str + """ + installer_path = os.path.join( + os.getcwd(), + "apps", "eip", "files") + launcher = vpnlaunchers.LinuxVPNLauncher + + # XXX refactor with darwin, same block. + + if os.path.isdir(installer_path): + fd, tempscript = tempfile.mkstemp(prefix="leap_installer-") + try: + pkexec = first(launcher.maybe_pkexec()) + scriptlines = launcher.cmd_for_missing_scripts(installer_path) + with os.fdopen(fd, 'w') as f: + f.write(scriptlines) + st = os.stat(tempscript) + os.chmod(tempscript, st.st_mode | stat.S_IEXEC | stat.S_IXUSR | + stat.S_IXGRP | stat.S_IXOTH) + cmdline = ["%s %s" % (pkexec, tempscript)] + ret = subprocess.call( + cmdline, stdout=subprocess.PIPE, + shell=True) + assert([ret]) # happy flakes + except Exception as exc: + logger.error(badexec) + logger.error("Error was: %r" % (exc,)) + finally: + try: + os.remove(tempscript) + except OSError as exc: + logger.error("%r" % (exc,)) + else: + logger.error(notfound) + logger.debug('path searched: %s' % (installer_path,)) + + +def LinuxInitializer(): + """ + Raises a dialog in case that either updown scripts or policykit file + are missing or they have incorrect permissions. + """ + check_missing() diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index fa2989bc..436072d2 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -35,6 +35,7 @@ from leap.common.check import leap_assert, leap_assert_type from leap.common.files import which from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipconfig import EIPConfig, VPNGatewaySelector +from leap.util import first logger = logging.getLogger(__name__) @@ -59,9 +60,11 @@ class VPNLauncher: """ Abstract launcher class """ - __metaclass__ = ABCMeta + UPDOWN_FILES = None + OTHER_FILES = None + @abstractmethod def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port=None): @@ -97,6 +100,35 @@ class VPNLauncher: """ return {} + @classmethod + def missing_updown_scripts(kls): + """ + Returns what updown scripts are missing. + :rtype: list + """ + leap_assert(kls.UPDOWN_FILES is not None, + "Need to define UPDOWN_FILES for this particular " + "auncher before calling this method") + file_exist = partial(_has_updown_scripts, warn=False) + zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) + missing = filter(lambda (path, exists): exists is False, zipped) + return [path for path, exists in missing] + + @classmethod + def missing_other_files(kls): + """ + Returns what other important files are missing during startup. + Same as missing_updown_scripts but does not check for exec bit. + :rtype: list + """ + leap_assert(kls.UPDOWN_FILES is not None, + "Need to define OTHER_FILES for this particular " + "auncher before calling this method") + file_exist = partial(_has_other_files, warn=False) + zipped = zip(kls.OTHER_FILES, map(file_exist, kls.OTHER_FILES)) + missing = filter(lambda (path, exists): exists is False, zipped) + return [path for path, exists in missing] + def get_platform_launcher(): launcher = globals()[platform.system() + "VPNLauncher"] @@ -117,7 +149,8 @@ def _is_pkexec_in_system(): def _has_updown_scripts(path, warn=True): """ - Checks the existence of the up/down scripts. + Checks the existence of the up/down scripts and its + exec bit if applicable. :param path: the path to be checked :type path: str @@ -132,6 +165,7 @@ def _has_updown_scripts(path, warn=True): logger.error("Could not find up/down script %s. " "Might produce DNS leaks." % (path,)) + # XXX check if applies in win is_exe = os.access(path, os.X_OK) if warn and not is_exe: logger.error("Up/down script %s is not executable. " @@ -139,6 +173,25 @@ def _has_updown_scripts(path, warn=True): return is_file and is_exe +def _has_other_files(path, warn=True): + """ + Checks the existence of other important files. + + :param path: the path to be checked + :type path: str + + :param warn: whether we should log the absence + :type warn: bool + + :rtype: bool + """ + is_file = os.path.isfile(path) + if warn and not is_file: + logger.warning("Could not find file during checks: %s. " % ( + path,)) + return is_file + + def _is_auth_agent_running(): """ Checks if a polkit daemon is running. @@ -160,8 +213,59 @@ class LinuxVPNLauncher(VPNLauncher): PKEXEC_BIN = 'pkexec' OPENVPN_BIN = 'openvpn' - UP_DOWN_SCRIPT = "/etc/leap/resolv-update" - OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" + SYSTEM_CONFIG = "/etc/leap" + UP_DOWN_FILE = "resolv-update" + UP_DOWN_PATH = "%s/%s" % (SYSTEM_CONFIG, UP_DOWN_FILE) + + # We assume this is there by our openvpn dependency, and + # we will put it there on the bundle too. + # TODO adapt to the bundle path. + OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-plugin-down-root.so" + + POLKIT_BASE = "/usr/share/polkit-1/actions" + POLKIT_FILE = "net.openvpn.gui.leap.policy" + POLKIT_PATH = "%s/%s" % (POLKIT_BASE, POLKIT_FILE) + + UPDOWN_FILES = (UP_DOWN_PATH,) + OTHER_FILES = (POLKIT_PATH,) + + @classmethod + def cmd_for_missing_scripts(kls, frompath): + """ + Returns a command that can copy the missing scripts. + :rtype: str + """ + to = kls.SYSTEM_CONFIG + cmd = "#!/bin/sh\nset -e\nmkdir -p %s\ncp %s/%s %s\ncp %s/%s %s" % ( + to, + frompath, kls.UP_DOWN_FILE, to, + frompath, kls.POLKIT_FILE, kls.POLKIT_PATH) + return cmd + + @classmethod + def maybe_pkexec(kls): + """ + Checks whether pkexec is available in the system, and + returns the path if found. + + Might raise EIPNoPkexecAvailable or EIPNoPolkitAuthAgentAvailable + + :returns: a list of the paths where pkexec is to be found + :rtype: list + """ + if _is_pkexec_in_system(): + if _is_auth_agent_running(): + pkexec_possibilities = which(kls.PKEXEC_BIN) + leap_assert(len(pkexec_possibilities) > 0, + "We couldn't find pkexec") + return pkexec_possibilities + else: + logger.warning("No polkit auth agent found. pkexec " + + "will use its own auth agent.") + raise EIPNoPolkitAuthAgentAvailable() + else: + logger.warning("System has no pkexec") + raise EIPNoPkexecAvailable() def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="unix"): @@ -201,30 +305,18 @@ class LinuxVPNLauncher(VPNLauncher): providerconfig.get_path_prefix(), "..", "apps", "eip") - openvpn_possibilities = which( - self.OPENVPN_BIN, - **kwargs) + openvpn_possibilities = which(self.OPENVPN_BIN, **kwargs) if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() - openvpn = openvpn_possibilities[0] + openvpn = first(openvpn_possibilities) args = [] - if _is_pkexec_in_system(): - if _is_auth_agent_running(): - pkexec_possibilities = which(self.PKEXEC_BIN) - leap_assert(len(pkexec_possibilities) > 0, - "We couldn't find pkexec") - args.append(openvpn) - openvpn = pkexec_possibilities[0] - else: - logger.warning("No polkit auth agent found. pkexec " + - "will use its own auth agent.") - raise EIPNoPolkitAuthAgentAvailable() - else: - logger.warning("System has no pkexec") - raise EIPNoPkexecAvailable() + pkexec = self.maybe_pkexec() + if pkexec: + args.append(openvpn) + openvpn = first(pkexec) # TODO: handle verbosity @@ -265,12 +357,12 @@ class LinuxVPNLauncher(VPNLauncher): '--script-security', '2' ] - if _has_updown_scripts(self.UP_DOWN_SCRIPT): + if _has_updown_scripts(self.UP_DOWN_PATH): args += [ - '--up', self.UP_DOWN_SCRIPT, - '--down', self.UP_DOWN_SCRIPT, + '--up', self.UP_DOWN_PATH, + '--down', self.UP_DOWN_PATH, '--plugin', self.OPENVPN_DOWN_ROOT, - '\'script_type=down %s\'' % self.UP_DOWN_SCRIPT + '\'script_type=down %s\'' % self.UP_DOWN_PATH ] args += [ @@ -323,17 +415,6 @@ class DarwinVPNLauncher(VPNLauncher): UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN) - @classmethod - def missing_updown_scripts(kls): - """ - Returns what updown scripts are missing. - :rtype: list - """ - file_exist = partial(_has_updown_scripts, warn=False) - zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) - missing = filter(lambda (path, exists): exists is False, zipped) - return [path for path, exists in missing] - @classmethod def cmd_for_missing_scripts(kls, frompath): """ @@ -387,7 +468,7 @@ class DarwinVPNLauncher(VPNLauncher): if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() - openvpn = openvpn_possibilities[0] + openvpn = first(openvpn_possibilities) args = [openvpn] # TODO: handle verbosity @@ -489,6 +570,8 @@ class WindowsVPNLauncher(VPNLauncher): OPENVPN_BIN = 'openvpn_leap.exe' + # XXX UPDOWN_FILES ... we do not have updown files defined yet! + def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="9876"): """ @@ -528,7 +611,7 @@ class WindowsVPNLauncher(VPNLauncher): if len(openvpn_possibilities) == 0: raise OpenVPNNotFoundException() - openvpn = openvpn_possibilities[0] + openvpn = first(openvpn_possibilities) args = [] # TODO: handle verbosity @@ -550,7 +633,6 @@ class WindowsVPNLauncher(VPNLauncher): ] openvpn_configuration = eipconfig.get_openvpn_configuration() - # XXX sanitize this for key, value in openvpn_configuration.items(): args += ['--%s' % (key,), value] @@ -558,13 +640,11 @@ class WindowsVPNLauncher(VPNLauncher): '--user', getpass.getuser(), #'--group', grp.getgrgid(os.getgroups()[-1]).gr_name ] - args += [ '--management-signal', '--management', socket_host, socket_port, '--script-security', '2' ] - args += [ '--cert', eipconfig.get_client_cert_path(providerconfig), '--key', eipconfig.get_client_cert_path(providerconfig), diff --git a/src/leap/util/__init__.py b/src/leap/util/__init__.py index 41358d38..5ceaede5 100644 --- a/src/leap/util/__init__.py +++ b/src/leap/util/__init__.py @@ -37,3 +37,13 @@ except ImportError: pass __full_version__ = __appname__ + '/' + str(__version__) + + +def first(things): + """ + Return the head of a collection. + """ + try: + return things[0] + except TypeError: + return None -- cgit v1.2.3 From 22afa7da7befa83f23eb039418edb39b0f5c2856 Mon Sep 17 00:00:00 2001 From: kali Date: Sat, 15 Jun 2013 23:43:50 +0900 Subject: Improve labels in the app * Clarify use of EIP * Closes #2695 --- src/leap/config/providerconfig.py | 5 ++++- src/leap/gui/mainwindow.py | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/leap/config/providerconfig.py b/src/leap/config/providerconfig.py index 68099ad4..6bbd7422 100644 --- a/src/leap/config/providerconfig.py +++ b/src/leap/config/providerconfig.py @@ -93,7 +93,10 @@ class ProviderConfig(BaseConfig): Returns a string with the services supported by the current provider, ready to be shown to the user. """ - return ", ".join(self.get_services()) + services_str = ", ".join(self.get_services()) + services_str = services_str.replace( + "openvpn", "Encrypted Internet") + return services_str def get_ca_cert_path(self, about_to_download=False): """ diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 51b96463..d5979395 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -474,8 +474,8 @@ class MainWindow(QtGui.QMainWindow): default_provider = self._settings.get_defaultprovider() if default_provider is None: - logger.info("Cannot autostart EIP because there's no default " - "provider configured") + logger.info("Cannot autostart Encrypted Internet because there is " + "no default provider configured") return self._action_eip_provider.setText(default_provider) @@ -983,7 +983,7 @@ class MainWindow(QtGui.QMainWindow): error=True) except OpenVPNNotFoundException: self._status_panel.set_eip_status( - self.tr("We couldn't find openvpn"), + self.tr("We couldn't find openvpn binary"), error=True) except VPNLauncherException as e: self._status_panel.set_eip_status("%s" % (e,), error=True) @@ -1087,7 +1087,7 @@ class MainWindow(QtGui.QMainWindow): else: if data[self._eip_bootstrapper.PASSED_KEY]: self._status_panel.set_eip_status( - self.tr("Could not load EIP Configuration"), + self.tr("Could not load Encrypted Internet Configuration"), error=True) else: self._status_panel.set_eip_status( -- cgit v1.2.3 From 77b4ddbfbe6f6697520614a925ac83e605905ca2 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 19 Jun 2013 13:11:11 -0300 Subject: Fix: clean lock files on Windows. Closes #2931 and #2909. --- src/leap/platform_init/locks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/platform_init/locks.py b/src/leap/platform_init/locks.py index fefc209b..c40c31d0 100644 --- a/src/leap/platform_init/locks.py +++ b/src/leap/platform_init/locks.py @@ -215,7 +215,7 @@ if platform_init.IS_WIN: try: shutil.rmtree(self.name) return True - except shutil.WindowsError as exc: + except WindowsError as exc: if exc.errno in (errno.EPIPE, errno.ENOENT, errno.ESRCH, errno.EACCES): logger.warning( -- cgit v1.2.3 From e699db4377d1b79c0c48eb3c753d373e44cceb13 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 19 Jun 2013 14:53:00 -0300 Subject: Comment out soledad temporarily. Closes #2932. --- src/leap/gui/mainwindow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index cf20c6eb..a5d022e9 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -42,7 +42,8 @@ from leap.gui.statuspanel import StatusPanelWidget from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.providerbootstrapper import ProviderBootstrapper -from leap.services.soledad.soledadbootstrapper import SoledadBootstrapper +# XXX: comment out soledad temporarily to avoid problem in Windows, issue #2932 +# from leap.services.soledad.soledadbootstrapper import SoledadBootstrapper from leap.services.mail.smtpbootstrapper import SMTPBootstrapper from leap.platform_init import IS_WIN from leap.platform_init.initializers import init_platform -- cgit v1.2.3 From d43a6905bc255946c8ecb02dfdfed5d159a86b26 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 20 Jun 2013 02:59:44 +0900 Subject: fix zero length field format in 2.6 --- src/leap/services/eip/vpnlaunchers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index af77c146..ef73ed94 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -478,7 +478,8 @@ class DarwinVPNLauncher(VPNLauncher): gateway_selector = VPNGatewaySelector(eipconfig) gateways = gateway_selector.get_gateways() - logger.debug("Using gateways ips: {}".format(', '.join(gateways))) + logger.debug("Using gateways ips: {gw}".format( + gw=', '.join(gateways))) for gw in gateways: args += ['--remote', gw, '1194', 'udp'] -- cgit v1.2.3 From cdf63528b09b702400e0aac0c6f39e79a7e29773 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 20 Jun 2013 03:02:09 +0900 Subject: nullify _tn instead of deleting --- src/leap/services/eip/vpnprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py index f3443533..4b558160 100644 --- a/src/leap/services/eip/vpnprocess.py +++ b/src/leap/services/eip/vpnprocess.py @@ -288,7 +288,7 @@ class VPNManager(object): self._tn.write("quit\n") self._tn.read_all() self._tn.get_socket().close() - del self._tn + self._tn = None def _connect_management(self, socket_host, socket_port): """ -- cgit v1.2.3 From 9a725220c393f28c3be5bb0f894b9eb7cf9af2ad Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 20 Jun 2013 03:19:57 +0900 Subject: fix for empty data --- src/leap/gui/statuspanel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/gui/statuspanel.py b/src/leap/gui/statuspanel.py index 19f84d4c..62a22725 100644 --- a/src/leap/gui/statuspanel.py +++ b/src/leap/gui/statuspanel.py @@ -177,11 +177,11 @@ class StatusPanelWidget(QtGui.QWidget): Updates the download/upload labels based on the data provided by the VPN thread """ - upload = float(data[VPNManager.TUNTAP_WRITE_KEY]) + upload = float(data[VPNManager.TUNTAP_WRITE_KEY] or "0") upload = upload / 1000.0 upload_str = "%12.2f Kb" % (upload,) self.ui.lblUpload.setText(upload_str) - download = float(data[VPNManager.TUNTAP_READ_KEY]) + download = float(data[VPNManager.TUNTAP_READ_KEY] or "0") download = download / 1000.0 download_str = "%12.2f Kb" % (download,) self.ui.lblDownload.setText(download_str) -- cgit v1.2.3 From c640e979a202021111a2b14e9ec089d78118fbb6 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 19 Jun 2013 16:44:04 -0300 Subject: Add psutil.error import to solve OSX problem. Closes #2936 --- src/leap/services/eip/vpnprocess.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py index f3443533..f1d06943 100644 --- a/src/leap/services/eip/vpnprocess.py +++ b/src/leap/services/eip/vpnprocess.py @@ -20,6 +20,7 @@ VPN Manager, spawned in a custom processProtocol. import logging import os import psutil +import psutil.error import shutil import socket -- cgit v1.2.3 From 8b690a0a84dcb19259e863e05d71423d1272b435 Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 20 Jun 2013 05:57:38 +0900 Subject: look for down-root plugin in bundle and system --- src/leap/services/eip/vpnlaunchers.py | 42 +++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index ef73ed94..01ec42cf 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -220,7 +220,11 @@ class LinuxVPNLauncher(VPNLauncher): # We assume this is there by our openvpn dependency, and # we will put it there on the bundle too. # TODO adapt to the bundle path. - OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-plugin-down-root.so" + OPENVPN_DOWN_ROOT_BASE = "/usr/lib/openvpn/" + OPENVPN_DOWN_ROOT_FILE = "openvpn-plugin-down-root.so" + OPENVPN_DOWN_ROOT_PATH = "%s/%s" % ( + OPENVPN_DOWN_ROOT_BASE, + OPENVPN_DOWN_ROOT_FILE) POLKIT_BASE = "/usr/share/polkit-1/actions" POLKIT_FILE = "net.openvpn.gui.leap.policy" @@ -267,6 +271,30 @@ class LinuxVPNLauncher(VPNLauncher): logger.warning("System has no pkexec") raise EIPNoPkexecAvailable() + @classmethod + def maybe_down_plugin(kls): + """ + Returns the path of the openvpn down-root-plugin, searching first + in the relative path for the standalone bundle, and then in the system + path where the debian package puts it. + + :returns: the path where the plugin was found, or None + :rtype: str or None + """ + cwd = os.getcwd() + rel_path_in_bundle = os.path.join( + 'apps', 'eip', 'files', kls.OPENVPN_DOWN_ROOT_FILE) + abs_path_in_bundle = os.path.join(cwd, rel_path_in_bundle) + if os.path.isfile(abs_path_in_bundle): + return abs_path_in_bundle + abs_path_in_system = kls.OPENVPN_DOWN_ROOT_FILE + if os.path.isfile(abs_path_in_system): + return abs_path_in_system + + logger.warning("We could not find the down-root-plugin, so no updown " + "scripts will be run. DNS leaks are likely!") + return None + def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="unix"): """ @@ -348,7 +376,7 @@ class LinuxVPNLauncher(VPNLauncher): '--group', grp.getgrgid(os.getgroups()[-1]).gr_name ] - if socket_port == "unix": + if socket_port == "unix": # that's always the case for linux args += [ '--management-client-user', getpass.getuser() ] @@ -359,11 +387,17 @@ class LinuxVPNLauncher(VPNLauncher): '--script-security', '2' ] - if _has_updown_scripts(self.UP_DOWN_PATH): + plugin_path = self.maybe_down_plugin() + # If we do not have the down plugin neither in the bundle + # nor in the system, we do not do updown scripts. The alternative + # is leaving the user without the ability to restore dns and routes + # to its original state. + + if plugin_path and _has_updown_scripts(self.UP_DOWN_PATH): args += [ '--up', self.UP_DOWN_PATH, '--down', self.UP_DOWN_PATH, - '--plugin', self.OPENVPN_DOWN_ROOT, + '--plugin', plugin_path, '\'script_type=down %s\'' % self.UP_DOWN_PATH ] -- cgit v1.2.3 From 6cb47a3627286dc64f781b5412ec66b6ab92560c Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 20 Jun 2013 07:57:51 +0900 Subject: do not apply systray menu workaround for osx Closes: #2920 --- src/leap/gui/mainwindow.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index a5d022e9..a3c601c5 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -39,6 +39,7 @@ from leap.gui.loggerwindow import LoggerWindow from leap.gui.wizard import Wizard from leap.gui.login import LoginWidget from leap.gui.statuspanel import StatusPanelWidget +from leap.platform_init import IS_MAC from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.providerbootstrapper import ProviderBootstrapper @@ -538,12 +539,12 @@ class MainWindow(QtGui.QMainWindow): self._action_visible.setText(get_action(visible)) context_menu = self._systray.contextMenu() - # for some reason, context_menu.show() - # is failing in a way beyond my understanding. - # (not working the first time it's clicked). - # this works however. - # XXX in osx it shows some glitches. - context_menu.exec_(self._systray.geometry().center()) + if not IS_MAC: + # for some reason, context_menu.show() + # is failing in a way beyond my understanding. + # (not working the first time it's clicked). + # this works however. + context_menu.exec_(self._systray.geometry().center()) def _toggle_visible(self): """ -- cgit v1.2.3 From d288330c6294a51cec6fd1dcc35b1b6f4f36932d Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 20 Jun 2013 08:38:23 +0900 Subject: tries to launch polkit authentication agent Closes: #2884 --- src/leap/services/eip/vpnlaunchers.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index ef73ed94..813527bd 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -23,6 +23,7 @@ import logging import getpass import os import platform +import subprocess try: import grp except ImportError: @@ -199,11 +200,25 @@ def _is_auth_agent_running(): :return: True if it's running, False if it's not. :rtype: boolean """ - polkit_gnome = 'ps aux | grep polkit-[g]nome-authentication-agent-1' - polkit_kde = 'ps aux | grep polkit-[k]de-authentication-agent-1' + ps = 'ps aux | grep polkit-%s-authentication-agent-1' + opts = (ps % case for case in ['[g]nome', '[k]de']) + is_running = map(lambda l: commands.getoutput(l), opts) + return any(is_running) - return (len(commands.getoutput(polkit_gnome)) > 0 or - len(commands.getoutput(polkit_kde)) > 0) + +def _try_to_launch_agent(): + """ + Tries to launch a polkit daemon. + """ + opts = [ + "/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1&", + # XXX add kde thing here + ] + for cmd in opts: + try: + subprocess.Popen([cmd], shell=True) + except: + pass class LinuxVPNLauncher(VPNLauncher): @@ -254,6 +269,8 @@ class LinuxVPNLauncher(VPNLauncher): :rtype: list """ if _is_pkexec_in_system(): + if not _is_auth_agent_running(): + _try_to_launch_agent() if _is_auth_agent_running(): pkexec_possibilities = which(kls.PKEXEC_BIN) leap_assert(len(pkexec_possibilities) > 0, -- cgit v1.2.3 From 4a422c92ae83453807074afca6c2f038823e296f Mon Sep 17 00:00:00 2001 From: kali Date: Thu, 20 Jun 2013 10:26:46 +0900 Subject: hide mx until we integrate it Closes: #2938 --- src/leap/config/providerconfig.py | 5 +++-- src/leap/gui/mainwindow.py | 4 ++++ src/leap/gui/wizard.py | 8 ++++++-- src/leap/services/__init__.py | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/config/providerconfig.py b/src/leap/config/providerconfig.py index 6bbd7422..94568c27 100644 --- a/src/leap/config/providerconfig.py +++ b/src/leap/config/providerconfig.py @@ -24,6 +24,7 @@ import os from leap.common.check import leap_assert from leap.common.config.baseconfig import BaseConfig, LocalizedKey from leap.config.provider_spec import leap_provider_spec +from leap.services import get_available logger = logging.getLogger(__name__) @@ -32,7 +33,6 @@ class ProviderConfig(BaseConfig): """ Provider configuration abstraction class """ - def __init__(self): BaseConfig.__init__(self) @@ -86,7 +86,8 @@ class ProviderConfig(BaseConfig): :rtype: list """ - return self._safe_get_value("services") + services = get_available(self._safe_get_value("services")) + return services def get_services_string(self): """ diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index a5d022e9..42ee5418 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -109,10 +109,14 @@ class MainWindow(QtGui.QMainWindow): QtGui.QMainWindow.__init__(self) # register leap events + + # XXX ------- this is blocking ----------- register(signal=proto.UPDATER_NEW_UPDATES, callback=self._new_updates_available) register(signal=proto.RAISE_WINDOW, callback=self._on_raise_window_event) + # XXX ------- this is blocking ----------- + self._quit_callback = quit_callback self._updates_content = "" diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 897bf94f..066a7db3 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -32,6 +32,7 @@ from leap.crypto.srpregister import SRPRegister from leap.util.privilege_policies import is_missing_policy_permissions from leap.util.request_helpers import get_content from leap.services.eip.providerbootstrapper import ProviderBootstrapper +from leap.services import get_available logger = logging.getLogger(__name__) @@ -514,13 +515,16 @@ class Wizard(QtGui.QWizard): def _populate_services(self): """ Loads the services that the provider provides into the UI for - the user to enable or disable + the user to enable or disable. """ self.ui.grpServices.setTitle( self.tr("Services by %s") % (self._provider_config.get_name(),)) - for service in self._provider_config.get_services(): + services = get_available( + self._provider_config.get_services()) + + for service in services: try: if service not in self._shown_services: checkbox = QtGui.QCheckBox(self) diff --git a/src/leap/services/__init__.py b/src/leap/services/__init__.py index e69de29b..70a5dcf2 100644 --- a/src/leap/services/__init__.py +++ b/src/leap/services/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# __init__.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Services module. +""" +NOT_YET_DEPLOYED = ["mx"] # for 0.2.2 release + + +def get_available(services): + """ + Returns a list of the available services. + + :param services: a list containing the services to be filtered. + :type services: list of str + + :returns: a list of the available services + :rtype: list of str + """ + return filter(lambda s: s not in NOT_YET_DEPLOYED, services) -- cgit v1.2.3 From b265380ebedb1603933251a6e8fd0e7c850eba5a Mon Sep 17 00:00:00 2001 From: Tomas Touceda Date: Fri, 21 Jun 2013 14:17:09 -0300 Subject: Use an alternative method to check for file permission --- src/leap/platform_init/initializers.py | 2 +- src/leap/services/eip/vpnlaunchers.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index 3374e32e..5345f11a 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -195,7 +195,7 @@ def WindowsInitializer(): dev_installer = os.path.join(driver_path, "devcon.exe") if os.path.isfile(dev_installer) and \ - os.access(dev_installer, os.X_OK): + stat.S_IXUSR & os.stat(dev_installer)[stat.ST_MODE] != 0: inf_path = os.path.join(driver_path, "OemWin2k.inf") cmd = [dev_installer, "install", inf_path, "tap0901"] diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 762b536d..c5b21eac 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -24,6 +24,7 @@ import getpass import os import platform import subprocess +import stat try: import grp except ImportError: @@ -167,7 +168,7 @@ def _has_updown_scripts(path, warn=True): "Might produce DNS leaks." % (path,)) # XXX check if applies in win - is_exe = os.access(path, os.X_OK) + is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0) if warn and not is_exe: logger.error("Up/down script %s is not executable. " "Might produce DNS leaks." % (path,)) -- cgit v1.2.3 From 440f2036e36985e9536644b28af0e62cea8704eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 21 Jun 2013 15:31:24 -0300 Subject: Disable remembering when not using a proper keyring backend --- src/leap/gui/login.py | 4 +++- src/leap/gui/mainwindow.py | 7 ++++--- src/leap/gui/wizard.py | 8 +++++--- src/leap/util/keyring_helpers.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 src/leap/util/keyring_helpers.py (limited to 'src') diff --git a/src/leap/gui/login.py b/src/leap/gui/login.py index c367b2fb..8fc6ec24 100644 --- a/src/leap/gui/login.py +++ b/src/leap/gui/login.py @@ -24,6 +24,8 @@ import keyring from PySide import QtCore, QtGui from ui_login import Ui_LoginWidget +from leap.util.keyring_helpers import has_keyring + logger = logging.getLogger(__name__) @@ -58,7 +60,7 @@ class LoginWidget(QtGui.QWidget): self.ui.chkRemember.stateChanged.connect( self._remember_state_changed) - self.ui.chkRemember.setEnabled(keyring.get_keyring() is not None) + self.ui.chkRemember.setEnabled(has_keyring()) self.ui.lnPassword.setEchoMode(QtGui.QLineEdit.Password) diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 2ed01917..9dc1e006 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -55,6 +55,7 @@ from leap.services.eip.vpnlaunchers import (VPNLauncherException, EIPNoPkexecAvailable, EIPNoPolkitAuthAgentAvailable) from leap.util import __version__ as VERSION +from leap.util.keyring_helpers import has_keyring from leap.services.mail.smtpconfig import SMTPConfig @@ -435,7 +436,7 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.set_user(possible_username) if possible_password is not None: self._login_widget.set_password(possible_password) - self._login_widget.set_remember(True) + self._login_widget.set_remember(has_keyring()) self._login() self._wizard = None self._settings.set_properprovider(True) @@ -454,7 +455,7 @@ class MainWindow(QtGui.QMainWindow): logger.error('Username@provider malformed. %r' % (e, )) saved_user = None - if saved_user is not None: + if saved_user is not None and has_keyring(): # fill the username self._login_widget.set_user(username) @@ -740,7 +741,7 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.set_status(self.tr("Logging in..."), error=False) self._login_widget.set_enabled(False) - if self._login_widget.get_remember(): + if self._login_widget.get_remember() and has_keyring(): # in the keyring and in the settings # we store the value 'usename@provider' username_domain = (username + '@' + provider).encode("utf8") diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 066a7db3..34eba84c 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -31,6 +31,7 @@ from leap.config.providerconfig import ProviderConfig from leap.crypto.srpregister import SRPRegister from leap.util.privilege_policies import is_missing_policy_permissions from leap.util.request_helpers import get_content +from leap.util.keyring_helpers import has_keyring from leap.services.eip.providerbootstrapper import ProviderBootstrapper from leap.services import get_available @@ -170,7 +171,7 @@ class Wizard(QtGui.QWizard): return self._password def get_remember(self): - return self.ui.chkRemember.isChecked() + return has_keyring() and self.ui.chkRemember.isChecked() def get_services(self): return self._selected_services @@ -286,8 +287,9 @@ class Wizard(QtGui.QWizard): self._set_registration_fields_visibility(False) # Allow the user to remember his password - self.ui.chkRemember.setVisible(True) - self.ui.chkRemember.setEnabled(True) + if has_keyring(): + self.ui.chkRemember.setVisible(True) + self.ui.chkRemember.setEnabled(True) self.page(self.REGISTER_USER_PAGE).set_completed() self.button(QtGui.QWizard.BackButton).setEnabled(False) diff --git a/src/leap/util/keyring_helpers.py b/src/leap/util/keyring_helpers.py new file mode 100644 index 00000000..b3dd0175 --- /dev/null +++ b/src/leap/util/keyring_helpers.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# privilege_policies.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" + +""" + +import keyring + +OBSOLETE_KEYRINGS = [ + keyring.backends.file.EncryptedKeyring, + keyring.backends.file.PlaintextKeyring +] + +def has_keyring(): + """ + + """ + kr = keyring.get_keyring() + return kr is not None and kr.__class__ not in OBSOLETE_KEYRINGS -- cgit v1.2.3 From 1f9acbe3366d08c280b9076274f612efabde3870 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 21 Jun 2013 17:35:46 -0300 Subject: Bugfix: return the correct gateway. After this fix we always returned the first gateway, no matter what the user asked for. --- src/leap/services/eip/eipconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index a85fe64a..ff98bf55 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -176,7 +176,7 @@ class EIPConfig(BaseConfig): index = 0 logger.warning("Provided an unknown gateway index %s, " + "defaulting to 0") - ip_addr_str = gateways[0]["ip_address"] + ip_addr_str = gateways[index]["ip_address"] try: ipaddr.IPAddress(ip_addr_str) -- cgit v1.2.3 From 4c54df049b3ef23b29c1e4e2c42201012843c8a1 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 21 Jun 2013 17:38:03 -0300 Subject: Allow to create the class using a specific offset. This is useful for testing purposes, so we can be consistent with the distance calculation. --- src/leap/services/eip/eipconfig.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index ff98bf55..e79314ce 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -39,17 +39,21 @@ class VPNGatewaySelector(object): VPN Gateway selector. """ - def __init__(self, eipconfig): + def __init__(self, eipconfig, tz_offset=None): ''' Constructor for VPNGatewaySelector. :param eipconfig: a valid EIP Configuration. :type eipconfig: EIPConfig + :param tz_offset: use this offset as a local distance to GMT. + :type tz_offset: datetime.timedelta ''' leap_assert_type(eipconfig, EIPConfig) - self._local_offset = 0 # defaults to GMT - self._local_timezone = None - self._set_local_offset() + + self._local_offset = tz_offset + if tz_offset is None: + self._local_offset = self._get_local_offset() + self._eipconfig = eipconfig def get_gateways(self): @@ -95,15 +99,17 @@ class VPNGatewaySelector(object): hours = diff.seconds / (60 * 60) return hours - def _set_local_offset(self): + def _get_local_offset(self): ''' - Sets the distance between GMT and the local timezone. + Returns the distance between GMT and the local timezone. + + :rtype: datetime.timedelta ''' local_offset = time.timezone if time.daylight: local_offset = time.altzone - self._local_offset = datetime.timedelta(seconds=-local_offset) + return datetime.timedelta(seconds=-local_offset) class EIPConfig(BaseConfig): @@ -233,6 +239,7 @@ if __name__ == "__main__": if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): print eipconfig.get_clusters() print eipconfig.get_gateways() + print eipconfig.get_locations() print eipconfig.get_openvpn_configuration() print eipconfig.get_serial() print eipconfig.get_version() -- cgit v1.2.3 From 63bc283c90a80ddc030e24fcc38c533b1e7c551d Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 24 Jun 2013 02:21:34 +0900 Subject: let errors go in their own panel --- src/leap/gui/mainwindow.py | 33 +++- src/leap/gui/statuspanel.py | 47 ++++- src/leap/gui/ui/statuspanel.ui | 343 +++++++++++++++++----------------- src/leap/services/eip/vpnlaunchers.py | 2 +- 4 files changed, 234 insertions(+), 191 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 9dc1e006..b0bf8a44 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -695,7 +695,7 @@ class MainWindow(QtGui.QMainWindow): download_if_needed=True) else: self._login_widget.set_status( - self.tr("Could not load provider configuration")) + self.tr("Could not load provider configuration.")) self._login_widget.set_enabled(True) else: self._login_widget.set_status( @@ -950,6 +950,7 @@ class MainWindow(QtGui.QMainWindow): Starts EIP """ + self._status_panel.eip_pre_up() provider_config = self._get_best_provider_config() try: @@ -977,7 +978,9 @@ class MainWindow(QtGui.QMainWindow): self._action_eip_startstop.triggered.connect( self._stop_eip) except EIPNoPolkitAuthAgentAvailable: - self._status_panel.set_eip_status( + self._status_panel.set_global_status( + # XXX this should change to polkit-kde where + # applicable. self.tr("We could not find any " "authentication " "agent in your system.
" @@ -986,20 +989,31 @@ class MainWindow(QtGui.QMainWindow): "agent-1
" "running and try again."), error=True) + self._set_eipstatus_off() except EIPNoPkexecAvailable: - self._status_panel.set_eip_status( + self._status_panel.set_global_status( self.tr("We could not find pkexec " "in your system."), error=True) + self._set_eipstatus_off() except OpenVPNNotFoundException: - self._status_panel.set_eip_status( - self.tr("We couldn't find openvpn binary"), + self._status_panel.set_global_status( + self.tr("We could not find openvpn binary."), error=True) + self._set_eipstatus_off() except VPNLauncherException as e: - self._status_panel.set_eip_status("%s" % (e,), error=True) + self._status_panel.set_gloal_status("%s" % (e,), error=True) + self._set_eipstatus_off() else: self._already_started_eip = True + #self._status_panel.set_startstop_enabled(True) + + def _set_eipstatus_off(self): + """ + Sets eip status to off + """ + self._status_panel.set_eip_status(self.tr("OFF"), error=True) self._status_panel.set_startstop_enabled(True) def _stop_eip(self): @@ -1009,7 +1023,7 @@ class MainWindow(QtGui.QMainWindow): """ self._vpn.terminate() - self._status_panel.set_eip_status(self.tr("Off")) + self._status_panel.set_eip_status(self.tr("OFF")) self._status_panel.set_eip_status_icon("error") self._status_panel.eip_stopped() self._action_eip_startstop.setText(self.tr("Turn ON")) @@ -1042,7 +1056,7 @@ class MainWindow(QtGui.QMainWindow): elif self._provisional_provider_config.loaded(): provider_config = self._provisional_provider_config else: - leap_assert(False, "We couldn't find any usable ProviderConfig") + leap_assert(False, "We could not find any usable ProviderConfig.") return provider_config @@ -1097,7 +1111,8 @@ class MainWindow(QtGui.QMainWindow): else: if data[self._eip_bootstrapper.PASSED_KEY]: self._status_panel.set_eip_status( - self.tr("Could not load Encrypted Internet Configuration"), + self.tr("Could not load Encrypted Internet " + "Configuration."), error=True) else: self._status_panel.set_eip_status( diff --git a/src/leap/gui/statuspanel.py b/src/leap/gui/statuspanel.py index 62a22725..de913f09 100644 --- a/src/leap/gui/statuspanel.py +++ b/src/leap/gui/statuspanel.py @@ -52,6 +52,8 @@ class StatusPanelWidget(QtGui.QWidget): self.ui.btnEipStartStop.clicked.connect( self.start_eip) + self.hide_status_box() + # Set the EIP status icons self.CONNECTING_ICON = None self.CONNECTED_ICON = None @@ -95,7 +97,7 @@ class StatusPanelWidget(QtGui.QWidget): def set_systray(self, systray): """ - Sets the systray object to use + Sets the systray object to use. :param systray: Systray object :type systray: QtGui.QSystemTrayIcon @@ -105,7 +107,7 @@ class StatusPanelWidget(QtGui.QWidget): def set_action_eip_status(self, action_eip_status): """ - Sets the action_eip_status to use + Sets the action_eip_status to use. :param action_eip_status: action_eip_status to be used :type action_eip_status: QtGui.QAction @@ -113,6 +115,28 @@ class StatusPanelWidget(QtGui.QWidget): leap_assert_type(action_eip_status, QtGui.QAction) self._action_eip_status = action_eip_status + def set_global_status(self, status, error=False): + """ + Sets the global status label. + + :param status: status message + :type status: str or unicode + :param error: if the status is an erroneous one, then set this + to True + :type error: bool + """ + leap_assert_type(error, bool) + if error: + status = "%s" % (status,) + self.ui.lblGlobalStatus.setText(status) + self.ui.globalStatusBox.show() + + def hide_status_box(self): + """ + Hide global status box. + """ + self.ui.globalStatusBox.hide() + def set_eip_status(self, status, error=False): """ Sets the status label at the VPN stage to status @@ -140,6 +164,15 @@ class StatusPanelWidget(QtGui.QWidget): leap_assert_type(value, bool) self.ui.btnEipStartStop.setEnabled(value) + def eip_pre_up(self): + """ + Triggered when the app activates eip. + Hides the status box and disables the start/stop button. + """ + self.hide_status_box() + self.set_startstop_enabled(False) + logger.debug('disabling buton.....................') + def eip_started(self): """ Sets the state of the widget to how it should look after EIP @@ -196,12 +229,14 @@ class StatusPanelWidget(QtGui.QWidget): """ status = data[VPNManager.STATUS_STEP_KEY] self.set_eip_status_icon(status) - if status == "AUTH": + if status == "CONNECTED": + self.set_eip_status(self.tr("ON")) + # Only now we can properly enable the button. + self.set_startstop_enabled(True) + elif status == "AUTH": self.set_eip_status(self.tr("Authenticating...")) elif status == "GET_CONFIG": self.set_eip_status(self.tr("Retrieving configuration...")) - elif status == "CONNECTED": - self.set_eip_status(self.tr("On")) elif status == "WAIT": self.set_eip_status(self.tr("Waiting to start...")) elif status == "ASSIGN_IP": @@ -210,7 +245,7 @@ class StatusPanelWidget(QtGui.QWidget): # Put the following calls in Qt's event queue, otherwise # the UI won't update properly QtCore.QTimer.singleShot(0, self.stop_eip) - QtCore.QTimer.singleShot(0, partial(self.set_eip_status, + QtCore.QTimer.singleShot(0, partial(self.set_global_status, self.tr("Unable to start VPN, " "it's already " "running."))) diff --git a/src/leap/gui/ui/statuspanel.ui b/src/leap/gui/ui/statuspanel.ui index 67f5f669..502ba78a 100644 --- a/src/leap/gui/ui/statuspanel.ui +++ b/src/leap/gui/ui/statuspanel.ui @@ -6,8 +6,8 @@ 0 0 - 506 - 403 + 542 + 477 @@ -29,200 +29,190 @@ + + true + - - - 12 - - - 16 - - - 0 - - - 0 - - - 0 - - - - - 0 + + + + + + + Encrypted Internet: + + + + + + + font: bold; + + + Off + + + Qt::AutoText + + + Qt::AlignCenter + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Turn On + + + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 0 + 11 + + + + + + + + + 64 + 64 + - - 0 + + + + :/images/light/64/network-eip-down.png + + + Qt::AlignCenter + + + + + + + 4 + + + QLayout::SetDefaultConstraint + + + + + + + + :/images/light/16/down-arrow.png + + + + + + + 0.0 Kb + + + - - + + + Qt::Horizontal + + + QSizePolicy::Fixed + + - 64 - 64 + 20 + 20 + + + + - :/images/light/64/network-eip-down.png + :/images/light/16/up-arrow.png - - Qt::AlignCenter + + + + + + 0.0 Kb - - - 0 - - - - - - - Encrypted Internet: - - - - - - - font: bold; - - - Off - - - Qt::AutoText - - - Qt::AlignCenter - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Turn On - - - - - - - - - 4 - - - QLayout::SetDefaultConstraint - - - - - - - - :/images/light/16/down-arrow.png - - - - - - - 0.0 Kb - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - - - - :/images/light/16/up-arrow.png - - - - - - - 0.0 Kb - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Preferred - - - - 0 - 11 - - - - - + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + false + + + + + + false + + + ... + + + + + + @@ -240,6 +230,9 @@ + lblProvider + status_rows + globalStatusBox diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index c5b21eac..939f51d7 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -212,7 +212,7 @@ def _try_to_launch_agent(): Tries to launch a polkit daemon. """ opts = [ - "/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1&", + "/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1", # XXX add kde thing here ] for cmd in opts: -- cgit v1.2.3 From 19c90e02a65d1d6fe30e6915cd2a9440878aef83 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 24 Jun 2013 02:57:26 +0900 Subject: Improve OpenVPN Process Control * Disable toggle button until we get "connected" status. #2956 * Catch abnormal termination and update UI accordingly. #2890 --- src/leap/gui/mainwindow.py | 54 ++++++++++++++++++++++++++++++++----- src/leap/gui/statuspanel.py | 1 - src/leap/gui/ui/statuspanel.ui | 3 +++ src/leap/services/eip/vpnprocess.py | 30 ++++++++++++++++++--- 4 files changed, 78 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index b0bf8a44..94343292 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -90,6 +90,9 @@ class MainWindow(QtGui.QMainWindow): new_updates = QtCore.Signal(object) raise_window = QtCore.Signal([]) + # We use this flag to detect abnormal terminations + user_stopped_eip = False + def __init__(self, quit_callback, standalone=False, bypass_checks=False): """ @@ -951,6 +954,7 @@ class MainWindow(QtGui.QMainWindow): Starts EIP """ self._status_panel.eip_pre_up() + self.user_stopped_eip = False provider_config = self._get_best_provider_config() try: @@ -1007,8 +1011,6 @@ class MainWindow(QtGui.QMainWindow): else: self._already_started_eip = True - #self._status_panel.set_startstop_enabled(True) - def _set_eipstatus_off(self): """ Sets eip status to off @@ -1016,11 +1018,16 @@ class MainWindow(QtGui.QMainWindow): self._status_panel.set_eip_status(self.tr("OFF"), error=True) self._status_panel.set_startstop_enabled(True) - def _stop_eip(self): + def _stop_eip(self, abnormal=False): """ Stops vpn process and makes gui adjustments to reflect the change of state. + + :param abnormal: whether this was an abnormal termination. + :type abnormal: bool + "" """ + self.user_stopped_eip = True self._vpn.terminate() self._status_panel.set_eip_status(self.tr("OFF")) @@ -1036,6 +1043,8 @@ class MainWindow(QtGui.QMainWindow): self._status_panel.set_provider( "%s@%s" % (self._logged_user, self._get_best_provider_config().get_domain())) + if abnormal: + self._status_panel.set_startstop_enabled(True) def _get_best_provider_config(self): """ @@ -1188,10 +1197,43 @@ class MainWindow(QtGui.QMainWindow): self._vpn.process_finished Triggered when the EIP/VPN process finishes to set the UI - accordingly + accordingly. """ - logger.debug("Finished VPN with exitCode %s" % (exitCode,)) - self._stop_eip() + logger.info("VPN process finished with exitCode %s..." + % (exitCode,)) + + # Ideally we would have the right exit code here, + # but the use of different wrappers (pkexec, cocoasudo) swallows + # the openvpn exit code so we get zero exit in some cases where we + # shouldn't. As a workaround we just use a flag to indicate + # a purposeful switch off, and mark everything else as unexpected. + + # In the near future we should trigger a native notification from here, + # since the user really really wants to know she is unprotected asap. + # And the right thing to do will be to fail-close. + + # TODO we should have a way of parsing the latest lines in the vpn + # log buffer so we can have a more precise idea of which type + # of error did we have (server side, local problem, etc) + abnormal = True + + # XXX check if these exitCodes are pkexec/cocoasudo specific + if exitCode in (126, 127): + self._status_panel.set_global_status( + self.tr("Encrypted Internet could not be launched " + "because you did not authenticate properly."), + error=True) + self._vpn.killit() + elif exitCode != 0 or not self.user_stopped_eip: + self._status_panel.set_global_status( + self.tr("Encrypted Internet finished in an " + "unexpected manner!"), error=True) + else: + abnormal = False + if exitCode == 0: + # XXX remove this warning after I fix cocoasudo. + logger.warning("The above exit code MIGHT BE WRONG.") + self._stop_eip(abnormal) def _on_raise_window_event(self, req): """ diff --git a/src/leap/gui/statuspanel.py b/src/leap/gui/statuspanel.py index de913f09..3e5a5093 100644 --- a/src/leap/gui/statuspanel.py +++ b/src/leap/gui/statuspanel.py @@ -171,7 +171,6 @@ class StatusPanelWidget(QtGui.QWidget): """ self.hide_status_box() self.set_startstop_enabled(False) - logger.debug('disabling buton.....................') def eip_started(self): """ diff --git a/src/leap/gui/ui/statuspanel.ui b/src/leap/gui/ui/statuspanel.ui index 502ba78a..1a2c77ad 100644 --- a/src/leap/gui/ui/statuspanel.ui +++ b/src/leap/gui/ui/statuspanel.ui @@ -208,6 +208,9 @@ ... + + true + diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py index d9fa85a6..0ec56ae7 100644 --- a/src/leap/services/eip/vpnprocess.py +++ b/src/leap/services/eip/vpnprocess.py @@ -142,6 +142,14 @@ class VPN(object): # after running out of patience, we try a killProcess logger.debug("Process did not died. Sending a SIGKILL.") + self.killit() + + def killit(self): + """ + Sends a kill signal to the process. + """ + self._stop_pollers() + self._vpnproc.aborted = True self._vpnproc.killProcess() def terminate(self, shutdown=False): @@ -225,11 +233,20 @@ class VPNManager(object): self._reactor = reactor self._tn = None self._qtsigs = qtsigs + self._aborted = False @property def qtsigs(self): return self._qtsigs + @property + def aborted(self): + return self._aborted + + @aborted.setter + def aborted(self, value): + self._aborted = value + def _seek_to_eof(self): """ Read as much as available. Position seek pointer to end of stream @@ -378,7 +395,7 @@ class VPNManager(object): """ # TODO decide about putting a max_lim to retries and signaling # an error. - if not self.is_connected(): + if not self.aborted and not self.is_connected(): self.connect_to_management(self._socket_host, self._socket_port) self._reactor.callLater( self.CONNECTION_RETRY_TIME, @@ -611,6 +628,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): self._last_state = None self._last_status = None + self._alive = False # processProtocol methods @@ -620,6 +638,8 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa """ + self._alive = True + self.aborted = False self.try_to_connect_to_management() def outReceived(self, data): @@ -643,6 +663,8 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): exit_code = reason.value.exitCode if isinstance(exit_code, int): logger.debug("processExited, status %d" % (exit_code,)) + self.qtsigs.process_finished.emit(exit_code) + self._alive = False def processEnded(self, reason): """ @@ -661,13 +683,15 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): """ Polls connection status. """ - self.get_status() + if self._alive: + self.get_status() def pollState(self): """ Polls connection state. """ - self.get_state() + if self._alive: + self.get_state() # launcher -- cgit v1.2.3 From f9257a1bf085736592a6a8daca4d4e11dfcc1748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 24 Jun 2013 11:33:38 -0300 Subject: Catch OSError when checking for permissions --- src/leap/services/eip/vpnlaunchers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index c5b21eac..7449acd0 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -168,7 +168,11 @@ def _has_updown_scripts(path, warn=True): "Might produce DNS leaks." % (path,)) # XXX check if applies in win - is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0) + is_exe = False + try: + is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0) + except OSError as e: + logger.warn("%s" % (e,)) if warn and not is_exe: logger.error("Up/down script %s is not executable. " "Might produce DNS leaks." % (path,)) -- cgit v1.2.3 From edda5a3c4762c7eeb3bdeda19ddfa0c72d98f387 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 24 Jun 2013 12:21:18 -0300 Subject: Bugfix: timezone calculation. Also use int notation instead of datetime.timedelta. --- src/leap/services/eip/eipconfig.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index e79314ce..97eb3dfb 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -21,7 +21,6 @@ Provider configuration import logging import os import re -import datetime import time import ipaddr @@ -46,7 +45,7 @@ class VPNGatewaySelector(object): :param eipconfig: a valid EIP Configuration. :type eipconfig: EIPConfig :param tz_offset: use this offset as a local distance to GMT. - :type tz_offset: datetime.timedelta + :type tz_offset: int ''' leap_assert_type(eipconfig, EIPConfig) @@ -93,23 +92,29 @@ class VPNGatewaySelector(object): :returns: distance between local offset and param offset. :rtype: int ''' - delta1 = datetime.timedelta(hours=offset) - delta2 = self._local_offset - diff = abs(delta1 - delta2) - hours = diff.seconds / (60 * 60) - return hours + timezones = range(-11, 13) + tz1 = offset + tz2 = self._local_offset + distance = abs(timezones.index(tz1) - timezones.index(tz2)) + if distance > 12: + if tz1 < 0: + distance = timezones.index(tz1) + timezones[::-1].index(tz2) + else: + distance = timezones[::-1].index(tz1) + timezones.index(tz2) + + return distance def _get_local_offset(self): ''' Returns the distance between GMT and the local timezone. - :rtype: datetime.timedelta + :rtype: int ''' local_offset = time.timezone if time.daylight: local_offset = time.altzone - return datetime.timedelta(seconds=-local_offset) + return local_offset / 3600 class EIPConfig(BaseConfig): -- cgit v1.2.3 From eeb9396cc3320e43e2e2f8ff62228aa53585fdc3 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 21 Jun 2013 17:40:20 -0300 Subject: Add test for vpngatewayselector class. --- .../services/eip/tests/test_vpngatewayselector.py | 86 ++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/leap/services/eip/tests/test_vpngatewayselector.py (limited to 'src') diff --git a/src/leap/services/eip/tests/test_vpngatewayselector.py b/src/leap/services/eip/tests/test_vpngatewayselector.py new file mode 100644 index 00000000..250e6e00 --- /dev/null +++ b/src/leap/services/eip/tests/test_vpngatewayselector.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# test_vpngatewayselector.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +tests for vpngatewayselector +""" + +import unittest + +from leap.services.eip.eipconfig import EIPConfig, VPNGatewaySelector +from leap.common.testing.basetest import BaseLeapTest +from mock import Mock + + +sample_gateways = [ + {u'host': u'gateway1.com', + u'ip_address': u'1.2.3.4', + u'location': u'location1'}, + {u'host': u'gateway2.com', + u'ip_address': u'2.3.4.5', + u'location': u'location2'}, + {u'host': u'gateway3.com', + u'ip_address': u'3.4.5.6', + u'location': u'location3'} +] + +sample_locations = { + u'location1': {u'timezone': u'2'}, + u'location2': {u'timezone': u'-7'}, + u'location3': {u'timezone': u'-4'} +} + + +class VPNGatewaySelectorTest(BaseLeapTest): + """ + VPNGatewaySelector's tests. + """ + def setUp(self): + self.eipconfig = EIPConfig() + self.eipconfig.get_gateways = Mock(return_value=sample_gateways) + self.eipconfig.get_locations = Mock(return_value=sample_locations) + + def tearDown(self): + pass + + def test_correct_order_gmt(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 0) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [u'1.2.3.4', u'3.4.5.6', u'2.3.4.5']) + + def test_correct_order_gmt_minus_3(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, -3) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [u'3.4.5.6', u'2.3.4.5', u'1.2.3.4']) + + def test_correct_order_gmt_minus_7(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, -7) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [u'2.3.4.5', u'3.4.5.6', u'1.2.3.4']) + + def test_correct_order_gmt_plus_5(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 5) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [u'1.2.3.4', u'3.4.5.6', u'2.3.4.5']) + + def test_correct_order_gmt_plus_10(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 10) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [u'2.3.4.5', u'1.2.3.4', u'3.4.5.6']) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From 6873b5b5ef4d1e4af44fe41a52f402d3d9a16d5c Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 20 Jun 2013 07:52:37 +0900 Subject: use cocoasudo instead of osascript Closes:#2925 --- src/leap/services/eip/vpnlaunchers.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 56df0b1c..570a7893 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -458,12 +458,12 @@ class DarwinVPNLauncher(VPNLauncher): VPN launcher for the Darwin Platform """ - OSASCRIPT_BIN = '/usr/bin/osascript' - OSX_ASADMIN = "do shell script \"%s\" with administrator privileges" + COCOASUDO = "cocoasudo" + # XXX need magic translate for this string + SUDO_MSG = ("LEAP needs administrative privileges to run " + "Encrypted Internet.") INSTALL_PATH = "/Applications/LEAP\ Client.app" - # OPENVPN_BIN = "/%s/Contents/Resources/openvpn.leap" % ( - # self.INSTALL_PATH,) OPENVPN_BIN = 'openvpn.leap' OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,) @@ -481,9 +481,25 @@ class DarwinVPNLauncher(VPNLauncher): """ to = kls.OPENVPN_PATH cmd = "#!/bin/sh\nmkdir -p %s\ncp \"%s/\"* %s" % (to, frompath, to) - #return kls.OSX_ASADMIN % cmd return cmd + def get_cocoasudo_cmd(self): + """ + Returns a string with the cocoasudo command needed to run openvpn + as admin with a nice password prompt. The actual command needs to be + appended. + + :rtype: (str, list) + """ + iconpath = os.path.abspath(os.path.join( + os.getcwd(), + "../../../Resources/leap-client.tiff")) + has_icon = os.path.isfile(iconpath) + args = ["--icon=%s" % iconpath] if has_icon else [] + args.append("--prompt=%s" % (self.SUDO_MSG,)) + + return self.COCOASUDO, args + def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="unix"): """ @@ -597,11 +613,8 @@ class DarwinVPNLauncher(VPNLauncher): '--ca', providerconfig.get_ca_cert_path() ] - # We are using osascript until we can write a proper wrapper - # for privilege escalation. - - command = self.OSASCRIPT_BIN - cmd_args = ["-e", self.OSX_ASADMIN % (' '.join(args),)] + command, cargs = self.get_cocoasudo_cmd() + cmd_args = cargs + args logger.debug("Running VPN with command:") logger.debug("%s %s" % (command, " ".join(cmd_args))) -- cgit v1.2.3 From 3def1e7b086f8048d2b07158ee1fc6c8bd2b506e Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 24 Jun 2013 16:09:59 -0300 Subject: Add PySide tests helper. Look at https://github.com/jasonmccampbell/PySide/blob/master/tests/signals/signal_emission_gui_test.py for an usage example. The original file is: PySide/tests/util/helper/__init__.py --- src/leap/util/pyside_tests_helper.py | 133 +++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/leap/util/pyside_tests_helper.py (limited to 'src') diff --git a/src/leap/util/pyside_tests_helper.py b/src/leap/util/pyside_tests_helper.py new file mode 100644 index 00000000..a010934a --- /dev/null +++ b/src/leap/util/pyside_tests_helper.py @@ -0,0 +1,133 @@ + +'''Helper classes and functions''' + +import os +import unittest + +from random import randint + +from PySide.QtCore import QCoreApplication, QTimer + +try: + from PySide.QtGui import QApplication +except ImportError: + has_gui = False +else: + has_gui = True + +def adjust_filename(filename, orig_mod_filename): + dirpath = os.path.dirname(os.path.abspath(orig_mod_filename)) + return os.path.join(dirpath, filename) + +class NoQtGuiError(Exception): + def __init__(self): + Exception.__init__(self, 'No QtGui found') + +class BasicPySlotCase(object): + '''Base class that tests python slots and signal emissions. + + Python slots are defined as any callable passed to QObject.connect(). + ''' + def setUp(self): + self.called = False + + def tearDown(self): + try: + del self.args + except: + pass + + def cb(self, *args): + '''Simple callback with arbitrary arguments. + + The test function must setup the 'args' attribute with a sequence + containing the arguments expected to be received by this slot. + Currently only a single connection is supported. + ''' + if tuple(self.args) == args: + self.called = True + else: + raise ValueError('Invalid arguments for callback') + + +_instance = None +_timed_instance = None + +if has_gui: + class UsesQApplication(unittest.TestCase): + '''Helper class to provide QApplication instances''' + + qapplication = True + + def setUp(self): + '''Creates the QApplication instance''' + + # Simple way of making instance a singleton + super(UsesQApplication, self).setUp() + global _instance + if _instance is None: + _instance = QApplication([]) + + self.app = _instance + + def tearDown(self): + '''Deletes the reference owned by self''' + del self.app + super(UsesQApplication, self).tearDown() + + + class TimedQApplication(unittest.TestCase): + '''Helper class with timed QApplication exec loop''' + + def setUp(self, timeout=100): + '''Setups this Application. + + timeout - timeout in milisseconds''' + global _timed_instance + if _timed_instance is None: + _timed_instance = QApplication([]) + + self.app = _timed_instance + QTimer.singleShot(timeout, self.app.quit) + + def tearDown(self): + '''Delete resources''' + del self.app +else: + class UsesQApplication(unittest.TestCase): + def setUp(self): + raise NoQtGuiError() + class TimedQapplication(unittest.TestCase): + def setUp(self): + raise NoQtGuiError() + + +_core_instance = None + +class UsesQCoreApplication(unittest.TestCase): + '''Helper class for test cases that require an QCoreApplication + Just connect or call self.exit_app_cb. When called, will ask + self.app to exit. + ''' + + def setUp(self): + '''Set up resources''' + + global _core_instance + if _core_instance is None: + _core_instance = QCoreApplication([]) + + self.app = _core_instance + + def tearDown(self): + '''Release resources''' + del self.app + + def exit_app_cb(self): + '''Quits the application''' + self.app.exit(0) + + +def random_string(size=5): + '''Generate random string with the given size''' + return ''.join(map(chr, [randint(33, 126) for x in range(size)])) -- cgit v1.2.3 From 36657c76b4c8511d5938dc6b81fde34c833ab7c7 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 25 Jun 2013 11:37:36 -0300 Subject: Change method for 'dependency injection' in test. --- src/leap/util/leap_log_handler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/util/leap_log_handler.py b/src/leap/util/leap_log_handler.py index e5bc87e1..271096d3 100644 --- a/src/leap/util/leap_log_handler.py +++ b/src/leap/util/leap_log_handler.py @@ -44,7 +44,7 @@ class LogHandler(logging.Handler): logging.Handler.__init__(self) self._qtsignal = qtsignal - def _set_format(self, logging_level): + def _get_format(self, logging_level): """ Sets the log format depending on the parameter. It uses html and css to set the colors for the logs. @@ -70,7 +70,7 @@ class LogHandler(logging.Handler): log_format = ' - '.join(format_attrs) formatter = logging.Formatter(log_format) - self.setFormatter(formatter) + return formatter def emit(self, logRecord): """ @@ -82,7 +82,7 @@ class LogHandler(logging.Handler): :param logRecord: the record emitted by the logging module. :type logRecord: logging.LogRecord. """ - self._set_format(logRecord.levelname) + self.setFormatter(self._get_format(logRecord.levelname)) log = self.format(logRecord) log_item = {self.RECORD_KEY: logRecord, self.MESSAGE_KEY: log} self._log_history.append(log_item) -- cgit v1.2.3 From 16ea8b5d78c990e75367d405619c55272ecf09e6 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 24 Jun 2013 17:08:34 -0300 Subject: Add test for the LEAP log handler --- src/leap/util/tests/__init__.py | 0 src/leap/util/tests/test_leap_log_handler.py | 118 +++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 src/leap/util/tests/__init__.py create mode 100644 src/leap/util/tests/test_leap_log_handler.py (limited to 'src') diff --git a/src/leap/util/tests/__init__.py b/src/leap/util/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/util/tests/test_leap_log_handler.py b/src/leap/util/tests/test_leap_log_handler.py new file mode 100644 index 00000000..ea509ea8 --- /dev/null +++ b/src/leap/util/tests/test_leap_log_handler.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# test_leap_log_handler.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +tests for leap_log_handler +""" + +import unittest + +import logging + +from leap.util.leap_log_handler import LeapLogHandler +from leap.common.testing.basetest import BaseLeapTest +from leap.util.pyside_tests_helper import BasicPySlotCase + +from mock import Mock + + +class LeapLogHandlerTest(BaseLeapTest, BasicPySlotCase): + """ + LeapLogHandlerTest's tests. + """ + def _callback(self, *args): + """ + Simple callback to track if a signal was emitted. + """ + self.called = True + self.emitted_msg = args[0][LeapLogHandler.MESSAGE_KEY] + + def setUp(self): + BasicPySlotCase.setUp(self) + + # Create the logger + level = logging.DEBUG + self.logger = logging.getLogger(name='test') + self.logger.setLevel(level) + + # Create the handler + self.leap_handler = LeapLogHandler() + self.leap_handler.setLevel(level) + self.logger.addHandler(self.leap_handler) + + def tearDown(self): + BasicPySlotCase.tearDown(self) + try: + self.leap_handler.new_log.disconnect() + except Exception: + pass + + def test_history_starts_empty(self): + self.assertEqual(self.leap_handler.log_history, []) + + def test_one_log_captured(self): + self.logger.debug('test') + self.assertEqual(len(self.leap_handler.log_history), 1) + + def test_history_records_order(self): + self.logger.debug('test 01') + self.logger.debug('test 02') + self.logger.debug('test 03') + + logs = [] + for message in self.leap_handler.log_history: + logs.append(message[LeapLogHandler.RECORD_KEY].msg) + + self.assertIn('test 01', logs) + self.assertIn('test 02', logs) + self.assertIn('test 03', logs) + + def test_history_messages_order(self): + self.logger.debug('test 01') + self.logger.debug('test 02') + self.logger.debug('test 03') + + logs = [] + for message in self.leap_handler.log_history: + logs.append(message[LeapLogHandler.MESSAGE_KEY]) + + self.assertIn('test 01', logs[0]) + self.assertIn('test 02', logs[1]) + self.assertIn('test 03', logs[2]) + + def test_emits_signal(self): + log_format = '%(name)s - %(levelname)s - %(message)s' + formatter = logging.Formatter(log_format) + get_format = Mock(return_value=formatter) + self.leap_handler._handler._get_format = get_format + + self.leap_handler.new_log.connect(self._callback) + self.logger.debug('test') + + expected_log_msg = "test - DEBUG - test" + + # signal emitted + self.assertTrue(self.called) + + # emitted message + self.assertEqual(self.emitted_msg, expected_log_msg) + + # Mock called + self.assertTrue(get_format.called) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From 38809bd51fe72a351de6eaf951df6495e6aa7541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 24 Jun 2013 20:35:27 -0300 Subject: Return the deferred so that we can use it for other things --- src/leap/services/abstractbootstrapper.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/leap/services/abstractbootstrapper.py b/src/leap/services/abstractbootstrapper.py index f0937197..633d818d 100644 --- a/src/leap/services/abstractbootstrapper.py +++ b/src/leap/services/abstractbootstrapper.py @@ -157,3 +157,4 @@ class AbstractBootstrapper(QtCore.QObject): d.addErrback(self._errback, signal=sig) d.addCallback(self._gui_notify, signal=sig) d.addErrback(self._gui_errback) + return d -- cgit v1.2.3 From 900543f380ec6e002023be4d5210e0d93c74ee70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 24 Jun 2013 20:44:10 -0300 Subject: Add tests for AbstractBootstrapper --- src/leap/services/tests/__init__.py | 0 .../services/tests/test_abstractbootstrapper.py | 196 +++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 src/leap/services/tests/__init__.py create mode 100644 src/leap/services/tests/test_abstractbootstrapper.py (limited to 'src') diff --git a/src/leap/services/tests/__init__.py b/src/leap/services/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/services/tests/test_abstractbootstrapper.py b/src/leap/services/tests/test_abstractbootstrapper.py new file mode 100644 index 00000000..a9ee220f --- /dev/null +++ b/src/leap/services/tests/test_abstractbootstrapper.py @@ -0,0 +1,196 @@ +## -*- coding: utf-8 -*- +# test_abstrctbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +Tests for the Abstract Boostrapper functionality +""" + +import mock + +from PySide import QtCore + +from nose.twistedtools import deferred + +from leap.services.abstractbootstrapper import AbstractBootstrapper +from leap.util.pyside_tests_helper import UsesQApplication, BasicPySlotCase + + +class TesterBootstrapper(AbstractBootstrapper): + test_signal1 = QtCore.Signal(dict) + test_signal2 = QtCore.Signal(dict) + test_signal3 = QtCore.Signal(dict) + + ERROR_MSG = "This is a test error msg" + + def _check_that_passes(self, *args): + pass + + def _second_check_that_passes(self, *args): + pass + + def _check_that_fails(self, *args): + raise Exception(self.ERROR_MSG) + + def run_checks_pass(self): + cb_chain = [ + (self._check_that_passes, self.test_signal1), + (self._second_check_that_passes, self.test_signal2), + ] + return self.addCallbackChain(cb_chain) + + def run_second_checks_pass(self): + cb_chain = [ + (self._check_that_passes, None), + ] + return self.addCallbackChain(cb_chain) + + def run_checks_fail(self): + cb_chain = [ + (self._check_that_passes, self.test_signal1), + (self._check_that_fails, self.test_signal2) + ] + return self.addCallbackChain(cb_chain) + + def run_second_checks_fail(self): + cb_chain = [ + (self._check_that_passes, self.test_signal1), + (self._check_that_fails, self.test_signal2), + (self._second_check_that_passes, self.test_signal1) + ] + return self.addCallbackChain(cb_chain) + + def run_third_checks_fail(self): + cb_chain = [ + (self._check_that_passes, self.test_signal1), + (self._check_that_fails, None) + ] + return self.addCallbackChain(cb_chain) + + +class AbstractBootstrapperTest(UsesQApplication, BasicPySlotCase): + def setUp(self): + UsesQApplication.setUp(self) + BasicPySlotCase.setUp(self) + + self.tbt = TesterBootstrapper() + self.called1 = self.called2 = 0 + + @deferred() + def test_all_checks_executed_once(self): + self.tbt._check_that_passes = mock.MagicMock() + self.tbt._second_check_that_passes = mock.MagicMock() + + d = self.tbt.run_checks_pass() + + def check(*args): + self.tbt._check_that_passes.assert_called_once_with() + self.tbt._second_check_that_passes.\ + assert_called_once_with(None) + + d.addCallback(check) + return d + + ####################################################################### + # Dummy callbacks that test the arguments expected from a certain + # signal and only allow being called once + + def cb1(self, *args): + if tuple(self.args1) == args: + self.called1 += 1 + else: + raise ValueError('Invalid arguments for callback') + + def cb2(self, *args): + if tuple(self.args2) == args: + self.called2 += 1 + else: + raise ValueError('Invalid arguments for callback') + + # + ####################################################################### + + def _check_cb12_once(self, *args): + self.assertEquals(self.called1, 1) + self.assertEquals(self.called2, 1) + + @deferred() + def test_emits_correct(self): + self.tbt.test_signal1.connect(self.cb1) + self.tbt.test_signal2.connect(self.cb2) + d = self.tbt.run_checks_pass() + + self.args1 = [{ + AbstractBootstrapper.PASSED_KEY: True, + AbstractBootstrapper.ERROR_KEY: "" + }] + + self.args2 = self.args1 + + d.addCallback(self._check_cb12_once) + return d + + @deferred() + def test_emits_failed(self): + self.tbt.test_signal1.connect(self.cb1) + self.tbt.test_signal2.connect(self.cb2) + d = self.tbt.run_checks_fail() + + self.args1 = [{ + AbstractBootstrapper.PASSED_KEY: True, + AbstractBootstrapper.ERROR_KEY: "" + }] + + self.args2 = [{ + AbstractBootstrapper.PASSED_KEY: False, + AbstractBootstrapper.ERROR_KEY: + TesterBootstrapper.ERROR_MSG + }] + + d.addCallback(self._check_cb12_once) + return d + + @deferred() + def test_emits_failed_and_stops(self): + self.tbt.test_signal1.connect(self.cb1) + self.tbt.test_signal2.connect(self.cb2) + self.tbt.test_signal3.connect(self.cb1) + d = self.tbt.run_second_checks_fail() + + self.args1 = [{ + AbstractBootstrapper.PASSED_KEY: True, + AbstractBootstrapper.ERROR_KEY: "" + }] + + self.args2 = [{ + AbstractBootstrapper.PASSED_KEY: False, + AbstractBootstrapper.ERROR_KEY: + TesterBootstrapper.ERROR_MSG + }] + + d.addCallback(self._check_cb12_once) + return d + + @deferred() + def test_failed_without_signal(self): + d = self.tbt.run_third_checks_fail() + return d + + @deferred() + def test_sucess_without_signal(self): + d = self.tbt.run_second_checks_pass() + return d -- cgit v1.2.3 From 77d9434c4a92528e13b008a50dfbcb5f2f1456a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Mon, 24 Jun 2013 22:45:04 -0300 Subject: pep8 fixes --- src/leap/util/keyring_helpers.py | 1 + src/leap/util/pyside_tests_helper.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/leap/util/keyring_helpers.py b/src/leap/util/keyring_helpers.py index b3dd0175..b815d385 100644 --- a/src/leap/util/keyring_helpers.py +++ b/src/leap/util/keyring_helpers.py @@ -26,6 +26,7 @@ OBSOLETE_KEYRINGS = [ keyring.backends.file.PlaintextKeyring ] + def has_keyring(): """ diff --git a/src/leap/util/pyside_tests_helper.py b/src/leap/util/pyside_tests_helper.py index a010934a..5c0eb8d6 100644 --- a/src/leap/util/pyside_tests_helper.py +++ b/src/leap/util/pyside_tests_helper.py @@ -15,14 +15,17 @@ except ImportError: else: has_gui = True + def adjust_filename(filename, orig_mod_filename): dirpath = os.path.dirname(os.path.abspath(orig_mod_filename)) return os.path.join(dirpath, filename) + class NoQtGuiError(Exception): def __init__(self): Exception.__init__(self, 'No QtGui found') + class BasicPySlotCase(object): '''Base class that tests python slots and signal emissions. @@ -75,7 +78,6 @@ if has_gui: del self.app super(UsesQApplication, self).tearDown() - class TimedQApplication(unittest.TestCase): '''Helper class with timed QApplication exec loop''' @@ -97,13 +99,14 @@ else: class UsesQApplication(unittest.TestCase): def setUp(self): raise NoQtGuiError() + class TimedQapplication(unittest.TestCase): def setUp(self): raise NoQtGuiError() - _core_instance = None + class UsesQCoreApplication(unittest.TestCase): '''Helper class for test cases that require an QCoreApplication Just connect or call self.exit_app_cb. When called, will ask -- cgit v1.2.3 From 320909489ad8f5d14e190968098edcded51ee016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 25 Jun 2013 15:55:23 -0300 Subject: Properly return the error responseCode when login is already taken --- src/leap/crypto/tests/fake_provider.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/leap/crypto/tests/fake_provider.py b/src/leap/crypto/tests/fake_provider.py index 74a735ff..b943040a 100755 --- a/src/leap/crypto/tests/fake_provider.py +++ b/src/leap/crypto/tests/fake_provider.py @@ -139,6 +139,7 @@ class FakeUsers(Resource): verifier = args['user[password_verifier]'][0] if login in _USERDB: + request.setResponseCode(422) return "%s\n" % json.dumps( {'errors': {'login': 'already taken!'}}) -- cgit v1.2.3 From bc3652f5c51bdd414d85a2388ee6cba757eca19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 25 Jun 2013 15:56:09 -0300 Subject: Improve SRPRegister tests --- src/leap/crypto/tests/test_srpregister.py | 139 ++++++++++++++---------------- src/leap/crypto/tests/wrongcert.pem | 33 +++++++ 2 files changed, 100 insertions(+), 72 deletions(-) create mode 100644 src/leap/crypto/tests/wrongcert.pem (limited to 'src') diff --git a/src/leap/crypto/tests/test_srpregister.py b/src/leap/crypto/tests/test_srpregister.py index 5ba7306f..f70382ce 100644 --- a/src/leap/crypto/tests/test_srpregister.py +++ b/src/leap/crypto/tests/test_srpregister.py @@ -27,8 +27,9 @@ import os import sys from mock import MagicMock -from nose.twistedtools import reactor, threaded_reactor, stop_reactor +from nose.twistedtools import reactor, deferred from twisted.python import log +from twisted.internet import threads from leap.common.testing.https_server import where from leap.config.providerconfig import ProviderConfig @@ -89,19 +90,6 @@ class SRPTestCase(unittest.TestCase): cls.register = srpregister.SRPRegister(provider_config=provider) cls.auth = srpauth.SRPAuth(provider) - cls._auth_instance = cls.auth.__dict__['_SRPAuth__instance'] - cls.authenticate = cls._auth_instance.authenticate - cls.logout = cls._auth_instance.logout - - # run! - threaded_reactor() - - @classmethod - def tearDownClass(cls): - """ - Stops reactor when tearing down the class - """ - stop_reactor() # helper methods @@ -114,6 +102,41 @@ class SRPTestCase(unittest.TestCase): # Register tests + def test_none_port(self): + provider = ProviderConfig() + provider.get_api_uri = MagicMock() + provider.get_api_uri.return_value = "http://localhost/" + loaded = provider.load(path=os.path.join( + _here, "test_provider.json")) + if not loaded: + raise ImproperlyConfiguredError( + "Could not load test provider config") + + register = srpregister.SRPRegister(provider_config=provider) + self.assertEquals(register._port, "443") + + @deferred() + def test_wrong_cert(self): + provider = ProviderConfig() + loaded = provider.load(path=os.path.join( + _here, "test_provider.json")) + provider.get_ca_cert_path = MagicMock() + provider.get_ca_cert_path.return_value = os.path.join( + _here, + "wrongcacert.pem") + provider.get_api_uri = MagicMock() + provider.get_api_uri.return_value = self._get_https_uri() + if not loaded: + raise ImproperlyConfiguredError( + "Could not load test provider config") + + register = srpregister.SRPRegister(provider_config=provider) + d = threads.deferToThread(register.register_user, "foouser_firsttime", + "barpass") + d.addCallback(self.assertFalse) + return d + + @deferred() def test_register_user(self): """ Checks if the registration of an unused name works as expected when @@ -121,17 +144,31 @@ class SRPTestCase(unittest.TestCase): when we request a user that is taken. """ # pristine registration - ok = self.register.register_user("foouser_firsttime", "barpass") - self.assertTrue(ok) - + d = threads.deferToThread(self.register.register_user, + "foouser_firsttime", + "barpass") + d.addCallback(self.assertTrue) + return d + + @deferred() + def test_second_register_user(self): # second registration attempt with the same user should return errors - ok = self.register.register_user("foouser_second", "barpass") - self.assertTrue(ok) + d = threads.deferToThread(self.register.register_user, + "foouser_second", + "barpass") + d.addCallback(self.assertTrue) # FIXME currently we are catching this in an upper layer, # we could bring the error validation to the SRPRegister class - ok = self.register.register_user("foouser_second", "barpass") - + def register_wrapper(_): + return threads.deferToThread(self.register.register_user, + "foouser_second", + "barpass") + d.addCallback(register_wrapper) + d.addCallback(self.assertFalse) + return d + + @deferred() def test_correct_http_uri(self): """ Checks that registration autocorrect http uris to https ones. @@ -151,57 +188,15 @@ class SRPTestCase(unittest.TestCase): raise ImproperlyConfiguredError( "Could not load test provider config") - self.register = srpregister.SRPRegister(provider_config=provider) + register = srpregister.SRPRegister(provider_config=provider) # ... and we check that we're correctly taking the HTTPS protocol # instead - self.assertEquals(self.register._get_registration_uri(), - HTTPS_URI) - ok = self.register.register_user("test_failhttp", "barpass") - self.assertTrue(ok) - - # XXX need to assert that _get_registration_uri was called too - - # Auth tests - - def test_auth(self): - """ - Checks whether a pair of valid credentials is able to be authenticated. - """ - TEST_USER = "register_test_auth" - TEST_PASS = "pass" - - # pristine registration, should go well - ok = self.register.register_user(TEST_USER, TEST_PASS) - self.assertTrue(ok) - - self.authenticate(TEST_USER, TEST_PASS) - with self.assertRaises(AssertionError): - # AssertionError: already logged in - # We probably could take this as its own exception - self.authenticate(TEST_USER, TEST_PASS) - - self.logout() - - # cannot log out two times in a row (there's no session) - with self.assertRaises(AssertionError): - self.logout() - - def test_auth_with_bad_credentials(self): - """ - Checks that auth does not succeed with bad credentials. - """ - TEST_USER = "register_test_auth" - TEST_PASS = "pass" - - # non-existent credentials, should fail - with self.assertRaises(srpauth.SRPAuthenticationError): - self.authenticate("baduser_1", "passwrong") - - # good user, bad password, should fail - with self.assertRaises(srpauth.SRPAuthenticationError): - self.authenticate(TEST_USER, "passwrong") - - # bad user, good password, should fail too :) - with self.assertRaises(srpauth.SRPAuthenticationError): - self.authenticate("myunclejoe", TEST_PASS) + reg_uri = register._get_registration_uri() + self.assertEquals(reg_uri, HTTPS_URI) + register._get_registration_uri = MagicMock(return_value=HTTPS_URI) + d = threads.deferToThread(register.register_user, "test_failhttp", + "barpass") + d.addCallback(self.assertTrue) + + return d diff --git a/src/leap/crypto/tests/wrongcert.pem b/src/leap/crypto/tests/wrongcert.pem new file mode 100644 index 00000000..e6cff38a --- /dev/null +++ b/src/leap/crypto/tests/wrongcert.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAIWZus5EIXNtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTMwNjI1MTc0NjExWhcNMTgwNjI1MTc0NjExWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEA2ObM7ESjyuxFZYD/Y68qOPQgjgggW+cdXfBpU2p4n7clsrUeMhWdW40Y +77Phzor9VOeqs3ZpHuyLzsYVp/kFDm8tKyo2ah5fJwzL0VCSLYaZkUQQ7GNUmTCk +furaxl8cQx/fg395V7/EngsS9B3/y5iHbctbA4MnH3jaotO5EGeo6hw7/eyCotQ9 +KbBV9GJMcY94FsXBCmUB+XypKklWTLhSaS6Cu4Fo8YLW6WmcnsyEOGS2F7WVf5at +7CBWFQZHaSgIBLmc818/mDYCnYmCVMFn/6Ndx7V2NTlz+HctWrQn0dmIOnCUeCwS +wXq9PnBR1rSx/WxwyF/WpyjOFkcIo7vm72kS70pfrYsXcZD4BQqkXYj3FyKnPt3O +ibLKtCxL8/83wOtErPcYpG6LgFkgAAlHQ9MkUi5dbmjCJtpqQmlZeK1RALdDPiB3 +K1KZimrGsmcE624dJxUIOJJpuwJDy21F8kh5ZAsAtE1prWETrQYNElNFjQxM83rS +ZR1Ql2MPSB4usEZT57+KvpEzlOnAT3elgCg21XrjSFGi14hCEao4g2OEZH5GAwm5 +frf6UlSRZ/g3tLTfI8Hv1prw15W2qO+7q7SBAplTODCRk+Yb0YoA2mMM/QXBUcXs +vKEDLSSxzNIBi3T62l39RB/ml+gPKo87ZMDivex1ZhrcJc3Yu3sCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUPjE+4pun+8FreIdpoR8v6N7xKtUwdQYDVR0jBG4wbIAUPjE+ +4pun+8FreIdpoR8v6N7xKtWhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCF +mbrORCFzbTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCpvCPdtvXJ +muTj379TZuCJs7/l0FhA7AHa1WAlHjsXHaA7N0+3ZWAbdtXDsowal6S+ldgU/kfV +Lq7NrRq+amJWC7SYj6cvVwhrSwSvu01fe/TWuOzHrRv1uTfJ/VXLonVufMDd9opo +bhqYxMaxLdIx6t/MYmZH4Wpiq0yfZuv//M8i7BBl/qvaWbLhg0yVAKRwjFvf59h6 +6tRFCLddELOIhLDQtk8zMbioPEbfAlKdwwP8kYGtDGj6/9/YTd/oTKRdgHuwyup3 +m0L20Y6LddC+tb0WpK5EyrNbCbEqj1L4/U7r6f/FKNA3bx6nfdXbscaMfYonKAKg +1cRrRg45sErmCz0QyTnWzXyvbjR4oQRzyW3kJ1JZudZ+AwOi00J5FYa3NiLuxl1u +gIGKWSrASQWhEdpa1nlCgX7PhdaQgYjEMpQvA0GCA0OF5JDu8en1yZqsOt1hCLIN +lkz/5jKPqrclY5hV99bE3hgCHRmIPNHCZG3wbZv2yJKxJX1YLMmQwAmSh2N7YwGG +yXRvCxQs5ChPHyRairuf/5MZCZnSVb45ppTVuNUijsbflKRUgfj/XvfqQ22f+C9N +Om2dmNvAiS2TOIfuP47CF2OUa5q4plUwmr+nyXQGM0SIoHNCj+MBdFfb3oxxAtI+ +SLhbnzQv5e84Doqz3YF0XW8jyR7q8GFLNA== +-----END CERTIFICATE----- -- cgit v1.2.3 From 0fe9f43baf0d9da887d595384f100146f27f2393 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 26 Jun 2013 16:29:39 -0300 Subject: Improve VPNGatewaySelector tests coverage. Add +13 and +14 timezones support. --- src/leap/services/eip/eipconfig.py | 12 ++++- .../services/eip/tests/test_vpngatewayselector.py | 63 ++++++++++++++++++---- 2 files changed, 65 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index 97eb3dfb..9e3a9b29 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -37,6 +37,8 @@ class VPNGatewaySelector(object): """ VPN Gateway selector. """ + # http://www.timeanddate.com/time/map/ + equivalent_timezones = {13: -11, 14: -10} def __init__(self, eipconfig, tz_offset=None): ''' @@ -51,7 +53,12 @@ class VPNGatewaySelector(object): self._local_offset = tz_offset if tz_offset is None: - self._local_offset = self._get_local_offset() + tz_offset = self._get_local_offset() + + if tz_offset in self.equivalent_timezones: + tz_offset = self.equivalent_timezones[tz_offset] + + self._local_offset = tz_offset self._eipconfig = eipconfig @@ -71,6 +78,9 @@ class VPNGatewaySelector(object): if gateway_location is not None: gw_offset = int(locations[gateway['location']]['timezone']) + if gw_offset in self.equivalent_timezones: + gw_offset = self.equivalent_timezones[gw_offset] + gateway_distance = self._get_timezone_distance(gw_offset) ip = self._eipconfig.get_gateway_ip(idx) diff --git a/src/leap/services/eip/tests/test_vpngatewayselector.py b/src/leap/services/eip/tests/test_vpngatewayselector.py index 250e6e00..c90681d7 100644 --- a/src/leap/services/eip/tests/test_vpngatewayselector.py +++ b/src/leap/services/eip/tests/test_vpngatewayselector.py @@ -34,15 +34,31 @@ sample_gateways = [ u'location': u'location2'}, {u'host': u'gateway3.com', u'ip_address': u'3.4.5.6', - u'location': u'location3'} + u'location': u'location3'}, + {u'host': u'gateway4.com', + u'ip_address': u'4.5.6.7', + u'location': u'location4'} +] + +sample_gateways_no_location = [ + {u'host': u'gateway1.com', + u'ip_address': u'1.2.3.4'}, + {u'host': u'gateway2.com', + u'ip_address': u'2.3.4.5'}, + {u'host': u'gateway3.com', + u'ip_address': u'3.4.5.6'} ] sample_locations = { u'location1': {u'timezone': u'2'}, u'location2': {u'timezone': u'-7'}, - u'location3': {u'timezone': u'-4'} + u'location3': {u'timezone': u'-4'}, + u'location4': {u'timezone': u'+13'} } +# 0 is not used, only for indexing from 1 in tests +ips = (0, u'1.2.3.4', u'2.3.4.5', u'3.4.5.6', u'4.5.6.7') + class VPNGatewaySelectorTest(BaseLeapTest): """ @@ -56,30 +72,59 @@ class VPNGatewaySelectorTest(BaseLeapTest): def tearDown(self): pass + def test_get_no_gateways(self): + gateway_selector = VPNGatewaySelector(self.eipconfig) + self.eipconfig.get_gateways = Mock(return_value=[]) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, []) + + def test_get_gateway_with_no_locations(self): + gateway_selector = VPNGatewaySelector(self.eipconfig) + self.eipconfig.get_gateways = Mock( + return_value=sample_gateways_no_location) + self.eipconfig.get_locations = Mock(return_value=[]) + gateways = gateway_selector.get_gateways() + gateways_default_order = [ + sample_gateways[0]['ip_address'], + sample_gateways[1]['ip_address'], + sample_gateways[2]['ip_address'] + ] + self.assertEqual(gateways, gateways_default_order) + def test_correct_order_gmt(self): gateway_selector = VPNGatewaySelector(self.eipconfig, 0) gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [u'1.2.3.4', u'3.4.5.6', u'2.3.4.5']) + self.assertEqual(gateways, [ips[1], ips[3], ips[2], ips[4]]) def test_correct_order_gmt_minus_3(self): gateway_selector = VPNGatewaySelector(self.eipconfig, -3) gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [u'3.4.5.6', u'2.3.4.5', u'1.2.3.4']) + self.assertEqual(gateways, [ips[3], ips[2], ips[1], ips[4]]) def test_correct_order_gmt_minus_7(self): gateway_selector = VPNGatewaySelector(self.eipconfig, -7) gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [u'2.3.4.5', u'3.4.5.6', u'1.2.3.4']) + self.assertEqual(gateways, [ips[2], ips[3], ips[4], ips[1]]) def test_correct_order_gmt_plus_5(self): gateway_selector = VPNGatewaySelector(self.eipconfig, 5) gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [u'1.2.3.4', u'3.4.5.6', u'2.3.4.5']) + self.assertEqual(gateways, [ips[1], ips[4], ips[3], ips[2]]) + + def test_correct_order_gmt_plus_12(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 12) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]]) + + def test_correct_order_gmt_minus_11(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, -11) + gateways = gateway_selector.get_gateways() + self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]]) - def test_correct_order_gmt_plus_10(self): - gateway_selector = VPNGatewaySelector(self.eipconfig, 10) + def test_correct_order_gmt_plus_14(self): + gateway_selector = VPNGatewaySelector(self.eipconfig, 14) gateways = gateway_selector.get_gateways() - self.assertEqual(gateways, [u'2.3.4.5', u'1.2.3.4', u'3.4.5.6']) + self.assertEqual(gateways, [ips[4], ips[2], ips[3], ips[1]]) if __name__ == "__main__": -- cgit v1.2.3 From d88f1e79fa5b6e3ec3ee7691e1c45680e85c6f12 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Tue, 25 Jun 2013 16:21:58 -0300 Subject: Refactor & add tests for eipconfig --- src/leap/services/eip/tests/test_eipconfig.py | 280 ++++++++++++++++++-------- 1 file changed, 199 insertions(+), 81 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/tests/test_eipconfig.py b/src/leap/services/eip/tests/test_eipconfig.py index 0bd19d5e..8b746b78 100644 --- a/src/leap/services/eip/tests/test_eipconfig.py +++ b/src/leap/services/eip/tests/test_eipconfig.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ -tests for eipconfig +Tests for eipconfig """ import copy import json @@ -24,6 +24,9 @@ import unittest from leap.common.testing.basetest import BaseLeapTest from leap.services.eip.eipconfig import EIPConfig +from leap.config.providerconfig import ProviderConfig + +from mock import Mock sample_config = { @@ -34,27 +37,50 @@ sample_config = { "filter_dns": True, "limited": True, "ports": [ - "1194", - "443", - "53", - "80" - ], - "protocols": [ - "tcp", - "udp"], - "transport": [ - "openvpn"], - "user_ips": False}, - "host": "host.dev.example.org", - "ip_address": "11.22.33.44", - "location": "cyberspace" - }], + "1194", + "443", + "53", + "80"], + "protocols": [ + "tcp", + "udp"], + "transport": ["openvpn"], + "user_ips": False}, + "host": "host.dev.example.org", + "ip_address": "11.22.33.44", + "location": "cyberspace" + }, { + "capabilities": { + "adblock": False, + "filter_dns": True, + "limited": True, + "ports": [ + "1194", + "443", + "53", + "80"], + "protocols": [ + "tcp", + "udp"], + "transport": ["openvpn"], + "user_ips": False}, + "host": "host2.dev.example.org", + "ip_address": "22.33.44.55", + "location": "cyberspace" + } + ], "locations": { "ankara": { - "country_code": "XX", - "hemisphere": "S", - "name": "Antarctica", - "timezone": "+2" + "country_code": "XX", + "hemisphere": "S", + "name": "Antarctica", + "timezone": "+2" + }, + "cyberspace": { + "country_code": "XX", + "hemisphere": "X", + "name": "outer space", + "timezone": "" } }, "openvpn_configuration": { @@ -70,126 +96,218 @@ sample_config = { class EIPConfigTest(BaseLeapTest): __name__ = "eip_config_tests" - #provider = "testprovider.example.org" maxDiff = None def setUp(self): - pass + self._old_ospath_exists = os.path.exists def tearDown(self): - pass + os.path.exists = self._old_ospath_exists - # - # helpers - # + def _write_config(self, data): + """ + Helper to write some data to a temp config file. - def write_config(self, data): - self.configfile = os.path.join( - self.tempdir, "eipconfig.json") + :param data: data to be used to save in the config file. + :data type: dict (valid json) + """ + self.configfile = os.path.join(self.tempdir, "eipconfig.json") conf = open(self.configfile, "w") conf.write(json.dumps(data)) conf.close() - def test_load_valid_config(self): + def _get_eipconfig(self, fromfile=True, data=sample_config): """ - load a sample config + Helper that returns an EIPConfig object using the data parameter + or a sample data. + + :param fromfile: sets if we should use a file or a string + :fromfile type: bool + :param data: sets the data to be used to load in the EIPConfig object + :data type: dict (valid json) + :rtype: EIPConfig """ - self.write_config(sample_config) config = EIPConfig() - #self.assertRaises( - #AssertionError, - #config.get_clusters) - self.assertTrue(config.load( - self.configfile, relative=False)) + loaded = False + if fromfile: + self._write_config(data) + loaded = config.load(self.configfile, relative=False) + else: + json_string = json.dumps(data) + loaded = config.load(data=json_string) + + if not loaded: + return None + + return config + + def test_loads_from_file(self): + config = self._get_eipconfig() + self.assertIsNotNone(config) + + def test_loads_from_data(self): + config = self._get_eipconfig(fromfile=False) + self.assertIsNotNone(config) + + def test_load_valid_config_from_file(self): + config = self._get_eipconfig() + self.assertIsNotNone(config) + self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) + + sample_ip = sample_config["gateways"][0]["ip_address"] self.assertEqual( config.get_gateway_ip(), - "11.22.33.44") - self.assertEqual(config.get_version(), 1) - self.assertEqual(config.get_serial(), 1) - self.assertEqual(config.get_gateways(), - sample_config["gateways"]) + sample_ip) + self.assertEqual(config.get_version(), sample_config["version"]) + self.assertEqual(config.get_serial(), sample_config["serial"]) + self.assertEqual(config.get_gateways(), sample_config["gateways"]) + self.assertEqual(config.get_locations(), sample_config["locations"]) + self.assertEqual(config.get_clusters(), None) + + def test_load_valid_config_from_data(self): + config = self._get_eipconfig(fromfile=False) + self.assertIsNotNone(config) + self.assertEqual( - config.get_clusters(), None) + config.get_openvpn_configuration(), + sample_config["openvpn_configuration"]) - def test_sanitize_config(self): - """ - check the sanitization of options - """ - # extra parameters + sample_ip = sample_config["gateways"][0]["ip_address"] + self.assertEqual( + config.get_gateway_ip(), + sample_ip) + + self.assertEqual(config.get_version(), sample_config["version"]) + self.assertEqual(config.get_serial(), sample_config["serial"]) + self.assertEqual(config.get_gateways(), sample_config["gateways"]) + self.assertEqual(config.get_locations(), sample_config["locations"]) + self.assertEqual(config.get_clusters(), None) + + def test_sanitize_extra_parameters(self): data = copy.deepcopy(sample_config) data['openvpn_configuration']["extra_param"] = "FOO" - self.write_config(data) - config = EIPConfig() - config.load( - self.configfile, relative=False) + config = self._get_eipconfig(data=data) + self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) - # non allowed chars + def test_sanitize_non_allowed_chars(self): data = copy.deepcopy(sample_config) data['openvpn_configuration']["auth"] = "SHA1;" - self.write_config(data) - config = EIPConfig() - config.load(self.configfile, relative=False) + config = self._get_eipconfig(data=data) + self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) - # non allowed chars data = copy.deepcopy(sample_config) data['openvpn_configuration']["auth"] = "SHA1>`&|" - self.write_config(data) - config = EIPConfig() - config.load(self.configfile, relative=False) + config = self._get_eipconfig(data=data) + self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) - # lowercase + def test_sanitize_lowercase(self): data = copy.deepcopy(sample_config) data['openvpn_configuration']["auth"] = "shaSHA1" - self.write_config(data) - config = EIPConfig() - config.load(self.configfile, relative=False) + config = self._get_eipconfig(data=data) + self.assertEqual( config.get_openvpn_configuration(), sample_config["openvpn_configuration"]) - # all characters invalid -> null value + def test_all_characters_invalid(self): data = copy.deepcopy(sample_config) data['openvpn_configuration']["auth"] = "sha&*!@#;" - self.write_config(data) - config = EIPConfig() - config.load(self.configfile, relative=False) + config = self._get_eipconfig(data=data) + self.assertEqual( config.get_openvpn_configuration(), {'cipher': 'AES-128-CBC', 'tls-cipher': 'DHE-RSA-AES128-SHA'}) - # bad_ip + def test_sanitize_bad_ip(self): data = copy.deepcopy(sample_config) data['gateways'][0]["ip_address"] = "11.22.33.44;" - self.write_config(data) - config = EIPConfig() - config.load(self.configfile, relative=False) - self.assertEqual( - config.get_gateway_ip(), - None) + config = self._get_eipconfig(data=data) + + self.assertEqual(config.get_gateway_ip(), None) data = copy.deepcopy(sample_config) data['gateways'][0]["ip_address"] = "11.22.33.44`" - self.write_config(data) - config = EIPConfig() - config.load(self.configfile, relative=False) - self.assertEqual( - config.get_gateway_ip(), - None) + config = self._get_eipconfig(data=data) + + self.assertEqual(config.get_gateway_ip(), None) + + def test_default_gateway_on_unknown_index(self): + config = self._get_eipconfig() + sample_ip = sample_config["gateways"][0]["ip_address"] + self.assertEqual(config.get_gateway_ip(999), sample_ip) + + def test_get_gateway_by_index(self): + config = self._get_eipconfig() + sample_ip_0 = sample_config["gateways"][0]["ip_address"] + sample_ip_1 = sample_config["gateways"][1]["ip_address"] + self.assertEqual(config.get_gateway_ip(0), sample_ip_0) + self.assertEqual(config.get_gateway_ip(1), sample_ip_1) + + def test_get_client_cert_path_as_expected(self): + config = self._get_eipconfig() + config.get_path_prefix = Mock(return_value='test') + + provider_config = ProviderConfig() + + # mock 'get_domain' so we don't need to load a config + provider_domain = 'test.provider.com' + provider_config.get_domain = Mock(return_value=provider_domain) + + expected_path = os.path.join('test', 'leap', 'providers', + provider_domain, 'keys', 'client', + 'openvpn.pem') + + # mock 'os.path.exists' so we don't get an error for unexisting file + os.path.exists = Mock(return_value=True) + cert_path = config.get_client_cert_path(provider_config) + + self.assertEqual(cert_path, expected_path) + + def test_get_client_cert_path_about_to_download(self): + config = self._get_eipconfig() + config.get_path_prefix = Mock(return_value='test') + + provider_config = ProviderConfig() + + # mock 'get_domain' so we don't need to load a config + provider_domain = 'test.provider.com' + provider_config.get_domain = Mock(return_value=provider_domain) + + expected_path = os.path.join('test', 'leap', 'providers', + provider_domain, 'keys', 'client', + 'openvpn.pem') + + cert_path = config.get_client_cert_path( + provider_config, about_to_download=True) + + self.assertEqual(cert_path, expected_path) + + def test_get_client_cert_path_fails(self): + config = self._get_eipconfig() + provider_config = ProviderConfig() + + # mock 'get_domain' so we don't need to load a config + provider_domain = 'test.provider.com' + provider_config.get_domain = Mock(return_value=provider_domain) + + with self.assertRaises(AssertionError): + config.get_client_cert_path(provider_config) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From 503ec0d1b02e802191981041ead4b823c858bd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 26 Jun 2013 21:28:25 -0300 Subject: Improve fake_provider to support the modified-if-needed feature --- src/leap/crypto/tests/fake_provider.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/crypto/tests/fake_provider.py b/src/leap/crypto/tests/fake_provider.py index b943040a..f86d5ca9 100755 --- a/src/leap/crypto/tests/fake_provider.py +++ b/src/leap/crypto/tests/fake_provider.py @@ -31,6 +31,7 @@ import binascii import json import os import sys +import time import srp @@ -39,7 +40,7 @@ from OpenSSL import SSL from zope.interface import Interface, Attribute, implements from twisted.web.server import Site, Request -from twisted.web.static import File +from twisted.web.static import File, Data from twisted.web.resource import Resource from twisted.internet import reactor @@ -300,6 +301,22 @@ class API_Sessions(Resource): return FakeSession(name) +class FileModified(File): + def render_GET(self, request): + since = request.getHeader('if-modified-since') + if since: + tsince = time.strptime(since.replace(" GMT", "")) + tfrom = time.strptime(time.ctime(os.path.getmtime( + os.path.join(_here, + "test_provider.json")))) + if tfrom > tsince: + return File.render_GET(self, request) + else: + request.setResponseCode(304) + return "" + return File.render_GET(self, request) + + class OpenSSLServerContextFactory(object): def getContext(self): @@ -325,8 +342,9 @@ def get_provider_factory(): :rparam: factory for a site :rtype: Site instance """ - root = Resource() - root.putChild("provider.json", File( + root = Data("", "") + root.putChild("", root) + root.putChild("provider.json", FileModified( os.path.join(_here, "test_provider.json"))) config = Resource() -- cgit v1.2.3 From 4000450fc563c7d5425cdf8531cfd5f716960036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 26 Jun 2013 21:29:21 -0300 Subject: Fix problem with an SRPRegister test --- src/leap/crypto/tests/test_srpregister.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/crypto/tests/test_srpregister.py b/src/leap/crypto/tests/test_srpregister.py index f70382ce..6d2b52e8 100644 --- a/src/leap/crypto/tests/test_srpregister.py +++ b/src/leap/crypto/tests/test_srpregister.py @@ -123,7 +123,7 @@ class SRPTestCase(unittest.TestCase): provider.get_ca_cert_path = MagicMock() provider.get_ca_cert_path.return_value = os.path.join( _here, - "wrongcacert.pem") + "wrongcert.pem") provider.get_api_uri = MagicMock() provider.get_api_uri.return_value = self._get_https_uri() if not loaded: -- cgit v1.2.3 From 6b4954c88e8106de355eb6a5889fc487dd816173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 27 Jun 2013 09:57:03 -0300 Subject: Add missing openvpn.pem certificate for tests --- src/leap/crypto/tests/openvpn.pem | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/leap/crypto/tests/openvpn.pem (limited to 'src') diff --git a/src/leap/crypto/tests/openvpn.pem b/src/leap/crypto/tests/openvpn.pem new file mode 100644 index 00000000..a95e9370 --- /dev/null +++ b/src/leap/crypto/tests/openvpn.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAIGJ8Dg+DtemMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTMwNjI2MjAyMDIyWhcNMTgwNjI2MjAyMDIyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAxJaN0lWjFu+3j48c0WG8BvmPUf026Xli5d5NE4EjGsirwfre0oTeWZT9 +WRxqLGd2wDh6Mc9r6UqH6dwqLZKbsgwB5zI2lag7UWFttJF1U1c6AJynhaLMoy73 +sL9USTmQ57iYRFrVP/nGj9/L6I1XnV6midPi7a5aZreH9q8dWaAhmc9eFDU+Y4vS +sTFS6aomajLrI6YWo5toKqLq8IMryD03IM78a7gJtLgfWs+pYZRUBlM5JaYX98eX +mVPAYYH9krWxLVN3hTt1ngECzK+epo275zQJh960/2fNCfVJSXqSXcficLs+bR7t +FEkNuOP1hFV6LuoLL+k5Su+hp5kXMYZTvYYDpW4nPJoBdSG1w5O5IxO6zh+9VLB7 +oLrlgoyWvBoou5coCBpZVU6UyWcOx58kuZF8wNr0GgdvWAFwOGVuVG5jmcVdhaKC +0C8NxHrxlhcrcp0zwtDaOxfmZfcxiXs35iwUip5vS18Nv+XBK8ad9T79Ox8nSzP3 +RGPVDpExz7gPbZglqSe47XBIk0ZuIzgOgYpJj4JrpoewoIYb+OmUgI7UZjoGsMrV ++B2BqOKs7kF0HW3i5bR9YAi0ZYvnhQgjBtwCKm4zvLqwuPZHz9VWgIk6uezgStCP +WyzQ8IcopK49fOjcKa6JT5JRU+27paIZf1BkQsTkJy/Nti4TvwMCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUEgXSd3Yl3xAzbkWa7xeNe27d99cwdQYDVR0jBG4wbIAUEgXS +d3Yl3xAzbkWa7xeNe27d99ehSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCB +ifA4Pg7XpjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQA6Vl9Ve4Qe +ewzXAxr0BabFRhtIuF7DV+/niT46qJhW2KgYe6rwZqdAhEbgH3kTPJ5JmmcUnAEH +nmrfoku/YAb5ObfdHUACsHy4cvSvFwBUQ9vXP6+oOFJhrGW4uzRI2pHGvnqB3lQ0 +JEPmPwduBCI5reRYauPbd4Wl4VhLGrjELb4JQZL24Q5ehXMnv415m7+aMkLzT2IA +p6B2xgRR+JAeUdyCNOV1f5AqJWyAUJPWGR0e1OTKNfc49+2skK0NmzrpGsoktSHa +uN6vGBCVGiZh7BTYblWMG5q9Am7idcdmC2fdpIf5yj7CKzV7WIPxPs0I7TuRcr41 +pUBLCAElcyCPB89lySol2BDs4gk4wZs4y2shUs3o0+mIpw/6o8tQF/9IL8ALkLqr +q9SuND7O1RXcg74o3HeVmRKtoI/KdgaVhJ0rFvcq83ftfu3KMyWB6SOKOu6ZYON8 +AcSjsDDpnDrwGFvjAYHiTkS9NaaJC1/g7Y6jjhxmbTkXPA6V8MvLKQiOvqk/9gCh +85FHsFkElIYnH6fbHIRxg20cnqmddTd+H5HgBIlhiKWuydtuoQFwzR/D3ypgLBaB +OWLcBP7I+RYhKlJFIWnfiyB0xbyI4W/UfL8p8jQI8TE9oIlm3WqxJXfebDEDEstj +8nS4Fb3G5Wr4pZMjfbtmBSAgHeWH6B90jg== +-----END CERTIFICATE----- -- cgit v1.2.3 From b1429c296ee852d941a0c88e976631d03140ddee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 27 Jun 2013 09:59:57 -0300 Subject: Fix minor bugs and return defer in the run_* methods --- src/leap/services/eip/providerbootstrapper.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/providerbootstrapper.py b/src/leap/services/eip/providerbootstrapper.py index e099eee7..754d0643 100644 --- a/src/leap/services/eip/providerbootstrapper.py +++ b/src/leap/services/eip/providerbootstrapper.py @@ -155,7 +155,7 @@ class ProviderBootstrapper(AbstractBootstrapper): if SupportedAPIs.supports(api_version): logger.debug("Provider definition has been modified") else: - api_supported = ', '.join(self._supported_api_versions) + api_supported = ', '.join(SupportedAPIs.SUPPORTED_APIS) error = ('Unsupported provider API version. ' 'Supported versions are: {}. ' 'Found: {}.').format(api_supported, api_version) @@ -185,7 +185,7 @@ class ProviderBootstrapper(AbstractBootstrapper): (self._download_provider_info, self.download_provider_info) ] - self.addCallbackChain(cb_chain) + return self.addCallbackChain(cb_chain) def _should_proceed_cert(self): """ @@ -217,6 +217,7 @@ class ProviderBootstrapper(AbstractBootstrapper): check_and_fix_urw_only( self._provider_config .get_ca_cert_path(about_to_download=True)) + return res = self._session.get(self._provider_config.get_ca_cert_uri(), verify=not self._bypass_checks) @@ -307,4 +308,4 @@ class ProviderBootstrapper(AbstractBootstrapper): (self._check_api_certificate, self.check_api_certificate) ] - self.addCallbackChain(cb_chain) + return self.addCallbackChain(cb_chain) -- cgit v1.2.3 From c66b1f7287660d349f189b7b316144e4cd72bd72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 27 Jun 2013 10:00:27 -0300 Subject: Add ProviderBootstrapper tests --- .../eip/tests/test_providerbootstrapper.py | 504 +++++++++++++++++++++ src/leap/services/eip/tests/wrongcert.pem | 33 ++ 2 files changed, 537 insertions(+) create mode 100644 src/leap/services/eip/tests/test_providerbootstrapper.py create mode 100644 src/leap/services/eip/tests/wrongcert.pem (limited to 'src') diff --git a/src/leap/services/eip/tests/test_providerbootstrapper.py b/src/leap/services/eip/tests/test_providerbootstrapper.py new file mode 100644 index 00000000..cd740793 --- /dev/null +++ b/src/leap/services/eip/tests/test_providerbootstrapper.py @@ -0,0 +1,504 @@ +# -*- coding: utf-8 -*- +# test_providerbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +Tests for the Provider Boostrapper checks + +These will be whitebox tests since we want to make sure the private +implementation is checking what we expect. +""" + +import os +import mock +import socket +import stat +import tempfile +import time +import requests +try: + import unittest2 as unittest +except ImportError: + import unittest + +from nose.twistedtools import deferred, reactor +from twisted.internet import threads +from requests.models import Response + +from leap.common.testing.https_server import where +from leap.common.testing.basetest import BaseLeapTest +from leap.services.eip.providerbootstrapper import ProviderBootstrapper +from leap.services.eip.providerbootstrapper import UnsupportedProviderAPI +from leap.provider.supportedapis import SupportedAPIs +from leap.config.providerconfig import ProviderConfig +from leap.crypto.tests import fake_provider +from leap.common.files import mkdir_p + + +class ProviderBootstrapperTest(BaseLeapTest): + def setUp(self): + self.pb = ProviderBootstrapper() + + def tearDown(self): + pass + + def test_name_resolution_check(self): + # Something highly likely to success + self.pb._domain = "google.com" + self.pb._check_name_resolution() + # Something highly likely to fail + self.pb._domain = "uquhqweuihowquie.abc.def" + with self.assertRaises(socket.gaierror): + self.pb._check_name_resolution() + + @deferred() + def test_run_provider_select_checks(self): + self.pb._check_name_resolution = mock.MagicMock() + self.pb._check_https = mock.MagicMock() + self.pb._download_provider_info = mock.MagicMock() + + d = self.pb.run_provider_select_checks("somedomain") + + def check(*args): + self.pb._check_name_resolution.assert_called_once_with() + self.pb._check_https.assert_called_once_with(None) + self.pb._download_provider_info.assert_called_once_with(None) + d.addCallback(check) + return d + + @deferred() + def test_run_provider_setup_checks(self): + self.pb._download_ca_cert = mock.MagicMock() + self.pb._check_ca_fingerprint = mock.MagicMock() + self.pb._check_api_certificate = mock.MagicMock() + + d = self.pb.run_provider_setup_checks(ProviderConfig()) + + def check(*args): + self.pb._download_ca_cert.assert_called_once_with() + self.pb._check_ca_fingerprint.assert_called_once_with(None) + self.pb._check_api_certificate.assert_called_once_with(None) + d.addCallback(check) + return d + + def test_should_proceed_cert(self): + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=where("cacert.pem")) + + self.pb._download_if_needed = False + self.assertTrue(self.pb._should_proceed_cert()) + + self.pb._download_if_needed = True + self.assertFalse(self.pb._should_proceed_cert()) + + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=where("somefilethatdoesntexist.pem")) + self.assertTrue(self.pb._should_proceed_cert()) + + def _check_download_ca_cert(self, should_proceed): + """ + Helper to check different paths easily for the download ca + cert check + + :param should_proceed: sets the _should_proceed_cert in the + provider bootstrapper being tested + :type should_proceed: bool + + :returns: The contents of the certificate, the expected + content depending on should_proceed, and the mode of + the file to be checked by the caller + :rtype: tuple of str, str, int + """ + old_content = "NOT THE NEW CERT" + new_content = "NEW CERT" + new_cert_path = os.path.join(tempfile.mkdtemp(), + "mynewcert.pem") + + with open(new_cert_path, "w") as c: + c.write(old_content) + + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=new_cert_path) + self.pb._domain = "somedomain" + + self.pb._should_proceed_cert = mock.MagicMock( + return_value=should_proceed) + + read = None + content_to_check = None + mode = None + + with mock.patch('requests.models.Response.content', + new_callable=mock.PropertyMock) as \ + content: + content.return_value = new_content + response_obj = Response() + response_obj.raise_for_status = mock.MagicMock() + + self.pb._session.get = mock.MagicMock(return_value=response_obj) + self.pb._download_ca_cert() + with open(new_cert_path, "r") as nc: + read = nc.read() + if should_proceed: + content_to_check = new_content + else: + content_to_check = old_content + mode = stat.S_IMODE(os.stat(new_cert_path).st_mode) + + os.unlink(new_cert_path) + return read, content_to_check, mode + + def test_download_ca_cert_no_saving(self): + read, expected_read, mode = self._check_download_ca_cert(False) + self.assertEqual(read, expected_read) + self.assertEqual(mode, int("600", 8)) + + def test_download_ca_cert_saving(self): + read, expected_read, mode = self._check_download_ca_cert(True) + self.assertEqual(read, expected_read) + self.assertEqual(mode, int("600", 8)) + + def test_check_ca_fingerprint_skips(self): + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( + return_value="") + self.pb._domain = "somedomain" + + self.pb._should_proceed_cert = mock.MagicMock(return_value=False) + + self.pb._check_ca_fingerprint() + self.assertFalse(self.pb._provider_config. + get_ca_cert_fingerprint.called) + + def test_check_ca_cert_fingerprint_raises_bad_format(self): + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( + return_value="wrongfprformat!!") + self.pb._domain = "somedomain" + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + with self.assertRaises(AssertionError): + self.pb._check_ca_fingerprint() + + # This two hashes different in the last byte, but that's good enough + # for the tests + KNOWN_BAD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034efe" \ + "7dd1b910062ca323eb4da5c7f" + KNOWN_GOOD_HASH = "SHA256: 0f17c033115f6b76ff67871872303ff65034ef" \ + "e7dd1b910062ca323eb4da5c7e" + KNOWN_GOOD_CERT = """ +-----BEGIN CERTIFICATE----- +MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt +YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v +Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw +FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV +BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai +dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB +7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84 +CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+ +znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4 +MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4 +lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0 +bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl +DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB +lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy +YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw +XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE +MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w +DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl +cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY +k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj +RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG +htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX +EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J +aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l +mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK +G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co +Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d +69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e +yV8e +-----END CERTIFICATE----- +""" + + def _prepare_provider_config_with(self, cert_path, cert_hash): + """ + Mocks the provider config to give the cert_path and cert_hash + specified + + :param cert_path: path for the certificate + :type cert_path: str + :param cert_hash: hash for the certificate as it would appear + in the provider config json + :type cert_hash: str + """ + self.pb._provider_config = mock.Mock() + self.pb._provider_config.get_ca_cert_fingerprint = mock.MagicMock( + return_value=cert_hash) + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=cert_path) + self.pb._domain = "somedomain" + + def test_check_ca_fingerprint_checksout(self): + cert_path = os.path.join(tempfile.mkdtemp(), + "mynewcert.pem") + + with open(cert_path, "w") as c: + c.write(self.KNOWN_GOOD_CERT) + + self._prepare_provider_config_with(cert_path, self.KNOWN_GOOD_HASH) + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + self.pb._check_ca_fingerprint() + + os.unlink(cert_path) + + def test_check_ca_fingerprint_fails(self): + cert_path = os.path.join(tempfile.mkdtemp(), + "mynewcert.pem") + + with open(cert_path, "w") as c: + c.write(self.KNOWN_GOOD_CERT) + + self._prepare_provider_config_with(cert_path, self.KNOWN_BAD_HASH) + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + with self.assertRaises(AssertionError): + self.pb._check_ca_fingerprint() + + os.unlink(cert_path) + + +############################################################################### +# Tests with a fake provider # +############################################################################### + +class ProviderBootstrapperActiveTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + factory = fake_provider.get_provider_factory() + http = reactor.listenTCP(8002, factory) + https = reactor.listenSSL( + 0, factory, + fake_provider.OpenSSLServerContextFactory()) + get_port = lambda p: p.getHost().port + cls.http_port = get_port(http) + cls.https_port = get_port(https) + + def setUp(self): + self.pb = ProviderBootstrapper() + + # At certain points we are going to be replacing these methods + # directly in ProviderConfig to be able to catch calls from + # new ProviderConfig objects inside the methods tested. We + # need to save the old implementation and restore it in + # tearDown so we are sure everything is as expected for each + # test. If we do it inside each specific test, a failure in + # the test will leave the implementation with the mock. + self.old_gpp = ProviderConfig.get_path_prefix + self.old_load = ProviderConfig.load + self.old_save = ProviderConfig.save + self.old_api_version = ProviderConfig.get_api_version + + def tearDown(self): + ProviderConfig.get_path_prefix = self.old_gpp + ProviderConfig.load = self.old_load + ProviderConfig.save = self.old_save + ProviderConfig.get_api_version = self.old_api_version + + def test_check_https_succeeds(self): + # XXX: Need a proper CA signed cert to test this + pass + + @deferred() + def test_check_https_fails(self): + self.pb._domain = "localhost:%s" % (self.https_port,) + + def check(*args): + with self.assertRaises(requests.exceptions.SSLError): + self.pb._check_https() + return threads.deferToThread(check) + + @deferred() + def test_second_check_https_fails(self): + self.pb._domain = "localhost:1234" + + def check(*args): + with self.assertRaises(Exception): + self.pb._check_https() + return threads.deferToThread(check) + + @deferred() + def test_check_https_succeeds_if_danger(self): + self.pb._domain = "localhost:%s" % (self.https_port,) + self.pb._bypass_checks = True + + def check(*args): + self.pb._check_https() + + return threads.deferToThread(check) + + def _setup_provider_config_with(self, api, path_prefix): + """ + Sets up the ProviderConfig with mocks for the path prefix, the + api returned and load/save methods. + It modifies ProviderConfig directly instead of an object + because the object used is created in the method itself and we + cannot control that. + + :param api: API to return + :type api: str + :param path_prefix: path prefix to be used when calculating + paths + :type path_prefix: str + """ + ProviderConfig.get_path_prefix = mock.MagicMock( + return_value=path_prefix) + ProviderConfig.get_api_version = mock.MagicMock( + return_value=api) + ProviderConfig.load = mock.MagicMock() + ProviderConfig.save = mock.MagicMock() + + def _setup_providerbootstrapper(self, ifneeded): + """ + Sets the provider bootstrapper's domain to + localhost:https_port, sets it to bypass https checks and sets + the download if needed based on the ifneeded value. + + :param ifneeded: Value for _download_if_needed + :type ifneeded: bool + """ + self.pb._domain = "localhost:%s" % (self.https_port,) + self.pb._bypass_checks = True + self.pb._download_if_needed = ifneeded + + def _produce_dummy_provider_json(self): + """ + Creates a dummy provider json on disk in order to test + behaviour around it (download if newer online, etc) + + :returns: the provider.json path used + :rtype: str + """ + provider_dir = os.path.join(ProviderConfig() + .get_path_prefix(), + "leap", + "providers", + self.pb._domain) + mkdir_p(provider_dir) + provider_path = os.path.join(provider_dir, + "provider.json") + + with open(provider_path, "w") as p: + p.write("A") + return provider_path + + def test_download_provider_info_not_modified(self): + self._setup_provider_config_with("1", tempfile.mkdtemp()) + self._setup_providerbootstrapper(True) + provider_path = self._produce_dummy_provider_json() + + # set mtime to something really new + os.utime(provider_path, (-1, time.time())) + + self.pb._download_provider_info() + # we check that it doesn't do anything with the provider + # config, because it's new enough + self.assertFalse(ProviderConfig.load.called) + self.assertFalse(ProviderConfig.save.called) + + def test_download_provider_info_modified(self): + self._setup_provider_config_with("1", tempfile.mkdtemp()) + self._setup_providerbootstrapper(True) + provider_path = self._produce_dummy_provider_json() + + # set mtime to something really old + os.utime(provider_path, (-1, 100)) + + self.pb._download_provider_info() + self.assertTrue(ProviderConfig.load.called) + self.assertTrue(ProviderConfig.save.called) + + def test_download_provider_info_unsupported_api_raises(self): + self._setup_provider_config_with("9999999", tempfile.mkdtemp()) + self._setup_providerbootstrapper(False) + self._produce_dummy_provider_json() + + with self.assertRaises(UnsupportedProviderAPI): + self.pb._download_provider_info() + + def test_download_provider_info_unsupported_api(self): + self._setup_provider_config_with(SupportedAPIs.SUPPORTED_APIS[0], + tempfile.mkdtemp()) + self._setup_providerbootstrapper(False) + self._produce_dummy_provider_json() + + self.pb._download_provider_info() + + def test_check_api_certificate_skips(self): + self.pb._provider_config = ProviderConfig() + self.pb._provider_config.get_api_uri = mock.MagicMock( + return_value="api.uri") + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value="/cert/path") + self.pb._session.get = mock.MagicMock(return_value=Response()) + + self.pb._should_proceed_cert = mock.MagicMock(return_value=False) + self.pb._check_api_certificate() + self.assertFalse(self.pb._session.get.called) + + @deferred() + def test_check_api_certificate_fails(self): + self.pb._provider_config = ProviderConfig() + self.pb._provider_config.get_api_uri = mock.MagicMock( + return_value="https://localhost:%s" % (self.https_port,)) + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=os.path.join( + os.path.split(__file__)[0], + "wrongcert.pem")) + self.pb._provider_config.get_api_version = mock.MagicMock( + return_value="1") + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + def check(*args): + with self.assertRaises(requests.exceptions.SSLError): + self.pb._check_api_certificate() + d = threads.deferToThread(check) + return d + + @deferred() + def test_check_api_certificate_succeeds(self): + self.pb._provider_config = ProviderConfig() + self.pb._provider_config.get_api_uri = mock.MagicMock( + return_value="https://localhost:%s" % (self.https_port,)) + self.pb._provider_config.get_ca_cert_path = mock.MagicMock( + return_value=where('cacert.pem')) + self.pb._provider_config.get_api_version = mock.MagicMock( + return_value="1") + + self.pb._should_proceed_cert = mock.MagicMock(return_value=True) + + def check(*args): + self.pb._check_api_certificate() + d = threads.deferToThread(check) + return d diff --git a/src/leap/services/eip/tests/wrongcert.pem b/src/leap/services/eip/tests/wrongcert.pem new file mode 100644 index 00000000..e6cff38a --- /dev/null +++ b/src/leap/services/eip/tests/wrongcert.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAIWZus5EIXNtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTMwNjI1MTc0NjExWhcNMTgwNjI1MTc0NjExWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEA2ObM7ESjyuxFZYD/Y68qOPQgjgggW+cdXfBpU2p4n7clsrUeMhWdW40Y +77Phzor9VOeqs3ZpHuyLzsYVp/kFDm8tKyo2ah5fJwzL0VCSLYaZkUQQ7GNUmTCk +furaxl8cQx/fg395V7/EngsS9B3/y5iHbctbA4MnH3jaotO5EGeo6hw7/eyCotQ9 +KbBV9GJMcY94FsXBCmUB+XypKklWTLhSaS6Cu4Fo8YLW6WmcnsyEOGS2F7WVf5at +7CBWFQZHaSgIBLmc818/mDYCnYmCVMFn/6Ndx7V2NTlz+HctWrQn0dmIOnCUeCwS +wXq9PnBR1rSx/WxwyF/WpyjOFkcIo7vm72kS70pfrYsXcZD4BQqkXYj3FyKnPt3O +ibLKtCxL8/83wOtErPcYpG6LgFkgAAlHQ9MkUi5dbmjCJtpqQmlZeK1RALdDPiB3 +K1KZimrGsmcE624dJxUIOJJpuwJDy21F8kh5ZAsAtE1prWETrQYNElNFjQxM83rS +ZR1Ql2MPSB4usEZT57+KvpEzlOnAT3elgCg21XrjSFGi14hCEao4g2OEZH5GAwm5 +frf6UlSRZ/g3tLTfI8Hv1prw15W2qO+7q7SBAplTODCRk+Yb0YoA2mMM/QXBUcXs +vKEDLSSxzNIBi3T62l39RB/ml+gPKo87ZMDivex1ZhrcJc3Yu3sCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUPjE+4pun+8FreIdpoR8v6N7xKtUwdQYDVR0jBG4wbIAUPjE+ +4pun+8FreIdpoR8v6N7xKtWhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCF +mbrORCFzbTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCpvCPdtvXJ +muTj379TZuCJs7/l0FhA7AHa1WAlHjsXHaA7N0+3ZWAbdtXDsowal6S+ldgU/kfV +Lq7NrRq+amJWC7SYj6cvVwhrSwSvu01fe/TWuOzHrRv1uTfJ/VXLonVufMDd9opo +bhqYxMaxLdIx6t/MYmZH4Wpiq0yfZuv//M8i7BBl/qvaWbLhg0yVAKRwjFvf59h6 +6tRFCLddELOIhLDQtk8zMbioPEbfAlKdwwP8kYGtDGj6/9/YTd/oTKRdgHuwyup3 +m0L20Y6LddC+tb0WpK5EyrNbCbEqj1L4/U7r6f/FKNA3bx6nfdXbscaMfYonKAKg +1cRrRg45sErmCz0QyTnWzXyvbjR4oQRzyW3kJ1JZudZ+AwOi00J5FYa3NiLuxl1u +gIGKWSrASQWhEdpa1nlCgX7PhdaQgYjEMpQvA0GCA0OF5JDu8en1yZqsOt1hCLIN +lkz/5jKPqrclY5hV99bE3hgCHRmIPNHCZG3wbZv2yJKxJX1YLMmQwAmSh2N7YwGG +yXRvCxQs5ChPHyRairuf/5MZCZnSVb45ppTVuNUijsbflKRUgfj/XvfqQ22f+C9N +Om2dmNvAiS2TOIfuP47CF2OUa5q4plUwmr+nyXQGM0SIoHNCj+MBdFfb3oxxAtI+ +SLhbnzQv5e84Doqz3YF0XW8jyR7q8GFLNA== +-----END CERTIFICATE----- -- cgit v1.2.3 From 20875f7a7abcd4b2403add47b5565f1098bb342a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 27 Jun 2013 17:50:32 -0300 Subject: Improve fake provider implementation --- src/leap/crypto/tests/fake_provider.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/leap/crypto/tests/fake_provider.py b/src/leap/crypto/tests/fake_provider.py index f86d5ca9..54af485d 100755 --- a/src/leap/crypto/tests/fake_provider.py +++ b/src/leap/crypto/tests/fake_provider.py @@ -306,9 +306,7 @@ class FileModified(File): since = request.getHeader('if-modified-since') if since: tsince = time.strptime(since.replace(" GMT", "")) - tfrom = time.strptime(time.ctime(os.path.getmtime( - os.path.join(_here, - "test_provider.json")))) + tfrom = time.strptime(time.ctime(os.path.getmtime(self.path))) if tfrom > tsince: return File.render_GET(self, request) else: @@ -350,12 +348,13 @@ def get_provider_factory(): config = Resource() config.putChild( "eip-service.json", - File("./eip-service.json")) + FileModified( + os.path.join(_here, "eip-service.json"))) apiv1 = Resource() apiv1.putChild("config", config) apiv1.putChild("sessions", API_Sessions()) apiv1.putChild("users", FakeUsers(None)) - apiv1.putChild("cert", File( + apiv1.putChild("cert", FileModified( os.path.join(_here, 'openvpn.pem'))) root.putChild("1", apiv1) -- cgit v1.2.3 From 0c836c3e474b88f39ce88ad9fadb0a13ee75189a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 27 Jun 2013 17:51:05 -0300 Subject: Add missing eip-service.json sample file for the fake provider --- src/leap/crypto/tests/eip-service.json | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/leap/crypto/tests/eip-service.json (limited to 'src') diff --git a/src/leap/crypto/tests/eip-service.json b/src/leap/crypto/tests/eip-service.json new file mode 100644 index 00000000..24df42a2 --- /dev/null +++ b/src/leap/crypto/tests/eip-service.json @@ -0,0 +1,43 @@ +{ + "gateways": [ + { + "capabilities": { + "adblock": false, + "filter_dns": false, + "limited": true, + "ports": [ + "1194", + "443", + "53", + "80" + ], + "protocols": [ + "tcp", + "udp" + ], + "transport": [ + "openvpn" + ], + "user_ips": false + }, + "host": "harrier.cdev.bitmask.net", + "ip_address": "199.254.238.50", + "location": "seattle__wa" + } + ], + "locations": { + "seattle__wa": { + "country_code": "US", + "hemisphere": "N", + "name": "Seattle, WA", + "timezone": "-7" + } + }, + "openvpn_configuration": { + "auth": "SHA1", + "cipher": "AES-128-CBC", + "tls-cipher": "DHE-RSA-AES128-SHA" + }, + "serial": 1, + "version": 1 +} \ No newline at end of file -- cgit v1.2.3 From 14ddf421eb7ef2c39d4a375b4203cf5692402839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 27 Jun 2013 17:51:36 -0300 Subject: Change leap.common.certs.* calls to be dependent on certs This gives us the possibility of mocking up the methods inside it. Also, return the deferred from the run_* method --- src/leap/services/eip/eipbootstrapper.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 4da8f90f..b2af0aea 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -25,7 +25,7 @@ import os from PySide import QtCore from leap.common.check import leap_assert, leap_assert_type -from leap.common.certs import is_valid_pemfile, should_redownload +from leap.common import certs from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth @@ -120,7 +120,7 @@ class EIPBootstrapper(AbstractBootstrapper): # For re-download if something is wrong with the cert self._download_if_needed = self._download_if_needed and \ - not should_redownload(client_cert_path) + not certs.should_redownload(client_cert_path) if self._download_if_needed and \ os.path.exists(client_cert_path): @@ -143,9 +143,7 @@ class EIPBootstrapper(AbstractBootstrapper): res.raise_for_status() client_cert = res.content - # TODO: check certificate validity - - if not is_valid_pemfile(client_cert): + if not certs.is_valid_pemfile(client_cert): raise Exception(self.tr("The downloaded certificate is not a " "valid PEM file")) @@ -177,4 +175,4 @@ class EIPBootstrapper(AbstractBootstrapper): self.download_client_certificate) ] - self.addCallbackChain(cb_chain) + return self.addCallbackChain(cb_chain) -- cgit v1.2.3 From 6fcf0faa8b23bbdc1b9dd462d24cbb30d0c79652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 27 Jun 2013 17:52:28 -0300 Subject: Add EIPBootstrapper tests --- .../services/eip/tests/test_eipbootstrapper.py | 347 +++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 src/leap/services/eip/tests/test_eipbootstrapper.py (limited to 'src') diff --git a/src/leap/services/eip/tests/test_eipbootstrapper.py b/src/leap/services/eip/tests/test_eipbootstrapper.py new file mode 100644 index 00000000..f2331eca --- /dev/null +++ b/src/leap/services/eip/tests/test_eipbootstrapper.py @@ -0,0 +1,347 @@ +# -*- coding: utf-8 -*- +# test_eipbootstrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +Tests for the EIP Boostrapper checks + +These will be whitebox tests since we want to make sure the private +implementation is checking what we expect. +""" + +import os +import mock +import tempfile +import time +try: + import unittest2 as unittest +except ImportError: + import unittest + +from nose.twistedtools import deferred, reactor +from twisted.internet import threads +from requests.models import Response + +from leap.common.testing.basetest import BaseLeapTest +from leap.services.eip.eipbootstrapper import EIPBootstrapper +from leap.services.eip.eipconfig import EIPConfig +from leap.config.providerconfig import ProviderConfig +from leap.crypto.tests import fake_provider +from leap.common.files import mkdir_p +from leap.crypto.srpauth import SRPAuth + + +class EIPBootstrapperActiveTest(BaseLeapTest): + @classmethod + def setUpClass(cls): + BaseLeapTest.setUpClass() + factory = fake_provider.get_provider_factory() + http = reactor.listenTCP(0, factory) + https = reactor.listenSSL( + 0, factory, + fake_provider.OpenSSLServerContextFactory()) + get_port = lambda p: p.getHost().port + cls.http_port = get_port(http) + cls.https_port = get_port(https) + + def setUp(self): + self.eb = EIPBootstrapper() + self.old_pp = EIPConfig.get_path_prefix + self.old_save = EIPConfig.save + self.old_load = EIPConfig.load + self.old_si = SRPAuth.get_session_id + + def tearDown(self): + EIPConfig.get_path_prefix = self.old_pp + EIPConfig.save = self.old_save + EIPConfig.load = self.old_load + SRPAuth.get_session_id = self.old_si + + def _download_config_test_template(self, ifneeded, new): + """ + All download config tests have the same structure, so this is + a parametrized test for that. + + :param ifneeded: sets _download_if_needed + :type ifneeded: bool + :param new: if True uses time.time() as mtime for the mocked + eip-service file, otherwise it uses 100 (a really + old mtime) + :type new: float or int (will be coersed) + """ + pc = ProviderConfig() + pc.get_domain = mock.MagicMock( + return_value="localhost:%s" % (self.https_port)) + self.eb._provider_config = pc + + pc.get_api_uri = mock.MagicMock( + return_value="https://%s" % (pc.get_domain())) + pc.get_api_version = mock.MagicMock(return_value="1") + + # This is to ignore https checking, since it's not the point + # of this test + pc.get_ca_cert_path = mock.MagicMock(return_value=False) + + path_prefix = tempfile.mkdtemp() + EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix) + EIPConfig.save = mock.MagicMock() + EIPConfig.load = mock.MagicMock() + + self.eb._download_if_needed = ifneeded + + provider_dir = os.path.join(EIPConfig.get_path_prefix(), + "leap", + "providers", + pc.get_domain()) + mkdir_p(provider_dir) + eip_config_path = os.path.join(provider_dir, + "eip-service.json") + + with open(eip_config_path, "w") as ec: + ec.write("A") + + # set mtime to something really new + if new: + os.utime(eip_config_path, (-1, time.time())) + else: + os.utime(eip_config_path, (-1, 100)) + + @deferred() + def test_download_config_not_modified(self): + self._download_config_test_template(True, True) + + d = threads.deferToThread(self.eb._download_config) + + def check(*args): + self.assertFalse(self.eb._eip_config.save.called) + d.addCallback(check) + return d + + @deferred() + def test_download_config_modified(self): + self._download_config_test_template(True, False) + + d = threads.deferToThread(self.eb._download_config) + + def check(*args): + self.assertTrue(self.eb._eip_config.save.called) + d.addCallback(check) + return d + + @deferred() + def test_download_config_ignores_mtime(self): + self._download_config_test_template(False, True) + + d = threads.deferToThread(self.eb._download_config) + + def check(*args): + self.eb._eip_config.save.assert_called_once_with( + ["leap", + "providers", + self.eb._provider_config.get_domain(), + "eip-service.json"]) + d.addCallback(check) + return d + + def _download_certificate_test_template(self, ifneeded, createcert): + """ + All download client certificate tests have the same structure, + so this is a parametrized test for that. + + :param ifneeded: sets _download_if_needed + :type ifneeded: bool + :param createcert: if True it creates a dummy file to play the + part of a downloaded certificate + :type createcert: bool + + :returns: the temp eip cert path and the dummy cert contents + :rtype: tuple of str, str + """ + pc = ProviderConfig() + ec = EIPConfig() + self.eb._provider_config = pc + self.eb._eip_config = ec + + pc.get_domain = mock.MagicMock( + return_value="localhost:%s" % (self.https_port)) + pc.get_api_uri = mock.MagicMock( + return_value="https://%s" % (pc.get_domain())) + pc.get_api_version = mock.MagicMock(return_value="1") + pc.get_ca_cert_path = mock.MagicMock(return_value=False) + + path_prefix = tempfile.mkdtemp() + EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix) + EIPConfig.save = mock.MagicMock() + EIPConfig.load = mock.MagicMock() + + self.eb._download_if_needed = ifneeded + + provider_dir = os.path.join(EIPConfig.get_path_prefix(), + "leap", + "providers", + "somedomain") + mkdir_p(provider_dir) + eip_cert_path = os.path.join(provider_dir, + "cert") + + ec.get_client_cert_path = mock.MagicMock( + return_value=eip_cert_path) + + cert_content = "A" + if createcert: + with open(eip_cert_path, "w") as ec: + ec.write(cert_content) + + return eip_cert_path, cert_content + + def test_download_client_certificate_not_modified(self): + cert_path, old_cert_content = self._download_certificate_test_template( + True, True) + + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=False): + self.eb._download_client_certificates() + with open(cert_path, "r") as c: + self.assertEqual(c.read(), old_cert_content) + + @deferred() + def test_download_client_certificate_old_cert(self): + cert_path, old_cert_content = self._download_certificate_test_template( + True, True) + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=True): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=True): + self.eb._download_client_certificates() + + def check(*args): + with open(cert_path, "r") as c: + self.assertNotEqual(c.read(), old_cert_content) + d = threads.deferToThread(wrapper) + d.addCallback(check) + + return d + + @deferred() + def test_download_client_certificate_no_cert(self): + cert_path, _ = self._download_certificate_test_template( + True, False) + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=False): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=True): + self.eb._download_client_certificates() + + def check(*args): + self.assertTrue(os.path.exists(cert_path)) + d = threads.deferToThread(wrapper) + d.addCallback(check) + + return d + + @deferred() + def test_download_client_certificate_force_not_valid(self): + cert_path, old_cert_content = self._download_certificate_test_template( + True, True) + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=True): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=True): + self.eb._download_client_certificates() + + def check(*args): + with open(cert_path, "r") as c: + self.assertNotEqual(c.read(), old_cert_content) + d = threads.deferToThread(wrapper) + d.addCallback(check) + + return d + + @deferred() + def test_download_client_certificate_invalid_download(self): + cert_path, _ = self._download_certificate_test_template( + False, False) + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=True): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=False): + with self.assertRaises(Exception): + self.eb._download_client_certificates() + d = threads.deferToThread(wrapper) + + return d + + @deferred() + def test_download_client_certificate_uses_session_id(self): + _, _ = self._download_certificate_test_template( + False, False) + + SRPAuth.get_session_id = mock.MagicMock(return_value="1") + + def check_cookie(*args, **kwargs): + cookies = kwargs.get("cookies", None) + self.assertEqual(cookies, {'_session_id': '1'}) + return Response() + + def wrapper(*args): + with mock.patch('leap.common.certs.should_redownload', + new_callable=mock.MagicMock, + return_value=False): + with mock.patch('leap.common.certs.is_valid_pemfile', + new_callable=mock.MagicMock, + return_value=True): + with mock.patch('requests.sessions.Session.get', + new_callable=mock.MagicMock, + side_effect=check_cookie): + with mock.patch('requests.models.Response.content', + new_callable=mock.PropertyMock, + return_value="A"): + self.eb._download_client_certificates() + + d = threads.deferToThread(wrapper) + + return d + + @deferred() + def test_run_eip_setup_checks(self): + self.eb._download_config = mock.MagicMock() + self.eb._download_client_certificates = mock.MagicMock() + + d = self.eb.run_eip_setup_checks(ProviderConfig()) + + def check(*args): + self.eb._download_config.assert_called_once_with() + self.eb._download_client_certificates.assert_called_once_with(None) + d.addCallback(check) + return d -- cgit v1.2.3 From ea8657d7352f01b300b9e4d9841590e570cb8270 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 27 Jun 2013 13:10:09 -0300 Subject: Add ProviderConfig tests --- src/leap/config/tests/test_providerconfig.py | 258 +++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 src/leap/config/tests/test_providerconfig.py (limited to 'src') diff --git a/src/leap/config/tests/test_providerconfig.py b/src/leap/config/tests/test_providerconfig.py new file mode 100644 index 00000000..57688fb0 --- /dev/null +++ b/src/leap/config/tests/test_providerconfig.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- +# test_providerconfig.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Tests for providerconfig +""" + +try: + import unittest2 as unittest +except ImportError: + import unittest + +import os +import json +import copy + +from leap.common.testing.basetest import BaseLeapTest +from leap.config.providerconfig import ProviderConfig + +from mock import Mock + + +sample_config = { + "api_uri": "https://api.test.bitmask.net:4430", + "api_version": "1", + "ca_cert_fingerprint": + "SHA256: 0f17c033115f6b76ff67871872303ff65034efe7dd1b910062ca323eb4da5c7e", + "ca_cert_uri": "https://test.bitmask.net/ca.crt", + "default_language": "en", + "description": { + "en": "Test description for provider", + "es": "Descripcion de prueba para el proveedor" + }, + "domain": "test.bitmask.net", + "enrollment_policy": "open", + "languages": [ + "en", + "es" + ], + "name": { + "en": "Bitmask testing environment", + "es": "Entorno de pruebas de Bitmask" + }, + "service": { + "allow_anonymous": True, + "allow_free": True, + "allow_limited_bandwidth": True, + "allow_paid": False, + "allow_registration": True, + "allow_unlimited_bandwidth": False, + "bandwidth_limit": 400000, + "default_service_level": 1, + "levels": [ + { + "bandwidth": "limited", + "id": 1, + "name": "anonymous" + }, + { + "bandwidth": "limited", + "id": 2, + "name": "free", + "storage": 50 + } + ] + }, + "services": [ + "openvpn" + ] +} + + +class ProviderConfigTest(BaseLeapTest): + """Tests for ProviderConfig""" + + def setUp(self): + self._provider_config = ProviderConfig() + json_string = json.dumps(sample_config) + self._provider_config.load(data=json_string) + + # At certain points we are going to be replacing these method + # to avoid creating a file. + # We need to save the old implementation and restore it in + # tearDown so we are sure everything is as expected for each + # test. If we do it inside each specific test, a failure in + # the test will leave the implementation with the mock. + self._old_ospath_exists = os.path.exists + + def tearDown(self): + os.path.exists = self._old_ospath_exists + + def test_configs_ok(self): + """ + Test if the configs loads ok + """ + # TODO: this test should go to the BaseConfig tests + pc = self._provider_config + self.assertEqual(pc.get_api_uri(), sample_config['api_uri']) + self.assertEqual(pc.get_api_version(), sample_config['api_version']) + self.assertEqual(pc.get_ca_cert_fingerprint(), + sample_config['ca_cert_fingerprint']) + self.assertEqual(pc.get_ca_cert_uri(), sample_config['ca_cert_uri']) + self.assertEqual(pc.get_default_language(), + sample_config['default_language']) + + self.assertEqual(pc.get_domain(), sample_config['domain']) + self.assertEqual(pc.get_enrollment_policy(), + sample_config['enrollment_policy']) + self.assertEqual(pc.get_languages(), sample_config['languages']) + + def test_localizations(self): + pc = self._provider_config + + self.assertEqual(pc.get_description(lang='en'), + sample_config['description']['en']) + self.assertEqual(pc.get_description(lang='es'), + sample_config['description']['es']) + + self.assertEqual(pc.get_name(lang='en'), sample_config['name']['en']) + self.assertEqual(pc.get_name(lang='es'), sample_config['name']['es']) + + def _localize(self, lang): + """ + Helper to change default language of the provider config. + """ + pc = self._provider_config + config = copy.deepcopy(sample_config) + config['default_language'] = lang + json_string = json.dumps(config) + pc.load(data=json_string) + + return config + + def test_default_localization1(self): + pc = self._provider_config + config = self._localize(sample_config['languages'][0]) + + default_language = config['default_language'] + default_description = config['description'][default_language] + default_name = config['name'][default_language] + + self.assertEqual(pc.get_description(lang='xx'), default_description) + self.assertEqual(pc.get_description(), default_description) + + self.assertEqual(pc.get_name(lang='xx'), default_name) + self.assertEqual(pc.get_name(), default_name) + + def test_default_localization2(self): + pc = self._provider_config + config = self._localize(sample_config['languages'][1]) + + default_language = config['default_language'] + default_description = config['description'][default_language] + default_name = config['name'][default_language] + + self.assertEqual(pc.get_description(lang='xx'), default_description) + self.assertEqual(pc.get_description(), default_description) + + self.assertEqual(pc.get_name(lang='xx'), default_name) + self.assertEqual(pc.get_name(), default_name) + + def test_get_ca_cert_path_as_expected(self): + pc = self._provider_config + pc.get_path_prefix = Mock(return_value='test') + + provider_domain = sample_config['domain'] + expected_path = os.path.join('test', 'leap', 'providers', + provider_domain, 'keys', 'ca', + 'cacert.pem') + + # mock 'os.path.exists' so we don't get an error for unexisting file + os.path.exists = Mock(return_value=True) + cert_path = pc.get_ca_cert_path() + + self.assertEqual(cert_path, expected_path) + + def test_get_ca_cert_path_about_to_download(self): + pc = self._provider_config + pc.get_path_prefix = Mock(return_value='test') + + provider_domain = sample_config['domain'] + expected_path = os.path.join('test', 'leap', 'providers', + provider_domain, 'keys', 'ca', + 'cacert.pem') + + cert_path = pc.get_ca_cert_path(about_to_download=True) + + self.assertEqual(cert_path, expected_path) + + def test_get_ca_cert_path_fails(self): + pc = self._provider_config + pc.get_path_prefix = Mock(return_value='test') + + # mock 'get_domain' so we don't need to load a config + provider_domain = 'test.provider.com' + pc.get_domain = Mock(return_value=provider_domain) + + with self.assertRaises(AssertionError): + pc.get_ca_cert_path() + + def test_provides_eip(self): + pc = self._provider_config + config = copy.deepcopy(sample_config) + + # It provides + config['services'] = ['openvpn', 'test_service'] + json_string = json.dumps(config) + pc.load(data=json_string) + self.assertTrue(pc.provides_eip()) + + # It does not provides + config['services'] = ['test_service', 'other_service'] + json_string = json.dumps(config) + pc.load(data=json_string) + self.assertFalse(pc.provides_eip()) + + def test_provides_mx(self): + pc = self._provider_config + config = copy.deepcopy(sample_config) + + # It provides + config['services'] = ['mx', 'other_service'] + json_string = json.dumps(config) + pc.load(data=json_string) + # TODO: we do not support mx yet + self.assertFalse(pc.provides_mx()) + + # It does not provides + config['services'] = ['test_service', 'other_service'] + json_string = json.dumps(config) + pc.load(data=json_string) + self.assertFalse(pc.provides_mx()) + + def test_get_services_string(self): + pc = self._provider_config + config = copy.deepcopy(sample_config) + config['services'] = ['test01', 'test02'] + json_string = json.dumps(config) + pc.load(data=json_string) + + self.assertEqual(pc.get_services_string(), "test01, test02") + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From 8ff98b63c077af25fb58dc73750e7765c35ce2f0 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 28 Jun 2013 10:59:56 -0300 Subject: Bugfix: Update available/supported implementation. Closes bug #3032. --- src/leap/config/providerconfig.py | 8 +++----- src/leap/gui/wizard.py | 4 ++-- src/leap/services/__init__.py | 6 +++--- 3 files changed, 8 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/leap/config/providerconfig.py b/src/leap/config/providerconfig.py index 94568c27..8b72153a 100644 --- a/src/leap/config/providerconfig.py +++ b/src/leap/config/providerconfig.py @@ -24,7 +24,6 @@ import os from leap.common.check import leap_assert from leap.common.config.baseconfig import BaseConfig, LocalizedKey from leap.config.provider_spec import leap_provider_spec -from leap.services import get_available logger = logging.getLogger(__name__) @@ -81,17 +80,16 @@ class ProviderConfig(BaseConfig): def get_services(self): """ - Returns a list with the services supported by the - current provider. + Returns a list with the available services in the current provider. :rtype: list """ - services = get_available(self._safe_get_value("services")) + services = self._safe_get_value("services") return services def get_services_string(self): """ - Returns a string with the services supported by the current + Returns a string with the available services in the current provider, ready to be shown to the user. """ services_str = ", ".join(self.get_services()) diff --git a/src/leap/gui/wizard.py b/src/leap/gui/wizard.py index 34eba84c..b29250c8 100644 --- a/src/leap/gui/wizard.py +++ b/src/leap/gui/wizard.py @@ -33,7 +33,7 @@ from leap.util.privilege_policies import is_missing_policy_permissions from leap.util.request_helpers import get_content from leap.util.keyring_helpers import has_keyring from leap.services.eip.providerbootstrapper import ProviderBootstrapper -from leap.services import get_available +from leap.services import get_supported logger = logging.getLogger(__name__) @@ -523,7 +523,7 @@ class Wizard(QtGui.QWizard): self.tr("Services by %s") % (self._provider_config.get_name(),)) - services = get_available( + services = get_supported( self._provider_config.get_services()) for service in services: diff --git a/src/leap/services/__init__.py b/src/leap/services/__init__.py index 70a5dcf2..fc4aa416 100644 --- a/src/leap/services/__init__.py +++ b/src/leap/services/__init__.py @@ -17,10 +17,10 @@ """ Services module. """ -NOT_YET_DEPLOYED = ["mx"] # for 0.2.2 release +DEPLOYED = ["openvpn"] # for 0.2.2 release -def get_available(services): +def get_supported(services): """ Returns a list of the available services. @@ -30,4 +30,4 @@ def get_available(services): :returns: a list of the available services :rtype: list of str """ - return filter(lambda s: s not in NOT_YET_DEPLOYED, services) + return filter(lambda s: s in DEPLOYED, services) -- cgit v1.2.3 From 32b6afae6eb66cd75608b26aecd86bbfc587736e Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 28 Jun 2013 11:05:59 -0300 Subject: Update and add tests for the api changes --- src/leap/config/tests/test_providerconfig.py | 29 ++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/leap/config/tests/test_providerconfig.py b/src/leap/config/tests/test_providerconfig.py index 57688fb0..4e86a5f7 100644 --- a/src/leap/config/tests/test_providerconfig.py +++ b/src/leap/config/tests/test_providerconfig.py @@ -29,6 +29,7 @@ import copy from leap.common.testing.basetest import BaseLeapTest from leap.config.providerconfig import ProviderConfig +from leap.services import get_supported from mock import Mock @@ -235,8 +236,7 @@ class ProviderConfigTest(BaseLeapTest): config['services'] = ['mx', 'other_service'] json_string = json.dumps(config) pc.load(data=json_string) - # TODO: we do not support mx yet - self.assertFalse(pc.provides_mx()) + self.assertTrue(pc.provides_mx()) # It does not provides config['services'] = ['test_service', 'other_service'] @@ -244,14 +244,35 @@ class ProviderConfigTest(BaseLeapTest): pc.load(data=json_string) self.assertFalse(pc.provides_mx()) + def test_supports_unknown_service(self): + pc = self._provider_config + config = copy.deepcopy(sample_config) + + config['services'] = ['unknown'] + json_string = json.dumps(config) + pc.load(data=json_string) + self.assertFalse('unknown' in get_supported(pc.get_services())) + + def test_provides_unknown_service(self): + pc = self._provider_config + config = copy.deepcopy(sample_config) + + config['services'] = ['unknown'] + json_string = json.dumps(config) + pc.load(data=json_string) + self.assertTrue('unknown' in pc.get_services()) + def test_get_services_string(self): pc = self._provider_config config = copy.deepcopy(sample_config) - config['services'] = ['test01', 'test02'] + config['services'] = [ + 'openvpn', 'asdf', 'openvpn', 'not_supported_service'] json_string = json.dumps(config) pc.load(data=json_string) - self.assertEqual(pc.get_services_string(), "test01, test02") + self.assertEqual(pc.get_services_string(), + "Encrypted Internet, asdf, Encrypted Internet," + " not_supported_service") if __name__ == "__main__": -- cgit v1.2.3