diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java index 79809f444b7fa97999576616b48fa58182f7278e..3d55604e30852e8986f33c80d1c0d2532dcf9b4a 100644 --- a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java @@ -2,6 +2,7 @@ package org.briarproject; import net.jodah.concurrentunit.Waiter; +import org.briarproject.api.Bytes; import org.briarproject.api.clients.MessageQueueManager; import org.briarproject.api.clients.PrivateGroupFactory; import org.briarproject.api.clients.SessionId; @@ -31,7 +32,6 @@ import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.SyncSession; import org.briarproject.api.sync.SyncSessionFactory; -import org.briarproject.api.sync.ValidationManager; import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.system.Clock; import org.briarproject.contact.ContactModule; @@ -71,16 +71,16 @@ import static org.junit.Assert.assertTrue; public class ForumSharingIntegrationTest extends BriarTestCase { - LifecycleManager lifecycleManager0, lifecycleManager1, lifecycleManager2; - SyncSessionFactory sync0, sync1, sync2; - ForumManager forumManager0, forumManager1, forumManager2; - ContactManager contactManager0, contactManager1, contactManager2; - ContactId contactId0, contactId2, contactId1, contactId21; - IdentityManager identityManager0, identityManager1, identityManager2; - LocalAuthor author0, author1, author2; - Forum forum0; - SharerListener listener0, listener2; - InviteeListener listener1; + private LifecycleManager lifecycleManager0, lifecycleManager1, lifecycleManager2; + private SyncSessionFactory sync0, sync1, sync2; + private ForumManager forumManager0, forumManager1; + private ContactManager contactManager0, contactManager1, contactManager2; + private ContactId contactId0, contactId2, contactId1, contactId21; + private IdentityManager identityManager0, identityManager1, identityManager2; + private LocalAuthor author0, author1, author2; + private Forum forum0; + private SharerListener listener0, listener2; + private InviteeListener listener1; @Inject Clock clock; @@ -138,7 +138,6 @@ public class ForumSharingIntegrationTest extends BriarTestCase { contactManager2 = t2.getContactManager(); forumManager0 = t0.getForumManager(); forumManager1 = t1.getForumManager(); - forumManager2 = t2.getForumManager(); forumSharingManager0 = t0.getForumSharingManager(); forumSharingManager1 = t1.getForumSharingManager(); forumSharingManager2 = t2.getForumSharingManager(); @@ -384,7 +383,7 @@ public class ForumSharingIntegrationTest extends BriarTestCase { // sharer un-subscribes from forum forumManager0.removeForum(forum0); - // from her on expect the response to fail with a DbException + // from here on expect the response to fail with a DbException thrown.expect(DbException.class); // sync first request message and leave message @@ -509,6 +508,75 @@ public class ForumSharingIntegrationTest extends BriarTestCase { } } + @Test + public void testSharingSameForumWithEachOtherAtSameTime() throws Exception { + startLifecycles(); + try { + // initialize and let invitee accept all requests + defaultInit(true); + + // invitee adds the same forum + DatabaseComponent db1 = t1.getDatabaseComponent(); + Transaction txn = db1.startTransaction(false); + db1.addGroup(txn, forum0.getGroup()); + txn.setComplete(); + db1.endTransaction(txn); + + // send invitation + forumSharingManager0 + .sendForumInvitation(forum0.getId(), contactId1, "Hi!"); + + // invitee now shares same forum back + forumSharingManager1.sendForumInvitation(forum0.getId(), + contactId0, "I am re-sharing this forum with you."); + + // find out who should be Alice, because of random keys + Bytes key0 = new Bytes(author0.getPublicKey()); + Bytes key1 = new Bytes(author1.getPublicKey()); + + // only now sync first request message + boolean alice = key1.compareTo(key0) < 0; + syncToInvitee(); + if (alice) { + assertFalse(listener1.requestReceived); + } else { + eventWaiter.await(TIMEOUT, 1); + assertTrue(listener1.requestReceived); + } + + // sync second invitation + alice = key0.compareTo(key1) < 0; + syncToSharer(); + if (alice) { + assertFalse(listener0.requestReceived); + + // sharer did not receive request, but response to own request + eventWaiter.await(TIMEOUT, 1); + assertTrue(listener0.responseReceived); + + assertEquals(1, forumSharingManager0 + .getForumInvitationMessages(contactId1).size()); + assertEquals(2, forumSharingManager1 + .getForumInvitationMessages(contactId0).size()); + } else { + eventWaiter.await(TIMEOUT, 1); + assertTrue(listener0.requestReceived); + + // send response from sharer to invitee and make sure it arrived + syncToInvitee(); + eventWaiter.await(TIMEOUT, 1); + assertTrue(listener1.responseReceived); + + assertEquals(2, forumSharingManager0 + .getForumInvitationMessages(contactId1).size()); + assertEquals(1, forumSharingManager1 + .getForumInvitationMessages(contactId0).size()); + } + } finally { + stopLifecycles(); + } + } + @Test public void testContactRemoved() throws Exception { startLifecycles(); @@ -670,8 +738,8 @@ public class ForumSharingIntegrationTest extends BriarTestCase { private class SharerListener implements EventListener { - public volatile boolean requestReceived = false; - public volatile boolean responseReceived = false; + volatile boolean requestReceived = false; + volatile boolean responseReceived = false; public void eventOccurred(Event e) { if (e instanceof MessageStateChangedEvent) { @@ -713,8 +781,8 @@ public class ForumSharingIntegrationTest extends BriarTestCase { private class InviteeListener implements EventListener { - public volatile boolean requestReceived = false; - public volatile boolean responseReceived = false; + volatile boolean requestReceived = false; + volatile boolean responseReceived = false; private final boolean accept, answer; diff --git a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java index 08e80a8b9b5c2e425c48b882cc1de7c0bd28b790..c03d781da293085113a06620c3385a1ba640fd0c 100644 --- a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java +++ b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java @@ -27,6 +27,7 @@ import org.briarproject.api.forum.ForumFactory; import org.briarproject.api.forum.ForumInvitationMessage; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumSharingManager; +import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; @@ -206,7 +207,7 @@ class ForumSharingManagerImpl extends BdfIncomingMessageHook ContactId contactId = getContactId(txn, m.getGroupId()); Contact contact = db.getContact(txn, contactId); if (!canBeShared(txn, f.getId(), contact)) - throw new FormatException(); + checkForRaceCondition(txn, f, contact); // initialize state and process invitation InviteeSessionState state = @@ -488,6 +489,46 @@ class ForumSharingManagerImpl extends BdfIncomingMessageHook } } + private void checkForRaceCondition(Transaction txn, Forum f, Contact c) + throws FormatException, DbException { + + GroupId contactGroup = getContactGroup(c).getId(); + if (!listContains(txn, contactGroup, f.getId(), TO_BE_SHARED_BY_US)) + // no race-condition, this invitation is invalid + throw new FormatException(); + + // we have an invitation race condition + LocalAuthor author = db.getLocalAuthor(txn, c.getLocalAuthorId()); + Bytes ourKey = new Bytes(author.getPublicKey()); + Bytes theirKey = new Bytes(c.getAuthor().getPublicKey()); + + // determine which invitation takes precedence + boolean alice = ourKey.compareTo(theirKey) < 0; + + if (alice) { + // our own invitation takes precedence, so just delete Bob's + LOG.info("Invitation race-condition: We are Alice deleting Bob's invitation."); + throw new FormatException(); + } else { + // we are Bob, so we need to "take back" our own invitation + LOG.info("Invitation race-condition: We are Bob taking back our invitation."); + ForumSharingSessionState state = + getSessionStateForLeaving(txn, f, c.getId()); + if (state instanceof SharerSessionState) { + //SharerEngine engine = new SharerEngine(); + //processSharerStateUpdate(txn, null, + // engine.onLocalAction((SharerSessionState) state, + // Action.LOCAL_LEAVE)); + + // simply remove from list instead of involving engine + removeFromList(txn, contactGroup, TO_BE_SHARED_BY_US, f); + // TODO here we could also remove the old session state + // and invitation message + } + } + + } + private SharerSessionState initializeSharerState(Transaction txn, Forum f, ContactId contactId) throws FormatException, DbException {