Merge branch '112-message-validators' into 'master'

Separate the sync layer from its clients. #112

This patch contains most of the work for #112. MessagingManager and ForumManager have been converted to BSP clients, and the sync layer's message and group formats no longer contain any client-specific data.

Each client has a MessageValidator that's called by the ValidationManager to validate messages belonging to the client. A new MessageValidatedEvent informs listeners when a message has been validated. This required some changes to the UI, especially the notification manager.

While working on the notification manager I realised it was calling Android API methods from background threads, which may be unsafe, and the notification logic was split across BriarService and AndroidNotificationManagerImpl. So I refactored those classes, along with AndroidExecutorImpl, which was using an unnecessary background thread.

Still to do:
* Synchronise blocks, not messages
* Add message type field to message header as per BSP spec
* Convert TransportPropertyManager into a BSP client
* Convert SubscriptionUpdates/Acks into forum client messages


See merge request !64
This commit is contained in:
akwizgran
2016-01-20 14:43:23 +00:00
119 changed files with 3228 additions and 3511 deletions

View File

@@ -1,87 +1,48 @@
package org.briarproject.android; package org.briarproject.android;
import java.util.concurrent.Callable; import android.app.Application;
import java.util.concurrent.CountDownLatch; import android.content.Context;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.api.android.AndroidExecutor;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import org.briarproject.api.android.AndroidExecutor;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import javax.inject.Inject;
class AndroidExecutorImpl implements AndroidExecutor { class AndroidExecutorImpl implements AndroidExecutor {
private static final int SHUTDOWN = 0, RUN = 1; private final Handler handler;
private static final Logger LOG =
Logger.getLogger(AndroidExecutorImpl.class.getName());
private final Runnable loop;
private final AtomicBoolean started = new AtomicBoolean(false);
private final CountDownLatch startLatch = new CountDownLatch(1);
private volatile Handler handler = null;
@Inject @Inject
AndroidExecutorImpl() { AndroidExecutorImpl(Application app) {
loop = new Runnable() { Context ctx = app.getApplicationContext();
public void run() { handler = new FutureTaskHandler(ctx.getMainLooper());
Looper.prepare();
handler = new FutureTaskHandler();
startLatch.countDown();
Looper.loop();
}
};
} }
private void startIfNecessary() { public <V> Future<V> submit(Callable<V> c) {
if (started.getAndSet(true)) return;
new Thread(loop, "AndroidExecutor").start();
try {
startLatch.await();
} catch (InterruptedException e) {
LOG.warning("Interrupted while starting executor thread");
Thread.currentThread().interrupt();
}
}
public <V> V call(Callable<V> c) throws InterruptedException,
ExecutionException {
startIfNecessary();
Future<V> f = new FutureTask<V>(c); Future<V> f = new FutureTask<V>(c);
Message m = Message.obtain(handler, RUN, f); handler.sendMessage(Message.obtain(handler, 0, f));
handler.sendMessage(m); return f;
return f.get();
} }
public void shutdown() { public void execute(Runnable r) {
if (handler != null) { handler.post(r);
Message m = Message.obtain(handler, SHUTDOWN);
handler.sendMessage(m);
}
} }
private static class FutureTaskHandler extends Handler { private static class FutureTaskHandler extends Handler {
private FutureTaskHandler(Looper looper) {
super(looper);
}
@Override @Override
public void handleMessage(Message m) { public void handleMessage(Message m) {
switch(m.what) { ((FutureTask<?>) m.obj).run();
case SHUTDOWN:
Looper.myLooper().quit();
break;
case RUN:
((FutureTask<?>) m.obj).run();
break;
default:
throw new IllegalArgumentException();
}
} }
} }
} }

View File

