Moved subscription updates to the client layer.

This commit is contained in:
akwizgran
2016-02-01 15:13:36 +00:00
parent 54272c8836
commit 18db17bf5b
30 changed files with 1286 additions and 646 deletions

View File

@@ -17,11 +17,10 @@ import org.briarproject.api.db.NoSuchGroupException;
import org.briarproject.api.event.Event; import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener; import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.GroupAddedEvent; import org.briarproject.api.event.MessageValidatedEvent;
import org.briarproject.api.event.GroupRemovedEvent;
import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumSharingManager;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.ClientId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@@ -44,7 +43,7 @@ implements EventListener, OnItemClickListener {
private ListView list = null; private ListView list = null;
// Fields that are accessed from background threads must be volatile // 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; @Inject private volatile EventBus eventBus;
@Override @Override
@@ -76,11 +75,10 @@ implements EventListener, OnItemClickListener {
Collection<ForumContacts> available = Collection<ForumContacts> available =
new ArrayList<ForumContacts>(); new ArrayList<ForumContacts>();
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
for (Forum f : forumManager.getAvailableForums()) { for (Forum f : forumSharingManager.getAvailableForums()) {
try { try {
GroupId id = f.getId();
Collection<Contact> c = Collection<Contact> c =
forumManager.getSubscribers(id); forumSharingManager.getSharedBy(f.getId());
available.add(new ForumContacts(f, c)); available.add(new ForumContacts(f, c));
} catch (NoSuchGroupException e) { } catch (NoSuchGroupException e) {
// Continue // Continue
@@ -122,17 +120,12 @@ implements EventListener, OnItemClickListener {
} }
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
// TODO: What other events are needed here? if (e instanceof MessageValidatedEvent) {
if (e instanceof GroupAddedEvent) { MessageValidatedEvent m = (MessageValidatedEvent) e;
GroupAddedEvent g = (GroupAddedEvent) e; ClientId c = m.getClientId();
if (g.getGroup().getClientId().equals(forumManager.getClientId())) { if (m.isValid() && !m.isLocal()
LOG.info("Forum added, reloading"); && c.equals(forumSharingManager.getClientId())) {
loadForums(); LOG.info("Available forums updated, reloading");
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
LOG.info("Forum removed, reloading");
loadForums(); loadForums();
} }
} }
@@ -141,20 +134,20 @@ implements EventListener, OnItemClickListener {
public void onItemClick(AdapterView<?> parent, View view, int position, public void onItemClick(AdapterView<?> parent, View view, int position,
long id) { long id) {
AvailableForumsItem item = adapter.getItem(position); AvailableForumsItem item = adapter.getItem(position);
Collection<ContactId> visible = new ArrayList<ContactId>(); Collection<ContactId> shared = new ArrayList<ContactId>();
for (Contact c : item.getContacts()) visible.add(c.getId()); for (Contact c : item.getContacts()) shared.add(c.getId());
addSubscription(item.getForum(), visible); subscribe(item.getForum(), shared);
String subscribed = getString(R.string.subscribed_toast); String subscribed = getString(R.string.subscribed_toast);
Toast.makeText(this, subscribed, LENGTH_SHORT).show(); Toast.makeText(this, subscribed, LENGTH_SHORT).show();
loadForums();
} }
private void addSubscription(final Forum f, private void subscribe(final Forum f, final Collection<ContactId> shared) {
final Collection<ContactId> visible) {
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
public void run() { public void run() {
try { try {
forumManager.addForum(f); forumSharingManager.addForum(f);
forumManager.setVisibility(f.getId(), visible); forumSharingManager.setSharedWith(f.getId(), shared);
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e); LOG.log(WARNING, e.toString(), e);

View File

@@ -18,7 +18,7 @@ import org.briarproject.android.BriarActivity;
import org.briarproject.android.util.LayoutUtils; import org.briarproject.android.util.LayoutUtils;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumSharingManager;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -51,7 +51,7 @@ implements OnEditorActionListener, OnClickListener {
private TextView feedback = null; private TextView feedback = null;
// Fields that are accessed from background threads must be volatile // Fields that are accessed from background threads must be volatile
@Inject private volatile ForumManager forumManager; @Inject private volatile ForumSharingManager forumSharingManager;
@Override @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
@@ -138,8 +138,8 @@ implements OnEditorActionListener, OnClickListener {
public void run() { public void run() {
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
Forum f = forumManager.createForum(name); Forum f = forumSharingManager.createForum(name);
forumManager.addForum(f); forumSharingManager.addForum(f);
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Storing forum took " + duration + " ms"); LOG.info("Storing forum took " + duration + " ms");

View File

@@ -25,6 +25,7 @@ import org.briarproject.android.util.LayoutUtils;
import org.briarproject.android.util.ListLoadingProgressBar; import org.briarproject.android.util.ListLoadingProgressBar;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchGroupException; import org.briarproject.api.db.NoSuchGroupException;
import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.Event; import org.briarproject.api.event.Event;
import org.briarproject.api.event.GroupAddedEvent; import org.briarproject.api.event.GroupAddedEvent;
import org.briarproject.api.event.GroupRemovedEvent; 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.Forum;
import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumManager;
import org.briarproject.api.forum.ForumPostHeader; import org.briarproject.api.forum.ForumPostHeader;
import org.briarproject.api.forum.ForumSharingManager;
import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
@@ -81,8 +83,8 @@ public class ForumListFragment extends BaseEventFragment implements
private ImageButton newForumButton = null; private ImageButton newForumButton = null;
// Fields that are accessed from background threads must be volatile // Fields that are accessed from background threads must be volatile
@Inject @Inject private volatile ForumManager forumManager;
private volatile ForumManager forumManager; @Inject private volatile ForumSharingManager forumSharingManager;
@Nullable @Nullable
@Override @Override
@@ -171,7 +173,8 @@ public class ForumListFragment extends BaseEventFragment implements
// Continue // Continue
} }
} }
int available = forumManager.getAvailableForums().size(); int available =
forumSharingManager.getAvailableForums().size();
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Full load took " + duration + " ms"); LOG.info("Full load took " + duration + " ms");
@@ -252,14 +255,9 @@ public class ForumListFragment extends BaseEventFragment implements
} }
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
// TODO: What other events are needed here? if (e instanceof ContactRemovedEvent) {
if (e instanceof MessageValidatedEvent) { LOG.info("Contact removed, reloading");
MessageValidatedEvent m = (MessageValidatedEvent) e; loadAvailable();
ClientId c = m.getClientId();
if (m.isValid() && c.equals(forumManager.getClientId())) {
LOG.info("Message added, reloading");
loadHeaders(m.getMessage().getGroupId());
}
} else if (e instanceof GroupAddedEvent) { } else if (e instanceof GroupAddedEvent) {
GroupAddedEvent g = (GroupAddedEvent) e; GroupAddedEvent g = (GroupAddedEvent) e;
if (g.getGroup().getClientId().equals(forumManager.getClientId())) { if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
@@ -272,6 +270,18 @@ public class ForumListFragment extends BaseEventFragment implements
LOG.info("Forum removed, reloading"); LOG.info("Forum removed, reloading");
loadHeaders(); 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() { public void run() {
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
int available = forumManager.getAvailableForums().size(); int available =
forumSharingManager.getAvailableForums().size();
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Loading available took " + duration + " ms"); LOG.info("Loading available took " + duration + " ms");
@@ -363,22 +374,22 @@ public class ForumListFragment extends BaseEventFragment implements
ContextMenuInfo info = menuItem.getMenuInfo(); ContextMenuInfo info = menuItem.getMenuInfo();
int position = ((AdapterContextMenuInfo) info).position; int position = ((AdapterContextMenuInfo) info).position;
ForumListItem item = adapter.getItem(position); ForumListItem item = adapter.getItem(position);
removeSubscription(item.getForum()); unsubscribe(item.getForum());
String unsubscribed = getString(R.string.unsubscribed_toast); String unsubscribed = getString(R.string.unsubscribed_toast);
Toast.makeText(getContext(), unsubscribed, LENGTH_SHORT).show(); Toast.makeText(getContext(), unsubscribed, LENGTH_SHORT).show();
} }
return true; return true;
} }
private void removeSubscription(final Forum f) { private void unsubscribe(final Forum f) {
listener.runOnDbThread(new Runnable() { listener.runOnDbThread(new Runnable() {
public void run() { public void run() {
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
forumManager.removeForum(f); forumSharingManager.removeForum(f);
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Removing group took " + duration + " ms"); LOG.info("Removing forum took " + duration + " ms");
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e); LOG.log(WARNING, e.toString(), e);

View File

@@ -19,7 +19,7 @@ import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager; import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.db.DbException; 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 org.briarproject.api.sync.GroupId;
import java.util.Collection; import java.util.Collection;
@@ -52,7 +52,7 @@ SelectContactsDialog.Listener {
// Fields that are accessed from background threads must be volatile // Fields that are accessed from background threads must be volatile
@Inject private volatile ContactManager contactManager; @Inject private volatile ContactManager contactManager;
@Inject private volatile ForumManager forumManager; @Inject private volatile ForumSharingManager forumSharingManager;
private volatile GroupId groupId = null; private volatile GroupId groupId = null;
private volatile Collection<Contact> contacts = null; private volatile Collection<Contact> contacts = null;
private volatile Collection<ContactId> selected = null; private volatile Collection<ContactId> selected = null;
@@ -139,7 +139,7 @@ SelectContactsDialog.Listener {
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
contacts = contactManager.getContacts(); contacts = contactManager.getContacts();
selected = forumManager.getVisibility(groupId); selected = forumSharingManager.getSharedWith(groupId);
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms"); LOG.info("Load took " + duration + " ms");
@@ -175,8 +175,9 @@ SelectContactsDialog.Listener {
public void run() { public void run() {
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
forumManager.setVisibleToAll(groupId, all); if (all) forumSharingManager.setSharedWithAll(groupId);
if (!all) forumManager.setVisibility(groupId, selected); else forumSharingManager.setSharedWith(groupId,
selected);
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Update took " + duration + " ms"); LOG.info("Update took " + duration + " ms");

View File

@@ -1,27 +1,27 @@
package org.briarproject.plugins; package org.briarproject.plugins;
import java.util.Arrays; import android.app.Application;
import java.util.Collection; import android.content.Context;
import java.util.Collections;
import java.util.concurrent.Executor; import com.google.inject.Provides;
import org.briarproject.api.android.AndroidExecutor; 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.lifecycle.IoExecutor;
import org.briarproject.api.plugins.duplex.DuplexPluginConfig; import org.briarproject.api.plugins.duplex.DuplexPluginConfig;
import org.briarproject.api.plugins.duplex.DuplexPluginFactory; import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
import org.briarproject.api.plugins.simplex.SimplexPluginConfig; import org.briarproject.api.plugins.simplex.SimplexPluginConfig;
import org.briarproject.api.plugins.simplex.SimplexPluginFactory; import org.briarproject.api.plugins.simplex.SimplexPluginFactory;
import org.briarproject.api.system.LocationUtils; import org.briarproject.api.system.LocationUtils;
import org.briarproject.api.event.EventBus;
import org.briarproject.plugins.droidtooth.DroidtoothPluginFactory; import org.briarproject.plugins.droidtooth.DroidtoothPluginFactory;
import org.briarproject.plugins.tcp.AndroidLanTcpPluginFactory; import org.briarproject.plugins.tcp.AndroidLanTcpPluginFactory;
import org.briarproject.plugins.tor.TorPluginFactory; import org.briarproject.plugins.tor.TorPluginFactory;
import android.app.Application; import java.security.SecureRandom;
import android.content.Context; import java.util.Arrays;
import java.util.Collection;
import com.google.inject.Provides; import java.util.Collections;
import java.util.concurrent.Executor;
public class AndroidPluginsModule extends PluginsModule { public class AndroidPluginsModule extends PluginsModule {
@@ -37,11 +37,11 @@ public class AndroidPluginsModule extends PluginsModule {
@Provides @Provides
DuplexPluginConfig getDuplexPluginConfig(@IoExecutor Executor ioExecutor, DuplexPluginConfig getDuplexPluginConfig(@IoExecutor Executor ioExecutor,
AndroidExecutor androidExecutor, Application app, AndroidExecutor androidExecutor, Application app,
CryptoComponent crypto, LocationUtils locationUtils, SecureRandom random, LocationUtils locationUtils,
EventBus eventBus) { EventBus eventBus) {
Context appContext = app.getApplicationContext(); Context appContext = app.getApplicationContext();
DuplexPluginFactory bluetooth = new DroidtoothPluginFactory(ioExecutor, DuplexPluginFactory bluetooth = new DroidtoothPluginFactory(ioExecutor,
androidExecutor, appContext, crypto.getSecureRandom()); androidExecutor, appContext, random);
DuplexPluginFactory tor = new TorPluginFactory(ioExecutor, appContext, DuplexPluginFactory tor = new TorPluginFactory(ioExecutor, appContext,
locationUtils, eventBus); locationUtils, eventBus);
DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor, DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor,

View File

@@ -179,6 +179,9 @@ public interface DatabaseComponent {
void incrementStreamCounter(ContactId c, TransportId t, long rotationPeriod) void incrementStreamCounter(ContactId c, TransportId t, long rotationPeriod)
throws DbException; 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 * Merges the given metadata with the existing metadata for the given
* group. * group.
@@ -246,16 +249,14 @@ public interface DatabaseComponent {
/** /**
* Makes a group visible to the given set of contacts and invisible to any * 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) void setVisibility(GroupId g, Collection<ContactId> visible)
throws DbException; throws DbException;
/** /** Makes a group visible or invisible to a contact. */
* Makes a group visible to all current and future contacts, or invisible void setVisibleToContact(ContactId c, GroupId g, boolean visible)
* to future contacts. throws DbException;
*/
void setVisibleToAll(GroupId g, boolean all) throws DbException;
/** /**
* Stores the given transport keys, deleting any keys they have replaced. * Stores the given transport keys, deleting any keys they have replaced.

View File

@@ -7,10 +7,12 @@ public class Forum {
private final Group group; private final Group group;
private final String name; 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.group = group;
this.name = name; this.name = name;
this.salt = salt;
} }
public GroupId getId() { public GroupId getId() {
@@ -25,6 +27,10 @@ public class Forum {
return name; return name;
} }
public byte[] getSalt() {
return salt;
}
@Override @Override
public int hashCode() { public int hashCode() {
return group.hashCode(); return group.hashCode();

View File

@@ -1,17 +1,16 @@
package org.briarproject.api.forum; 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; import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
public interface ForumConstants { public interface ForumConstants {
/** The maximum length of a forum's name in bytes. */ /** The maximum length of a forum's name in UTF-8 bytes. */
int MAX_FORUM_NAME_LENGTH = MAX_GROUP_DESCRIPTOR_LENGTH - 10; int MAX_FORUM_NAME_LENGTH = 100;
/** The length of a forum's random salt in bytes. */ /** The length of a forum's random salt in bytes. */
int FORUM_SALT_LENGTH = 32; 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; int MAX_CONTENT_TYPE_LENGTH = 50;
/** The maximum length of a forum post's body in bytes. */ /** The maximum length of a forum post's body in bytes. */

View File

@@ -1,7 +1,5 @@
package org.briarproject.api.forum; 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.db.DbException;
import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
@@ -14,18 +12,9 @@ public interface ForumManager {
/** Returns the unique ID of the forum client. */ /** Returns the unique ID of the forum client. */
ClientId getClientId(); 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. */ /** Stores a local forum post. */
void addLocalPost(ForumPost p) throws DbException; 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. */ /** Returns the forum with the given ID. */
Forum getForum(GroupId g) throws DbException; Forum getForum(GroupId g) throws DbException;
@@ -38,28 +27,6 @@ public interface ForumManager {
/** Returns the headers of all posts in the given forum. */ /** Returns the headers of all posts in the given forum. */
Collection<ForumPostHeader> getPostHeaders(GroupId g) throws DbException; 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. */ /** Marks a forum post as read or unread. */
void setReadFlag(MessageId m, boolean read) throws DbException; 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;
} }

View File

@@ -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;
}

View File

@@ -8,5 +8,5 @@ public interface MessageValidator {
* Validates the given message and returns its metadata if the message * Validates the given message and returns its metadata if the message
* is valid, or null if the message is invalid. * is valid, or null if the message is invalid.
*/ */
Metadata validateMessage(Message m); Metadata validateMessage(Message m, Group g);
} }

View File

@@ -598,13 +598,6 @@ interface Database<T> {
void setReorderingWindow(T txn, ContactId c, TransportId t, void setReorderingWindow(T txn, ContactId c, TransportId t,
long rotationPeriod, long base, byte[] bitmap) throws DbException; 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 * Updates the transmission count and expiry time of the given message
* with respect to the given contact, using the latency of the transport * with respect to the given contact, using the latency of the transport

View File

@@ -223,6 +223,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
if (added) { if (added) {
eventBus.broadcast(new MessageAddedEvent(m, null)); eventBus.broadcast(new MessageAddedEvent(m, null));
eventBus.broadcast(new MessageValidatedEvent(m, c, true, true)); 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) public void mergeGroupMetadata(GroupId g, Metadata meta)
throws DbException { throws DbException {
lock.writeLock().lock(); lock.writeLock().lock();
@@ -900,7 +923,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
duplicate = db.containsMessage(txn, m.getId()); duplicate = db.containsMessage(txn, m.getId());
visible = db.containsVisibleGroup(txn, c, m.getGroupId()); visible = db.containsVisibleGroup(txn, c, m.getGroupId());
if (visible) { 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.raiseAckFlag(txn, c, m.getId());
} }
db.commitTransaction(txn); db.commitTransaction(txn);
@@ -1162,7 +1185,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
if (!db.containsGroup(txn, g)) if (!db.containsGroup(txn, g))
throw new NoSuchGroupException(); throw new NoSuchGroupException();
// Use HashSets for O(1) lookups, O(n) overall running time // 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); Collection<ContactId> before = db.getVisibility(txn, g);
before = new HashSet<ContactId>(before); before = new HashSet<ContactId>(before);
// Set the group's visibility for each current contact // Set the group's visibility for each current contact
@@ -1177,8 +1200,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
affected.add(c); affected.add(c);
} }
} }
// Make the group invisible to future contacts
db.setVisibleToAll(txn, g, false);
db.commitTransaction(txn); db.commitTransaction(txn);
} catch (DbException e) { } catch (DbException e) {
db.abortTransaction(txn); db.abortTransaction(txn);
@@ -1191,27 +1212,20 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
eventBus.broadcast(new GroupVisibilityUpdatedEvent(affected)); eventBus.broadcast(new GroupVisibilityUpdatedEvent(affected));
} }
public void setVisibleToAll(GroupId g, boolean all) throws DbException { public void setVisibleToContact(ContactId c, GroupId g, boolean visible)
Collection<ContactId> affected = new ArrayList<ContactId>(); throws DbException {
boolean wasVisible = false;
lock.writeLock().lock(); lock.writeLock().lock();
try { try {
T txn = db.startTransaction(); T txn = db.startTransaction();
try { try {
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
if (!db.containsGroup(txn, g)) if (!db.containsGroup(txn, g))
throw new NoSuchGroupException(); throw new NoSuchGroupException();
// Make the group visible or invisible to future contacts wasVisible = db.containsVisibleGroup(txn, c, g);
db.setVisibleToAll(txn, g, all); if (visible && !wasVisible) db.addVisibility(txn, c, g);
if (all) { else if (!visible && wasVisible) db.removeVisibility(txn, c, g);
// 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);
}
}
}
db.commitTransaction(txn); db.commitTransaction(txn);
} catch (DbException e) { } catch (DbException e) {
db.abortTransaction(txn); db.abortTransaction(txn);
@@ -1220,8 +1234,10 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} finally { } finally {
lock.writeLock().unlock(); lock.writeLock().unlock();
} }
if (!affected.isEmpty()) if (visible != wasVisible) {
eventBus.broadcast(new GroupVisibilityUpdatedEvent(affected)); eventBus.broadcast(new GroupVisibilityUpdatedEvent(
Collections.singletonList(c)));
}
} }
public void updateTransportKeys(Map<ContactId, TransportKeys> keys) public void updateTransportKeys(Map<ContactId, TransportKeys> keys)

View File

@@ -66,8 +66,8 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry;
*/ */
abstract class JdbcDatabase implements Database<Connection> { abstract class JdbcDatabase implements Database<Connection> {
private static final int SCHEMA_VERSION = 19; private static final int SCHEMA_VERSION = 20;
private static final int MIN_SCHEMA_VERSION = 19; private static final int MIN_SCHEMA_VERSION = 20;
private static final String CREATE_SETTINGS = private static final String CREATE_SETTINGS =
"CREATE TABLE settings" "CREATE TABLE settings"
@@ -104,7 +104,6 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " (groupId HASH NOT NULL," + " (groupId HASH NOT NULL,"
+ " clientId HASH NOT NULL," + " clientId HASH NOT NULL,"
+ " descriptor BINARY NOT NULL," + " descriptor BINARY NOT NULL,"
+ " visibleToAll BOOLEAN NOT NULL,"
+ " PRIMARY KEY (groupId))"; + " PRIMARY KEY (groupId))";
private static final String CREATE_GROUP_METADATA = private static final String CREATE_GROUP_METADATA =
@@ -511,30 +510,6 @@ abstract class JdbcDatabase implements Database<Connection> {
if (rows != 1) throw new DbStateException(); if (rows != 1) throw new DbStateException();
ps.close(); 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; return c;
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(rs); tryToClose(rs);
@@ -546,9 +521,8 @@ abstract class JdbcDatabase implements Database<Connection> {
public void addGroup(Connection txn, Group g) throws DbException { public void addGroup(Connection txn, Group g) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
String sql = "INSERT INTO groups" String sql = "INSERT INTO groups (groupId, clientId, descriptor)"
+ " (groupId, clientId, descriptor, visibleToAll)" + " VALUES (?, ?, ?)";
+ " VALUES (?, ?, ?, FALSE)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, g.getId().getBytes()); ps.setBytes(1, g.getId().getBytes());
ps.setBytes(2, g.getClientId().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, public void updateExpiryTime(Connection txn, ContactId c, MessageId m,
int maxLatency) throws DbException { int maxLatency) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;

View File

@@ -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;
}
}
}

View File

@@ -4,13 +4,10 @@ import com.google.inject.Inject;
import org.briarproject.api.FormatException; import org.briarproject.api.FormatException;
import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.data.BdfDictionary; import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfReader; import org.briarproject.api.data.BdfReader;
import org.briarproject.api.data.BdfReaderFactory; 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.MetadataEncoder;
import org.briarproject.api.data.MetadataParser; import org.briarproject.api.data.MetadataParser;
import org.briarproject.api.db.DatabaseComponent; 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.identity.LocalAuthor;
import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group; import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupFactory;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -42,6 +36,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Logger; import java.util.logging.Logger;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
@@ -63,25 +58,23 @@ class ForumManagerImpl implements ForumManager {
Logger.getLogger(ForumManagerImpl.class.getName()); Logger.getLogger(ForumManagerImpl.class.getName());
private final DatabaseComponent db; private final DatabaseComponent db;
private final GroupFactory groupFactory; private final ContactManager contactManager;
private final BdfReaderFactory bdfReaderFactory; private final BdfReaderFactory bdfReaderFactory;
private final BdfWriterFactory bdfWriterFactory;
private final MetadataEncoder metadataEncoder; private final MetadataEncoder metadataEncoder;
private final MetadataParser metadataParser; private final MetadataParser metadataParser;
private final SecureRandom random;
/** Ensures isolation between database reads and writes. */
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
@Inject @Inject
ForumManagerImpl(CryptoComponent crypto, DatabaseComponent db, ForumManagerImpl(DatabaseComponent db, ContactManager contactManager,
GroupFactory groupFactory, BdfReaderFactory bdfReaderFactory, BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
MetadataParser metadataParser) { MetadataParser metadataParser) {
this.db = db; this.db = db;
this.groupFactory = groupFactory; this.contactManager = contactManager;
this.bdfReaderFactory = bdfReaderFactory; this.bdfReaderFactory = bdfReaderFactory;
this.bdfWriterFactory = bdfWriterFactory;
this.metadataEncoder = metadataEncoder; this.metadataEncoder = metadataEncoder;
this.metadataParser = metadataParser; this.metadataParser = metadataParser;
random = crypto.getSecureRandom();
} }
@Override @Override
@@ -89,62 +82,154 @@ class ForumManagerImpl implements ForumManager {
return CLIENT_ID; 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 @Override
public void addLocalPost(ForumPost p) throws DbException { public void addLocalPost(ForumPost p) throws DbException {
BdfDictionary d = new BdfDictionary(); lock.writeLock().lock();
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);
try { 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); Metadata meta = metadataEncoder.encode(d);
db.addLocalMessage(p.getMessage(), CLIENT_ID, meta, true); db.addLocalMessage(p.getMessage(), CLIENT_ID, meta, true);
} catch (FormatException e) { } catch (FormatException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); throw new RuntimeException(e);
} finally {
lock.writeLock().unlock();
} }
} }
@Override @Override
public Collection<Forum> getAvailableForums() throws DbException { public Forum getForum(GroupId g) throws DbException {
// TODO lock.readLock().lock();
return Collections.emptyList(); try {
return parseForum(db.getGroup(g));
} catch (FormatException e) {
throw new DbException(e);
} finally {
lock.readLock().unlock();
}
}
@Override
public Collection<Forum> getForums() throws DbException {
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();
}
}
@Override
public byte[] getPostBody(MessageId m) throws DbException {
lock.readLock().lock();
try {
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
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) {
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 {
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);
}
}
return headers;
} finally {
lock.readLock().unlock();
}
}
@Override
public void setReadFlag(MessageId m, boolean read) throws DbException {
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();
}
} }
private Forum parseForum(Group g) throws FormatException { private Forum parseForum(Group g) throws FormatException {
@@ -153,12 +238,10 @@ class ForumManagerImpl implements ForumManager {
try { try {
r.readListStart(); r.readListStart();
String name = r.readString(MAX_FORUM_NAME_LENGTH); String name = r.readString(MAX_FORUM_NAME_LENGTH);
if (name.length() == 0) throw new FormatException();
byte[] salt = r.readRaw(FORUM_SALT_LENGTH); byte[] salt = r.readRaw(FORUM_SALT_LENGTH);
if (salt.length != FORUM_SALT_LENGTH) throw new FormatException();
r.readListEnd(); r.readListEnd();
if (!r.eof()) throw new FormatException(); if (!r.eof()) throw new FormatException();
return new Forum(g, name); return new Forum(g, name, salt);
} catch (FormatException e) { } catch (FormatException e) {
throw e; throw e;
} catch (IOException e) { } catch (IOException e) {
@@ -166,137 +249,4 @@ class ForumManagerImpl implements ForumManager {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@Override
public Forum getForum(GroupId g) throws DbException {
Group group = db.getGroup(g);
if (!group.getClientId().equals(CLIENT_ID))
throw new IllegalArgumentException();
try {
return parseForum(group);
} catch (FormatException e) {
throw new IllegalArgumentException();
}
}
@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);
}
}
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);
try {
// Extract the forum post body
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);
} catch (FormatException e) {
// Not a valid forum post
throw new IllegalArgumentException();
} catch (IOException e) {
// Shouldn't happen with ByteArrayInputStream
throw new RuntimeException(e);
}
}
@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;
}
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;
}
@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);
try {
db.mergeMessageMetadata(m, metadataEncoder.encode(d));
} catch (FormatException e) {
throw new RuntimeException(e);
}
}
@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);
}
} }

View File

@@ -3,39 +3,63 @@ package org.briarproject.forum;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
import com.google.inject.Provides; import com.google.inject.Provides;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.data.BdfReaderFactory; import org.briarproject.api.data.BdfReaderFactory;
import org.briarproject.api.data.BdfWriterFactory; import org.briarproject.api.data.BdfWriterFactory;
import org.briarproject.api.data.MetadataEncoder; import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.data.ObjectReader; import org.briarproject.api.data.ObjectReader;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumManager;
import org.briarproject.api.forum.ForumPostFactory; import org.briarproject.api.forum.ForumPostFactory;
import org.briarproject.api.forum.ForumSharingManager;
import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.ValidationManager; import org.briarproject.api.sync.ValidationManager;
import org.briarproject.api.system.Clock; import org.briarproject.api.system.Clock;
import javax.inject.Singleton; import javax.inject.Singleton;
import static org.briarproject.forum.ForumManagerImpl.CLIENT_ID;
public class ForumModule extends AbstractModule { public class ForumModule extends AbstractModule {
@Override @Override
protected void configure() { protected void configure() {
bind(ForumManager.class).to(ForumManagerImpl.class); bind(ForumManager.class).to(ForumManagerImpl.class).in(Singleton.class);
bind(ForumPostFactory.class).to(ForumPostFactoryImpl.class); bind(ForumPostFactory.class).to(ForumPostFactoryImpl.class);
} }
@Provides @Singleton @Provides @Singleton
ForumPostValidator getValidator(ValidationManager validationManager, ForumPostValidator getForumPostValidator(
CryptoComponent crypto, BdfReaderFactory bdfReaderFactory, ValidationManager validationManager, CryptoComponent crypto,
BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory, BdfWriterFactory bdfWriterFactory,
ObjectReader<Author> authorReader, MetadataEncoder metadataEncoder, ObjectReader<Author> authorReader, MetadataEncoder metadataEncoder,
Clock clock) { Clock clock) {
ForumPostValidator validator = new ForumPostValidator(crypto, ForumPostValidator validator = new ForumPostValidator(crypto,
bdfReaderFactory, bdfWriterFactory, authorReader, bdfReaderFactory, bdfWriterFactory, authorReader,
metadataEncoder, clock); metadataEncoder, clock);
validationManager.registerMessageValidator(CLIENT_ID, validator); validationManager.registerMessageValidator(
ForumManagerImpl.CLIENT_ID, validator);
return 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;
}
} }

View File

@@ -15,6 +15,7 @@ import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.data.ObjectReader; import org.briarproject.api.data.ObjectReader;
import org.briarproject.api.db.Metadata; import org.briarproject.api.db.Metadata;
import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.MessageValidator; import org.briarproject.api.sync.MessageValidator;
@@ -26,8 +27,6 @@ import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.logging.Logger; 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_CONTENT_TYPE_LENGTH;
import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH; import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_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 Clock clock;
private final KeyParser keyParser; private final KeyParser keyParser;
@Inject
ForumPostValidator(CryptoComponent crypto, ForumPostValidator(CryptoComponent crypto,
BdfReaderFactory bdfReaderFactory, BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory, BdfWriterFactory bdfWriterFactory,
@@ -63,7 +61,7 @@ class ForumPostValidator implements MessageValidator {
} }
@Override @Override
public Metadata validateMessage(Message m) { public Metadata validateMessage(Message m, Group g) {
// Reject the message if it's too far in the future // Reject the message if it's too far in the future
long now = clock.currentTimeMillis(); long now = clock.currentTimeMillis();
if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) { if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
@@ -78,8 +76,7 @@ class ForumPostValidator implements MessageValidator {
BdfReader r = bdfReaderFactory.createReader(in); BdfReader r = bdfReaderFactory.createReader(in);
MessageId parent = null; MessageId parent = null;
Author author = null; Author author = null;
String contentType; byte[] sig = null;
byte[] postBody, sig = null;
r.readListStart(); r.readListStart();
// Read the parent ID, if any // Read the parent ID, if any
if (r.hasRaw()) { if (r.hasRaw()) {
@@ -93,9 +90,9 @@ class ForumPostValidator implements MessageValidator {
if (r.hasList()) author = authorReader.readObject(r); if (r.hasList()) author = authorReader.readObject(r);
else r.readNull(); else r.readNull();
// Read the content type // Read the content type
contentType = r.readString(MAX_CONTENT_TYPE_LENGTH); String contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
// Read the forum post body // 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 // Read the signature, if any
if (r.hasRaw()) sig = r.readRaw(MAX_SIGNATURE_LENGTH); if (r.hasRaw()) sig = r.readRaw(MAX_SIGNATURE_LENGTH);

View File

@@ -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;
}
}
}

View File

@@ -5,6 +5,7 @@ import com.google.inject.Inject;
import org.briarproject.api.FormatException; import org.briarproject.api.FormatException;
import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId; 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.AddContactHook;
import org.briarproject.api.contact.ContactManager.RemoveContactHook; import org.briarproject.api.contact.ContactManager.RemoveContactHook;
import org.briarproject.api.data.BdfDictionary; import org.briarproject.api.data.BdfDictionary;
@@ -50,18 +51,19 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
Logger.getLogger(MessagingManagerImpl.class.getName()); Logger.getLogger(MessagingManagerImpl.class.getName());
private final DatabaseComponent db; private final DatabaseComponent db;
private final ContactManager contactManager;
private final PrivateGroupFactory privateGroupFactory; private final PrivateGroupFactory privateGroupFactory;
private final BdfReaderFactory bdfReaderFactory; private final BdfReaderFactory bdfReaderFactory;
private final MetadataEncoder metadataEncoder; private final MetadataEncoder metadataEncoder;
private final MetadataParser metadataParser; private final MetadataParser metadataParser;
@Inject @Inject
MessagingManagerImpl(DatabaseComponent db, MessagingManagerImpl(DatabaseComponent db, ContactManager contactManager,
PrivateGroupFactory privateGroupFactory, PrivateGroupFactory privateGroupFactory,
BdfReaderFactory bdfReaderFactory, BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
MetadataEncoder metadataEncoder,
MetadataParser metadataParser) { MetadataParser metadataParser) {
this.db = db; this.db = db;
this.contactManager = contactManager;
this.privateGroupFactory = privateGroupFactory; this.privateGroupFactory = privateGroupFactory;
this.bdfReaderFactory = bdfReaderFactory; this.bdfReaderFactory = bdfReaderFactory;
this.metadataEncoder = metadataEncoder; this.metadataEncoder = metadataEncoder;
@@ -71,8 +73,8 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
@Override @Override
public void addingContact(ContactId c) { public void addingContact(ContactId c) {
try { try {
// Create the conversation group // Create a group to share with the contact
Group g = getConversationGroup(db.getContact(c)); Group g = getContactGroup(db.getContact(c));
// Store the group and share it with the contact // Store the group and share it with the contact
db.addGroup(g); db.addGroup(g);
db.setVisibility(g.getId(), Collections.singletonList(c)); 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); return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
} }
@Override @Override
public void removingContact(ContactId c) { public void removingContact(ContactId c) {
try { try {
db.removeGroup(getConversationGroup(db.getContact(c))); db.removeGroup(getContactGroup(db.getContact(c)));
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
} }
@@ -135,7 +137,7 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
@Override @Override
public GroupId getConversationId(ContactId c) throws DbException { public GroupId getConversationId(ContactId c) throws DbException {
return getConversationGroup(db.getContact(c)).getId(); return getContactGroup(contactManager.getContact(c)).getId();
} }
@Override @Override
@@ -172,15 +174,16 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
BdfReader r = bdfReaderFactory.createReader(in); BdfReader r = bdfReaderFactory.createReader(in);
try { try {
// Extract the private message body
r.readListStart(); r.readListStart();
if (r.hasRaw()) r.skipRaw(); // Parent ID if (r.hasRaw()) r.skipRaw(); // Parent ID
else r.skipNull(); // No parent else r.skipNull(); // No parent
r.skipString(); // Content type 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) { } catch (FormatException e) {
// Not a valid private message throw new DbException(e);
throw new IllegalArgumentException();
} catch (IOException e) { } catch (IOException e) {
// Shouldn't happen with ByteArrayInputStream // Shouldn't happen with ByteArrayInputStream
throw new RuntimeException(e); throw new RuntimeException(e);

View File

@@ -7,6 +7,7 @@ import org.briarproject.api.data.BdfReader;
import org.briarproject.api.data.BdfReaderFactory; import org.briarproject.api.data.BdfReaderFactory;
import org.briarproject.api.data.MetadataEncoder; import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.db.Metadata; import org.briarproject.api.db.Metadata;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.MessageValidator; import org.briarproject.api.sync.MessageValidator;
@@ -16,8 +17,6 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.logging.Logger; 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_CONTENT_TYPE_LENGTH;
import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH; import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_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 MetadataEncoder metadataEncoder;
private final Clock clock; private final Clock clock;
@Inject
PrivateMessageValidator(BdfReaderFactory bdfReaderFactory, PrivateMessageValidator(BdfReaderFactory bdfReaderFactory,
MetadataEncoder metadataEncoder, Clock clock) { MetadataEncoder metadataEncoder, Clock clock) {
this.bdfReaderFactory = bdfReaderFactory; this.bdfReaderFactory = bdfReaderFactory;
@@ -41,7 +39,7 @@ class PrivateMessageValidator implements MessageValidator {
} }
@Override @Override
public Metadata validateMessage(Message m) { public Metadata validateMessage(Message m, Group g) {
// Reject the message if it's too far in the future // Reject the message if it's too far in the future
long now = clock.currentTimeMillis(); long now = clock.currentTimeMillis();
if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) { if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
@@ -55,7 +53,6 @@ class PrivateMessageValidator implements MessageValidator {
MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
BdfReader r = bdfReaderFactory.createReader(in); BdfReader r = bdfReaderFactory.createReader(in);
MessageId parent = null; MessageId parent = null;
String contentType;
r.readListStart(); r.readListStart();
// Read the parent ID, if any // Read the parent ID, if any
if (r.hasRaw()) { if (r.hasRaw()) {
@@ -66,7 +63,7 @@ class PrivateMessageValidator implements MessageValidator {
r.readNull(); r.readNull();
} }
// Read the content type // Read the content type
contentType = r.readString(MAX_CONTENT_TYPE_LENGTH); String contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
// Read the private message body // Read the private message body
r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH); r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
r.readListEnd(); r.readListEnd();

View File

@@ -7,6 +7,7 @@ import org.briarproject.api.FormatException;
import org.briarproject.api.TransportId; import org.briarproject.api.TransportId;
import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId; 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.AddContactHook;
import org.briarproject.api.contact.ContactManager.RemoveContactHook; import org.briarproject.api.contact.ContactManager.RemoveContactHook;
import org.briarproject.api.data.BdfDictionary; import org.briarproject.api.data.BdfDictionary;
@@ -60,6 +61,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
Logger.getLogger(TransportPropertyManagerImpl.class.getName()); Logger.getLogger(TransportPropertyManagerImpl.class.getName());
private final DatabaseComponent db; private final DatabaseComponent db;
private final ContactManager contactManager;
private final PrivateGroupFactory privateGroupFactory; private final PrivateGroupFactory privateGroupFactory;
private final MessageFactory messageFactory; private final MessageFactory messageFactory;
private final BdfReaderFactory bdfReaderFactory; private final BdfReaderFactory bdfReaderFactory;
@@ -74,11 +76,13 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
@Inject @Inject
TransportPropertyManagerImpl(DatabaseComponent db, TransportPropertyManagerImpl(DatabaseComponent db,
GroupFactory groupFactory, PrivateGroupFactory privateGroupFactory, ContactManager contactManager, GroupFactory groupFactory,
PrivateGroupFactory privateGroupFactory,
MessageFactory messageFactory, BdfReaderFactory bdfReaderFactory, MessageFactory messageFactory, BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder, BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
MetadataParser metadataParser, Clock clock) { MetadataParser metadataParser, Clock clock) {
this.db = db; this.db = db;
this.contactManager = contactManager;
this.privateGroupFactory = privateGroupFactory; this.privateGroupFactory = privateGroupFactory;
this.messageFactory = messageFactory; this.messageFactory = messageFactory;
this.bdfReaderFactory = bdfReaderFactory; this.bdfReaderFactory = bdfReaderFactory;
@@ -109,7 +113,145 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
} catch (FormatException e) { } catch (FormatException e) {
throw new RuntimeException(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 addRemoteProperties(ContactId c, DeviceId dev,
Map<TransportId, TransportProperties> props) throws DbException {
lock.writeLock().lock();
try {
Group g = getContactGroup(db.getContact(c));
for (Entry<TransportId, TransportProperties> e : props.entrySet()) {
storeMessage(g.getId(), dev, e.getKey(), e.getValue(), 0, false,
false);
}
} catch (FormatException e) {
throw new DbException(e);
} finally {
lock.writeLock().unlock();
}
}
@Override
public Map<TransportId, TransportProperties> getLocalProperties()
throws DbException {
lock.readLock().lock();
try {
// Find the latest local update for each transport
Map<TransportId, LatestUpdate> latest =
findLatest(localGroup.getId(), true);
// 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(), parseProperties(raw));
}
return Collections.unmodifiableMap(local);
} catch (NoSuchGroupException e) {
// Local group doesn't exist - there are no local properties
return Collections.emptyMap();
} catch (IOException e) {
throw new DbException(e);
} finally {
lock.readLock().unlock();
}
}
@Override
public TransportProperties getLocalProperties(TransportId t)
throws DbException {
lock.readLock().lock();
try {
// Find the latest local update
LatestUpdate latest = findLatest(localGroup.getId(), t, true);
if (latest == null) return null;
// 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;
} catch (IOException e) {
throw new DbException(e);
} finally {
lock.readLock().unlock();
}
}
@Override
public Map<ContactId, TransportProperties> getRemoteProperties(
TransportId t) throws DbException {
lock.readLock().lock();
try {
Map<ContactId, TransportProperties> remote =
new HashMap<ContactId, TransportProperties>();
for (Contact c : contactManager.getContacts()) {
Group g = getContactGroup(c);
// Find the latest remote update
LatestUpdate latest = findLatest(g.getId(), t, false);
if (latest != null) {
// Retrieve and parse the latest remote properties
byte[] raw = db.getRawMessage(latest.messageId);
remote.put(c.getId(), parseProperties(raw));
}
}
return Collections.unmodifiableMap(remote);
} catch (IOException e) {
throw new DbException(e);
} finally {
lock.readLock().unlock();
}
}
@Override
public void mergeLocalProperties(TransportId t, TransportProperties p)
throws DbException {
lock.writeLock().lock();
try {
// Create the local group if necessary
db.addGroup(localGroup);
// Merge the new properties with any existing properties
TransportProperties merged;
LatestUpdate latest = findLatest(localGroup.getId(), t, true);
if (latest == null) {
merged = p;
} else {
byte[] raw = db.getRawMessage(latest.messageId);
TransportProperties old = parseProperties(raw);
merged = new TransportProperties(old);
merged.putAll(p);
if (merged.equals(old)) return; // Unchanged
}
// Store the merged properties in the local group
DeviceId dev = db.getDeviceId();
long version = latest == null ? 1 : latest.version + 1;
storeMessage(localGroup.getId(), dev, t, merged, version, true,
false);
// Store the merged properties in each contact's group
for (Contact c : contactManager.getContacts()) {
Group g = getContactGroup(c);
latest = findLatest(g.getId(), t, true);
version = latest == null ? 1 : latest.version + 1;
storeMessage(g.getId(), dev, t, merged, version, true, true);
}
} catch (IOException e) {
throw new DbException(e);
} finally { } finally {
lock.writeLock().unlock(); lock.writeLock().unlock();
} }
@@ -119,6 +261,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
return privateGroupFactory.createPrivateGroup(CLIENT_ID, c); return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
} }
// Locking: lock.writeLock
private void storeMessage(GroupId g, DeviceId dev, TransportId t, private void storeMessage(GroupId g, DeviceId dev, TransportId t,
TransportProperties p, long version, boolean local, boolean shared) TransportProperties p, long version, boolean local, boolean shared)
throws DbException, FormatException { throws DbException, FormatException {
@@ -150,80 +293,43 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
return out.toByteArray(); return out.toByteArray();
} }
@Override // Locking: lock.readLock
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 addRemoteProperties(ContactId c, DeviceId dev,
Map<TransportId, TransportProperties> props) throws DbException {
lock.writeLock().lock();
try {
Group g = getContactGroup(db.getContact(c));
for (Entry<TransportId, TransportProperties> e : props.entrySet()) {
storeMessage(g.getId(), dev, e.getKey(), e.getValue(), 0, false,
false);
}
} catch (IOException e) {
throw new DbException(e);
} finally {
lock.writeLock().unlock();
}
}
@Override
public Map<TransportId, TransportProperties> getLocalProperties()
throws DbException {
lock.readLock().lock();
try {
// Find the latest local version for each transport
Map<TransportId, LatestUpdate> latest =
findLatest(localGroup.getId(), true);
// Retrieve and decode 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));
}
return Collections.unmodifiableMap(local);
} catch (NoSuchGroupException e) {
// Local group doesn't exist - there are no local properties
return Collections.emptyMap();
} catch (IOException e) {
throw new DbException(e);
} finally {
lock.readLock().unlock();
}
}
private Map<TransportId, LatestUpdate> findLatest(GroupId g, boolean local) private Map<TransportId, LatestUpdate> findLatest(GroupId g, boolean local)
throws DbException, FormatException { throws DbException, FormatException {
// TODO: Use metadata queries
Map<TransportId, LatestUpdate> latestUpdates = Map<TransportId, LatestUpdate> latestUpdates =
new HashMap<TransportId, LatestUpdate>(); new HashMap<TransportId, LatestUpdate>();
Map<MessageId, Metadata> metadata = db.getMessageMetadata(g); Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
for (Entry<MessageId, Metadata> e : metadata.entrySet()) { for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
BdfDictionary d = metadataParser.parse(e.getValue()); BdfDictionary d = metadataParser.parse(e.getValue());
if (d.getBoolean("local") != local) continue; if (d.getBoolean("local") == local) {
TransportId t = new TransportId(d.getString("transportId")); TransportId t = new TransportId(d.getString("transportId"));
long version = d.getInteger("version"); long version = d.getInteger("version");
LatestUpdate latest = latestUpdates.get(t); LatestUpdate latest = latestUpdates.get(t);
if (latest == null || version > latest.version) if (latest == null || version > latest.version)
latestUpdates.put(t, new LatestUpdate(e.getKey(), version)); latestUpdates.put(t, new LatestUpdate(e.getKey(), version));
}
} }
return latestUpdates; return latestUpdates;
} }
private TransportProperties decodeProperties(byte[] raw) // 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 { throws IOException {
TransportProperties p = new TransportProperties(); TransportProperties p = new TransportProperties();
ByteArrayInputStream in = new ByteArrayInputStream(raw, ByteArrayInputStream in = new ByteArrayInputStream(raw,
@@ -239,92 +345,12 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
String value = r.readString(MAX_PROPERTY_LENGTH); String value = r.readString(MAX_PROPERTY_LENGTH);
p.put(key, value); p.put(key, value);
} }
r.readDictionaryEnd();
r.readListEnd();
if (!r.eof()) throw new FormatException();
return p; 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);
if (latest == null) return null;
// Retrieve and decode the latest local properties
return decodeProperties(db.getRawMessage(latest.messageId));
} catch (NoSuchGroupException e) {
// Local group doesn't exist - there are no local properties
return null;
} catch (IOException e) {
throw new DbException(e);
} finally {
lock.readLock().unlock();
}
}
@Override
public Map<ContactId, TransportProperties> getRemoteProperties(
TransportId t) throws DbException {
lock.readLock().lock();
try {
Map<ContactId, TransportProperties> remote =
new HashMap<ContactId, TransportProperties>();
for (Contact c : db.getContacts()) {
Group g = getContactGroup(c);
// Find the latest remote version
LatestUpdate latest = findLatest(g.getId(), false).get(t);
if (latest != null) {
// Retrieve and decode the latest remote properties
byte[] raw = db.getRawMessage(latest.messageId);
remote.put(c.getId(), decodeProperties(raw));
}
}
return Collections.unmodifiableMap(remote);
} catch (IOException e) {
throw new DbException(e);
} finally {
lock.readLock().unlock();
}
}
@Override
public void mergeLocalProperties(TransportId t, TransportProperties p)
throws DbException {
lock.writeLock().lock();
try {
// Create the local group if necessary
db.addGroup(localGroup);
// Merge the new properties with any existing properties
TransportProperties merged;
LatestUpdate latest = findLatest(localGroup.getId(), true).get(t);
if (latest == null) {
merged = p;
} else {
byte[] raw = db.getRawMessage(latest.messageId);
TransportProperties old = decodeProperties(raw);
merged = new TransportProperties(old);
merged.putAll(p);
if (merged.equals(old)) return; // Unchanged
}
// Store the merged properties in the local group
DeviceId dev = db.getDeviceId();
long version = latest == null ? 1 : latest.version + 1;
storeMessage(localGroup.getId(), dev, t, merged, version, true,
false);
// Store the merged properties in each contact's group
for (Contact c : db.getContacts()) {
Group g = getContactGroup(c);
latest = findLatest(g.getId(), true).get(t);
version = latest == null ? 1 : latest.version + 1;
storeMessage(g.getId(), dev, t, merged, version, true, true);
}
} catch (IOException e) {
throw new DbException(e);
} finally {
lock.writeLock().unlock();
}
}
private static class LatestUpdate { private static class LatestUpdate {
private final MessageId messageId; private final MessageId messageId;

View File

@@ -7,6 +7,7 @@ import org.briarproject.api.data.BdfReader;
import org.briarproject.api.data.BdfReaderFactory; import org.briarproject.api.data.BdfReaderFactory;
import org.briarproject.api.data.MetadataEncoder; import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.db.Metadata; import org.briarproject.api.db.Metadata;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageValidator; import org.briarproject.api.sync.MessageValidator;
import org.briarproject.api.system.Clock; import org.briarproject.api.system.Clock;
@@ -15,8 +16,6 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject;
import static org.briarproject.api.TransportId.MAX_TRANSPORT_ID_LENGTH; 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_PROPERTIES_PER_TRANSPORT;
import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH; import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
@@ -32,7 +31,6 @@ class TransportPropertyValidator implements MessageValidator {
private final MetadataEncoder metadataEncoder; private final MetadataEncoder metadataEncoder;
private final Clock clock; private final Clock clock;
@Inject
TransportPropertyValidator(BdfReaderFactory bdfReaderFactory, TransportPropertyValidator(BdfReaderFactory bdfReaderFactory,
MetadataEncoder metadataEncoder, Clock clock) { MetadataEncoder metadataEncoder, Clock clock) {
this.bdfReaderFactory = bdfReaderFactory; this.bdfReaderFactory = bdfReaderFactory;
@@ -41,7 +39,7 @@ class TransportPropertyValidator implements MessageValidator {
} }
@Override @Override
public Metadata validateMessage(Message m) { public Metadata validateMessage(Message m, Group g) {
// Reject the message if it's too far in the future // Reject the message if it's too far in the future
long now = clock.currentTimeMillis(); long now = clock.currentTimeMillis();
if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) { if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {

View File

@@ -13,7 +13,6 @@ import org.briarproject.api.event.MessageRequestedEvent;
import org.briarproject.api.event.MessageSharedEvent; import org.briarproject.api.event.MessageSharedEvent;
import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToAckEvent;
import org.briarproject.api.event.MessageToRequestEvent; import org.briarproject.api.event.MessageToRequestEvent;
import org.briarproject.api.event.MessageValidatedEvent;
import org.briarproject.api.event.ShutdownEvent; import org.briarproject.api.event.ShutdownEvent;
import org.briarproject.api.event.TransportRemovedEvent; import org.briarproject.api.event.TransportRemovedEvent;
import org.briarproject.api.sync.Ack; import org.briarproject.api.sync.Ack;
@@ -151,9 +150,6 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
if (c.getContactId().equals(contactId)) interrupt(); if (c.getContactId().equals(contactId)) interrupt();
} else if (e instanceof MessageSharedEvent) { } else if (e instanceof MessageSharedEvent) {
dbExecutor.execute(new GenerateOffer()); dbExecutor.execute(new GenerateOffer());
} else if (e instanceof MessageValidatedEvent) {
if (((MessageValidatedEvent) e).isValid())
dbExecutor.execute(new GenerateOffer());
} else if (e instanceof GroupVisibilityUpdatedEvent) { } else if (e instanceof GroupVisibilityUpdatedEvent) {
GroupVisibilityUpdatedEvent g = (GroupVisibilityUpdatedEvent) e; GroupVisibilityUpdatedEvent g = (GroupVisibilityUpdatedEvent) e;
if (g.getAffectedContacts().contains(contactId)) if (g.getAffectedContacts().contains(contactId))

View File

@@ -15,6 +15,7 @@ import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageAddedEvent; import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.lifecycle.Service; import org.briarproject.api.lifecycle.Service;
import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
@@ -84,7 +85,8 @@ class ValidationManagerImpl implements ValidationManager, Service,
for (MessageId id : db.getMessagesToValidate(c)) { for (MessageId id : db.getMessagesToValidate(c)) {
try { try {
Message m = parseMessage(id, db.getRawMessage(id)); Message m = parseMessage(id, db.getRawMessage(id));
validateMessage(m, c); Group g = db.getGroup(m.getGroupId());
validateMessage(m, g);
} catch (NoSuchMessageException e) { } catch (NoSuchMessageException e) {
LOG.info("Message removed before validation"); LOG.info("Message removed before validation");
} }
@@ -106,15 +108,15 @@ class ValidationManagerImpl implements ValidationManager, Service,
return new Message(id, new GroupId(groupId), timestamp, raw); 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() { cryptoExecutor.execute(new Runnable() {
public void run() { public void run() {
MessageValidator v = validators.get(c); MessageValidator v = validators.get(g.getClientId());
if (v == null) { if (v == null) {
LOG.warning("No validator"); LOG.warning("No validator");
} else { } else {
Metadata meta = v.validateMessage(m); Metadata meta = v.validateMessage(m, g);
storeValidationResult(m, c, meta); storeValidationResult(m, g.getClientId(), meta);
} }
} }
}); });
@@ -132,6 +134,7 @@ class ValidationManagerImpl implements ValidationManager, Service,
hook.validatingMessage(m, c, meta); hook.validatingMessage(m, c, meta);
db.mergeMessageMetadata(m.getId(), meta); db.mergeMessageMetadata(m.getId(), meta);
db.setMessageValid(m, c, true); db.setMessageValid(m, c, true);
db.setMessageShared(m, true);
} }
} catch (NoSuchMessageException e) { } catch (NoSuchMessageException e) {
LOG.info("Message removed during validation"); LOG.info("Message removed during validation");
@@ -146,18 +149,17 @@ class ValidationManagerImpl implements ValidationManager, Service,
@Override @Override
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (e instanceof MessageAddedEvent) { if (e instanceof MessageAddedEvent) {
MessageAddedEvent m = (MessageAddedEvent) e;
// Validate the message if it wasn't created locally // 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() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {
ClientId c = db.getGroup(m.getGroupId()).getClientId(); validateMessage(m, db.getGroup(m.getGroupId()));
validateMessage(m, c);
} catch (NoSuchGroupException e) { } catch (NoSuchGroupException e) {
LOG.info("Group removed before validation"); LOG.info("Group removed before validation");
} catch (DbException e) { } catch (DbException e) {

View File

@@ -2,7 +2,6 @@ package org.briarproject.plugins;
import com.google.inject.Provides; import com.google.inject.Provides;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.lifecycle.IoExecutor; import org.briarproject.api.lifecycle.IoExecutor;
import org.briarproject.api.lifecycle.ShutdownManager; import org.briarproject.api.lifecycle.ShutdownManager;
import org.briarproject.api.plugins.duplex.DuplexPluginConfig; 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.LanTcpPluginFactory;
import org.briarproject.plugins.tcp.WanTcpPluginFactory; import org.briarproject.plugins.tcp.WanTcpPluginFactory;
import java.security.SecureRandom;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -39,10 +39,10 @@ public class DesktopPluginsModule extends PluginsModule {
@Provides @Provides
DuplexPluginConfig getDuplexPluginConfig(@IoExecutor Executor ioExecutor, DuplexPluginConfig getDuplexPluginConfig(@IoExecutor Executor ioExecutor,
CryptoComponent crypto, ReliabilityLayerFactory reliabilityFactory, SecureRandom random, ReliabilityLayerFactory reliabilityFactory,
ShutdownManager shutdownManager) { ShutdownManager shutdownManager) {
DuplexPluginFactory bluetooth = new BluetoothPluginFactory( DuplexPluginFactory bluetooth = new BluetoothPluginFactory(ioExecutor,
ioExecutor, crypto.getSecureRandom()); random);
DuplexPluginFactory modem = new ModemPluginFactory(ioExecutor, DuplexPluginFactory modem = new ModemPluginFactory(ioExecutor,
reliabilityFactory); reliabilityFactory);
DuplexPluginFactory lan = new LanTcpPluginFactory(ioExecutor); DuplexPluginFactory lan = new LanTcpPluginFactory(ioExecutor);

View File

@@ -20,6 +20,7 @@ import org.briarproject.api.event.GroupRemovedEvent;
import org.briarproject.api.event.GroupVisibilityUpdatedEvent; import org.briarproject.api.event.GroupVisibilityUpdatedEvent;
import org.briarproject.api.event.MessageAddedEvent; import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageRequestedEvent;
import org.briarproject.api.event.MessageSharedEvent;
import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToAckEvent;
import org.briarproject.api.event.MessageToRequestEvent; import org.briarproject.api.event.MessageToRequestEvent;
import org.briarproject.api.event.MessageValidatedEvent; 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 // The message was added, so the listeners should be called
oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class))); oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class)));
oneOf(eventBus).broadcast(with(any(MessageValidatedEvent.class))); oneOf(eventBus).broadcast(with(any(MessageValidatedEvent.class)));
oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class)));
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
shutdown); shutdown);
@@ -265,11 +267,11 @@ public class DatabaseComponentImplTest extends BriarTestCase {
final EventBus eventBus = context.mock(EventBus.class); final EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the contact is in the DB (which it's not) // 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)); will(returnValue(txn));
exactly(15).of(database).containsContact(txn, contactId); exactly(17).of(database).containsContact(txn, contactId);
will(returnValue(false)); will(returnValue(false));
exactly(15).of(database).abortTransaction(txn); exactly(17).of(database).abortTransaction(txn);
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
shutdown); shutdown);
@@ -337,6 +339,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
// Expected // Expected
} }
try {
db.isVisibleToContact(contactId, groupId);
fail();
} catch (NoSuchContactException expected) {
// Expected
}
try { try {
Ack a = new Ack(Collections.singletonList(messageId)); Ack a = new Ack(Collections.singletonList(messageId));
db.receiveAck(contactId, a); db.receiveAck(contactId, a);
@@ -382,6 +391,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
// Expected // Expected
} }
try {
db.setVisibleToContact(contactId, groupId, true);
fail();
} catch (NoSuchContactException expected) {
// Expected
}
context.assertIsSatisfied(); context.assertIsSatisfied();
} }
@@ -438,13 +454,14 @@ public class DatabaseComponentImplTest extends BriarTestCase {
final EventBus eventBus = context.mock(EventBus.class); final EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the group is in the DB (which it's not) // 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)); will(returnValue(txn));
exactly(7).of(database).containsGroup(txn, groupId); exactly(9).of(database).containsGroup(txn, groupId);
will(returnValue(false)); will(returnValue(false));
exactly(7).of(database).abortTransaction(txn); exactly(9).of(database).abortTransaction(txn);
// This is needed for getMessageStatus() to proceed // This is needed for getMessageStatus(), isVisibleToContact(), and
exactly(1).of(database).containsContact(txn, contactId); // setVisibleToContact() to proceed
exactly(3).of(database).containsContact(txn, contactId);
will(returnValue(true)); will(returnValue(true));
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
@@ -478,6 +495,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
// Expected // Expected
} }
try {
db.isVisibleToContact(contactId, groupId);
fail();
} catch (NoSuchGroupException expected) {
// Expected
}
try { try {
db.mergeGroupMetadata(groupId, metadata); db.mergeGroupMetadata(groupId, metadata);
fail(); fail();
@@ -499,6 +523,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
// Expected // Expected
} }
try {
db.setVisibleToContact(contactId, groupId, true);
fail();
} catch (NoSuchGroupException expected) {
// Expected
}
context.assertIsSatisfied(); context.assertIsSatisfied();
} }
@@ -847,7 +878,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
will(returnValue(false)); will(returnValue(false));
oneOf(database).containsVisibleGroup(txn, contactId, groupId); oneOf(database).containsVisibleGroup(txn, contactId, groupId);
will(returnValue(true)); will(returnValue(true));
oneOf(database).addMessage(txn, message, UNKNOWN, true); oneOf(database).addMessage(txn, message, UNKNOWN, false);
oneOf(database).getVisibility(txn, groupId); oneOf(database).getVisibility(txn, groupId);
will(returnValue(Collections.singletonList(contactId))); will(returnValue(Collections.singletonList(contactId)));
oneOf(database).getContactIds(txn); oneOf(database).getContactIds(txn);
@@ -1019,7 +1050,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
oneOf(database).getContactIds(txn); oneOf(database).getContactIds(txn);
will(returnValue(both)); will(returnValue(both));
oneOf(database).removeVisibility(txn, contactId1, groupId); oneOf(database).removeVisibility(txn, contactId1, groupId);
oneOf(database).setVisibleToAll(txn, groupId, false);
oneOf(database).commitTransaction(txn); oneOf(database).commitTransaction(txn);
oneOf(eventBus).broadcast(with(any( oneOf(eventBus).broadcast(with(any(
GroupVisibilityUpdatedEvent.class))); GroupVisibilityUpdatedEvent.class)));
@@ -1051,7 +1081,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
will(returnValue(both)); will(returnValue(both));
oneOf(database).getContactIds(txn); oneOf(database).getContactIds(txn);
will(returnValue(both)); will(returnValue(both));
oneOf(database).setVisibleToAll(txn, groupId, false);
oneOf(database).commitTransaction(txn); oneOf(database).commitTransaction(txn);
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
@@ -1062,55 +1091,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
context.assertIsSatisfied(); 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 @Test
public void testTransportKeys() throws Exception { public void testTransportKeys() throws Exception {
final TransportKeys keys = createTransportKeys(); final TransportKeys keys = createTransportKeys();

View File

@@ -205,7 +205,7 @@ public class H2DatabaseTest extends BriarTestCase {
Database<Connection> db = open(false); Database<Connection> db = open(false);
Connection txn = db.startTransaction(); 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); db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId)); assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addGroup(txn, group); db.addGroup(txn, group);
@@ -243,7 +243,7 @@ public class H2DatabaseTest extends BriarTestCase {
Database<Connection> db = open(false); Database<Connection> db = open(false);
Connection txn = db.startTransaction(); 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); db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId)); assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addGroup(txn, group); db.addGroup(txn, group);
@@ -329,7 +329,7 @@ public class H2DatabaseTest extends BriarTestCase {
ids = db.getMessagesToOffer(txn, contactId, 100); ids = db.getMessagesToOffer(txn, contactId, 100);
assertEquals(Collections.singletonList(messageId), ids); 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); db.removeVisibility(txn, contactId, groupId);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertTrue(ids.isEmpty()); assertTrue(ids.isEmpty());
@@ -1017,6 +1017,31 @@ public class H2DatabaseTest extends BriarTestCase {
db.close(); 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 @Test
public void testDifferentLocalPseudonymsCanHaveTheSameContact() public void testDifferentLocalPseudonymsCanHaveTheSameContact()
throws Exception { throws Exception {

View File

@@ -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
}
}

View File

@@ -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
}
}