diff --git a/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java b/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java index 90d52d1ba3e8b8b02f5df5d8b0f8305bfadbd919..339eeaaa09c2198c1bd32a9cfe51dbb904c47a65 100644 --- a/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java +++ b/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java @@ -17,11 +17,10 @@ import org.briarproject.api.db.NoSuchGroupException; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventListener; -import org.briarproject.api.event.GroupAddedEvent; -import org.briarproject.api.event.GroupRemovedEvent; +import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.forum.Forum; -import org.briarproject.api.forum.ForumManager; -import org.briarproject.api.sync.GroupId; +import org.briarproject.api.forum.ForumSharingManager; +import org.briarproject.api.sync.ClientId; import java.util.ArrayList; import java.util.Collection; @@ -44,7 +43,7 @@ implements EventListener, OnItemClickListener { private ListView list = null; // Fields that are accessed from background threads must be volatile - @Inject private volatile ForumManager forumManager; + @Inject private volatile ForumSharingManager forumSharingManager; @Inject private volatile EventBus eventBus; @Override @@ -76,11 +75,10 @@ implements EventListener, OnItemClickListener { Collection<ForumContacts> available = new ArrayList<ForumContacts>(); long now = System.currentTimeMillis(); - for (Forum f : forumManager.getAvailableForums()) { + for (Forum f : forumSharingManager.getAvailableForums()) { try { - GroupId id = f.getId(); Collection<Contact> c = - forumManager.getSubscribers(id); + forumSharingManager.getSharedBy(f.getId()); available.add(new ForumContacts(f, c)); } catch (NoSuchGroupException e) { // Continue @@ -122,17 +120,12 @@ implements EventListener, OnItemClickListener { } public void eventOccurred(Event e) { - // TODO: What other events are needed here? - if (e instanceof GroupAddedEvent) { - GroupAddedEvent g = (GroupAddedEvent) e; - if (g.getGroup().getClientId().equals(forumManager.getClientId())) { - LOG.info("Forum added, reloading"); - loadForums(); - } - } else if (e instanceof GroupRemovedEvent) { - GroupRemovedEvent g = (GroupRemovedEvent) e; - if (g.getGroup().getClientId().equals(forumManager.getClientId())) { - LOG.info("Forum removed, reloading"); + if (e instanceof MessageValidatedEvent) { + MessageValidatedEvent m = (MessageValidatedEvent) e; + ClientId c = m.getClientId(); + if (m.isValid() && !m.isLocal() + && c.equals(forumSharingManager.getClientId())) { + LOG.info("Available forums updated, reloading"); loadForums(); } } @@ -141,20 +134,20 @@ implements EventListener, OnItemClickListener { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { AvailableForumsItem item = adapter.getItem(position); - Collection<ContactId> visible = new ArrayList<ContactId>(); - for (Contact c : item.getContacts()) visible.add(c.getId()); - addSubscription(item.getForum(), visible); + Collection<ContactId> shared = new ArrayList<ContactId>(); + for (Contact c : item.getContacts()) shared.add(c.getId()); + subscribe(item.getForum(), shared); String subscribed = getString(R.string.subscribed_toast); Toast.makeText(this, subscribed, LENGTH_SHORT).show(); + loadForums(); } - private void addSubscription(final Forum f, - final Collection<ContactId> visible) { + private void subscribe(final Forum f, final Collection<ContactId> shared) { runOnDbThread(new Runnable() { public void run() { try { - forumManager.addForum(f); - forumManager.setVisibility(f.getId(), visible); + forumSharingManager.addForum(f); + forumSharingManager.setSharedWith(f.getId(), shared); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); diff --git a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java index 8ae263061408e2be827da4f176c3e0ef32de08a6..ca0b2ab13c8d3f0777937a2c293be8d2d5cf632e 100644 --- a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java @@ -18,7 +18,7 @@ import org.briarproject.android.BriarActivity; import org.briarproject.android.util.LayoutUtils; import org.briarproject.api.db.DbException; import org.briarproject.api.forum.Forum; -import org.briarproject.api.forum.ForumManager; +import org.briarproject.api.forum.ForumSharingManager; import org.briarproject.util.StringUtils; import java.util.logging.Logger; @@ -51,7 +51,7 @@ implements OnEditorActionListener, OnClickListener { private TextView feedback = null; // Fields that are accessed from background threads must be volatile - @Inject private volatile ForumManager forumManager; + @Inject private volatile ForumSharingManager forumSharingManager; @Override public void onCreate(Bundle state) { @@ -138,8 +138,8 @@ implements OnEditorActionListener, OnClickListener { public void run() { try { long now = System.currentTimeMillis(); - Forum f = forumManager.createForum(name); - forumManager.addForum(f); + Forum f = forumSharingManager.createForum(name); + forumSharingManager.addForum(f); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) LOG.info("Storing forum took " + duration + " ms"); diff --git a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java index 0b1606d76819ac6ef72692f32776a3ba7a0c60a4..c879e2bd0b10754d7a752f06f8d3ac8aa63b8509 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java +++ b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java @@ -25,6 +25,7 @@ import org.briarproject.android.util.LayoutUtils; import org.briarproject.android.util.ListLoadingProgressBar; import org.briarproject.api.db.DbException; import org.briarproject.api.db.NoSuchGroupException; +import org.briarproject.api.event.ContactRemovedEvent; import org.briarproject.api.event.Event; import org.briarproject.api.event.GroupAddedEvent; import org.briarproject.api.event.GroupRemovedEvent; @@ -32,6 +33,7 @@ import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPostHeader; +import org.briarproject.api.forum.ForumSharingManager; import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.GroupId; @@ -81,8 +83,8 @@ public class ForumListFragment extends BaseEventFragment implements private ImageButton newForumButton = null; // Fields that are accessed from background threads must be volatile - @Inject - private volatile ForumManager forumManager; + @Inject private volatile ForumManager forumManager; + @Inject private volatile ForumSharingManager forumSharingManager; @Nullable @Override @@ -171,7 +173,8 @@ public class ForumListFragment extends BaseEventFragment implements // Continue } } - int available = forumManager.getAvailableForums().size(); + int available = + forumSharingManager.getAvailableForums().size(); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) LOG.info("Full load took " + duration + " ms"); @@ -252,14 +255,9 @@ public class ForumListFragment extends BaseEventFragment implements } public void eventOccurred(Event e) { - // TODO: What other events are needed here? - if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent m = (MessageValidatedEvent) e; - ClientId c = m.getClientId(); - if (m.isValid() && c.equals(forumManager.getClientId())) { - LOG.info("Message added, reloading"); - loadHeaders(m.getMessage().getGroupId()); - } + if (e instanceof ContactRemovedEvent) { + LOG.info("Contact removed, reloading"); + loadAvailable(); } else if (e instanceof GroupAddedEvent) { GroupAddedEvent g = (GroupAddedEvent) e; if (g.getGroup().getClientId().equals(forumManager.getClientId())) { @@ -272,6 +270,18 @@ public class ForumListFragment extends BaseEventFragment implements LOG.info("Forum removed, reloading"); loadHeaders(); } + } else if (e instanceof MessageValidatedEvent) { + MessageValidatedEvent m = (MessageValidatedEvent) e; + if (m.isValid()) { + ClientId c = m.getClientId(); + if (c.equals(forumManager.getClientId())) { + LOG.info("Forum post added, reloading"); + loadHeaders(m.getMessage().getGroupId()); + } else if (c.equals(forumSharingManager.getClientId())) { + LOG.info("Available forums updated, reloading"); + loadAvailable(); + } + } } } @@ -319,7 +329,8 @@ public class ForumListFragment extends BaseEventFragment implements public void run() { try { long now = System.currentTimeMillis(); - int available = forumManager.getAvailableForums().size(); + int available = + forumSharingManager.getAvailableForums().size(); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) LOG.info("Loading available took " + duration + " ms"); @@ -363,22 +374,22 @@ public class ForumListFragment extends BaseEventFragment implements ContextMenuInfo info = menuItem.getMenuInfo(); int position = ((AdapterContextMenuInfo) info).position; ForumListItem item = adapter.getItem(position); - removeSubscription(item.getForum()); + unsubscribe(item.getForum()); String unsubscribed = getString(R.string.unsubscribed_toast); Toast.makeText(getContext(), unsubscribed, LENGTH_SHORT).show(); } return true; } - private void removeSubscription(final Forum f) { + private void unsubscribe(final Forum f) { listener.runOnDbThread(new Runnable() { public void run() { try { long now = System.currentTimeMillis(); - forumManager.removeForum(f); + forumSharingManager.removeForum(f); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) - LOG.info("Removing group took " + duration + " ms"); + LOG.info("Removing forum took " + duration + " ms"); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); diff --git a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java index 2b95559df059930f6c812a2a121f7e8ce7a7762b..2d237711938c893aa0e12040503e9f451e73620b 100644 --- a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java @@ -19,7 +19,7 @@ import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactManager; import org.briarproject.api.db.DbException; -import org.briarproject.api.forum.ForumManager; +import org.briarproject.api.forum.ForumSharingManager; import org.briarproject.api.sync.GroupId; import java.util.Collection; @@ -52,7 +52,7 @@ SelectContactsDialog.Listener { // Fields that are accessed from background threads must be volatile @Inject private volatile ContactManager contactManager; - @Inject private volatile ForumManager forumManager; + @Inject private volatile ForumSharingManager forumSharingManager; private volatile GroupId groupId = null; private volatile Collection<Contact> contacts = null; private volatile Collection<ContactId> selected = null; @@ -139,7 +139,7 @@ SelectContactsDialog.Listener { try { long now = System.currentTimeMillis(); contacts = contactManager.getContacts(); - selected = forumManager.getVisibility(groupId); + selected = forumSharingManager.getSharedWith(groupId); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) LOG.info("Load took " + duration + " ms"); @@ -175,8 +175,9 @@ SelectContactsDialog.Listener { public void run() { try { long now = System.currentTimeMillis(); - forumManager.setVisibleToAll(groupId, all); - if (!all) forumManager.setVisibility(groupId, selected); + if (all) forumSharingManager.setSharedWithAll(groupId); + else forumSharingManager.setSharedWith(groupId, + selected); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) LOG.info("Update took " + duration + " ms"); diff --git a/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java b/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java index 2016396234633cbc8ccd114411aed646023a0286..7b6d598a4e4133809084a6b504012a96052a7d84 100644 --- a/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java +++ b/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java @@ -1,27 +1,27 @@ package org.briarproject.plugins; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.concurrent.Executor; +import android.app.Application; +import android.content.Context; + +import com.google.inject.Provides; import org.briarproject.api.android.AndroidExecutor; -import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.event.EventBus; import org.briarproject.api.lifecycle.IoExecutor; import org.briarproject.api.plugins.duplex.DuplexPluginConfig; import org.briarproject.api.plugins.duplex.DuplexPluginFactory; import org.briarproject.api.plugins.simplex.SimplexPluginConfig; import org.briarproject.api.plugins.simplex.SimplexPluginFactory; import org.briarproject.api.system.LocationUtils; -import org.briarproject.api.event.EventBus; import org.briarproject.plugins.droidtooth.DroidtoothPluginFactory; import org.briarproject.plugins.tcp.AndroidLanTcpPluginFactory; import org.briarproject.plugins.tor.TorPluginFactory; -import android.app.Application; -import android.content.Context; - -import com.google.inject.Provides; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.Executor; public class AndroidPluginsModule extends PluginsModule { @@ -37,11 +37,11 @@ public class AndroidPluginsModule extends PluginsModule { @Provides DuplexPluginConfig getDuplexPluginConfig(@IoExecutor Executor ioExecutor, AndroidExecutor androidExecutor, Application app, - CryptoComponent crypto, LocationUtils locationUtils, + SecureRandom random, LocationUtils locationUtils, EventBus eventBus) { Context appContext = app.getApplicationContext(); DuplexPluginFactory bluetooth = new DroidtoothPluginFactory(ioExecutor, - androidExecutor, appContext, crypto.getSecureRandom()); + androidExecutor, appContext, random); DuplexPluginFactory tor = new TorPluginFactory(ioExecutor, appContext, locationUtils, eventBus); DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor, diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java index 3f01a4d40b45ea30262388b56dc41682a3f4b092..465a886bd7d4b2f8c8189d0bafdeb4068e215b9c 100644 --- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java +++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java @@ -179,6 +179,9 @@ public interface DatabaseComponent { void incrementStreamCounter(ContactId c, TransportId t, long rotationPeriod) throws DbException; + /** Returns true if the given group is visible to the given contact. */ + boolean isVisibleToContact(ContactId c, GroupId g) throws DbException; + /** * Merges the given metadata with the existing metadata for the given * group. @@ -246,16 +249,14 @@ public interface DatabaseComponent { /** * Makes a group visible to the given set of contacts and invisible to any - * other current or future contacts. + * other contacts. */ void setVisibility(GroupId g, Collection<ContactId> visible) throws DbException; - /** - * Makes a group visible to all current and future contacts, or invisible - * to future contacts. - */ - void setVisibleToAll(GroupId g, boolean all) throws DbException; + /** Makes a group visible or invisible to a contact. */ + void setVisibleToContact(ContactId c, GroupId g, boolean visible) + throws DbException; /** * Stores the given transport keys, deleting any keys they have replaced. diff --git a/briar-api/src/org/briarproject/api/forum/Forum.java b/briar-api/src/org/briarproject/api/forum/Forum.java index 720744a9695976c2f847be5c769ac3499899bbfd..2eeadc21664f069e32db944305a5d0756ec05561 100644 --- a/briar-api/src/org/briarproject/api/forum/Forum.java +++ b/briar-api/src/org/briarproject/api/forum/Forum.java @@ -7,10 +7,12 @@ public class Forum { private final Group group; private final String name; + private final byte[] salt; - public Forum(Group group, String name) { + public Forum(Group group, String name, byte[] salt) { this.group = group; this.name = name; + this.salt = salt; } public GroupId getId() { @@ -25,6 +27,10 @@ public class Forum { return name; } + public byte[] getSalt() { + return salt; + } + @Override public int hashCode() { return group.hashCode(); diff --git a/briar-api/src/org/briarproject/api/forum/ForumConstants.java b/briar-api/src/org/briarproject/api/forum/ForumConstants.java index 4c66fd14295a6081a0c192b331aa7da5861a05cb..035a06cd1ef800f91b1789fa3ae3f51da0c5f457 100644 --- a/briar-api/src/org/briarproject/api/forum/ForumConstants.java +++ b/briar-api/src/org/briarproject/api/forum/ForumConstants.java @@ -1,17 +1,16 @@ package org.briarproject.api.forum; -import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; public interface ForumConstants { - /** The maximum length of a forum's name in bytes. */ - int MAX_FORUM_NAME_LENGTH = MAX_GROUP_DESCRIPTOR_LENGTH - 10; + /** The maximum length of a forum's name in UTF-8 bytes. */ + int MAX_FORUM_NAME_LENGTH = 100; /** The length of a forum's random salt in bytes. */ int FORUM_SALT_LENGTH = 32; - /** The maximum length of a forum post's content type in bytes. */ + /** The maximum length of a forum post's content type in UTF-8 bytes. */ int MAX_CONTENT_TYPE_LENGTH = 50; /** The maximum length of a forum post's body in bytes. */ diff --git a/briar-api/src/org/briarproject/api/forum/ForumManager.java b/briar-api/src/org/briarproject/api/forum/ForumManager.java index 76464bac400574d8f1fe6fc3d063321d5887aaec..1d32544654c799eed46be9320f34749665d1889c 100644 --- a/briar-api/src/org/briarproject/api/forum/ForumManager.java +++ b/briar-api/src/org/briarproject/api/forum/ForumManager.java @@ -1,7 +1,5 @@ package org.briarproject.api.forum; -import org.briarproject.api.contact.Contact; -import org.briarproject.api.contact.ContactId; import org.briarproject.api.db.DbException; import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.GroupId; @@ -14,18 +12,9 @@ public interface ForumManager { /** Returns the unique ID of the forum client. */ ClientId getClientId(); - /** Creates a forum with the given name. */ - Forum createForum(String name); - - /** Subscribes to a forum. */ - void addForum(Forum f) throws DbException; - /** Stores a local forum post. */ void addLocalPost(ForumPost p) throws DbException; - /** Returns all forums to which the user could subscribe. */ - Collection<Forum> getAvailableForums() throws DbException; - /** Returns the forum with the given ID. */ Forum getForum(GroupId g) throws DbException; @@ -38,28 +27,6 @@ public interface ForumManager { /** Returns the headers of all posts in the given forum. */ Collection<ForumPostHeader> getPostHeaders(GroupId g) throws DbException; - /** Returns all contacts who subscribe to the given forum. */ - Collection<Contact> getSubscribers(GroupId g) throws DbException; - - /** Returns the IDs of all contacts to which the given forum is visible. */ - Collection<ContactId> getVisibility(GroupId g) throws DbException; - - /** Unsubscribes from a forum. */ - void removeForum(Forum f) throws DbException; - /** Marks a forum post as read or unread. */ void setReadFlag(MessageId m, boolean read) throws DbException; - - /** - * Makes a forum visible to the given set of contacts and invisible to any - * other current or future contacts. - */ - void setVisibility(GroupId g, Collection<ContactId> visible) - throws DbException; - - /** - * Makes a forum visible to all current and future contacts, or invisible - * to future contacts. - */ - void setVisibleToAll(GroupId g, boolean all) throws DbException; } diff --git a/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java b/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java new file mode 100644 index 0000000000000000000000000000000000000000..91b806e0e828d4d228c17e348ce830c5260fbc78 --- /dev/null +++ b/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java @@ -0,0 +1,43 @@ +package org.briarproject.api.forum; + +import org.briarproject.api.contact.Contact; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.db.DbException; +import org.briarproject.api.sync.ClientId; +import org.briarproject.api.sync.GroupId; + +import java.util.Collection; + +public interface ForumSharingManager { + + /** Returns the unique ID of the forum sharing client. */ + ClientId getClientId(); + + /** Creates a forum with the given name. */ + Forum createForum(String name); + + /** Subscribes to a forum. */ + void addForum(Forum f) throws DbException; + + /** Unsubscribes from a forum. */ + void removeForum(Forum f) throws DbException; + + /** Returns all forums to which the user could subscribe. */ + Collection<Forum> getAvailableForums() throws DbException; + + /** Returns all contacts who are sharing the given forum with the user. */ + Collection<Contact> getSharedBy(GroupId g) throws DbException; + + /** Returns the IDs of all contacts with whom the given forum is shared. */ + Collection<ContactId> getSharedWith(GroupId g) throws DbException; + + /** + * Shares a forum with the given contacts and unshares it with any other + * contacts. + */ + void setSharedWith(GroupId g, Collection<ContactId> shared) + throws DbException; + + /** Shares a forum with all current and future contacts. */ + void setSharedWithAll(GroupId g) throws DbException; +} diff --git a/briar-api/src/org/briarproject/api/sync/MessageValidator.java b/briar-api/src/org/briarproject/api/sync/MessageValidator.java index d3d7ef520a2d0f29554f74dcb160417a28f86e19..58ee5dc2b96b980d0dea03a85a50951b0f23e4c5 100644 --- a/briar-api/src/org/briarproject/api/sync/MessageValidator.java +++ b/briar-api/src/org/briarproject/api/sync/MessageValidator.java @@ -8,5 +8,5 @@ public interface MessageValidator { * Validates the given message and returns its metadata if the message * is valid, or null if the message is invalid. */ - Metadata validateMessage(Message m); + Metadata validateMessage(Message m, Group g); } diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java index e5b65296d33857f32fe3eb02c6e25249e2a7a04e..a5a22b135cd93aeae3bcd274ee09192af0220e7c 100644 --- a/briar-core/src/org/briarproject/db/Database.java +++ b/briar-core/src/org/briarproject/db/Database.java @@ -598,13 +598,6 @@ interface Database<T> { void setReorderingWindow(T txn, ContactId c, TransportId t, long rotationPeriod, long base, byte[] bitmap) throws DbException; - /** - * Makes a group visible or invisible to future contacts by default. - * <p> - * Locking: write. - */ - void setVisibleToAll(T txn, GroupId g, boolean all) throws DbException; - /** * Updates the transmission count and expiry time of the given message * with respect to the given contact, using the latency of the transport diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java index 44c177dc8226ea6ff90eb2ee9c9c10e6f41abfb9..9e929d86c83865c158c4c58e3a4cab9f4b191109 100644 --- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java +++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java @@ -223,6 +223,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { if (added) { eventBus.broadcast(new MessageAddedEvent(m, null)); eventBus.broadcast(new MessageValidatedEvent(m, c, true, true)); + if (shared) eventBus.broadcast(new MessageSharedEvent(m)); } } @@ -801,6 +802,28 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } + public boolean isVisibleToContact(ContactId c, GroupId g) + throws DbException { + lock.readLock().lock(); + try { + T txn = db.startTransaction(); + try { + if (!db.containsContact(txn, c)) + throw new NoSuchContactException(); + if (!db.containsGroup(txn, g)) + throw new NoSuchGroupException(); + boolean visible = db.containsVisibleGroup(txn, c, g); + db.commitTransaction(txn); + return visible; + } catch (DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + lock.readLock().unlock(); + } + } + public void mergeGroupMetadata(GroupId g, Metadata meta) throws DbException { lock.writeLock().lock(); @@ -900,7 +923,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { duplicate = db.containsMessage(txn, m.getId()); visible = db.containsVisibleGroup(txn, c, m.getGroupId()); if (visible) { - if (!duplicate) addMessage(txn, m, UNKNOWN, true, c); + if (!duplicate) addMessage(txn, m, UNKNOWN, false, c); db.raiseAckFlag(txn, c, m.getId()); } db.commitTransaction(txn); @@ -1162,7 +1185,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { if (!db.containsGroup(txn, g)) throw new NoSuchGroupException(); // Use HashSets for O(1) lookups, O(n) overall running time - HashSet<ContactId> now = new HashSet<ContactId>(visible); + Collection<ContactId> now = new HashSet<ContactId>(visible); Collection<ContactId> before = db.getVisibility(txn, g); before = new HashSet<ContactId>(before); // Set the group's visibility for each current contact @@ -1177,8 +1200,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { affected.add(c); } } - // Make the group invisible to future contacts - db.setVisibleToAll(txn, g, false); db.commitTransaction(txn); } catch (DbException e) { db.abortTransaction(txn); @@ -1191,27 +1212,20 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { eventBus.broadcast(new GroupVisibilityUpdatedEvent(affected)); } - public void setVisibleToAll(GroupId g, boolean all) throws DbException { - Collection<ContactId> affected = new ArrayList<ContactId>(); + public void setVisibleToContact(ContactId c, GroupId g, boolean visible) + throws DbException { + boolean wasVisible = false; lock.writeLock().lock(); try { T txn = db.startTransaction(); try { + if (!db.containsContact(txn, c)) + throw new NoSuchContactException(); if (!db.containsGroup(txn, g)) throw new NoSuchGroupException(); - // Make the group visible or invisible to future contacts - db.setVisibleToAll(txn, g, all); - if (all) { - // Make the group visible to all current contacts - Collection<ContactId> before = db.getVisibility(txn, g); - before = new HashSet<ContactId>(before); - for (ContactId c : db.getContactIds(txn)) { - if (!before.contains(c)) { - db.addVisibility(txn, c, g); - affected.add(c); - } - } - } + wasVisible = db.containsVisibleGroup(txn, c, g); + if (visible && !wasVisible) db.addVisibility(txn, c, g); + else if (!visible && wasVisible) db.removeVisibility(txn, c, g); db.commitTransaction(txn); } catch (DbException e) { db.abortTransaction(txn); @@ -1220,8 +1234,10 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } finally { lock.writeLock().unlock(); } - if (!affected.isEmpty()) - eventBus.broadcast(new GroupVisibilityUpdatedEvent(affected)); + if (visible != wasVisible) { + eventBus.broadcast(new GroupVisibilityUpdatedEvent( + Collections.singletonList(c))); + } } public void updateTransportKeys(Map<ContactId, TransportKeys> keys) diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java index f922c548c724e2c3b5b783b3b0a9baef4336fbae..79c07da039a7ec53fe8a477c4d99be11dbfa76b9 100644 --- a/briar-core/src/org/briarproject/db/JdbcDatabase.java +++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java @@ -66,8 +66,8 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry; */ abstract class JdbcDatabase implements Database<Connection> { - private static final int SCHEMA_VERSION = 19; - private static final int MIN_SCHEMA_VERSION = 19; + private static final int SCHEMA_VERSION = 20; + private static final int MIN_SCHEMA_VERSION = 20; private static final String CREATE_SETTINGS = "CREATE TABLE settings" @@ -104,7 +104,6 @@ abstract class JdbcDatabase implements Database<Connection> { + " (groupId HASH NOT NULL," + " clientId HASH NOT NULL," + " descriptor BINARY NOT NULL," - + " visibleToAll BOOLEAN NOT NULL," + " PRIMARY KEY (groupId))"; private static final String CREATE_GROUP_METADATA = @@ -511,30 +510,6 @@ abstract class JdbcDatabase implements Database<Connection> { if (rows != 1) throw new DbStateException(); ps.close(); } - // Make groups that are visible to everyone visible to this contact - sql = "SELECT groupId FROM groups WHERE visibleToAll = TRUE"; - ps = txn.prepareStatement(sql); - rs = ps.executeQuery(); - ids = new ArrayList<byte[]>(); - while (rs.next()) ids.add(rs.getBytes(1)); - rs.close(); - ps.close(); - if (!ids.isEmpty()) { - sql = "INSERT INTO groupVisibilities (contactId, groupId)" - + " VALUES (?, ?)"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - for (byte[] id : ids) { - ps.setBytes(2, id); - ps.addBatch(); - } - int[] batchAffected = ps.executeBatch(); - if (batchAffected.length != ids.size()) - throw new DbStateException(); - for (int rows : batchAffected) - if (rows != 1) throw new DbStateException(); - ps.close(); - } return c; } catch (SQLException e) { tryToClose(rs); @@ -546,9 +521,8 @@ abstract class JdbcDatabase implements Database<Connection> { public void addGroup(Connection txn, Group g) throws DbException { PreparedStatement ps = null; try { - String sql = "INSERT INTO groups" - + " (groupId, clientId, descriptor, visibleToAll)" - + " VALUES (?, ?, ?, FALSE)"; + String sql = "INSERT INTO groups (groupId, clientId, descriptor)" + + " VALUES (?, ?, ?)"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getId().getBytes()); ps.setBytes(2, g.getClientId().getBytes()); @@ -2137,23 +2111,6 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public void setVisibleToAll(Connection txn, GroupId g, boolean all) - throws DbException { - PreparedStatement ps = null; - try { - String sql = "UPDATE groups SET visibleToAll = ? WHERE groupId = ?"; - ps = txn.prepareStatement(sql); - ps.setBoolean(1, all); - ps.setBytes(2, g.getBytes()); - int affected = ps.executeUpdate(); - if (affected < 0 || affected > 1) throw new DbStateException(); - ps.close(); - } catch (SQLException e) { - tryToClose(ps); - throw new DbException(e); - } - } - public void updateExpiryTime(Connection txn, ContactId c, MessageId m, int maxLatency) throws DbException { PreparedStatement ps = null; diff --git a/briar-core/src/org/briarproject/forum/ForumListValidator.java b/briar-core/src/org/briarproject/forum/ForumListValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..98d905c2989f081368cb4ae104460d17f73ebe6d --- /dev/null +++ b/briar-core/src/org/briarproject/forum/ForumListValidator.java @@ -0,0 +1,69 @@ +package org.briarproject.forum; + +import org.briarproject.api.FormatException; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfReader; +import org.briarproject.api.data.BdfReaderFactory; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.db.Metadata; +import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageValidator; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.logging.Logger; + +import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH; +import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; + +class ForumListValidator implements MessageValidator { + + private static final Logger LOG = + Logger.getLogger(ForumListValidator.class.getName()); + + private final BdfReaderFactory bdfReaderFactory; + private final MetadataEncoder metadataEncoder; + + ForumListValidator(BdfReaderFactory bdfReaderFactory, + MetadataEncoder metadataEncoder) { + this.bdfReaderFactory = bdfReaderFactory; + this.metadataEncoder = metadataEncoder; + } + + @Override + public Metadata validateMessage(Message m, Group g) { + try { + // Parse the message body + byte[] raw = m.getRaw(); + ByteArrayInputStream in = new ByteArrayInputStream(raw, + MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); + BdfReader r = bdfReaderFactory.createReader(in); + r.readListStart(); + long version = r.readInteger(); + if (version < 0) throw new FormatException(); + r.readListStart(); + while (!r.hasListEnd()) { + r.readListStart(); + String name = r.readString(MAX_FORUM_NAME_LENGTH); + if (name.length() == 0) throw new FormatException(); + byte[] salt = r.readRaw(FORUM_SALT_LENGTH); + if (salt.length != FORUM_SALT_LENGTH) + throw new FormatException(); + r.readListEnd(); + } + r.readListEnd(); + r.readListEnd(); + if (!r.eof()) throw new FormatException(); + // Return the metadata + BdfDictionary d = new BdfDictionary(); + d.put("version", version); + d.put("local", false); + return metadataEncoder.encode(d); + } catch (IOException e) { + LOG.info("Invalid forum list"); + return null; + } + } +} diff --git a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java index 872974c299ad37372f299cc14dbe2ec6aa187405..0e2c581c84e293d97fbe61eb8f6ca947ad6cea18 100644 --- a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java +++ b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java @@ -4,13 +4,10 @@ import com.google.inject.Inject; import org.briarproject.api.FormatException; import org.briarproject.api.contact.Contact; -import org.briarproject.api.contact.ContactId; -import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.contact.ContactManager; import org.briarproject.api.data.BdfDictionary; import org.briarproject.api.data.BdfReader; import org.briarproject.api.data.BdfReaderFactory; -import org.briarproject.api.data.BdfWriter; -import org.briarproject.api.data.BdfWriterFactory; import org.briarproject.api.data.MetadataEncoder; import org.briarproject.api.data.MetadataParser; import org.briarproject.api.db.DatabaseComponent; @@ -25,15 +22,12 @@ import org.briarproject.api.identity.AuthorId; import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; -import org.briarproject.api.sync.GroupFactory; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; import org.briarproject.util.StringUtils; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -42,6 +36,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Logger; import static java.util.logging.Level.WARNING; @@ -63,25 +58,23 @@ class ForumManagerImpl implements ForumManager { Logger.getLogger(ForumManagerImpl.class.getName()); private final DatabaseComponent db; - private final GroupFactory groupFactory; + private final ContactManager contactManager; private final BdfReaderFactory bdfReaderFactory; - private final BdfWriterFactory bdfWriterFactory; private final MetadataEncoder metadataEncoder; private final MetadataParser metadataParser; - private final SecureRandom random; + + /** Ensures isolation between database reads and writes. */ + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); @Inject - ForumManagerImpl(CryptoComponent crypto, DatabaseComponent db, - GroupFactory groupFactory, BdfReaderFactory bdfReaderFactory, - BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder, + ForumManagerImpl(DatabaseComponent db, ContactManager contactManager, + BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder, MetadataParser metadataParser) { this.db = db; - this.groupFactory = groupFactory; + this.contactManager = contactManager; this.bdfReaderFactory = bdfReaderFactory; - this.bdfWriterFactory = bdfWriterFactory; this.metadataEncoder = metadataEncoder; this.metadataParser = metadataParser; - random = crypto.getSecureRandom(); } @Override @@ -89,214 +82,171 @@ class ForumManagerImpl implements ForumManager { return CLIENT_ID; } - @Override - public Forum createForum(String name) { - int length = StringUtils.toUtf8(name).length; - if (length == 0) throw new IllegalArgumentException(); - if (length > MAX_FORUM_NAME_LENGTH) - throw new IllegalArgumentException(); - byte[] salt = new byte[FORUM_SALT_LENGTH]; - random.nextBytes(salt); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - BdfWriter w = bdfWriterFactory.createWriter(out); - try { - w.writeListStart(); - w.writeString(name); - w.writeRaw(salt); - w.writeListEnd(); - } catch (IOException e) { - // Shouldn't happen with ByteArrayOutputStream - throw new RuntimeException(e); - } - Group g = groupFactory.createGroup(CLIENT_ID, out.toByteArray()); - return new Forum(g, name); - } - - @Override - public void addForum(Forum f) throws DbException { - db.addGroup(f.getGroup()); - } - @Override public void addLocalPost(ForumPost p) throws DbException { - BdfDictionary d = new BdfDictionary(); - d.put("timestamp", p.getMessage().getTimestamp()); - if (p.getParent() != null) d.put("parent", p.getParent().getBytes()); - if (p.getAuthor() != null) { - Author a = p.getAuthor(); - BdfDictionary d1 = new BdfDictionary(); - d1.put("id", a.getId().getBytes()); - d1.put("name", a.getName()); - d1.put("publicKey", a.getPublicKey()); - d.put("author", d1); - } - d.put("contentType", p.getContentType()); - d.put("local", true); - d.put("read", true); + lock.writeLock().lock(); try { + BdfDictionary d = new BdfDictionary(); + d.put("timestamp", p.getMessage().getTimestamp()); + if (p.getParent() != null) + d.put("parent", p.getParent().getBytes()); + if (p.getAuthor() != null) { + Author a = p.getAuthor(); + BdfDictionary d1 = new BdfDictionary(); + d1.put("id", a.getId().getBytes()); + d1.put("name", a.getName()); + d1.put("publicKey", a.getPublicKey()); + d.put("author", d1); + } + d.put("contentType", p.getContentType()); + d.put("local", true); + d.put("read", true); Metadata meta = metadataEncoder.encode(d); db.addLocalMessage(p.getMessage(), CLIENT_ID, meta, true); } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - } - } - - @Override - public Collection<Forum> getAvailableForums() throws DbException { - // TODO - return Collections.emptyList(); - } - - private Forum parseForum(Group g) throws FormatException { - ByteArrayInputStream in = new ByteArrayInputStream(g.getDescriptor()); - BdfReader r = bdfReaderFactory.createReader(in); - try { - r.readListStart(); - String name = r.readString(MAX_FORUM_NAME_LENGTH); - if (name.length() == 0) throw new FormatException(); - byte[] salt = r.readRaw(FORUM_SALT_LENGTH); - if (salt.length != FORUM_SALT_LENGTH) throw new FormatException(); - r.readListEnd(); - if (!r.eof()) throw new FormatException(); - return new Forum(g, name); - } catch (FormatException e) { - throw e; - } catch (IOException e) { - // Shouldn't happen with ByteArrayInputStream throw new RuntimeException(e); + } finally { + lock.writeLock().unlock(); } } @Override public Forum getForum(GroupId g) throws DbException { - Group group = db.getGroup(g); - if (!group.getClientId().equals(CLIENT_ID)) - throw new IllegalArgumentException(); + lock.readLock().lock(); try { - return parseForum(group); + return parseForum(db.getGroup(g)); } catch (FormatException e) { - throw new IllegalArgumentException(); + throw new DbException(e); + } finally { + lock.readLock().unlock(); } } @Override public Collection<Forum> getForums() throws DbException { - Collection<Group> groups = db.getGroups(CLIENT_ID); - List<Forum> forums = new ArrayList<Forum>(groups.size()); - for (Group g : groups) { - try { - forums.add(parseForum(g)); - } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - } + lock.readLock().lock(); + try { + List<Forum> forums = new ArrayList<Forum>(); + for (Group g : db.getGroups(CLIENT_ID)) forums.add(parseForum(g)); + return Collections.unmodifiableList(forums); + } catch (FormatException e) { + throw new DbException(e); + } finally { + lock.readLock().unlock(); } - return Collections.unmodifiableList(forums); } @Override public byte[] getPostBody(MessageId m) throws DbException { - byte[] raw = db.getRawMessage(m); - ByteArrayInputStream in = new ByteArrayInputStream(raw, - MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); - BdfReader r = bdfReaderFactory.createReader(in); + lock.readLock().lock(); try { - // Extract the forum post body + byte[] raw = db.getRawMessage(m); + ByteArrayInputStream in = new ByteArrayInputStream(raw, + MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); + BdfReader r = bdfReaderFactory.createReader(in); r.readListStart(); if (r.hasRaw()) r.skipRaw(); // Parent ID else r.skipNull(); // No parent if (r.hasList()) r.skipList(); // Author else r.skipNull(); // No author r.skipString(); // Content type - return r.readRaw(MAX_FORUM_POST_BODY_LENGTH); + byte[] postBody = r.readRaw(MAX_FORUM_POST_BODY_LENGTH); + if (r.hasRaw()) r.skipRaw(); // Signature + else r.skipNull(); + r.readListEnd(); + if (!r.eof()) throw new FormatException(); + return postBody; } catch (FormatException e) { - // Not a valid forum post - throw new IllegalArgumentException(); + throw new DbException(e); } catch (IOException e) { // Shouldn't happen with ByteArrayInputStream throw new RuntimeException(e); + } finally { + lock.readLock().unlock(); } } @Override public Collection<ForumPostHeader> getPostHeaders(GroupId g) throws DbException { - // Load the IDs of the user's own identities and contacts' identities - Set<AuthorId> localAuthorIds = new HashSet<AuthorId>(); - for (LocalAuthor a : db.getLocalAuthors()) - localAuthorIds.add(a.getId()); - Set<AuthorId> contactAuthorIds = new HashSet<AuthorId>(); - for (Contact c : db.getContacts()) - contactAuthorIds.add(c.getAuthor().getId()); - // Load and parse the metadata - Map<MessageId, Metadata> metadata = db.getMessageMetadata(g); - Collection<ForumPostHeader> headers = new ArrayList<ForumPostHeader>(); - for (Entry<MessageId, Metadata> e : metadata.entrySet()) { - MessageId messageId = e.getKey(); - Metadata meta = e.getValue(); - try { - BdfDictionary d = metadataParser.parse(meta); - long timestamp = d.getInteger("timestamp"); - Author author = null; - Author.Status authorStatus = ANONYMOUS; - BdfDictionary d1 = d.getDictionary("author", null); - if (d1 != null) { - AuthorId authorId = new AuthorId(d1.getRaw("id")); - String name = d1.getString("name"); - byte[] publicKey = d1.getRaw("publicKey"); - author = new Author(authorId, name, publicKey); - if (localAuthorIds.contains(authorId)) - authorStatus = VERIFIED; - else if (contactAuthorIds.contains(authorId)) - authorStatus = VERIFIED; - else authorStatus = UNKNOWN; + lock.readLock().lock(); + try { + // Load the IDs of the user's identities + Set<AuthorId> localAuthorIds = new HashSet<AuthorId>(); + for (LocalAuthor a : db.getLocalAuthors()) + localAuthorIds.add(a.getId()); + // Load the IDs of contacts' identities + Set<AuthorId> contactAuthorIds = new HashSet<AuthorId>(); + for (Contact c : contactManager.getContacts()) + contactAuthorIds.add(c.getAuthor().getId()); + // Load and parse the metadata + Map<MessageId, Metadata> metadata = db.getMessageMetadata(g); + Collection<ForumPostHeader> headers = + new ArrayList<ForumPostHeader>(); + for (Entry<MessageId, Metadata> e : metadata.entrySet()) { + MessageId messageId = e.getKey(); + Metadata meta = e.getValue(); + try { + BdfDictionary d = metadataParser.parse(meta); + long timestamp = d.getInteger("timestamp"); + Author author = null; + Author.Status authorStatus = ANONYMOUS; + BdfDictionary d1 = d.getDictionary("author", null); + if (d1 != null) { + AuthorId authorId = new AuthorId(d1.getRaw("id")); + String name = d1.getString("name"); + byte[] publicKey = d1.getRaw("publicKey"); + author = new Author(authorId, name, publicKey); + if (localAuthorIds.contains(authorId)) + authorStatus = VERIFIED; + else if (contactAuthorIds.contains(authorId)) + authorStatus = VERIFIED; + else authorStatus = UNKNOWN; + } + String contentType = d.getString("contentType"); + boolean read = d.getBoolean("read"); + headers.add(new ForumPostHeader(messageId, timestamp, + author, authorStatus, contentType, read)); + } catch (FormatException ex) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, ex.toString(), ex); } - String contentType = d.getString("contentType"); - boolean read = d.getBoolean("read"); - headers.add(new ForumPostHeader(messageId, timestamp, author, - authorStatus, contentType, read)); - } catch (FormatException ex) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, ex.toString(), ex); } + return headers; + } finally { + lock.readLock().unlock(); } - return headers; - } - - @Override - public Collection<Contact> getSubscribers(GroupId g) throws DbException { - // TODO - return Collections.emptyList(); - } - - @Override - public Collection<ContactId> getVisibility(GroupId g) throws DbException { - return db.getVisibility(g); - } - - @Override - public void removeForum(Forum f) throws DbException { - db.removeGroup(f.getGroup()); } @Override public void setReadFlag(MessageId m, boolean read) throws DbException { - BdfDictionary d = new BdfDictionary(); - d.put("read", read); + lock.writeLock().lock(); try { + BdfDictionary d = new BdfDictionary(); + d.put("read", read); db.mergeMessageMetadata(m, metadataEncoder.encode(d)); } catch (FormatException e) { throw new RuntimeException(e); + } finally { + lock.writeLock().unlock(); } } - @Override - public void setVisibility(GroupId g, Collection<ContactId> visible) - throws DbException { - db.setVisibility(g, visible); - } - - @Override - public void setVisibleToAll(GroupId g, boolean all) throws DbException { - db.setVisibleToAll(g, all); + private Forum parseForum(Group g) throws FormatException { + ByteArrayInputStream in = new ByteArrayInputStream(g.getDescriptor()); + BdfReader r = bdfReaderFactory.createReader(in); + try { + r.readListStart(); + String name = r.readString(MAX_FORUM_NAME_LENGTH); + byte[] salt = r.readRaw(FORUM_SALT_LENGTH); + r.readListEnd(); + if (!r.eof()) throw new FormatException(); + return new Forum(g, name, salt); + } catch (FormatException e) { + throw e; + } catch (IOException e) { + // Shouldn't happen with ByteArrayInputStream + throw new RuntimeException(e); + } } } diff --git a/briar-core/src/org/briarproject/forum/ForumModule.java b/briar-core/src/org/briarproject/forum/ForumModule.java index 9dd05f22d69963fb7d209bfa106e63e490b40263..fc24c6c00cf1835bf0e23521b9084e007ed5c58f 100644 --- a/briar-core/src/org/briarproject/forum/ForumModule.java +++ b/briar-core/src/org/briarproject/forum/ForumModule.java @@ -3,39 +3,63 @@ package org.briarproject.forum; import com.google.inject.AbstractModule; import com.google.inject.Provides; +import org.briarproject.api.contact.ContactManager; import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.data.BdfReaderFactory; import org.briarproject.api.data.BdfWriterFactory; import org.briarproject.api.data.MetadataEncoder; import org.briarproject.api.data.ObjectReader; +import org.briarproject.api.event.EventBus; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPostFactory; +import org.briarproject.api.forum.ForumSharingManager; import org.briarproject.api.identity.Author; import org.briarproject.api.sync.ValidationManager; import org.briarproject.api.system.Clock; import javax.inject.Singleton; -import static org.briarproject.forum.ForumManagerImpl.CLIENT_ID; - public class ForumModule extends AbstractModule { @Override protected void configure() { - bind(ForumManager.class).to(ForumManagerImpl.class); + bind(ForumManager.class).to(ForumManagerImpl.class).in(Singleton.class); bind(ForumPostFactory.class).to(ForumPostFactoryImpl.class); } @Provides @Singleton - ForumPostValidator getValidator(ValidationManager validationManager, - CryptoComponent crypto, BdfReaderFactory bdfReaderFactory, + ForumPostValidator getForumPostValidator( + ValidationManager validationManager, CryptoComponent crypto, + BdfReaderFactory bdfReaderFactory, BdfWriterFactory bdfWriterFactory, ObjectReader<Author> authorReader, MetadataEncoder metadataEncoder, Clock clock) { ForumPostValidator validator = new ForumPostValidator(crypto, bdfReaderFactory, bdfWriterFactory, authorReader, metadataEncoder, clock); - validationManager.registerMessageValidator(CLIENT_ID, validator); + validationManager.registerMessageValidator( + ForumManagerImpl.CLIENT_ID, validator); + return validator; + } + + @Provides @Singleton + ForumListValidator getForumListValidator( + ValidationManager validationManager, + BdfReaderFactory bdfReaderFactory, + MetadataEncoder metadataEncoder) { + ForumListValidator validator = new ForumListValidator(bdfReaderFactory, + metadataEncoder); + validationManager.registerMessageValidator( + ForumSharingManagerImpl.CLIENT_ID, validator); return validator; } + + @Provides @Singleton + ForumSharingManager getForumSharingManager(ContactManager contactManager, + EventBus eventBus, ForumSharingManagerImpl forumSharingManager) { + contactManager.registerAddContactHook(forumSharingManager); + contactManager.registerRemoveContactHook(forumSharingManager); + eventBus.addListener(forumSharingManager); + return forumSharingManager; + } } diff --git a/briar-core/src/org/briarproject/forum/ForumPostValidator.java b/briar-core/src/org/briarproject/forum/ForumPostValidator.java index 1ee770cf1875bc3294df0ee9151cd87891110b80..a21f546252e5c3151e6bada39fe02cf2506ccf9f 100644 --- a/briar-core/src/org/briarproject/forum/ForumPostValidator.java +++ b/briar-core/src/org/briarproject/forum/ForumPostValidator.java @@ -15,6 +15,7 @@ import org.briarproject.api.data.MetadataEncoder; import org.briarproject.api.data.ObjectReader; import org.briarproject.api.db.Metadata; import org.briarproject.api.identity.Author; +import org.briarproject.api.sync.Group; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageValidator; @@ -26,8 +27,6 @@ import java.io.IOException; import java.security.GeneralSecurityException; import java.util.logging.Logger; -import javax.inject.Inject; - import static org.briarproject.api.forum.ForumConstants.MAX_CONTENT_TYPE_LENGTH; import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH; import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; @@ -47,7 +46,6 @@ class ForumPostValidator implements MessageValidator { private final Clock clock; private final KeyParser keyParser; - @Inject ForumPostValidator(CryptoComponent crypto, BdfReaderFactory bdfReaderFactory, BdfWriterFactory bdfWriterFactory, @@ -63,7 +61,7 @@ class ForumPostValidator implements MessageValidator { } @Override - public Metadata validateMessage(Message m) { + public Metadata validateMessage(Message m, Group g) { // Reject the message if it's too far in the future long now = clock.currentTimeMillis(); if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) { @@ -78,8 +76,7 @@ class ForumPostValidator implements MessageValidator { BdfReader r = bdfReaderFactory.createReader(in); MessageId parent = null; Author author = null; - String contentType; - byte[] postBody, sig = null; + byte[] sig = null; r.readListStart(); // Read the parent ID, if any if (r.hasRaw()) { @@ -93,9 +90,9 @@ class ForumPostValidator implements MessageValidator { if (r.hasList()) author = authorReader.readObject(r); else r.readNull(); // Read the content type - contentType = r.readString(MAX_CONTENT_TYPE_LENGTH); + String contentType = r.readString(MAX_CONTENT_TYPE_LENGTH); // Read the forum post body - postBody = r.readRaw(MAX_FORUM_POST_BODY_LENGTH); + byte[] postBody = r.readRaw(MAX_FORUM_POST_BODY_LENGTH); // Read the signature, if any if (r.hasRaw()) sig = r.readRaw(MAX_SIGNATURE_LENGTH); diff --git a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..f415051a1c7c5445f5174a2138bd62071000fcd1 --- /dev/null +++ b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java @@ -0,0 +1,558 @@ +package org.briarproject.forum; + +import com.google.inject.Inject; + +import org.briarproject.api.FormatException; +import org.briarproject.api.contact.Contact; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.contact.ContactManager; +import org.briarproject.api.contact.ContactManager.AddContactHook; +import org.briarproject.api.contact.ContactManager.RemoveContactHook; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfReader; +import org.briarproject.api.data.BdfReaderFactory; +import org.briarproject.api.data.BdfWriter; +import org.briarproject.api.data.BdfWriterFactory; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.data.MetadataParser; +import org.briarproject.api.db.DatabaseComponent; +import org.briarproject.api.db.DatabaseExecutor; +import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Metadata; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.forum.Forum; +import org.briarproject.api.forum.ForumManager; +import org.briarproject.api.forum.ForumSharingManager; +import org.briarproject.api.sync.ClientId; +import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.GroupFactory; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageFactory; +import org.briarproject.api.sync.MessageId; +import org.briarproject.api.sync.PrivateGroupFactory; +import org.briarproject.api.system.Clock; +import org.briarproject.util.StringUtils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH; +import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; + +class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook, + RemoveContactHook, EventListener { + + static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString( + "cd11a5d04dccd9e2931d6fc3df456313" + + "63bb3e9d9d0e9405fccdb051f41f5449")); + + private static final byte[] LOCAL_GROUP_DESCRIPTOR = new byte[0]; + + private static final Logger LOG = + Logger.getLogger(ForumSharingManagerImpl.class.getName()); + + private final DatabaseComponent db; + private final Executor dbExecutor; + private final ContactManager contactManager; + private final ForumManager forumManager; + private final GroupFactory groupFactory; + private final PrivateGroupFactory privateGroupFactory; + private final MessageFactory messageFactory; + private final BdfReaderFactory bdfReaderFactory; + private final BdfWriterFactory bdfWriterFactory; + private final MetadataEncoder metadataEncoder; + private final MetadataParser metadataParser; + private final SecureRandom random; + private final Clock clock; + private final Group localGroup; + + /** Ensures isolation between database reads and writes. */ + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + @Inject + ForumSharingManagerImpl(DatabaseComponent db, + @DatabaseExecutor Executor dbExecutor, + ContactManager contactManager, ForumManager forumManager, + GroupFactory groupFactory, PrivateGroupFactory privateGroupFactory, + MessageFactory messageFactory, BdfReaderFactory bdfReaderFactory, + BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder, + MetadataParser metadataParser, SecureRandom random, Clock clock) { + this.db = db; + this.dbExecutor = dbExecutor; + this.contactManager = contactManager; + this.forumManager = forumManager; + this.groupFactory = groupFactory; + this.privateGroupFactory = privateGroupFactory; + this.messageFactory = messageFactory; + this.bdfReaderFactory = bdfReaderFactory; + this.bdfWriterFactory = bdfWriterFactory; + this.metadataEncoder = metadataEncoder; + this.metadataParser = metadataParser; + this.random = random; + this.clock = clock; + localGroup = groupFactory.createGroup(CLIENT_ID, + LOCAL_GROUP_DESCRIPTOR); + } + + @Override + public void addingContact(ContactId c) { + lock.writeLock().lock(); + try { + // Create a group to share with the contact + Group g = getContactGroup(db.getContact(c)); + // Store the group and share it with the contact + db.addGroup(g); + db.setVisibility(g.getId(), Collections.singletonList(c)); + // Attach the contact ID to the group + BdfDictionary d = new BdfDictionary(); + d.put("contactId", c.getInt()); + db.mergeGroupMetadata(g.getId(), metadataEncoder.encode(d)); + // Share any forums that are shared with all contacts + List<Forum> shared = getForumsSharedWithAllContacts(); + storeMessage(g.getId(), shared, 0); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public void removingContact(ContactId c) { + lock.writeLock().lock(); + try { + db.removeGroup(getContactGroup(db.getContact(c))); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof MessageValidatedEvent) { + MessageValidatedEvent m = (MessageValidatedEvent) e; + ClientId c = m.getClientId(); + if (m.isValid() && !m.isLocal() && c.equals(CLIENT_ID)) + remoteForumsUpdated(m.getMessage().getGroupId()); + } + } + + @Override + public ClientId getClientId() { + return CLIENT_ID; + } + + @Override + public Forum createForum(String name) { + int length = StringUtils.toUtf8(name).length; + if (length == 0) throw new IllegalArgumentException(); + if (length > MAX_FORUM_NAME_LENGTH) + throw new IllegalArgumentException(); + byte[] salt = new byte[FORUM_SALT_LENGTH]; + random.nextBytes(salt); + return createForum(name, salt); + } + + @Override + public void addForum(Forum f) throws DbException { + lock.writeLock().lock(); + try { + db.addGroup(f.getGroup()); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public void removeForum(Forum f) throws DbException { + lock.writeLock().lock(); + try { + // Update the list of forums shared with each contact + for (Contact c : contactManager.getContacts()) { + Group contactGroup = getContactGroup(c); + removeFromList(contactGroup.getId(), f); + } + db.removeGroup(f.getGroup()); + } catch (IOException e) { + throw new DbException(e); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public Collection<Forum> getAvailableForums() throws DbException { + lock.readLock().lock(); + try { + // Get any forums we subscribe to + Set<Group> subscribed = new HashSet<Group>(db.getGroups( + forumManager.getClientId())); + // Get all forums shared by contacts + Set<Forum> available = new HashSet<Forum>(); + for (Contact c : contactManager.getContacts()) { + Group g = getContactGroup(c); + // Find the latest update version + LatestUpdate latest = findLatest(g.getId(), false); + if (latest != null) { + // Retrieve and parse the latest update + byte[] raw = db.getRawMessage(latest.messageId); + for (Forum f : parseForumList(raw)) { + if (!subscribed.contains(f.getGroup())) + available.add(f); + } + } + } + return Collections.unmodifiableSet(available); + } catch (IOException e) { + throw new DbException(e); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Collection<Contact> getSharedBy(GroupId g) throws DbException { + lock.readLock().lock(); + try { + List<Contact> subscribers = new ArrayList<Contact>(); + for (Contact c : contactManager.getContacts()) { + Group contactGroup = getContactGroup(c); + if (listContains(contactGroup.getId(), g, false)) + subscribers.add(c); + } + return Collections.unmodifiableList(subscribers); + } catch (IOException e) { + throw new DbException(e); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Collection<ContactId> getSharedWith(GroupId g) throws DbException { + lock.readLock().lock(); + try { + List<ContactId> shared = new ArrayList<ContactId>(); + for (Contact c : contactManager.getContacts()) { + Group contactGroup = getContactGroup(c); + if (listContains(contactGroup.getId(), g, true)) + shared.add(c.getId()); + } + return Collections.unmodifiableList(shared); + } catch (FormatException e) { + throw new DbException(e); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public void setSharedWith(GroupId g, Collection<ContactId> shared) + throws DbException { + lock.writeLock().lock(); + try { + // Retrieve the forum + Forum f = parseForum(db.getGroup(g)); + // Remove the forum from the list of forums shared with all contacts + removeFromList(localGroup.getId(), f); + // Update the list of forums shared with each contact + shared = new HashSet<ContactId>(shared); + for (Contact c : contactManager.getContacts()) { + Group contactGroup = getContactGroup(c); + if (shared.contains(c.getId())) { + if (addToList(contactGroup.getId(), f)) { + // If the contact is sharing the forum, make it visible + if (listContains(contactGroup.getId(), g, false)) + db.setVisibleToContact(c.getId(), g, true); + } + } else { + removeFromList(contactGroup.getId(), f); + db.setVisibleToContact(c.getId(), g, false); + } + } + } catch (FormatException e) { + throw new DbException(e); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public void setSharedWithAll(GroupId g) throws DbException { + lock.writeLock().lock(); + try { + // Retrieve the forum + Forum f = parseForum(db.getGroup(g)); + // Add the forum to the list of forums shared with all contacts + addToList(localGroup.getId(), f); + // Add the forum to the list of forums shared with each contact + for (Contact c : contactManager.getContacts()) { + Group contactGroup = getContactGroup(c); + if (addToList(contactGroup.getId(), f)) { + // If the contact is sharing the forum, make it visible + if (listContains(contactGroup.getId(), g, false)) + db.setVisibleToContact(getContactId(g), g, true); + } + } + } catch (FormatException e) { + throw new DbException(e); + } finally { + lock.writeLock().unlock(); + } + } + + private Group getContactGroup(Contact c) { + return privateGroupFactory.createPrivateGroup(CLIENT_ID, c); + } + + // Locking: lock.writeLock + private List<Forum> getForumsSharedWithAllContacts() throws DbException, + FormatException { + // Ensure the local group exists + db.addGroup(localGroup); + // Find the latest update in the local group + LatestUpdate latest = findLatest(localGroup.getId(), true); + if (latest == null) return Collections.emptyList(); + // Retrieve and parse the latest update + return parseForumList(db.getRawMessage(latest.messageId)); + } + + // Locking: lock.readLock + private LatestUpdate findLatest(GroupId g, boolean local) + throws DbException, FormatException { + LatestUpdate latest = null; + Map<MessageId, Metadata> metadata = db.getMessageMetadata(g); + for (Entry<MessageId, Metadata> e : metadata.entrySet()) { + BdfDictionary d = metadataParser.parse(e.getValue()); + if (d.getBoolean("local") != local) continue; + long version = d.getInteger("version"); + if (latest == null || version > latest.version) + latest = new LatestUpdate(e.getKey(), version); + } + return latest; + } + + private List<Forum> parseForumList(byte[] raw) throws FormatException { + List<Forum> forums = new ArrayList<Forum>(); + ByteArrayInputStream in = new ByteArrayInputStream(raw, + MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); + BdfReader r = bdfReaderFactory.createReader(in); + try { + r.readListStart(); + r.skipInteger(); // Version + r.readListStart(); + while (!r.hasListEnd()) { + r.readListStart(); + String name = r.readString(MAX_FORUM_NAME_LENGTH); + byte[] salt = r.readRaw(FORUM_SALT_LENGTH); + r.readListEnd(); + forums.add(createForum(name, salt)); + } + r.readListEnd(); + r.readListEnd(); + if (!r.eof()) throw new FormatException(); + return forums; + } catch (FormatException e) { + throw e; + } catch (IOException e) { + // Shouldn't happen with ByteArrayInputStream + throw new RuntimeException(e); + } + } + + // Locking: lock.writeLock + private void storeMessage(GroupId g, List<Forum> forums, long version) + throws DbException, FormatException { + byte[] body = encodeForumList(forums, version); + long now = clock.currentTimeMillis(); + Message m = messageFactory.createMessage(g, now, body); + BdfDictionary d = new BdfDictionary(); + d.put("version", version); + d.put("local", true); + db.addLocalMessage(m, CLIENT_ID, metadataEncoder.encode(d), true); + } + + private byte[] encodeForumList(List<Forum> forums, long version) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BdfWriter w = bdfWriterFactory.createWriter(out); + try { + w.writeListStart(); + w.writeInteger(version); + w.writeListStart(); + for (Forum f : forums) { + w.writeListStart(); + w.writeString(f.getName()); + w.writeRaw(f.getSalt()); + w.writeListEnd(); + } + w.writeListEnd(); + w.writeListEnd(); + } catch (IOException e) { + // Shouldn't happen with ByteArrayOutputStream + throw new RuntimeException(e); + } + return out.toByteArray(); + } + + private void remoteForumsUpdated(final GroupId g) { + dbExecutor.execute(new Runnable() { + public void run() { + lock.writeLock().lock(); + try { + setForumVisibility(getContactId(g), getVisibleForums(g)); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } finally { + lock.writeLock().unlock(); + } + } + }); + } + + // Locking: lock.readLock + private ContactId getContactId(GroupId contactGroupId) throws DbException, + FormatException { + Metadata meta = db.getGroupMetadata(contactGroupId); + BdfDictionary d = metadataParser.parse(meta); + int id = d.getInteger("contactId").intValue(); + return new ContactId(id); + } + + // Locking: lock.readLock + private Set<GroupId> getVisibleForums(GroupId contactGroupId) + throws DbException, FormatException { + // Get the latest local and remote updates + LatestUpdate local = findLatest(contactGroupId, true); + LatestUpdate remote = findLatest(contactGroupId, false); + // If there's no local and/or remote update, no forums are visible + if (local == null || remote == null) return Collections.emptySet(); + // Intersect the sets of shared forums + byte[] localRaw = db.getRawMessage(local.messageId); + Set<Forum> shared = new HashSet<Forum>(parseForumList(localRaw)); + byte[] remoteRaw = db.getRawMessage(remote.messageId); + shared.retainAll(parseForumList(remoteRaw)); + // Forums in the intersection should be visible + Set<GroupId> visible = new HashSet<GroupId>(shared.size()); + for (Forum f : shared) visible.add(f.getId()); + return visible; + } + + // Locking: lock.writeLock + private void setForumVisibility(ContactId c, Set<GroupId> visible) + throws DbException { + for (Group g : db.getGroups(forumManager.getClientId())) { + boolean isVisible = db.isVisibleToContact(c, g.getId()); + boolean shouldBeVisible = visible.contains(g.getId()); + if (isVisible && !shouldBeVisible) + db.setVisibleToContact(c, g.getId(), false); + else if (!isVisible && shouldBeVisible) + db.setVisibleToContact(c, g.getId(), true); + } + } + + private Forum createForum(String name, byte[] salt) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BdfWriter w = bdfWriterFactory.createWriter(out); + try { + w.writeListStart(); + w.writeString(name); + w.writeRaw(salt); + w.writeListEnd(); + } catch (IOException e) { + // Shouldn't happen with ByteArrayOutputStream + throw new RuntimeException(e); + } + Group g = groupFactory.createGroup(forumManager.getClientId(), + out.toByteArray()); + return new Forum(g, name, salt); + } + + private Forum parseForum(Group g) throws FormatException { + ByteArrayInputStream in = new ByteArrayInputStream(g.getDescriptor()); + BdfReader r = bdfReaderFactory.createReader(in); + try { + r.readListStart(); + String name = r.readString(MAX_FORUM_NAME_LENGTH); + byte[] salt = r.readRaw(FORUM_SALT_LENGTH); + r.readListEnd(); + if (!r.eof()) throw new FormatException(); + return new Forum(g, name, salt); + } catch (FormatException e) { + throw e; + } catch (IOException e) { + // Shouldn't happen with ByteArrayInputStream + throw new RuntimeException(e); + } + } + + // Locking: lock.readLock + private boolean listContains(GroupId g, GroupId forum, boolean local) + throws DbException, FormatException { + LatestUpdate latest = findLatest(g, local); + if (latest == null) return false; + List<Forum> list = parseForumList(db.getRawMessage(latest.messageId)); + for (Forum f : list) if (f.getId().equals(forum)) return true; + return false; + } + + // Locking: lock.writeLock + private boolean addToList(GroupId g, Forum f) throws DbException, + FormatException { + LatestUpdate latest = findLatest(g, true); + if (latest == null) { + storeMessage(g, Collections.singletonList(f), 0); + return true; + } + List<Forum> list = parseForumList(db.getRawMessage(latest.messageId)); + if (list.contains(f)) return false; + list.add(f); + storeMessage(g, list, latest.version + 1); + return true; + } + + // Locking: lock.writeLock + private void removeFromList(GroupId g, Forum f) throws DbException, + FormatException { + LatestUpdate latest = findLatest(g, true); + if (latest == null) return; + List<Forum> list = parseForumList(db.getRawMessage(latest.messageId)); + if (list.remove(f)) storeMessage(g, list, latest.version + 1); + } + + private static class LatestUpdate { + + private final MessageId messageId; + private final long version; + + private LatestUpdate(MessageId messageId, long version) { + this.messageId = messageId; + this.version = version; + } + } +} diff --git a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java index 1260fdff1b7c89fa0b76b512e0ad3454db45e8d3..65f0432893f5d5d54c9806719b8d2be007f92cb6 100644 --- a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java +++ b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java @@ -5,6 +5,7 @@ import com.google.inject.Inject; import org.briarproject.api.FormatException; import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; +import org.briarproject.api.contact.ContactManager; import org.briarproject.api.contact.ContactManager.AddContactHook; import org.briarproject.api.contact.ContactManager.RemoveContactHook; import org.briarproject.api.data.BdfDictionary; @@ -50,18 +51,19 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook, Logger.getLogger(MessagingManagerImpl.class.getName()); private final DatabaseComponent db; + private final ContactManager contactManager; private final PrivateGroupFactory privateGroupFactory; private final BdfReaderFactory bdfReaderFactory; private final MetadataEncoder metadataEncoder; private final MetadataParser metadataParser; @Inject - MessagingManagerImpl(DatabaseComponent db, + MessagingManagerImpl(DatabaseComponent db, ContactManager contactManager, PrivateGroupFactory privateGroupFactory, - BdfReaderFactory bdfReaderFactory, - MetadataEncoder metadataEncoder, + BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder, MetadataParser metadataParser) { this.db = db; + this.contactManager = contactManager; this.privateGroupFactory = privateGroupFactory; this.bdfReaderFactory = bdfReaderFactory; this.metadataEncoder = metadataEncoder; @@ -71,8 +73,8 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook, @Override public void addingContact(ContactId c) { try { - // Create the conversation group - Group g = getConversationGroup(db.getContact(c)); + // Create a group to share with the contact + Group g = getContactGroup(db.getContact(c)); // Store the group and share it with the contact db.addGroup(g); db.setVisibility(g.getId(), Collections.singletonList(c)); @@ -87,14 +89,14 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook, } } - private Group getConversationGroup(Contact c) { + private Group getContactGroup(Contact c) { return privateGroupFactory.createPrivateGroup(CLIENT_ID, c); } @Override public void removingContact(ContactId c) { try { - db.removeGroup(getConversationGroup(db.getContact(c))); + db.removeGroup(getContactGroup(db.getContact(c))); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); } @@ -135,7 +137,7 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook, @Override public GroupId getConversationId(ContactId c) throws DbException { - return getConversationGroup(db.getContact(c)).getId(); + return getContactGroup(contactManager.getContact(c)).getId(); } @Override @@ -172,15 +174,16 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook, MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); BdfReader r = bdfReaderFactory.createReader(in); try { - // Extract the private message body r.readListStart(); if (r.hasRaw()) r.skipRaw(); // Parent ID else r.skipNull(); // No parent r.skipString(); // Content type - return r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH); + byte[] messageBody = r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH); + r.readListEnd(); + if (!r.eof()) throw new FormatException(); + return messageBody; } catch (FormatException e) { - // Not a valid private message - throw new IllegalArgumentException(); + throw new DbException(e); } catch (IOException e) { // Shouldn't happen with ByteArrayInputStream throw new RuntimeException(e); diff --git a/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java b/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java index c0134934f1283334830f0910ca152eebb7917148..fa3e0ec5f3d9cd90c2d9cafa1b08089c7db290ca 100644 --- a/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java +++ b/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java @@ -7,6 +7,7 @@ import org.briarproject.api.data.BdfReader; import org.briarproject.api.data.BdfReaderFactory; import org.briarproject.api.data.MetadataEncoder; import org.briarproject.api.db.Metadata; +import org.briarproject.api.sync.Group; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageValidator; @@ -16,8 +17,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.logging.Logger; -import javax.inject.Inject; - import static org.briarproject.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_LENGTH; import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH; import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; @@ -32,7 +31,6 @@ class PrivateMessageValidator implements MessageValidator { private final MetadataEncoder metadataEncoder; private final Clock clock; - @Inject PrivateMessageValidator(BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder, Clock clock) { this.bdfReaderFactory = bdfReaderFactory; @@ -41,7 +39,7 @@ class PrivateMessageValidator implements MessageValidator { } @Override - public Metadata validateMessage(Message m) { + public Metadata validateMessage(Message m, Group g) { // Reject the message if it's too far in the future long now = clock.currentTimeMillis(); if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) { @@ -55,7 +53,6 @@ class PrivateMessageValidator implements MessageValidator { MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); BdfReader r = bdfReaderFactory.createReader(in); MessageId parent = null; - String contentType; r.readListStart(); // Read the parent ID, if any if (r.hasRaw()) { @@ -66,7 +63,7 @@ class PrivateMessageValidator implements MessageValidator { r.readNull(); } // Read the content type - contentType = r.readString(MAX_CONTENT_TYPE_LENGTH); + String contentType = r.readString(MAX_CONTENT_TYPE_LENGTH); // Read the private message body r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH); r.readListEnd(); diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java index 135b74931e109f0f260cbe947903937fb1cb868a..bbc6cd57d16f68433e7debf2e87977ced7094c3e 100644 --- a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java +++ b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java @@ -7,6 +7,7 @@ import org.briarproject.api.FormatException; import org.briarproject.api.TransportId; import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; +import org.briarproject.api.contact.ContactManager; import org.briarproject.api.contact.ContactManager.AddContactHook; import org.briarproject.api.contact.ContactManager.RemoveContactHook; import org.briarproject.api.data.BdfDictionary; @@ -60,6 +61,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, Logger.getLogger(TransportPropertyManagerImpl.class.getName()); private final DatabaseComponent db; + private final ContactManager contactManager; private final PrivateGroupFactory privateGroupFactory; private final MessageFactory messageFactory; private final BdfReaderFactory bdfReaderFactory; @@ -74,11 +76,13 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, @Inject TransportPropertyManagerImpl(DatabaseComponent db, - GroupFactory groupFactory, PrivateGroupFactory privateGroupFactory, + ContactManager contactManager, GroupFactory groupFactory, + PrivateGroupFactory privateGroupFactory, MessageFactory messageFactory, BdfReaderFactory bdfReaderFactory, BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder, MetadataParser metadataParser, Clock clock) { this.db = db; + this.contactManager = contactManager; this.privateGroupFactory = privateGroupFactory; this.messageFactory = messageFactory; this.bdfReaderFactory = bdfReaderFactory; @@ -109,47 +113,12 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); } catch (FormatException e) { - throw new RuntimeException(e); + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); } finally { lock.writeLock().unlock(); } } - private Group getContactGroup(Contact c) { - return privateGroupFactory.createPrivateGroup(CLIENT_ID, c); - } - - private void storeMessage(GroupId g, DeviceId dev, TransportId t, - TransportProperties p, long version, boolean local, boolean shared) - throws DbException, FormatException { - byte[] body = encodeProperties(dev, t, p, version); - long now = clock.currentTimeMillis(); - Message m = messageFactory.createMessage(g, now, body); - BdfDictionary d = new BdfDictionary(); - d.put("transportId", t.getString()); - d.put("version", version); - d.put("local", local); - db.addLocalMessage(m, CLIENT_ID, metadataEncoder.encode(d), shared); - } - - private byte[] encodeProperties(DeviceId dev, TransportId t, - TransportProperties p, long version) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - BdfWriter w = bdfWriterFactory.createWriter(out); - try { - w.writeListStart(); - w.writeRaw(dev.getBytes()); - w.writeString(t.getString()); - w.writeInteger(version); - w.writeDictionary(p); - w.writeListEnd(); - } catch (IOException e) { - // Shouldn't happen with ByteArrayOutputStream - throw new RuntimeException(e); - } - return out.toByteArray(); - } - @Override public void removingContact(ContactId c) { lock.writeLock().lock(); @@ -172,7 +141,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, storeMessage(g.getId(), dev, e.getKey(), e.getValue(), 0, false, false); } - } catch (IOException e) { + } catch (FormatException e) { throw new DbException(e); } finally { lock.writeLock().unlock(); @@ -184,15 +153,15 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, throws DbException { lock.readLock().lock(); try { - // Find the latest local version for each transport + // Find the latest local update for each transport Map<TransportId, LatestUpdate> latest = findLatest(localGroup.getId(), true); - // Retrieve and decode the latest local properties + // Retrieve and parse the latest local properties Map<TransportId, TransportProperties> local = new HashMap<TransportId, TransportProperties>(); for (Entry<TransportId, LatestUpdate> e : latest.entrySet()) { byte[] raw = db.getRawMessage(e.getValue().messageId); - local.put(e.getKey(), decodeProperties(raw)); + local.put(e.getKey(), parseProperties(raw)); } return Collections.unmodifiableMap(local); } catch (NoSuchGroupException e) { @@ -205,53 +174,16 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, } } - private Map<TransportId, LatestUpdate> findLatest(GroupId g, boolean local) - throws DbException, FormatException { - // TODO: Use metadata queries - Map<TransportId, LatestUpdate> latestUpdates = - new HashMap<TransportId, LatestUpdate>(); - Map<MessageId, Metadata> metadata = db.getMessageMetadata(g); - for (Entry<MessageId, Metadata> e : metadata.entrySet()) { - BdfDictionary d = metadataParser.parse(e.getValue()); - if (d.getBoolean("local") != local) continue; - TransportId t = new TransportId(d.getString("transportId")); - long version = d.getInteger("version"); - LatestUpdate latest = latestUpdates.get(t); - if (latest == null || version > latest.version) - latestUpdates.put(t, new LatestUpdate(e.getKey(), version)); - } - return latestUpdates; - } - - private TransportProperties decodeProperties(byte[] raw) - throws IOException { - TransportProperties p = new TransportProperties(); - ByteArrayInputStream in = new ByteArrayInputStream(raw, - MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); - BdfReader r = bdfReaderFactory.createReader(in); - r.readListStart(); - r.skipRaw(); // Device ID - r.skipString(); // Transport ID - r.skipInteger(); // Version - r.readDictionaryStart(); - while (!r.hasDictionaryEnd()) { - String key = r.readString(MAX_PROPERTY_LENGTH); - String value = r.readString(MAX_PROPERTY_LENGTH); - p.put(key, value); - } - return p; - } - @Override public TransportProperties getLocalProperties(TransportId t) throws DbException { lock.readLock().lock(); try { - // Find the latest local version - LatestUpdate latest = findLatest(localGroup.getId(), true).get(t); + // Find the latest local update + LatestUpdate latest = findLatest(localGroup.getId(), t, true); if (latest == null) return null; - // Retrieve and decode the latest local properties - return decodeProperties(db.getRawMessage(latest.messageId)); + // Retrieve and parse the latest local properties + return parseProperties(db.getRawMessage(latest.messageId)); } catch (NoSuchGroupException e) { // Local group doesn't exist - there are no local properties return null; @@ -269,14 +201,14 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, try { Map<ContactId, TransportProperties> remote = new HashMap<ContactId, TransportProperties>(); - for (Contact c : db.getContacts()) { + for (Contact c : contactManager.getContacts()) { Group g = getContactGroup(c); - // Find the latest remote version - LatestUpdate latest = findLatest(g.getId(), false).get(t); + // Find the latest remote update + LatestUpdate latest = findLatest(g.getId(), t, false); if (latest != null) { - // Retrieve and decode the latest remote properties + // Retrieve and parse the latest remote properties byte[] raw = db.getRawMessage(latest.messageId); - remote.put(c.getId(), decodeProperties(raw)); + remote.put(c.getId(), parseProperties(raw)); } } return Collections.unmodifiableMap(remote); @@ -296,12 +228,12 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, db.addGroup(localGroup); // Merge the new properties with any existing properties TransportProperties merged; - LatestUpdate latest = findLatest(localGroup.getId(), true).get(t); + LatestUpdate latest = findLatest(localGroup.getId(), t, true); if (latest == null) { merged = p; } else { byte[] raw = db.getRawMessage(latest.messageId); - TransportProperties old = decodeProperties(raw); + TransportProperties old = parseProperties(raw); merged = new TransportProperties(old); merged.putAll(p); if (merged.equals(old)) return; // Unchanged @@ -312,9 +244,9 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, storeMessage(localGroup.getId(), dev, t, merged, version, true, false); // Store the merged properties in each contact's group - for (Contact c : db.getContacts()) { + for (Contact c : contactManager.getContacts()) { Group g = getContactGroup(c); - latest = findLatest(g.getId(), true).get(t); + latest = findLatest(g.getId(), t, true); version = latest == null ? 1 : latest.version + 1; storeMessage(g.getId(), dev, t, merged, version, true, true); } @@ -325,6 +257,100 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, } } + private Group getContactGroup(Contact c) { + return privateGroupFactory.createPrivateGroup(CLIENT_ID, c); + } + + // Locking: lock.writeLock + private void storeMessage(GroupId g, DeviceId dev, TransportId t, + TransportProperties p, long version, boolean local, boolean shared) + throws DbException, FormatException { + byte[] body = encodeProperties(dev, t, p, version); + long now = clock.currentTimeMillis(); + Message m = messageFactory.createMessage(g, now, body); + BdfDictionary d = new BdfDictionary(); + d.put("transportId", t.getString()); + d.put("version", version); + d.put("local", local); + db.addLocalMessage(m, CLIENT_ID, metadataEncoder.encode(d), shared); + } + + private byte[] encodeProperties(DeviceId dev, TransportId t, + TransportProperties p, long version) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BdfWriter w = bdfWriterFactory.createWriter(out); + try { + w.writeListStart(); + w.writeRaw(dev.getBytes()); + w.writeString(t.getString()); + w.writeInteger(version); + w.writeDictionary(p); + w.writeListEnd(); + } catch (IOException e) { + // Shouldn't happen with ByteArrayOutputStream + throw new RuntimeException(e); + } + return out.toByteArray(); + } + + // Locking: lock.readLock + private Map<TransportId, LatestUpdate> findLatest(GroupId g, boolean local) + throws DbException, FormatException { + Map<TransportId, LatestUpdate> latestUpdates = + new HashMap<TransportId, LatestUpdate>(); + Map<MessageId, Metadata> metadata = db.getMessageMetadata(g); + for (Entry<MessageId, Metadata> e : metadata.entrySet()) { + BdfDictionary d = metadataParser.parse(e.getValue()); + if (d.getBoolean("local") == local) { + TransportId t = new TransportId(d.getString("transportId")); + long version = d.getInteger("version"); + LatestUpdate latest = latestUpdates.get(t); + if (latest == null || version > latest.version) + latestUpdates.put(t, new LatestUpdate(e.getKey(), version)); + } + } + return latestUpdates; + } + + // Locking: lock.readLock + private LatestUpdate findLatest(GroupId g, TransportId t, boolean local) + throws DbException, FormatException { + LatestUpdate latest = null; + Map<MessageId, Metadata> metadata = db.getMessageMetadata(g); + for (Entry<MessageId, Metadata> e : metadata.entrySet()) { + BdfDictionary d = metadataParser.parse(e.getValue()); + if (d.getString("transportId").equals(t.getString()) + && d.getBoolean("local") == local) { + long version = d.getInteger("version"); + if (latest == null || version > latest.version) + latest = new LatestUpdate(e.getKey(), version); + } + } + return latest; + } + + private TransportProperties parseProperties(byte[] raw) + throws IOException { + TransportProperties p = new TransportProperties(); + ByteArrayInputStream in = new ByteArrayInputStream(raw, + MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); + BdfReader r = bdfReaderFactory.createReader(in); + r.readListStart(); + r.skipRaw(); // Device ID + r.skipString(); // Transport ID + r.skipInteger(); // Version + r.readDictionaryStart(); + while (!r.hasDictionaryEnd()) { + String key = r.readString(MAX_PROPERTY_LENGTH); + String value = r.readString(MAX_PROPERTY_LENGTH); + p.put(key, value); + } + r.readDictionaryEnd(); + r.readListEnd(); + if (!r.eof()) throw new FormatException(); + return p; + } + private static class LatestUpdate { private final MessageId messageId; diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java b/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java index eeee9f333447c2489c62e27d42da582eca6bde9c..a3b7c17832ac5997d3ece03b33418e5ebee9d3c7 100644 --- a/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java +++ b/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java @@ -7,6 +7,7 @@ import org.briarproject.api.data.BdfReader; import org.briarproject.api.data.BdfReaderFactory; import org.briarproject.api.data.MetadataEncoder; import org.briarproject.api.db.Metadata; +import org.briarproject.api.sync.Group; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageValidator; import org.briarproject.api.system.Clock; @@ -15,8 +16,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.logging.Logger; -import javax.inject.Inject; - import static org.briarproject.api.TransportId.MAX_TRANSPORT_ID_LENGTH; import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT; import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH; @@ -32,7 +31,6 @@ class TransportPropertyValidator implements MessageValidator { private final MetadataEncoder metadataEncoder; private final Clock clock; - @Inject TransportPropertyValidator(BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder, Clock clock) { this.bdfReaderFactory = bdfReaderFactory; @@ -41,7 +39,7 @@ class TransportPropertyValidator implements MessageValidator { } @Override - public Metadata validateMessage(Message m) { + public Metadata validateMessage(Message m, Group g) { // Reject the message if it's too far in the future long now = clock.currentTimeMillis(); if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) { diff --git a/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java b/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java index dfb93ec3fe7deab3e663d1bf0bf11db715249353..f6ec4779598c335b2493ef1c070fba258978ee5b 100644 --- a/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java +++ b/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java @@ -13,7 +13,6 @@ import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageSharedEvent; import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToRequestEvent; -import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.ShutdownEvent; import org.briarproject.api.event.TransportRemovedEvent; import org.briarproject.api.sync.Ack; @@ -151,9 +150,6 @@ class DuplexOutgoingSession implements SyncSession, EventListener { if (c.getContactId().equals(contactId)) interrupt(); } else if (e instanceof MessageSharedEvent) { dbExecutor.execute(new GenerateOffer()); - } else if (e instanceof MessageValidatedEvent) { - if (((MessageValidatedEvent) e).isValid()) - dbExecutor.execute(new GenerateOffer()); } else if (e instanceof GroupVisibilityUpdatedEvent) { GroupVisibilityUpdatedEvent g = (GroupVisibilityUpdatedEvent) e; if (g.getAffectedContacts().contains(contactId)) diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java index 855f59b0df73b98739ebfe3ce858b3f339478a8f..5fca1458452be5ed09fc776e6de3d3f3b1c1f773 100644 --- a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java +++ b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java @@ -15,6 +15,7 @@ import org.briarproject.api.event.EventListener; import org.briarproject.api.event.MessageAddedEvent; import org.briarproject.api.lifecycle.Service; import org.briarproject.api.sync.ClientId; +import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; @@ -84,7 +85,8 @@ class ValidationManagerImpl implements ValidationManager, Service, for (MessageId id : db.getMessagesToValidate(c)) { try { Message m = parseMessage(id, db.getRawMessage(id)); - validateMessage(m, c); + Group g = db.getGroup(m.getGroupId()); + validateMessage(m, g); } catch (NoSuchMessageException e) { LOG.info("Message removed before validation"); } @@ -106,15 +108,15 @@ class ValidationManagerImpl implements ValidationManager, Service, return new Message(id, new GroupId(groupId), timestamp, raw); } - private void validateMessage(final Message m, final ClientId c) { + private void validateMessage(final Message m, final Group g) { cryptoExecutor.execute(new Runnable() { public void run() { - MessageValidator v = validators.get(c); + MessageValidator v = validators.get(g.getClientId()); if (v == null) { LOG.warning("No validator"); } else { - Metadata meta = v.validateMessage(m); - storeValidationResult(m, c, meta); + Metadata meta = v.validateMessage(m, g); + storeValidationResult(m, g.getClientId(), meta); } } }); @@ -132,6 +134,7 @@ class ValidationManagerImpl implements ValidationManager, Service, hook.validatingMessage(m, c, meta); db.mergeMessageMetadata(m.getId(), meta); db.setMessageValid(m, c, true); + db.setMessageShared(m, true); } } catch (NoSuchMessageException e) { LOG.info("Message removed during validation"); @@ -146,18 +149,17 @@ class ValidationManagerImpl implements ValidationManager, Service, @Override public void eventOccurred(Event e) { if (e instanceof MessageAddedEvent) { - MessageAddedEvent m = (MessageAddedEvent) e; // Validate the message if it wasn't created locally - if (m.getContactId() != null) loadClientId(m.getMessage()); + MessageAddedEvent m = (MessageAddedEvent) e; + if (m.getContactId() != null) loadGroup(m.getMessage()); } } - private void loadClientId(final Message m) { + private void loadGroup(final Message m) { dbExecutor.execute(new Runnable() { public void run() { try { - ClientId c = db.getGroup(m.getGroupId()).getClientId(); - validateMessage(m, c); + validateMessage(m, db.getGroup(m.getGroupId())); } catch (NoSuchGroupException e) { LOG.info("Group removed before validation"); } catch (DbException e) { diff --git a/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java b/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java index 92f2d671751809b3f5c68c588a5c9f910208daf4..0b05dc0120adc42580da2e4d22f5072bfe97f5e9 100644 --- a/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java +++ b/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java @@ -2,7 +2,6 @@ package org.briarproject.plugins; import com.google.inject.Provides; -import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.lifecycle.IoExecutor; import org.briarproject.api.lifecycle.ShutdownManager; import org.briarproject.api.plugins.duplex.DuplexPluginConfig; @@ -16,6 +15,7 @@ import org.briarproject.plugins.modem.ModemPluginFactory; import org.briarproject.plugins.tcp.LanTcpPluginFactory; import org.briarproject.plugins.tcp.WanTcpPluginFactory; +import java.security.SecureRandom; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -39,10 +39,10 @@ public class DesktopPluginsModule extends PluginsModule { @Provides DuplexPluginConfig getDuplexPluginConfig(@IoExecutor Executor ioExecutor, - CryptoComponent crypto, ReliabilityLayerFactory reliabilityFactory, + SecureRandom random, ReliabilityLayerFactory reliabilityFactory, ShutdownManager shutdownManager) { - DuplexPluginFactory bluetooth = new BluetoothPluginFactory( - ioExecutor, crypto.getSecureRandom()); + DuplexPluginFactory bluetooth = new BluetoothPluginFactory(ioExecutor, + random); DuplexPluginFactory modem = new ModemPluginFactory(ioExecutor, reliabilityFactory); DuplexPluginFactory lan = new LanTcpPluginFactory(ioExecutor); diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java index 238b62970b05dd3fcda690283a7aa81545117332..b0930805f3613372bff049863e98c2ac5bf43015 100644 --- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java +++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java @@ -20,6 +20,7 @@ import org.briarproject.api.event.GroupRemovedEvent; import org.briarproject.api.event.GroupVisibilityUpdatedEvent; import org.briarproject.api.event.MessageAddedEvent; import org.briarproject.api.event.MessageRequestedEvent; +import org.briarproject.api.event.MessageSharedEvent; import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToRequestEvent; import org.briarproject.api.event.MessageValidatedEvent; @@ -246,6 +247,7 @@ public class DatabaseComponentImplTest extends BriarTestCase { // The message was added, so the listeners should be called oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class))); oneOf(eventBus).broadcast(with(any(MessageValidatedEvent.class))); + oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class))); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, shutdown); @@ -265,11 +267,11 @@ public class DatabaseComponentImplTest extends BriarTestCase { final EventBus eventBus = context.mock(EventBus.class); context.checking(new Expectations() {{ // Check whether the contact is in the DB (which it's not) - exactly(15).of(database).startTransaction(); + exactly(17).of(database).startTransaction(); will(returnValue(txn)); - exactly(15).of(database).containsContact(txn, contactId); + exactly(17).of(database).containsContact(txn, contactId); will(returnValue(false)); - exactly(15).of(database).abortTransaction(txn); + exactly(17).of(database).abortTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, shutdown); @@ -337,6 +339,13 @@ public class DatabaseComponentImplTest extends BriarTestCase { // Expected } + try { + db.isVisibleToContact(contactId, groupId); + fail(); + } catch (NoSuchContactException expected) { + // Expected + } + try { Ack a = new Ack(Collections.singletonList(messageId)); db.receiveAck(contactId, a); @@ -382,6 +391,13 @@ public class DatabaseComponentImplTest extends BriarTestCase { // Expected } + try { + db.setVisibleToContact(contactId, groupId, true); + fail(); + } catch (NoSuchContactException expected) { + // Expected + } + context.assertIsSatisfied(); } @@ -438,13 +454,14 @@ public class DatabaseComponentImplTest extends BriarTestCase { final EventBus eventBus = context.mock(EventBus.class); context.checking(new Expectations() {{ // Check whether the group is in the DB (which it's not) - exactly(7).of(database).startTransaction(); + exactly(9).of(database).startTransaction(); will(returnValue(txn)); - exactly(7).of(database).containsGroup(txn, groupId); + exactly(9).of(database).containsGroup(txn, groupId); will(returnValue(false)); - exactly(7).of(database).abortTransaction(txn); - // This is needed for getMessageStatus() to proceed - exactly(1).of(database).containsContact(txn, contactId); + exactly(9).of(database).abortTransaction(txn); + // This is needed for getMessageStatus(), isVisibleToContact(), and + // setVisibleToContact() to proceed + exactly(3).of(database).containsContact(txn, contactId); will(returnValue(true)); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, @@ -478,6 +495,13 @@ public class DatabaseComponentImplTest extends BriarTestCase { // Expected } + try { + db.isVisibleToContact(contactId, groupId); + fail(); + } catch (NoSuchGroupException expected) { + // Expected + } + try { db.mergeGroupMetadata(groupId, metadata); fail(); @@ -499,6 +523,13 @@ public class DatabaseComponentImplTest extends BriarTestCase { // Expected } + try { + db.setVisibleToContact(contactId, groupId, true); + fail(); + } catch (NoSuchGroupException expected) { + // Expected + } + context.assertIsSatisfied(); } @@ -847,7 +878,7 @@ public class DatabaseComponentImplTest extends BriarTestCase { will(returnValue(false)); oneOf(database).containsVisibleGroup(txn, contactId, groupId); will(returnValue(true)); - oneOf(database).addMessage(txn, message, UNKNOWN, true); + oneOf(database).addMessage(txn, message, UNKNOWN, false); oneOf(database).getVisibility(txn, groupId); will(returnValue(Collections.singletonList(contactId))); oneOf(database).getContactIds(txn); @@ -1019,7 +1050,6 @@ public class DatabaseComponentImplTest extends BriarTestCase { oneOf(database).getContactIds(txn); will(returnValue(both)); oneOf(database).removeVisibility(txn, contactId1, groupId); - oneOf(database).setVisibleToAll(txn, groupId, false); oneOf(database).commitTransaction(txn); oneOf(eventBus).broadcast(with(any( GroupVisibilityUpdatedEvent.class))); @@ -1051,7 +1081,6 @@ public class DatabaseComponentImplTest extends BriarTestCase { will(returnValue(both)); oneOf(database).getContactIds(txn); will(returnValue(both)); - oneOf(database).setVisibleToAll(txn, groupId, false); oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, @@ -1062,55 +1091,6 @@ public class DatabaseComponentImplTest extends BriarTestCase { context.assertIsSatisfied(); } - @Test - public void testSettingVisibleToAllAffectsCurrentContacts() - throws Exception { - final ContactId contactId1 = new ContactId(123); - final Collection<ContactId> both = Arrays.asList(contactId, contactId1); - Mockery context = new Mockery(); - @SuppressWarnings("unchecked") - final Database<Object> database = context.mock(Database.class); - final ShutdownManager shutdown = context.mock(ShutdownManager.class); - final EventBus eventBus = context.mock(EventBus.class); - context.checking(new Expectations() {{ - // setVisibility() - oneOf(database).startTransaction(); - will(returnValue(txn)); - oneOf(database).containsGroup(txn, groupId); - will(returnValue(true)); - oneOf(database).getVisibility(txn, groupId); - will(returnValue(Collections.emptyList())); - oneOf(database).getContactIds(txn); - will(returnValue(both)); - oneOf(database).addVisibility(txn, contactId, groupId); - oneOf(database).setVisibleToAll(txn, groupId, false); - oneOf(database).commitTransaction(txn); - oneOf(eventBus).broadcast(with(any( - GroupVisibilityUpdatedEvent.class))); - // setVisibleToAll() - oneOf(database).startTransaction(); - will(returnValue(txn)); - oneOf(database).containsGroup(txn, groupId); - will(returnValue(true)); - oneOf(database).setVisibleToAll(txn, groupId, true); - oneOf(database).getVisibility(txn, groupId); - will(returnValue(Collections.singletonList(contactId))); - oneOf(database).getContactIds(txn); - will(returnValue(both)); - oneOf(database).addVisibility(txn, contactId1, groupId); - oneOf(database).commitTransaction(txn); - oneOf(eventBus).broadcast(with(any( - GroupVisibilityUpdatedEvent.class))); - }}); - DatabaseComponent db = createDatabaseComponent(database, eventBus, - shutdown); - - db.setVisibility(groupId, Collections.singletonList(contactId)); - db.setVisibleToAll(groupId, true); - - context.assertIsSatisfied(); - } - @Test public void testTransportKeys() throws Exception { final TransportKeys keys = createTransportKeys(); diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java index cce4c883470f35b5c781ecbdb6b5ed4bf20400c5..2fe48c6c3e4f2240e5d00c09431581b400cc2882 100644 --- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java +++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java @@ -205,7 +205,7 @@ public class H2DatabaseTest extends BriarTestCase { Database<Connection> db = open(false); Connection txn = db.startTransaction(); - // Add a contact, subscribe to a group and store an unvalidated message + // Add a contact, a group and an unvalidated message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); db.addGroup(txn, group); @@ -243,7 +243,7 @@ public class H2DatabaseTest extends BriarTestCase { Database<Connection> db = open(false); Connection txn = db.startTransaction(); - // Add a contact, subscribe to a group and store an unshared message + // Add a contact, a group and an unshared message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); db.addGroup(txn, group); @@ -329,7 +329,7 @@ public class H2DatabaseTest extends BriarTestCase { ids = db.getMessagesToOffer(txn, contactId, 100); assertEquals(Collections.singletonList(messageId), ids); - // Making the subscription invisible should make the message unsendable + // Making the group invisible should make the message unsendable db.removeVisibility(txn, contactId, groupId); ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); assertTrue(ids.isEmpty()); @@ -1017,6 +1017,31 @@ public class H2DatabaseTest extends BriarTestCase { db.close(); } + @Test + public void testGroupsVisibleToContacts() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and a group + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId)); + db.addGroup(txn, group); + + // The group should not be visible to the contact + assertFalse(db.containsVisibleGroup(txn, contactId, groupId)); + + // Make the group visible to the contact + db.addVisibility(txn, contactId, groupId); + assertTrue(db.containsVisibleGroup(txn, contactId, groupId)); + + // Make the group invisible to the contact + db.removeVisibility(txn, contactId, groupId); + assertFalse(db.containsVisibleGroup(txn, contactId, groupId)); + + db.commitTransaction(txn); + db.close(); + } + @Test public void testDifferentLocalPseudonymsCanHaveTheSameContact() throws Exception { diff --git a/briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java b/briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e10623d584a3e65ed4e662d17b43b4ac672569d2 --- /dev/null +++ b/briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java @@ -0,0 +1,14 @@ +package org.briarproject.forum; + +import org.briarproject.BriarTestCase; +import org.junit.Test; + +import static org.junit.Assert.fail; + +public class ForumListValidatorTest extends BriarTestCase { + + @Test + public void testUnitTestsExist() { + fail(); // FIXME: Write tests + } +} diff --git a/briar-tests/src/org/briarproject/forum/ForumSharingManagerImplTest.java b/briar-tests/src/org/briarproject/forum/ForumSharingManagerImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..07fc1ad80a77dacffcdf1093c7f09a954c1fffe1 --- /dev/null +++ b/briar-tests/src/org/briarproject/forum/ForumSharingManagerImplTest.java @@ -0,0 +1,14 @@ +package org.briarproject.forum; + +import org.briarproject.BriarTestCase; +import org.junit.Test; + +import static org.junit.Assert.fail; + +public class ForumSharingManagerImplTest extends BriarTestCase { + + @Test + public void testUnitTestsExist() { + fail(); // FIXME: Write tests + } +}