diff options
author | kali <kali@leap.se> | 2013-01-25 07:44:36 +0900 |
---|---|---|
committer | kali <kali@leap.se> | 2013-01-25 07:44:36 +0900 |
commit | 7102309d9636eeb458cc1cdac521a8555172c728 (patch) | |
tree | 7c3033c8e30fbfcf0090fee97922bb874e74fd43 /src/leap | |
parent | aeabcbd2a771b36ecbd8f4a356e86dd65220ca7b (diff) | |
parent | 9efe6ee0fcf0d000d5da60cbf7dddf6479454aa6 (diff) |
Merge branch 'develop' of ssh://leap.se/leap_client into develop
Diffstat (limited to 'src/leap')
-rw-r--r-- | src/leap/email/__init__.py | 0 | ||||
-rw-r--r-- | src/leap/email/smtp/README | 41 | ||||
-rw-r--r-- | src/leap/email/smtp/__init__.py | 0 | ||||
-rw-r--r-- | src/leap/email/smtp/smtprelay.py | 203 | ||||
-rw-r--r-- | src/leap/email/smtp/tests/185CA770.key | 79 | ||||
-rw-r--r-- | src/leap/email/smtp/tests/185CA770.pub | 52 | ||||
-rw-r--r-- | src/leap/email/smtp/tests/__init__.py | 196 | ||||
-rw-r--r-- | src/leap/email/smtp/tests/mail.txt | 10 | ||||
-rw-r--r-- | src/leap/email/smtp/tests/test_smtprelay.py | 76 | ||||
-rw-r--r-- | src/leap/soledad/backends/couch.py | 69 | ||||
-rw-r--r-- | src/leap/soledad/backends/objectstore.py | 418 | ||||
-rw-r--r-- | src/leap/soledad/tests/test_couch.py | 39 |
12 files changed, 764 insertions, 419 deletions
diff --git a/src/leap/email/__init__.py b/src/leap/email/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/leap/email/__init__.py diff --git a/src/leap/email/smtp/README b/src/leap/email/smtp/README new file mode 100644 index 00000000..a351dcec --- /dev/null +++ b/src/leap/email/smtp/README @@ -0,0 +1,41 @@ +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] + +[1] http://pypi.python.org/pypi/Twisted/12.3.0 + + +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 new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/leap/email/smtp/__init__.py diff --git a/src/leap/email/smtp/smtprelay.py b/src/leap/email/smtp/smtprelay.py new file mode 100644 index 00000000..f44aeb6f --- /dev/null +++ b/src/leap/email/smtp/smtprelay.py @@ -0,0 +1,203 @@ +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() + + +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" # this has to be changed 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 new file mode 100644 index 00000000..587b4164 --- /dev/null +++ b/src/leap/email/smtp/tests/185CA770.key @@ -0,0 +1,79 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +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 +=6HcJ +-----END PGP PRIVATE KEY BLOCK----- diff --git a/src/leap/email/smtp/tests/185CA770.pub b/src/leap/email/smtp/tests/185CA770.pub new file mode 100644 index 00000000..38af19f8 --- /dev/null +++ b/src/leap/email/smtp/tests/185CA770.pub @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +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 +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 new file mode 100644 index 00000000..d00ebeb5 --- /dev/null +++ b/src/leap/email/smtp/tests/__init__.py @@ -0,0 +1,196 @@ +from leap.email.smtp.smtprelay import GPGWrapper +import shutil +from twisted.trial import unittest + +class OpenPGPTestCase(unittest.TestCase): + + PREFIX = "/var/tmp" + GNUPG_HOME = "%s/gnupg" % PREFIX + EMAIL = 'leap@leap.se' + + def setUp(self): + 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): + shutil.rmtree(self.GNUPG_HOME) + + 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) + +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 +=MuOY +-----END PGP PUBLIC KEY BLOCK----- +""" +PRIVATE_KEY = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +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 +=JTFu +-----END PGP PRIVATE KEY BLOCK----- +""" diff --git a/src/leap/email/smtp/tests/mail.txt b/src/leap/email/smtp/tests/mail.txt new file mode 100644 index 00000000..95420470 --- /dev/null +++ b/src/leap/email/smtp/tests/mail.txt @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000..dc0055c6 --- /dev/null +++ b/src/leap/email/smtp/tests/test_smtprelay.py @@ -0,0 +1,76 @@ +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.internet.error import ConnectionDone +from twisted.test import proto_helpers +from twisted.internet import defer +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: <user@leap.se>', + 'RCPT TO: <leap@leap.se>', + 'DATA', + 'From: User <user@leap.se>', + 'To: Leap <leap@leap.se>', + '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/soledad/backends/couch.py b/src/leap/soledad/backends/couch.py index 8ba42d78..c8dadfa8 100644 --- a/src/leap/soledad/backends/couch.py +++ b/src/leap/soledad/backends/couch.py @@ -1,6 +1,8 @@ import uuid from base64 import b64encode, b64decode +from u1db import errors from u1db.sync import LocalSyncTarget +from u1db.backends.inmemory import InMemoryIndex from couchdb.client import Server, Document as CouchDocument from couchdb.http import ResourceNotFound from leap.soledad.backends.objectstore import ObjectStore @@ -36,7 +38,7 @@ class CouchDatabase(ObjectStore): super(CouchDatabase, self).__init__(replica_uid=replica_uid) #------------------------------------------------------------------------- - # implemented methods from Database + # methods from Database #------------------------------------------------------------------------- def _get_doc(self, doc_id, check_for_conflicts=False): @@ -95,6 +97,23 @@ class CouchDatabase(ObjectStore): 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. @@ -110,35 +129,47 @@ class CouchDatabase(ObjectStore): return Synchronizer(self, CouchSyncTarget(url, creds=creds)).sync( autocreate=autocreate) - def _initialize(self): + #------------------------------------------------------------------------- + # 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 = {'sync_log': [], - 'transaction_log': [], - 'conflict_log': b64encode(json.dumps([])), + 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) - self._sync_log.log = content['sync_log'] - self._transaction_log.log = content['transaction_log'] - self._conflict_log.log = json.loads(b64decode(content['conflict_log'])) + # 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 = { - 'sync_log': self._sync_log.log, - 'transaction_log': self._transaction_log.log, + 'transaction_log': self._transaction_log, # Here, the b64 encode ensures that document content # does not cause strange behaviour in couchdb because # of encoding. - 'conflict_log': b64encode(json.dumps(self._conflict_log.log)), + '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) @@ -150,6 +181,22 @@ class CouchDatabase(ObjectStore): 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): diff --git a/src/leap/soledad/backends/objectstore.py b/src/leap/soledad/backends/objectstore.py index d72a2ecc..588fc7a1 100644 --- a/src/leap/soledad/backends/objectstore.py +++ b/src/leap/soledad/backends/objectstore.py @@ -1,77 +1,35 @@ -from u1db.backends import CommonBackend -from u1db import errors, Document, vectorclock +from u1db.backends.inmemory import InMemoryDatabase +from u1db import errors -class ObjectStore(CommonBackend): +class ObjectStore(InMemoryDatabase): """ A backend for storing u1db data in an object store. """ def __init__(self, replica_uid=None): - # This initialization method should be called after the connection - # with the database is established in each implementation, so it can - # ensure that u1db data is configured and up-to-date. - self.set_document_factory(Document) - self._sync_log = SyncLog() - self._transaction_log = TransactionLog() - self._conflict_log = ConflictLog(self._factory) - self._replica_uid = replica_uid - self._ensure_u1db_data() + super(ObjectStore, self).__init__(replica_uid) + # 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() #------------------------------------------------------------------------- - # implemented methods from Database + # methods from Database #------------------------------------------------------------------------- - def set_document_factory(self, factory): - self._factory = factory - - def set_document_size_limit(self, limit): - raise NotImplementedError(self.set_document_size_limit) - - def whats_changed(self, old_generation=0): - self._get_u1db_data() - return self._transaction_log.whats_changed(old_generation) - - def get_doc(self, doc_id, include_deleted=False): - doc = self._get_doc(doc_id, check_for_conflicts=True) - if doc is None: - return None - if doc.is_tombstone() and not include_deleted: - return None - return doc + 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 _update_gen_and_transaction_log(self, doc_id): - new_gen = self._get_generation() + 1 - trans_id = self._allocate_transaction_id() - self._transaction_log.append((new_gen, doc_id, trans_id)) - self._set_u1db_data() + def _get_doc(self, doc): + raise NotImplementedError(self._get_doc) - def put_doc(self, doc): - # consistency check - if doc.doc_id is None: - raise errors.InvalidDocId() - self._check_doc_id(doc.doc_id) - self._check_doc_size(doc) - # check if document exists - old_doc = self._get_doc(doc.doc_id, check_for_conflicts=True) - if old_doc and old_doc.has_conflicts: - raise errors.ConflictedDoc() - if old_doc and doc.rev is None and old_doc.is_tombstone(): - new_rev = self._allocate_doc_rev(old_doc.rev) - else: - if old_doc is not None: - if old_doc.rev != doc.rev: - raise errors.RevisionConflict() - else: - if doc.rev is not None: - raise errors.RevisionConflict() - new_rev = self._allocate_doc_rev(doc.rev) - doc.rev = new_rev - self._put_and_update_indexes(old_doc, doc) - return doc.rev + 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) @@ -89,130 +47,49 @@ class ObjectStore(CommonBackend): self._put_and_update_indexes(old_doc, doc) return new_rev - # start of index-related methods: these are not supported by this backend. + # index-related methods def create_index(self, index_name, *index_expressions): - return False + raise NotImplementedError(self.create_index) def delete_index(self, index_name): - return False - - def list_indexes(self): - return [] - - def get_from_index(self, index_name, *key_values): - return [] - - def get_range_from_index(self, index_name, start_value=None, - end_value=None): - return [] - - def get_index_keys(self, index_name): - return [] - - # end of index-related methods: these are not supported by this backend. - - def get_doc_conflicts(self, doc_id): - self._get_u1db_data() - conflict_docs = self._conflict_log.get_conflicts(doc_id) - if not conflict_docs: - return [] - this_doc = self._get_doc(doc_id) - this_doc.has_conflicts = True - return [this_doc] + list(conflict_docs) - - def resolve_doc(self, doc, conflicted_doc_revs): - cur_doc = self._get_doc(doc.doc_id) - new_rev = self._ensure_maximal_rev(cur_doc.rev, - conflicted_doc_revs) - superseded_revs = set(conflicted_doc_revs) - doc.rev = new_rev - if cur_doc.rev in superseded_revs: - self._put_and_update_indexes(cur_doc, doc) - else: - self._add_conflict(doc.doc_id, new_rev, doc.get_json()) - self._delete_conflicts(doc, superseded_revs) - - def _get_replica_gen_and_trans_id(self, other_replica_uid): - self._get_u1db_data() - return self._sync_log.get_replica_gen_and_trans_id(other_replica_uid) + super(ObjectStore, self).delete_index(index_name) + self._set_u1db_data() - def _set_replica_gen_and_trans_id(self, other_replica_uid, - other_generation, other_transaction_id): - return self._do_set_replica_gen_and_trans_id( - other_replica_uid, - other_generation, - other_transaction_id) + 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): - self._sync_log.set_replica_gen_and_trans_id(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() - def _get_transaction_log(self): - self._get_u1db_data() - return self._transaction_log.get_transaction_log() - #------------------------------------------------------------------------- # implemented methods from CommonBackend #------------------------------------------------------------------------- - def _get_generation(self): - self._get_u1db_data() - return self._transaction_log.get_generation() - - def _get_generation_info(self): - self._get_u1db_data() - return self._transaction_log.get_generation_info() - - def _has_conflicts(self, doc_id): - self._get_u1db_data() - return self._conflict_log.has_conflicts(doc_id) - def _put_and_update_indexes(self, old_doc, doc): - # for now we ignore indexes as this backend is used to store encrypted - # blobs of data in the server. + 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._update_gen_and_transaction_log(doc.doc_id) - - def _get_trans_id_for_gen(self, generation): - self._get_u1db_data() - trans_id = self._transaction_log.get_trans_id_for_gen(generation) - if trans_id is None: - raise errors.InvalidGeneration - return trans_id + self._transaction_log.append((doc.doc_id, trans_id)) + self._set_u1db_data() #------------------------------------------------------------------------- # methods specific for object stores #------------------------------------------------------------------------- - def _ensure_u1db_data(self): - """ - Guarantee that u1db data (logs and replica info) exists in store. - """ - if not self._is_initialized(): - self._initialize() - self._get_u1db_data() - U1DB_DATA_DOC_ID = 'u1db_data' - def _is_initialized(self): - """ - Verify if u1db data exists in store. - """ - if not self._get_doc(self.U1DB_DATA_DOC_ID): - return False - return True - - def _initialize(self): - """ - Create u1db data object in store. - """ - NotImplementedError(self._initialize) - def _get_u1db_data(self): """ Fetch u1db configuration data from backend storage. @@ -225,227 +102,8 @@ class ObjectStore(CommonBackend): """ NotImplementedError(self._set_u1db_data) - def _set_replica_uid(self, replica_uid): - self._replica_uid = replica_uid - self._set_u1db_data() - - def _get_replica_uid(self): - return self._replica_uid - - replica_uid = property( - _get_replica_uid, _set_replica_uid, doc="Replica UID of the database") - - #------------------------------------------------------------------------- - # The methods below were cloned from u1db sqlite backend. They should at - # least exist and raise a NotImplementedError exception in CommonBackend - # (should we maybe fill a bug in u1db bts?). - #------------------------------------------------------------------------- - - def _add_conflict(self, doc_id, my_doc_rev, my_content): - self._conflict_log.append((doc_id, my_doc_rev, my_content)) - self._set_u1db_data() - - def _delete_conflicts(self, doc, conflict_revs): - deleting = [(doc.doc_id, c_rev) for c_rev in conflict_revs] - self._conflict_log.delete_conflicts(deleting) - self._set_u1db_data() - doc.has_conflicts = self._has_conflicts(doc.doc_id) - - def _prune_conflicts(self, doc, doc_vcr): - if self._has_conflicts(doc.doc_id): - autoresolved = False - c_revs_to_prune = [] - for c_doc in self._conflict_log.get_conflicts(doc.doc_id): - c_vcr = vectorclock.VectorClockRev(c_doc.rev) - if doc_vcr.is_newer(c_vcr): - c_revs_to_prune.append(c_doc.rev) - elif doc.same_content_as(c_doc): - c_revs_to_prune.append(c_doc.rev) - doc_vcr.maximize(c_vcr) - autoresolved = True - if autoresolved: - doc_vcr.increment(self._replica_uid) - doc.rev = doc_vcr.as_str() - self._delete_conflicts(doc, c_revs_to_prune) - - def _force_doc_sync_conflict(self, doc): - my_doc = self._get_doc(doc.doc_id) - self._prune_conflicts(doc, vectorclock.VectorClockRev(doc.rev)) - self._add_conflict(doc.doc_id, my_doc.rev, my_doc.get_json()) - doc.has_conflicts = True - self._put_and_update_indexes(my_doc, doc) - - -#---------------------------------------------------------------------------- -# U1DB's TransactionLog, SyncLog, ConflictLog, and Index -#---------------------------------------------------------------------------- - -class SimpleList(object): - def __init__(self): - self._data = [] - - def _set_data(self, data): - self._data = data - - def _get_data(self): - return self._data - - data = property( - _get_data, _set_data, doc="List contents.") - - def append(self, msg): - self._data.append(msg) - - def reduce(self, func, initializer=None): - return reduce(func, self._data, initializer) - - def map(self, func): - return map(func, self._get_data()) - - def filter(self, func): - return filter(func, self._get_data()) - - -class SimpleLog(SimpleList): - - def _set_log(self, log): - self._data = log - - def _get_log(self): - return self._data - - log = property( - _get_log, _set_log, doc="Log contents.") - - -class TransactionLog(SimpleLog): - """ - An ordered list of (generation, doc_id, transaction_id) tuples. - """ - - def _set_log(self, log): - self._data = log - - def _get_data(self, reverse=True): - return sorted(self._data, reverse=reverse) - - _get_log = _get_data - - log = property( - _get_log, _set_log, doc="Log contents.") - - def get_generation(self): - """ - Return the current generation. + def _init_u1db_data(self): """ - gens = self.map(lambda x: x[0]) - if not gens: - return 0 - return max(gens) - - def get_generation_info(self): - """ - Return the current generation and transaction id. - """ - if not self._get_log(): - return(0, '') - info = self.map(lambda x: (x[0], x[2])) - return reduce(lambda x, y: x if (x[0] > y[0]) else y, info) - - def get_trans_id_for_gen(self, gen): - """ - Get the transaction id corresponding to a particular generation. - """ - log = self.reduce(lambda x, y: y if y[0] == gen else x) - if log is None: - return None - return log[2] - - def whats_changed(self, old_generation): - """ - Return a list of documents that have changed since old_generation. + Initialize u1db configuration data on backend storage. """ - results = self.filter(lambda x: x[0] > old_generation) - seen = set() - changes = [] - newest_trans_id = '' - for generation, doc_id, trans_id in results: - if doc_id not in seen: - changes.append((doc_id, generation, trans_id)) - seen.add(doc_id) - if changes: - cur_gen = changes[0][1] # max generation - newest_trans_id = changes[0][2] - changes.reverse() - else: - results = self._get_log() - if not results: - cur_gen = 0 - newest_trans_id = '' - else: - cur_gen, _, newest_trans_id = results[0] - - return cur_gen, newest_trans_id, changes - - def get_transaction_log(self): - """ - Return only a list of (doc_id, transaction_id) - """ - return map(lambda x: (x[1], x[2]), - sorted(self._get_log(reverse=False))) - - -class SyncLog(SimpleLog): - """ - A list of (replica_id, generation, transaction_id) tuples. - """ - - def find_by_replica_uid(self, replica_uid): - if not self._get_log(): - return () - return self.reduce(lambda x, y: y if y[0] == replica_uid else x) - - def get_replica_gen_and_trans_id(self, other_replica_uid): - """ - Return the last known generation and transaction id for the other db - replica. - """ - info = self.find_by_replica_uid(other_replica_uid) - if not info: - return (0, '') - return (info[1], info[2]) - - def set_replica_gen_and_trans_id(self, other_replica_uid, - other_generation, other_transaction_id): - """ - Set the last-known generation and transaction id for the other - database replica. - """ - self._set_log(self.filter(lambda x: x[0] != other_replica_uid)) - self.append((other_replica_uid, other_generation, - other_transaction_id)) - - -class ConflictLog(SimpleLog): - """ - A list of (doc_id, my_doc_rev, my_content) tuples. - """ - - def __init__(self, factory): - super(ConflictLog, self).__init__() - self._factory = factory - - def delete_conflicts(self, conflicts): - for conflict in conflicts: - self._set_log(self.filter(lambda x: - x[0] != conflict[0] or x[1] != conflict[1])) - - def get_conflicts(self, doc_id): - conflicts = self.filter(lambda x: x[0] == doc_id) - if not conflicts: - return [] - return reversed(map(lambda x: self._factory(doc_id, x[1], x[2]), - conflicts)) - - def has_conflicts(self, doc_id): - return bool(self.filter(lambda x: x[0] == doc_id)) + NotImplementedError(self._init_u1db_data) diff --git a/src/leap/soledad/tests/test_couch.py b/src/leap/soledad/tests/test_couch.py index 5e8d6126..6c3d7daf 100644 --- a/src/leap/soledad/tests/test_couch.py +++ b/src/leap/soledad/tests/test_couch.py @@ -46,9 +46,10 @@ def copy_couch_database_for_test(test, db): gen, docs = db.get_all_docs(include_deleted=True) for doc in docs: new_db._put_doc(doc) - new_db._transaction_log._data = copy.deepcopy(db._transaction_log._data) - new_db._sync_log._data = copy.deepcopy(db._sync_log._data) - new_db._conflict_log._data = copy.deepcopy(db._conflict_log._data) + 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 @@ -112,13 +113,13 @@ class CouchWithConflictsTests( # the server, so indexing makes no sense. Thus, we ignore index testing for # now. -# class CouchIndexTests(DatabaseIndexTests): -# -# scenarios = COUCH_SCENARIOS -# -# def tearDown(self): -# self.db.delete_database() -# super(CouchIndexTests, self).tearDown() +class CouchIndexTests(test_backends.DatabaseIndexTests): + + scenarios = COUCH_SCENARIOS + + def tearDown(self): + self.db.delete_database() + super(CouchIndexTests, self).tearDown() #----------------------------------------------------------------------------- @@ -196,23 +197,5 @@ class CouchDatabaseSyncTests(test_sync.DatabaseSyncTests): db.delete_database() super(CouchDatabaseSyncTests, self).tearDown() - # The following tests use indexing, so we eliminate them for now because - # indexing is still not implemented in couch backend. - - def test_sync_pulls_changes(self): - pass - - def test_sync_sees_remote_conflicted(self): - pass - - def test_sync_sees_remote_delete_conflicted(self): - pass - - def test_sync_local_race_conflicted(self): - pass - - def test_sync_propagates_deletes(self): - pass - load_tests = tests.load_with_scenarios |