@@ -14,23 +14,25 @@ import org.briarproject.android.contact.ConversationActivity;
import org.briarproject.android.forum.ForumActivity; import org.briarproject.android.forum.ForumActivity;
import org.briarproject.android.forum.ForumListActivity; import org.briarproject.android.forum.ForumListActivity;
import org.briarproject.api.Settings; import org.briarproject.api.Settings;
import org.briarproject.api.android.AndroidExecutor;
import org.briarproject.api.android.AndroidNotificationManager; import org.briarproject.api.android.AndroidNotificationManager;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.db.DatabaseComponent; import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DatabaseExecutor; import org.briarproject.api.db.DatabaseExecutor;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
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.MessageValidatedEvent;
import org.briarproject.api.event.SettingsUpdatedEvent; import org.briarproject.api.event.SettingsUpdatedEvent;
import org.briarproject.api.forum.ForumManager;
import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
@@ -44,7 +46,7 @@ import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
class AndroidNotificationManagerImpl implements AndroidNotificationManager, class AndroidNotificationManagerImpl implements AndroidNotificationManager,
EventListener { EventListener {
private static final int PRIVATE_MESSAGE_NOTIFICATION_ID = 3; private static final int PRIVATE_MESSAGE_NOTIFICATION_ID = 3;
private static final int FORUM_POST_NOTIFICATION_ID = 4; private static final int FORUM_POST_NOTIFICATION_ID = 4;
@@ -59,27 +61,33 @@ EventListener {
private final DatabaseComponent db; private final DatabaseComponent db;
private final Executor dbExecutor; private final Executor dbExecutor;
private final EventBus eventBus; private final EventBus eventBus;
private final MessagingManager messagingManager;
private final ForumManager forumManager;
private final AndroidExecutor androidExecutor;
private final Context appContext; private final Context appContext;
private final Lock lock = new ReentrantLock();
// The following are locking: lock // The following must only be accessed on the main UI thread
private final Map<ContactId, Integer> contactCounts = private final Map<GroupId, Integer> contactCounts =
new HashMap<ContactId, Integer>(); new HashMap<GroupId, Integer>();
private final Map<GroupId, Integer> forumCounts = private final Map<GroupId, Integer> forumCounts =
new HashMap<GroupId, Integer>(); new HashMap<GroupId, Integer>();
private int contactTotal = 0, forumTotal = 0; private int contactTotal = 0, forumTotal = 0;
private int nextRequestId = 0; private int nextRequestId = 0;
private ContactId activeContact; private GroupId visibleGroup = null;
private volatile Settings settings = new Settings(); private volatile Settings settings = new Settings();
@Inject @Inject
public AndroidNotificationManagerImpl(DatabaseComponent db, public AndroidNotificationManagerImpl(DatabaseComponent db,
@DatabaseExecutor Executor dbExecutor, EventBus eventBus, @DatabaseExecutor Executor dbExecutor, EventBus eventBus,
Application app) { MessagingManager messagingManager, ForumManager forumManager,
AndroidExecutor androidExecutor, Application app) {
this.db = db; this.db = db;
this.dbExecutor = dbExecutor; this.dbExecutor = dbExecutor;
this.eventBus = eventBus; this.eventBus = eventBus;
this.messagingManager = messagingManager;
this.forumManager = forumManager;
this.androidExecutor = androidExecutor;
appContext = app.getApplicationContext(); appContext = app.getApplicationContext();
} }
@@ -104,62 +112,71 @@ EventListener {
public boolean stop() { public boolean stop() {
eventBus.removeListener(this); eventBus.removeListener(this);
clearNotifications();
return true; return true;
} }
public void eventOccurred(Event e) { private void clearNotifications() {
if (e instanceof SettingsUpdatedEvent) loadSettings(); androidExecutor.execute(new Runnable() {
public void run() {
clearPrivateMessageNotification();
clearForumPostNotification();
}
});
} }
public void showPrivateMessageNotification(ContactId c) { private void clearPrivateMessageNotification() {
lock.lock(); Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
try { NotificationManager nm = (NotificationManager) o;
// check first if user has this conversation open at the moment nm.cancel(PRIVATE_MESSAGE_NOTIFICATION_ID);
if (activeContact == null || !activeContact.equals(c)) { }
Integer count = contactCounts.get(c);
if (count == null) contactCounts.put(c, 1); private void clearForumPostNotification() {
else contactCounts.put(c, count + 1); Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.cancel(FORUM_POST_NOTIFICATION_ID);
}
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {
loadSettings();
} else if (e instanceof MessageValidatedEvent) {
MessageValidatedEvent m = (MessageValidatedEvent) e;
if (m.isValid() && !m.isLocal()) {
ClientId c = m.getClientId();
if (c.equals(messagingManager.getClientId()))
showPrivateMessageNotification(m.getMessage().getGroupId());
else if (c.equals(forumManager.getClientId()))
showForumPostNotification(m.getMessage().getGroupId());
}
}
}
public void showPrivateMessageNotification(final GroupId g) {
androidExecutor.execute(new Runnable() {
public void run() {
Integer count = contactCounts.get(g);
if (count == null) contactCounts.put(g, 1);
else contactCounts.put(g, count + 1);
contactTotal++; contactTotal++;
if (!g.equals(visibleGroup))
updatePrivateMessageNotification();
}
});
}
public void clearPrivateMessageNotification(final GroupId g) {
androidExecutor.execute(new Runnable() {
public void run() {
Integer count = contactCounts.remove(g);
if (count == null) return; // Already cleared
contactTotal -= count;
// FIXME: If the notification isn't showing, this may show it
updatePrivateMessageNotification(); updatePrivateMessageNotification();
} }
} finally { });
lock.unlock();
}
} }
public void clearPrivateMessageNotification(ContactId c) {
lock.lock();
try {
Integer count = contactCounts.remove(c);
if (count == null) return; // Already cleared
contactTotal -= count;
updatePrivateMessageNotification();
} finally {
lock.unlock();
}
}
public void blockPrivateMessageNotification(ContactId c) {
lock.lock();
try {
activeContact = c;
} finally {
lock.unlock();
}
}
public void unblockPrivateMessageNotification(ContactId c) {
lock.lock();
try {
if (activeContact != null && activeContact.equals(c)) {
activeContact = null;
}
} finally {
lock.unlock();
}
}
// Locking: lock
private void updatePrivateMessageNotification() { private void updatePrivateMessageNotification() {
if (contactTotal == 0) { if (contactTotal == 0) {
clearPrivateMessageNotification(); clearPrivateMessageNotification();
@@ -180,9 +197,10 @@ EventListener {
b.setAutoCancel(true); b.setAutoCancel(true);
if (contactCounts.size() == 1) { if (contactCounts.size() == 1) {
Intent i = new Intent(appContext, ConversationActivity.class); Intent i = new Intent(appContext, ConversationActivity.class);
ContactId c = contactCounts.keySet().iterator().next(); GroupId g = contactCounts.keySet().iterator().next();
i.putExtra("briar.CONTACT_ID", c.getInt()); i.putExtra("briar.GROUP_ID", g.getBytes());
i.setData(Uri.parse(CONTACT_URI + "/" + c.getInt())); String idHex = StringUtils.toHexString(g.getBytes());
i.setData(Uri.parse(CONTACT_URI + "/" + idHex));
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
TaskStackBuilder t = TaskStackBuilder.create(appContext); TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(ConversationActivity.class); t.addParentStack(ConversationActivity.class);
@@ -202,13 +220,6 @@ EventListener {
} }
} }
// Locking: lock
private void clearPrivateMessageNotification() {
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.cancel(PRIVATE_MESSAGE_NOTIFICATION_ID);
}
private int getDefaults() { private int getDefaults() {
int defaults = DEFAULT_LIGHTS; int defaults = DEFAULT_LIGHTS;
boolean sound = settings.getBoolean("notifySound", true); boolean sound = settings.getBoolean("notifySound", true);
@@ -220,32 +231,31 @@ EventListener {
return defaults; return defaults;
} }
public void showForumPostNotification(GroupId g) { public void showForumPostNotification(final GroupId g) {
lock.lock(); androidExecutor.execute(new Runnable() {
try { public void run() {
Integer count = forumCounts.get(g); Integer count = forumCounts.get(g);
if (count == null) forumCounts.put(g, 1); if (count == null) forumCounts.put(g, 1);
else forumCounts.put(g, count + 1); else forumCounts.put(g, count + 1);
forumTotal++; forumTotal++;
updateForumPostNotification(); if (!g.equals(visibleGroup))
} finally { updateForumPostNotification();
lock.unlock(); }
} });
} }
public void clearForumPostNotification(GroupId g) { public void clearForumPostNotification(final GroupId g) {
lock.lock(); androidExecutor.execute(new Runnable() {
try { public void run() {
Integer count = forumCounts.remove(g); Integer count = forumCounts.remove(g);
if (count == null) return; // Already cleared if (count == null) return; // Already cleared
forumTotal -= count; forumTotal -= count;
updateForumPostNotification(); // FIXME: If the notification isn't showing, this may show it
} finally { updateForumPostNotification();
lock.unlock(); }
} });
} }
// Locking: lock
private void updateForumPostNotification() { private void updateForumPostNotification() {
if (forumTotal == 0) { if (forumTotal == 0) {
clearForumPostNotification(); clearForumPostNotification();
@@ -288,23 +298,19 @@ EventListener {
} }
} }
// Locking: lock public void blockNotification(final GroupId g) {
private void clearForumPostNotification() { androidExecutor.execute(new Runnable() {
Object o = appContext.getSystemService(NOTIFICATION_SERVICE); public void run() {
NotificationManager nm = (NotificationManager) o; visibleGroup = g;
nm.cancel(FORUM_POST_NOTIFICATION_ID); }
});
} }
public void clearNotifications() { public void unblockNotification(final GroupId g) {
lock.lock(); androidExecutor.execute(new Runnable() {
try { public void run() {
contactCounts.clear(); if (g.equals(visibleGroup)) visibleGroup = null;
forumCounts.clear(); }
contactTotal = forumTotal = 0; });
clearPrivateMessageNotification();
clearForumPostNotification();
} finally {
lock.unlock();
}
} }
} }

View File

@@ -128,6 +128,7 @@ public abstract class BaseActivity extends AppCompatActivity
return scopedObjects; return scopedObjects;
} }
// FIXME: Factor out prefs code so it can be used by SplashScreenActivity
private SharedPreferences getSharedPrefs() { private SharedPreferences getSharedPrefs() {
return getSharedPreferences(PREFS_DB, MODE_PRIVATE); return getSharedPreferences(PREFS_DB, MODE_PRIVATE);
} }

View File

@@ -11,22 +11,11 @@ import android.support.v4.app.NotificationCompat;
import org.briarproject.R; import org.briarproject.R;
import org.briarproject.api.android.AndroidExecutor; import org.briarproject.api.android.AndroidExecutor;
import org.briarproject.api.android.AndroidNotificationManager;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.db.DatabaseConfig; import org.briarproject.api.db.DatabaseConfig;
import org.briarproject.api.db.DatabaseExecutor;
import org.briarproject.api.db.DbException;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.lifecycle.LifecycleManager; import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.lifecycle.LifecycleManager.StartResult; import org.briarproject.api.lifecycle.LifecycleManager.StartResult;
import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.sync.GroupId;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -34,6 +23,7 @@ import javax.inject.Inject;
import roboguice.service.RoboService; import roboguice.service.RoboService;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
@@ -41,7 +31,7 @@ import static java.util.logging.Level.WARNING;
import static org.briarproject.api.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING; import static org.briarproject.api.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING;
import static org.briarproject.api.lifecycle.LifecycleManager.StartResult.SUCCESS; import static org.briarproject.api.lifecycle.LifecycleManager.StartResult.SUCCESS;
public class BriarService extends RoboService implements EventListener { public class BriarService extends RoboService {
private static final int ONGOING_NOTIFICATION_ID = 1; private static final int ONGOING_NOTIFICATION_ID = 1;
private static final int FAILURE_NOTIFICATION_ID = 2; private static final int FAILURE_NOTIFICATION_ID = 2;
@@ -53,14 +43,10 @@ public class BriarService extends RoboService implements EventListener {
private final Binder binder = new BriarBinder(); private final Binder binder = new BriarBinder();
@Inject private DatabaseConfig databaseConfig; @Inject private DatabaseConfig databaseConfig;
@Inject private AndroidNotificationManager notificationManager;
// Fields that are accessed from background threads must be volatile // Fields that are accessed from background threads must be volatile
@Inject private volatile LifecycleManager lifecycleManager; @Inject private volatile LifecycleManager lifecycleManager;
@Inject private volatile AndroidExecutor androidExecutor; @Inject private volatile AndroidExecutor androidExecutor;
@Inject @DatabaseExecutor private volatile Executor dbExecutor;
@Inject private volatile MessagingManager messagingManager;
@Inject private volatile EventBus eventBus;
private volatile boolean started = false; private volatile boolean started = false;
@Override @Override
@@ -95,7 +81,6 @@ public class BriarService extends RoboService implements EventListener {
public void run() { public void run() {
StartResult result = lifecycleManager.startServices(); StartResult result = lifecycleManager.startServices();
if (result == SUCCESS) { if (result == SUCCESS) {
eventBus.addListener(BriarService.this);
started = true; started = true;
} else if (result == ALREADY_RUNNING) { } else if (result == ALREADY_RUNNING) {
LOG.info("Already running"); LOG.info("Already running");
@@ -110,25 +95,34 @@ public class BriarService extends RoboService implements EventListener {
}.start(); }.start();
} }
private void showStartupFailureNotification(StartResult result) { private void showStartupFailureNotification(final StartResult result) {
NotificationCompat.Builder b = new NotificationCompat.Builder(this); androidExecutor.execute(new Runnable() {
b.setSmallIcon(android.R.drawable.stat_notify_error); public void run() {
b.setContentTitle(getText(R.string.startup_failed_notification_title)); NotificationCompat.Builder b =
b.setContentText(getText(R.string.startup_failed_notification_text)); new NotificationCompat.Builder(BriarService.this);
Intent i = new Intent(this, StartupFailureActivity.class); b.setSmallIcon(android.R.drawable.stat_notify_error);
i.setFlags(FLAG_ACTIVITY_NEW_TASK); b.setContentTitle(getText(
i.putExtra("briar.START_RESULT", result); R.string.startup_failed_notification_title));
i.putExtra("briar.FAILURE_NOTIFICATION_ID", FAILURE_NOTIFICATION_ID); b.setContentText(getText(
b.setContentIntent(PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT)); R.string.startup_failed_notification_text));
Object o = getSystemService(NOTIFICATION_SERVICE); Intent i = new Intent(BriarService.this,
NotificationManager nm = (NotificationManager) o; StartupFailureActivity.class);
nm.notify(FAILURE_NOTIFICATION_ID, b.build()); i.setFlags(FLAG_ACTIVITY_NEW_TASK);
i.putExtra("briar.START_RESULT", result);
// Bring the dashboard to the front to clear all other activities i.putExtra("briar.FAILURE_NOTIFICATION_ID",
i = new Intent(this, DashboardActivity.class); FAILURE_NOTIFICATION_ID);
i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP); b.setContentIntent(PendingIntent.getActivity(BriarService.this,
i.putExtra("briar.STARTUP_FAILED", true); 0, i, FLAG_UPDATE_CURRENT));
startActivity(i); Object o = getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.notify(FAILURE_NOTIFICATION_ID, b.build());
// Bring the dashboard to the front to clear the back stack
i = new Intent(BriarService.this, DashboardActivity.class);
i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP);
i.putExtra("briar.STARTUP_FAILED", true);
startActivity(i);
}
});
} }
@Override @Override
@@ -146,16 +140,11 @@ public class BriarService extends RoboService implements EventListener {
super.onDestroy(); super.onDestroy();
LOG.info("Destroyed"); LOG.info("Destroyed");
stopForeground(true); stopForeground(true);
notificationManager.clearNotifications();
// Stop the services in a background thread // Stop the services in a background thread
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
if (started) { if (started) lifecycleManager.stopServices();
eventBus.removeListener(BriarService.this);
lifecycleManager.stopServices();
}
androidExecutor.shutdown();
} }
}.start(); }.start();
} }
@@ -167,39 +156,6 @@ public class BriarService extends RoboService implements EventListener {
// FIXME: Work out what to do about it // FIXME: Work out what to do about it
} }
public void eventOccurred(Event e) {
if (e instanceof MessageAddedEvent) {
MessageAddedEvent m = (MessageAddedEvent) e;
GroupId g = m.getGroupId();
ContactId c = m.getContactId();
if (c != null) showMessageNotification(g, c);
}
}
private void showMessageNotification(final GroupId g, final ContactId c) {
dbExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
if (g.equals(messagingManager.getConversationId(c)))
notificationManager.showPrivateMessageNotification(c);
else notificationManager.showForumPostNotification(g);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch (InterruptedException e) {
LOG.info("Interruped while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
/** Waits for the database to be opened before returning. */
public void waitForDatabase() throws InterruptedException {
lifecycleManager.waitForDatabase();
}
/** Waits for all services to start before returning. */ /** Waits for all services to start before returning. */
public void waitForStartup() throws InterruptedException { public void waitForStartup() throws InterruptedException {
lifecycleManager.waitForStartup(); lifecycleManager.waitForStartup();

View File

@@ -74,12 +74,12 @@ public class CrashReportActivity extends AppCompatActivity implements OnClickLis
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(CrashReportActivity.class.getName()); Logger.getLogger(CrashReportActivity.class.getName());
private final AndroidExecutor androidExecutor = new AndroidExecutorImpl(); private final AndroidExecutor androidExecutor =
new AndroidExecutorImpl(getApplication());
private ScrollView scroll = null; private ScrollView scroll = null;
private ListLoadingProgressBar progress = null; private ListLoadingProgressBar progress = null;
private LinearLayout status = null; private LinearLayout status = null;
private ImageButton share = null;
private File temp = null; private File temp = null;
private volatile String stack = null; private volatile String stack = null;
@@ -120,7 +120,7 @@ public class CrashReportActivity extends AppCompatActivity implements OnClickLis
Resources res = getResources(); Resources res = getResources();
int background = res.getColor(R.color.button_bar_background); int background = res.getColor(R.color.button_bar_background);
footer.setBackgroundColor(background); footer.setBackgroundColor(background);
share = new ImageButton(this); ImageButton share = new ImageButton(this);
share.setBackgroundResource(0); share.setBackgroundResource(0);
share.setImageResource(R.drawable.social_share); share.setImageResource(R.drawable.social_share);
share.setOnClickListener(this); share.setOnClickListener(this);
@@ -323,11 +323,11 @@ public class CrashReportActivity extends AppCompatActivity implements OnClickLis
// Is Bluetooth available? // Is Bluetooth available?
BluetoothAdapter bt = null; BluetoothAdapter bt = null;
try { try {
bt = androidExecutor.call(new Callable<BluetoothAdapter>() { bt = androidExecutor.submit(new Callable<BluetoothAdapter>() {
public BluetoothAdapter call() throws Exception { public BluetoothAdapter call() throws Exception {
return BluetoothAdapter.getDefaultAdapter(); return BluetoothAdapter.getDefaultAdapter();
} }
}); }).get();
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOG.warning("Interrupted while getting BluetoothAdapter"); LOG.warning("Interrupted while getting BluetoothAdapter");
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();

View File

@@ -16,8 +16,8 @@ import com.google.inject.Injector;
import org.briarproject.R; import org.briarproject.R;
import org.briarproject.android.util.LayoutUtils; import org.briarproject.android.util.LayoutUtils;
import org.briarproject.api.db.DatabaseConfig; import org.briarproject.api.db.DatabaseConfig;
import org.briarproject.util.FileUtils;
import java.io.File;
import java.util.logging.Logger; import java.util.logging.Logger;
import roboguice.RoboGuice; import roboguice.RoboGuice;
@@ -86,8 +86,9 @@ public class SplashScreenActivity extends RoboSplashActivity {
if (hex != null && databaseConfig.databaseExists()) { if (hex != null && databaseConfig.databaseExists()) {
startActivity(new Intent(this, DashboardActivity.class)); startActivity(new Intent(this, DashboardActivity.class));
} else { } else {
prefs.edit().clear().commit(); prefs.edit().clear().apply();
delete(databaseConfig.getDatabaseDirectory()); FileUtils.deleteFileOrDir(
databaseConfig.getDatabaseDirectory());
startActivity(new Intent(this, SetupActivity.class)); startActivity(new Intent(this, SetupActivity.class));
} }
} }
@@ -106,17 +107,11 @@ public class SplashScreenActivity extends RoboSplashActivity {
} }
} }
private void delete(File f) {
if (f.isFile()) f.delete();
else if (f.isDirectory()) for (File child : f.listFiles()) delete(child);
}
private void setPreferencesDefaults() { private void setPreferencesDefaults() {
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
PreferenceManager PreferenceManager.setDefaultValues(SplashScreenActivity.this,
.setDefaultValues(SplashScreenActivity.this,
R.xml.panic_preferences, false); R.xml.panic_preferences, false);
} }
}.start(); }.start();

View File

@@ -326,11 +326,11 @@ public class TestingActivity extends BriarActivity implements OnClickListener {
// Is Bluetooth available? // Is Bluetooth available?
BluetoothAdapter bt = null; BluetoothAdapter bt = null;
try { try {
bt = androidExecutor.call(new Callable<BluetoothAdapter>() { bt = androidExecutor.submit(new Callable<BluetoothAdapter>() {
public BluetoothAdapter call() throws Exception { public BluetoothAdapter call() throws Exception {
return BluetoothAdapter.getDefaultAdapter(); return BluetoothAdapter.getDefaultAdapter();
} }
}); }).get();
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOG.warning("Interrupted while getting BluetoothAdapter"); LOG.warning("Interrupted while getting BluetoothAdapter");
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();

View File

@@ -3,7 +3,6 @@ package org.briarproject.android.contact;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.design.widget.FloatingActionButton; import android.support.design.widget.FloatingActionButton;
import android.support.v7.util.SortedList;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.view.View; import android.view.View;
@@ -23,10 +22,11 @@ import org.briarproject.api.event.ContactRemovedEvent;
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.MessageAddedEvent; import org.briarproject.api.event.MessageValidatedEvent;
import org.briarproject.api.messaging.MessagingManager; import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateMessageHeader; import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.plugins.ConnectionRegistry; import org.briarproject.api.plugins.ConnectionRegistry;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import java.util.ArrayList; import java.util.ArrayList;
@@ -36,6 +36,7 @@ import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import static android.support.v7.util.SortedList.INVALID_POSITION;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
@@ -105,18 +106,16 @@ public class ContactListActivity extends BriarActivity
for (Contact c : contactManager.getContacts()) { for (Contact c : contactManager.getContacts()) {
try { try {
ContactId id = c.getId(); ContactId id = c.getId();
GroupId conversation = GroupId groupId =
messagingManager.getConversationId(id); messagingManager.getConversationId(id);
Collection<PrivateMessageHeader> headers = Collection<PrivateMessageHeader> headers =
messagingManager.getMessageHeaders(id); messagingManager.getMessageHeaders(id);
boolean connected = boolean connected =
connectionRegistry.isConnected(c.getId()); connectionRegistry.isConnected(c.getId());
contacts.add(new ContactListItem(c, connected, contacts.add(new ContactListItem(c, connected,
conversation, groupId, headers));
headers));
} catch (NoSuchContactException e) { } catch (NoSuchContactException e) {
// Continue LOG.info("Contact removed");
} }
} }
displayContacts(contacts); displayContacts(contacts);
@@ -141,7 +140,7 @@ public class ContactListActivity extends BriarActivity
// sorting criteria and cause duplicates // sorting criteria and cause duplicates
for (ContactListItem contact : contacts) { for (ContactListItem contact : contacts) {
int position = adapter.findItemPosition(contact); int position = adapter.findItemPosition(contact);
if (position == SortedList.INVALID_POSITION) { if (position == INVALID_POSITION) {
adapter.add(contact); adapter.add(contact);
} else { } else {
adapter.updateItem(position, contact); adapter.updateItem(position, contact);
@@ -169,19 +168,22 @@ public class ContactListActivity extends BriarActivity
} else if (e instanceof ContactRemovedEvent) { } else if (e instanceof ContactRemovedEvent) {
LOG.info("Contact removed"); LOG.info("Contact removed");
removeItem(((ContactRemovedEvent) e).getContactId()); removeItem(((ContactRemovedEvent) e).getContactId());
} else if (e instanceof MessageAddedEvent) { } else if (e instanceof MessageValidatedEvent) {
LOG.info("Message added, reloading"); MessageValidatedEvent m = (MessageValidatedEvent) e;
ContactId source = ((MessageAddedEvent) e).getContactId(); ClientId c = m.getClientId();
if (source == null) loadContacts(); if (m.isValid() && c.equals(messagingManager.getClientId())) {
else reloadContact(source); LOG.info("Message added, reloading");
reloadConversation(m.getMessage().getGroupId());
}
} }
} }
private void reloadContact(final ContactId c) { private void reloadConversation(final GroupId g) {
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
public void run() { public void run() {
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
ContactId c = messagingManager.getContactId(g);
Collection<PrivateMessageHeader> headers = Collection<PrivateMessageHeader> headers =
messagingManager.getMessageHeaders(c); messagingManager.getMessageHeaders(c);
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
@@ -189,7 +191,7 @@ public class ContactListActivity extends BriarActivity
LOG.info("Partial load took " + duration + " ms"); LOG.info("Partial load took " + duration + " ms");
updateItem(c, headers); updateItem(c, headers);
} catch (NoSuchContactException e) { } catch (NoSuchContactException e) {
removeItem(c); LOG.info("Contact removed");
} 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

@@ -14,11 +14,12 @@ import android.widget.TextView;
import org.briarproject.R; import org.briarproject.R;
import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactId;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import java.util.List; import java.util.List;
import static android.support.v7.util.SortedList.INVALID_POSITION;
public class ContactListAdapter public class ContactListAdapter
extends RecyclerView.Adapter<ContactListAdapter.ContactHolder> { extends RecyclerView.Adapter<ContactListAdapter.ContactHolder> {
@@ -130,17 +131,9 @@ public class ContactListAdapter
ui.layout.setOnClickListener(new View.OnClickListener() { ui.layout.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
ContactId contactId = item.getContact().getId(); GroupId groupId = item.getGroupId();
String contactName = item.getContact().getAuthor().getName();
GroupId groupId = item.getConversationId();
AuthorId localAuthorId = item.getContact().getLocalAuthorId();
Intent i = new Intent(ctx, ConversationActivity.class); Intent i = new Intent(ctx, ConversationActivity.class);
i.putExtra("briar.CONTACT_ID", contactId.getInt());
i.putExtra("briar.CONTACT_NAME", contactName);
i.putExtra("briar.GROUP_ID", groupId.getBytes()); i.putExtra("briar.GROUP_ID", groupId.getBytes());
i.putExtra("briar.LOCAL_AUTHOR_ID", localAuthorId.getBytes());
ctx.startActivity(i); ctx.startActivity(i);
} }
}); });
@@ -156,8 +149,7 @@ public class ContactListAdapter
} }
public ContactListItem getItem(int position) { public ContactListItem getItem(int position) {
if (position == SortedList.INVALID_POSITION || if (position == INVALID_POSITION || contacts.size() <= position) {
contacts.size() <= position) {
return null; // Not found return null; // Not found
} }
return contacts.get(position); return contacts.get(position);
@@ -186,7 +178,7 @@ public class ContactListAdapter
ContactListItem item = getItem(i); ContactListItem item = getItem(i);
if (item.getContact().getId().equals(c)) return i; if (item.getContact().getId().equals(c)) return i;
} }
return SortedList.INVALID_POSITION; // Not found return INVALID_POSITION; // Not found
} }
public void addAll(final List<ContactListItem> contacts) { public void addAll(final List<ContactListItem> contacts) {

View File

@@ -10,15 +10,15 @@ import java.util.Collection;
class ContactListItem { class ContactListItem {
private final Contact contact; private final Contact contact;
private final GroupId conversation; private final GroupId groupId;
private boolean connected, empty; private boolean connected, empty;
private long timestamp; private long timestamp;
private int unread; private int unread;
ContactListItem(Contact contact, boolean connected, GroupId conversation, ContactListItem(Contact contact, boolean connected, GroupId groupId,
Collection<PrivateMessageHeader> headers) { Collection<PrivateMessageHeader> headers) {
this.contact = contact; this.contact = contact;
this.conversation = conversation; this.groupId = groupId;
this.connected = connected; this.connected = connected;
setHeaders(headers); setHeaders(headers);
} }
@@ -39,8 +39,8 @@ class ContactListItem {
return contact; return contact;
} }
GroupId getConversationId() { GroupId getGroupId() {
return conversation; return groupId;
} }
boolean isConnected() { boolean isConnected() {

View File

@@ -27,21 +27,19 @@ import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchContactException; import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.db.NoSuchMessageException; import org.briarproject.api.db.NoSuchMessageException;
import org.briarproject.api.db.NoSuchSubscriptionException;
import org.briarproject.api.event.ContactConnectedEvent; import org.briarproject.api.event.ContactConnectedEvent;
import org.briarproject.api.event.ContactDisconnectedEvent; import org.briarproject.api.event.ContactDisconnectedEvent;
import org.briarproject.api.event.ContactRemovedEvent; import org.briarproject.api.event.ContactRemovedEvent;
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.MessageAddedEvent; import org.briarproject.api.event.MessageValidatedEvent;
import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.MessagesAckedEvent;
import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.event.MessagesSentEvent;
import org.briarproject.api.messaging.MessagingManager; import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateConversation; import org.briarproject.api.messaging.PrivateMessage;
import org.briarproject.api.messaging.PrivateMessageFactory; import org.briarproject.api.messaging.PrivateMessageFactory;
import org.briarproject.api.messaging.PrivateMessageHeader; import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.messaging.PrivateMessageHeader.Status;
import org.briarproject.api.plugins.ConnectionRegistry; import org.briarproject.api.plugins.ConnectionRegistry;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.Message;
@@ -66,8 +64,6 @@ import javax.inject.Inject;
import static android.widget.Toast.LENGTH_SHORT; import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.api.messaging.PrivateMessageHeader.Status.DELIVERED;
import static org.briarproject.api.messaging.PrivateMessageHeader.Status.SENT;
public class ConversationActivity extends BriarActivity public class ConversationActivity extends BriarActivity
implements EventListener, OnClickListener { implements EventListener, OnClickListener {
@@ -89,20 +85,19 @@ public class ConversationActivity extends BriarActivity
@Inject private volatile MessagingManager messagingManager; @Inject private volatile MessagingManager messagingManager;
@Inject private volatile EventBus eventBus; @Inject private volatile EventBus eventBus;
@Inject private volatile PrivateMessageFactory privateMessageFactory; @Inject private volatile PrivateMessageFactory privateMessageFactory;
private volatile GroupId groupId = null;
private volatile ContactId contactId = null; private volatile ContactId contactId = null;
private volatile String contactName = null; private volatile String contactName = null;
private volatile GroupId groupId = null; private volatile boolean connected = false;
private volatile PrivateConversation conversation = null;
private volatile boolean connected;
@Override @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(state); super.onCreate(state);
Intent i = getIntent(); Intent i = getIntent();
int id = i.getIntExtra("briar.CONTACT_ID", -1); byte[] b = i.getByteArrayExtra("briar.GROUP_ID");
if (id == -1) throw new IllegalStateException(); if (b == null) throw new IllegalStateException();
contactId = new ContactId(id); groupId = new GroupId(b);
setContentView(R.layout.activity_conversation); setContentView(R.layout.activity_conversation);
@@ -122,19 +117,17 @@ public class ConversationActivity extends BriarActivity
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
eventBus.addListener(this); eventBus.addListener(this);
notificationManager.blockPrivateMessageNotification(contactId); notificationManager.blockNotification(groupId);
loadContactAndGroup(); notificationManager.clearPrivateMessageNotification(groupId);
loadContactDetails();
loadHeaders(); loadHeaders();
// remove the notification for this conversation since we see it now
notificationManager.clearPrivateMessageNotification(contactId);
} }
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
eventBus.removeListener(this); eventBus.removeListener(this);
notificationManager.unblockPrivateMessageNotification(contactId); notificationManager.unblockNotification(groupId);
if (isFinishing()) markMessagesRead(); if (isFinishing()) markMessagesRead();
} }
@@ -164,26 +157,21 @@ public class ConversationActivity extends BriarActivity
} }
} }
private void loadContactAndGroup() { private void loadContactDetails() {
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
public void run() { public void run() {
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
contactId = messagingManager.getContactId(groupId);
Contact contact = contactManager.getContact(contactId); Contact contact = contactManager.getContact(contactId);
contactName = contact.getAuthor().getName(); contactName = contact.getAuthor().getName();
groupId = messagingManager.getConversationId(contactId);
conversation = messagingManager.getConversation(groupId);
connected = connectionRegistry.isConnected(contactId); connected = connectionRegistry.isConnected(contactId);
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO))
LOG.info("Loading contact and conversation took " LOG.info("Loading contact took " + duration + " ms");
+ duration + " ms");
}
displayContactDetails(); displayContactDetails();
} catch (NoSuchContactException e) { } catch (NoSuchContactException e) {
finishOnUiThread(); finishOnUiThread();
} catch (NoSuchSubscriptionException e) {
finishOnUiThread();
} 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);
@@ -234,7 +222,11 @@ public class ConversationActivity extends BriarActivity
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
sendButton.setEnabled(true); sendButton.setEnabled(true);
if (!headers.isEmpty()) { if (headers.isEmpty()) {
// we have no messages,
// so let the list know to hide progress bar
list.showData();
} else {
for (PrivateMessageHeader h : headers) { for (PrivateMessageHeader h : headers) {
ConversationItem item = new ConversationItem(h); ConversationItem item = new ConversationItem(h);
byte[] body = bodyCache.get(h.getId()); byte[] body = bodyCache.get(h.getId());
@@ -244,10 +236,6 @@ public class ConversationActivity extends BriarActivity
} }
// Scroll to the bottom // Scroll to the bottom
list.scrollToPosition(adapter.getItemCount() - 1); list.scrollToPosition(adapter.getItemCount() - 1);
} else {
// we have no messages,
// so let the list know to hide progress bar
list.showData();
} }
} }
}); });
@@ -278,17 +266,13 @@ public class ConversationActivity extends BriarActivity
public void run() { public void run() {
bodyCache.put(m, body); bodyCache.put(m, body);
int count = adapter.getItemCount(); int count = adapter.getItemCount();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
ConversationItem item = adapter.getItem(i); ConversationItem item = adapter.getItem(i);
if (item.getHeader().getId().equals(m)) { if (item.getHeader().getId().equals(m)) {
item.setBody(body); item.setBody(body);
adapter.notifyItemChanged(i); adapter.notifyItemChanged(i);
// Scroll to the bottom // Scroll to the bottom
list.scrollToPosition(count - 1); list.scrollToPosition(count - 1);
return; return;
} }
} }
@@ -297,7 +281,6 @@ public class ConversationActivity extends BriarActivity
} }
private void markMessagesRead() { private void markMessagesRead() {
notificationManager.clearPrivateMessageNotification(contactId);
List<MessageId> unread = new ArrayList<MessageId>(); List<MessageId> unread = new ArrayList<MessageId>();
int count = adapter.getItemCount(); int count = adapter.getItemCount();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@@ -335,35 +318,25 @@ public class ConversationActivity extends BriarActivity
LOG.info("Contact removed"); LOG.info("Contact removed");
finishOnUiThread(); finishOnUiThread();
} }
} else if (e instanceof MessageAddedEvent) { } else if (e instanceof MessageValidatedEvent) {
MessageAddedEvent mEvent = (MessageAddedEvent) e; MessageValidatedEvent m = (MessageValidatedEvent) e;
GroupId g = mEvent.getGroupId(); if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) {
if (g.equals(groupId)) {
// mark new incoming messages as read directly
if (mEvent.getContactId() != null) {
ConversationItem item = adapter.getLastItem();
if (item != null) {
markIncomingMessageRead(mEvent.getMessage(),
item.getHeader().getTimestamp());
}
}
LOG.info("Message added, reloading"); LOG.info("Message added, reloading");
// TODO: get and add the ConversationItem here to prevent // Mark new incoming messages as read directly
// reloading the entire conversation if (m.isLocal()) loadHeaders();
loadHeaders(); else markMessageReadIfNew(m.getMessage());
} }
} else if (e instanceof MessagesSentEvent) { } else if (e instanceof MessagesSentEvent) {
MessagesSentEvent m = (MessagesSentEvent) e; MessagesSentEvent m = (MessagesSentEvent) e;
if (m.getContactId().equals(contactId)) { if (m.getContactId().equals(contactId)) {
LOG.info("Messages sent"); LOG.info("Messages sent");
markMessages(m.getMessageIds(), SENT); markMessages(m.getMessageIds(), true, false);
} }
} else if (e instanceof MessagesAckedEvent) { } else if (e instanceof MessagesAckedEvent) {
MessagesAckedEvent m = (MessagesAckedEvent) e; MessagesAckedEvent m = (MessagesAckedEvent) e;
if (m.getContactId().equals(contactId)) { if (m.getContactId().equals(contactId)) {
LOG.info("Messages acked"); LOG.info("Messages acked");
markMessages(m.getMessageIds(), DELIVERED); markMessages(m.getMessageIds(), true, true);
} }
} else if (e instanceof ContactConnectedEvent) { } else if (e instanceof ContactConnectedEvent) {
ContactConnectedEvent c = (ContactConnectedEvent) e; ContactConnectedEvent c = (ContactConnectedEvent) e;
@@ -382,8 +355,37 @@ public class ConversationActivity extends BriarActivity
} }
} }
private void markMessageReadIfNew(final Message m) {
runOnUiThread(new Runnable() {
public void run() {
ConversationItem item = adapter.getLastItem();
if (item != null) {
// Mark the message read if it's the newest message
long lastMsgTime = item.getHeader().getTimestamp();
long newMsgTime = m.getTimestamp();
if (newMsgTime > lastMsgTime) markNewMessageRead(m);
else loadHeaders();
}
}
});
}
private void markNewMessageRead(final Message m) {
runOnDbThread(new Runnable() {
public void run() {
try {
messagingManager.setReadFlag(m.getId(), true);
loadHeaders();
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void markMessages(final Collection<MessageId> messageIds, private void markMessages(final Collection<MessageId> messageIds,
final Status status) { final boolean sent, final boolean seen) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
Set<MessageId> messages = new HashSet<MessageId>(messageIds); Set<MessageId> messages = new HashSet<MessageId>(messageIds);
@@ -391,7 +393,8 @@ public class ConversationActivity extends BriarActivity
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
ConversationItem item = adapter.getItem(i); ConversationItem item = adapter.getItem(i);
if (messages.contains(item.getHeader().getId())) { if (messages.contains(item.getHeader().getId())) {
item.setStatus(status); item.setSent(sent);
item.setSeen(seen);
adapter.notifyItemChanged(i); adapter.notifyItemChanged(i);
} }
} }
@@ -399,41 +402,8 @@ public class ConversationActivity extends BriarActivity
}); });
} }
private void markIncomingMessageRead(final Message m,
final long lastMsgTime) {
// stop here if message is older than latest message we have
long newMsgTime = m.getTimestamp();
if (newMsgTime < lastMsgTime) return;
runOnDbThread(new Runnable() {
public void run() {
try {
// mark messages as read, because is latest
messagingManager.setReadFlag(m.getId(), true);
showIncomingMessageRead();
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
// TODO else: smooth-scroll up to unread messages if out of view
});
}
private void showIncomingMessageRead() {
runOnUiThread(new Runnable() {
public void run() {
// this is only called from markIncomingMessageRead()
// so we can assume that it was the last message that changed
adapter.notifyItemChanged(adapter.getItemCount() - 1);
}
});
}
public void onClick(View view) { public void onClick(View view) {
markMessagesRead(); markMessagesRead();
String message = content.getText().toString(); String message = content.getText().toString();
if (message.equals("")) return; if (message.equals("")) return;
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
@@ -445,21 +415,16 @@ public class ConversationActivity extends BriarActivity
private long getMinTimestampForNewMessage() { private long getMinTimestampForNewMessage() {
// Don't use an earlier timestamp than the newest message // Don't use an earlier timestamp than the newest message
long timestamp = 0;
ConversationItem item = adapter.getLastItem(); ConversationItem item = adapter.getLastItem();
if (item != null) { return item == null ? 0 : item.getHeader().getTimestamp() + 1;
timestamp = item.getHeader().getTimestamp();
}
return timestamp + 1;
} }
private void createMessage(final byte[] body, final long timestamp) { private void createMessage(final byte[] body, final long timestamp) {
cryptoExecutor.execute(new Runnable() { cryptoExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {
Message m = privateMessageFactory.createPrivateMessage(null, storeMessage(privateMessageFactory.createPrivateMessage(
conversation, "text/plain", timestamp, body); groupId, timestamp, null, "text/plain", body));
storeMessage(m);
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} catch (IOException e) { } catch (IOException e) {
@@ -469,7 +434,7 @@ public class ConversationActivity extends BriarActivity
}); });
} }
private void storeMessage(final Message m) { private void storeMessage(final PrivateMessage m) {
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
public void run() { public void run() {
try { try {
@@ -487,29 +452,20 @@ public class ConversationActivity extends BriarActivity
} }
private void askToRemoveContact() { private void askToRemoveContact() {
runOnUiThread(new Runnable() { DialogInterface.OnClickListener okListener =
@Override new DialogInterface.OnClickListener() {
public void run() { @Override
DialogInterface.OnClickListener okListener = public void onClick(DialogInterface dialog, int which) {
new DialogInterface.OnClickListener() { removeContact();
@Override }
public void onClick(DialogInterface dialog, };
int which) { AlertDialog.Builder builder =
removeContact(); new AlertDialog.Builder(ConversationActivity.this);
} builder.setTitle(getString(R.string.dialog_title_delete_contact));
}; builder.setMessage(getString(R.string.dialog_message_delete_contact));
builder.setPositiveButton(android.R.string.ok, okListener);
AlertDialog.Builder builder = builder.setNegativeButton(android.R.string.cancel, null);
new AlertDialog.Builder(ConversationActivity.this); builder.show();
builder.setTitle(
getString(R.string.dialog_title_delete_contact));
builder.setMessage(
getString(R.string.dialog_message_delete_contact));
builder.setPositiveButton(android.R.string.ok, okListener);
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
});
} }
private void removeContact() { private void removeContact() {

View File

@@ -14,9 +14,6 @@ import org.briarproject.R;
import org.briarproject.api.messaging.PrivateMessageHeader; import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
import static org.briarproject.api.messaging.PrivateMessageHeader.Status.DELIVERED;
import static org.briarproject.api.messaging.PrivateMessageHeader.Status.SENT;
class ConversationAdapter extends class ConversationAdapter extends
RecyclerView.Adapter<ConversationAdapter.MessageHolder> { RecyclerView.Adapter<ConversationAdapter.MessageHolder> {
@@ -113,18 +110,18 @@ class ConversationAdapter extends
PrivateMessageHeader header = item.getHeader(); PrivateMessageHeader header = item.getHeader();
if (header.isLocal()) { if (header.isLocal()) {
if (item.getStatus() == DELIVERED) { if (item.isSeen()) {
ui.status.setImageResource(R.drawable.message_delivered); ui.status.setImageResource(R.drawable.message_delivered);
} else if (item.getStatus() == SENT) { } else if (item.isSent()) {
ui.status.setImageResource(R.drawable.message_sent); ui.status.setImageResource(R.drawable.message_sent);
} else { } else {
ui.status.setImageResource(R.drawable.message_stored); ui.status.setImageResource(R.drawable.message_stored);
} }
} else if (!header.isRead()) { } else if (!header.isRead()) {
int bottom = ui.layout.getPaddingBottom(); int left = ui.layout.getPaddingLeft();
int top = ui.layout.getPaddingTop(); int top = ui.layout.getPaddingTop();
int right = ui.layout.getPaddingRight(); int right = ui.layout.getPaddingRight();
int left = ui.layout.getPaddingLeft(); int bottom = ui.layout.getPaddingBottom();
// show unread messages in different color to not miss them // show unread messages in different color to not miss them
ui.layout.setBackgroundResource(R.drawable.msg_in_unread); ui.layout.setBackgroundResource(R.drawable.msg_in_unread);
@@ -185,7 +182,9 @@ class ConversationAdapter extends
this.messages.endBatchedUpdates(); this.messages.endBatchedUpdates();
} }
// TODO: Does this class need to be public?
public static class MessageHolder extends RecyclerView.ViewHolder { public static class MessageHolder extends RecyclerView.ViewHolder {
public ViewGroup layout; public ViewGroup layout;
public TextView body; public TextView body;
public TextView date; public TextView date;

View File

@@ -1,19 +1,19 @@
package org.briarproject.android.contact; package org.briarproject.android.contact;
import org.briarproject.api.messaging.PrivateMessageHeader; import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.messaging.PrivateMessageHeader.Status;
// This class is not thread-safe // This class is not thread-safe
class ConversationItem { class ConversationItem {
private final PrivateMessageHeader header; private final PrivateMessageHeader header;
private byte[] body; private byte[] body;
private Status status; private boolean sent, seen;
ConversationItem(PrivateMessageHeader header) { ConversationItem(PrivateMessageHeader header) {
this.header = header; this.header = header;
body = null; body = null;
status = header.getStatus(); sent = header.isSent();
seen = header.isSeen();
} }
PrivateMessageHeader getHeader() { PrivateMessageHeader getHeader() {
@@ -28,11 +28,19 @@ class ConversationItem {
this.body = body; this.body = body;
} }
Status getStatus() { boolean isSent() {
return status; return sent;
} }
void setStatus(Status status) { void setSent(boolean sent) {
this.status = status; this.sent = sent;
}
boolean isSeen() {
return seen;
}
void setSeen(boolean seen) {
this.seen = seen;
} }
} }

View File

@@ -18,7 +18,6 @@ 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.ForumFactory;
import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumManager;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
@@ -38,7 +37,7 @@ import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH; import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP; import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import static org.briarproject.api.sync.MessagingConstants.MAX_GROUP_NAME_LENGTH; import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
public class CreateForumActivity extends BriarActivity public class CreateForumActivity extends BriarActivity
implements OnEditorActionListener, OnClickListener { implements OnEditorActionListener, OnClickListener {
@@ -52,7 +51,6 @@ 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 ForumFactory forumFactory;
@Inject private volatile ForumManager forumManager; @Inject private volatile ForumManager forumManager;
@Override @Override
@@ -115,8 +113,9 @@ implements OnEditorActionListener, OnClickListener {
} }
private boolean validateName() { private boolean validateName() {
int length = StringUtils.toUtf8(nameEntry.getText().toString()).length; String name = nameEntry.getText().toString();
if (length > MAX_GROUP_NAME_LENGTH) { int length = StringUtils.toUtf8(name).length;
if (length > MAX_FORUM_NAME_LENGTH) {
feedback.setText(R.string.name_too_long); feedback.setText(R.string.name_too_long);
return false; return false;
} }
@@ -138,8 +137,8 @@ implements OnEditorActionListener, OnClickListener {
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
public void run() { public void run() {
try { try {
Forum f = forumFactory.createForum(name);
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
Forum f = forumManager.createForum(name);
forumManager.addForum(f); forumManager.addForum(f);
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))

View File

@@ -24,7 +24,7 @@ import org.briarproject.api.db.NoSuchSubscriptionException;
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.MessageAddedEvent; import org.briarproject.api.event.MessageValidatedEvent;
import org.briarproject.api.event.SubscriptionRemovedEvent; import org.briarproject.api.event.SubscriptionRemovedEvent;
import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumManager;
@@ -56,7 +56,7 @@ import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1; import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
public class ForumActivity extends BriarActivity implements EventListener, public class ForumActivity extends BriarActivity implements EventListener,
OnClickListener, OnItemClickListener { OnClickListener, OnItemClickListener {
private static final int REQUEST_READ = 2; private static final int REQUEST_READ = 2;
private static final Logger LOG = private static final Logger LOG =
@@ -143,6 +143,8 @@ OnClickListener, OnItemClickListener {
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
eventBus.addListener(this); eventBus.addListener(this);
notificationManager.blockNotification(groupId);
notificationManager.clearForumPostNotification(groupId);
loadForum(); loadForum();
loadHeaders(); loadHeaders();
} }
@@ -276,11 +278,11 @@ OnClickListener, OnItemClickListener {
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
eventBus.removeListener(this); eventBus.removeListener(this);
notificationManager.unblockNotification(groupId);
if (isFinishing()) markPostsRead(); if (isFinishing()) markPostsRead();
} }
private void markPostsRead() { private void markPostsRead() {
notificationManager.clearForumPostNotification(groupId);
List<MessageId> unread = new ArrayList<MessageId>(); List<MessageId> unread = new ArrayList<MessageId>();
int count = adapter.getCount(); int count = adapter.getCount();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@@ -312,8 +314,9 @@ OnClickListener, OnItemClickListener {
} }
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (e instanceof MessageAddedEvent) { if (e instanceof MessageValidatedEvent) {
if (((MessageAddedEvent) e).getGroupId().equals(groupId)) { MessageValidatedEvent m = (MessageValidatedEvent) e;
if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) {
LOG.info("Message added, reloading"); LOG.info("Message added, reloading");
loadHeaders(); loadHeaders();
} }

View File

@@ -28,19 +28,18 @@ import org.briarproject.api.db.NoSuchSubscriptionException;
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.MessageAddedEvent; import org.briarproject.api.event.MessageValidatedEvent;
import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent; import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
import org.briarproject.api.event.SubscriptionAddedEvent; import org.briarproject.api.event.SubscriptionAddedEvent;
import org.briarproject.api.event.SubscriptionRemovedEvent; import org.briarproject.api.event.SubscriptionRemovedEvent;
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.sync.ClientId;
import org.briarproject.api.sync.Group; import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import java.util.Collection; import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
@@ -60,16 +59,13 @@ import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1; import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
public class ForumListActivity extends BriarActivity public class ForumListActivity extends BriarActivity
implements EventListener, OnClickListener, OnItemClickListener, implements EventListener, OnClickListener, OnItemClickListener,
OnCreateContextMenuListener { OnCreateContextMenuListener {
private static final int MENU_ITEM_UNSUBSCRIBE = 1; private static final int MENU_ITEM_UNSUBSCRIBE = 1;
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(ForumListActivity.class.getName()); Logger.getLogger(ForumListActivity.class.getName());
private final Map<GroupId, GroupId> groupIds =
new ConcurrentHashMap<GroupId, GroupId>();
private TextView empty = null; private TextView empty = null;
private ForumListAdapter adapter = null; private ForumListAdapter adapter = null;
private ListView list = null; private ListView list = null;
@@ -179,7 +175,6 @@ OnCreateContextMenuListener {
private void clearHeaders() { private void clearHeaders() {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
groupIds.clear();
empty.setVisibility(GONE); empty.setVisibility(GONE);
list.setVisibility(GONE); list.setVisibility(GONE);
available.setVisibility(GONE); available.setVisibility(GONE);
@@ -194,12 +189,10 @@ OnCreateContextMenuListener {
final Collection<ForumPostHeader> headers) { final Collection<ForumPostHeader> headers) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
GroupId id = f.getId();
groupIds.put(id, id);
list.setVisibility(VISIBLE); list.setVisibility(VISIBLE);
loading.setVisibility(GONE); loading.setVisibility(GONE);
// Remove the old item, if any // Remove the old item, if any
ForumListItem item = findForum(id); ForumListItem item = findForum(f.getId());
if (item != null) adapter.remove(item); if (item != null) adapter.remove(item);
// Add a new item // Add a new item
adapter.add(new ForumListItem(f, headers)); adapter.add(new ForumListItem(f, headers));
@@ -255,11 +248,12 @@ OnCreateContextMenuListener {
} }
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (e instanceof MessageAddedEvent) { if (e instanceof MessageValidatedEvent) {
GroupId g = ((MessageAddedEvent) e).getGroupId(); MessageValidatedEvent m = (MessageValidatedEvent) e;
if (groupIds.containsKey(g)) { ClientId c = m.getClientId();
if (m.isValid() && c.equals(forumManager.getClientId())) {
LOG.info("Message added, reloading"); LOG.info("Message added, reloading");
loadHeaders(g); loadHeaders(m.getMessage().getGroupId());
} }
} else if (e instanceof RemoteSubscriptionsUpdatedEvent) { } else if (e instanceof RemoteSubscriptionsUpdatedEvent) {
LOG.info("Remote subscriptions changed, reloading"); LOG.info("Remote subscriptions changed, reloading");
@@ -269,7 +263,7 @@ OnCreateContextMenuListener {
loadHeaders(); loadHeaders();
} else if (e instanceof SubscriptionRemovedEvent) { } else if (e instanceof SubscriptionRemovedEvent) {
Group g = ((SubscriptionRemovedEvent) e).getGroup(); Group g = ((SubscriptionRemovedEvent) e).getGroup();
if (groupIds.containsKey(g.getId())) { if (g.getClientId().equals(forumManager.getClientId())) {
LOG.info("Group removed, reloading"); LOG.info("Group removed, reloading");
loadHeaders(); loadHeaders();
} }
@@ -303,7 +297,6 @@ OnCreateContextMenuListener {
public void run() { public void run() {
ForumListItem item = findForum(g); ForumListItem item = findForum(g);
if (item != null) { if (item != null) {
groupIds.remove(g);
adapter.remove(item); adapter.remove(item);
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
if (adapter.isEmpty()) { if (adapter.isEmpty()) {

View File

@@ -30,12 +30,12 @@ import org.briarproject.api.crypto.PrivateKey;
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.ForumManager;
import org.briarproject.api.forum.ForumPost;
import org.briarproject.api.forum.ForumPostFactory; import org.briarproject.api.forum.ForumPostFactory;
import org.briarproject.api.identity.AuthorId; import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
@@ -267,35 +267,35 @@ implements OnItemSelectedListener, OnClickListener {
// Don't use an earlier timestamp than the newest post // Don't use an earlier timestamp than the newest post
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, minTimestamp); timestamp = Math.max(timestamp, minTimestamp);
Message m; ForumPost p;
try { try {
if (localAuthor == null) { if (localAuthor == null) {
m = forumPostFactory.createAnonymousPost(parentId, p = forumPostFactory.createAnonymousPost(groupId,
forum, "text/plain", timestamp, body); timestamp, parentId, "text/plain", body);
} else { } else {
KeyParser keyParser = crypto.getSignatureKeyParser(); KeyParser keyParser = crypto.getSignatureKeyParser();
byte[] b = localAuthor.getPrivateKey(); byte[] b = localAuthor.getPrivateKey();
PrivateKey authorKey = keyParser.parsePrivateKey(b); PrivateKey authorKey = keyParser.parsePrivateKey(b);
m = forumPostFactory.createPseudonymousPost(parentId, p = forumPostFactory.createPseudonymousPost(groupId,
forum, localAuthor, authorKey, "text/plain", timestamp, parentId, localAuthor, "text/plain",
timestamp, body); body, authorKey);
} }
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
storePost(m); storePost(p);
} }
}); });
} }
private void storePost(final Message m) { private void storePost(final ForumPost p) {
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
public void run() { public void run() {
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
forumManager.addLocalPost(m); forumManager.addLocalPost(p);
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Storing message took " + duration + " ms"); LOG.info("Storing message took " + duration + " ms");

View File

@@ -110,11 +110,11 @@ class DroidtoothPlugin implements DuplexPlugin {
// BluetoothAdapter.getDefaultAdapter() must be called on a thread // BluetoothAdapter.getDefaultAdapter() must be called on a thread
// with a message queue, so submit it to the AndroidExecutor // with a message queue, so submit it to the AndroidExecutor
try { try {
adapter = androidExecutor.call(new Callable<BluetoothAdapter>() { adapter = androidExecutor.submit(new Callable<BluetoothAdapter>() {
public BluetoothAdapter call() throws Exception { public BluetoothAdapter call() throws Exception {
return BluetoothAdapter.getDefaultAdapter(); return BluetoothAdapter.getDefaultAdapter();
} }
}); }).get();
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new IOException("Interrupted while getting BluetoothAdapter"); throw new IOException("Interrupted while getting BluetoothAdapter");

View File

@@ -1,6 +1,7 @@
package org.briarproject.api; package org.briarproject.api;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator;
public abstract class UniqueId { public abstract class UniqueId {
@@ -27,4 +28,20 @@ public abstract class UniqueId {
if (hashCode == -1) hashCode = Arrays.hashCode(id); if (hashCode == -1) hashCode = Arrays.hashCode(id);
return hashCode; return hashCode;
} }
public static class IdComparator implements Comparator<UniqueId> {
public static final IdComparator INSTANCE = new IdComparator();
@Override
public int compare(UniqueId a, UniqueId b) {
byte[] aBytes = a.getBytes(), bBytes = b.getBytes();
for (int i = 0; i < UniqueId.LENGTH; i++) {
int aUnsigned = aBytes[i] & 0xFF, bUnsigned = bBytes[i] & 0xFF;
if (aUnsigned < bUnsigned) return -1;
if (aUnsigned > bUnsigned) return 1;
}
return 0;
}
}
} }

View File

@@ -1,7 +1,7 @@
package org.briarproject.api.android; package org.briarproject.api.android;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.Future;
/** /**
* Enables background threads to make Android API calls that must be made from * Enables background threads to make Android API calls that must be made from
@@ -10,10 +10,11 @@ import java.util.concurrent.ExecutionException;
public interface AndroidExecutor { public interface AndroidExecutor {
/** /**
* Runs the given task on a thread with a message queue and returns the * Runs the given task on the main UI thread and returns a Future for
* result of the task. * getting the result.
*/ */
<V> V call(Callable<V> c) throws InterruptedException, ExecutionException; <V> Future<V> submit(Callable<V> c);
void shutdown(); /** Runs the given task on the main UI thread. */
void execute(Runnable r);
} }

View File

@@ -1,26 +1,20 @@
package org.briarproject.api.android; package org.briarproject.api.android;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.lifecycle.Service; import org.briarproject.api.lifecycle.Service;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
/** /** Manages notifications for private messages and forum posts. */
* Manages notifications for private messages and group posts. All methods must
* be called from the Android UI thread.
*/
public interface AndroidNotificationManager extends Service { public interface AndroidNotificationManager extends Service {
void showPrivateMessageNotification(ContactId c); void showPrivateMessageNotification(GroupId g);
void clearPrivateMessageNotification(ContactId c); void clearPrivateMessageNotification(GroupId g);
void blockPrivateMessageNotification(ContactId c);
void unblockPrivateMessageNotification(ContactId c);
void showForumPostNotification(GroupId g); void showForumPostNotification(GroupId g);
void clearForumPostNotification(GroupId g); void clearForumPostNotification(GroupId g);
void clearNotifications(); void blockNotification(GroupId g);
void unblockNotification(GroupId g);
} }

View File

@@ -57,9 +57,6 @@ public interface CryptoComponent {
*/ */
byte[] deriveSignatureNonce(SecretKey master, boolean alice); byte[] deriveSignatureNonce(SecretKey master, boolean alice);
/** Derives a group salt from the given master secret. */
byte[] deriveGroupSalt(SecretKey master);
/** /**
* Derives initial transport keys for the given transport in the given * Derives initial transport keys for the given transport in the given
* rotation period from the given master secret. * rotation period from the given master secret.
@@ -77,6 +74,12 @@ public interface CryptoComponent {
/** Encodes the pseudo-random tag that is used to recognise a stream. */ /** Encodes the pseudo-random tag that is used to recognise a stream. */
void encodeTag(byte[] tag, SecretKey tagKey, long streamNumber); void encodeTag(byte[] tag, SecretKey tagKey, long streamNumber);
/**
* Returns the hash of the given inputs. The inputs are unambiguously
* combined by prefixing each input with its length.
*/
byte[] hash(byte[]... inputs);
/** /**
* Encrypts and authenticates the given plaintext so it can be written to * Encrypts and authenticates the given plaintext so it can be written to
* storage. The encryption and authentication keys are derived from the * storage. The encryption and authentication keys are derived from the

View File

@@ -1,9 +1,18 @@
package org.briarproject.api.data; package org.briarproject.api.data;
import java.util.HashMap; import org.briarproject.api.FormatException;
// This class is not thread-safe import java.util.Hashtable;
public class BdfDictionary extends HashMap<String, Object> {
public class BdfDictionary extends Hashtable<String, Object> {
public static final Object NULL_VALUE = new Object();
public Boolean getBoolean(String key) throws FormatException {
Object o = get(key);
if (o instanceof Boolean) return (Boolean) o;
throw new FormatException();
}
public Boolean getBoolean(String key, Boolean defaultValue) { public Boolean getBoolean(String key, Boolean defaultValue) {
Object o = get(key); Object o = get(key);
@@ -11,36 +20,72 @@ public class BdfDictionary extends HashMap<String, Object> {
return defaultValue; return defaultValue;
} }
public Long getInteger(String key) throws FormatException {
Object o = get(key);
if (o instanceof Long) return (Long) o;
throw new FormatException();
}
public Long getInteger(String key, Long defaultValue) { public Long getInteger(String key, Long defaultValue) {
Object o = get(key); Object o = get(key);
if (o instanceof Long) return (Long) o; if (o instanceof Long) return (Long) o;
return defaultValue; return defaultValue;
} }
public Double getFloat(String key) throws FormatException {
Object o = get(key);
if (o instanceof Double) return (Double) o;
throw new FormatException();
}
public Double getFloat(String key, Double defaultValue) { public Double getFloat(String key, Double defaultValue) {
Object o = get(key); Object o = get(key);
if (o instanceof Double) return (Double) o; if (o instanceof Double) return (Double) o;
return defaultValue; return defaultValue;
} }
public String getString(String key) throws FormatException {
Object o = get(key);
if (o instanceof String) return (String) o;
throw new FormatException();
}
public String getString(String key, String defaultValue) { public String getString(String key, String defaultValue) {
Object o = get(key); Object o = get(key);
if (o instanceof String) return (String) o; if (o instanceof String) return (String) o;
return defaultValue; return defaultValue;
} }
public byte[] getRaw(String key) throws FormatException {
Object o = get(key);
if (o instanceof byte[]) return (byte[]) o;
throw new FormatException();
}
public byte[] getRaw(String key, byte[] defaultValue) { public byte[] getRaw(String key, byte[] defaultValue) {
Object o = get(key); Object o = get(key);
if (o instanceof byte[]) return (byte[]) o; if (o instanceof byte[]) return (byte[]) o;
return defaultValue; return defaultValue;
} }
public BdfList getList(String key) throws FormatException {
Object o = get(key);
if (o instanceof BdfList) return (BdfList) o;
throw new FormatException();
}
public BdfList getList(String key, BdfList defaultValue) { public BdfList getList(String key, BdfList defaultValue) {
Object o = get(key); Object o = get(key);
if (o instanceof BdfList) return (BdfList) o; if (o instanceof BdfList) return (BdfList) o;
return defaultValue; return defaultValue;
} }
public BdfDictionary getDictionary(String key) throws FormatException {
Object o = get(key);
if (o instanceof BdfDictionary) return (BdfDictionary) o;
throw new FormatException();
}
public BdfDictionary getDictionary(String key, BdfDictionary defaultValue) { public BdfDictionary getDictionary(String key, BdfDictionary defaultValue) {
Object o = get(key); Object o = get(key);
if (o instanceof BdfDictionary) return (BdfDictionary) o; if (o instanceof BdfDictionary) return (BdfDictionary) o;

View File

@@ -1,9 +1,16 @@
package org.briarproject.api.data; package org.briarproject.api.data;
import java.util.ArrayList; import org.briarproject.api.FormatException;
// This class is not thread-safe import java.util.Vector;
public class BdfList extends ArrayList<Object> {
public class BdfList extends Vector<Object> {
public Boolean getBoolean(int index) throws FormatException {
Object o = get(index);
if (o instanceof Boolean) return (Boolean) o;
throw new FormatException();
}
public Boolean getBoolean(int index, Boolean defaultValue) { public Boolean getBoolean(int index, Boolean defaultValue) {
Object o = get(index); Object o = get(index);
@@ -11,36 +18,72 @@ public class BdfList extends ArrayList<Object> {
return defaultValue; return defaultValue;
} }
public Long getInteger(int index) throws FormatException {
Object o = get(index);
if (o instanceof Long) return (Long) o;
throw new FormatException();
}
public Long getInteger(int index, Long defaultValue) { public Long getInteger(int index, Long defaultValue) {
Object o = get(index); Object o = get(index);
if (o instanceof Long) return (Long) o; if (o instanceof Long) return (Long) o;
return defaultValue; return defaultValue;
} }
public Double getFloat(int index) throws FormatException {
Object o = get(index);
if (o instanceof Double) return (Double) o;
throw new FormatException();
}
public Double getFloat(int index, Double defaultValue) { public Double getFloat(int index, Double defaultValue) {
Object o = get(index); Object o = get(index);
if (o instanceof Double) return (Double) o; if (o instanceof Double) return (Double) o;
return defaultValue; return defaultValue;
} }
public String getString(int index) throws FormatException {
Object o = get(index);
if (o instanceof String) return (String) o;
throw new FormatException();
}
public String getString(int index, String defaultValue) { public String getString(int index, String defaultValue) {
Object o = get(index); Object o = get(index);
if (o instanceof String) return (String) o; if (o instanceof String) return (String) o;
return defaultValue; return defaultValue;
} }
public byte[] getRaw(int index) throws FormatException {
Object o = get(index);
if (o instanceof byte[]) return (byte[]) o;
throw new FormatException();
}
public byte[] getRaw(int index, byte[] defaultValue) { public byte[] getRaw(int index, byte[] defaultValue) {
Object o = get(index); Object o = get(index);
if (o instanceof byte[]) return (byte[]) o; if (o instanceof byte[]) return (byte[]) o;
return defaultValue; return defaultValue;
} }
public BdfList getList(int index) throws FormatException {
Object o = get(index);
if (o instanceof BdfList) return (BdfList) o;
throw new FormatException();
}
public BdfList getList(int index, BdfList defaultValue) { public BdfList getList(int index, BdfList defaultValue) {
Object o = get(index); Object o = get(index);
if (o instanceof BdfList) return (BdfList) o; if (o instanceof BdfList) return (BdfList) o;
return defaultValue; return defaultValue;
} }
public BdfDictionary getDictionary(int index) throws FormatException {
Object o = get(index);
if (o instanceof BdfDictionary) return (BdfDictionary) o;
throw new FormatException();
}
public BdfDictionary getDictionary(int index, BdfDictionary defaultValue) { public BdfDictionary getDictionary(int index, BdfDictionary defaultValue) {
Object o = get(index); Object o = get(index);
if (o instanceof BdfDictionary) return (BdfDictionary) o; if (o instanceof BdfDictionary) return (BdfDictionary) o;

View File

@@ -7,9 +7,6 @@ public interface BdfReader {
boolean eof() throws IOException; boolean eof() throws IOException;
void close() throws IOException; void close() throws IOException;
void addConsumer(Consumer c);
void removeConsumer(Consumer c);
boolean hasNull() throws IOException; boolean hasNull() throws IOException;
void readNull() throws IOException; void readNull() throws IOException;
void skipNull() throws IOException; void skipNull() throws IOException;

View File

@@ -1,12 +0,0 @@
package org.briarproject.api.data;
import org.briarproject.api.UniqueId;
public interface DataConstants {
int LIST_START_LENGTH = 1;
int LIST_END_LENGTH = 1;
int UNIQUE_ID_LENGTH = 2 + UniqueId.LENGTH;
}

View File

@@ -9,11 +9,12 @@ import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorId; import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.Ack; import org.briarproject.api.sync.Ack;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group; 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.MessageHeader;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.MessageStatus;
import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.Offer;
import org.briarproject.api.sync.Request; import org.briarproject.api.sync.Request;
import org.briarproject.api.sync.SubscriptionAck; import org.briarproject.api.sync.SubscriptionAck;
@@ -44,9 +45,12 @@ public interface DatabaseComponent {
*/ */
ContactId addContact(Author remote, AuthorId local) throws DbException; ContactId addContact(Author remote, AuthorId local) throws DbException;
/** Adds a group to the given contact's subscriptions. */
void addContactGroup(ContactId c, Group g) throws DbException;
/** /**
* Subscribes to a group, or returns false if the user already has the * Subscribes to a group, or returns false if the user already has the
* maximum number of public subscriptions. * maximum number of subscriptions.
*/ */
boolean addGroup(Group g) throws DbException; boolean addGroup(Group g) throws DbException;
@@ -54,7 +58,8 @@ public interface DatabaseComponent {
void addLocalAuthor(LocalAuthor a) throws DbException; void addLocalAuthor(LocalAuthor a) throws DbException;
/** Stores a local message. */ /** Stores a local message. */
void addLocalMessage(Message m) throws DbException; void addLocalMessage(Message m, ClientId c, Metadata meta)
throws DbException;
/** /**
* Stores a transport and returns true if the transport was not previously * Stores a transport and returns true if the transport was not previously
@@ -135,8 +140,11 @@ public interface DatabaseComponent {
Collection<TransportUpdate> generateTransportUpdates(ContactId c, Collection<TransportUpdate> generateTransportUpdates(ContactId c,
int maxLatency) throws DbException; int maxLatency) throws DbException;
/** Returns all groups to which the user could subscribe. */ /**
Collection<Group> getAvailableGroups() throws DbException; * Returns all groups belonging to the given client to which the user could
* subscribe.
*/
Collection<Group> getAvailableGroups(ClientId c) throws DbException;
/** Returns the contact with the given ID. */ /** Returns the contact with the given ID. */
Contact getContact(ContactId c) throws DbException; Contact getContact(ContactId c) throws DbException;
@@ -147,21 +155,11 @@ public interface DatabaseComponent {
/** Returns the group with the given ID, if the user subscribes to it. */ /** Returns the group with the given ID, if the user subscribes to it. */
Group getGroup(GroupId g) throws DbException; Group getGroup(GroupId g) throws DbException;
/** Returns all groups to which the user subscribes, excluding inboxes. */
Collection<Group> getGroups() throws DbException;
/** /**
* Returns the ID of the inbox group for the given contact, or null if no * Returns all groups belonging to the given client to which the user
* inbox group has been set. * subscribes.
*/ */
GroupId getInboxGroupId(ContactId c) throws DbException; Collection<Group> getGroups(ClientId c) throws DbException;
/**
* Returns the headers of all messages in the inbox group for the given
* contact, or null if no inbox group has been set.
*/
Collection<MessageHeader> getInboxMessageHeaders(ContactId c)
throws DbException;
/** Returns the local pseudonym with the given ID. */ /** Returns the local pseudonym with the given ID. */
LocalAuthor getLocalAuthor(AuthorId a) throws DbException; LocalAuthor getLocalAuthor(AuthorId a) throws DbException;
@@ -176,15 +174,35 @@ public interface DatabaseComponent {
/** Returns the local transport properties for the given transport. */ /** Returns the local transport properties for the given transport. */
TransportProperties getLocalProperties(TransportId t) throws DbException; TransportProperties getLocalProperties(TransportId t) throws DbException;
/** Returns the body of the message with the given ID. */ /**
byte[] getMessageBody(MessageId m) throws DbException; * Returns the IDs of any messages that need to be validated by the given
* client.
*/
Collection<MessageId> getMessagesToValidate(ClientId c) throws DbException;
/** Returns the headers of all messages in the given group. */ /** Returns the message with the given ID, in serialised form. */
Collection<MessageHeader> getMessageHeaders(GroupId g) byte[] getRawMessage(MessageId m) throws DbException;
/** Returns the metadata for all messages in the given group. */
Map<MessageId, Metadata> getMessageMetadata(GroupId g)
throws DbException; throws DbException;
/** Returns true if the given message is marked as read. */ /** Returns the metadata for the given message. */
boolean getReadFlag(MessageId m) throws DbException; Metadata getMessageMetadata(MessageId m) throws DbException;
/**
* Returns the status of all messages in the given group with respect to
* the given contact.
*/
Collection<MessageStatus> getMessageStatus(ContactId c, GroupId g)
throws DbException;
/**
* Returns the status of the given message with respect to the given
* contact.
*/
MessageStatus getMessageStatus(ContactId c, MessageId m)
throws DbException;
/** Returns all remote transport properties for the given transport. */ /** Returns all remote transport properties for the given transport. */
Map<ContactId, TransportProperties> getRemoteProperties(TransportId t) Map<ContactId, TransportProperties> getRemoteProperties(TransportId t)
@@ -203,9 +221,6 @@ public interface DatabaseComponent {
/** Returns the maximum latencies in milliseconds of all transports. */ /** Returns the maximum latencies in milliseconds of all transports. */
Map<TransportId, Integer> getTransportLatencies() throws DbException; Map<TransportId, Integer> getTransportLatencies() throws DbException;
/** Returns the number of unread messages in each subscribed group. */
Map<GroupId, Integer> getUnreadMessageCounts() throws DbException;
/** Returns the IDs of all contacts to which the given group is visible. */ /** Returns the IDs of all contacts to which the given group is visible. */
Collection<ContactId> getVisibility(GroupId g) throws DbException; Collection<ContactId> getVisibility(GroupId g) throws DbException;
@@ -223,6 +238,12 @@ public interface DatabaseComponent {
void mergeLocalProperties(TransportId t, TransportProperties p) void mergeLocalProperties(TransportId t, TransportProperties p)
throws DbException; throws DbException;
/**
* Merges the given metadata with the existing metadata for the given
* message.
*/
void mergeMessageMetadata(MessageId m, Metadata meta) throws DbException;
/** /**
* Merges the given settings with the existing settings in the given * Merges the given settings with the existing settings in the given
* namespace. * namespace.
@@ -276,16 +297,9 @@ public interface DatabaseComponent {
*/ */
void removeTransport(TransportId t) throws DbException; void removeTransport(TransportId t) throws DbException;
/** /** Marks the given message as valid or invalid. */
* Makes a group visible to the given contact, adds it to the contact's void setMessageValidity(Message m, ClientId c, boolean valid)
* subscriptions, and sets it as the inbox group for the contact. throws DbException;
*/
void setInboxGroup(ContactId c, Group g) throws DbException;
/**
* Marks a message as read or unread.
*/
void setReadFlag(MessageId m, boolean read) throws DbException;
/** /**
* Sets the remote transport properties for the given contact, replacing * Sets the remote transport properties for the given contact, replacing

View File

@@ -0,0 +1,8 @@
package org.briarproject.api.db;
/**
* Thrown when a duplicate message is added to the database. This exception may
* occur due to concurrent updates and does not indicate a database error.
*/
public class MessageExistsException extends DbException {
}

View File

@@ -22,7 +22,7 @@ public class MessageAddedEvent extends Event {
/** Returns the ID of the group to which the message belongs. */ /** Returns the ID of the group to which the message belongs. */
public GroupId getGroupId() { public GroupId getGroupId() {
return message.getGroup().getId(); return message.getGroupId();
} }
/** /**

View File

@@ -0,0 +1,38 @@
package org.briarproject.api.event;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Message;
/**
* An event that is broadcast when a message has passed or failed validation.
*/
public class MessageValidatedEvent extends Event {
private final Message message;
private final ClientId clientId;
private final boolean local, valid;
public MessageValidatedEvent(Message message, ClientId clientId,
boolean local, boolean valid) {
this.message = message;
this.clientId = clientId;
this.local = local;
this.valid = valid;
}
public Message getMessage() {
return message;
}
public ClientId getClientId() {
return clientId;
}
public boolean isLocal() {
return local;
}
public boolean isValid() {
return valid;
}
}

View File

@@ -1,10 +1,37 @@
package org.briarproject.api.forum; package org.briarproject.api.forum;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
public interface Forum { public class Forum {
GroupId getId(); private final Group group;
private final String name;
String getName(); public Forum(Group group, String name) {
this.group = group;
this.name = name;
}
public GroupId getId() {
return group.getId();
}
public Group getGroup() {
return group;
}
public String getName() {
return name;
}
@Override
public int hashCode() {
return group.hashCode();
}
@Override
public boolean equals(Object o) {
return o instanceof Forum && group.equals(((Forum) o).group);
}
} }

View File

@@ -0,0 +1,19 @@
package org.briarproject.api.forum;
import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
public interface ForumConstants {
/** The maximum length of a forum's name in bytes. */
int MAX_FORUM_NAME_LENGTH = MAX_GROUP_DESCRIPTOR_LENGTH - 10;
/** The length of a forum's random salt in bytes. */
int FORUM_SALT_LENGTH = 32;
/** The maximum length of a forum post's content type in bytes. */
int MAX_CONTENT_TYPE_LENGTH = 50;
/** The maximum length of a forum post's body in bytes. */
int MAX_FORUM_POST_BODY_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024;
}

View File

@@ -1,7 +0,0 @@
package org.briarproject.api.forum;
public interface ForumFactory {
/** Creates a forum with the given name and a random salt. */
Forum createForum(String name);
}

View File

@@ -3,14 +3,20 @@ package org.briarproject.api.forum;
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.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import java.util.Collection; import java.util.Collection;
public interface ForumManager { public interface ForumManager {
/** Returns the unique ID of the forum client. */
ClientId getClientId();
/** Creates a forum with the given name. */
Forum createForum(String name);
/** /**
* Subscribes to a forum, or returns false if the user already has the * Subscribes to a forum, or returns false if the user already has the
* maximum number of forum subscriptions. * maximum number of forum subscriptions.
@@ -18,7 +24,7 @@ public interface ForumManager {
boolean addForum(Forum f) throws DbException; boolean addForum(Forum f) throws DbException;
/** Stores a local forum post. */ /** Stores a local forum post. */
void addLocalPost(Message m) throws DbException; void addLocalPost(ForumPost p) throws DbException;
/** Returns all forums to which the user could subscribe. */ /** Returns all forums to which the user could subscribe. */
Collection<Forum> getAvailableForums() throws DbException; Collection<Forum> getAvailableForums() throws DbException;

View File

@@ -0,0 +1,37 @@
package org.briarproject.api.forum;
import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
public class ForumPost {
private final Message message;
private final MessageId parent;
private final Author author;
private final String contentType;
public ForumPost(Message message, MessageId parent, Author author,
String contentType) {
this.message = message;
this.parent = parent;
this.author = author;
this.contentType = contentType;
}
public Message getMessage() {
return message;
}
public MessageId getParent() {
return parent;
}
public Author getAuthor() {
return author;
}
public String getContentType() {
return contentType;
}
}

View File

@@ -2,7 +2,7 @@ package org.briarproject.api.forum;
import org.briarproject.api.crypto.PrivateKey; import org.briarproject.api.crypto.PrivateKey;
import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import java.io.IOException; import java.io.IOException;
@@ -10,12 +10,12 @@ import java.security.GeneralSecurityException;
public interface ForumPostFactory { public interface ForumPostFactory {
Message createAnonymousPost(MessageId parent, Forum forum, ForumPost createAnonymousPost(GroupId groupId, long timestamp,
String contentType, long timestamp, byte[] body) throws IOException, MessageId parent, String contentType, byte[] body)
GeneralSecurityException; throws IOException, GeneralSecurityException;
Message createPseudonymousPost(MessageId parent, Forum forum, ForumPost createPseudonymousPost(GroupId groupId, long timestamp,
Author author, PrivateKey privateKey, String contentType, MessageId parent, Author author, String contentType, byte[] body,
long timestamp, byte[] body) throws IOException, PrivateKey privateKey) throws IOException,
GeneralSecurityException; GeneralSecurityException;
} }

View File

@@ -3,17 +3,46 @@ package org.briarproject.api.forum;
import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
public interface ForumPostHeader { public class ForumPostHeader {
MessageId getId(); private final MessageId id;
private final long timestamp;
private final Author author;
private final Author.Status authorStatus;
private final String contentType;
private final boolean read;
Author getAuthor(); public ForumPostHeader(MessageId id, long timestamp, Author author,
Author.Status authorStatus, String contentType, boolean read) {
this.id = id;
this.timestamp = timestamp;
this.author = author;
this.authorStatus = authorStatus;
this.contentType = contentType;
this.read = read;
}
Author.Status getAuthorStatus(); public MessageId getId() {
return id;
}
String getContentType(); public Author getAuthor() {
return author;
}
long getTimestamp(); public Author.Status getAuthorStatus() {
return authorStatus;
}
boolean isRead(); public String getContentType() {
return contentType;
}
public long getTimestamp() {
return timestamp;
}
public boolean isRead() {
return read;
}
} }

View File

@@ -2,22 +2,25 @@ package org.briarproject.api.identity;
import org.briarproject.api.UniqueId; import org.briarproject.api.UniqueId;
import java.nio.charset.Charset;
import java.util.Arrays; import java.util.Arrays;
/** /**
* Type-safe wrapper for a byte array that uniquely identifies an * Type-safe wrapper for a byte array that uniquely identifies an
* {@link Author}. * {@link org.briarproject.api.identity.Author Author}.
*/ */
public class AuthorId extends UniqueId { public class AuthorId extends UniqueId {
/** Label for hashing authors to calculate their identities. */
public static final byte[] LABEL =
"AUTHOR_ID".getBytes(Charset.forName("US-ASCII"));
public AuthorId(byte[] id) { public AuthorId(byte[] id) {
super(id); super(id);
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o instanceof AuthorId) return o instanceof AuthorId && Arrays.equals(id, ((AuthorId) o).id);
return Arrays.equals(id, ((AuthorId) o).id);
return false;
} }
} }

View File

@@ -0,0 +1,12 @@
package org.briarproject.api.messaging;
import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
public interface MessagingConstants {
/** The maximum length of a private message's content type in bytes. */
int MAX_CONTENT_TYPE_LENGTH = 50;
/** The maximum length of a private message's body in bytes. */
int MAX_PRIVATE_MESSAGE_BODY_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024;
}

View File

@@ -1,37 +1,35 @@
package org.briarproject.api.messaging; package org.briarproject.api.messaging;
import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactId;
import org.briarproject.api.crypto.SecretKey;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import java.util.Collection; import java.util.Collection;
public interface MessagingManager { public interface MessagingManager {
/** Returns the unique ID of the messaging client. */
ClientId getClientId();
/** /**
* Informs the messaging manager that a new contact has been added. * Informs the messaging manager that a new contact has been added.
* Creates a private conversation with the contact. * Creates a private conversation with the contact.
*/ */
void addContact(ContactId c, SecretKey master) throws DbException; void addContact(ContactId c) throws DbException;
/** Stores a local private message. */ /** Stores a local private message. */
void addLocalMessage(Message m) throws DbException; void addLocalMessage(PrivateMessage m) throws DbException;
/** Returns the private conversation with the given ID. */ /** Returns the ID of the contact with the given private conversation. */
PrivateConversation getConversation(GroupId g) throws DbException; ContactId getContactId(GroupId g) throws DbException;
/** /** Returns the ID of the private conversation with the given contact. */
* Returns the ID of the private conversation with the given contact, or
* null if no private conversation ID has been set.
*/
GroupId getConversationId(ContactId c) throws DbException; GroupId getConversationId(ContactId c) throws DbException;
/** /**
* Returns the headers of all messages in the private conversation with the * Returns the headers of all messages in the given private conversation.
* given contact, or null if no private conversation ID has been set.
*/ */
Collection<PrivateMessageHeader> getMessageHeaders(ContactId c) Collection<PrivateMessageHeader> getMessageHeaders(ContactId c)
throws DbException; throws DbException;
@@ -39,13 +37,6 @@ public interface MessagingManager {
/** Returns the body of the private message with the given ID. */ /** Returns the body of the private message with the given ID. */
byte[] getMessageBody(MessageId m) throws DbException; byte[] getMessageBody(MessageId m) throws DbException;
/**
* Makes a private conversation visible to the given contact, adds it to
* the contact's subscriptions, and sets it as the private conversation for
* the contact.
*/
void setConversation(ContactId c, PrivateConversation p) throws DbException;
/** Marks a private message as read or unread. */ /** Marks a private message as read or unread. */
void setReadFlag(MessageId m, boolean read) throws DbException; void setReadFlag(MessageId m, boolean read) throws DbException;
} }

View File

@@ -1,8 +1,33 @@
package org.briarproject.api.messaging; package org.briarproject.api.messaging;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
public interface PrivateConversation { // TODO: Remove if no longer needed
public class PrivateConversation {
GroupId getId(); private final Group group;
public PrivateConversation(Group group) {
this.group = group;
}
public GroupId getId() {
return group.getId();
}
public Group getGroup() {
return group;
}
@Override
public int hashCode() {
return group.hashCode();
}
@Override
public boolean equals(Object o) {
return o instanceof PrivateConversation
&& group.equals(((PrivateConversation) o).group);
}
} }

View File

@@ -0,0 +1,30 @@
package org.briarproject.api.messaging;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
public class PrivateMessage {
private final Message message;
private final MessageId parent;
private final String contentType;
public PrivateMessage(Message message, MessageId parent,
String contentType) {
this.message = message;
this.parent = parent;
this.contentType = contentType;
}
public Message getMessage() {
return message;
}
public MessageId getParent() {
return parent;
}
public String getContentType() {
return contentType;
}
}

View File

@@ -1,6 +1,6 @@
package org.briarproject.api.messaging; package org.briarproject.api.messaging;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import java.io.IOException; import java.io.IOException;
@@ -8,8 +8,7 @@ import java.security.GeneralSecurityException;
public interface PrivateMessageFactory { public interface PrivateMessageFactory {
Message createPrivateMessage(MessageId parent, PrivateMessage createPrivateMessage(GroupId groupId, long timestamp,
PrivateConversation conversation, String contentType, MessageId parent, String contentType, byte[] body)
long timestamp, byte[] body) throws IOException, throws IOException, GeneralSecurityException;
GeneralSecurityException;
} }

View File

@@ -1,23 +1,51 @@
package org.briarproject.api.messaging; package org.briarproject.api.messaging;
import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
public interface PrivateMessageHeader { public class PrivateMessageHeader {
enum Status { STORED, SENT, DELIVERED } private final MessageId id;
private final long timestamp;
private final String contentType;
private final boolean local, read, sent, seen;
MessageId getId(); public PrivateMessageHeader(MessageId id, long timestamp,
String contentType, boolean local, boolean read, boolean sent,
boolean seen) {
this.id = id;
this.timestamp = timestamp;
this.contentType = contentType;
this.local = local;
this.read = read;
this.sent = sent;
this.seen = seen;
}
Author getAuthor(); public MessageId getId() {
return id;
}
String getContentType(); public String getContentType() {
return contentType;
}
long getTimestamp(); public long getTimestamp() {
return timestamp;
}
boolean isLocal(); public boolean isLocal() {
return local;
}
boolean isRead(); public boolean isRead() {
return read;
}
Status getStatus(); public boolean isSent() {
return sent;
}
public boolean isSeen() {
return seen;
}
} }

View File

@@ -0,0 +1,20 @@
package org.briarproject.api.sync;
import org.briarproject.api.UniqueId;
import java.util.Arrays;
/**
* Type-safe wrapper for a byte array that uniquely identifies a sync client.
*/
public class ClientId extends UniqueId {
public ClientId(byte[] id) {
super(id);
}
@Override
public boolean equals(Object o) {
return o instanceof ClientId && Arrays.equals(id, ((ClientId) o).id);
}
}

View File

@@ -1,28 +1,20 @@
package org.briarproject.api.sync; package org.briarproject.api.sync;
import java.io.UnsupportedEncodingException; import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
/** A group to which users may subscribe. */ /** A group to which users may subscribe. */
public class Group { public class Group {
private final GroupId id; private final GroupId id;
private final String name; private final ClientId clientId;
private final byte[] salt; private final byte[] descriptor;
public Group(GroupId id, String name, byte[] salt) { public Group(GroupId id, ClientId clientId, byte[] descriptor) {
int length; if (descriptor.length > MAX_GROUP_DESCRIPTOR_LENGTH)
try {
length = name.getBytes("UTF-8").length;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
if (length == 0 || length > MessagingConstants.MAX_GROUP_NAME_LENGTH)
throw new IllegalArgumentException();
if (salt.length != MessagingConstants.GROUP_SALT_LENGTH)
throw new IllegalArgumentException(); throw new IllegalArgumentException();
this.id = id; this.id = id;
this.name = name; this.clientId = clientId;
this.salt = salt; this.descriptor = descriptor;
} }
/** Returns the group's unique identifier. */ /** Returns the group's unique identifier. */
@@ -30,17 +22,14 @@ public class Group {
return id; return id;
} }
/** Returns the group's name. */ /** Returns the ID of the client to which the group belongs. */
public String getName() { public ClientId getClientId() {
return name; return clientId;
} }
/** /** Returns the group's descriptor. */
* Returns the salt used to distinguish the group from other groups with public byte[] getDescriptor() {
* the same name. return descriptor;
*/
public byte[] getSalt() {
return salt;
} }
@Override @Override

View File

@@ -2,9 +2,6 @@ package org.briarproject.api.sync;
public interface GroupFactory { public interface GroupFactory {
/** Creates a group with the given name and a random salt. */ /** Creates a group with the given client ID and descriptor. */
Group createGroup(String name); Group createGroup(ClientId c, byte[] descriptor);
/** Creates a group with the given name and salt. */
Group createGroup(String name, byte[] salt);
} }

View File

@@ -2,6 +2,7 @@ package org.briarproject.api.sync;
import org.briarproject.api.UniqueId; import org.briarproject.api.UniqueId;
import java.nio.charset.Charset;
import java.util.Arrays; import java.util.Arrays;
/** /**
@@ -9,14 +10,16 @@ import java.util.Arrays;
*/ */
public class GroupId extends UniqueId { public class GroupId extends UniqueId {
/** Label for hashing groups to calculate their identifiers. */
public static final byte[] LABEL =
"GROUP_ID".getBytes(Charset.forName("US-ASCII"));
public GroupId(byte[] id) { public GroupId(byte[] id) {
super(id); super(id);
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o instanceof GroupId) return o instanceof GroupId && Arrays.equals(id, ((GroupId) o).id);
return Arrays.equals(id, ((GroupId) o).id);
return false;
} }
} }

View File

@@ -1,42 +1,58 @@
package org.briarproject.api.sync; package org.briarproject.api.sync;
import org.briarproject.api.identity.Author; import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_LENGTH;
import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
public interface Message { public class Message {
private final MessageId id;
private final GroupId groupId;
private final long timestamp;
private final byte[] raw;
public Message(MessageId id, GroupId groupId, long timestamp, byte[] raw) {
if (raw.length <= MESSAGE_HEADER_LENGTH)
throw new IllegalArgumentException();
if (raw.length > MAX_MESSAGE_LENGTH)
throw new IllegalArgumentException();
this.id = id;
this.groupId = groupId;
this.timestamp = timestamp;
this.raw = raw;
}
/** Returns the message's unique identifier. */ /** Returns the message's unique identifier. */
MessageId getId(); public MessageId getId() {
return id;
}
/** /** Returns the ID of the {@link Group} to which the message belongs. */
* Returns the identifier of the message's parent, or null if this is the public GroupId getGroupId() {
* first message in a thread. return groupId;
*/ }
MessageId getParent();
/**
* Returns the {@link Group} to which the message belongs, or null if this
* is a private message.
*/
Group getGroup();
/**
* Returns the message's {@link Author Author}, or null
* if this is an anonymous message.
*/
Author getAuthor();
/** Returns the message's content type. */
String getContentType();
/** Returns the message's timestamp in milliseconds since the Unix epoch. */ /** Returns the message's timestamp in milliseconds since the Unix epoch. */
long getTimestamp(); public long getTimestamp() {
return timestamp;
}
/** Returns the serialised message. */ /** Returns the length of the raw message in bytes. */
byte[] getSerialised(); public int getLength() {
return raw.length;
}
/** Returns the offset of the message body within the serialised message. */ /** Returns the raw message. */
int getBodyStart(); public byte[] getRaw() {
return raw;
}
/** Returns the length of the message body in bytes. */ @Override
int getBodyLength(); public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object o) {
return o instanceof Message && id.equals(((Message) o).getId());
}
} }

View File

@@ -1,19 +1,9 @@
package org.briarproject.api.sync; package org.briarproject.api.sync;
import org.briarproject.api.crypto.PrivateKey;
import org.briarproject.api.identity.Author;
import java.io.IOException; import java.io.IOException;
import java.security.GeneralSecurityException;
public interface MessageFactory { public interface MessageFactory {
Message createAnonymousMessage(MessageId parent, Group group, Message createMessage(GroupId groupId, long timestamp, byte[] body)
String contentType, long timestamp, byte[] body) throws IOException, throws IOException;
GeneralSecurityException;
Message createPseudonymousMessage(MessageId parent, Group group,
Author author, PrivateKey privateKey, String contentType,
long timestamp, byte[] body) throws IOException,
GeneralSecurityException;
} }

View File

@@ -1,92 +0,0 @@
package org.briarproject.api.sync;
import org.briarproject.api.identity.Author;
public class MessageHeader {
public enum State { STORED, SENT, DELIVERED }
private final MessageId id, parent;
private final GroupId groupId;
private final Author author;
private final Author.Status authorStatus;
private final String contentType;
private final long timestamp;
private final boolean local, read;
private final State status;
public MessageHeader(MessageId id, MessageId parent, GroupId groupId,
Author author, Author.Status authorStatus, String contentType,
long timestamp, boolean local, boolean read, State status) {
this.id = id;
this.parent = parent;
this.groupId = groupId;
this.author = author;
this.authorStatus = authorStatus;
this.contentType = contentType;
this.timestamp = timestamp;
this.local = local;
this.read = read;
this.status = status;
}
/** Returns the message's unique identifier. */
public MessageId getId() {
return id;
}
/**
* Returns the message's parent, or null if this is the first message in a
* thread.
*/
public MessageId getParent() {
return parent;
}
/**
* Returns the unique identifier of the group to which the message belongs.
*/
public GroupId getGroupId() {
return groupId;
}
/**
* Returns the message's author, or null if this is an anonymous message.
*/
public Author getAuthor() {
return author;
}
/** Returns the status of the message's author. */
public Author.Status getAuthorStatus() {
return authorStatus;
}
/** Returns the message's content type. */
public String getContentType() {
return contentType;
}
/** Returns the timestamp created by the message's author. */
public long getTimestamp() {
return timestamp;
}
/** Returns true if the message was locally generated. */
public boolean isLocal() {
return local;
}
/** Returns true if the message has been read. */
public boolean isRead() {
return read;
}
/**
* Returns message status. (This only applies to locally generated private
* messages.)
*/
public State getStatus() {
return status;
}
}

View File

@@ -2,6 +2,7 @@ package org.briarproject.api.sync;
import org.briarproject.api.UniqueId; import org.briarproject.api.UniqueId;
import java.nio.charset.Charset;
import java.util.Arrays; import java.util.Arrays;
/** /**
@@ -10,14 +11,16 @@ import java.util.Arrays;
*/ */
public class MessageId extends UniqueId { public class MessageId extends UniqueId {
/** Label for hashing messages to calculate their identifiers. */
public static final byte[] LABEL =
"MESSAGE_ID".getBytes(Charset.forName("US-ASCII"));
public MessageId(byte[] id) { public MessageId(byte[] id) {
super(id); super(id);
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o instanceof MessageId) return o instanceof MessageId && Arrays.equals(id, ((MessageId) o).id);
return Arrays.equals(id, ((MessageId) o).id);
return false;
} }
} }

View File

@@ -0,0 +1,38 @@
package org.briarproject.api.sync;
import org.briarproject.api.contact.ContactId;
public class MessageStatus {
private final MessageId messageId;
private final ContactId contactId;
private final boolean sent, seen;
public MessageStatus(MessageId messageId, ContactId contactId,
boolean sent, boolean seen) {
this.messageId = messageId;
this.contactId = contactId;
this.sent = sent;
this.seen = seen;
}
/** Returns the ID of the message. */
public MessageId getMessageId() {
return messageId;
}
/** Returns the ID of the contact. */
public ContactId getContactId() {
return contactId;
}
/** Returns true if the message has been sent to the contact. */
public boolean isSent() {
return sent;
}
/** Returns true if the message has been seen by the contact. */
public boolean isSeen() {
return seen;
}
}

View File

@@ -0,0 +1,13 @@
package org.briarproject.api.sync;
import org.briarproject.api.db.Metadata;
import org.briarproject.api.lifecycle.Service;
public interface MessageValidator extends Service {
/**
* Validates the given message and returns its metadata if the message
* is valid, or null if the message is invalid.
*/
Metadata validateMessage(Message m);
}

View File

@@ -1,9 +0,0 @@
package org.briarproject.api.sync;
import java.security.GeneralSecurityException;
/** Verifies the signatures on an {@link UnverifiedMessage}. */
public interface MessageVerifier {
Message verifyMessage(UnverifiedMessage m) throws GeneralSecurityException;
}

View File

@@ -1,46 +0,0 @@
package org.briarproject.api.sync;
public interface MessagingConstants {
/** The current version of the messaging protocol. */
byte PROTOCOL_VERSION = 0;
/** The length of the packet header in bytes. */
int HEADER_LENGTH = 4;
/** The maximum length of the packet payload in bytes. */
int MAX_PAYLOAD_LENGTH = 32 * 1024; // 32 KiB
/** The maximum number of public groups a user may subscribe to. */
int MAX_SUBSCRIPTIONS = 300;
/** The maximum length of a group's name in UTF-8 bytes. */
int MAX_GROUP_NAME_LENGTH = 50;
/** The length of a group's random salt in bytes. */
int GROUP_SALT_LENGTH = 32;
/**
* The maximum length of a message body in bytes. To allow for future
* changes in the protocol, this is smaller than the maximum payload length
* even when all the message's other fields have their maximum lengths.
*/
int MAX_BODY_LENGTH = MAX_PAYLOAD_LENGTH - 1024;
/** The maximum length of a message's content type in UTF-8 bytes. */
int MAX_CONTENT_TYPE_LENGTH = 50;
/** The maximum length of a message's subject line in UTF-8 bytes. */
int MAX_SUBJECT_LENGTH = 100;
/** The length of a message's random salt in bytes. */
int MESSAGE_SALT_LENGTH = 32;
/**
* When calculating the retention time of the database, the timestamp of
* the oldest message in the database is rounded down to a multiple of
* this value to avoid revealing the presence of any particular message.
*/
int RETENTION_GRANULARITY = 60 * 1000; // 1 minute
}

View File

@@ -10,7 +10,7 @@ public interface PacketReader {
Ack readAck() throws IOException; Ack readAck() throws IOException;
boolean hasMessage() throws IOException; boolean hasMessage() throws IOException;
UnverifiedMessage readMessage() throws IOException; Message readMessage() throws IOException;
boolean hasOffer() throws IOException; boolean hasOffer() throws IOException;
Offer readOffer() throws IOException; Offer readOffer() throws IOException;

View File

@@ -1,6 +1,6 @@
package org.briarproject.api.sync; package org.briarproject.api.sync;
/** Packet types for the messaging protocol. */ /** Packet types for the sync protocol. */
public interface PacketTypes { public interface PacketTypes {
byte ACK = 0; byte ACK = 0;

View File

@@ -0,0 +1,30 @@
package org.briarproject.api.sync;
import org.briarproject.api.UniqueId;
public interface SyncConstants {
/** The current version of the sync protocol. */
byte PROTOCOL_VERSION = 0;
/** The length of the packet header in bytes. */
int PACKET_HEADER_LENGTH = 4;
/** The maximum length of the packet payload in bytes. */
int MAX_PACKET_PAYLOAD_LENGTH = 32 * 1024; // 32 KiB
/** The maximum number of groups a user may subscribe to. */
int MAX_SUBSCRIPTIONS = 200;
/** The maximum length of a group descriptor in bytes. */
int MAX_GROUP_DESCRIPTOR_LENGTH = 100;
/** The maximum length of a message in bytes. */
int MAX_MESSAGE_LENGTH = MAX_PACKET_PAYLOAD_LENGTH - PACKET_HEADER_LENGTH;
/** The length of the message header in bytes. */
int MESSAGE_HEADER_LENGTH = UniqueId.LENGTH + 8;
/** The maximum length of a message body in bytes. */
int MAX_MESSAGE_BODY_LENGTH = MAX_MESSAGE_LENGTH - MESSAGE_HEADER_LENGTH;
}

View File

@@ -2,7 +2,7 @@ package org.briarproject.api.sync;
import java.io.IOException; import java.io.IOException;
public interface MessagingSession { public interface SyncSession {
/** /**
* Runs the session. This method returns when there are no more packets to * Runs the session. This method returns when there are no more packets to

View File

@@ -6,14 +6,14 @@ import org.briarproject.api.contact.ContactId;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
public interface MessagingSessionFactory { public interface SyncSessionFactory {
MessagingSession createIncomingSession(ContactId c, TransportId t, SyncSession createIncomingSession(ContactId c, TransportId t,
InputStream in); InputStream in);
MessagingSession createSimplexOutgoingSession(ContactId c, TransportId t, SyncSession createSimplexOutgoingSession(ContactId c, TransportId t,
int maxLatency, OutputStream out); int maxLatency, OutputStream out);
MessagingSession createDuplexOutgoingSession(ContactId c, TransportId t, SyncSession createDuplexOutgoingSession(ContactId c, TransportId t,
int maxLatency, int maxIdleTime, OutputStream out); int maxLatency, int maxIdleTime, OutputStream out);
} }

View File

@@ -1,94 +0,0 @@
package org.briarproject.api.sync;
import org.briarproject.api.identity.Author;
/** A {@link Message} that has not yet had its signatures (if any) verified. */
public class UnverifiedMessage {
private final MessageId parent;
private final Group group;
private final Author author;
private final String contentType;
private final long timestamp;
private final byte[] raw, signature;
private final int bodyStart, bodyLength, signedLength;
public UnverifiedMessage(MessageId parent, Group group, Author author,
String contentType, long timestamp, byte[] raw, byte[] signature,
int bodyStart, int bodyLength, int signedLength) {
this.parent = parent;
this.group = group;
this.author = author;
this.contentType = contentType;
this.timestamp = timestamp;
this.raw = raw;
this.signature = signature;
this.bodyStart = bodyStart;
this.bodyLength = bodyLength;
this.signedLength = signedLength;
}
/**
* Returns the identifier of the message's parent, or null if this is the
* first message in a thread.
*/
public MessageId getParent() {
return parent;
}
/**
* Returns the {@link Group} to which the message belongs, or null if this
* is a private message.
*/
public Group getGroup() {
return group;
}
/**
* Returns the message's {@link Author Author}, or null
* if this is an anonymous message.
*/
public Author getAuthor() {
return author;
}
/** Returns the message's content type. */
public String getContentType() {
return contentType;
}
/** Returns the message's timestamp. */
public long getTimestamp() {
return timestamp;
}
/** Returns the serialised message. */
public byte[] getSerialised() {
return raw;
}
/**
* Returns the author's signature, or null if this is an anonymous message.
*/
public byte[] getSignature() {
return signature;
}
/** Returns the offset of the message body within the serialised message. */
public int getBodyStart() {
return bodyStart;
}
/** Returns the length of the message body in bytes. */
public int getBodyLength() {
return bodyLength;
}
/**
* Returns the length in bytes of the data covered by the author's
* signature.
*/
public int getSignedLength() {
return signedLength;
}
}

View File

@@ -0,0 +1,13 @@
package org.briarproject.api.sync;
import org.briarproject.api.lifecycle.Service;
/**
* Responsible for managing message validators and passing them messages to
* validate.
*/
public interface ValidationManager extends Service {
/** Sets the message validator for the given client. */
void setMessageValidator(ClientId c, MessageValidator v);
}

View File

@@ -73,8 +73,6 @@ class CryptoComponentImpl implements CryptoComponent {
// KDF labels for signature nonce derivation // KDF labels for signature nonce derivation
private static final byte[] A_NONCE = ascii("ALICE_SIGNATURE_NONCE"); private static final byte[] A_NONCE = ascii("ALICE_SIGNATURE_NONCE");
private static final byte[] B_NONCE = ascii("BOB_SIGNATURE_NONCE"); private static final byte[] B_NONCE = ascii("BOB_SIGNATURE_NONCE");
// KDF label for group salt derivation
private static final byte[] SALT = ascii("SALT");
// KDF labels for tag key derivation // KDF labels for tag key derivation
private static final byte[] A_TAG = ascii("ALICE_TAG_KEY"); private static final byte[] A_TAG = ascii("ALICE_TAG_KEY");
private static final byte[] B_TAG = ascii("BOB_TAG_KEY"); private static final byte[] B_TAG = ascii("BOB_TAG_KEY");
@@ -233,10 +231,6 @@ class CryptoComponentImpl implements CryptoComponent {
return macKdf(master, alice ? A_NONCE : B_NONCE); return macKdf(master, alice ? A_NONCE : B_NONCE);
} }
public byte[] deriveGroupSalt(SecretKey master) {
return macKdf(master, SALT);
}
public TransportKeys deriveTransportKeys(TransportId t, public TransportKeys deriveTransportKeys(TransportId t,
SecretKey master, long rotationPeriod, boolean alice) { SecretKey master, long rotationPeriod, boolean alice) {
// Keys for the previous period are derived from the master secret // Keys for the previous period are derived from the master secret
@@ -325,6 +319,17 @@ class CryptoComponentImpl implements CryptoComponent {
System.arraycopy(mac, 0, tag, 0, TAG_LENGTH); System.arraycopy(mac, 0, tag, 0, TAG_LENGTH);
} }
public byte[] hash(byte[]... inputs) {
MessageDigest digest = getMessageDigest();
byte[] length = new byte[INT_32_BYTES];
for (byte[] input : inputs) {
ByteUtils.writeUint32(input.length, length, 0);
digest.update(length);
digest.update(input);
}
return digest.digest();
}
public byte[] encryptWithPassword(byte[] input, String password) { public byte[] encryptWithPassword(byte[] input, String password) {
AuthenticatedCipher cipher = new XSalsa20Poly1305AuthenticatedCipher(); AuthenticatedCipher cipher = new XSalsa20Poly1305AuthenticatedCipher();
int macBytes = cipher.getMacBytes(); int macBytes = cipher.getMacBytes();

View File

@@ -2,12 +2,9 @@ package org.briarproject.data;
import org.briarproject.api.FormatException; import org.briarproject.api.FormatException;
import org.briarproject.api.data.BdfReader; import org.briarproject.api.data.BdfReader;
import org.briarproject.api.data.Consumer;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import static org.briarproject.data.Types.DICTIONARY; import static org.briarproject.data.Types.DICTIONARY;
import static org.briarproject.data.Types.END; import static org.briarproject.data.Types.END;
@@ -33,7 +30,6 @@ class BdfReaderImpl implements BdfReader {
private static final byte[] EMPTY_BUFFER = new byte[] {}; private static final byte[] EMPTY_BUFFER = new byte[] {};
private final InputStream in; private final InputStream in;
private final Collection<Consumer> consumers = new ArrayList<Consumer>(0);
private boolean hasLookahead = false, eof = false; private boolean hasLookahead = false, eof = false;
private byte next; private byte next;
@@ -44,8 +40,8 @@ class BdfReaderImpl implements BdfReader {
} }
private void readLookahead() throws IOException { private void readLookahead() throws IOException {
assert !eof; if (eof) throw new IllegalStateException();
assert !hasLookahead; if (hasLookahead) throw new IllegalStateException();
// Read a lookahead byte // Read a lookahead byte
int i = in.read(); int i = in.read();
if (i == -1) { if (i == -1) {
@@ -56,27 +52,18 @@ class BdfReaderImpl implements BdfReader {
hasLookahead = true; hasLookahead = true;
} }
private void consumeLookahead() throws IOException { private void readIntoBuffer(byte[] b, int length) throws IOException {
assert hasLookahead;
for (Consumer c : consumers) c.write(next);
hasLookahead = false;
}
private void readIntoBuffer(byte[] b, int length, boolean consume)
throws IOException {
int offset = 0; int offset = 0;
while (offset < length) { while (offset < length) {
int read = in.read(b, offset, length - offset); int read = in.read(b, offset, length - offset);
if (read == -1) throw new FormatException(); if (read == -1) throw new FormatException();
offset += read; offset += read;
} }
if (consume) for (Consumer c : consumers) c.write(b, 0, length);
} }
private void readIntoBuffer(int length, boolean consume) private void readIntoBuffer(int length) throws IOException {
throws IOException {
if (buf.length < length) buf = new byte[length]; if (buf.length < length) buf = new byte[length];
readIntoBuffer(buf, length, consume); readIntoBuffer(buf, length);
} }
private void skip(int length) throws IOException { private void skip(int length) throws IOException {
@@ -108,14 +95,6 @@ class BdfReaderImpl implements BdfReader {
in.close(); in.close();
} }
public void addConsumer(Consumer c) {
consumers.add(c);
}
public void removeConsumer(Consumer c) {
if (!consumers.remove(c)) throw new IllegalArgumentException();
}
public boolean hasNull() throws IOException { public boolean hasNull() throws IOException {
if (!hasLookahead) readLookahead(); if (!hasLookahead) readLookahead();
if (eof) return false; if (eof) return false;
@@ -124,7 +103,7 @@ class BdfReaderImpl implements BdfReader {
public void readNull() throws IOException { public void readNull() throws IOException {
if (!hasNull()) throw new FormatException(); if (!hasNull()) throw new FormatException();
consumeLookahead(); hasLookahead = false;
} }
public void skipNull() throws IOException { public void skipNull() throws IOException {
@@ -141,7 +120,7 @@ class BdfReaderImpl implements BdfReader {
public boolean readBoolean() throws IOException { public boolean readBoolean() throws IOException {
if (!hasBoolean()) throw new FormatException(); if (!hasBoolean()) throw new FormatException();
boolean bool = next == TRUE; boolean bool = next == TRUE;
consumeLookahead(); hasLookahead = false;
return bool; return bool;
} }
@@ -159,32 +138,32 @@ class BdfReaderImpl implements BdfReader {
public long readInteger() throws IOException { public long readInteger() throws IOException {
if (!hasInteger()) throw new FormatException(); if (!hasInteger()) throw new FormatException();
consumeLookahead(); hasLookahead = false;
if (next == INT_8) return readInt8(true); if (next == INT_8) return readInt8();
if (next == INT_16) return readInt16(true); if (next == INT_16) return readInt16();
if (next == INT_32) return readInt32(true); if (next == INT_32) return readInt32();
return readInt64(true); return readInt64();
} }
private int readInt8(boolean consume) throws IOException { private int readInt8() throws IOException {
readIntoBuffer(1, consume); readIntoBuffer(1);
return buf[0]; return buf[0];
} }
private short readInt16(boolean consume) throws IOException { private short readInt16() throws IOException {
readIntoBuffer(2, consume); readIntoBuffer(2);
return (short) (((buf[0] & 0xFF) << 8) + (buf[1] & 0xFF)); return (short) (((buf[0] & 0xFF) << 8) + (buf[1] & 0xFF));
} }
private int readInt32(boolean consume) throws IOException { private int readInt32() throws IOException {
readIntoBuffer(4, consume); readIntoBuffer(4);
int value = 0; int value = 0;
for (int i = 0; i < 4; i++) value |= (buf[i] & 0xFF) << (24 - i * 8); for (int i = 0; i < 4; i++) value |= (buf[i] & 0xFF) << (24 - i * 8);
return value; return value;
} }
private long readInt64(boolean consume) throws IOException { private long readInt64() throws IOException {
readIntoBuffer(8, consume); readIntoBuffer(8);
long value = 0; long value = 0;
for (int i = 0; i < 8; i++) value |= (buf[i] & 0xFFL) << (56 - i * 8); for (int i = 0; i < 8; i++) value |= (buf[i] & 0xFFL) << (56 - i * 8);
return value; return value;
@@ -207,8 +186,8 @@ class BdfReaderImpl implements BdfReader {
public double readFloat() throws IOException { public double readFloat() throws IOException {
if (!hasFloat()) throw new FormatException(); if (!hasFloat()) throw new FormatException();
consumeLookahead(); hasLookahead = false;
readIntoBuffer(8, true); readIntoBuffer(8);
long value = 0; long value = 0;
for (int i = 0; i < 8; i++) value |= (buf[i] & 0xFFL) << (56 - i * 8); for (int i = 0; i < 8; i++) value |= (buf[i] & 0xFFL) << (56 - i * 8);
return Double.longBitsToDouble(value); return Double.longBitsToDouble(value);
@@ -228,24 +207,24 @@ class BdfReaderImpl implements BdfReader {
public String readString(int maxLength) throws IOException { public String readString(int maxLength) throws IOException {
if (!hasString()) throw new FormatException(); if (!hasString()) throw new FormatException();
consumeLookahead(); hasLookahead = false;
int length = readStringLength(true); int length = readStringLength();
if (length < 0 || length > maxLength) throw new FormatException(); if (length < 0 || length > maxLength) throw new FormatException();
if (length == 0) return ""; if (length == 0) return "";
readIntoBuffer(length, true); readIntoBuffer(length);
return new String(buf, 0, length, "UTF-8"); return new String(buf, 0, length, "UTF-8");
} }
private int readStringLength(boolean consume) throws IOException { private int readStringLength() throws IOException {
if (next == STRING_8) return readInt8(consume); if (next == STRING_8) return readInt8();
if (next == STRING_16) return readInt16(consume); if (next == STRING_16) return readInt16();
if (next == STRING_32) return readInt32(consume); if (next == STRING_32) return readInt32();
throw new FormatException(); throw new FormatException();
} }
public void skipString() throws IOException { public void skipString() throws IOException {
if (!hasString()) throw new FormatException(); if (!hasString()) throw new FormatException();
int length = readStringLength(false); int length = readStringLength();
if (length < 0) throw new FormatException(); if (length < 0) throw new FormatException();
skip(length); skip(length);
hasLookahead = false; hasLookahead = false;
@@ -259,25 +238,25 @@ class BdfReaderImpl implements BdfReader {
public byte[] readRaw(int maxLength) throws IOException { public byte[] readRaw(int maxLength) throws IOException {
if (!hasRaw()) throw new FormatException(); if (!hasRaw()) throw new FormatException();
consumeLookahead(); hasLookahead = false;
int length = readRawLength(true); int length = readRawLength();
if (length < 0 || length > maxLength) throw new FormatException(); if (length < 0 || length > maxLength) throw new FormatException();
if (length == 0) return EMPTY_BUFFER; if (length == 0) return EMPTY_BUFFER;
byte[] b = new byte[length]; byte[] b = new byte[length];
readIntoBuffer(b, length, true); readIntoBuffer(b, length);
return b; return b;
} }
private int readRawLength(boolean consume) throws IOException { private int readRawLength() throws IOException {
if (next == RAW_8) return readInt8(consume); if (next == RAW_8) return readInt8();
if (next == RAW_16) return readInt16(consume); if (next == RAW_16) return readInt16();
if (next == RAW_32) return readInt32(consume); if (next == RAW_32) return readInt32();
throw new FormatException(); throw new FormatException();
} }
public void skipRaw() throws IOException { public void skipRaw() throws IOException {
if (!hasRaw()) throw new FormatException(); if (!hasRaw()) throw new FormatException();
int length = readRawLength(false); int length = readRawLength();
if (length < 0) throw new FormatException(); if (length < 0) throw new FormatException();
skip(length); skip(length);
hasLookahead = false; hasLookahead = false;
@@ -291,7 +270,7 @@ class BdfReaderImpl implements BdfReader {
public void readListStart() throws IOException { public void readListStart() throws IOException {
if (!hasList()) throw new FormatException(); if (!hasList()) throw new FormatException();
consumeLookahead(); hasLookahead = false;
} }
public boolean hasListEnd() throws IOException { public boolean hasListEnd() throws IOException {
@@ -310,7 +289,7 @@ class BdfReaderImpl implements BdfReader {
private void readEnd() throws IOException { private void readEnd() throws IOException {
if (!hasEnd()) throw new FormatException(); if (!hasEnd()) throw new FormatException();
consumeLookahead(); hasLookahead = false;
} }
public void skipList() throws IOException { public void skipList() throws IOException {
@@ -328,7 +307,7 @@ class BdfReaderImpl implements BdfReader {
public void readDictionaryStart() throws IOException { public void readDictionaryStart() throws IOException {
if (!hasDictionary()) throw new FormatException(); if (!hasDictionary()) throw new FormatException();
consumeLookahead(); hasLookahead = false;
} }
public boolean hasDictionaryEnd() throws IOException { public boolean hasDictionaryEnd() throws IOException {

View File

@@ -11,6 +11,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import static org.briarproject.api.data.BdfDictionary.NULL_VALUE;
import static org.briarproject.api.db.Metadata.REMOVE; import static org.briarproject.api.db.Metadata.REMOVE;
import static org.briarproject.data.Types.DICTIONARY; import static org.briarproject.data.Types.DICTIONARY;
import static org.briarproject.data.Types.END; import static org.briarproject.data.Types.END;
@@ -37,7 +38,7 @@ class MetadataEncoderImpl implements MetadataEncoder {
Metadata m = new Metadata(); Metadata m = new Metadata();
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
for (Entry<String, Object> e : d.entrySet()) { for (Entry<String, Object> e : d.entrySet()) {
if (e.getValue() == null) { if (e.getValue() == NULL_VALUE) {
// Special case: if the value is null, the key is being removed // Special case: if the value is null, the key is being removed
m.put(e.getKey(), REMOVE); m.put(e.getKey(), REMOVE);
} else { } else {
@@ -51,7 +52,7 @@ class MetadataEncoderImpl implements MetadataEncoder {
private void encodeObject(ByteArrayOutputStream out, Object o) private void encodeObject(ByteArrayOutputStream out, Object o)
throws FormatException { throws FormatException {
if (o == null) out.write(NULL); if (o == NULL_VALUE) out.write(NULL);
else if (o instanceof Boolean) out.write((Boolean) o ? TRUE : FALSE); else if (o instanceof Boolean) out.write((Boolean) o ? TRUE : FALSE);
else if (o instanceof Byte) encodeInteger(out, (Byte) o); else if (o instanceof Byte) encodeInteger(out, (Byte) o);
else if (o instanceof Short) encodeInteger(out, (Short) o); else if (o instanceof Short) encodeInteger(out, (Short) o);

View File

@@ -10,6 +10,7 @@ import org.briarproject.util.StringUtils;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.util.Map.Entry; import java.util.Map.Entry;
import static org.briarproject.api.data.BdfDictionary.NULL_VALUE;
import static org.briarproject.api.db.Metadata.REMOVE; import static org.briarproject.api.db.Metadata.REMOVE;
import static org.briarproject.data.Types.DICTIONARY; import static org.briarproject.data.Types.DICTIONARY;
import static org.briarproject.data.Types.END; import static org.briarproject.data.Types.END;
@@ -33,14 +34,14 @@ class MetadataParserImpl implements MetadataParser {
@Override @Override
public BdfDictionary parse(Metadata m) throws FormatException { public BdfDictionary parse(Metadata m) throws FormatException {
BdfDictionary dict = new BdfDictionary(); BdfDictionary d = new BdfDictionary();
for (Entry<String, byte[]> e : m.entrySet()) for (Entry<String, byte[]> e : m.entrySet())
dict.put(e.getKey(), parseObject(e.getValue())); d.put(e.getKey(), parseValue(e.getValue()));
return dict; return d;
} }
private Object parseObject(byte[] b) throws FormatException { private Object parseValue(byte[] b) throws FormatException {
if (b == REMOVE) return null; if (b == REMOVE) return NULL_VALUE;
ByteArrayInputStream in = new ByteArrayInputStream(b); ByteArrayInputStream in = new ByteArrayInputStream(b);
Object o = parseObject(in); Object o = parseObject(in);
if (in.available() > 0) throw new FormatException(); if (in.available() > 0) throw new FormatException();
@@ -50,7 +51,7 @@ class MetadataParserImpl implements MetadataParser {
private Object parseObject(ByteArrayInputStream in) throws FormatException { private Object parseObject(ByteArrayInputStream in) throws FormatException {
switch(in.read()) { switch(in.read()) {
case NULL: case NULL:
return null; return NULL_VALUE;
case TRUE: case TRUE:
return Boolean.TRUE; return Boolean.TRUE;
case FALSE: case FALSE:

View File

@@ -6,14 +6,16 @@ import org.briarproject.api.TransportProperties;
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.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Metadata;
import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorId; 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.Group; 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.MessageHeader;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.MessageStatus;
import org.briarproject.api.sync.SubscriptionAck; import org.briarproject.api.sync.SubscriptionAck;
import org.briarproject.api.sync.SubscriptionUpdate; import org.briarproject.api.sync.SubscriptionUpdate;
import org.briarproject.api.sync.TransportAck; import org.briarproject.api.sync.TransportAck;
@@ -85,6 +87,13 @@ interface Database<T> {
ContactId addContact(T txn, Author remote, AuthorId local) ContactId addContact(T txn, Author remote, AuthorId local)
throws DbException; throws DbException;
/**
* Adds a group to the given contact's subscriptions.
* <p>
* Locking: write.
*/
void addContactGroup(T txn, ContactId c, Group g) throws DbException;
/** /**
* Subscribes to a group, or returns false if the user already has the * Subscribes to a group, or returns false if the user already has the
* maximum number of subscriptions. * maximum number of subscriptions.
@@ -216,11 +225,12 @@ interface Database<T> {
int countOfferedMessages(T txn, ContactId c) throws DbException; int countOfferedMessages(T txn, ContactId c) throws DbException;
/** /**
* Returns all groups to which the user could subscribe. * Returns all groups belonging to the given client to which the user could
* subscribe.
* <p> * <p>
* Locking: read. * Locking: read.
*/ */
Collection<Group> getAvailableGroups(T txn) throws DbException; Collection<Group> getAvailableGroups(T txn, ClientId c) throws DbException;
/** /**
* Returns the contact with the given ID. * Returns the contact with the given ID.
@@ -265,28 +275,12 @@ interface Database<T> {
Group getGroup(T txn, GroupId g) throws DbException; Group getGroup(T txn, GroupId g) throws DbException;
/** /**
* Returns all groups to which the user subscribes, excluding inboxes. * Returns all groups belonging to the given client to which the user
* subscribes.
* <p> * <p>
* Locking: read. * Locking: read.
*/ */
Collection<Group> getGroups(T txn) throws DbException; Collection<Group> getGroups(T txn, ClientId c) throws DbException;
/**
* Returns the ID of the inbox group for the given contact, or null if no
* inbox group has been set.
* <p>
* Locking: read.
*/
GroupId getInboxGroupId(T txn, ContactId c) throws DbException;
/**
* Returns the headers of all messages in the inbox group for the given
* contact, or null if no inbox group has been set.
* <p>
* Locking: read.
*/
Collection<MessageHeader> getInboxMessageHeaders(T txn, ContactId c)
throws DbException;
/** /**
* Returns the local pseudonym with the given ID. * Returns the local pseudonym with the given ID.
@@ -319,19 +313,37 @@ interface Database<T> {
throws DbException; throws DbException;
/** /**
* Returns the body of the message identified by the given ID. * Returns the metadata for all messages in the given group.
* <p> * <p>
* Locking: read. * Locking: read.
*/ */
byte[] getMessageBody(T txn, MessageId m) throws DbException; Map<MessageId, Metadata> getMessageMetadata(T txn, GroupId g)
throws DbException;
/** /**
* Returns the headers of all messages in the given group. * Returns the metadata for the given message.
* <p> * <p>
* Locking: read. * Locking: read.
*/ */
Collection<MessageHeader> getMessageHeaders(T txn, GroupId g) Metadata getMessageMetadata(T txn, MessageId m) throws DbException;
throws DbException;
/**
* Returns the status of all messages in the given group with respect
* to the given contact.
* <p>
* Locking: read
*/
Collection<MessageStatus> getMessageStatus(T txn, ContactId c, GroupId g)
throws DbException;
/**
* Returns the status of the given message with respect to the given
* contact.
* <p>
* Locking: read
*/
MessageStatus getMessageStatus(T txn, ContactId c, MessageId m)
throws DbException;
/** /**
* Returns the IDs of some messages received from the given contact that * Returns the IDs of some messages received from the given contact that
@@ -370,28 +382,21 @@ interface Database<T> {
int maxMessages) throws DbException; int maxMessages) throws DbException;
/** /**
* Returns the parent of the given message, or null if either the message * Returns the IDs of any messages that need to be validated by the given
* has no parent, or the parent is absent from the database, or the parent * client.
* belongs to a different group.
* <p> * <p>
* Locking: read. * Locking: read.
*/ */
MessageId getParent(T txn, MessageId m) throws DbException; Collection<MessageId> getMessagesToValidate(T txn, ClientId c)
throws DbException;
/** /**
* Returns the message identified by the given ID, in serialised form. * Returns the message with the given ID, in serialised form.
* <p> * <p>
* Locking: read. * Locking: read.
*/ */
byte[] getRawMessage(T txn, MessageId m) throws DbException; byte[] getRawMessage(T txn, MessageId m) throws DbException;
/**
* Returns true if the given message is marked as read.
* <p>
* Locking: read.
*/
boolean getReadFlag(T txn, MessageId m) throws DbException;
/** /**
* Returns all remote properties for the given transport. * Returns all remote properties for the given transport.
* <p> * <p>
@@ -475,13 +480,6 @@ interface Database<T> {
Collection<TransportUpdate> getTransportUpdates(T txn, ContactId c, Collection<TransportUpdate> getTransportUpdates(T txn, ContactId c,
int maxLatency) throws DbException; int maxLatency) throws DbException;
/**
* Returns the number of unread messages in each subscribed group.
* <p>
* Locking: read.
*/
Map<GroupId, Integer> getUnreadMessageCounts(T txn) throws DbException;
/** /**
* Returns the IDs of all contacts to which the given group is visible. * Returns the IDs of all contacts to which the given group is visible.
* <p> * <p>
@@ -525,6 +523,15 @@ interface Database<T> {
void mergeLocalProperties(T txn, TransportId t, TransportProperties p) void mergeLocalProperties(T txn, TransportId t, TransportProperties p)
throws DbException; throws DbException;
/*
* Merges the given metadata with the existing metadata for the given
* message.
* <p>
* Locking: write.
*/
void mergeMessageMetadata(T txn, MessageId m, Metadata meta)
throws DbException;
/** /**
* Merges the given settings with the existing settings in the given * Merges the given settings with the existing settings in the given
* namespace. * namespace.
@@ -624,6 +631,10 @@ interface Database<T> {
*/ */
void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException; void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException;
/** Marks the given message as valid or invalid. */
void setMessageValidity(T txn, MessageId m, boolean valid)
throws DbException;
/** /**
* Sets the reordering window for the given contact and transport in the * Sets the reordering window for the given contact and transport in the
* given rotation period. * given rotation period.
@@ -634,30 +645,15 @@ interface Database<T> {
long rotationPeriod, long base, byte[] bitmap) throws DbException; long rotationPeriod, long base, byte[] bitmap) throws DbException;
/** /**
* Updates the groups to which the given contact subscribes and returns * Updates the given contact's subscriptions and returns true, unless an
* true, unless an update with an equal or higher version number has * update with an equal or higher version number has already been received
* already been received from the contact. * from the contact.
* <p> * <p>
* Locking: write. * Locking: write.
*/ */
boolean setGroups(T txn, ContactId c, Collection<Group> groups, boolean setGroups(T txn, ContactId c, Collection<Group> groups,
long version) throws DbException; long version) throws DbException;
/**
* Makes a group visible to the given contact, adds it to the contact's
* subscriptions, and sets it as the inbox group for the contact.
* <p>
* Locking: write.
*/
void setInboxGroup(T txn, ContactId c, Group g) throws DbException;
/**
* Marks a message as read or unread.
* <p>
* Locking: write.
*/
void setReadFlag(T txn, MessageId m, boolean read) throws DbException;
/** /**
* Sets the remote transport properties for the given contact, replacing * Sets the remote transport properties for the given contact, replacing
* any existing properties. * any existing properties.

View File

@@ -9,6 +9,8 @@ import org.briarproject.api.db.ContactExistsException;
import org.briarproject.api.db.DatabaseComponent; import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.db.LocalAuthorExistsException; import org.briarproject.api.db.LocalAuthorExistsException;
import org.briarproject.api.db.MessageExistsException;
import org.briarproject.api.db.Metadata;
import org.briarproject.api.db.NoSuchContactException; import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.db.NoSuchLocalAuthorException; import org.briarproject.api.db.NoSuchLocalAuthorException;
import org.briarproject.api.db.NoSuchMessageException; import org.briarproject.api.db.NoSuchMessageException;
@@ -25,6 +27,7 @@ import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageRequestedEvent;
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.MessagesAckedEvent; import org.briarproject.api.event.MessagesAckedEvent;
import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.event.MessagesSentEvent;
import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent; import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
@@ -39,11 +42,12 @@ import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.lifecycle.ShutdownManager; import org.briarproject.api.lifecycle.ShutdownManager;
import org.briarproject.api.sync.Ack; import org.briarproject.api.sync.Ack;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group; 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.MessageHeader;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.MessageStatus;
import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.Offer;
import org.briarproject.api.sync.Request; import org.briarproject.api.sync.Request;
import org.briarproject.api.sync.SubscriptionAck; import org.briarproject.api.sync.SubscriptionAck;
@@ -165,6 +169,22 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return c; return c;
} }
public void addContactGroup(ContactId c, Group g) throws DbException {
lock.writeLock().lock();
try {
T txn = db.startTransaction();
try {
db.addContactGroup(txn, c, g);
db.commitTransaction(txn);
} catch (DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
lock.writeLock().unlock();
}
}
public boolean addGroup(Group g) throws DbException { public boolean addGroup(Group g) throws DbException {
boolean added = false; boolean added = false;
lock.writeLock().lock(); lock.writeLock().lock();
@@ -204,15 +224,18 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
eventBus.broadcast(new LocalAuthorAddedEvent(a.getId())); eventBus.broadcast(new LocalAuthorAddedEvent(a.getId()));
} }
public void addLocalMessage(Message m) throws DbException { public void addLocalMessage(Message m, ClientId c, Metadata meta)
boolean duplicate, subscribed; throws DbException {
lock.writeLock().lock(); lock.writeLock().lock();
try { try {
T txn = db.startTransaction(); T txn = db.startTransaction();
try { try {
duplicate = db.containsMessage(txn, m.getId()); if (db.containsMessage(txn, m.getId()))
subscribed = db.containsGroup(txn, m.getGroup().getId()); throw new MessageExistsException();
if (!duplicate && subscribed) addMessage(txn, m, null); if (!db.containsGroup(txn, m.getGroupId()))
throw new NoSuchSubscriptionException();
addMessage(txn, m, null);
db.mergeMessageMetadata(txn, m.getId(), meta);
db.commitTransaction(txn); db.commitTransaction(txn);
} catch (DbException e) { } catch (DbException e) {
db.abortTransaction(txn); db.abortTransaction(txn);
@@ -221,28 +244,21 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} finally { } finally {
lock.writeLock().unlock(); lock.writeLock().unlock();
} }
if (!duplicate && subscribed) { eventBus.broadcast(new MessageAddedEvent(m, null));
eventBus.broadcast(new MessageAddedEvent(m, null)); eventBus.broadcast(new MessageValidatedEvent(m, c, true, true));
}
} }
/** /**
* Stores a message, initialises its status with respect to each contact, * Stores a message and initialises its status with respect to each contact.
* and marks it as read if it was locally generated.
* <p> * <p>
* Locking: write. * Locking: write.
* @param sender null for a locally generated message. * @param sender null for a locally generated message.
*/ */
private void addMessage(T txn, Message m, ContactId sender) private void addMessage(T txn, Message m, ContactId sender)
throws DbException { throws DbException {
if (sender == null) { db.addMessage(txn, m, sender == null);
db.addMessage(txn, m, true); GroupId g = m.getGroupId();
db.setReadFlag(txn, m.getId(), true); Collection<ContactId> visibility = db.getVisibility(txn, g);
} else {
db.addMessage(txn, m, false);
}
Group g = m.getGroup();
Collection<ContactId> visibility = db.getVisibility(txn, g.getId());
visibility = new HashSet<ContactId>(visibility); visibility = new HashSet<ContactId>(visibility);
for (ContactId c : db.getContactIds(txn)) { for (ContactId c : db.getContactIds(txn)) {
if (visibility.contains(c)) { if (visibility.contains(c)) {
@@ -506,12 +522,12 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} }
} }
public Collection<Group> getAvailableGroups() throws DbException { public Collection<Group> getAvailableGroups(ClientId c) throws DbException {
lock.readLock().lock(); lock.readLock().lock();
try { try {
T txn = db.startTransaction(); T txn = db.startTransaction();
try { try {
Collection<Group> groups = db.getAvailableGroups(txn); Collection<Group> groups = db.getAvailableGroups(txn, c);
db.commitTransaction(txn); db.commitTransaction(txn);
return groups; return groups;
} catch (DbException e) { } catch (DbException e) {
@@ -578,12 +594,12 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} }
} }
public Collection<Group> getGroups() throws DbException { public Collection<Group> getGroups(ClientId c) throws DbException {
lock.readLock().lock(); lock.readLock().lock();
try { try {
T txn = db.startTransaction(); T txn = db.startTransaction();
try { try {
Collection<Group> groups = db.getGroups(txn); Collection<Group> groups = db.getGroups(txn, c);
db.commitTransaction(txn); db.commitTransaction(txn);
return groups; return groups;
} catch (DbException e) { } catch (DbException e) {
@@ -595,46 +611,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} }
} }
public GroupId getInboxGroupId(ContactId c) throws DbException {
lock.readLock().lock();
try {
T txn = db.startTransaction();
try {
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
GroupId inbox = db.getInboxGroupId(txn, c);
db.commitTransaction(txn);
return inbox;
} catch (DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
lock.readLock().unlock();
}
}
public Collection<MessageHeader> getInboxMessageHeaders(ContactId c)
throws DbException {
lock.readLock().lock();
try {
T txn = db.startTransaction();
try {
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
Collection<MessageHeader> headers =
db.getInboxMessageHeaders(txn, c);
db.commitTransaction(txn);
return headers;
} catch (DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
lock.readLock().unlock();
}
}
public LocalAuthor getLocalAuthor(AuthorId a) throws DbException { public LocalAuthor getLocalAuthor(AuthorId a) throws DbException {
lock.readLock().lock(); lock.readLock().lock();
try { try {
@@ -710,16 +686,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} }
} }
public byte[] getMessageBody(MessageId m) throws DbException { public Collection<MessageId> getMessagesToValidate(ClientId c)
throws DbException {
lock.readLock().lock(); lock.readLock().lock();
try { try {
T txn = db.startTransaction(); T txn = db.startTransaction();
try { try {
if (!db.containsMessage(txn, m)) Collection<MessageId> ids = db.getMessagesToValidate(txn, c);
throw new NoSuchMessageException();
byte[] body = db.getMessageBody(txn, m);
db.commitTransaction(txn); db.commitTransaction(txn);
return body; return ids;
} catch (DbException e) { } catch (DbException e) {
db.abortTransaction(txn); db.abortTransaction(txn);
throw e; throw e;
@@ -729,7 +704,26 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} }
} }
public Collection<MessageHeader> getMessageHeaders(GroupId g) public byte[] getRawMessage(MessageId m) throws DbException {
lock.readLock().lock();
try {
T txn = db.startTransaction();
try {
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
byte[] raw = db.getRawMessage(txn, m);
db.commitTransaction(txn);
return raw;
} catch (DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
lock.readLock().unlock();
}
}
public Map<MessageId, Metadata> getMessageMetadata(GroupId g)
throws DbException { throws DbException {
lock.readLock().lock(); lock.readLock().lock();
try { try {
@@ -737,10 +731,10 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
try { try {
if (!db.containsGroup(txn, g)) if (!db.containsGroup(txn, g))
throw new NoSuchSubscriptionException(); throw new NoSuchSubscriptionException();
Collection<MessageHeader> headers = Map<MessageId, Metadata> metadata =
db.getMessageHeaders(txn, g); db.getMessageMetadata(txn, g);
db.commitTransaction(txn); db.commitTransaction(txn);
return headers; return metadata;
} catch (DbException e) { } catch (DbException e) {
db.abortTransaction(txn); db.abortTransaction(txn);
throw e; throw e;
@@ -750,16 +744,61 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} }
} }
public boolean getReadFlag(MessageId m) throws DbException { public Metadata getMessageMetadata(MessageId m) throws DbException {
lock.readLock().lock(); lock.readLock().lock();
try { try {
T txn = db.startTransaction(); T txn = db.startTransaction();
try { try {
if (!db.containsMessage(txn, m)) if (!db.containsMessage(txn, m))
throw new NoSuchMessageException(); throw new NoSuchMessageException();
boolean read = db.getReadFlag(txn, m); Metadata metadata = db.getMessageMetadata(txn, m);
db.commitTransaction(txn); db.commitTransaction(txn);
return read; return metadata;
} catch (DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
lock.readLock().unlock();
}
}
public Collection<MessageStatus> getMessageStatus(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 NoSuchSubscriptionException();
Collection<MessageStatus> statuses =
db.getMessageStatus(txn, c, g);
db.commitTransaction(txn);
return statuses;
} catch (DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
lock.readLock().unlock();
}
}
public MessageStatus getMessageStatus(ContactId c, MessageId m)
throws DbException {
lock.readLock().lock();
try {
T txn = db.startTransaction();
try {
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
MessageStatus status = db.getMessageStatus(txn, c, m);
db.commitTransaction(txn);
return status;
} catch (DbException e) { } catch (DbException e) {
db.abortTransaction(txn); db.abortTransaction(txn);
throw e; throw e;
@@ -862,23 +901,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} }
} }
public Map<GroupId, Integer> getUnreadMessageCounts() throws DbException {
lock.readLock().lock();
try {
T txn = db.startTransaction();
try {
Map<GroupId, Integer> counts = db.getUnreadMessageCounts(txn);
db.commitTransaction(txn);
return counts;
} catch (DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
lock.readLock().unlock();
}
}
public Collection<ContactId> getVisibility(GroupId g) throws DbException { public Collection<ContactId> getVisibility(GroupId g) throws DbException {
lock.readLock().lock(); lock.readLock().lock();
try { try {
@@ -943,6 +965,25 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
if (changed) eventBus.broadcast(new LocalTransportsUpdatedEvent()); if (changed) eventBus.broadcast(new LocalTransportsUpdatedEvent());
} }
public void mergeMessageMetadata(MessageId m, Metadata meta)
throws DbException {
lock.writeLock().lock();
try {
T txn = db.startTransaction();
try {
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
db.mergeMessageMetadata(txn, m, meta);
db.commitTransaction(txn);
} catch (DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
lock.writeLock().unlock();
}
}
public void mergeSettings(Settings s, String namespace) throws DbException { public void mergeSettings(Settings s, String namespace) throws DbException {
boolean changed = false; boolean changed = false;
lock.writeLock().lock(); lock.writeLock().lock();
@@ -998,7 +1039,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
if (!db.containsContact(txn, c)) if (!db.containsContact(txn, c))
throw new NoSuchContactException(); throw new NoSuchContactException();
duplicate = db.containsMessage(txn, m.getId()); duplicate = db.containsMessage(txn, m.getId());
visible = db.containsVisibleGroup(txn, c, m.getGroup().getId()); visible = db.containsVisibleGroup(txn, c, m.getGroupId());
if (visible) { if (visible) {
if (!duplicate) addMessage(txn, m, c); if (!duplicate) addMessage(txn, m, c);
db.raiseAckFlag(txn, c, m.getId()); db.raiseAckFlag(txn, c, m.getId());
@@ -1012,9 +1053,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
lock.writeLock().unlock(); lock.writeLock().unlock();
} }
if (visible) { if (visible) {
if (!duplicate) { if (!duplicate)
eventBus.broadcast(new MessageAddedEvent(m, c)); eventBus.broadcast(new MessageAddedEvent(m, c));
}
eventBus.broadcast(new MessageToAckEvent(c)); eventBus.broadcast(new MessageToAckEvent(c));
} }
} }
@@ -1170,8 +1210,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
try { try {
if (!db.containsContact(txn, c)) if (!db.containsContact(txn, c))
throw new NoSuchContactException(); throw new NoSuchContactException();
GroupId g = db.getInboxGroupId(txn, c);
if (g != null) db.removeGroup(txn, g);
db.removeContact(txn, c); db.removeContact(txn, c);
db.commitTransaction(txn); db.commitTransaction(txn);
} catch (DbException e) { } catch (DbException e) {
@@ -1216,10 +1254,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
if (!db.containsLocalAuthor(txn, a)) if (!db.containsLocalAuthor(txn, a))
throw new NoSuchLocalAuthorException(); throw new NoSuchLocalAuthorException();
affected = db.getContacts(txn, a); affected = db.getContacts(txn, a);
for (ContactId c : affected) {
GroupId g = db.getInboxGroupId(txn, c);
if (g != null) db.removeGroup(txn, g);
}
db.removeLocalAuthor(txn, a); db.removeLocalAuthor(txn, a);
db.commitTransaction(txn); db.commitTransaction(txn);
} catch (DbException e) { } catch (DbException e) {
@@ -1253,32 +1287,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
eventBus.broadcast(new TransportRemovedEvent(t)); eventBus.broadcast(new TransportRemovedEvent(t));
} }
public void setInboxGroup(ContactId c, Group g) throws DbException { public void setMessageValidity(Message m, ClientId c, boolean valid)
throws DbException {
lock.writeLock().lock(); lock.writeLock().lock();
try { try {
T txn = db.startTransaction(); T txn = db.startTransaction();
try { try {
if (!db.containsContact(txn, c)) if (!db.containsMessage(txn, m.getId()))
throw new NoSuchContactException();
db.setInboxGroup(txn, c, g);
db.commitTransaction(txn);
} catch (DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
lock.writeLock().unlock();
}
}
public void setReadFlag(MessageId m, boolean read) throws DbException {
lock.writeLock().lock();
try {
T txn = db.startTransaction();
try {
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException(); throw new NoSuchMessageException();
db.setReadFlag(txn, m, read); db.setMessageValidity(txn, m.getId(), valid);
db.commitTransaction(txn); db.commitTransaction(txn);
} catch (DbException e) { } catch (DbException e) {
db.abortTransaction(txn); db.abortTransaction(txn);
@@ -1287,6 +1304,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} finally { } finally {
lock.writeLock().unlock(); lock.writeLock().unlock();
} }
eventBus.broadcast(new MessageValidatedEvent(m, c, false, valid));
} }
public void setRemoteProperties(ContactId c, public void setRemoteProperties(ContactId c,

View File

@@ -8,34 +8,4 @@ interface DatabaseConstants {
* limit is reached, additional offers will not be stored. * limit is reached, additional offers will not be stored.
*/ */
int MAX_OFFERED_MESSAGES = 1000; int MAX_OFFERED_MESSAGES = 1000;
// FIXME: These should be configurable
/**
* The minimum amount of space in bytes that should be kept free on the
* device where the database is stored. Whenever less than this much space
* is free, old messages will be expired from the database.
*/
long MIN_FREE_SPACE = 50 * 1024 * 1024; // 50 MiB
/**
* The minimum amount of space in bytes that must be kept free on the device
* where the database is stored. If less than this much space is free and
* there are no more messages to expire, an Error will be thrown.
*/
long CRITICAL_FREE_SPACE = 10 * 1024 * 1024; // 10 MiB
/**
* The amount of free space will be checked whenever this many transactions
* have been started since the last check.
* <p>
* FIXME: Increase this after implementing BTPv2 (smaller packets)?
*/
int MAX_TRANSACTIONS_BETWEEN_SPACE_CHECKS = 10;
/**
* Up to this many bytes of messages will be expired from the database each
* time it is necessary to expire messages.
*/
int BYTES_PER_SWEEP = 10 * 1024 * 1024; // 10 MiB
} }

View File

@@ -8,15 +8,16 @@ import org.briarproject.api.contact.ContactId;
import org.briarproject.api.crypto.SecretKey; import org.briarproject.api.crypto.SecretKey;
import org.briarproject.api.db.DbClosedException; import org.briarproject.api.db.DbClosedException;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Metadata;
import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorId; 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.Group; 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.MessageHeader;
import org.briarproject.api.sync.MessageHeader.State;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.MessageStatus;
import org.briarproject.api.sync.SubscriptionAck; import org.briarproject.api.sync.SubscriptionAck;
import org.briarproject.api.sync.SubscriptionUpdate; import org.briarproject.api.sync.SubscriptionUpdate;
import org.briarproject.api.sync.TransportAck; import org.briarproject.api.sync.TransportAck;
@@ -48,13 +49,9 @@ import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger; import java.util.logging.Logger;
import static java.sql.Types.BINARY;
import static java.sql.Types.VARCHAR;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.api.identity.Author.Status.ANONYMOUS; import static org.briarproject.api.db.Metadata.REMOVE;
import static org.briarproject.api.identity.Author.Status.UNKNOWN; import static org.briarproject.api.sync.SyncConstants.MAX_SUBSCRIPTIONS;
import static org.briarproject.api.identity.Author.Status.VERIFIED;
import static org.briarproject.api.sync.MessagingConstants.MAX_SUBSCRIPTIONS;
import static org.briarproject.db.ExponentialBackoff.calculateExpiry; import static org.briarproject.db.ExponentialBackoff.calculateExpiry;
/** /**
@@ -63,8 +60,12 @@ 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 = 12; private static final int SCHEMA_VERSION = 14;
private static final int MIN_SCHEMA_VERSION = 12; private static final int MIN_SCHEMA_VERSION = 14;
private static final int VALIDATION_UNKNOWN = 0;
private static final int VALIDATION_INVALID = 1;
private static final int VALIDATION_VALID = 2;
private static final String CREATE_SETTINGS = private static final String CREATE_SETTINGS =
"CREATE TABLE settings" "CREATE TABLE settings"
@@ -98,8 +99,8 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final String CREATE_GROUPS = private static final String CREATE_GROUPS =
"CREATE TABLE groups" "CREATE TABLE groups"
+ " (groupId HASH NOT NULL," + " (groupId HASH NOT NULL,"
+ " name VARCHAR NOT NULL," + " clientId HASH NOT NULL,"
+ " salt BINARY NOT NULL," + " descriptor BINARY NOT NULL,"
+ " visibleToAll BOOLEAN NOT NULL," + " visibleToAll BOOLEAN NOT NULL,"
+ " PRIMARY KEY (groupId))"; + " PRIMARY KEY (groupId))";
@@ -107,7 +108,6 @@ abstract class JdbcDatabase implements Database<Connection> {
"CREATE TABLE groupVisibilities" "CREATE TABLE groupVisibilities"
+ " (contactId INT NOT NULL," + " (contactId INT NOT NULL,"
+ " groupId HASH NOT NULL," + " groupId HASH NOT NULL,"
+ " inbox BOOLEAN NOT NULL,"
+ " PRIMARY KEY (contactId, groupId)," + " PRIMARY KEY (contactId, groupId),"
+ " FOREIGN KEY (contactId)" + " FOREIGN KEY (contactId)"
+ " REFERENCES contacts (contactId)" + " REFERENCES contacts (contactId)"
@@ -120,8 +120,8 @@ abstract class JdbcDatabase implements Database<Connection> {
"CREATE TABLE contactGroups" "CREATE TABLE contactGroups"
+ " (contactId INT NOT NULL," + " (contactId INT NOT NULL,"
+ " groupId HASH NOT NULL," // Not a foreign key + " groupId HASH NOT NULL," // Not a foreign key
+ " name VARCHAR NOT NULL," + " clientId HASH NOT NULL,"
+ " salt BINARY NOT NULL," + " descriptor BINARY NOT NULL,"
+ " PRIMARY KEY (contactId, groupId)," + " PRIMARY KEY (contactId, groupId),"
+ " FOREIGN KEY (contactId)" + " FOREIGN KEY (contactId)"
+ " REFERENCES contacts (contactId)" + " REFERENCES contacts (contactId)"
@@ -144,26 +144,26 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final String CREATE_MESSAGES = private static final String CREATE_MESSAGES =
"CREATE TABLE messages" "CREATE TABLE messages"
+ " (messageId HASH NOT NULL," + " (messageId HASH NOT NULL,"
+ " parentId HASH," // Null for the first msg in a thread
+ " groupId HASH NOT NULL," + " groupId HASH NOT NULL,"
+ " authorId HASH," // Null for private/anon messages
+ " authorName VARCHAR," // Null for private/anon messages
+ " authorKey VARCHAR," // Null for private/anon messages
+ " contentType VARCHAR NOT NULL,"
+ " timestamp BIGINT NOT NULL," + " timestamp BIGINT NOT NULL,"
+ " length INT NOT NULL,"
+ " bodyStart INT NOT NULL,"
+ " bodyLength INT NOT NULL,"
+ " raw BLOB NOT NULL,"
+ " local BOOLEAN NOT NULL," + " local BOOLEAN NOT NULL,"
+ " read BOOLEAN NOT NULL," + " valid INT NOT NULL,"
+ " length INT NOT NULL,"
+ " raw BLOB NOT NULL,"
+ " PRIMARY KEY (messageId)," + " PRIMARY KEY (messageId),"
+ " FOREIGN KEY (groupId)" + " FOREIGN KEY (groupId)"
+ " REFERENCES groups (groupId)" + " REFERENCES groups (groupId)"
+ " ON DELETE CASCADE)"; + " ON DELETE CASCADE)";
private static final String INDEX_MESSAGES_BY_TIMESTAMP = private static final String CREATE_MESSAGE_METADATA =
"CREATE INDEX messagesByTimestamp ON messages (timestamp)"; "CREATE TABLE messageMetadata"
+ " (messageId HASH NOT NULL,"
+ " key VARCHAR NOT NULL,"
+ " value BINARY NOT NULL,"
+ " PRIMARY KEY (messageId, key),"
+ " FOREIGN KEY (messageId)"
+ " REFERENCES messages (messageId)"
+ " ON DELETE CASCADE)";
private static final String CREATE_OFFERS = private static final String CREATE_OFFERS =
"CREATE TABLE offers" "CREATE TABLE offers"
@@ -191,12 +191,6 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " REFERENCES contacts (contactId)" + " REFERENCES contacts (contactId)"
+ " ON DELETE CASCADE)"; + " ON DELETE CASCADE)";
private static final String INDEX_STATUSES_BY_MESSAGE =
"CREATE INDEX statusesByMessage ON statuses (messageId)";
private static final String INDEX_STATUSES_BY_CONTACT =
"CREATE INDEX statusesByContact ON statuses (contactId)";
private static final String CREATE_TRANSPORTS = private static final String CREATE_TRANSPORTS =
"CREATE TABLE transports" "CREATE TABLE transports"
+ " (transportId VARCHAR NOT NULL," + " (transportId VARCHAR NOT NULL,"
@@ -393,11 +387,9 @@ abstract class JdbcDatabase implements Database<Connection> {
s.executeUpdate(insertTypeNames(CREATE_CONTACT_GROUPS)); s.executeUpdate(insertTypeNames(CREATE_CONTACT_GROUPS));
s.executeUpdate(insertTypeNames(CREATE_GROUP_VERSIONS)); s.executeUpdate(insertTypeNames(CREATE_GROUP_VERSIONS));
s.executeUpdate(insertTypeNames(CREATE_MESSAGES)); s.executeUpdate(insertTypeNames(CREATE_MESSAGES));
s.executeUpdate(INDEX_MESSAGES_BY_TIMESTAMP); s.executeUpdate(insertTypeNames(CREATE_MESSAGE_METADATA));
s.executeUpdate(insertTypeNames(CREATE_OFFERS)); s.executeUpdate(insertTypeNames(CREATE_OFFERS));
s.executeUpdate(insertTypeNames(CREATE_STATUSES)); s.executeUpdate(insertTypeNames(CREATE_STATUSES));
s.executeUpdate(INDEX_STATUSES_BY_MESSAGE);
s.executeUpdate(INDEX_STATUSES_BY_CONTACT);
s.executeUpdate(insertTypeNames(CREATE_TRANSPORTS)); s.executeUpdate(insertTypeNames(CREATE_TRANSPORTS));
s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_CONFIGS)); s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_CONFIGS));
s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_PROPS)); s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_PROPS));
@@ -596,9 +588,8 @@ abstract class JdbcDatabase implements Database<Connection> {
rs.close(); rs.close();
ps.close(); ps.close();
if (!ids.isEmpty()) { if (!ids.isEmpty()) {
sql = "INSERT INTO groupVisibilities" sql = "INSERT INTO groupVisibilities (contactId, groupId)"
+ " (contactId, groupId, inbox)" + " VALUES (?, ?)";
+ " VALUES (?, ?, FALSE)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); ps.setInt(1, c.getInt());
for (byte[] id : ids) { for (byte[] id : ids) {
@@ -656,6 +647,40 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
public void addContactGroup(Connection txn, ContactId c, Group g)
throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT NULL FROM contactGroups"
+ " WHERE contactId = ? AND groupId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setBytes(2, g.getId().getBytes());
rs = ps.executeQuery();
boolean found = rs.next();
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
if (found) return;
sql = "INSERT INTO contactGroups"
+ " (contactId, groupId, clientId, descriptor)"
+ " VALUES (?, ?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setBytes(2, g.getId().getBytes());
ps.setBytes(3, g.getClientId().getBytes());
ps.setBytes(4, g.getDescriptor());
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
public boolean addGroup(Connection txn, Group g) throws DbException { public boolean addGroup(Connection txn, Group g) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
@@ -671,12 +696,12 @@ abstract class JdbcDatabase implements Database<Connection> {
if (count > MAX_SUBSCRIPTIONS) throw new DbStateException(); if (count > MAX_SUBSCRIPTIONS) throw new DbStateException();
if (count == MAX_SUBSCRIPTIONS) return false; if (count == MAX_SUBSCRIPTIONS) return false;
sql = "INSERT INTO groups" sql = "INSERT INTO groups"
+ " (groupId, name, salt, visibleToAll)" + " (groupId, clientId, descriptor, visibleToAll)"
+ " VALUES (?, ?, ?, FALSE)"; + " VALUES (?, ?, ?, FALSE)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, g.getId().getBytes()); ps.setBytes(1, g.getId().getBytes());
ps.setString(2, g.getName()); ps.setBytes(2, g.getClientId().getBytes());
ps.setBytes(3, g.getSalt()); ps.setBytes(3, g.getDescriptor());
int affected = ps.executeUpdate(); int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException(); if (affected != 1) throw new DbStateException();
ps.close(); ps.close();
@@ -714,34 +739,18 @@ abstract class JdbcDatabase implements Database<Connection> {
throws DbException { throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
String sql = "INSERT INTO messages (messageId, parentId, groupId," String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
+ " authorId, authorName, authorKey, contentType," + " local, valid, length, raw)"
+ " timestamp, length, bodyStart, bodyLength, raw," + " VALUES (?, ?, ?, ?, ?, ?, ?)";
+ " local, read)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getId().getBytes()); ps.setBytes(1, m.getId().getBytes());
if (m.getParent() == null) ps.setNull(2, BINARY); ps.setBytes(2, m.getGroupId().getBytes());
else ps.setBytes(2, m.getParent().getBytes()); ps.setLong(3, m.getTimestamp());
ps.setBytes(3, m.getGroup().getId().getBytes()); ps.setBoolean(4, local);
Author a = m.getAuthor(); ps.setInt(5, local ? VALIDATION_VALID : VALIDATION_UNKNOWN);
if (a == null) { byte[] raw = m.getRaw();
ps.setNull(4, BINARY); ps.setInt(6, raw.length);
ps.setNull(5, VARCHAR); ps.setBytes(7, raw);
ps.setNull(6, BINARY);
} else {
ps.setBytes(4, a.getId().getBytes());
ps.setString(5, a.getName());
ps.setBytes(6, a.getPublicKey());
}
ps.setString(7, m.getContentType());
ps.setLong(8, m.getTimestamp());
byte[] raw = m.getSerialised();
ps.setInt(9, raw.length);
ps.setInt(10, m.getBodyStart());
ps.setInt(11, m.getBodyLength());
ps.setBytes(12, raw);
ps.setBoolean(13, local);
int affected = ps.executeUpdate(); int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException(); if (affected != 1) throw new DbStateException();
ps.close(); ps.close();
@@ -924,9 +933,8 @@ abstract class JdbcDatabase implements Database<Connection> {
throws DbException { throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
String sql = "INSERT INTO groupVisibilities" String sql = "INSERT INTO groupVisibilities (contactId, groupId)"
+ " (contactId, groupId, inbox)" + " VALUES (?, ?)";
+ " VALUES (?, ?, FALSE)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); ps.setInt(1, c.getInt());
ps.setBytes(2, g.getBytes()); ps.setBytes(2, g.getBytes());
@@ -1147,27 +1155,28 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
public Collection<Group> getAvailableGroups(Connection txn) public Collection<Group> getAvailableGroups(Connection txn, ClientId c)
throws DbException { throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT DISTINCT cg.groupId, cg.name, cg.salt" String sql = "SELECT DISTINCT cg.groupId, cg.descriptor"
+ " FROM contactGroups AS cg" + " FROM contactGroups AS cg"
+ " LEFT OUTER JOIN groups AS g" + " LEFT OUTER JOIN groups AS g"
+ " ON cg.groupId = g.groupId" + " ON cg.groupId = g.groupId"
+ " WHERE g.groupId IS NULL" + " WHERE cg.clientId = ?"
+ " AND g.groupId IS NULL"
+ " GROUP BY cg.groupId"; + " GROUP BY cg.groupId";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, c.getBytes());
rs = ps.executeQuery(); rs = ps.executeQuery();
List<Group> groups = new ArrayList<Group>(); List<Group> groups = new ArrayList<Group>();
Set<GroupId> ids = new HashSet<GroupId>(); Set<GroupId> ids = new HashSet<GroupId>();
while (rs.next()) { while (rs.next()) {
GroupId id = new GroupId(rs.getBytes(1)); GroupId id = new GroupId(rs.getBytes(1));
if (!ids.add(id)) throw new DbStateException(); if (!ids.add(id)) throw new DbStateException();
String name = rs.getString(2); byte[] descriptor = rs.getBytes(2);
byte[] salt = rs.getBytes(3); groups.add(new Group(id, c, descriptor));
groups.add(new Group(id, name, salt));
} }
rs.close(); rs.close();
ps.close(); ps.close();
@@ -1281,16 +1290,17 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT name, salt FROM groups WHERE groupId = ?"; String sql = "SELECT clientId, descriptor FROM groups"
+ " WHERE groupId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, g.getBytes()); ps.setBytes(1, g.getBytes());
rs = ps.executeQuery(); rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException(); if (!rs.next()) throw new DbStateException();
String name = rs.getString(1); ClientId clientId = new ClientId(rs.getBytes(1));
byte[] salt = rs.getBytes(2); byte[] descriptor = rs.getBytes(2);
rs.close(); rs.close();
ps.close(); ps.close();
return new Group(g, name, salt); return new Group(g, clientId, descriptor);
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(rs); tryToClose(rs);
tryToClose(ps); tryToClose(ps);
@@ -1298,125 +1308,25 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
public Collection<Group> getGroups(Connection txn) throws DbException { public Collection<Group> getGroups(Connection txn, ClientId c)
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT DISTINCT g.groupId, name, salt"
+ " FROM groups AS g"
+ " LEFT OUTER JOIN groupVisibilities AS gv"
+ " ON g.groupId = gv.groupId"
+ " WHERE gv.inbox IS NULL OR gv.inbox = FALSE"
+ " GROUP BY g.groupId";
ps = txn.prepareStatement(sql);
rs = ps.executeQuery();
List<Group> groups = new ArrayList<Group>();
while (rs.next()) {
GroupId id = new GroupId(rs.getBytes(1));
String name = rs.getString(2);
byte[] salt = rs.getBytes(3);
groups.add(new Group(id, name, salt));
}
rs.close();
ps.close();
return Collections.unmodifiableList(groups);
} catch (SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
public GroupId getInboxGroupId(Connection txn, ContactId c)
throws DbException { throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT groupId FROM groupVisibilities" String sql = "SELECT groupId, descriptor FROM groups"
+ " WHERE contactId = ?" + " WHERE clientId = ?";
+ " AND inbox = TRUE";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); ps.setBytes(1, c.getBytes());
rs = ps.executeQuery(); rs = ps.executeQuery();
GroupId inbox = null; List<Group> groups = new ArrayList<Group>();
if (rs.next()) inbox = new GroupId(rs.getBytes(1));
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
return inbox;
} catch (SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
public Collection<MessageHeader> getInboxMessageHeaders(Connection txn,
ContactId c) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
// Get the local and remote authors
String sql = "SELECT la.authorId, la.name, la.publicKey,"
+ " c.authorId, c.name, c.publicKey"
+ " FROM localAuthors AS la"
+ " JOIN contacts AS c"
+ " ON la.authorId = c.localAuthorId"
+ " WHERE contactId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
rs = ps.executeQuery();
if (!rs.next()) throw new DbException();
AuthorId localId = new AuthorId(rs.getBytes(1));
String localName = rs.getString(2);
byte[] localKey = rs.getBytes(3);
Author localAuthor = new Author(localId, localName, localKey);
AuthorId remoteId = new AuthorId(rs.getBytes(4));
String remoteName = rs.getString(5);
byte[] remoteKey = rs.getBytes(6);
Author remoteAuthor = new Author(remoteId, remoteName, remoteKey);
if (rs.next()) throw new DbException();
// Get the message headers
sql = "SELECT m.messageId, parentId, m.groupId, contentType,"
+ " timestamp, local, read, seen, s.txCount"
+ " FROM messages AS m"
+ " JOIN groups AS g"
+ " ON m.groupId = g.groupId"
+ " JOIN groupVisibilities AS gv"
+ " ON m.groupId = gv.groupId"
+ " JOIN statuses AS s"
+ " ON m.messageId = s.messageId"
+ " AND gv.contactId = s.contactId"
+ " WHERE gv.contactId = ?"
+ " AND inbox = TRUE";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
rs = ps.executeQuery();
List<MessageHeader> headers = new ArrayList<MessageHeader>();
while (rs.next()) { while (rs.next()) {
MessageId id = new MessageId(rs.getBytes(1)); GroupId id = new GroupId(rs.getBytes(1));
byte[] b = rs.getBytes(2); byte[] descriptor = rs.getBytes(2);
MessageId parent = b == null ? null : new MessageId(b); groups.add(new Group(id, c, descriptor));
GroupId groupId = new GroupId(rs.getBytes(3));
String contentType = rs.getString(4);
long timestamp = rs.getLong(5);
boolean local = rs.getBoolean(6);
boolean read = rs.getBoolean(7);
boolean seen = rs.getBoolean(8);
Author author = local ? localAuthor : remoteAuthor;
// initialize message status
State status;
if (seen) status = State.DELIVERED;
else if (rs.getInt(9) > 0) status = State.SENT;
else status = State.STORED;
headers.add(new MessageHeader(id, parent, groupId, author,
VERIFIED, contentType, timestamp, local, read, status));
} }
rs.close(); rs.close();
ps.close(); ps.close();
return Collections.unmodifiableList(headers); return Collections.unmodifiableList(groups);
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(rs); tryToClose(rs);
tryToClose(ps); tryToClose(ps);
@@ -1538,25 +1448,35 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
public byte[] getMessageBody(Connection txn, MessageId m) public Map<MessageId, Metadata> getMessageMetadata(Connection txn,
throws DbException { GroupId g) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT bodyStart, bodyLength, raw FROM messages" String sql = "SELECT m.messageId, key, value"
+ " WHERE messageId = ?"; + " FROM messages AS m"
+ " JOIN messageMetadata AS md"
+ " ON m.messageId = md.messageId"
+ " WHERE groupId = ?"
+ " ORDER BY m.messageId";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes()); ps.setBytes(1, g.getBytes());
rs = ps.executeQuery(); rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException(); Map<MessageId, Metadata> all = new HashMap<MessageId, Metadata>();
int bodyStart = rs.getInt(1); Metadata metadata = null;
int bodyLength = rs.getInt(2); MessageId lastMessageId = null;
// Bytes are indexed from 1 rather than 0 while (rs.next()) {
byte[] body = rs.getBlob(3).getBytes(bodyStart + 1, bodyLength); MessageId messageId = new MessageId(rs.getBytes(1));
if (rs.next()) throw new DbStateException(); if (!messageId.equals(lastMessageId)) {
metadata = new Metadata();
all.put(messageId, metadata);
lastMessageId = messageId;
}
metadata.put(rs.getString(2), rs.getBytes(3));
}
rs.close(); rs.close();
ps.close(); ps.close();
return body; return Collections.unmodifiableMap(all);
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(rs); tryToClose(rs);
tryToClose(ps); tryToClose(ps);
@@ -1564,60 +1484,81 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
/** public Metadata getMessageMetadata(Connection txn, MessageId m)
* This method is used to get group messages. throws DbException {
* The message status won't be used.
*/
public Collection<MessageHeader> getMessageHeaders(Connection txn,
GroupId g) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT messageId, parentId, m.authorId, authorName," String sql = "SELECT key, value"
+ " authorKey, contentType, timestamp, local, read," + " FROM messageMetadata"
+ " la.authorId IS NOT NULL, c.authorId IS NOT NULL" + " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
rs = ps.executeQuery();
Metadata metadata = new Metadata();
while (rs.next()) metadata.put(rs.getString(1), rs.getBytes(2));
rs.close();
ps.close();
return metadata;
} catch (SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
public Collection<MessageStatus> getMessageStatus(Connection txn,
ContactId c, GroupId g) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT m.messageId, txCount > 0, seen"
+ " FROM messages AS m" + " FROM messages AS m"
+ " LEFT OUTER JOIN localAuthors AS la" + " JOIN statuses AS s"
+ " ON m.authorId = la.authorId" + " ON m.messageId = s.messageId"
+ " LEFT OUTER JOIN contacts AS c" + " WHERE groupId = ?"
+ " ON m.authorId = c.authorId" + " AND contactId = ?";
+ " WHERE groupId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, g.getBytes()); ps.setBytes(1, g.getBytes());
ps.setInt(2, c.getInt());
rs = ps.executeQuery(); rs = ps.executeQuery();
List<MessageHeader> headers = new ArrayList<MessageHeader>(); List<MessageStatus> statuses = new ArrayList<MessageStatus>();
while (rs.next()) { while (rs.next()) {
MessageId id = new MessageId(rs.getBytes(1)); MessageId messageId = new MessageId(rs.getBytes(1));
byte[] b = rs.getBytes(2); boolean sent = rs.getBoolean(2);
MessageId parent = b == null ? null : new MessageId(b); boolean seen = rs.getBoolean(3);
Author author; statuses.add(new MessageStatus(messageId, c, sent, seen));
b = rs.getBytes(3);
if (b == null) {
author = null;
} else {
AuthorId authorId = new AuthorId(b);
String authorName = rs.getString(4);
byte[] authorKey = rs.getBytes(5);
author = new Author(authorId, authorName, authorKey);
}
String contentType = rs.getString(6);
long timestamp = rs.getLong(7);
boolean local = rs.getBoolean(8);
boolean read = rs.getBoolean(9);
boolean isSelf = rs.getBoolean(10);
boolean isContact = rs.getBoolean(11);
Author.Status status;
if (author == null) status = ANONYMOUS;
else if (isSelf || isContact) status = VERIFIED;
else status = UNKNOWN;
headers.add(new MessageHeader(id, parent, g, author, status,
contentType, timestamp, local, read, State.STORED));
} }
rs.close(); rs.close();
ps.close(); ps.close();
return Collections.unmodifiableList(headers); return Collections.unmodifiableList(statuses);
} catch (SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
public MessageStatus getMessageStatus(Connection txn,
ContactId c, MessageId m) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT txCount > 0, seen"
+ " FROM statuses"
+ " WHERE messageId = ?"
+ " AND contactId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setInt(2, c.getInt());
rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException();
boolean sent = rs.getBoolean(1);
boolean seen = rs.getBoolean(2);
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
return new MessageStatus(m, c, sent, seen);
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(rs); tryToClose(rs);
tryToClose(ps); tryToClose(ps);
@@ -1665,13 +1606,15 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " ON m.messageId = s.messageId" + " ON m.messageId = s.messageId"
+ " AND cg.contactId = s.contactId" + " AND cg.contactId = s.contactId"
+ " WHERE cg.contactId = ?" + " WHERE cg.contactId = ?"
+ " AND valid = ?"
+ " AND seen = FALSE AND requested = FALSE" + " AND seen = FALSE AND requested = FALSE"
+ " AND s.expiry < ?" + " AND s.expiry < ?"
+ " ORDER BY timestamp DESC LIMIT ?"; + " ORDER BY timestamp DESC LIMIT ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); ps.setInt(1, c.getInt());
ps.setLong(2, now); ps.setInt(2, VALIDATION_VALID);
ps.setInt(3, maxMessages); ps.setLong(3, now);
ps.setInt(4, maxMessages);
rs = ps.executeQuery(); rs = ps.executeQuery();
List<MessageId> ids = new ArrayList<MessageId>(); List<MessageId> ids = new ArrayList<MessageId>();
while (rs.next()) ids.add(new MessageId(rs.getBytes(1))); while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
@@ -1725,12 +1668,14 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " ON m.messageId = s.messageId" + " ON m.messageId = s.messageId"
+ " AND cg.contactId = s.contactId" + " AND cg.contactId = s.contactId"
+ " WHERE cg.contactId = ?" + " WHERE cg.contactId = ?"
+ " AND valid = ?"
+ " AND seen = FALSE" + " AND seen = FALSE"
+ " AND s.expiry < ?" + " AND s.expiry < ?"
+ " ORDER BY timestamp DESC"; + " ORDER BY timestamp DESC";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); ps.setInt(1, c.getInt());
ps.setLong(2, now); ps.setInt(2, VALIDATION_VALID);
ps.setLong(3, now);
rs = ps.executeQuery(); rs = ps.executeQuery();
List<MessageId> ids = new ArrayList<MessageId>(); List<MessageId> ids = new ArrayList<MessageId>();
int total = 0; int total = 0;
@@ -1750,26 +1695,23 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
public MessageId getParent(Connection txn, MessageId m) throws DbException { public Collection<MessageId> getMessagesToValidate(Connection txn,
ClientId c) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT m1.parentId FROM messages AS m1" String sql = "SELECT messageId FROM messages AS m"
+ " JOIN messages AS m2" + " JOIN groups AS g ON m.groupId = g.groupId"
+ " ON m1.parentId = m2.messageId" + " WHERE valid = ? AND clientId = ?";
+ " AND m1.groupId = m2.groupId"
+ " WHERE m1.messageId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes()); ps.setInt(1, VALIDATION_UNKNOWN);
ps.setBytes(2, c.getBytes());
rs = ps.executeQuery(); rs = ps.executeQuery();
MessageId parent = null; List<MessageId> ids = new ArrayList<MessageId>();
if (rs.next()) { while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
parent = new MessageId(rs.getBytes(1));
if (rs.next()) throw new DbStateException();
}
rs.close(); rs.close();
ps.close(); ps.close();
return parent; return Collections.unmodifiableList(ids);
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(rs); tryToClose(rs);
tryToClose(ps); tryToClose(ps);
@@ -1782,14 +1724,12 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT length, raw FROM messages WHERE messageId = ?"; String sql = "SELECT raw FROM messages WHERE messageId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes()); ps.setBytes(1, m.getBytes());
rs = ps.executeQuery(); rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException(); if (!rs.next()) throw new DbStateException();
int length = rs.getInt(1); byte[] raw = rs.getBytes(1);
byte[] raw = rs.getBlob(2).getBytes(1, length);
if (raw.length != length) throw new DbStateException();
if (rs.next()) throw new DbStateException(); if (rs.next()) throw new DbStateException();
rs.close(); rs.close();
ps.close(); ps.close();
@@ -1801,27 +1741,6 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
public boolean getReadFlag(Connection txn, MessageId m) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT read FROM messages WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException();
boolean read = rs.getBoolean(1);
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
return read;
} catch (SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
public Map<ContactId, TransportProperties> getRemoteProperties( public Map<ContactId, TransportProperties> getRemoteProperties(
Connection txn, TransportId t) throws DbException { Connection txn, TransportId t) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
@@ -1874,12 +1793,14 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " ON m.messageId = s.messageId" + " ON m.messageId = s.messageId"
+ " AND cg.contactId = s.contactId" + " AND cg.contactId = s.contactId"
+ " WHERE cg.contactId = ?" + " WHERE cg.contactId = ?"
+ " AND valid = ?"
+ " AND seen = FALSE AND requested = TRUE" + " AND seen = FALSE AND requested = TRUE"
+ " AND s.expiry < ?" + " AND s.expiry < ?"
+ " ORDER BY timestamp DESC"; + " ORDER BY timestamp DESC";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); ps.setInt(1, c.getInt());
ps.setLong(2, now); ps.setInt(2, VALIDATION_VALID);
ps.setLong(3, now);
rs = ps.executeQuery(); rs = ps.executeQuery();
List<MessageId> ids = new ArrayList<MessageId>(); List<MessageId> ids = new ArrayList<MessageId>();
int total = 0; int total = 0;
@@ -1993,7 +1914,8 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT g.groupId, name, salt, localVersion, txCount" String sql = "SELECT g.groupId, clientId, descriptor,"
+ " localVersion, txCount"
+ " FROM groups AS g" + " FROM groups AS g"
+ " JOIN groupVisibilities AS gvis" + " JOIN groupVisibilities AS gvis"
+ " ON g.groupId = gvis.groupId" + " ON g.groupId = gvis.groupId"
@@ -2013,9 +1935,9 @@ abstract class JdbcDatabase implements Database<Connection> {
while (rs.next()) { while (rs.next()) {
GroupId id = new GroupId(rs.getBytes(1)); GroupId id = new GroupId(rs.getBytes(1));
if (!ids.add(id)) throw new DbStateException(); if (!ids.add(id)) throw new DbStateException();
String name = rs.getString(2); ClientId clientId = new ClientId(rs.getBytes(2));
byte[] salt = rs.getBytes(3); byte[] descriptor = rs.getBytes(3);
groups.add(new Group(id, name, salt)); groups.add(new Group(id, clientId, descriptor));
version = rs.getLong(4); version = rs.getLong(4);
txCount = rs.getInt(5); txCount = rs.getInt(5);
} }
@@ -2233,32 +2155,6 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
public Map<GroupId, Integer> getUnreadMessageCounts(Connection txn)
throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT groupId, COUNT(*)"
+ " FROM messages AS m"
+ " WHERE read = FALSE"
+ " GROUP BY groupId";
ps = txn.prepareStatement(sql);
rs = ps.executeQuery();
Map<GroupId, Integer> counts = new HashMap<GroupId, Integer>();
while (rs.next()) {
GroupId groupId = new GroupId(rs.getBytes(1));
counts.put(groupId, rs.getInt(2));
}
rs.close();
ps.close();
return Collections.unmodifiableMap(counts);
} catch (SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
public Collection<ContactId> getVisibility(Connection txn, GroupId g) public Collection<ContactId> getVisibility(Connection txn, GroupId g)
throws DbException { throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
@@ -2419,11 +2315,87 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
public void mergeMessageMetadata(Connection txn, MessageId m, Metadata meta)
throws DbException {
PreparedStatement ps = null;
try {
// Determine which keys are being removed
List<String> removed = new ArrayList<String>();
Map<String, byte[]> retained = new HashMap<String, byte[]>();
for (Entry<String, byte[]> e : meta.entrySet()) {
if (e.getValue() == REMOVE) removed.add(e.getKey());
else retained.put(e.getKey(), e.getValue());
}
// Delete any keys that are being removed
if (!removed.isEmpty()) {
String sql = "DELETE FROM messageMetadata"
+ " WHERE messageId = ? AND key = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
for (String key : removed) {
ps.setString(2, key);
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
if (batchAffected.length != removed.size())
throw new DbStateException();
for (int i = 0; i < batchAffected.length; i++) {
if (batchAffected[i] < 0) throw new DbStateException();
if (batchAffected[i] > 1) throw new DbStateException();
}
ps.close();
}
if (retained.isEmpty()) return;
// Update any keys that already exist
String sql = "UPDATE messageMetadata SET value = ?"
+ " WHERE messageId = ? AND key = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(2, m.getBytes());
for (Entry<String, byte[]> e : retained.entrySet()) {
ps.setBytes(1, e.getValue());
ps.setString(3, e.getKey());
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
if (batchAffected.length != retained.size())
throw new DbStateException();
for (int i = 0; i < batchAffected.length; i++) {
if (batchAffected[i] < 0) throw new DbStateException();
if (batchAffected[i] > 1) throw new DbStateException();
}
// Insert any keys that don't already exist
sql = "INSERT INTO messageMetadata (messageId, key, value)"
+ " VALUES (?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
int updateIndex = 0, inserted = 0;
for (Entry<String, byte[]> e : retained.entrySet()) {
if (batchAffected[updateIndex] == 0) {
ps.setString(2, e.getKey());
ps.setBytes(3, e.getValue());
ps.addBatch();
inserted++;
}
updateIndex++;
}
batchAffected = ps.executeBatch();
if (batchAffected.length != inserted) throw new DbStateException();
for (int i = 0; i < batchAffected.length; i++) {
if (batchAffected[i] != 1) throw new DbStateException();
}
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
}
}
public void mergeSettings(Connection txn, Settings s, String namespace) throws DbException { public void mergeSettings(Connection txn, Settings s, String namespace) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
// Update any settings that already exist // Update any settings that already exist
String sql = "UPDATE settings SET value = ? WHERE key = ? AND namespace = ?"; String sql = "UPDATE settings SET value = ?"
+ " WHERE key = ? AND namespace = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
for (Entry<String, String> e : s.entrySet()) { for (Entry<String, String> e : s.entrySet()) {
ps.setString(1, e.getValue()); ps.setString(1, e.getValue());
@@ -2438,7 +2410,8 @@ abstract class JdbcDatabase implements Database<Connection> {
if (batchAffected[i] > 1) throw new DbStateException(); if (batchAffected[i] > 1) throw new DbStateException();
} }
// Insert any settings that don't already exist // Insert any settings that don't already exist
sql = "INSERT INTO settings (key, value, namespace) VALUES (?, ?, ?)"; sql = "INSERT INTO settings (key, value, namespace)"
+ " VALUES (?, ?, ?)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
int updateIndex = 0, inserted = 0; int updateIndex = 0, inserted = 0;
for (Entry<String, String> e : s.entrySet()) { for (Entry<String, String> e : s.entrySet()) {
@@ -2714,6 +2687,23 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
public void setMessageValidity(Connection txn, MessageId m, boolean valid)
throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE messages SET valid = ? WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, valid ? VALIDATION_VALID : VALIDATION_INVALID);
ps.setBytes(2, m.getBytes());
int affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
}
}
public void setReorderingWindow(Connection txn, ContactId c, TransportId t, public void setReorderingWindow(Connection txn, ContactId c, TransportId t,
long rotationPeriod, long base, byte[] bitmap) throws DbException { long rotationPeriod, long base, byte[] bitmap) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
@@ -2798,14 +2788,14 @@ abstract class JdbcDatabase implements Database<Connection> {
// Store the new subscriptions, if any // Store the new subscriptions, if any
if (groups.isEmpty()) return true; if (groups.isEmpty()) return true;
sql = "INSERT INTO contactGroups" sql = "INSERT INTO contactGroups"
+ " (contactId, groupId, name, salt)" + " (contactId, groupId, clientId, descriptor)"
+ " VALUES (?, ?, ?, ?)"; + " VALUES (?, ?, ?, ?)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); ps.setInt(1, c.getInt());
for (Group g : groups) { for (Group g : groups) {
ps.setBytes(2, g.getId().getBytes()); ps.setBytes(2, g.getId().getBytes());
ps.setString(3, g.getName()); ps.setBytes(3, g.getClientId().getBytes());
ps.setBytes(4, g.getSalt()); ps.setBytes(4, g.getDescriptor());
ps.addBatch(); ps.addBatch();
} }
int[] batchAffected = ps.executeBatch(); int[] batchAffected = ps.executeBatch();
@@ -2823,66 +2813,6 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
public void setInboxGroup(Connection txn, ContactId c, Group g)
throws DbException {
PreparedStatement ps = null;
try {
// Unset any existing inbox group for the contact
String sql = "UPDATE groupVisibilities"
+ " SET inbox = FALSE"
+ " WHERE contactId = ?"
+ " AND inbox = TRUE";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.executeUpdate();
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
// Make the group visible to the contact and set it as the inbox
sql = "INSERT INTO groupVisibilities"
+ " (contactId, groupId, inbox)"
+ " VALUES (?, ?, TRUE)";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setBytes(2, g.getId().getBytes());
affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
// Add the group to the contact's subscriptions
sql = "INSERT INTO contactGroups"
+ " (contactId, groupId, name, salt)"
+ " VALUES (?, ?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setBytes(2, g.getId().getBytes());
ps.setString(3, g.getName());
ps.setBytes(4, g.getSalt());
affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
}
}
public void setReadFlag(Connection txn, MessageId m, boolean read)
throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE messages SET read = ? WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBoolean(1, read);
ps.setBytes(2, m.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 setRemoteProperties(Connection txn, ContactId c, public void setRemoteProperties(Connection txn, ContactId c,
Map<TransportId, TransportProperties> p) throws DbException { Map<TransportId, TransportProperties> p) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;

View File

@@ -1,22 +0,0 @@
package org.briarproject.forum;
import com.google.inject.Inject;
import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumFactory;
import org.briarproject.api.sync.GroupFactory;
// Temporary facade during sync protocol refactoring
class ForumFactoryImpl implements ForumFactory {
private final GroupFactory groupFactory;
@Inject
ForumFactoryImpl(GroupFactory groupFactory) {
this.groupFactory = groupFactory;
}
public Forum createForum(String name) {
return new ForumImpl(groupFactory.createGroup(name));
}
}

View File

@@ -1,37 +0,0 @@
package org.briarproject.forum;
import org.briarproject.api.forum.Forum;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId;
// Temporary facade during sync protocol refactoring
class ForumImpl implements Forum {
private final Group group;
ForumImpl(Group group) {
this.group = group;
}
public GroupId getId() {
return group.getId();
}
public String getName() {
return group.getName();
}
Group getGroup() {
return group;
}
@Override
public int hashCode() {
return group.hashCode();
}
@Override
public boolean equals(Object o) {
return o instanceof ForumImpl && group.equals(((ForumImpl) o).group);
}
}

View File

@@ -2,79 +2,272 @@ package org.briarproject.forum;
import com.google.inject.Inject; import com.google.inject.Inject;
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.crypto.CryptoComponent;
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.DatabaseComponent;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Metadata;
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.ForumPost;
import org.briarproject.api.forum.ForumPostHeader; import org.briarproject.api.forum.ForumPostHeader;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group; import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupFactory;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageHeader;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.util.StringUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
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.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
import static org.briarproject.api.identity.Author.Status.ANONYMOUS;
import static org.briarproject.api.identity.Author.Status.UNKNOWN;
import static org.briarproject.api.identity.Author.Status.VERIFIED;
import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
// Temporary facade during sync protocol refactoring
class ForumManagerImpl implements ForumManager { class ForumManagerImpl implements ForumManager {
static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString(
"859a7be50dca035b64bd6902fb797097"
+ "795af837abbf8c16d750b3c2ccc186ea"));
private static final Logger LOG =
Logger.getLogger(ForumManagerImpl.class.getName());
private final DatabaseComponent db; private final DatabaseComponent db;
private final GroupFactory groupFactory;
private final BdfReaderFactory bdfReaderFactory;
private final BdfWriterFactory bdfWriterFactory;
private final MetadataEncoder metadataEncoder;
private final MetadataParser metadataParser;
private final SecureRandom random;
@Inject @Inject
ForumManagerImpl(DatabaseComponent db) { ForumManagerImpl(CryptoComponent crypto, DatabaseComponent db,
GroupFactory groupFactory, BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
MetadataParser metadataParser) {
this.db = db; this.db = db;
this.groupFactory = groupFactory;
this.bdfReaderFactory = bdfReaderFactory;
this.bdfWriterFactory = bdfWriterFactory;
this.metadataEncoder = metadataEncoder;
this.metadataParser = metadataParser;
random = crypto.getSecureRandom();
}
@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);
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 @Override
public boolean addForum(Forum f) throws DbException { public boolean addForum(Forum f) throws DbException {
return db.addGroup(((ForumImpl) f).getGroup()); return db.addGroup(f.getGroup());
} }
@Override @Override
public void addLocalPost(Message m) throws DbException { public void addLocalPost(ForumPost p) throws DbException {
db.addLocalMessage(m); 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);
try {
Metadata meta = metadataEncoder.encode(d);
db.addLocalMessage(p.getMessage(), CLIENT_ID, meta);
} catch (FormatException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
} }
@Override @Override
public Collection<Forum> getAvailableForums() throws DbException { public Collection<Forum> getAvailableForums() throws DbException {
Collection<Group> groups = db.getAvailableGroups(); Collection<Group> groups = db.getAvailableGroups(CLIENT_ID);
List<Forum> forums = new ArrayList<Forum>(groups.size()); List<Forum> forums = new ArrayList<Forum>(groups.size());
for (Group g : groups) forums.add(new ForumImpl(g)); 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); return Collections.unmodifiableList(forums);
} }
private Forum parseForum(Group g) throws FormatException {
ByteArrayInputStream in = new ByteArrayInputStream(g.getDescriptor());
BdfReader r = bdfReaderFactory.createReader(in);
try {
r.readListStart();
String name = r.readString(MAX_FORUM_NAME_LENGTH);
if (name.length() == 0) throw new FormatException();
byte[] salt = r.readRaw(FORUM_SALT_LENGTH);
if (salt.length != FORUM_SALT_LENGTH) throw new FormatException();
r.readListEnd();
if (!r.eof()) throw new FormatException();
return new Forum(g, name);
} catch (FormatException e) {
throw e;
} catch (IOException e) {
// Shouldn't happen with ByteArrayInputStream
throw new RuntimeException(e);
}
}
@Override @Override
public Forum getForum(GroupId g) throws DbException { public Forum getForum(GroupId g) throws DbException {
return new ForumImpl(db.getGroup(g)); 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 @Override
public Collection<Forum> getForums() throws DbException { public Collection<Forum> getForums() throws DbException {
Collection<Group> groups = db.getGroups(); Collection<Group> groups = db.getGroups(CLIENT_ID);
List<Forum> forums = new ArrayList<Forum>(groups.size()); List<Forum> forums = new ArrayList<Forum>(groups.size());
for (Group g : groups) forums.add(new ForumImpl(g)); 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); return Collections.unmodifiableList(forums);
} }
@Override @Override
public byte[] getPostBody(MessageId m) throws DbException { public byte[] getPostBody(MessageId m) throws DbException {
return db.getMessageBody(m); 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 @Override
public Collection<ForumPostHeader> getPostHeaders(GroupId g) public Collection<ForumPostHeader> getPostHeaders(GroupId g)
throws DbException { throws DbException {
Collection<MessageHeader> headers = db.getMessageHeaders(g); // Load the IDs of the user's own identities and contacts' identities
List<ForumPostHeader> postHeaders = Set<AuthorId> localAuthorIds = new HashSet<AuthorId>();
new ArrayList<ForumPostHeader>(headers.size()); for (LocalAuthor a : db.getLocalAuthors())
for (MessageHeader m : headers) localAuthorIds.add(a.getId());
postHeaders.add(new ForumPostHeaderImpl(m)); Set<AuthorId> contactAuthorIds = new HashSet<AuthorId>();
return Collections.unmodifiableList(postHeaders); 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 @Override
@@ -89,12 +282,18 @@ class ForumManagerImpl implements ForumManager {
@Override @Override
public void removeForum(Forum f) throws DbException { public void removeForum(Forum f) throws DbException {
db.removeGroup(((ForumImpl) f).getGroup()); db.removeGroup(f.getGroup());
} }
@Override @Override
public void setReadFlag(MessageId m, boolean read) throws DbException { public void setReadFlag(MessageId m, boolean read) throws DbException {
db.setReadFlag(m, read); BdfDictionary d = new BdfDictionary();
d.put("read", read);
try {
db.mergeMessageMetadata(m, metadataEncoder.encode(d));
} catch (FormatException e) {
throw new RuntimeException(e);
}
} }
@Override @Override

View File

@@ -1,17 +1,41 @@
package org.briarproject.forum; package org.briarproject.forum;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import org.briarproject.api.forum.ForumFactory; import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.data.BdfReaderFactory;
import org.briarproject.api.data.BdfWriterFactory;
import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.data.ObjectReader;
import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumManager;
import org.briarproject.api.forum.ForumPostFactory; import org.briarproject.api.forum.ForumPostFactory;
import org.briarproject.api.identity.Author;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.sync.ValidationManager;
import org.briarproject.api.system.Clock;
import javax.inject.Singleton;
public class ForumModule extends AbstractModule { public class ForumModule extends AbstractModule {
@Override @Override
protected void configure() { protected void configure() {
bind(ForumFactory.class).to(ForumFactoryImpl.class);
bind(ForumManager.class).to(ForumManagerImpl.class); bind(ForumManager.class).to(ForumManagerImpl.class);
bind(ForumPostFactory.class).to(ForumPostFactoryImpl.class); bind(ForumPostFactory.class).to(ForumPostFactoryImpl.class);
} }
@Provides @Singleton
ForumPostValidator getValidator(LifecycleManager lifecycleManager,
CryptoComponent crypto, ValidationManager validationManager,
BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory,
ObjectReader<Author> authorReader, MetadataEncoder metadataEncoder,
Clock clock) {
ForumPostValidator validator = new ForumPostValidator(crypto,
validationManager, bdfReaderFactory, bdfWriterFactory,
authorReader, metadataEncoder, clock);
lifecycleManager.register(validator);
return validator;
}
} }

View File

@@ -1,43 +1,114 @@
package org.briarproject.forum; package org.briarproject.forum;
import com.google.inject.Inject; import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.PrivateKey; import org.briarproject.api.crypto.PrivateKey;
import org.briarproject.api.forum.Forum; import org.briarproject.api.crypto.Signature;
import org.briarproject.api.data.BdfWriter;
import org.briarproject.api.data.BdfWriterFactory;
import org.briarproject.api.forum.ForumPost;
import org.briarproject.api.forum.ForumPostFactory; import org.briarproject.api.forum.ForumPostFactory;
import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageFactory; import org.briarproject.api.sync.MessageFactory;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.util.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
// Temporary facade during sync protocol refactoring import javax.inject.Inject;
import static org.briarproject.api.forum.ForumConstants.MAX_CONTENT_TYPE_LENGTH;
import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
class ForumPostFactoryImpl implements ForumPostFactory { class ForumPostFactoryImpl implements ForumPostFactory {
private final CryptoComponent crypto;
private final MessageFactory messageFactory; private final MessageFactory messageFactory;
private final BdfWriterFactory bdfWriterFactory;
@Inject @Inject
ForumPostFactoryImpl(MessageFactory messageFactory) { ForumPostFactoryImpl(CryptoComponent crypto, MessageFactory messageFactory,
BdfWriterFactory bdfWriterFactory) {
this.crypto = crypto;
this.messageFactory = messageFactory; this.messageFactory = messageFactory;
this.bdfWriterFactory = bdfWriterFactory;
} }
@Override @Override
public Message createAnonymousPost(MessageId parent, Forum forum, public ForumPost createAnonymousPost(GroupId groupId, long timestamp,
String contentType, long timestamp, byte[] body) MessageId parent, String contentType, byte[] body)
throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
return messageFactory.createAnonymousMessage(parent, // Validate the arguments
((ForumImpl) forum).getGroup(), contentType, timestamp, body); if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH)
throw new IllegalArgumentException();
if (body.length > MAX_FORUM_POST_BODY_LENGTH)
throw new IllegalArgumentException();
// Serialise the message to a buffer
ByteArrayOutputStream out = new ByteArrayOutputStream();
BdfWriter w = bdfWriterFactory.createWriter(out);
w.writeListStart();
if (parent == null) w.writeNull();
else w.writeRaw(parent.getBytes());
w.writeNull(); // No author
w.writeString(contentType);
w.writeRaw(body);
w.writeNull(); // No signature
w.writeListEnd();
Message m = messageFactory.createMessage(groupId, timestamp,
out.toByteArray());
return new ForumPost(m, parent, null, contentType);
} }
@Override @Override
public Message createPseudonymousPost(MessageId parent, Forum forum, public ForumPost createPseudonymousPost(GroupId groupId, long timestamp,
Author author, PrivateKey privateKey, String contentType, MessageId parent, Author author, String contentType, byte[] body,
long timestamp, byte[] body) PrivateKey privateKey) throws IOException,
throws IOException, GeneralSecurityException { GeneralSecurityException {
return messageFactory.createPseudonymousMessage(parent, // Validate the arguments
((ForumImpl) forum).getGroup(), author, privateKey, contentType, if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH)
timestamp, body); throw new IllegalArgumentException();
if (body.length > MAX_FORUM_POST_BODY_LENGTH)
throw new IllegalArgumentException();
// Serialise the data to be signed
ByteArrayOutputStream out = new ByteArrayOutputStream();
BdfWriter w = bdfWriterFactory.createWriter(out);
w.writeListStart();
w.writeRaw(groupId.getBytes());
w.writeInteger(timestamp);
if (parent == null) w.writeNull();
else w.writeRaw(parent.getBytes());
writeAuthor(w, author);
w.writeString(contentType);
w.writeRaw(body);
w.writeListEnd();
// Generate the signature
Signature signature = crypto.getSignature();
signature.initSign(privateKey);
signature.update(out.toByteArray());
byte[] sig = signature.sign();
// Serialise the signed message
out.reset();
w = bdfWriterFactory.createWriter(out);
w.writeListStart();
if (parent == null) w.writeNull();
else w.writeRaw(parent.getBytes());
writeAuthor(w, author);
w.writeString(contentType);
w.writeRaw(body);
w.writeRaw(sig);
w.writeListEnd();
Message m = messageFactory.createMessage(groupId, timestamp,
out.toByteArray());
return new ForumPost(m, parent, author, contentType);
}
private void writeAuthor(BdfWriter w, Author a) throws IOException {
w.writeListStart();
w.writeString(a.getName());
w.writeRaw(a.getPublicKey());
w.writeListEnd();
} }
} }

View File

@@ -1,46 +0,0 @@
package org.briarproject.forum;
import org.briarproject.api.forum.ForumPostHeader;
import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.MessageHeader;
import org.briarproject.api.sync.MessageId;
// Temporary facade during sync protocol refactoring
class ForumPostHeaderImpl implements ForumPostHeader {
private final MessageHeader messageHeader;
ForumPostHeaderImpl(MessageHeader messageHeader) {
this.messageHeader = messageHeader;
}
@Override
public MessageId getId() {
return messageHeader.getId();
}
@Override
public Author getAuthor() {
return messageHeader.getAuthor();
}
@Override
public Author.Status getAuthorStatus() {
return messageHeader.getAuthorStatus();
}
@Override
public String getContentType() {
return messageHeader.getContentType();
}
@Override
public long getTimestamp() {
return messageHeader.getTimestamp();
}
@Override
public boolean isRead() {
return messageHeader.isRead();
}
}

View File

@@ -0,0 +1,184 @@
package org.briarproject.forum;
import org.briarproject.api.FormatException;
import org.briarproject.api.UniqueId;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.KeyParser;
import org.briarproject.api.crypto.PublicKey;
import org.briarproject.api.crypto.Signature;
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.ObjectReader;
import org.briarproject.api.db.Metadata;
import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.MessageValidator;
import org.briarproject.api.sync.ValidationManager;
import org.briarproject.api.system.Clock;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.logging.Logger;
import javax.inject.Inject;
import static org.briarproject.api.forum.ForumConstants.MAX_CONTENT_TYPE_LENGTH;
import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
import static org.briarproject.forum.ForumManagerImpl.CLIENT_ID;
class ForumPostValidator implements MessageValidator {
private static final Logger LOG =
Logger.getLogger(ForumPostValidator.class.getName());
private final CryptoComponent crypto;
private final ValidationManager validationManager;
private final BdfReaderFactory bdfReaderFactory;
private final BdfWriterFactory bdfWriterFactory;
private final ObjectReader<Author> authorReader;
private final MetadataEncoder metadataEncoder;
private final Clock clock;
private final KeyParser keyParser;
@Inject
ForumPostValidator(CryptoComponent crypto,
ValidationManager validationManager,
BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory,
ObjectReader<Author> authorReader,
MetadataEncoder metadataEncoder, Clock clock) {
this.crypto = crypto;
this.validationManager = validationManager;
this.bdfReaderFactory = bdfReaderFactory;
this.bdfWriterFactory = bdfWriterFactory;
this.authorReader = authorReader;
this.metadataEncoder = metadataEncoder;
this.clock = clock;
keyParser = crypto.getSignatureKeyParser();
}
@Override
public boolean start() {
validationManager.setMessageValidator(CLIENT_ID, this);
return true;
}
@Override
public boolean stop() {
return true;
}
@Override
public Metadata validateMessage(Message m) {
// Reject the message if it's too far in the future
long now = clock.currentTimeMillis();
if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
LOG.info("Timestamp is too far in the future");
return null;
}
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);
MessageId parent = null;
Author author = null;
String contentType;
byte[] postBody, sig = null;
r.readListStart();
// Read the parent ID, if any
if (r.hasRaw()) {
byte[] id = r.readRaw(UniqueId.LENGTH);
if (id.length < UniqueId.LENGTH) throw new FormatException();
parent = new MessageId(id);
} else {
r.readNull();
}
// Read the author, if any
if (r.hasList()) author = authorReader.readObject(r);
else r.readNull();
// Read the content type
contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
// Read the forum post body
postBody = r.readRaw(MAX_FORUM_POST_BODY_LENGTH);
// Read the signature, if any
if (r.hasRaw()) sig = r.readRaw(MAX_SIGNATURE_LENGTH);
else r.readNull();
r.readListEnd();
if (!r.eof()) throw new FormatException();
// If there's an author there must be a signature and vice versa
if (author != null && sig == null) {
LOG.info("Author without signature");
return null;
}
if (author == null && sig != null) {
LOG.info("Signature without author");
return null;
}
// Verify the signature, if any
if (author != null) {
// Parse the public key
PublicKey key = keyParser.parsePublicKey(author.getPublicKey());
// Serialise the data to be signed
ByteArrayOutputStream out = new ByteArrayOutputStream();
BdfWriter w = bdfWriterFactory.createWriter(out);
w.writeListStart();
w.writeRaw(m.getGroupId().getBytes());
w.writeInteger(m.getTimestamp());
if (parent == null) w.writeNull();
else w.writeRaw(parent.getBytes());
writeAuthor(w, author);
w.writeString(contentType);
w.writeRaw(postBody);
w.writeListEnd();
// Verify the signature
Signature signature = crypto.getSignature();
signature.initVerify(key);
signature.update(out.toByteArray());
if (!signature.verify(sig)) {
LOG.info("Invalid signature");
return null;
}
}
// Return the metadata
BdfDictionary d = new BdfDictionary();
d.put("timestamp", m.getTimestamp());
if (parent != null) d.put("parent", parent.getBytes());
if (author != null) {
BdfDictionary d1 = new BdfDictionary();
d1.put("id", author.getId().getBytes());
d1.put("name", author.getName());
d1.put("publicKey", author.getPublicKey());
d.put("author", d1);
}
d.put("contentType", contentType);
d.put("read", false);
return metadataEncoder.encode(d);
} catch (IOException e) {
LOG.info("Invalid forum post");
return null;
} catch (GeneralSecurityException e) {
LOG.info("Invalid public key");
return null;
}
}
private void writeAuthor(BdfWriter w, Author a) throws IOException {
w.writeListStart();
w.writeString(a.getName());
w.writeRaw(a.getPublicKey());
w.writeListEnd();
}
}

View File

@@ -282,13 +282,13 @@ abstract class Connector extends Thread {
// Add the contact to the database // Add the contact to the database
contactId = contactManager.addContact(remoteAuthor, contactId = contactManager.addContact(remoteAuthor,
localAuthor.getId()); localAuthor.getId());
// Create a private messaging conversation
messagingManager.addContact(contactId, master);
// Store the remote transport properties // Store the remote transport properties
transportPropertyManager.setRemoteProperties(contactId, remoteProps); transportPropertyManager.setRemoteProperties(contactId, remoteProps);
// Derive transport keys for each transport shared with the contact // Derive transport keys for each transport shared with the contact
keyManager.addContact(contactId, remoteProps.keySet(), master, keyManager.addContact(contactId, remoteProps.keySet(), master,
timestamp, alice); timestamp, alice);
// Create a private messaging conversation
messagingManager.addContact(contactId);
} }
protected void tryToClose(DuplexTransportConnection conn, protected void tryToClose(DuplexTransportConnection conn,

View File

@@ -2,88 +2,205 @@ package org.briarproject.messaging;
import com.google.inject.Inject; import com.google.inject.Inject;
import org.briarproject.api.FormatException;
import org.briarproject.api.UniqueId;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactId;
import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.crypto.SecretKey; 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.DatabaseComponent;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Metadata;
import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.messaging.MessagingManager; import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateConversation; import org.briarproject.api.messaging.PrivateMessage;
import org.briarproject.api.messaging.PrivateMessageHeader; import org.briarproject.api.messaging.PrivateMessageHeader;
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.GroupFactory;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageHeader;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.MessageStatus;
import org.briarproject.util.StringUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.Map;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
// Temporary facade during sync protocol refactoring
class MessagingManagerImpl implements MessagingManager { class MessagingManagerImpl implements MessagingManager {
static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString(
"6bcdc006c0910b0f44e40644c3b31f1a"
+ "8bf9a6d6021d40d219c86b731b903070"));
private static final Logger LOG =
Logger.getLogger(MessagingManagerImpl.class.getName());
private final DatabaseComponent db; private final DatabaseComponent db;
private final CryptoComponent crypto;
private final GroupFactory groupFactory; private final GroupFactory groupFactory;
private final BdfReaderFactory bdfReaderFactory;
private final BdfWriterFactory bdfWriterFactory;
private final MetadataEncoder metadataEncoder;
private final MetadataParser metadataParser;
@Inject @Inject
MessagingManagerImpl(DatabaseComponent db, CryptoComponent crypto, MessagingManagerImpl(DatabaseComponent db, GroupFactory groupFactory,
GroupFactory groupFactory) { BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
MetadataParser metadataParser) {
this.db = db; this.db = db;
this.crypto = crypto;
this.groupFactory = groupFactory; this.groupFactory = groupFactory;
this.bdfReaderFactory = bdfReaderFactory;
this.bdfWriterFactory = bdfWriterFactory;
this.metadataEncoder = metadataEncoder;
this.metadataParser = metadataParser;
} }
@Override @Override
public void addContact(ContactId c, SecretKey master) throws DbException { public ClientId getClientId() {
byte[] salt = crypto.deriveGroupSalt(master); return CLIENT_ID;
Group inbox = groupFactory.createGroup("Inbox", salt);
db.addGroup(inbox);
db.setInboxGroup(c, inbox);
} }
@Override @Override
public void addLocalMessage(Message m) throws DbException { public void addContact(ContactId c) throws DbException {
db.addLocalMessage(m); // Create the conversation group
Group conversation = createConversationGroup(db.getContact(c));
// Subscribe to the group and share it with the contact
db.addGroup(conversation);
db.addContactGroup(c, conversation);
db.setVisibility(conversation.getId(), Collections.singletonList(c));
}
private Group createConversationGroup(Contact c) {
AuthorId local = c.getLocalAuthorId();
AuthorId remote = c.getAuthor().getId();
byte[] descriptor = createGroupDescriptor(local, remote);
return groupFactory.createGroup(CLIENT_ID, descriptor);
}
private byte[] createGroupDescriptor(AuthorId local, AuthorId remote) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
BdfWriter w = bdfWriterFactory.createWriter(out);
try {
w.writeListStart();
if (UniqueId.IdComparator.INSTANCE.compare(local, remote) < 0) {
w.writeRaw(local.getBytes());
w.writeRaw(remote.getBytes());
} else {
w.writeRaw(remote.getBytes());
w.writeRaw(local.getBytes());
}
w.writeListEnd();
} catch (IOException e) {
// Shouldn't happen with ByteArrayOutputStream
throw new RuntimeException(e);
}
return out.toByteArray();
} }
@Override @Override
public PrivateConversation getConversation(GroupId g) throws DbException { public void addLocalMessage(PrivateMessage m) throws DbException {
return new PrivateConversationImpl(db.getGroup(g)); BdfDictionary d = new BdfDictionary();
d.put("timestamp", m.getMessage().getTimestamp());
if (m.getParent() != null) d.put("parent", m.getParent().getBytes());
d.put("contentType", m.getContentType());
d.put("local", true);
d.put("read", true);
try {
Metadata meta = metadataEncoder.encode(d);
db.addLocalMessage(m.getMessage(), CLIENT_ID, meta);
} catch (FormatException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
@Override
public ContactId getContactId(GroupId g) throws DbException {
// TODO: Make this more efficient
for (Contact c : db.getContacts()) {
Group conversation = createConversationGroup(c);
if (conversation.getId().equals(g)) return c.getId();
}
throw new NoSuchContactException();
} }
@Override @Override
public GroupId getConversationId(ContactId c) throws DbException { public GroupId getConversationId(ContactId c) throws DbException {
return db.getInboxGroupId(c); return createConversationGroup(db.getContact(c)).getId();
} }
@Override @Override
public Collection<PrivateMessageHeader> getMessageHeaders(ContactId c) public Collection<PrivateMessageHeader> getMessageHeaders(ContactId c)
throws DbException { throws DbException {
Collection<MessageHeader> headers = db.getInboxMessageHeaders(c); GroupId groupId = getConversationId(c);
List<PrivateMessageHeader> privateHeaders = Map<MessageId, Metadata> metadata = db.getMessageMetadata(groupId);
new ArrayList<PrivateMessageHeader>(headers.size()); Collection<MessageStatus> statuses = db.getMessageStatus(c, groupId);
for (MessageHeader m : headers) Collection<PrivateMessageHeader> headers =
privateHeaders.add(new PrivateMessageHeaderImpl(m)); new ArrayList<PrivateMessageHeader>();
return Collections.unmodifiableList(privateHeaders); for (MessageStatus s : statuses) {
MessageId id = s.getMessageId();
Metadata m = metadata.get(id);
if (m == null) continue;
try {
BdfDictionary d = metadataParser.parse(m);
long timestamp = d.getInteger("timestamp");
String contentType = d.getString("contentType");
boolean local = d.getBoolean("local");
boolean read = d.getBoolean("read");
headers.add(new PrivateMessageHeader(id, timestamp, contentType,
local, read, s.isSent(), s.isSeen()));
} catch (FormatException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
return headers;
} }
@Override @Override
public byte[] getMessageBody(MessageId m) throws DbException { public byte[] getMessageBody(MessageId m) throws DbException {
return db.getMessageBody(m); byte[] raw = db.getRawMessage(m);
} ByteArrayInputStream in = new ByteArrayInputStream(raw,
MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
@Override BdfReader r = bdfReaderFactory.createReader(in);
public void setConversation(ContactId c, PrivateConversation p) try {
throws DbException { // Extract the private message body
db.setInboxGroup(c, ((PrivateConversationImpl) p).getGroup()); r.readListStart();
if (r.hasRaw()) r.skipRaw(); // Parent ID
else r.skipNull(); // No parent
r.skipString(); // Content type
return r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
} catch (FormatException e) {
// Not a valid private message
throw new IllegalArgumentException();
} catch (IOException e) {
// Shouldn't happen with ByteArrayInputStream
throw new RuntimeException(e);
}
} }
@Override @Override
public void setReadFlag(MessageId m, boolean read) throws DbException { public void setReadFlag(MessageId m, boolean read) throws DbException {
db.setReadFlag(m, read); BdfDictionary d = new BdfDictionary();
d.put("read", read);
try {
db.mergeMessageMetadata(m, metadataEncoder.encode(d));
} catch (FormatException e) {
throw new RuntimeException(e);
}
} }
} }

View File

@@ -1,9 +1,17 @@
package org.briarproject.messaging; package org.briarproject.messaging;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import org.briarproject.api.data.BdfReaderFactory;
import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.MessagingManager; import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateMessageFactory; import org.briarproject.api.messaging.PrivateMessageFactory;
import org.briarproject.api.sync.ValidationManager;
import org.briarproject.api.system.Clock;
import javax.inject.Singleton;
public class MessagingModule extends AbstractModule { public class MessagingModule extends AbstractModule {
@@ -12,4 +20,15 @@ public class MessagingModule extends AbstractModule {
bind(MessagingManager.class).to(MessagingManagerImpl.class); bind(MessagingManager.class).to(MessagingManagerImpl.class);
bind(PrivateMessageFactory.class).to(PrivateMessageFactoryImpl.class); bind(PrivateMessageFactory.class).to(PrivateMessageFactoryImpl.class);
} }
@Provides @Singleton
PrivateMessageValidator getValidator(LifecycleManager lifecycleManager,
ValidationManager validationManager,
BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
Clock clock) {
PrivateMessageValidator validator = new PrivateMessageValidator(
validationManager, bdfReaderFactory, metadataEncoder, clock);
lifecycleManager.register(validator);
return validator;
}
} }

View File

@@ -1,35 +0,0 @@
package org.briarproject.messaging;
import org.briarproject.api.messaging.PrivateConversation;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId;
// Temporary facade during sync protocol refactoring
class PrivateConversationImpl implements PrivateConversation {
private final Group group;
PrivateConversationImpl(Group group) {
this.group = group;
}
@Override
public GroupId getId() {
return group.getId();
}
Group getGroup() {
return group;
}
@Override
public int hashCode() {
return group.hashCode();
}
@Override
public boolean equals(Object o) {
return o instanceof PrivateConversationImpl
&& group.equals(((PrivateConversationImpl) o).group);
}
}

View File

@@ -1,33 +1,56 @@
package org.briarproject.messaging; package org.briarproject.messaging;
import com.google.inject.Inject; import org.briarproject.api.data.BdfWriter;
import org.briarproject.api.data.BdfWriterFactory;
import org.briarproject.api.messaging.PrivateConversation; import org.briarproject.api.messaging.PrivateMessage;
import org.briarproject.api.messaging.PrivateMessageFactory; import org.briarproject.api.messaging.PrivateMessageFactory;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageFactory; import org.briarproject.api.sync.MessageFactory;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.util.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
// Temporary facade during sync protocol refactoring import javax.inject.Inject;
import static org.briarproject.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_LENGTH;
import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
class PrivateMessageFactoryImpl implements PrivateMessageFactory { class PrivateMessageFactoryImpl implements PrivateMessageFactory {
private final MessageFactory messageFactory; private final MessageFactory messageFactory;
private final BdfWriterFactory bdfWriterFactory;
@Inject @Inject
PrivateMessageFactoryImpl(MessageFactory messageFactory) { PrivateMessageFactoryImpl(MessageFactory messageFactory,
BdfWriterFactory bdfWriterFactory) {
this.messageFactory = messageFactory; this.messageFactory = messageFactory;
this.bdfWriterFactory = bdfWriterFactory;
} }
@Override @Override
public Message createPrivateMessage(MessageId parent, public PrivateMessage createPrivateMessage(GroupId groupId, long timestamp,
PrivateConversation conversation, String contentType, MessageId parent, String contentType, byte[] body)
long timestamp, byte[] body)
throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
return messageFactory.createAnonymousMessage(parent, // Validate the arguments
((PrivateConversationImpl) conversation).getGroup(), if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH)
contentType, timestamp, body); throw new IllegalArgumentException();
if (body.length > MAX_PRIVATE_MESSAGE_BODY_LENGTH)
throw new IllegalArgumentException();
// Serialise the message
ByteArrayOutputStream out = new ByteArrayOutputStream();
BdfWriter w = bdfWriterFactory.createWriter(out);
w.writeListStart();
if (parent == null) w.writeNull();
else w.writeRaw(parent.getBytes());
w.writeString(contentType);
w.writeRaw(body);
w.writeListEnd();
Message m = messageFactory.createMessage(groupId, timestamp,
out.toByteArray());
return new PrivateMessage(m, parent, contentType);
} }
} }

View File

@@ -1,60 +0,0 @@
package org.briarproject.messaging;
import org.briarproject.api.identity.Author;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.sync.MessageHeader;
import org.briarproject.api.sync.MessageId;
// Temporary facade during sync protocol refactoring
public class PrivateMessageHeaderImpl implements PrivateMessageHeader {
private final MessageHeader messageHeader;
PrivateMessageHeaderImpl(MessageHeader messageHeader) {
this.messageHeader = messageHeader;
}
@Override
public MessageId getId() {
return messageHeader.getId();
}
@Override
public Author getAuthor() {
return messageHeader.getAuthor();
}
@Override
public String getContentType() {
return messageHeader.getContentType();
}
@Override
public long getTimestamp() {
return messageHeader.getTimestamp();
}
@Override
public boolean isLocal() {
return messageHeader.isLocal();
}
@Override
public boolean isRead() {
return messageHeader.isRead();
}
@Override
public Status getStatus() {
switch (messageHeader.getStatus()) {
case STORED:
return Status.STORED;
case SENT:
return Status.SENT;
case DELIVERED:
return Status.DELIVERED;
default:
throw new IllegalStateException();
}
}
}

View File

@@ -0,0 +1,103 @@
package org.briarproject.messaging;
import org.briarproject.api.FormatException;
import org.briarproject.api.UniqueId;
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.Message;
import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.MessageValidator;
import org.briarproject.api.sync.ValidationManager;
import org.briarproject.api.system.Clock;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.logging.Logger;
import javax.inject.Inject;
import static org.briarproject.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_LENGTH;
import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
import static org.briarproject.messaging.MessagingManagerImpl.CLIENT_ID;
class PrivateMessageValidator implements MessageValidator {
private static final Logger LOG =
Logger.getLogger(PrivateMessageValidator.class.getName());
private final ValidationManager validationManager;
private final BdfReaderFactory bdfReaderFactory;
private final MetadataEncoder metadataEncoder;
private final Clock clock;
@Inject
PrivateMessageValidator(ValidationManager validationManager,
BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
Clock clock) {
this.validationManager = validationManager;
this.bdfReaderFactory = bdfReaderFactory;
this.metadataEncoder = metadataEncoder;
this.clock = clock;
}
@Override
public boolean start() {
validationManager.setMessageValidator(CLIENT_ID, this);
return true;
}
@Override
public boolean stop() {
return true;
}
@Override
public Metadata validateMessage(Message m) {
// Reject the message if it's too far in the future
long now = clock.currentTimeMillis();
if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
LOG.info("Timestamp is too far in the future");
return null;
}
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);
MessageId parent = null;
String contentType;
r.readListStart();
// Read the parent ID, if any
if (r.hasRaw()) {
byte[] id = r.readRaw(UniqueId.LENGTH);
if (id.length < UniqueId.LENGTH) throw new FormatException();
parent = new MessageId(id);
} else {
r.readNull();
}
// Read the content type
contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
// Read the private message body
r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
r.readListEnd();
if (!r.eof()) throw new FormatException();
// Return the metadata
BdfDictionary d = new BdfDictionary();
d.put("timestamp", m.getTimestamp());
if (parent != null) d.put("parent", parent.getBytes());
d.put("contentType", contentType);
d.put("local", false);
d.put("read", false);
return metadataEncoder.encode(d);
} catch (IOException e) {
LOG.info("Invalid private message");
return null;
}
}
}

View File

@@ -8,8 +8,8 @@ import org.briarproject.api.plugins.ConnectionRegistry;
import org.briarproject.api.plugins.TransportConnectionReader; import org.briarproject.api.plugins.TransportConnectionReader;
import org.briarproject.api.plugins.TransportConnectionWriter; import org.briarproject.api.plugins.TransportConnectionWriter;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection; import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
import org.briarproject.api.sync.MessagingSession; import org.briarproject.api.sync.SyncSession;
import org.briarproject.api.sync.MessagingSessionFactory; import org.briarproject.api.sync.SyncSessionFactory;
import org.briarproject.api.transport.KeyManager; import org.briarproject.api.transport.KeyManager;
import org.briarproject.api.transport.StreamContext; import org.briarproject.api.transport.StreamContext;
import org.briarproject.api.transport.StreamReaderFactory; import org.briarproject.api.transport.StreamReaderFactory;
@@ -36,20 +36,20 @@ class ConnectionManagerImpl implements ConnectionManager {
private final KeyManager keyManager; private final KeyManager keyManager;
private final StreamReaderFactory streamReaderFactory; private final StreamReaderFactory streamReaderFactory;
private final StreamWriterFactory streamWriterFactory; private final StreamWriterFactory streamWriterFactory;
private final MessagingSessionFactory messagingSessionFactory; private final SyncSessionFactory syncSessionFactory;
private final ConnectionRegistry connectionRegistry; private final ConnectionRegistry connectionRegistry;
@Inject @Inject
ConnectionManagerImpl(@IoExecutor Executor ioExecutor, ConnectionManagerImpl(@IoExecutor Executor ioExecutor,
KeyManager keyManager, StreamReaderFactory streamReaderFactory, KeyManager keyManager, StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory, StreamWriterFactory streamWriterFactory,
MessagingSessionFactory messagingSessionFactory, SyncSessionFactory syncSessionFactory,
ConnectionRegistry connectionRegistry) { ConnectionRegistry connectionRegistry) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.keyManager = keyManager; this.keyManager = keyManager;
this.streamReaderFactory = streamReaderFactory; this.streamReaderFactory = streamReaderFactory;
this.streamWriterFactory = streamWriterFactory; this.streamWriterFactory = streamWriterFactory;
this.messagingSessionFactory = messagingSessionFactory; this.syncSessionFactory = syncSessionFactory;
this.connectionRegistry = connectionRegistry; this.connectionRegistry = connectionRegistry;
} }
@@ -87,28 +87,28 @@ class ConnectionManagerImpl implements ConnectionManager {
return tag; return tag;
} }
private MessagingSession createIncomingSession(StreamContext ctx, private SyncSession createIncomingSession(StreamContext ctx,
TransportConnectionReader r) throws IOException { TransportConnectionReader r) throws IOException {
InputStream streamReader = streamReaderFactory.createStreamReader( InputStream streamReader = streamReaderFactory.createStreamReader(
r.getInputStream(), ctx); r.getInputStream(), ctx);
return messagingSessionFactory.createIncomingSession( return syncSessionFactory.createIncomingSession(
ctx.getContactId(), ctx.getTransportId(), streamReader); ctx.getContactId(), ctx.getTransportId(), streamReader);
} }
private MessagingSession createSimplexOutgoingSession(StreamContext ctx, private SyncSession createSimplexOutgoingSession(StreamContext ctx,
TransportConnectionWriter w) throws IOException { TransportConnectionWriter w) throws IOException {
OutputStream streamWriter = streamWriterFactory.createStreamWriter( OutputStream streamWriter = streamWriterFactory.createStreamWriter(
w.getOutputStream(), ctx); w.getOutputStream(), ctx);
return messagingSessionFactory.createSimplexOutgoingSession( return syncSessionFactory.createSimplexOutgoingSession(
ctx.getContactId(), ctx.getTransportId(), w.getMaxLatency(), ctx.getContactId(), ctx.getTransportId(), w.getMaxLatency(),
streamWriter); streamWriter);
} }
private MessagingSession createDuplexOutgoingSession(StreamContext ctx, private SyncSession createDuplexOutgoingSession(StreamContext ctx,
TransportConnectionWriter w) throws IOException { TransportConnectionWriter w) throws IOException {
OutputStream streamWriter = streamWriterFactory.createStreamWriter( OutputStream streamWriter = streamWriterFactory.createStreamWriter(
w.getOutputStream(), ctx); w.getOutputStream(), ctx);
return messagingSessionFactory.createDuplexOutgoingSession( return syncSessionFactory.createDuplexOutgoingSession(
ctx.getContactId(), ctx.getTransportId(), w.getMaxLatency(), ctx.getContactId(), ctx.getTransportId(), w.getMaxLatency(),
w.getMaxIdleTime(), streamWriter); w.getMaxIdleTime(), streamWriter);
} }
@@ -214,8 +214,8 @@ class ConnectionManagerImpl implements ConnectionManager {
private final TransportConnectionWriter writer; private final TransportConnectionWriter writer;
private volatile ContactId contactId = null; private volatile ContactId contactId = null;
private volatile MessagingSession incomingSession = null; private volatile SyncSession incomingSession = null;
private volatile MessagingSession outgoingSession = null; private volatile SyncSession outgoingSession = null;
private ManageIncomingDuplexConnection(TransportId transportId, private ManageIncomingDuplexConnection(TransportId transportId,
DuplexTransportConnection transport) { DuplexTransportConnection transport) {
@@ -309,8 +309,8 @@ class ConnectionManagerImpl implements ConnectionManager {
private final TransportConnectionReader reader; private final TransportConnectionReader reader;
private final TransportConnectionWriter writer; private final TransportConnectionWriter writer;
private volatile MessagingSession incomingSession = null; private volatile SyncSession incomingSession = null;
private volatile MessagingSession outgoingSession = null; private volatile SyncSession outgoingSession = null;
private ManageOutgoingDuplexConnection(ContactId contactId, private ManageOutgoingDuplexConnection(ContactId contactId,
TransportId transportId, DuplexTransportConnection transport) { TransportId transportId, DuplexTransportConnection transport) {

View File

@@ -1,7 +1,6 @@
package org.briarproject.sync; package org.briarproject.sync;
import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.MessageDigest;
import org.briarproject.api.data.BdfWriter; import org.briarproject.api.data.BdfWriter;
import org.briarproject.api.data.BdfWriterFactory; import org.briarproject.api.data.BdfWriterFactory;
import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author;
@@ -49,10 +48,8 @@ class AuthorFactoryImpl implements AuthorFactory {
w.writeListEnd(); w.writeListEnd();
} catch (IOException e) { } catch (IOException e) {
// Shouldn't happen with ByteArrayOutputStream // Shouldn't happen with ByteArrayOutputStream
throw new RuntimeException(); throw new RuntimeException(e);
} }
MessageDigest messageDigest = crypto.getMessageDigest(); return new AuthorId(crypto.hash(AuthorId.LABEL, out.toByteArray()));
messageDigest.update(out.toByteArray());
return new AuthorId(messageDigest.digest());
} }
} }

View File

@@ -1,12 +1,10 @@
package org.briarproject.sync; package org.briarproject.sync;
import org.briarproject.api.FormatException; import org.briarproject.api.FormatException;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.MessageDigest;
import org.briarproject.api.data.BdfReader; import org.briarproject.api.data.BdfReader;
import org.briarproject.api.data.ObjectReader; import org.briarproject.api.data.ObjectReader;
import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorId; import org.briarproject.api.identity.AuthorFactory;
import java.io.IOException; import java.io.IOException;
@@ -15,26 +13,18 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGT
class AuthorReader implements ObjectReader<Author> { class AuthorReader implements ObjectReader<Author> {
private final MessageDigest messageDigest; private final AuthorFactory authorFactory;
AuthorReader(CryptoComponent crypto) { AuthorReader(AuthorFactory authorFactory) {
messageDigest = crypto.getMessageDigest(); this.authorFactory = authorFactory;
} }
public Author readObject(BdfReader r) throws IOException { public Author readObject(BdfReader r) throws IOException {
// Set up the reader
DigestingConsumer digesting = new DigestingConsumer(messageDigest);
r.addConsumer(digesting);
// Read and digest the data
r.readListStart(); r.readListStart();
String name = r.readString(MAX_AUTHOR_NAME_LENGTH); String name = r.readString(MAX_AUTHOR_NAME_LENGTH);
if (name.length() == 0) throw new FormatException(); if (name.length() == 0) throw new FormatException();
byte[] publicKey = r.readRaw(MAX_PUBLIC_KEY_LENGTH); byte[] publicKey = r.readRaw(MAX_PUBLIC_KEY_LENGTH);
r.readListEnd(); r.readListEnd();
// Reset the reader return authorFactory.createAuthor(name, publicKey);
r.removeConsumer(digesting);
// Build and return the author
AuthorId id = new AuthorId(messageDigest.digest());
return new Author(id, name, publicKey);
} }
} }

View File

@@ -1,24 +0,0 @@
package org.briarproject.sync;
import org.briarproject.api.data.Consumer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/** A consumer that makes a copy of the bytes consumed. */
class CopyingConsumer implements Consumer {
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
public byte[] getCopy() {
return out.toByteArray();
}
public void write(byte b) throws IOException {
out.write(b);
}
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
}
}

View File

@@ -1,34 +0,0 @@
package org.briarproject.sync;
import org.briarproject.api.FormatException;
import org.briarproject.api.data.Consumer;
import java.io.IOException;
/**
* A consumer that counts the number of bytes consumed and throws a
* FormatException if the count exceeds a given limit.
*/
class CountingConsumer implements Consumer {
private final long limit;
private long count = 0;
public CountingConsumer(long limit) {
this.limit = limit;
}
public long getCount() {
return count;
}
public void write(byte b) throws IOException {
count++;
if (count > limit) throw new FormatException();
}
public void write(byte[] b, int off, int len) throws IOException {
count += len;
if (count > limit) throw new FormatException();
}
}

View File

@@ -1,22 +0,0 @@
package org.briarproject.sync;
import org.briarproject.api.crypto.MessageDigest;
import org.briarproject.api.data.Consumer;
/** A consumer that passes its input through a message digest. */
class DigestingConsumer implements Consumer {
private final MessageDigest messageDigest;
public DigestingConsumer(MessageDigest messageDigest) {
this.messageDigest = messageDigest;
}
public void write(byte b) {
messageDigest.update(b);
}
public void write(byte[] b, int off, int len) {
messageDigest.update(b, off, len);
}
}

View File

@@ -10,21 +10,21 @@ import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener; import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.LocalSubscriptionsUpdatedEvent; import org.briarproject.api.event.LocalSubscriptionsUpdatedEvent;
import org.briarproject.api.event.LocalTransportsUpdatedEvent; import org.briarproject.api.event.LocalTransportsUpdatedEvent;
import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageRequestedEvent;
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.RemoteSubscriptionsUpdatedEvent; import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
import org.briarproject.api.event.RemoteTransportsUpdatedEvent; import org.briarproject.api.event.RemoteTransportsUpdatedEvent;
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;
import org.briarproject.api.sync.MessagingSession;
import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.Offer;
import org.briarproject.api.sync.PacketWriter; import org.briarproject.api.sync.PacketWriter;
import org.briarproject.api.sync.Request; import org.briarproject.api.sync.Request;
import org.briarproject.api.sync.SubscriptionAck; import org.briarproject.api.sync.SubscriptionAck;
import org.briarproject.api.sync.SubscriptionUpdate; import org.briarproject.api.sync.SubscriptionUpdate;
import org.briarproject.api.sync.SyncSession;
import org.briarproject.api.sync.TransportAck; import org.briarproject.api.sync.TransportAck;
import org.briarproject.api.sync.TransportUpdate; import org.briarproject.api.sync.TransportUpdate;
import org.briarproject.api.system.Clock; import org.briarproject.api.system.Clock;
@@ -39,15 +39,15 @@ import java.util.logging.Logger;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.api.sync.MessagingConstants.MAX_PAYLOAD_LENGTH; import static org.briarproject.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH;
/** /**
* An outgoing {@link org.briarproject.api.sync.MessagingSession * An outgoing {@link org.briarproject.api.sync.SyncSession SyncSession}
* MessagingSession} suitable for duplex transports. The session offers * suitable for duplex transports. The session offers messages before sending
* messages before sending them, keeps its output stream open when there are no * them, keeps its output stream open when there are no packets to send, and
* packets to send, and reacts to events that make packets available to send. * reacts to events that make packets available to send.
*/ */
class DuplexOutgoingSession implements MessagingSession, EventListener { class DuplexOutgoingSession implements SyncSession, EventListener {
// Check for retransmittable packets once every 60 seconds // Check for retransmittable packets once every 60 seconds
private static final int RETX_QUERY_INTERVAL = 60 * 1000; private static final int RETX_QUERY_INTERVAL = 60 * 1000;
@@ -161,8 +161,9 @@ class DuplexOutgoingSession implements MessagingSession, EventListener {
if (e instanceof ContactRemovedEvent) { if (e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e; ContactRemovedEvent c = (ContactRemovedEvent) e;
if (c.getContactId().equals(contactId)) interrupt(); if (c.getContactId().equals(contactId)) interrupt();
} else if (e instanceof MessageAddedEvent) { } else if (e instanceof MessageValidatedEvent) {
dbExecutor.execute(new GenerateOffer()); if (((MessageValidatedEvent) e).isValid())
dbExecutor.execute(new GenerateOffer());
} else if (e instanceof LocalSubscriptionsUpdatedEvent) { } else if (e instanceof LocalSubscriptionsUpdatedEvent) {
LocalSubscriptionsUpdatedEvent l = LocalSubscriptionsUpdatedEvent l =
(LocalSubscriptionsUpdatedEvent) e; (LocalSubscriptionsUpdatedEvent) e;
@@ -243,7 +244,7 @@ class DuplexOutgoingSession implements MessagingSession, EventListener {
if (interrupted) return; if (interrupted) return;
try { try {
Collection<byte[]> b = db.generateRequestedBatch(contactId, Collection<byte[]> b = db.generateRequestedBatch(contactId,
MAX_PAYLOAD_LENGTH, maxLatency); MAX_PACKET_PAYLOAD_LENGTH, maxLatency);
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Generated batch: " + (b != null)); LOG.info("Generated batch: " + (b != null));
if (b != null) writerTasks.add(new WriteBatch(b)); if (b != null) writerTasks.add(new WriteBatch(b));

View File

@@ -1,52 +1,24 @@
package org.briarproject.sync; package org.briarproject.sync;
import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.MessageDigest; import org.briarproject.api.sync.ClientId;
import org.briarproject.api.data.BdfWriter;
import org.briarproject.api.data.BdfWriterFactory;
import org.briarproject.api.sync.Group; import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupFactory; import org.briarproject.api.sync.GroupFactory;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import javax.inject.Inject; import javax.inject.Inject;
import static org.briarproject.api.sync.MessagingConstants.GROUP_SALT_LENGTH;
class GroupFactoryImpl implements GroupFactory { class GroupFactoryImpl implements GroupFactory {
private final CryptoComponent crypto; private final CryptoComponent crypto;
private final BdfWriterFactory bdfWriterFactory;
@Inject @Inject
GroupFactoryImpl(CryptoComponent crypto, BdfWriterFactory bdfWriterFactory) { GroupFactoryImpl(CryptoComponent crypto) {
this.crypto = crypto; this.crypto = crypto;
this.bdfWriterFactory = bdfWriterFactory;
} }
public Group createGroup(String name) { public Group createGroup(ClientId c, byte[] descriptor) {
byte[] salt = new byte[GROUP_SALT_LENGTH]; byte[] hash = crypto.hash(GroupId.LABEL, c.getBytes(), descriptor);
crypto.getSecureRandom().nextBytes(salt); return new Group(new GroupId(hash), c, descriptor);
return createGroup(name, salt);
}
public Group createGroup(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();
}
MessageDigest messageDigest = crypto.getMessageDigest();
messageDigest.update(out.toByteArray());
GroupId id = new GroupId(messageDigest.digest());
return new Group(id, name, salt);
} }
} }

View File

@@ -1,39 +1,31 @@
package org.briarproject.sync; package org.briarproject.sync;
import org.briarproject.api.FormatException; import org.briarproject.api.FormatException;
import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.UniqueId;
import org.briarproject.api.crypto.MessageDigest;
import org.briarproject.api.data.BdfReader; import org.briarproject.api.data.BdfReader;
import org.briarproject.api.data.ObjectReader; import org.briarproject.api.data.ObjectReader;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group; import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupFactory;
import java.io.IOException; import java.io.IOException;
import static org.briarproject.api.sync.MessagingConstants.GROUP_SALT_LENGTH; import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.api.sync.MessagingConstants.MAX_GROUP_NAME_LENGTH;
class GroupReader implements ObjectReader<Group> { class GroupReader implements ObjectReader<Group> {
private final MessageDigest messageDigest; private final GroupFactory groupFactory;
GroupReader(CryptoComponent crypto) { GroupReader(GroupFactory groupFactory) {
messageDigest = crypto.getMessageDigest(); this.groupFactory = groupFactory;
} }
public Group readObject(BdfReader r) throws IOException { public Group readObject(BdfReader r) throws IOException {
DigestingConsumer digesting = new DigestingConsumer(messageDigest);
// Read and digest the data
r.addConsumer(digesting);
r.readListStart(); r.readListStart();
String name = r.readString(MAX_GROUP_NAME_LENGTH); byte[] id = r.readRaw(UniqueId.LENGTH);
if (name.length() == 0) throw new FormatException(); if (id.length != UniqueId.LENGTH) throw new FormatException();
byte[] salt = r.readRaw(GROUP_SALT_LENGTH); byte[] descriptor = r.readRaw(MAX_GROUP_DESCRIPTOR_LENGTH);
if (salt.length != GROUP_SALT_LENGTH) throw new FormatException();
r.readListEnd(); r.readListEnd();
r.removeConsumer(digesting); return groupFactory.createGroup(new ClientId(id), descriptor);
// Build and return the group
GroupId id = new GroupId(messageDigest.digest());
return new Group(id, name, salt);
} }
} }

View File

@@ -13,37 +13,30 @@ 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;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageVerifier;
import org.briarproject.api.sync.MessagingSession;
import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.Offer;
import org.briarproject.api.sync.PacketReader; import org.briarproject.api.sync.PacketReader;
import org.briarproject.api.sync.Request; import org.briarproject.api.sync.Request;
import org.briarproject.api.sync.SubscriptionAck; import org.briarproject.api.sync.SubscriptionAck;
import org.briarproject.api.sync.SubscriptionUpdate; import org.briarproject.api.sync.SubscriptionUpdate;
import org.briarproject.api.sync.SyncSession;
import org.briarproject.api.sync.TransportAck; import org.briarproject.api.sync.TransportAck;
import org.briarproject.api.sync.TransportUpdate; import org.briarproject.api.sync.TransportUpdate;
import org.briarproject.api.sync.UnverifiedMessage;
import java.io.IOException; import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
/** /** An incoming {@link org.briarproject.api.sync.SyncSession SyncSession}. */
* An incoming {@link org.briarproject.api.sync.MessagingSession class IncomingSession implements SyncSession, EventListener {
* MessagingSession}.
*/
class IncomingSession implements MessagingSession, EventListener {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(IncomingSession.class.getName()); Logger.getLogger(IncomingSession.class.getName());
private final DatabaseComponent db; private final DatabaseComponent db;
private final Executor dbExecutor, cryptoExecutor; private final Executor dbExecutor;
private final EventBus eventBus; private final EventBus eventBus;
private final MessageVerifier messageVerifier;
private final ContactId contactId; private final ContactId contactId;
private final TransportId transportId; private final TransportId transportId;
private final PacketReader packetReader; private final PacketReader packetReader;
@@ -51,14 +44,11 @@ class IncomingSession implements MessagingSession, EventListener {
private volatile boolean interrupted = false; private volatile boolean interrupted = false;
IncomingSession(DatabaseComponent db, Executor dbExecutor, IncomingSession(DatabaseComponent db, Executor dbExecutor,
Executor cryptoExecutor, EventBus eventBus, EventBus eventBus, ContactId contactId, TransportId transportId,
MessageVerifier messageVerifier, ContactId contactId, PacketReader packetReader) {
TransportId transportId, PacketReader packetReader) {
this.db = db; this.db = db;
this.dbExecutor = dbExecutor; this.dbExecutor = dbExecutor;
this.cryptoExecutor = cryptoExecutor;
this.eventBus = eventBus; this.eventBus = eventBus;
this.messageVerifier = messageVerifier;
this.contactId = contactId; this.contactId = contactId;
this.transportId = transportId; this.transportId = transportId;
this.packetReader = packetReader; this.packetReader = packetReader;
@@ -73,8 +63,8 @@ class IncomingSession implements MessagingSession, EventListener {
Ack a = packetReader.readAck(); Ack a = packetReader.readAck();
dbExecutor.execute(new ReceiveAck(a)); dbExecutor.execute(new ReceiveAck(a));
} else if (packetReader.hasMessage()) { } else if (packetReader.hasMessage()) {
UnverifiedMessage m = packetReader.readMessage(); Message m = packetReader.readMessage();
cryptoExecutor.execute(new VerifyMessage(m)); dbExecutor.execute(new ReceiveMessage(m));
} else if (packetReader.hasOffer()) { } else if (packetReader.hasOffer()) {
Offer o = packetReader.readOffer(); Offer o = packetReader.readOffer();
dbExecutor.execute(new ReceiveOffer(o)); dbExecutor.execute(new ReceiveOffer(o));
@@ -137,25 +127,6 @@ class IncomingSession implements MessagingSession, EventListener {
} }
} }
private class VerifyMessage implements Runnable {
private final UnverifiedMessage message;
private VerifyMessage(UnverifiedMessage message) {
this.message = message;
}
public void run() {
try {
Message m = messageVerifier.verifyMessage(message);
dbExecutor.execute(new ReceiveMessage(m));
} catch (GeneralSecurityException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
interrupt();
}
}
}
private class ReceiveMessage implements Runnable { private class ReceiveMessage implements Runnable {
private final Message message; private final Message message;

View File

@@ -1,129 +1,39 @@
package org.briarproject.sync; package org.briarproject.sync;
import com.google.inject.Inject;
import org.briarproject.api.UniqueId;
import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.MessageDigest; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.crypto.PrivateKey;
import org.briarproject.api.crypto.Signature;
import org.briarproject.api.data.BdfWriter;
import org.briarproject.api.data.BdfWriterFactory;
import org.briarproject.api.data.Consumer;
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.MessageFactory; import org.briarproject.api.sync.MessageFactory;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.util.StringUtils; import org.briarproject.util.ByteUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import javax.inject.Inject; import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
import static org.briarproject.api.sync.MessagingConstants.MAX_BODY_LENGTH;
import static org.briarproject.api.sync.MessagingConstants.MAX_CONTENT_TYPE_LENGTH;
import static org.briarproject.api.sync.MessagingConstants.MAX_PAYLOAD_LENGTH;
import static org.briarproject.api.sync.MessagingConstants.MESSAGE_SALT_LENGTH;
class MessageFactoryImpl implements MessageFactory { class MessageFactoryImpl implements MessageFactory {
private final Signature signature; private final CryptoComponent crypto;
private final SecureRandom random;
private final MessageDigest messageDigest;
private final BdfWriterFactory bdfWriterFactory;
@Inject @Inject
MessageFactoryImpl(CryptoComponent crypto, BdfWriterFactory bdfWriterFactory) { MessageFactoryImpl(CryptoComponent crypto) {
signature = crypto.getSignature(); this.crypto = crypto;
random = crypto.getSecureRandom();
messageDigest = crypto.getMessageDigest();
this.bdfWriterFactory = bdfWriterFactory;
} }
public Message createAnonymousMessage(MessageId parent, Group group, @Override
String contentType, long timestamp, byte[] body) throws IOException, public Message createMessage(GroupId groupId, long timestamp, byte[] body)
GeneralSecurityException { throws IOException {
return createMessage(parent, group, null, null, contentType, timestamp, if (body.length > MAX_MESSAGE_BODY_LENGTH)
body);
}
public Message createPseudonymousMessage(MessageId parent, Group group,
Author author, PrivateKey privateKey, String contentType,
long timestamp, byte[] body) throws IOException,
GeneralSecurityException {
return createMessage(parent, group, author, privateKey, contentType,
timestamp, body);
}
private Message createMessage(MessageId parent, Group group, Author author,
PrivateKey privateKey, String contentType, long timestamp,
byte[] body) throws IOException, GeneralSecurityException {
// Validate the arguments
if ((author == null) != (privateKey == null))
throw new IllegalArgumentException(); throw new IllegalArgumentException();
if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH) byte[] raw = new byte[MESSAGE_HEADER_LENGTH + body.length];
throw new IllegalArgumentException(); System.arraycopy(groupId.getBytes(), 0, raw, 0, UniqueId.LENGTH);
if (body.length > MAX_BODY_LENGTH) ByteUtils.writeUint64(timestamp, raw, UniqueId.LENGTH);
throw new IllegalArgumentException(); System.arraycopy(body, 0, raw, MESSAGE_HEADER_LENGTH, body.length);
// Serialise the message to a buffer MessageId id = new MessageId(crypto.hash(MessageId.LABEL, raw));
ByteArrayOutputStream out = new ByteArrayOutputStream(); return new Message(id, groupId, timestamp, raw);
BdfWriter w = bdfWriterFactory.createWriter(out);
// Initialise the consumers
CountingConsumer counting = new CountingConsumer(MAX_PAYLOAD_LENGTH);
w.addConsumer(counting);
Consumer digestingConsumer = new DigestingConsumer(messageDigest);
w.addConsumer(digestingConsumer);
Consumer signingConsumer = null;
if (privateKey != null) {
signature.initSign(privateKey);
signingConsumer = new SigningConsumer(signature);
w.addConsumer(signingConsumer);
}
// Write the message
w.writeListStart();
if (parent == null) w.writeNull();
else w.writeRaw(parent.getBytes());
writeGroup(w, group);
if (author == null) w.writeNull();
else writeAuthor(w, author);
w.writeString(contentType);
w.writeInteger(timestamp);
byte[] salt = new byte[MESSAGE_SALT_LENGTH];
random.nextBytes(salt);
w.writeRaw(salt);
w.writeRaw(body);
int bodyStart = (int) counting.getCount() - body.length;
// Sign the message with the author's private key, if there is one
if (privateKey == null) {
w.writeNull();
} else {
w.removeConsumer(signingConsumer);
byte[] sig = signature.sign();
if (sig.length > MAX_SIGNATURE_LENGTH)
throw new IllegalArgumentException();
w.writeRaw(sig);
}
w.writeListEnd();
// Hash the message, including the signature, to get the message ID
w.removeConsumer(digestingConsumer);
MessageId id = new MessageId(messageDigest.digest());
return new MessageImpl(id, parent, group, author, contentType,
timestamp, out.toByteArray(), bodyStart, body.length);
}
private void writeGroup(BdfWriter w, Group g) throws IOException {
w.writeListStart();
w.writeString(g.getName());
w.writeRaw(g.getSalt());
w.writeListEnd();
}
private void writeAuthor(BdfWriter w, Author a) throws IOException {
w.writeListStart();
w.writeString(a.getName());
w.writeRaw(a.getPublicKey());
w.writeListEnd();
} }
} }

View File

@@ -1,84 +0,0 @@
package org.briarproject.sync;
import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
import static org.briarproject.api.sync.MessagingConstants.MAX_BODY_LENGTH;
/** A simple in-memory implementation of a message. */
class MessageImpl implements Message {
private final MessageId id, parent;
private final Group group;
private final Author author;
private final String contentType;
private final long timestamp;
private final byte[] raw;
private final int bodyStart, bodyLength;
public MessageImpl(MessageId id, MessageId parent, Group group,
Author author, String contentType, long timestamp,
byte[] raw, int bodyStart, int bodyLength) {
if (bodyStart + bodyLength > raw.length)
throw new IllegalArgumentException();
if (bodyLength > MAX_BODY_LENGTH)
throw new IllegalArgumentException();
this.id = id;
this.parent = parent;
this.group = group;
this.author = author;
this.contentType = contentType;
this.timestamp = timestamp;
this.raw = raw;
this.bodyStart = bodyStart;
this.bodyLength = bodyLength;
}
public MessageId getId() {
return id;
}
public MessageId getParent() {
return parent;
}
public Group getGroup() {
return group;
}
public Author getAuthor() {
return author;
}
public String getContentType() {
return contentType;
}
public long getTimestamp() {
return timestamp;
}
public byte[] getSerialised() {
return raw;
}
public int getBodyStart() {
return bodyStart;
}
public int getBodyLength() {
return bodyLength;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object o) {
return o instanceof Message && id.equals(((Message) o).getId());
}
}

View File

@@ -1,82 +0,0 @@
package org.briarproject.sync;
import org.briarproject.api.FormatException;
import org.briarproject.api.UniqueId;
import org.briarproject.api.data.BdfReader;
import org.briarproject.api.data.ObjectReader;
import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.UnverifiedMessage;
import java.io.IOException;
import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
import static org.briarproject.api.sync.MessagingConstants.MAX_BODY_LENGTH;
import static org.briarproject.api.sync.MessagingConstants.MAX_CONTENT_TYPE_LENGTH;
import static org.briarproject.api.sync.MessagingConstants.MAX_PAYLOAD_LENGTH;
import static org.briarproject.api.sync.MessagingConstants.MESSAGE_SALT_LENGTH;
class MessageReader implements ObjectReader<UnverifiedMessage> {
private final ObjectReader<Group> groupReader;
private final ObjectReader<Author> authorReader;
MessageReader(ObjectReader<Group> groupReader,
ObjectReader<Author> authorReader) {
this.groupReader = groupReader;
this.authorReader = authorReader;
}
public UnverifiedMessage readObject(BdfReader r) throws IOException {
CopyingConsumer copying = new CopyingConsumer();
CountingConsumer counting = new CountingConsumer(MAX_PAYLOAD_LENGTH);
r.addConsumer(copying);
r.addConsumer(counting);
// Read the start of the message
r.readListStart();
// Read the parent's message ID, if there is one
MessageId parent = null;
if (r.hasNull()) {
r.readNull();
} else {
byte[] b = r.readRaw(UniqueId.LENGTH);
if (b.length < UniqueId.LENGTH) throw new FormatException();
parent = new MessageId(b);
}
// Read the group
Group group = groupReader.readObject(r);
// Read the author, if there is one
Author author = null;
if (r.hasNull()) r.readNull();
else author = authorReader.readObject(r);
// Read the content type
String contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
// Read the timestamp
long timestamp = r.readInteger();
if (timestamp < 0) throw new FormatException();
// Read the salt
byte[] salt = r.readRaw(MESSAGE_SALT_LENGTH);
if (salt.length < MESSAGE_SALT_LENGTH) throw new FormatException();
// Read the message body
byte[] body = r.readRaw(MAX_BODY_LENGTH);
// Record the offset of the body within the message
int bodyStart = (int) counting.getCount() - body.length;
// Record the length of the data covered by the author's signature
int signedLength = (int) counting.getCount();
// Read the author's signature, if there is one
byte[] signature = null;
if (author == null) r.readNull();
else signature = r.readRaw(MAX_SIGNATURE_LENGTH);
// Read the end of the message
r.readListEnd();
// Reset the reader
r.removeConsumer(counting);
r.removeConsumer(copying);
// Build and return the unverified message
byte[] raw = copying.getCopy();
return new UnverifiedMessage(parent, group, author, contentType,
timestamp, raw, signature, bodyStart, body.length,
signedLength);
}
}

View File

@@ -1,68 +0,0 @@
package org.briarproject.sync;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.KeyParser;
import org.briarproject.api.crypto.MessageDigest;
import org.briarproject.api.crypto.PublicKey;
import org.briarproject.api.crypto.Signature;
import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.MessageVerifier;
import org.briarproject.api.sync.UnverifiedMessage;
import org.briarproject.api.system.Clock;
import java.security.GeneralSecurityException;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
class MessageVerifierImpl implements MessageVerifier {
private static final Logger LOG =
Logger.getLogger(MessageVerifierImpl.class.getName());
private final CryptoComponent crypto;
private final Clock clock;
private final KeyParser keyParser;
@Inject
MessageVerifierImpl(CryptoComponent crypto, Clock clock) {
this.crypto = crypto;
this.clock = clock;
keyParser = crypto.getSignatureKeyParser();
}
public Message verifyMessage(UnverifiedMessage m)
throws GeneralSecurityException {
long now = System.currentTimeMillis();
MessageDigest messageDigest = crypto.getMessageDigest();
Signature signature = crypto.getSignature();
// Reject the message if it's too far in the future
if (m.getTimestamp() > clock.currentTimeMillis() + MAX_CLOCK_DIFFERENCE)
throw new GeneralSecurityException();
// Hash the message to get the message ID
byte[] raw = m.getSerialised();
messageDigest.update(raw);
MessageId id = new MessageId(messageDigest.digest());
// Verify the author's signature, if there is one
Author author = m.getAuthor();
if (author != null) {
PublicKey k = keyParser.parsePublicKey(author.getPublicKey());
signature.initVerify(k);
signature.update(raw, 0, m.getSignedLength());
if (!signature.verify(m.getSignature()))
throw new GeneralSecurityException();
}
Message verified = new MessageImpl(id, m.getParent(), m.getGroup(),
author, m.getContentType(), m.getTimestamp(), raw,
m.getBodyStart(), m.getBodyLength());
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Verifying message took " + duration + " ms");
return verified;
}
}

View File

@@ -1,11 +1,11 @@
package org.briarproject.sync; package org.briarproject.sync;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.data.BdfReaderFactory; import org.briarproject.api.data.BdfReaderFactory;
import org.briarproject.api.data.ObjectReader; import org.briarproject.api.data.ObjectReader;
import org.briarproject.api.sync.PacketReader; import org.briarproject.api.sync.PacketReader;
import org.briarproject.api.sync.PacketReaderFactory; import org.briarproject.api.sync.PacketReaderFactory;
import org.briarproject.api.sync.SubscriptionUpdate; import org.briarproject.api.sync.SubscriptionUpdate;
import org.briarproject.api.sync.UnverifiedMessage;
import java.io.InputStream; import java.io.InputStream;
@@ -13,21 +13,21 @@ import javax.inject.Inject;
class PacketReaderFactoryImpl implements PacketReaderFactory { class PacketReaderFactoryImpl implements PacketReaderFactory {
private final CryptoComponent crypto;
private final BdfReaderFactory bdfReaderFactory; private final BdfReaderFactory bdfReaderFactory;
private final ObjectReader<UnverifiedMessage> messageReader;
private final ObjectReader<SubscriptionUpdate> subscriptionUpdateReader; private final ObjectReader<SubscriptionUpdate> subscriptionUpdateReader;
@Inject @Inject
PacketReaderFactoryImpl(BdfReaderFactory bdfReaderFactory, PacketReaderFactoryImpl(CryptoComponent crypto,
ObjectReader<UnverifiedMessage> messageReader, BdfReaderFactory bdfReaderFactory,
ObjectReader<SubscriptionUpdate> subscriptionUpdateReader) { ObjectReader<SubscriptionUpdate> subscriptionUpdateReader) {
this.crypto = crypto;
this.bdfReaderFactory = bdfReaderFactory; this.bdfReaderFactory = bdfReaderFactory;
this.messageReader = messageReader;
this.subscriptionUpdateReader = subscriptionUpdateReader; this.subscriptionUpdateReader = subscriptionUpdateReader;
} }
public PacketReader createPacketReader(InputStream in) { public PacketReader createPacketReader(InputStream in) {
return new PacketReaderImpl(bdfReaderFactory, messageReader, return new PacketReaderImpl(crypto, bdfReaderFactory,
subscriptionUpdateReader, in); subscriptionUpdateReader, in);
} }
} }

Some files were not shown because too many files have changed in this diff Show More