diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java index ca66eeb03..bf9259752 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java @@ -87,7 +87,7 @@ class AndroidBluetoothPlugin extends Clock clock, Backoff backoff, PluginCallback callback, - int maxLatency, + long maxLatency, int maxIdleTime) { super(connectionLimiter, connectionFactory, ioExecutor, wakefulIoExecutor, secureRandom, backoff, callback, diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPluginFactory.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPluginFactory.java index 96f094d72..e3a274574 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPluginFactory.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPluginFactory.java @@ -47,7 +47,7 @@ public class AndroidBluetoothPluginFactory implements DuplexPluginFactory { private final BackoffFactory backoffFactory; @Inject - public AndroidBluetoothPluginFactory(@IoExecutor Executor ioExecutor, + AndroidBluetoothPluginFactory(@IoExecutor Executor ioExecutor, @WakefulIoExecutor Executor wakefulIoExecutor, AndroidExecutor androidExecutor, AndroidWakeLockManager wakeLockManager, @@ -75,7 +75,7 @@ public class AndroidBluetoothPluginFactory implements DuplexPluginFactory { } @Override - public int getMaxLatency() { + public long getMaxLatency() { return MAX_LATENCY; } diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/file/AndroidRemovableDrivePlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/file/AndroidRemovableDrivePlugin.java new file mode 100644 index 000000000..c781fc86a --- /dev/null +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/file/AndroidRemovableDrivePlugin.java @@ -0,0 +1,44 @@ +package org.briarproject.bramble.plugin.file; + +import android.app.Application; +import android.net.Uri; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.PluginCallback; +import org.briarproject.bramble.api.properties.TransportProperties; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.PROP_URI; +import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; + +@Immutable +@NotNullByDefault +class AndroidRemovableDrivePlugin extends RemovableDrivePlugin { + + private final Application app; + + AndroidRemovableDrivePlugin(Application app, PluginCallback callback, + long maxLatency) { + super(callback, maxLatency); + this.app = app; + } + + @Override + InputStream openInputStream(TransportProperties p) throws IOException { + String uri = p.get(PROP_URI); + if (isNullOrEmpty(uri)) throw new IllegalArgumentException(); + return app.getContentResolver().openInputStream(Uri.parse(uri)); + } + + @Override + OutputStream openOutputStream(TransportProperties p) throws IOException { + String uri = p.get(PROP_URI); + if (isNullOrEmpty(uri)) throw new IllegalArgumentException(); + return app.getContentResolver().openOutputStream(Uri.parse(uri)); + } +} diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/file/AndroidRemovableDrivePluginFactory.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/file/AndroidRemovableDrivePluginFactory.java new file mode 100644 index 000000000..d13aeda93 --- /dev/null +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/file/AndroidRemovableDrivePluginFactory.java @@ -0,0 +1,47 @@ +package org.briarproject.bramble.plugin.file; + +import android.app.Application; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.PluginCallback; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; +import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static java.util.concurrent.TimeUnit.DAYS; +import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.ID; + +@Immutable +@NotNullByDefault +public class AndroidRemovableDrivePluginFactory implements + SimplexPluginFactory { + + private static final int MAX_LATENCY = (int) DAYS.toMillis(14); + + private final Application app; + + @Inject + AndroidRemovableDrivePluginFactory(Application app) { + this.app = app; + } + + @Override + public TransportId getId() { + return ID; + } + + @Override + public long getMaxLatency() { + return MAX_LATENCY; + } + + @Nullable + @Override + public SimplexPlugin createPlugin(PluginCallback callback) { + return new AndroidRemovableDrivePlugin(app, callback, MAX_LATENCY); + } +} diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java index 46bb20e4a..9bb4f9ade 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java @@ -67,7 +67,7 @@ class AndroidLanTcpPlugin extends LanTcpPlugin { Application app, Backoff backoff, PluginCallback callback, - int maxLatency, + long maxLatency, int maxIdleTime, int connectionTimeout) { super(ioExecutor, wakefulIoExecutor, backoff, callback, maxLatency, diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPluginFactory.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPluginFactory.java index beedd97a4..de3098113 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPluginFactory.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPluginFactory.java @@ -37,7 +37,7 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory { private final Application app; @Inject - public AndroidLanTcpPluginFactory(@IoExecutor Executor ioExecutor, + AndroidLanTcpPluginFactory(@IoExecutor Executor ioExecutor, @WakefulIoExecutor Executor wakefulIoExecutor, EventBus eventBus, BackoffFactory backoffFactory, @@ -55,7 +55,7 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory { } @Override - public int getMaxLatency() { + public long getMaxLatency() { return MAX_LATENCY; } diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java index a4b2a1d8f..617c70518 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java @@ -68,7 +68,7 @@ class AndroidTorPlugin extends TorPlugin { TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback, String architecture, - int maxLatency, + long maxLatency, int maxIdleTime, File torDirectory) { super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPluginFactory.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPluginFactory.java index 2a8bedfff..1434ea229 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPluginFactory.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPluginFactory.java @@ -58,7 +58,7 @@ public class AndroidTorPluginFactory implements DuplexPluginFactory { private final File torDirectory; @Inject - public AndroidTorPluginFactory(@IoExecutor Executor ioExecutor, + AndroidTorPluginFactory(@IoExecutor Executor ioExecutor, @WakefulIoExecutor Executor wakefulIoExecutor, Application app, NetworkManager networkManager, @@ -94,7 +94,7 @@ public class AndroidTorPluginFactory implements DuplexPluginFactory { } @Override - public int getMaxLatency() { + public long getMaxLatency() { return MAX_LATENCY; } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/Consumer.java b/bramble-api/src/main/java/org/briarproject/bramble/api/Consumer.java new file mode 100644 index 000000000..4e025a728 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/Consumer.java @@ -0,0 +1,9 @@ +package org.briarproject.bramble.api; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +public interface Consumer { + + void accept(T t); +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java b/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java index 1f818e59a..f65892be1 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java @@ -14,4 +14,6 @@ public interface FeatureFlags { boolean shouldEnableConnectViaBluetooth(); boolean shouldEnableShareAppViaOfflineHotspot(); + + boolean shouldEnableTransferData(); } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/client/BdfIncomingMessageHook.java b/bramble-api/src/main/java/org/briarproject/bramble/api/client/BdfIncomingMessageHook.java index e99aec8ba..8597c0117 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/client/BdfIncomingMessageHook.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/client/BdfIncomingMessageHook.java @@ -32,28 +32,31 @@ public abstract class BdfIncomingMessageHook implements IncomingMessageHook { /** * Called once for each incoming message that passes validation. + *

+ * If an unexpected exception occurs while handling data that is assumed + * to be valid (e.g. locally created metadata), it may be sensible to + * rethrow the unexpected exception as a DbException so that delivery is + * attempted again at next startup. This will allow delivery to succeed if + * the unexpected exception was caused by a bug that has subsequently been + * fixed. * * @param txn A read-write transaction - * @return Whether or not this message should be shared - * @throws DbException Should only be used for real database errors. - * If this is thrown, delivery will be attempted again at next startup, - * whereas if a FormatException is thrown, the message will be permanently - * invalidated. - * @throws FormatException Use this for any non-database error - * that occurs while handling remotely created data. - * This includes errors that occur while handling locally created data - * in a context controlled by remotely created data - * (for example, parsing the metadata of a dependency - * of an incoming message). - * Never rethrow DbException as FormatException! + * @throws DbException if a database error occurs while delivering the + * message. Delivery will be attempted again at next startup. Throwing + * this exception has the same effect as returning + * {@link DeliveryAction#DEFER}. + * @throws FormatException if the message is invalid in the context of its + * dependencies. The message and any dependents will be marked as invalid + * and deleted along with their metadata. Throwing this exception has the + * same effect as returning {@link DeliveryAction#REJECT}. */ - protected abstract boolean incomingMessage(Transaction txn, Message m, - BdfList body, BdfDictionary meta) throws DbException, - FormatException; + protected abstract DeliveryAction incomingMessage(Transaction txn, + Message m, BdfList body, BdfDictionary meta) + throws DbException, FormatException; @Override - public boolean incomingMessage(Transaction txn, Message m, Metadata meta) - throws DbException, InvalidMessageException { + public DeliveryAction incomingMessage(Transaction txn, Message m, + Metadata meta) throws DbException, InvalidMessageException { try { BdfList body = clientHelper.toList(m); BdfDictionary metaDictionary = metadataParser.parse(meta); diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/SecretKey.java b/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/SecretKey.java index 319f9305b..b88bc01da 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/SecretKey.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/SecretKey.java @@ -1,23 +1,19 @@ package org.briarproject.bramble.api.crypto; +import org.briarproject.bramble.api.Bytes; + /** * A secret key used for encryption and/or authentication. */ -public class SecretKey { +public class SecretKey extends Bytes { /** * The length of a secret key in bytes. */ public static final int LENGTH = 32; - private final byte[] key; - public SecretKey(byte[] key) { + super(key); if (key.length != LENGTH) throw new IllegalArgumentException(); - this.key = key; - } - - public byte[] getBytes() { - return key; } } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java index b5cf1d617..3413dc080 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java @@ -101,7 +101,7 @@ public interface DatabaseComponent extends TransactionManager { /** * Stores a transport. */ - void addTransport(Transaction txn, TransportId t, int maxLatency) + void addTransport(Transaction txn, TransportId t, long maxLatency) throws DbException; /** @@ -118,6 +118,18 @@ public interface DatabaseComponent extends TransactionManager { KeySetId addTransportKeys(Transaction txn, PendingContactId p, TransportKeys k) throws DbException; + /** + * Returns true if there are any acks or messages to send to the given + * contact over a transport with the given maximum latency. + *

+ * Read-only. + * + * @param eager True if messages that are not yet due for retransmission + * should be included + */ + boolean containsAnythingToSend(Transaction txn, ContactId c, + long maxLatency, boolean eager) throws DbException; + /** * Returns true if the database contains the given contact for the given * local pseudonym. @@ -150,6 +162,16 @@ public interface DatabaseComponent extends TransactionManager { boolean containsPendingContact(Transaction txn, PendingContactId p) throws DbException; + /** + * Returns true if the database contains keys for communicating with the + * given contact over the given transport. Handshake mode and rotation mode + * keys are included, whether activated or not. + *

+ * Read-only. + */ + boolean containsTransportKeys(Transaction txn, ContactId c, TransportId t) + throws DbException; + /** * Deletes the message with the given ID. Unlike * {@link #removeMessage(Transaction, MessageId)}, the message ID, @@ -178,7 +200,19 @@ public interface DatabaseComponent extends TransactionManager { */ @Nullable Collection generateBatch(Transaction txn, ContactId c, - int maxLength, int maxLatency) throws DbException; + int maxLength, long maxLatency) throws DbException; + + /** + * Returns a batch of messages for the given contact containing the + * messages with the given IDs, for transmission over a transport with + * the given maximum latency. + *

+ * If any of the given messages are not in the database or are not visible + * to the contact, they are omitted from the batch without throwing an + * exception. + */ + Collection generateBatch(Transaction txn, ContactId c, + Collection ids, long maxLatency) throws DbException; /** * Returns an offer for the given contact for transmission over a @@ -187,7 +221,7 @@ public interface DatabaseComponent extends TransactionManager { */ @Nullable Offer generateOffer(Transaction txn, ContactId c, int maxMessages, - int maxLatency) throws DbException; + long maxLatency) throws DbException; /** * Returns a request for the given contact, or null if there are no @@ -206,7 +240,7 @@ public interface DatabaseComponent extends TransactionManager { */ @Nullable Collection generateRequestedBatch(Transaction txn, ContactId c, - int maxLength, int maxLatency) throws DbException; + int maxLength, long maxLatency) throws DbException; /** * Returns the contact with the given ID. @@ -426,6 +460,27 @@ public interface DatabaseComponent extends TransactionManager { MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m) throws DbException; + /** + * Returns the IDs of all messages that are eligible to be sent to the + * given contact, together with their raw lengths. This may include + * messages that have already been sent and are not yet due for + * retransmission. + *

+ * Read-only. + */ + Map getUnackedMessagesToSend(Transaction txn, + ContactId c) throws DbException; + + /** + * Returns the total length, including headers, of all messages that are + * eligible to be sent to the given contact. This may include messages + * that have already been sent and are not yet due for retransmission. + *

+ * Read-only. + */ + long getUnackedMessageBytesToSend(Transaction txn, ContactId c) + throws DbException; + /** * Returns the next time (in milliseconds since the Unix epoch) when a * message is due to be deleted, or {@link #NO_CLEANUP_DEADLINE} @@ -483,6 +538,16 @@ public interface DatabaseComponent extends TransactionManager { Collection getTransportKeys(Transaction txn, TransportId t) throws DbException; + /** + * Returns the contact IDs and transport IDs for which the DB contains + * at least one set of transport keys. Handshake mode and rotation mode + * keys are included, whether activated or not. + *

+ * Read-only. + */ + Map> getTransportsWithKeys( + Transaction txn) throws DbException; + /** * Increments the outgoing stream counter for the given transport keys. */ diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java index ab14b669f..12bc36248 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java @@ -5,6 +5,7 @@ import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Wakeful; import java.util.concurrent.ExecutorService; @@ -22,6 +23,7 @@ public interface LifecycleManager { */ enum StartResult { ALREADY_RUNNING, + CLOCK_ERROR, DB_ERROR, DATA_TOO_OLD_ERROR, DATA_TOO_NEW_ERROR, @@ -65,6 +67,10 @@ public interface LifecycleManager { /** * Opens the {@link DatabaseComponent} using the given key and starts any * registered {@link Service Services}. + * + * @return {@link StartResult#CLOCK_ERROR} if the system clock is earlier + * than {@link Clock#MIN_REASONABLE_TIME_MS} or later than + * {@link Clock#MAX_REASONABLE_TIME_MS}. */ @Wakeful StartResult startServices(SecretKey dbKey); diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/Plugin.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/Plugin.java index 1318bcca4..0ace3778a 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/Plugin.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/Plugin.java @@ -61,7 +61,7 @@ public interface Plugin { /** * Returns the transport's maximum latency in milliseconds. */ - int getMaxLatency(); + long getMaxLatency(); /** * Returns the transport's maximum idle time in milliseconds. diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginFactory.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginFactory.java new file mode 100644 index 000000000..d0cf92e9b --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginFactory.java @@ -0,0 +1,25 @@ +package org.briarproject.bramble.api.plugin; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.Nullable; + +@NotNullByDefault +public interface PluginFactory

{ + + /** + * Returns the plugin's transport identifier. + */ + TransportId getId(); + + /** + * Returns the maximum latency of the transport in milliseconds. + */ + long getMaxLatency(); + + /** + * Creates and returns a plugin, or null if no plugin can be created. + */ + @Nullable + P createPlugin(PluginCallback callback); +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/TransportConnectionWriter.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/TransportConnectionWriter.java index 219f33efe..540e56acc 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/TransportConnectionWriter.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/TransportConnectionWriter.java @@ -15,13 +15,18 @@ public interface TransportConnectionWriter { /** * Returns the maximum latency of the transport in milliseconds. */ - int getMaxLatency(); + long getMaxLatency(); /** * Returns the maximum idle time of the transport in milliseconds. */ int getMaxIdleTime(); + /** + * Returns true if the transport is lossy and cheap. + */ + boolean isLossyAndCheap(); + /** * Returns an output stream for writing to the transport connection. */ diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/AbstractDuplexTransportConnection.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/AbstractDuplexTransportConnection.java index 27aad596a..157a03344 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/AbstractDuplexTransportConnection.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/AbstractDuplexTransportConnection.java @@ -70,7 +70,7 @@ public abstract class AbstractDuplexTransportConnection private class Writer implements TransportConnectionWriter { @Override - public int getMaxLatency() { + public long getMaxLatency() { return plugin.getMaxLatency(); } @@ -79,6 +79,11 @@ public abstract class AbstractDuplexTransportConnection return plugin.getMaxIdleTime(); } + @Override + public boolean isLossyAndCheap() { + return false; + } + @Override public OutputStream getOutputStream() throws IOException { return AbstractDuplexTransportConnection.this.getOutputStream(); diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/DuplexPluginFactory.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/DuplexPluginFactory.java index ef6560d54..991747494 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/DuplexPluginFactory.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/duplex/DuplexPluginFactory.java @@ -1,30 +1,11 @@ package org.briarproject.bramble.api.plugin.duplex; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.plugin.PluginCallback; -import org.briarproject.bramble.api.plugin.TransportId; - -import javax.annotation.Nullable; +import org.briarproject.bramble.api.plugin.PluginFactory; /** * Factory for creating a plugin for a duplex transport. */ @NotNullByDefault -public interface DuplexPluginFactory { - - /** - * Returns the plugin's transport identifier. - */ - TransportId getId(); - - /** - * Returns the maximum latency of the transport in milliseconds. - */ - int getMaxLatency(); - - /** - * Creates and returns a plugin, or null if no plugin can be created. - */ - @Nullable - DuplexPlugin createPlugin(PluginCallback callback); +public interface DuplexPluginFactory extends PluginFactory { } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/FileConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/file/FileConstants.java similarity index 56% rename from bramble-api/src/main/java/org/briarproject/bramble/api/plugin/FileConstants.java rename to bramble-api/src/main/java/org/briarproject/bramble/api/plugin/file/FileConstants.java index bed296874..1564f415d 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/FileConstants.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/file/FileConstants.java @@ -1,4 +1,4 @@ -package org.briarproject.bramble.api.plugin; +package org.briarproject.bramble.api.plugin.file; public interface FileConstants { diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/file/RemovableDriveConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/file/RemovableDriveConstants.java new file mode 100644 index 000000000..d414b88e0 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/file/RemovableDriveConstants.java @@ -0,0 +1,12 @@ +package org.briarproject.bramble.api.plugin.file; + +import org.briarproject.bramble.api.plugin.TransportId; + +public interface RemovableDriveConstants { + + TransportId ID = new TransportId("org.briarproject.bramble.drive"); + + String PROP_PATH = "path"; + String PROP_URI = "uri"; + String PROP_SUPPORTED = "supported"; +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/file/RemovableDriveManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/file/RemovableDriveManager.java new file mode 100644 index 000000000..2838f87cf --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/file/RemovableDriveManager.java @@ -0,0 +1,52 @@ +package org.briarproject.bramble.api.plugin.file; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.properties.TransportProperties; + +import javax.annotation.Nullable; + +@NotNullByDefault +public interface RemovableDriveManager { + + /** + * Returns the currently running reader task, or null if no reader task + * is running. + */ + @Nullable + RemovableDriveTask getCurrentReaderTask(); + + /** + * Returns the currently running writer task, or null if no writer task + * is running. + */ + @Nullable + RemovableDriveTask getCurrentWriterTask(); + + /** + * Starts and returns a reader task, reading from a stream described by + * the given transport properties. If a reader task is already running, + * it will be returned and the argument will be ignored. + */ + RemovableDriveTask startReaderTask(TransportProperties p); + + /** + * Starts and returns a writer task for the given contact, writing to + * a stream described by the given transport properties. If a writer task + * is already running, it will be returned and the arguments will be + * ignored. + */ + RemovableDriveTask startWriterTask(ContactId c, TransportProperties p); + + /** + * Returns true if the given contact has indicated support for the + * removable drive transport. + */ + boolean isTransportSupportedByContact(ContactId c) throws DbException; + + /** + * Returns true if there is anything to send to the given contact. + */ + boolean isWriterTaskNeeded(ContactId c) throws DbException; +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/file/RemovableDriveTask.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/file/RemovableDriveTask.java new file mode 100644 index 000000000..ad153d52d --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/file/RemovableDriveTask.java @@ -0,0 +1,65 @@ +package org.briarproject.bramble.api.plugin.file; + +import org.briarproject.bramble.api.Consumer; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.properties.TransportProperties; + +@NotNullByDefault +public interface RemovableDriveTask extends Runnable { + + /** + * Returns the {@link TransportProperties} that were used for creating + * this task. + */ + TransportProperties getTransportProperties(); + + /** + * Adds an observer to the task. The observer will be notified of state + * changes on the event thread. If the task has already finished, the + * observer will be notified of its final state. + */ + void addObserver(Consumer observer); + + /** + * Removes an observer from the task. + */ + void removeObserver(Consumer observer); + + class State { + + private final long done, total; + private final boolean finished, success; + + public State(long done, long total, boolean finished, boolean success) { + this.done = done; + this.total = total; + this.finished = finished; + this.success = success; + } + + /** + * Returns the total length in bytes of the messages read or written + * so far, or zero if the total is unknown. + */ + public long getDone() { + return done; + } + + /** + * Returns the total length in bytes of the messages that will have + * been read or written when the task is complete, or zero if the + * total is unknown. + */ + public long getTotal() { + return total; + } + + public boolean isFinished() { + return finished; + } + + public boolean isSuccess() { + return success; + } + } +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/simplex/SimplexPlugin.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/simplex/SimplexPlugin.java index 9df61968d..f7cf1e801 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/simplex/SimplexPlugin.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/simplex/SimplexPlugin.java @@ -15,6 +15,12 @@ import javax.annotation.Nullable; @NotNullByDefault public interface SimplexPlugin extends Plugin { + /** + * Returns true if the transport is likely to lose streams and the cost of + * transmitting redundant copies of data is cheap. + */ + boolean isLossyAndCheap(); + /** * Attempts to create and return a reader for the given transport * properties. Returns null if a reader cannot be created. diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/simplex/SimplexPluginFactory.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/simplex/SimplexPluginFactory.java index 4a96ee3f1..2b345d510 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/simplex/SimplexPluginFactory.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/simplex/SimplexPluginFactory.java @@ -1,30 +1,11 @@ package org.briarproject.bramble.api.plugin.simplex; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.plugin.PluginCallback; -import org.briarproject.bramble.api.plugin.TransportId; - -import javax.annotation.Nullable; +import org.briarproject.bramble.api.plugin.PluginFactory; /** * Factory for creating a plugin for a simplex transport. */ @NotNullByDefault -public interface SimplexPluginFactory { - - /** - * Returns the plugin's transport identifier. - */ - TransportId getId(); - - /** - * Returns the maximum latency of the transport in milliseconds. - */ - int getMaxLatency(); - - /** - * Creates and returns a plugin, or null if no plugin can be created. - */ - @Nullable - SimplexPlugin createPlugin(PluginCallback callback); +public interface SimplexPluginFactory extends PluginFactory { } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncConstants.java index 2e9241bed..b1b5e2f32 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncConstants.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncConstants.java @@ -5,6 +5,7 @@ import org.briarproject.bramble.api.UniqueId; import java.util.List; import static java.util.Collections.singletonList; +import static java.util.concurrent.TimeUnit.DAYS; import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES; public interface SyncConstants { @@ -55,4 +56,9 @@ public interface SyncConstants { * connections. */ int PRIORITY_NONCE_BYTES = 16; + + /** + * The maximum allowed latency for any transport, in milliseconds. + */ + long MAX_TRANSPORT_LATENCY = DAYS.toMillis(365); } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncSessionFactory.java b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncSessionFactory.java index a19e211fb..bb54f58e4 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncSessionFactory.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncSessionFactory.java @@ -16,9 +16,9 @@ public interface SyncSessionFactory { PriorityHandler handler); SyncSession createSimplexOutgoingSession(ContactId c, TransportId t, - int maxLatency, StreamWriter streamWriter); + long maxLatency, boolean eager, StreamWriter streamWriter); SyncSession createDuplexOutgoingSession(ContactId c, TransportId t, - int maxLatency, int maxIdleTime, StreamWriter streamWriter, + long maxLatency, int maxIdleTime, StreamWriter streamWriter, @Nullable Priority priority); } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/event/MessagesSentEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/event/MessagesSentEvent.java index 1f392417d..179907a3a 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/event/MessagesSentEvent.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/event/MessagesSentEvent.java @@ -18,11 +18,13 @@ public class MessagesSentEvent extends Event { private final ContactId contactId; private final Collection messageIds; + private final long totalLength; public MessagesSentEvent(ContactId contactId, - Collection messageIds) { + Collection messageIds, long totalLength) { this.contactId = contactId; this.messageIds = messageIds; + this.totalLength = totalLength; } public ContactId getContactId() { @@ -32,4 +34,8 @@ public class MessagesSentEvent extends Event { public Collection getMessageIds() { return messageIds; } + + public long getTotalLength() { + return totalLength; + } } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/validation/IncomingMessageHook.java b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/validation/IncomingMessageHook.java index 356f54248..5683f1710 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/validation/IncomingMessageHook.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/validation/IncomingMessageHook.java @@ -10,23 +10,54 @@ public interface IncomingMessageHook { /** * Called once for each incoming message that passes validation. + *

+ * If an unexpected exception occurs while handling data that is assumed + * to be valid (e.g. locally created metadata), it may be sensible to + * rethrow the unexpected exception as a DbException so that delivery is + * attempted again at next startup. This will allow delivery to succeed if + * the unexpected exception was caused by a bug that has subsequently been + * fixed. * * @param txn A read-write transaction - * @return Whether or not this message should be shared - * @throws DbException Should only be used for real database errors. - * If this is thrown, delivery will be attempted again at next startup, - * whereas if an InvalidMessageException is thrown, - * the message will be permanently invalidated. - * @throws InvalidMessageException for any non-database error - * that occurs while handling remotely created data. - * This includes errors that occur while handling locally created data - * in a context controlled by remotely created data - * (for example, parsing the metadata of a dependency - * of an incoming message). - * Throwing this will delete the incoming message and its metadata - * marking it as invalid in the database. - * Never rethrow DbException as InvalidMessageException! + * @throws DbException if a database error occurs while delivering the + * message. Delivery will be attempted again at next startup. Throwing + * this exception has the same effect as returning + * {@link DeliveryAction#DEFER}. + * @throws InvalidMessageException if the message is invalid in the context + * of its dependencies. The message and any dependents will be marked as + * invalid and deleted along with their metadata. Throwing this exception + * has the same effect as returning {@link DeliveryAction#REJECT}. */ - boolean incomingMessage(Transaction txn, Message m, Metadata meta) + DeliveryAction incomingMessage(Transaction txn, Message m, Metadata meta) throws DbException, InvalidMessageException; + + enum DeliveryAction { + + /** + * The message and any dependent messages will be moved to the + * {@link MessageState#INVALID INVALID state} and deleted, along with + * their metadata. + */ + REJECT, + + /** + * The message will be moved to the + * {@link MessageState#PENDING PENDING state}. Delivery will be + * attempted again at next startup. + */ + DEFER, + + /** + * The message will be moved to the + * {@link MessageState#DELIVERED DELIVERED state} and shared. + */ + ACCEPT_SHARE, + + /** + * The message will be moved to the + * {@link MessageState#DELIVERED DELIVERED state} and will not be + * shared. + */ + ACCEPT_DO_NOT_SHARE + } } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/validation/MessageState.java b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/validation/MessageState.java index 42a1adb85..770d57617 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/validation/MessageState.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/validation/MessageState.java @@ -1,8 +1,33 @@ package org.briarproject.bramble.api.sync.validation; +import org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction; + public enum MessageState { - UNKNOWN(0), INVALID(1), PENDING(2), DELIVERED(3); + /** + * A remote message that has not yet been validated. + */ + UNKNOWN(0), + + /** + * A remote message that has failed validation, has been + * {@link DeliveryAction#REJECT rejected} by the local sync client, or + * depends on another message that has failed validation or been rejected. + */ + INVALID(1), + + /** + * A remote message that has passed validation and is awaiting delivery to + * the local sync client. The message will not be delivered until all its + * dependencies have been validated and delivered. + */ + PENDING(2), + + /** + * A local message, or a remote message that has passed validation and + * been delivered to the local sync client. + */ + DELIVERED(3); private final int value; diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/system/Clock.java b/bramble-api/src/main/java/org/briarproject/bramble/api/system/Clock.java index 2b55c4196..1835aa2bc 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/system/Clock.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/system/Clock.java @@ -6,6 +6,22 @@ package org.briarproject.bramble.api.system; */ public interface Clock { + /** + * The minimum reasonable value for the system clock, in milliseconds + * since the Unix epoch. + *

+ * 1 Jan 2021, 00:00:00 UTC + */ + long MIN_REASONABLE_TIME_MS = 1_609_459_200_000L; + + /** + * The maximum reasonable value for the system clock, in milliseconds + * since the Unix epoch. + *

+ * 1 Jan 2121, 00:00:00 UTC + */ + long MAX_REASONABLE_TIME_MS = 4_765_132_800_000L; + /** * @see System#currentTimeMillis() */ diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/transport/KeyManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/transport/KeyManager.java index 50f7d8aa0..db906e649 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/transport/KeyManager.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/transport/KeyManager.java @@ -22,8 +22,24 @@ public interface KeyManager { /** * Derives and stores a set of rotation mode transport keys for - * communicating with the given contact over each transport and returns the - * key set IDs. + * communicating with the given contact over the given transport and + * returns the key set ID, or null if the transport is not supported. + *

+ * {@link StreamContext StreamContexts} for the contact can be created + * after this method has returned. + * + * @param alice True if the local party is Alice + * @param active Whether the derived keys can be used for outgoing streams + */ + @Nullable + KeySetId addRotationKeys(Transaction txn, ContactId c, TransportId t, + SecretKey rootKey, long timestamp, boolean alice, + boolean active) throws DbException; + + /** + * Derives and stores a set of rotation mode transport keys for + * communicating with the given contact over each supported transport and + * returns the key set IDs. *

* {@link StreamContext StreamContexts} for the contact can be created * after this method has returned. diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/transport/agreement/TransportKeyAgreementManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/transport/agreement/TransportKeyAgreementManager.java new file mode 100644 index 000000000..1b8fc6bc1 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/transport/agreement/TransportKeyAgreementManager.java @@ -0,0 +1,24 @@ +package org.briarproject.bramble.api.transport.agreement; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.ClientId; + +@NotNullByDefault +public interface TransportKeyAgreementManager { + + /** + * The unique ID of the transport key agreement client. + */ + ClientId CLIENT_ID = + new ClientId("org.briarproject.bramble.transport.agreement"); + + /** + * The current major version of the transport key agreement client. + */ + int MAJOR_VERSION = 0; + + /** + * The current minor version of the transport key agreement client. + */ + int MINOR_VERSION = 0; +} diff --git a/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java b/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java index e39e9aa62..3e171904e 100644 --- a/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java +++ b/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java @@ -163,10 +163,15 @@ public class TestUtils { public static Message getMessage(GroupId groupId) { int bodyLength = 1 + random.nextInt(MAX_MESSAGE_BODY_LENGTH); - return getMessage(groupId, bodyLength); + return getMessage(groupId, bodyLength, timestamp); } public static Message getMessage(GroupId groupId, int bodyLength) { + return getMessage(groupId, bodyLength, timestamp); + } + + public static Message getMessage(GroupId groupId, int bodyLength, + long timestamp) { MessageId id = new MessageId(getRandomId()); byte[] body = getRandomBytes(bodyLength); return new Message(id, groupId, timestamp, body); diff --git a/bramble-core/build.gradle b/bramble-core/build.gradle index 559a2e5bd..1cd54717a 100644 --- a/bramble-core/build.gradle +++ b/bramble-core/build.gradle @@ -21,6 +21,7 @@ dependencies { testImplementation project(path: ':bramble-api', configuration: 'testOutput') testImplementation 'org.hsqldb:hsqldb:2.3.5' // The last version that supports Java 1.6 + testImplementation 'net.jodah:concurrentunit:0.4.2' testImplementation "junit:junit:$junit_version" testImplementation "org.jmock:jmock:$jmock_version" testImplementation "org.jmock:jmock-junit4:$jmock_version" diff --git a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java index c256759ff..6732f8001 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java @@ -11,6 +11,7 @@ import org.briarproject.bramble.properties.PropertiesModule; import org.briarproject.bramble.rendezvous.RendezvousModule; import org.briarproject.bramble.sync.validation.ValidationModule; import org.briarproject.bramble.transport.TransportModule; +import org.briarproject.bramble.transport.agreement.TransportKeyAgreementModule; import org.briarproject.bramble.versioning.VersioningModule; public interface BrambleCoreEagerSingletons { @@ -33,6 +34,8 @@ public interface BrambleCoreEagerSingletons { void inject(RendezvousModule.EagerSingletons init); + void inject(TransportKeyAgreementModule.EagerSingletons init); + void inject(TransportModule.EagerSingletons init); void inject(ValidationModule.EagerSingletons init); @@ -51,6 +54,7 @@ public interface BrambleCoreEagerSingletons { c.inject(new RendezvousModule.EagerSingletons()); c.inject(new PluginModule.EagerSingletons()); c.inject(new PropertiesModule.EagerSingletons()); + c.inject(new TransportKeyAgreementModule.EagerSingletons()); c.inject(new TransportModule.EagerSingletons()); c.inject(new ValidationModule.EagerSingletons()); c.inject(new VersioningModule.EagerSingletons()); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java index 447bd5cb6..51069c526 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java @@ -23,6 +23,7 @@ import org.briarproject.bramble.settings.SettingsModule; import org.briarproject.bramble.sync.SyncModule; import org.briarproject.bramble.sync.validation.ValidationModule; import org.briarproject.bramble.transport.TransportModule; +import org.briarproject.bramble.transport.agreement.TransportKeyAgreementModule; import org.briarproject.bramble.versioning.VersioningModule; import dagger.Module; @@ -49,6 +50,7 @@ import dagger.Module; RendezvousModule.class, SettingsModule.class, SyncModule.class, + TransportKeyAgreementModule.class, TransportModule.class, ValidationModule.class, VersioningModule.class diff --git a/bramble-core/src/main/java/org/briarproject/bramble/connection/Connection.java b/bramble-core/src/main/java/org/briarproject/bramble/connection/Connection.java index 1efe2d480..9ca5c672e 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/connection/Connection.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/connection/Connection.java @@ -45,7 +45,6 @@ abstract class Connection { @Nullable StreamContext recogniseTag(TransportConnectionReader reader, TransportId transportId) { - StreamContext ctx; try { byte[] tag = readTag(reader.getInputStream()); return keyManager.getStreamContext(transportId, tag); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/connection/OutgoingSimplexSyncConnection.java b/bramble-core/src/main/java/org/briarproject/bramble/connection/OutgoingSimplexSyncConnection.java index a5ad6dfc8..9bec08193 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/connection/OutgoingSimplexSyncConnection.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/connection/OutgoingSimplexSyncConnection.java @@ -71,8 +71,10 @@ class OutgoingSimplexSyncConnection extends SyncConnection implements Runnable { StreamWriter streamWriter = streamWriterFactory.createStreamWriter( w.getOutputStream(), ctx); ContactId c = requireNonNull(ctx.getContactId()); + // Use eager retransmission if the transport is lossy and cheap return syncSessionFactory.createSimplexOutgoingSession(c, - ctx.getTransportId(), w.getMaxLatency(), streamWriter); + ctx.getTransportId(), w.getMaxLatency(), w.isLossyAndCheap(), + streamWriter); } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeManagerImpl.java index 9e3bd03a7..27870ca23 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeManagerImpl.java @@ -47,6 +47,7 @@ import javax.inject.Inject; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; +import static org.briarproject.bramble.api.system.Clock.MIN_REASONABLE_TIME_MS; import static org.briarproject.bramble.contact.ContactExchangeConstants.PROTOCOL_VERSION; import static org.briarproject.bramble.contact.ContactExchangeRecordTypes.CONTACT_INFO; import static org.briarproject.bramble.util.ValidationUtils.checkLength; @@ -184,6 +185,10 @@ class ContactExchangeManagerImpl implements ContactExchangeManager { // The agreed timestamp is the minimum of the peers' timestamps long timestamp = Math.min(localTimestamp, remoteInfo.timestamp); + if (timestamp < MIN_REASONABLE_TIME_MS) { + LOG.warning("Timestamp is too old"); + throw new FormatException(); + } // Add the contact Contact contact = addContact(p, remoteInfo.author, localAuthor, diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java index b6832d1e9..ab2376a4b 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java @@ -145,7 +145,7 @@ interface Database { /** * Stores a transport. */ - void addTransport(T txn, TransportId t, int maxLatency) + void addTransport(T txn, TransportId t, long maxLatency) throws DbException; /** @@ -162,6 +162,18 @@ interface Database { KeySetId addTransportKeys(T txn, PendingContactId p, TransportKeys k) throws DbException; + /** + * Returns true if there are any acks or messages to send to the given + * contact over a transport with the given maximum latency. + *

+ * Read-only. + * + * @param eager True if messages that are not yet due for retransmission + * should be included + */ + boolean containsAnythingToSend(T txn, ContactId c, long maxLatency, + boolean eager) throws DbException; + /** * Returns true if the database contains the given contact for the given * local pseudonym. @@ -215,6 +227,16 @@ interface Database { */ boolean containsTransport(T txn, TransportId t) throws DbException; + /** + * Returns true if the database contains keys for communicating with the + * given contact over the given transport. Handshake mode and rotation mode + * keys are included, whether activated or not. + *

+ * Read-only. + */ + boolean containsTransportKeys(T txn, ContactId c, TransportId t) + throws DbException; + /** * Returns true if the database contains the given message, the message is * shared, and the visibility of the message's group to the given contact @@ -461,7 +483,7 @@ interface Database { * Read-only. */ Collection getMessagesToOffer(T txn, ContactId c, - int maxMessages, int maxLatency) throws DbException; + int maxMessages, long maxLatency) throws DbException; /** * Returns the IDs of some messages that are eligible to be requested from @@ -476,10 +498,36 @@ interface Database { * Returns the IDs of some messages that are eligible to be sent to the * given contact, up to the given total length. *

+ * Unlike {@link #getUnackedMessagesToSend(Object, ContactId)} this method + * does not return messages that have already been sent unless they are + * due for retransmission. + *

* Read-only. */ Collection getMessagesToSend(T txn, ContactId c, int maxLength, - int maxLatency) throws DbException; + long maxLatency) throws DbException; + + /** + * Returns the IDs of all messages that are eligible to be sent to the + * given contact, together with their raw lengths. + *

+ * Unlike {@link #getMessagesToSend(Object, ContactId, int, long)} this + * method may return messages that have already been sent and are not yet + * due for retransmission. + *

+ * Read-only. + */ + Map getUnackedMessagesToSend(T txn, ContactId c) + throws DbException; + + /** + * Returns the total length, including headers, of all messages that are + * eligible to be sent to the given contact. This may include messages + * that have already been sent and are not yet due for retransmission. + *

+ * Read-only. + */ + long getUnackedMessageBytesToSend(T txn, ContactId c) throws DbException; /** * Returns the IDs of any messages that need to be validated. @@ -556,7 +604,7 @@ interface Database { * Read-only. */ Collection getRequestedMessagesToSend(T txn, ContactId c, - int maxLength, int maxLatency) throws DbException; + int maxLength, long maxLatency) throws DbException; /** * Returns all settings in the given namespace. @@ -580,6 +628,16 @@ interface Database { Collection getTransportKeys(T txn, TransportId t) throws DbException; + /** + * Returns the contact IDs and transport IDs for which the DB contains + * at least one set of transport keys. Handshake mode and rotation mode + * keys are included, whether activated or not. + *

+ * Read-only. + */ + Map> getTransportsWithKeys(T txn) + throws DbException; + /** * Increments the outgoing stream counter for the given transport keys. */ @@ -787,7 +845,7 @@ interface Database { * of the given message with respect to the given contact, using the latency * of the transport over which it was sent. */ - void updateExpiryTimeAndEta(T txn, ContactId c, MessageId m, int maxLatency) + void updateExpiryTimeAndEta(T txn, ContactId c, MessageId m, long maxLatency) throws DbException; /** diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java index 3bb052fe9..4cbf4b865 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java @@ -310,7 +310,7 @@ class DatabaseComponentImpl implements DatabaseComponent { @Override public void addTransport(Transaction transaction, TransportId t, - int maxLatency) throws DbException { + long maxLatency) throws DbException { if (transaction.isReadOnly()) throw new IllegalArgumentException(); T txn = unbox(transaction); if (!db.containsTransport(txn, t)) @@ -341,6 +341,15 @@ class DatabaseComponentImpl implements DatabaseComponent { return db.addTransportKeys(txn, p, k); } + @Override + public boolean containsAnythingToSend(Transaction transaction, ContactId c, + long maxLatency, boolean eager) throws DbException { + T txn = unbox(transaction); + if (!db.containsContact(txn, c)) + throw new NoSuchContactException(); + return db.containsAnythingToSend(txn, c, maxLatency, eager); + } + @Override public boolean containsContact(Transaction transaction, AuthorId remote, AuthorId local) throws DbException { @@ -371,6 +380,13 @@ class DatabaseComponentImpl implements DatabaseComponent { return db.containsPendingContact(txn, p); } + @Override + public boolean containsTransportKeys(Transaction transaction, ContactId c, + TransportId t) throws DbException { + T txn = unbox(transaction); + return db.containsTransportKeys(txn, c, t); + } + @Override public void deleteMessage(Transaction transaction, MessageId m) throws DbException { @@ -408,28 +424,57 @@ class DatabaseComponentImpl implements DatabaseComponent { @Nullable @Override public Collection generateBatch(Transaction transaction, - ContactId c, int maxLength, int maxLatency) throws DbException { + ContactId c, int maxLength, long maxLatency) throws DbException { if (transaction.isReadOnly()) throw new IllegalArgumentException(); T txn = unbox(transaction); if (!db.containsContact(txn, c)) throw new NoSuchContactException(); Collection ids = db.getMessagesToSend(txn, c, maxLength, maxLatency); + long totalLength = 0; List messages = new ArrayList<>(ids.size()); for (MessageId m : ids) { - messages.add(db.getMessage(txn, m)); + Message message = db.getMessage(txn, m); + totalLength += message.getRawLength(); + messages.add(message); db.updateExpiryTimeAndEta(txn, c, m, maxLatency); } if (ids.isEmpty()) return null; db.lowerRequestedFlag(txn, c, ids); - transaction.attach(new MessagesSentEvent(c, ids)); + transaction.attach(new MessagesSentEvent(c, ids, totalLength)); + return messages; + } + + @Override + public Collection generateBatch(Transaction transaction, + ContactId c, Collection ids, long maxLatency) + throws DbException { + if (transaction.isReadOnly()) throw new IllegalArgumentException(); + T txn = unbox(transaction); + if (!db.containsContact(txn, c)) + throw new NoSuchContactException(); + long totalLength = 0; + List messages = new ArrayList<>(ids.size()); + List sentIds = new ArrayList<>(ids.size()); + for (MessageId m : ids) { + if (db.containsVisibleMessage(txn, c, m)) { + Message message = db.getMessage(txn, m); + totalLength += message.getRawLength(); + messages.add(message); + sentIds.add(m); + db.updateExpiryTimeAndEta(txn, c, m, maxLatency); + } + } + if (messages.isEmpty()) return messages; + db.lowerRequestedFlag(txn, c, sentIds); + transaction.attach(new MessagesSentEvent(c, sentIds, totalLength)); return messages; } @Nullable @Override public Offer generateOffer(Transaction transaction, ContactId c, - int maxMessages, int maxLatency) throws DbException { + int maxMessages, long maxLatency) throws DbException { if (transaction.isReadOnly()) throw new IllegalArgumentException(); T txn = unbox(transaction); if (!db.containsContact(txn, c)) @@ -460,21 +505,24 @@ class DatabaseComponentImpl implements DatabaseComponent { @Nullable @Override public Collection generateRequestedBatch(Transaction transaction, - ContactId c, int maxLength, int maxLatency) throws DbException { + ContactId c, int maxLength, long maxLatency) throws DbException { if (transaction.isReadOnly()) throw new IllegalArgumentException(); T txn = unbox(transaction); if (!db.containsContact(txn, c)) throw new NoSuchContactException(); Collection ids = db.getRequestedMessagesToSend(txn, c, maxLength, maxLatency); + long totalLength = 0; List messages = new ArrayList<>(ids.size()); for (MessageId m : ids) { - messages.add(db.getMessage(txn, m)); + Message message = db.getMessage(txn, m); + totalLength += message.getRawLength(); + messages.add(message); db.updateExpiryTimeAndEta(txn, c, m, maxLatency); } if (ids.isEmpty()) return null; db.lowerRequestedFlag(txn, c, ids); - transaction.attach(new MessagesSentEvent(c, ids)); + transaction.attach(new MessagesSentEvent(c, ids, totalLength)); return messages; } @@ -692,6 +740,25 @@ class DatabaseComponentImpl implements DatabaseComponent { return status; } + @Override + public Map getUnackedMessagesToSend( + Transaction transaction, + ContactId c) throws DbException { + T txn = unbox(transaction); + if (!db.containsContact(txn, c)) + throw new NoSuchContactException(); + return db.getUnackedMessagesToSend(txn, c); + } + + @Override + public long getUnackedMessageBytesToSend(Transaction transaction, + ContactId c) throws DbException { + T txn = unbox(transaction); + if (!db.containsContact(txn, c)) + throw new NoSuchContactException(); + return db.getUnackedMessageBytesToSend(txn, c); + } + @Override public Map getMessageDependencies( Transaction transaction, MessageId m) throws DbException { @@ -765,6 +832,13 @@ class DatabaseComponentImpl implements DatabaseComponent { return db.getTransportKeys(txn, t); } + @Override + public Map> getTransportsWithKeys( + Transaction transaction) throws DbException { + T txn = unbox(transaction); + return db.getTransportsWithKeys(txn); + } + @Override public void incrementStreamCounter(Transaction transaction, TransportId t, KeySetId k) throws DbException { diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/ExponentialBackoff.java b/bramble-core/src/main/java/org/briarproject/bramble/db/ExponentialBackoff.java index 0e2f89bcf..c869da100 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/db/ExponentialBackoff.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/ExponentialBackoff.java @@ -1,5 +1,7 @@ package org.briarproject.bramble.db; +import static org.briarproject.bramble.api.sync.SyncConstants.MAX_TRANSPORT_LATENCY; + class ExponentialBackoff { /** @@ -11,9 +13,11 @@ class ExponentialBackoff { * transmissions increases exponentially. If the expiry time would * be greater than Long.MAX_VALUE, Long.MAX_VALUE is returned. */ - static long calculateExpiry(long now, int maxLatency, int txCount) { + static long calculateExpiry(long now, long maxLatency, int txCount) { if (now < 0) throw new IllegalArgumentException(); - if (maxLatency <= 0) throw new IllegalArgumentException(); + if (maxLatency <= 0 || maxLatency > MAX_TRANSPORT_LATENCY) { + throw new IllegalArgumentException(); + } if (txCount < 0) throw new IllegalArgumentException(); // The maximum round-trip time is twice the maximum latency long roundTrip = maxLatency * 2L; diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java index 72ceec594..1588e6a89 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java @@ -51,6 +51,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -101,7 +102,7 @@ import static org.briarproject.bramble.util.LogUtils.now; abstract class JdbcDatabase implements Database { // Package access for testing - static final int CODE_SCHEMA_VERSION = 48; + static final int CODE_SCHEMA_VERSION = 49; // Time period offsets for incoming transport keys private static final int OFFSET_PREV = -1; @@ -266,7 +267,7 @@ abstract class JdbcDatabase implements Database { private static final String CREATE_TRANSPORTS = "CREATE TABLE transports" + " (transportId _STRING NOT NULL," - + " maxLatency INT NOT NULL," + + " maxLatency BIGINT NOT NULL," + " PRIMARY KEY (transportId))"; private static final String CREATE_PENDING_CONTACTS = @@ -344,6 +345,11 @@ abstract class JdbcDatabase implements Database { "CREATE INDEX IF NOT EXISTS statusesByContactIdTimestamp" + " ON statuses (contactId, timestamp)"; + private static final String + INDEX_STATUSES_BY_CONTACT_ID_TX_COUNT_TIMESTAMP = + "CREATE INDEX IF NOT EXISTS statusesByContactIdTxCountTimestamp" + + " ON statuses (contactId, txCount, timestamp)"; + private static final String INDEX_MESSAGES_BY_CLEANUP_DEADLINE = "CREATE INDEX IF NOT EXISTS messagesByCleanupDeadline" + " ON messages (cleanupDeadline)"; @@ -492,7 +498,8 @@ abstract class JdbcDatabase implements Database { new Migration44_45(), new Migration45_46(), new Migration46_47(dbTypes), - new Migration47_48() + new Migration47_48(), + new Migration48_49() ); } @@ -570,6 +577,7 @@ abstract class JdbcDatabase implements Database { s.executeUpdate(INDEX_MESSAGE_DEPENDENCIES_BY_DEPENDENCY_ID); s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID); s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP); + s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TX_COUNT_TIMESTAMP); s.executeUpdate(INDEX_MESSAGES_BY_CLEANUP_DEADLINE); s.close(); } catch (SQLException e) { @@ -999,7 +1007,7 @@ abstract class JdbcDatabase implements Database { } @Override - public void addTransport(Connection txn, TransportId t, int maxLatency) + public void addTransport(Connection txn, TransportId t, long maxLatency) throws DbException { PreparedStatement ps = null; try { @@ -1120,6 +1128,55 @@ abstract class JdbcDatabase implements Database { } } + @Override + public boolean containsAnythingToSend(Connection txn, ContactId c, + long maxLatency, boolean eager) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT NULL FROM statuses" + + " WHERE contactId = ? AND ack = TRUE"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + rs = ps.executeQuery(); + boolean acksToSend = rs.next(); + rs.close(); + ps.close(); + if (acksToSend) return true; + if (eager) { + sql = "SELECT NULL from statuses" + + " WHERE contactId = ? AND state = ?" + + " AND groupShared = TRUE AND messageShared = TRUE" + + " AND deleted = FALSE AND seen = FALSE"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.setInt(2, DELIVERED.getValue()); + } else { + long now = clock.currentTimeMillis(); + long eta = now + maxLatency; + sql = "SELECT NULL FROM statuses" + + " WHERE contactId = ? AND state = ?" + + " AND groupShared = TRUE AND messageShared = TRUE" + + " AND deleted = FALSE AND seen = FALSE" + + " AND (expiry <= ? OR eta > ?)"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.setInt(2, DELIVERED.getValue()); + ps.setLong(3, now); + ps.setLong(4, eta); + } + rs = ps.executeQuery(); + boolean messagesToSend = rs.next(); + rs.close(); + ps.close(); + return messagesToSend; + } catch (SQLException e) { + tryToClose(rs, LOG, WARNING); + tryToClose(ps, LOG, WARNING); + throw new DbException(e); + } + } + @Override public boolean containsContact(Connection txn, AuthorId remote, AuthorId local) throws DbException { @@ -1277,6 +1334,29 @@ abstract class JdbcDatabase implements Database { } } + @Override + public boolean containsTransportKeys(Connection txn, ContactId c, + TransportId t) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT NULL FROM outgoingKeys" + + " WHERE contactId = ? AND transportId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.setString(2, t.getString()); + rs = ps.executeQuery(); + boolean found = rs.next(); + rs.close(); + ps.close(); + return found; + } catch (SQLException e) { + tryToClose(rs, LOG, WARNING); + tryToClose(ps, LOG, WARNING); + throw new DbException(e); + } + } + @Override public boolean containsVisibleMessage(Connection txn, ContactId c, MessageId m) throws DbException { @@ -2109,7 +2189,7 @@ abstract class JdbcDatabase implements Database { @Override public Collection getMessagesToOffer(Connection txn, - ContactId c, int maxMessages, int maxLatency) throws DbException { + ContactId c, int maxMessages, long maxLatency) throws DbException { long now = clock.currentTimeMillis(); long eta = now + maxLatency; PreparedStatement ps = null; @@ -2168,7 +2248,7 @@ abstract class JdbcDatabase implements Database { @Override public Collection getMessagesToSend(Connection txn, ContactId c, - int maxLength, int maxLatency) throws DbException { + int maxLength, long maxLatency) throws DbException { long now = clock.currentTimeMillis(); long eta = now + maxLatency; PreparedStatement ps = null; @@ -2205,6 +2285,63 @@ abstract class JdbcDatabase implements Database { } } + @Override + public Map getUnackedMessagesToSend(Connection txn, + ContactId c) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT length, messageId FROM statuses" + + " WHERE contactId = ? AND state = ?" + + " AND groupShared = TRUE AND messageShared = TRUE" + + " AND deleted = FALSE AND seen = FALSE" + + " ORDER BY txCount, timestamp"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.setInt(2, DELIVERED.getValue()); + rs = ps.executeQuery(); + Map results = new LinkedHashMap<>(); + while (rs.next()) { + int length = rs.getInt(1); + MessageId id = new MessageId(rs.getBytes(2)); + results.put(id, length); + } + rs.close(); + ps.close(); + return results; + } catch (SQLException e) { + tryToClose(rs, LOG, WARNING); + tryToClose(ps, LOG, WARNING); + throw new DbException(e); + } + } + + @Override + public long getUnackedMessageBytesToSend(Connection txn, ContactId c) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT SUM(length) FROM statuses" + + " WHERE contactId = ? AND state = ?" + + " AND groupShared = TRUE AND messageShared = TRUE" + + " AND deleted = FALSE AND seen = FALSE"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.setInt(2, DELIVERED.getValue()); + rs = ps.executeQuery(); + rs.next(); + long total = rs.getInt(1); + rs.close(); + ps.close(); + return total; + } catch (SQLException e) { + tryToClose(rs, LOG, WARNING); + tryToClose(ps, LOG, WARNING); + throw new DbException(e); + } + } + @Override public Collection getMessagesToValidate(Connection txn) throws DbException { @@ -2410,7 +2547,7 @@ abstract class JdbcDatabase implements Database { @Override public Collection getRequestedMessagesToSend(Connection txn, - ContactId c, int maxLength, int maxLatency) throws DbException { + ContactId c, int maxLength, long maxLatency) throws DbException { long now = clock.currentTimeMillis(); long eta = now + maxLatency; PreparedStatement ps = null; @@ -2574,6 +2711,38 @@ abstract class JdbcDatabase implements Database { } } + @Override + public Map> getTransportsWithKeys( + Connection txn) throws DbException { + Statement s = null; + ResultSet rs = null; + try { + String sql = "SELECT DISTINCT contactId, transportId" + + " FROM outgoingKeys"; + s = txn.createStatement(); + rs = s.executeQuery(sql); + Map> ids = new HashMap<>(); + while (rs.next()) { + ContactId c = new ContactId(rs.getInt(1)); + TransportId t = new TransportId(rs.getString(2)); + Collection transportIds = ids.get(c); + if (transportIds == null) { + transportIds = new ArrayList<>(); + ids.put(c, transportIds); + } + transportIds.add(t); + } + rs.close(); + s.close(); + return ids; + } catch (SQLException e) { + tryToClose(rs, LOG, WARNING); + tryToClose(s, LOG, WARNING); + tryToClose(s, LOG, WARNING); + throw new DbException(e); + } + } + @Override public void incrementStreamCounter(Connection txn, TransportId t, KeySetId k) throws DbException { @@ -3449,7 +3618,7 @@ abstract class JdbcDatabase implements Database { @Override public void updateExpiryTimeAndEta(Connection txn, ContactId c, MessageId m, - int maxLatency) throws DbException { + long maxLatency) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/Migration48_49.java b/bramble-core/src/main/java/org/briarproject/bramble/db/Migration48_49.java new file mode 100644 index 000000000..99dda0b97 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/Migration48_49.java @@ -0,0 +1,41 @@ +package org.briarproject.bramble.db; + +import org.briarproject.bramble.api.db.DbException; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.db.JdbcUtils.tryToClose; + +class Migration48_49 implements Migration { + + private static final Logger LOG = getLogger(Migration48_49.class.getName()); + + @Override + public int getStartVersion() { + return 48; + } + + @Override + public int getEndVersion() { + return 49; + } + + @Override + public void migrate(Connection txn) throws DbException { + Statement s = null; + try { + s = txn.createStatement(); + s.execute("ALTER TABLE transports" + + " ALTER COLUMN maxLatency" + + " SET DATA TYPE BIGINT"); + } catch (SQLException e) { + tryToClose(s, LOG, WARNING); + throw new DbException(e); + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java index fc1259d2a..992f05bdb 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java @@ -12,6 +12,7 @@ import org.briarproject.bramble.api.lifecycle.Service; import org.briarproject.bramble.api.lifecycle.ServiceException; import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.system.Clock; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -34,11 +35,14 @@ import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleS import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING; +import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.CLOCK_ERROR; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DATA_TOO_NEW_ERROR; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DATA_TOO_OLD_ERROR; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DB_ERROR; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SERVICE_ERROR; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SUCCESS; +import static org.briarproject.bramble.api.system.Clock.MAX_REASONABLE_TIME_MS; +import static org.briarproject.bramble.api.system.Clock.MIN_REASONABLE_TIME_MS; import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.now; @@ -52,6 +56,7 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener { private final DatabaseComponent db; private final EventBus eventBus; + private final Clock clock; private final List services; private final List openDatabaseHooks; private final List executors; @@ -63,9 +68,11 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener { private volatile LifecycleState state = STARTING; @Inject - LifecycleManagerImpl(DatabaseComponent db, EventBus eventBus) { + LifecycleManagerImpl(DatabaseComponent db, EventBus eventBus, + Clock clock) { this.db = db; this.eventBus = eventBus; + this.clock = clock; services = new CopyOnWriteArrayList<>(); openDatabaseHooks = new CopyOnWriteArrayList<>(); executors = new CopyOnWriteArrayList<>(); @@ -99,6 +106,13 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener { LOG.info("Already starting or stopping"); return ALREADY_RUNNING; } + long now = clock.currentTimeMillis(); + if (now < MIN_REASONABLE_TIME_MS || now > MAX_REASONABLE_TIME_MS) { + if (LOG.isLoggable(WARNING)) { + LOG.warning("System clock is unreasonable: " + now); + } + return CLOCK_ERROR; + } try { LOG.info("Opening database"); long start = now(); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java index bc0d42885..1af124561 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java @@ -79,7 +79,8 @@ abstract class AbstractBluetoothPlugin implements BluetoothPlugin, private final SecureRandom secureRandom; private final Backoff backoff; private final PluginCallback callback; - private final int maxLatency, maxIdleTime; + private final long maxLatency; + private final int maxIdleTime; private final AtomicBoolean used = new AtomicBoolean(false); private final AtomicBoolean everConnected = new AtomicBoolean(false); @@ -121,7 +122,7 @@ abstract class AbstractBluetoothPlugin implements BluetoothPlugin, SecureRandom secureRandom, Backoff backoff, PluginCallback callback, - int maxLatency, + long maxLatency, int maxIdleTime) { this.connectionLimiter = connectionLimiter; this.connectionFactory = connectionFactory; @@ -158,7 +159,7 @@ abstract class AbstractBluetoothPlugin implements BluetoothPlugin, } @Override - public int getMaxLatency() { + public long getMaxLatency() { return maxLatency; } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/AbstractRemovableDrivePlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/AbstractRemovableDrivePlugin.java new file mode 100644 index 000000000..ff4b9c048 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/AbstractRemovableDrivePlugin.java @@ -0,0 +1,126 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.Pair; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.ConnectionHandler; +import org.briarproject.bramble.api.plugin.PluginCallback; +import org.briarproject.bramble.api.plugin.TransportConnectionReader; +import org.briarproject.bramble.api.plugin.TransportConnectionWriter; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; +import org.briarproject.bramble.api.properties.TransportProperties; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collection; +import java.util.logging.Logger; + +import javax.annotation.concurrent.Immutable; + +import static java.util.Collections.singletonMap; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; +import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.ID; +import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.PROP_SUPPORTED; +import static org.briarproject.bramble.util.LogUtils.logException; + +@Immutable +@NotNullByDefault +abstract class AbstractRemovableDrivePlugin implements SimplexPlugin { + + private static final Logger LOG = + getLogger(AbstractRemovableDrivePlugin.class.getName()); + + private final long maxLatency; + private final PluginCallback callback; + + abstract InputStream openInputStream(TransportProperties p) + throws IOException; + + abstract OutputStream openOutputStream(TransportProperties p) + throws IOException; + + AbstractRemovableDrivePlugin(PluginCallback callback, long maxLatency) { + this.callback = callback; + this.maxLatency = maxLatency; + } + + @Override + public TransportId getId() { + return ID; + } + + @Override + public long getMaxLatency() { + return maxLatency; + } + + @Override + public int getMaxIdleTime() { + // Unused for simplex transports + throw new UnsupportedOperationException(); + } + + @Override + public void start() { + callback.mergeLocalProperties( + new TransportProperties(singletonMap(PROP_SUPPORTED, "true"))); + } + + @Override + public void stop() { + } + + @Override + public State getState() { + return ACTIVE; + } + + @Override + public int getReasonsDisabled() { + return 0; + } + + @Override + public boolean shouldPoll() { + return false; + } + + @Override + public int getPollingInterval() { + throw new UnsupportedOperationException(); + } + + @Override + public void poll( + Collection> properties) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLossyAndCheap() { + return true; + } + + @Override + public TransportConnectionReader createReader(TransportProperties p) { + try { + return new TransportInputStreamReader(openInputStream(p)); + } catch (IOException e) { + logException(LOG, WARNING, e); + return null; + } + } + + @Override + public TransportConnectionWriter createWriter(TransportProperties p) { + try { + return new TransportOutputStreamWriter(this, openOutputStream(p)); + } catch (IOException e) { + logException(LOG, WARNING, e); + return null; + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FilePlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FilePlugin.java index 8a2673a7a..69b368a55 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FilePlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FilePlugin.java @@ -15,8 +15,8 @@ import java.util.logging.Logger; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; -import static org.briarproject.bramble.api.plugin.FileConstants.PROP_PATH; import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; +import static org.briarproject.bramble.api.plugin.file.FileConstants.PROP_PATH; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; @@ -27,20 +27,20 @@ abstract class FilePlugin implements SimplexPlugin { getLogger(FilePlugin.class.getName()); protected final PluginCallback callback; - protected final int maxLatency; + protected final long maxLatency; protected abstract void writerFinished(File f, boolean exception); protected abstract void readerFinished(File f, boolean exception, boolean recognised); - FilePlugin(PluginCallback callback, int maxLatency) { + FilePlugin(PluginCallback callback, long maxLatency) { this.callback = callback; this.maxLatency = maxLatency; } @Override - public int getMaxLatency() { + public long getMaxLatency() { return maxLatency; } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FileTransportWriter.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FileTransportWriter.java index 2ab164784..3dc6fc428 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FileTransportWriter.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FileTransportWriter.java @@ -27,7 +27,7 @@ class FileTransportWriter implements TransportConnectionWriter { } @Override - public int getMaxLatency() { + public long getMaxLatency() { return plugin.getMaxLatency(); } @@ -36,6 +36,11 @@ class FileTransportWriter implements TransportConnectionWriter { return plugin.getMaxIdleTime(); } + @Override + public boolean isLossyAndCheap() { + return plugin.isLossyAndCheap(); + } + @Override public OutputStream getOutputStream() { return out; diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveManagerImpl.java new file mode 100644 index 000000000..386463dd7 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveManagerImpl.java @@ -0,0 +1,125 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.lifecycle.IoExecutor; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.file.RemovableDriveManager; +import org.briarproject.bramble.api.plugin.file.RemovableDriveTask; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.properties.TransportPropertyManager; +import org.briarproject.bramble.api.transport.KeyManager; + +import java.util.concurrent.Executor; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.ID; +import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.PROP_SUPPORTED; +import static org.briarproject.bramble.plugin.file.RemovableDrivePluginFactory.MAX_LATENCY; + +@ThreadSafe +@NotNullByDefault +class RemovableDriveManagerImpl + implements RemovableDriveManager, RemovableDriveTaskRegistry { + + private final Executor ioExecutor; + private final DatabaseComponent db; + private final KeyManager keyManager; + private final TransportPropertyManager transportPropertyManager; + private final RemovableDriveTaskFactory taskFactory; + private final Object lock = new Object(); + + @GuardedBy("lock") + @Nullable + private RemovableDriveTask reader = null; + @GuardedBy("lock") + @Nullable + private RemovableDriveTask writer = null; + + @Inject + RemovableDriveManagerImpl( + @IoExecutor Executor ioExecutor, + DatabaseComponent db, + KeyManager keyManager, + TransportPropertyManager transportPropertyManager, + RemovableDriveTaskFactory taskFactory) { + this.ioExecutor = ioExecutor; + this.db = db; + this.keyManager = keyManager; + this.transportPropertyManager = transportPropertyManager; + this.taskFactory = taskFactory; + } + + @Nullable + @Override + public RemovableDriveTask getCurrentReaderTask() { + synchronized (lock) { + return reader; + } + } + + @Nullable + @Override + public RemovableDriveTask getCurrentWriterTask() { + synchronized (lock) { + return writer; + } + } + + @Override + public RemovableDriveTask startReaderTask(TransportProperties p) { + RemovableDriveTask created; + synchronized (lock) { + if (reader != null) return reader; + reader = created = taskFactory.createReader(this, p); + } + ioExecutor.execute(created); + return created; + } + + @Override + public RemovableDriveTask startWriterTask(ContactId c, + TransportProperties p) { + RemovableDriveTask created; + synchronized (lock) { + if (writer != null) return writer; + writer = created = taskFactory.createWriter(this, c, p); + } + ioExecutor.execute(created); + return created; + } + + @Override + public boolean isTransportSupportedByContact(ContactId c) + throws DbException { + if (!keyManager.canSendOutgoingStreams(c, ID)) return false; + TransportProperties p = + transportPropertyManager.getRemoteProperties(c, ID); + return "true".equals(p.get(PROP_SUPPORTED)); + } + + @Override + public boolean isWriterTaskNeeded(ContactId c) throws DbException { + return db.transactionWithResult(true, txn -> + db.containsAnythingToSend(txn, c, MAX_LATENCY, true)); + } + + @Override + public void removeReader(RemovableDriveTask task) { + synchronized (lock) { + if (reader == task) reader = null; + } + } + + @Override + public void removeWriter(RemovableDriveTask task) { + synchronized (lock) { + if (writer == task) writer = null; + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveModule.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveModule.java new file mode 100644 index 000000000..3857cc338 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveModule.java @@ -0,0 +1,25 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.plugin.file.RemovableDriveManager; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class RemovableDriveModule { + + @Provides + @Singleton + RemovableDriveManager provideRemovableDriveManager( + RemovableDriveManagerImpl removableDriveManager) { + return removableDriveManager; + } + + @Provides + RemovableDriveTaskFactory provideTaskFactory( + RemovableDriveTaskFactoryImpl taskFactory) { + return taskFactory; + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDrivePlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDrivePlugin.java new file mode 100644 index 000000000..d3464d725 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDrivePlugin.java @@ -0,0 +1,39 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.PluginCallback; +import org.briarproject.bramble.api.properties.TransportProperties; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.PROP_PATH; +import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; + +@Immutable +@NotNullByDefault +class RemovableDrivePlugin extends AbstractRemovableDrivePlugin { + + RemovableDrivePlugin(PluginCallback callback, long maxLatency) { + super(callback, maxLatency); + } + + @Override + InputStream openInputStream(TransportProperties p) throws IOException { + String path = p.get(PROP_PATH); + if (isNullOrEmpty(path)) throw new IllegalArgumentException(); + return new FileInputStream(path); + } + + @Override + OutputStream openOutputStream(TransportProperties p) throws IOException { + String path = p.get(PROP_PATH); + if (isNullOrEmpty(path)) throw new IllegalArgumentException(); + return new FileOutputStream(path); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDrivePluginFactory.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDrivePluginFactory.java new file mode 100644 index 000000000..a05b50715 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDrivePluginFactory.java @@ -0,0 +1,41 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.PluginCallback; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; +import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static java.util.concurrent.TimeUnit.DAYS; +import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.ID; + +@Immutable +@NotNullByDefault +public class RemovableDrivePluginFactory implements SimplexPluginFactory { + + static final long MAX_LATENCY = DAYS.toMillis(28); + + @Inject + RemovableDrivePluginFactory() { + } + + @Override + public TransportId getId() { + return ID; + } + + @Override + public long getMaxLatency() { + return MAX_LATENCY; + } + + @Nullable + @Override + public SimplexPlugin createPlugin(PluginCallback callback) { + return new RemovableDrivePlugin(callback, MAX_LATENCY); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveReaderTask.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveReaderTask.java new file mode 100644 index 000000000..5dc4b6762 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveReaderTask.java @@ -0,0 +1,69 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.connection.ConnectionManager; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.PluginManager; +import org.briarproject.bramble.api.plugin.TransportConnectionReader; +import org.briarproject.bramble.api.properties.TransportProperties; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.ID; + +@NotNullByDefault +class RemovableDriveReaderTask extends RemovableDriveTaskImpl { + + private final static Logger LOG = + getLogger(RemovableDriveReaderTask.class.getName()); + + RemovableDriveReaderTask( + Executor eventExecutor, + PluginManager pluginManager, + ConnectionManager connectionManager, + EventBus eventBus, + RemovableDriveTaskRegistry registry, + TransportProperties transportProperties) { + super(eventExecutor, pluginManager, connectionManager, eventBus, + registry, transportProperties); + } + + @Override + public void run() { + TransportConnectionReader r = + getPlugin().createReader(transportProperties); + if (r == null) { + LOG.warning("Failed to create reader"); + registry.removeReader(this); + setSuccess(false); + return; + } + connectionManager.manageIncomingConnection(ID, new DecoratedReader(r)); + } + + private class DecoratedReader implements TransportConnectionReader { + + private final TransportConnectionReader delegate; + + private DecoratedReader(TransportConnectionReader delegate) { + this.delegate = delegate; + } + + @Override + public InputStream getInputStream() throws IOException { + return delegate.getInputStream(); + } + + @Override + public void dispose(boolean exception, boolean recognised) + throws IOException { + delegate.dispose(exception, recognised); + registry.removeReader(RemovableDriveReaderTask.this); + setSuccess(!exception && recognised); + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskFactory.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskFactory.java new file mode 100644 index 000000000..c6a126d81 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskFactory.java @@ -0,0 +1,16 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.file.RemovableDriveTask; +import org.briarproject.bramble.api.properties.TransportProperties; + +@NotNullByDefault +interface RemovableDriveTaskFactory { + + RemovableDriveTask createReader(RemovableDriveTaskRegistry registry, + TransportProperties p); + + RemovableDriveTask createWriter(RemovableDriveTaskRegistry registry, + ContactId c, TransportProperties p); +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskFactoryImpl.java new file mode 100644 index 000000000..596f0b11e --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskFactoryImpl.java @@ -0,0 +1,55 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.connection.ConnectionManager; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventExecutor; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.PluginManager; +import org.briarproject.bramble.api.plugin.file.RemovableDriveTask; +import org.briarproject.bramble.api.properties.TransportProperties; + +import java.util.concurrent.Executor; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +@Immutable +@NotNullByDefault +class RemovableDriveTaskFactoryImpl implements RemovableDriveTaskFactory { + + private final DatabaseComponent db; + private final Executor eventExecutor; + private final PluginManager pluginManager; + private final ConnectionManager connectionManager; + private final EventBus eventBus; + + @Inject + RemovableDriveTaskFactoryImpl( + DatabaseComponent db, + @EventExecutor Executor eventExecutor, + PluginManager pluginManager, + ConnectionManager connectionManager, + EventBus eventBus) { + this.db = db; + this.eventExecutor = eventExecutor; + this.pluginManager = pluginManager; + this.connectionManager = connectionManager; + this.eventBus = eventBus; + } + + @Override + public RemovableDriveTask createReader(RemovableDriveTaskRegistry registry, + TransportProperties p) { + return new RemovableDriveReaderTask(eventExecutor, pluginManager, + connectionManager, eventBus, registry, p); + } + + @Override + public RemovableDriveTask createWriter(RemovableDriveTaskRegistry registry, + ContactId c, TransportProperties p) { + return new RemovableDriveWriterTask(db, eventExecutor, pluginManager, + connectionManager, eventBus, registry, c, p); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskImpl.java new file mode 100644 index 000000000..c17cc48ac --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskImpl.java @@ -0,0 +1,114 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.Consumer; +import org.briarproject.bramble.api.connection.ConnectionManager; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.PluginManager; +import org.briarproject.bramble.api.plugin.file.RemovableDriveTask; +import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; +import org.briarproject.bramble.api.properties.TransportProperties; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +import static java.lang.Math.min; +import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; +import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.ID; + +@ThreadSafe +@NotNullByDefault +abstract class RemovableDriveTaskImpl implements RemovableDriveTask { + + private final Executor eventExecutor; + private final PluginManager pluginManager; + final ConnectionManager connectionManager; + final EventBus eventBus; + final RemovableDriveTaskRegistry registry; + final TransportProperties transportProperties; + + private final Object lock = new Object(); + @GuardedBy("lock") + private final List> observers = new ArrayList<>(); + @GuardedBy("lock") + private State state = new State(0, 0, false, false); + + RemovableDriveTaskImpl( + Executor eventExecutor, + PluginManager pluginManager, + ConnectionManager connectionManager, + EventBus eventBus, + RemovableDriveTaskRegistry registry, + TransportProperties transportProperties) { + this.eventExecutor = eventExecutor; + this.pluginManager = pluginManager; + this.connectionManager = connectionManager; + this.eventBus = eventBus; + this.registry = registry; + this.transportProperties = transportProperties; + } + + @Override + public TransportProperties getTransportProperties() { + return transportProperties; + } + + @Override + public void addObserver(Consumer o) { + State state; + synchronized (lock) { + observers.add(o); + state = this.state; + eventExecutor.execute(() -> o.accept(state)); + } + } + + @Override + public void removeObserver(Consumer o) { + synchronized (lock) { + observers.remove(o); + } + } + + SimplexPlugin getPlugin() { + return (SimplexPlugin) requireNonNull(pluginManager.getPlugin(ID)); + } + + void setTotal(long total) { + synchronized (lock) { + state = new State(state.getDone(), total, state.isFinished(), + state.isSuccess()); + notifyObservers(); + } + } + + void addDone(long done) { + synchronized (lock) { + // Done and total come from different sources; make them consistent + done = min(state.getDone() + done, state.getTotal()); + state = new State(done, state.getTotal(), state.isFinished(), + state.isSuccess()); + notifyObservers(); + } + } + + void setSuccess(boolean success) { + synchronized (lock) { + state = new State(state.getDone(), state.getTotal(), true, success); + notifyObservers(); + } + } + + @GuardedBy("lock") + private void notifyObservers() { + List> observers = new ArrayList<>(this.observers); + State state = this.state; + eventExecutor.execute(() -> { + for (Consumer o : observers) o.accept(state); + }); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskRegistry.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskRegistry.java new file mode 100644 index 000000000..1f073bbf2 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskRegistry.java @@ -0,0 +1,12 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.file.RemovableDriveTask; + +@NotNullByDefault +interface RemovableDriveTaskRegistry { + + void removeReader(RemovableDriveTask task); + + void removeWriter(RemovableDriveTask task); +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveWriterTask.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveWriterTask.java new file mode 100644 index 000000000..e78fae50e --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveWriterTask.java @@ -0,0 +1,126 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.connection.ConnectionManager; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventListener; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.PluginManager; +import org.briarproject.bramble.api.plugin.TransportConnectionWriter; +import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.sync.event.MessagesSentEvent; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.ID; +import static org.briarproject.bramble.util.LogUtils.logException; + +@NotNullByDefault +class RemovableDriveWriterTask extends RemovableDriveTaskImpl + implements EventListener { + + private static final Logger LOG = + getLogger(RemovableDriveWriterTask.class.getName()); + + private final DatabaseComponent db; + private final ContactId contactId; + + RemovableDriveWriterTask( + DatabaseComponent db, + Executor eventExecutor, + PluginManager pluginManager, + ConnectionManager connectionManager, + EventBus eventBus, + RemovableDriveTaskRegistry registry, + ContactId contactId, + TransportProperties transportProperties) { + super(eventExecutor, pluginManager, connectionManager, eventBus, + registry, transportProperties); + this.db = db; + this.contactId = contactId; + } + + @Override + public void run() { + SimplexPlugin plugin = getPlugin(); + TransportConnectionWriter w = plugin.createWriter(transportProperties); + if (w == null) { + LOG.warning("Failed to create writer"); + registry.removeWriter(this); + setSuccess(false); + return; + } + try { + setTotal(db.transactionWithResult(true, txn -> + db.getUnackedMessageBytesToSend(txn, contactId))); + } catch (DbException e) { + logException(LOG, WARNING, e); + registry.removeWriter(this); + setSuccess(false); + return; + } + eventBus.addListener(this); + connectionManager.manageOutgoingConnection(contactId, ID, + new DecoratedWriter(w)); + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof MessagesSentEvent) { + MessagesSentEvent m = (MessagesSentEvent) e; + if (contactId.equals(m.getContactId())) { + if (LOG.isLoggable(INFO)) { + LOG.info(m.getMessageIds().size() + " messages sent"); + } + addDone(m.getTotalLength()); + } + } + } + + private class DecoratedWriter implements TransportConnectionWriter { + + private final TransportConnectionWriter delegate; + + private DecoratedWriter(TransportConnectionWriter delegate) { + this.delegate = delegate; + } + + @Override + public long getMaxLatency() { + return delegate.getMaxLatency(); + } + + @Override + public int getMaxIdleTime() { + return delegate.getMaxIdleTime(); + } + + @Override + public boolean isLossyAndCheap() { + return delegate.isLossyAndCheap(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return delegate.getOutputStream(); + } + + @Override + public void dispose(boolean exception) throws IOException { + delegate.dispose(exception); + registry.removeWriter(RemovableDriveWriterTask.this); + eventBus.removeListener(RemovableDriveWriterTask.this); + setSuccess(!exception); + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/TransportInputStreamReader.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/TransportInputStreamReader.java new file mode 100644 index 000000000..8971136c9 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/TransportInputStreamReader.java @@ -0,0 +1,34 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportConnectionReader; + +import java.io.InputStream; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.IoUtils.tryToClose; + +@NotNullByDefault +class TransportInputStreamReader implements TransportConnectionReader { + + private static final Logger LOG = + getLogger(TransportInputStreamReader.class.getName()); + + private final InputStream in; + + TransportInputStreamReader(InputStream in) { + this.in = in; + } + + @Override + public InputStream getInputStream() { + return in; + } + + @Override + public void dispose(boolean exception, boolean recognised) { + tryToClose(in, LOG, WARNING); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/TransportOutputStreamWriter.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/TransportOutputStreamWriter.java new file mode 100644 index 000000000..ec2f6240a --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/TransportOutputStreamWriter.java @@ -0,0 +1,52 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportConnectionWriter; +import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; + +import java.io.OutputStream; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.IoUtils.tryToClose; + +@NotNullByDefault +class TransportOutputStreamWriter implements TransportConnectionWriter { + + private static final Logger LOG = + getLogger(TransportOutputStreamWriter.class.getName()); + + private final SimplexPlugin plugin; + private final OutputStream out; + + TransportOutputStreamWriter(SimplexPlugin plugin, OutputStream out) { + this.plugin = plugin; + this.out = out; + } + + @Override + public long getMaxLatency() { + return plugin.getMaxLatency(); + } + + @Override + public int getMaxIdleTime() { + return plugin.getMaxIdleTime(); + } + + @Override + public boolean isLossyAndCheap() { + return plugin.isLossyAndCheap(); + } + + @Override + public OutputStream getOutputStream() { + return out; + } + + @Override + public void dispose(boolean exception) { + tryToClose(out, LOG, WARNING); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java index 87706b7e5..f0860e0ad 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java @@ -92,7 +92,7 @@ class LanTcpPlugin extends TcpPlugin { Executor wakefulIoExecutor, Backoff backoff, PluginCallback callback, - int maxLatency, + long maxLatency, int maxIdleTime, int connectionTimeout) { super(ioExecutor, wakefulIoExecutor, backoff, callback, maxLatency, diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginFactory.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginFactory.java index d7f62737f..80f4fa1d0 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginFactory.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginFactory.java @@ -50,7 +50,7 @@ public class LanTcpPluginFactory implements DuplexPluginFactory { } @Override - public int getMaxLatency() { + public long getMaxLatency() { return MAX_LATENCY; } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java index f67121685..5b1acb376 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java @@ -69,8 +69,8 @@ abstract class TcpPlugin implements DuplexPlugin, EventListener { protected final Executor ioExecutor, wakefulIoExecutor, bindExecutor; protected final Backoff backoff; protected final PluginCallback callback; - protected final int maxLatency, maxIdleTime; - protected final int connectionTimeout, socketTimeout; + protected final long maxLatency; + protected final int maxIdleTime, connectionTimeout, socketTimeout; protected final AtomicBoolean used = new AtomicBoolean(false); protected final PluginState state = new PluginState(); @@ -111,7 +111,7 @@ abstract class TcpPlugin implements DuplexPlugin, EventListener { Executor wakefulIoExecutor, Backoff backoff, PluginCallback callback, - int maxLatency, + long maxLatency, int maxIdleTime, int connectionTimeout) { this.ioExecutor = ioExecutor; @@ -129,7 +129,7 @@ abstract class TcpPlugin implements DuplexPlugin, EventListener { } @Override - public int getMaxLatency() { + public long getMaxLatency() { return maxLatency; } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPlugin.java index 64bc90517..4dd061724 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPlugin.java @@ -35,7 +35,7 @@ class WanTcpPlugin extends TcpPlugin { Backoff backoff, PortMapper portMapper, PluginCallback callback, - int maxLatency, + long maxLatency, int maxIdleTime, int connectionTimeout) { super(ioExecutor, wakefulIoExecutor, backoff, callback, maxLatency, diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPluginFactory.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPluginFactory.java index f8aa5f085..9468d6823 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPluginFactory.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPluginFactory.java @@ -54,7 +54,7 @@ public class WanTcpPluginFactory implements DuplexPluginFactory { } @Override - public int getMaxLatency() { + public long getMaxLatency() { return MAX_LATENCY; } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java index 17bb73c79..4714489d9 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java @@ -131,7 +131,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { private final String architecture; private final CircumventionProvider circumventionProvider; private final ResourceProvider resourceProvider; - private final int maxLatency, maxIdleTime, socketTimeout; + private final long maxLatency; + private final int maxIdleTime, socketTimeout; private final File torDirectory, geoIpFile, configFile; private final File doneFile, cookieFile; private final AtomicBoolean used = new AtomicBoolean(false); @@ -159,7 +160,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback, String architecture, - int maxLatency, + long maxLatency, int maxIdleTime, File torDirectory) { this.ioExecutor = ioExecutor; @@ -204,7 +205,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } @Override - public int getMaxLatency() { + public long getMaxLatency() { return maxLatency; } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java index d5f2d4212..cabf9f95d 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/properties/TransportPropertyManagerImpl.java @@ -44,6 +44,7 @@ import static org.briarproject.bramble.api.properties.TransportPropertyConstants import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_TRANSPORT_ID; import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_VERSION; import static org.briarproject.bramble.api.properties.TransportPropertyConstants.REFLECTED_PROPERTY_PREFIX; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; @Immutable @@ -115,8 +116,8 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, } @Override - public boolean incomingMessage(Transaction txn, Message m, Metadata meta) - throws DbException, InvalidMessageException { + public DeliveryAction incomingMessage(Transaction txn, Message m, + Metadata meta) throws DbException, InvalidMessageException { try { // Find the latest update for this transport, if any BdfDictionary d = metadataParser.parse(meta); @@ -131,14 +132,14 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, // We've already received a newer update - delete this one db.deleteMessage(txn, m.getId()); db.deleteMessageMetadata(txn, m.getId()); - return false; + return ACCEPT_DO_NOT_SHARE; } } txn.attach(new RemoteTransportPropertiesUpdatedEvent(t)); } catch (FormatException e) { throw new InvalidMessageException(e); } - return false; + return ACCEPT_DO_NOT_SHARE; } @Override diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java index 6eed42511..92e287203 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java @@ -77,7 +77,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener { private final Clock clock; private final ContactId contactId; private final TransportId transportId; - private final int maxLatency, maxIdleTime; + private final long maxLatency, maxIdleTime; private final StreamWriter streamWriter; private final SyncRecordWriter recordWriter; @Nullable @@ -95,7 +95,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener { DuplexOutgoingSession(DatabaseComponent db, Executor dbExecutor, EventBus eventBus, Clock clock, ContactId contactId, - TransportId transportId, int maxLatency, int maxIdleTime, + TransportId transportId, long maxLatency, int maxIdleTime, StreamWriter streamWriter, SyncRecordWriter recordWriter, @Nullable Priority priority) { this.db = db; diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/SimplexOutgoingSession.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/SimplexOutgoingSession.java index 1d32ca4ee..90c6be0b4 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/sync/SimplexOutgoingSession.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/SimplexOutgoingSession.java @@ -15,6 +15,7 @@ import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent; import org.briarproject.bramble.api.sync.Ack; import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.SyncRecordWriter; import org.briarproject.bramble.api.sync.SyncSession; import org.briarproject.bramble.api.sync.Versions; @@ -22,7 +23,11 @@ import org.briarproject.bramble.api.sync.event.CloseSyncConnectionsEvent; import org.briarproject.bramble.api.transport.StreamWriter; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; @@ -60,7 +65,8 @@ class SimplexOutgoingSession implements SyncSession, EventListener { private final EventBus eventBus; private final ContactId contactId; private final TransportId transportId; - private final int maxLatency; + private final long maxLatency; + private final boolean eager; private final StreamWriter streamWriter; private final SyncRecordWriter recordWriter; private final AtomicInteger outstandingQueries; @@ -70,7 +76,7 @@ class SimplexOutgoingSession implements SyncSession, EventListener { SimplexOutgoingSession(DatabaseComponent db, Executor dbExecutor, EventBus eventBus, ContactId contactId, TransportId transportId, - int maxLatency, StreamWriter streamWriter, + long maxLatency, boolean eager, StreamWriter streamWriter, SyncRecordWriter recordWriter) { this.db = db; this.dbExecutor = dbExecutor; @@ -78,6 +84,7 @@ class SimplexOutgoingSession implements SyncSession, EventListener { this.contactId = contactId; this.transportId = transportId; this.maxLatency = maxLatency; + this.eager = eager; this.streamWriter = streamWriter; this.recordWriter = recordWriter; outstandingQueries = new AtomicInteger(2); // One per type of record @@ -92,8 +99,9 @@ class SimplexOutgoingSession implements SyncSession, EventListener { // Send our supported protocol versions recordWriter.writeVersions(new Versions(SUPPORTED_VERSIONS)); // Start a query for each type of record - dbExecutor.execute(new GenerateAck()); - dbExecutor.execute(new GenerateBatch()); + dbExecutor.execute(this::generateAck); + if (eager) dbExecutor.execute(this::loadUnackedMessageIds); + else dbExecutor.execute(this::generateBatch); // Write records until interrupted or no more records to write try { while (!interrupted) { @@ -138,81 +146,110 @@ class SimplexOutgoingSession implements SyncSession, EventListener { } } - private class GenerateAck implements Runnable { - - @DatabaseExecutor - @Override - public void run() { - if (interrupted) return; - try { - Ack a = db.transactionWithNullableResult(false, txn -> - db.generateAck(txn, contactId, MAX_MESSAGE_IDS)); - if (LOG.isLoggable(INFO)) - LOG.info("Generated ack: " + (a != null)); - if (a == null) decrementOutstandingQueries(); - else writerTasks.add(new WriteAck(a)); - } catch (DbException e) { - logException(LOG, WARNING, e); - interrupt(); + @DatabaseExecutor + private void loadUnackedMessageIds() { + if (interrupted) return; + try { + Map ids = db.transactionWithResult(true, txn -> + db.getUnackedMessagesToSend(txn, contactId)); + if (LOG.isLoggable(INFO)) { + LOG.info(ids.size() + " unacked messages to send"); } + if (ids.isEmpty()) decrementOutstandingQueries(); + else dbExecutor.execute(() -> generateEagerBatch(ids)); + } catch (DbException e) { + logException(LOG, WARNING, e); + interrupt(); } } - private class WriteAck implements ThrowingRunnable { - - private final Ack ack; - - private WriteAck(Ack ack) { - this.ack = ack; + @DatabaseExecutor + private void generateEagerBatch(Map ids) { + if (interrupted) return; + // Take some message IDs from `ids` to form a batch + Collection batchIds = new ArrayList<>(); + long totalLength = 0; + Iterator> it = ids.entrySet().iterator(); + while (it.hasNext()) { + // Check whether the next message will fit in the batch + Entry e = it.next(); + int length = e.getValue(); + if (totalLength + length > MAX_RECORD_PAYLOAD_BYTES) break; + // Add the message to the batch + it.remove(); + batchIds.add(e.getKey()); + totalLength += length; } - - @IoExecutor - @Override - public void run() throws IOException { - if (interrupted) return; - recordWriter.writeAck(ack); - LOG.info("Sent ack"); - dbExecutor.execute(new GenerateAck()); + if (batchIds.isEmpty()) throw new AssertionError(); + try { + Collection batch = + db.transactionWithResult(false, txn -> + db.generateBatch(txn, contactId, batchIds, + maxLatency)); + writerTasks.add(() -> writeEagerBatch(batch, ids)); + } catch (DbException e) { + logException(LOG, WARNING, e); + interrupt(); } } - private class GenerateBatch implements Runnable { + @IoExecutor + private void writeEagerBatch(Collection batch, + Map ids) throws IOException { + if (interrupted) return; + for (Message m : batch) recordWriter.writeMessage(m); + LOG.info("Sent eager batch"); + if (ids.isEmpty()) decrementOutstandingQueries(); + else dbExecutor.execute(() -> generateEagerBatch(ids)); + } - @DatabaseExecutor - @Override - public void run() { - if (interrupted) return; - try { - Collection b = - db.transactionWithNullableResult(false, txn -> - db.generateBatch(txn, contactId, - MAX_RECORD_PAYLOAD_BYTES, maxLatency)); - if (LOG.isLoggable(INFO)) - LOG.info("Generated batch: " + (b != null)); - if (b == null) decrementOutstandingQueries(); - else writerTasks.add(new WriteBatch(b)); - } catch (DbException e) { - logException(LOG, WARNING, e); - interrupt(); - } + @DatabaseExecutor + private void generateAck() { + if (interrupted) return; + try { + Ack a = db.transactionWithNullableResult(false, txn -> + db.generateAck(txn, contactId, MAX_MESSAGE_IDS)); + if (LOG.isLoggable(INFO)) + LOG.info("Generated ack: " + (a != null)); + if (a == null) decrementOutstandingQueries(); + else writerTasks.add(() -> writeAck(a)); + } catch (DbException e) { + logException(LOG, WARNING, e); + interrupt(); } } - private class WriteBatch implements ThrowingRunnable { + @IoExecutor + private void writeAck(Ack ack) throws IOException { + if (interrupted) return; + recordWriter.writeAck(ack); + LOG.info("Sent ack"); + dbExecutor.execute(this::generateAck); + } - private final Collection batch; - - private WriteBatch(Collection batch) { - this.batch = batch; + @DatabaseExecutor + private void generateBatch() { + if (interrupted) return; + try { + Collection b = + db.transactionWithNullableResult(false, txn -> + db.generateBatch(txn, contactId, + MAX_RECORD_PAYLOAD_BYTES, maxLatency)); + if (LOG.isLoggable(INFO)) + LOG.info("Generated batch: " + (b != null)); + if (b == null) decrementOutstandingQueries(); + else writerTasks.add(() -> writeBatch(b)); + } catch (DbException e) { + logException(LOG, WARNING, e); + interrupt(); } + } - @IoExecutor - @Override - public void run() throws IOException { - if (interrupted) return; - for (Message m : batch) recordWriter.writeMessage(m); - LOG.info("Sent batch"); - dbExecutor.execute(new GenerateBatch()); - } + @IoExecutor + private void writeBatch(Collection batch) throws IOException { + if (interrupted) return; + for (Message m : batch) recordWriter.writeMessage(m); + LOG.info("Sent batch"); + dbExecutor.execute(this::generateBatch); } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncSessionFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncSessionFactoryImpl.java index 4c590df73..70fd0df8b 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncSessionFactoryImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncSessionFactoryImpl.java @@ -60,17 +60,17 @@ class SyncSessionFactoryImpl implements SyncSessionFactory { @Override public SyncSession createSimplexOutgoingSession(ContactId c, TransportId t, - int maxLatency, StreamWriter streamWriter) { + long maxLatency, boolean eager, StreamWriter streamWriter) { OutputStream out = streamWriter.getOutputStream(); SyncRecordWriter recordWriter = recordWriterFactory.createRecordWriter(out); return new SimplexOutgoingSession(db, dbExecutor, eventBus, c, t, - maxLatency, streamWriter, recordWriter); + maxLatency, eager, streamWriter, recordWriter); } @Override public SyncSession createDuplexOutgoingSession(ContactId c, TransportId t, - int maxLatency, int maxIdleTime, StreamWriter streamWriter, + long maxLatency, int maxIdleTime, StreamWriter streamWriter, @Nullable Priority priority) { OutputStream out = streamWriter.getOutputStream(); SyncRecordWriter recordWriter = diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/validation/ValidationManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/validation/ValidationManagerImpl.java index d0d583195..cec81615d 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/sync/validation/ValidationManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/validation/ValidationManagerImpl.java @@ -20,6 +20,7 @@ import org.briarproject.bramble.api.sync.MessageContext; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.event.MessageAddedEvent; import org.briarproject.bramble.api.sync.validation.IncomingMessageHook; +import org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction; import org.briarproject.bramble.api.sync.validation.MessageState; import org.briarproject.bramble.api.sync.validation.MessageValidator; import org.briarproject.bramble.api.sync.validation.ValidationManager; @@ -40,6 +41,10 @@ import javax.inject.Inject; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_SHARE; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.DEFER; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.REJECT; import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED; import static org.briarproject.bramble.api.sync.validation.MessageState.INVALID; import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING; @@ -185,16 +190,19 @@ class ValidationManagerImpl implements ValidationManager, Service, int majorVersion = g.getMajorVersion(); Metadata meta = db.getMessageMetadataForValidator(txn, id); - DeliveryResult result = + DeliveryAction action = deliverMessage(txn, m, c, majorVersion, meta); - if (result.valid) { - addPendingDependents(txn, id, pending); - if (result.share) { - db.setMessageShared(txn, id); - toShare.addAll(states.keySet()); - } - } else { + if (action == REJECT) { + invalidateMessage(txn, id); addDependentsToInvalidate(txn, id, invalidate); + } else if (action == ACCEPT_SHARE) { + db.setMessageState(txn, m.getId(), DELIVERED); + addPendingDependents(txn, id, pending); + db.setMessageShared(txn, id); + toShare.addAll(states.keySet()); + } else if (action == ACCEPT_DO_NOT_SHARE) { + db.setMessageState(txn, m.getId(), DELIVERED); + addPendingDependents(txn, id, pending); } } } @@ -275,16 +283,21 @@ class ValidationManagerImpl implements ValidationManager, Service, Metadata meta = context.getMetadata(); db.mergeMessageMetadata(txn, id, meta); if (allDelivered) { - DeliveryResult result = + DeliveryAction action = deliverMessage(txn, m, c, majorVersion, meta); - if (result.valid) { - addPendingDependents(txn, id, pending); - if (result.share) { - db.setMessageShared(txn, id); - toShare.addAll(dependencies); - } - } else { + if (action == REJECT) { + invalidateMessage(txn, id); addDependentsToInvalidate(txn, id, invalidate); + } else if (action == DEFER) { + db.setMessageState(txn, id, PENDING); + } else if (action == ACCEPT_SHARE) { + db.setMessageState(txn, id, DELIVERED); + addPendingDependents(txn, id, pending); + db.setMessageShared(txn, id); + toShare.addAll(dependencies); + } else if (action == ACCEPT_DO_NOT_SHARE) { + db.setMessageState(txn, id, DELIVERED); + addPendingDependents(txn, id, pending); } } else { db.setMessageState(txn, id, PENDING); @@ -304,23 +317,21 @@ class ValidationManagerImpl implements ValidationManager, Service, } @DatabaseExecutor - private DeliveryResult deliverMessage(Transaction txn, Message m, - ClientId c, int majorVersion, Metadata meta) throws DbException { - // Deliver the message to the client if it's registered a hook - boolean shareMsg = false; + private DeliveryAction deliverMessage(Transaction txn, Message m, + ClientId c, int majorVersion, Metadata meta) { + // Deliver the message to the client if it has registered a hook ClientMajorVersion cv = new ClientMajorVersion(c, majorVersion); IncomingMessageHook hook = hooks.get(cv); - if (hook != null) { - try { - shareMsg = hook.incomingMessage(txn, m, meta); - } catch (InvalidMessageException e) { - logException(LOG, INFO, e); - invalidateMessage(txn, m.getId()); - return new DeliveryResult(false, false); - } + if (hook == null) return ACCEPT_DO_NOT_SHARE; + try { + return hook.incomingMessage(txn, m, meta); + } catch (DbException e) { + logException(LOG, INFO, e); + return DEFER; + } catch (InvalidMessageException e) { + logException(LOG, INFO, e); + return REJECT; } - db.setMessageState(txn, m.getId(), DELIVERED); - return new DeliveryResult(true, shareMsg); } @DatabaseExecutor @@ -447,14 +458,4 @@ class ValidationManagerImpl implements ValidationManager, Service, logException(LOG, WARNING, e); } } - - private static class DeliveryResult { - - private final boolean valid, share; - - private DeliveryResult(boolean valid, boolean share) { - this.valid = valid; - this.share = share; - } - } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/KeyManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/KeyManagerImpl.java index 58aeb7cea..9a3d9b049 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/transport/KeyManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/KeyManagerImpl.java @@ -19,9 +19,8 @@ import org.briarproject.bramble.api.lifecycle.Service; import org.briarproject.bramble.api.lifecycle.ServiceException; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.PluginConfig; +import org.briarproject.bramble.api.plugin.PluginFactory; import org.briarproject.bramble.api.plugin.TransportId; -import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory; -import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; import org.briarproject.bramble.api.transport.KeyManager; import org.briarproject.bramble.api.transport.KeySetId; import org.briarproject.bramble.api.transport.StreamContext; @@ -40,6 +39,7 @@ import javax.annotation.concurrent.ThreadSafe; import javax.inject.Inject; import static java.util.logging.Level.INFO; +import static org.briarproject.bramble.api.sync.SyncConstants.MAX_TRANSPORT_LATENCY; @ThreadSafe @NotNullByDefault @@ -51,7 +51,6 @@ class KeyManagerImpl implements KeyManager, Service, EventListener { private final DatabaseComponent db; private final Executor dbExecutor; private final PluginConfig pluginConfig; - private final TransportKeyManagerFactory transportKeyManagerFactory; private final TransportCrypto transportCrypto; private final ConcurrentHashMap managers; @@ -61,34 +60,35 @@ class KeyManagerImpl implements KeyManager, Service, EventListener { KeyManagerImpl(DatabaseComponent db, @DatabaseExecutor Executor dbExecutor, PluginConfig pluginConfig, - TransportKeyManagerFactory transportKeyManagerFactory, - TransportCrypto transportCrypto) { + TransportCrypto transportCrypto, + TransportKeyManagerFactory transportKeyManagerFactory) { this.db = db; this.dbExecutor = dbExecutor; this.pluginConfig = pluginConfig; - this.transportKeyManagerFactory = transportKeyManagerFactory; this.transportCrypto = transportCrypto; managers = new ConcurrentHashMap<>(); + for (PluginFactory f : pluginConfig.getSimplexFactories()) { + TransportKeyManager m = transportKeyManagerFactory. + createTransportKeyManager(f.getId(), f.getMaxLatency()); + managers.put(f.getId(), m); + } + for (PluginFactory f : pluginConfig.getDuplexFactories()) { + TransportKeyManager m = transportKeyManagerFactory. + createTransportKeyManager(f.getId(), f.getMaxLatency()); + managers.put(f.getId(), m); + } } @Override public void startService() throws ServiceException { if (used.getAndSet(true)) throw new IllegalStateException(); - Map transports = new HashMap<>(); - for (SimplexPluginFactory f : pluginConfig.getSimplexFactories()) - transports.put(f.getId(), f.getMaxLatency()); - for (DuplexPluginFactory f : pluginConfig.getDuplexFactories()) - transports.put(f.getId(), f.getMaxLatency()); try { db.transaction(false, txn -> { - for (Entry e : transports.entrySet()) - db.addTransport(txn, e.getKey(), e.getValue()); - for (Entry e : transports.entrySet()) { - TransportKeyManager m = transportKeyManagerFactory - .createTransportKeyManager(e.getKey(), - e.getValue()); - managers.put(e.getKey(), m); - m.start(txn); + for (PluginFactory f : pluginConfig.getSimplexFactories()) { + addTransport(txn, f); + } + for (PluginFactory f : pluginConfig.getDuplexFactories()) { + addTransport(txn, f); } }); } catch (DbException e) { @@ -96,14 +96,32 @@ class KeyManagerImpl implements KeyManager, Service, EventListener { } } + private void addTransport(Transaction txn, PluginFactory f) + throws DbException { + long maxLatency = f.getMaxLatency(); + if (maxLatency > MAX_TRANSPORT_LATENCY) { + throw new IllegalStateException(); + } + db.addTransport(txn, f.getId(), maxLatency); + managers.get(f.getId()).start(txn); + } + @Override public void stopService() { } @Override - public Map addRotationKeys( - Transaction txn, ContactId c, SecretKey rootKey, long timestamp, - boolean alice, boolean active) throws DbException { + public KeySetId addRotationKeys(Transaction txn, ContactId c, + TransportId t, SecretKey rootKey, long timestamp, boolean alice, + boolean active) throws DbException { + return withManager(t, m -> + m.addRotationKeys(txn, c, rootKey, timestamp, alice, active)); + } + + @Override + public Map addRotationKeys(Transaction txn, + ContactId c, SecretKey rootKey, long timestamp, boolean alice, + boolean active) throws DbException { Map ids = new HashMap<>(); for (Entry e : managers.entrySet()) { TransportId t = e.getKey(); @@ -137,7 +155,7 @@ class KeyManagerImpl implements KeyManager, Service, EventListener { PendingContactId p, PublicKey theirPublicKey, KeyPair ourKeyPair) throws DbException, GeneralSecurityException { SecretKey staticMasterKey = transportCrypto - .deriveStaticMasterKey(theirPublicKey, ourKeyPair); + .deriveStaticMasterKey(theirPublicKey, ourKeyPair); SecretKey rootKey = transportCrypto.deriveHandshakeRootKey(staticMasterKey, true); boolean alice = transportCrypto.isAlice(theirPublicKey, ourKeyPair); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageEncoder.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageEncoder.java new file mode 100644 index 000000000..058fc894d --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageEncoder.java @@ -0,0 +1,22 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; + +@NotNullByDefault +interface MessageEncoder { + + Message encodeKeyMessage(GroupId contactGroupId, + TransportId transportId, PublicKey publicKey); + + Message encodeActivateMessage(GroupId contactGroupId, + TransportId transportId, MessageId previousMessageId); + + BdfDictionary encodeMessageMetadata(TransportId transportId, + MessageType type, boolean local); +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageEncoderImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageEncoderImpl.java new file mode 100644 index 000000000..35d019a79 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageEncoderImpl.java @@ -0,0 +1,77 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfEntry; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.bramble.transport.agreement.MessageType.ACTIVATE; +import static org.briarproject.bramble.transport.agreement.MessageType.KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_IS_SESSION; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_LOCAL; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_MESSAGE_TYPE; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_TRANSPORT_ID; + +@Immutable +@NotNullByDefault +class MessageEncoderImpl implements MessageEncoder { + + private final ClientHelper clientHelper; + private final Clock clock; + + @Inject + MessageEncoderImpl(ClientHelper clientHelper, Clock clock) { + this.clientHelper = clientHelper; + this.clock = clock; + } + + @Override + public Message encodeKeyMessage(GroupId contactGroupId, + TransportId transportId, PublicKey publicKey) { + BdfList body = BdfList.of( + KEY.getValue(), + transportId.getString(), + publicKey.getEncoded()); + return encodeMessage(contactGroupId, body); + } + + @Override + public Message encodeActivateMessage(GroupId contactGroupId, + TransportId transportId, MessageId previousMessageId) { + BdfList body = BdfList.of( + ACTIVATE.getValue(), + transportId.getString(), + previousMessageId); + return encodeMessage(contactGroupId, body); + } + + @Override + public BdfDictionary encodeMessageMetadata(TransportId transportId, + MessageType type, boolean local) { + return BdfDictionary.of( + new BdfEntry(MSG_KEY_IS_SESSION, false), + new BdfEntry(MSG_KEY_TRANSPORT_ID, transportId.getString()), + new BdfEntry(MSG_KEY_MESSAGE_TYPE, type.getValue()), + new BdfEntry(MSG_KEY_LOCAL, local)); + } + + private Message encodeMessage(GroupId contactGroupId, BdfList body) { + try { + return clientHelper.createMessage(contactGroupId, + clock.currentTimeMillis(), clientHelper.toByteArray(body)); + } catch (FormatException e) { + throw new AssertionError(); + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageType.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageType.java new file mode 100644 index 000000000..1e63464e3 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageType.java @@ -0,0 +1,29 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +enum MessageType { + + KEY(0), + ACTIVATE(1); + + private final int value; + + MessageType(int value) { + this.value = value; + } + + int getValue() { + return value; + } + + static MessageType fromValue(int value) throws FormatException { + for (MessageType t : values()) if (t.value == value) return t; + throw new FormatException(); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/Session.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/Session.java new file mode 100644 index 000000000..f431bdfd1 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/Session.java @@ -0,0 +1,58 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.transport.KeySetId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class Session { + + private final State state; + @Nullable + private final MessageId lastLocalMessageId; + @Nullable + private final KeyPair localKeyPair; + @Nullable + private final Long localTimestamp; + @Nullable + private final KeySetId keySetId; + + Session(State state, @Nullable MessageId lastLocalMessageId, + @Nullable KeyPair localKeyPair, @Nullable Long localTimestamp, + @Nullable KeySetId keySetId) { + this.state = state; + this.lastLocalMessageId = lastLocalMessageId; + this.localKeyPair = localKeyPair; + this.localTimestamp = localTimestamp; + this.keySetId = keySetId; + } + + State getState() { + return state; + } + + @Nullable + MessageId getLastLocalMessageId() { + return lastLocalMessageId; + } + + @Nullable + KeyPair getLocalKeyPair() { + return localKeyPair; + } + + @Nullable + Long getLocalTimestamp() { + return localTimestamp; + } + + @Nullable + KeySetId getKeySetId() { + return keySetId; + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionEncoder.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionEncoder.java new file mode 100644 index 000000000..949ed977b --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionEncoder.java @@ -0,0 +1,13 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; + +@NotNullByDefault +interface SessionEncoder { + + BdfDictionary encodeSession(Session s, TransportId transportId); + + BdfDictionary getSessionQuery(TransportId transportId); +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionEncoderImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionEncoderImpl.java new file mode 100644 index 000000000..8b79f45fa --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionEncoderImpl.java @@ -0,0 +1,68 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfEntry; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.transport.KeySetId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_IS_SESSION; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_TRANSPORT_ID; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_KEY_SET_ID; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LOCAL_PRIVATE_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LOCAL_PUBLIC_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LOCAL_TIMESTAMP; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_STATE; + +@Immutable +@NotNullByDefault +class SessionEncoderImpl implements SessionEncoder { + + @Inject + SessionEncoderImpl() { + } + + @Override + public BdfDictionary encodeSession(Session s, TransportId transportId) { + BdfDictionary meta = new BdfDictionary(); + meta.put(MSG_KEY_IS_SESSION, true); + meta.put(MSG_KEY_TRANSPORT_ID, transportId.getString()); + meta.put(SESSION_KEY_STATE, s.getState().getValue()); + putNullable(meta, SESSION_KEY_LAST_LOCAL_MESSAGE_ID, + s.getLastLocalMessageId()); + KeyPair localKeyPair = s.getLocalKeyPair(); + if (localKeyPair == null) { + meta.put(SESSION_KEY_LOCAL_PUBLIC_KEY, NULL_VALUE); + meta.put(SESSION_KEY_LOCAL_PRIVATE_KEY, NULL_VALUE); + } else { + meta.put(SESSION_KEY_LOCAL_PUBLIC_KEY, + localKeyPair.getPublic().getEncoded()); + meta.put(SESSION_KEY_LOCAL_PRIVATE_KEY, + localKeyPair.getPrivate().getEncoded()); + } + putNullable(meta, SESSION_KEY_LOCAL_TIMESTAMP, s.getLocalTimestamp()); + KeySetId keySetId = s.getKeySetId(); + if (keySetId == null) meta.put(SESSION_KEY_KEY_SET_ID, NULL_VALUE); + else meta.put(SESSION_KEY_KEY_SET_ID, keySetId.getInt()); + return meta; + } + + @Override + public BdfDictionary getSessionQuery(TransportId transportId) { + return BdfDictionary.of( + new BdfEntry(MSG_KEY_IS_SESSION, true), + new BdfEntry(MSG_KEY_TRANSPORT_ID, transportId.getString())); + } + + private void putNullable(BdfDictionary meta, String key, + @Nullable Object o) { + meta.put(key, o == null ? NULL_VALUE : o); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionParser.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionParser.java new file mode 100644 index 000000000..7b9e2a3e8 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionParser.java @@ -0,0 +1,11 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +interface SessionParser { + + Session parseSession(BdfDictionary meta) throws FormatException; +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionParserImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionParserImpl.java new file mode 100644 index 000000000..14ba99fb8 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionParserImpl.java @@ -0,0 +1,67 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.PrivateKey; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.transport.KeySetId; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_KEY_SET_ID; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LOCAL_PRIVATE_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LOCAL_PUBLIC_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LOCAL_TIMESTAMP; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_STATE; + +@Immutable +@NotNullByDefault +class SessionParserImpl implements SessionParser { + + private final TransportKeyAgreementCrypto crypto; + + @Inject + SessionParserImpl(TransportKeyAgreementCrypto crypto) { + this.crypto = crypto; + } + + @Override + public Session parseSession(BdfDictionary meta) throws FormatException { + State state = + State.fromValue(meta.getLong(SESSION_KEY_STATE).intValue()); + + MessageId lastLocalMessageId = null; + byte[] lastLocalMessageIdBytes = + meta.getOptionalRaw(SESSION_KEY_LAST_LOCAL_MESSAGE_ID); + if (lastLocalMessageIdBytes != null) { + lastLocalMessageId = new MessageId(lastLocalMessageIdBytes); + } + + KeyPair localKeyPair = null; + byte[] localPublicKeyBytes = + meta.getOptionalRaw(SESSION_KEY_LOCAL_PUBLIC_KEY); + byte[] localPrivateKeyBytes = + meta.getOptionalRaw(SESSION_KEY_LOCAL_PRIVATE_KEY); + if (localPublicKeyBytes != null && localPrivateKeyBytes != null) { + PublicKey pub = crypto.parsePublicKey(localPublicKeyBytes); + PrivateKey priv = crypto.parsePrivateKey(localPrivateKeyBytes); + localKeyPair = new KeyPair(pub, priv); + } + + Long localTimestamp = meta.getOptionalLong(SESSION_KEY_LOCAL_TIMESTAMP); + + KeySetId keySetId = null; + Long keySetIdLong = meta.getOptionalLong(SESSION_KEY_KEY_SET_ID); + if (keySetIdLong != null) { + keySetId = new KeySetId(keySetIdLong.intValue()); + } + + return new Session(state, lastLocalMessageId, localKeyPair, + localTimestamp, keySetId); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/State.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/State.java new file mode 100644 index 000000000..9ab3f97da --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/State.java @@ -0,0 +1,43 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +enum State { + + /** + * We've sent a key message and are awaiting the contact's key message. + */ + AWAIT_KEY(0), + + /** + * We've exchanged key messages, derived the transport keys and sent an + * activate message, and now we're awaiting the contact's activate message. + */ + AWAIT_ACTIVATE(1), + + /** + * We've exchanged key messages and activate messages, and have derived and + * activated the transport keys. This is the end state. + */ + ACTIVATED(2); + + private final int value; + + State(int value) { + this.value = value; + } + + int getValue() { + return value; + } + + static State fromValue(int value) throws FormatException { + for (State s : values()) if (s.value == value) return s; + throw new FormatException(); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementConstants.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementConstants.java new file mode 100644 index 000000000..ae2fd4474 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementConstants.java @@ -0,0 +1,27 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +interface TransportKeyAgreementConstants { + + String MSG_KEY_IS_SESSION = "isSession"; + String MSG_KEY_MESSAGE_TYPE = "messageType"; + String MSG_KEY_TRANSPORT_ID = "transportId"; + String MSG_KEY_PUBLIC_KEY = "publicKey"; + String MSG_KEY_LOCAL = "local"; + + String SESSION_KEY_STATE = "state"; + String SESSION_KEY_LAST_LOCAL_MESSAGE_ID = "lastLocalMessageId"; + String SESSION_KEY_LOCAL_PUBLIC_KEY = "localPublicKey"; + String SESSION_KEY_LOCAL_PRIVATE_KEY = "localPrivateKey"; + String SESSION_KEY_LOCAL_TIMESTAMP = "localTimestamp"; + String SESSION_KEY_KEY_SET_ID = "keySetId"; + + /** + * Label for deriving the root key from key pairs. + */ + String ROOT_KEY_LABEL = + "org.briarproject.bramble.transport.agreement/ROOT_KEY"; + +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementCrypto.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementCrypto.java new file mode 100644 index 000000000..888e33d6e --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementCrypto.java @@ -0,0 +1,23 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.PrivateKey; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.security.GeneralSecurityException; + +@NotNullByDefault +interface TransportKeyAgreementCrypto { + + KeyPair generateKeyPair(); + + SecretKey deriveRootKey(KeyPair localKeyPair, PublicKey remotePublicKey) + throws GeneralSecurityException; + + PublicKey parsePublicKey(byte[] encoded) throws FormatException; + + PrivateKey parsePrivateKey(byte[] encoded) throws FormatException; +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementCryptoImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementCryptoImpl.java new file mode 100644 index 000000000..5fed461e1 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementCryptoImpl.java @@ -0,0 +1,66 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.PrivateKey; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.security.GeneralSecurityException; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.bramble.api.Bytes.compare; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.ROOT_KEY_LABEL; + +@Immutable +@NotNullByDefault +class TransportKeyAgreementCryptoImpl implements TransportKeyAgreementCrypto { + + private final CryptoComponent crypto; + + @Inject + TransportKeyAgreementCryptoImpl(CryptoComponent crypto) { + this.crypto = crypto; + } + + @Override + public KeyPair generateKeyPair() { + return crypto.generateAgreementKeyPair(); + } + + @Override + public SecretKey deriveRootKey(KeyPair localKeyPair, + PublicKey remotePublicKey) throws GeneralSecurityException { + byte[] theirPublic = remotePublicKey.getEncoded(); + byte[] ourPublic = localKeyPair.getPublic().getEncoded(); + boolean alice = compare(ourPublic, theirPublic) < 0; + byte[][] inputs = { + alice ? ourPublic : theirPublic, + alice ? theirPublic : ourPublic + }; + return crypto.deriveSharedSecret(ROOT_KEY_LABEL, remotePublicKey, + localKeyPair, inputs); + } + + @Override + public PublicKey parsePublicKey(byte[] encoded) throws FormatException { + try { + return crypto.getAgreementKeyParser().parsePublicKey(encoded); + } catch (GeneralSecurityException e) { + throw new FormatException(); + } + } + + @Override + public PrivateKey parsePrivateKey(byte[] encoded) throws FormatException { + try { + return crypto.getAgreementKeyParser().parsePrivateKey(encoded); + } catch (GeneralSecurityException e) { + throw new FormatException(); + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementManagerImpl.java new file mode 100644 index 000000000..8fab1c8b2 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementManagerImpl.java @@ -0,0 +1,408 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.BdfIncomingMessageHook; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.ContactGroupFactory; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.contact.ContactManager.ContactHook; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.data.MetadataParser; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Metadata; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.PluginConfig; +import org.briarproject.bramble.api.plugin.PluginFactory; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.Group.Visibility; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.transport.KeyManager; +import org.briarproject.bramble.api.transport.KeySetId; +import org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager; +import org.briarproject.bramble.api.versioning.ClientVersioningManager; +import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook; + +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static java.lang.Math.min; +import static java.util.Collections.singletonMap; +import static java.util.logging.Level.INFO; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.Bytes.compare; +import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.DEFER; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.REJECT; +import static org.briarproject.bramble.transport.agreement.MessageType.ACTIVATE; +import static org.briarproject.bramble.transport.agreement.MessageType.KEY; +import static org.briarproject.bramble.transport.agreement.State.ACTIVATED; +import static org.briarproject.bramble.transport.agreement.State.AWAIT_ACTIVATE; +import static org.briarproject.bramble.transport.agreement.State.AWAIT_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_MESSAGE_TYPE; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_PUBLIC_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_TRANSPORT_ID; + +@Immutable +@NotNullByDefault +class TransportKeyAgreementManagerImpl extends BdfIncomingMessageHook + implements TransportKeyAgreementManager, OpenDatabaseHook, ContactHook, + ClientVersioningHook { + + private static final Logger LOG = + getLogger(TransportKeyAgreementManagerImpl.class.getName()); + + private final ContactGroupFactory contactGroupFactory; + private final ClientVersioningManager clientVersioningManager; + private final IdentityManager identityManager; + private final KeyManager keyManager; + private final MessageEncoder messageEncoder; + private final SessionEncoder sessionEncoder; + private final SessionParser sessionParser; + private final TransportKeyAgreementCrypto crypto; + + private final List transports; + private final Group localGroup; + + @Inject + TransportKeyAgreementManagerImpl( + DatabaseComponent db, + ClientHelper clientHelper, + MetadataParser metadataParser, + ContactGroupFactory contactGroupFactory, + ClientVersioningManager clientVersioningManager, + IdentityManager identityManager, + KeyManager keyManager, + MessageEncoder messageEncoder, + SessionEncoder sessionEncoder, + SessionParser sessionParser, + TransportKeyAgreementCrypto crypto, + PluginConfig config) { + super(db, clientHelper, metadataParser); + this.contactGroupFactory = contactGroupFactory; + this.clientVersioningManager = clientVersioningManager; + this.identityManager = identityManager; + this.keyManager = keyManager; + this.messageEncoder = messageEncoder; + this.sessionEncoder = sessionEncoder; + this.sessionParser = sessionParser; + this.crypto = crypto; + transports = new ArrayList<>(); + for (PluginFactory f : config.getDuplexFactories()) { + transports.add(f.getId()); + } + for (PluginFactory f : config.getSimplexFactories()) { + transports.add(f.getId()); + } + localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID, + MAJOR_VERSION); + } + + @Override + public void onDatabaseOpened(Transaction txn) throws DbException { + Collection contacts = db.getContacts(txn); + if (!db.containsGroup(txn, localGroup.getId())) { + db.addGroup(txn, localGroup); + // Set things up for any pre-existing contacts + for (Contact c : contacts) addingContact(txn, c); + } + // Find any contacts and transports that need keys + Map> transportsWithKeys = + db.getTransportsWithKeys(txn); + for (Contact c : contacts) { + Collection withKeys = + transportsWithKeys.get(c.getId()); + for (TransportId t : transports) { + if (withKeys == null || !withKeys.contains(t)) { + // We need keys for this contact and transport + GroupId contactGroupId = getContactGroup(c).getId(); + SavedSession ss = loadSession(txn, contactGroupId, t); + if (ss == null) { + // Start a session by sending our key message + startSession(txn, contactGroupId, t); + } + } + } + } + } + + @Override + public void addingContact(Transaction txn, Contact c) throws DbException { + // Create a group to share with the contact + Group g = getContactGroup(c); + db.addGroup(txn, g); + // Attach the contact ID to the group + clientHelper.setContactId(txn, g.getId(), c.getId()); + // Apply the client's visibility to the contact group + Visibility client = clientVersioningManager.getClientVisibility(txn, + c.getId(), CLIENT_ID, MAJOR_VERSION); + db.setGroupVisibility(txn, c.getId(), g.getId(), client); + } + + @Override + public void removingContact(Transaction txn, Contact c) throws DbException { + db.removeGroup(txn, getContactGroup(c)); + } + + @Override + public void onClientVisibilityChanging(Transaction txn, Contact c, + Visibility v) throws DbException { + // Apply the client's visibility to the contact group + Group g = getContactGroup(c); + db.setGroupVisibility(txn, c.getId(), g.getId(), v); + } + + @Override + protected DeliveryAction incomingMessage(Transaction txn, Message m, + BdfList body, BdfDictionary meta) + throws DbException, FormatException { + MessageType type = MessageType.fromValue( + meta.getLong(MSG_KEY_MESSAGE_TYPE).intValue()); + TransportId t = new TransportId(meta.getString(MSG_KEY_TRANSPORT_ID)); + if (LOG.isLoggable(INFO)) { + LOG.info("Received " + type + " message for " + t); + } + if (!transports.contains(t)) { + // Defer handling the message until we support the transport + return DEFER; + } + SavedSession ss = loadSession(txn, m.getGroupId(), t); + if (type == KEY) return handleKeyMessage(txn, t, m, meta, ss); + else if (type == ACTIVATE) return handleActivateMessage(txn, t, ss); + else throw new AssertionError(); + } + + private DeliveryAction handleKeyMessage(Transaction txn, TransportId t, + Message m, BdfDictionary meta, @Nullable SavedSession ss) + throws DbException, FormatException { + ContactId c = clientHelper.getContactId(txn, m.getGroupId()); + boolean haveKeys = db.containsTransportKeys(txn, c, t); + if (ss == null) { + if (haveKeys) { + // We have keys but no session, so we must have derived keys + // when adding the contact. If the contact didn't support + // the transport when they added us, they wouldn't have + // derived keys at that time. If they later added support for + // the transport then they would have started a session, so a + // key message is valid in this case + return handleKeyMessageForNewSession(txn, c, t, m, meta); + } else { + // We don't have keys, so we should have created a session at + // startup + throw new IllegalStateException(); + } + } else if (ss.session.getState() == AWAIT_KEY) { + if (haveKeys) { + // We have keys, so we shouldn't be in the AWAIT_KEY state, + // even if the contact didn't derive keys when adding us and + // later started a session + throw new IllegalStateException(); + } else { + // This is the key message we're waiting for + return handleKeyMessageForExistingSession(txn, c, t, m, meta, + ss); + } + } else { + return REJECT; // Not valid in this state + } + } + + private DeliveryAction handleActivateMessage(Transaction txn, + TransportId t, @Nullable SavedSession ss) throws DbException { + if (ss != null && ss.session.getState() == AWAIT_ACTIVATE) { + // Activate the keys and finish the session + KeySetId keySetId = requireNonNull(ss.session.getKeySetId()); + keyManager.activateKeys(txn, singletonMap(t, keySetId)); + Session session = new Session(ACTIVATED, + ss.session.getLastLocalMessageId(), null, null, null); + saveSession(txn, t, ss.storageId, session); + return ACCEPT_DO_NOT_SHARE; + } else { + return REJECT; // Not valid in this state + } + } + + private DeliveryAction handleKeyMessageForNewSession(Transaction txn, + ContactId c, TransportId t, Message m, BdfDictionary meta) + throws DbException, FormatException { + KeyPair localKeyPair = crypto.generateKeyPair(); + PublicKey remotePublicKey = + crypto.parsePublicKey(meta.getRaw(MSG_KEY_PUBLIC_KEY)); + Message keyMessage = sendKeyMessage(txn, m.getGroupId(), t, + localKeyPair.getPublic()); + long minTimestamp = min(keyMessage.getTimestamp(), m.getTimestamp()); + SecretKey rootKey; + try { + rootKey = crypto.deriveRootKey(localKeyPair, remotePublicKey); + } catch (GeneralSecurityException e) { + return REJECT; // Invalid public key + } + boolean alice = isLocalPartyAlice(txn, db.getContact(txn, c)); + KeySetId keySetId = keyManager.addRotationKeys(txn, c, t, rootKey, + minTimestamp, alice, false); + Message activateMessage = + sendActivateMessage(txn, m.getGroupId(), t, keyMessage.getId()); + Session session = new Session(AWAIT_ACTIVATE, activateMessage.getId(), + null, null, keySetId); + saveNewSession(txn, m.getGroupId(), t, session); + return ACCEPT_DO_NOT_SHARE; + } + + private DeliveryAction handleKeyMessageForExistingSession(Transaction txn, + ContactId c, TransportId t, Message m, BdfDictionary meta, + SavedSession ss) throws DbException, FormatException { + KeyPair localKeyPair = requireNonNull(ss.session.getLocalKeyPair()); + PublicKey remotePublicKey = + crypto.parsePublicKey(meta.getRaw(MSG_KEY_PUBLIC_KEY)); + long localTimestamp = requireNonNull(ss.session.getLocalTimestamp()); + long minTimestamp = min(localTimestamp, m.getTimestamp()); + SecretKey rootKey; + try { + rootKey = crypto.deriveRootKey(localKeyPair, remotePublicKey); + } catch (GeneralSecurityException e) { + return REJECT; // Invalid public key + } + boolean alice = isLocalPartyAlice(txn, db.getContact(txn, c)); + KeySetId keySetId = keyManager.addRotationKeys(txn, c, t, rootKey, + minTimestamp, alice, false); + MessageId previousMessageId = + requireNonNull(ss.session.getLastLocalMessageId()); + Message activateMessage = + sendActivateMessage(txn, m.getGroupId(), t, previousMessageId); + Session session = new Session(AWAIT_ACTIVATE, activateMessage.getId(), + null, null, keySetId); + saveSession(txn, t, ss.storageId, session); + return ACCEPT_DO_NOT_SHARE; + } + + private void startSession(Transaction txn, GroupId contactGroupId, + TransportId t) throws DbException { + KeyPair localKeyPair = crypto.generateKeyPair(); + Message keyMessage = sendKeyMessage(txn, contactGroupId, t, + localKeyPair.getPublic()); + Session session = new Session(AWAIT_KEY, keyMessage.getId(), + localKeyPair, keyMessage.getTimestamp(), null); + saveNewSession(txn, contactGroupId, t, session); + } + + @Nullable + private SavedSession loadSession(Transaction txn, GroupId contactGroupId, + TransportId t) throws DbException { + try { + BdfDictionary query = sessionEncoder.getSessionQuery(t); + Collection ids = + clientHelper.getMessageIds(txn, contactGroupId, query); + if (ids.size() > 1) throw new DbException(); + if (ids.isEmpty()) { + if (LOG.isLoggable(INFO)) LOG.info("No session for " + t); + return null; + } + MessageId storageId = ids.iterator().next(); + BdfDictionary bdfSession = + clientHelper.getMessageMetadataAsDictionary(txn, storageId); + Session session = sessionParser.parseSession(bdfSession); + if (LOG.isLoggable(INFO)) { + LOG.info("Loaded session in state " + session.getState() + + " for " + t); + } + return new SavedSession(session, storageId); + } catch (FormatException e) { + throw new DbException(e); + } + } + + private void saveNewSession(Transaction txn, GroupId contactGroupId, + TransportId t, Session session) throws DbException { + Message m = + clientHelper.createMessageForStoringMetadata(contactGroupId); + db.addLocalMessage(txn, m, new Metadata(), false, false); + MessageId storageId = m.getId(); + saveSession(txn, t, storageId, session); + } + + private void saveSession(Transaction txn, TransportId t, + MessageId storageId, Session session) throws DbException { + if (LOG.isLoggable(INFO)) { + LOG.info("Saving session in state " + session.getState() + + " for " + t); + } + BdfDictionary meta = sessionEncoder.encodeSession(session, t); + try { + clientHelper.mergeMessageMetadata(txn, storageId, meta); + } catch (FormatException e) { + throw new AssertionError(); + } + } + + private Message sendKeyMessage(Transaction txn, GroupId contactGroupId, + TransportId t, PublicKey publicKey) throws DbException { + Message m = messageEncoder.encodeKeyMessage(contactGroupId, t, + publicKey); + sendMessage(txn, t, m, KEY); + return m; + } + + private Message sendActivateMessage(Transaction txn, + GroupId contactGroupId, TransportId t, MessageId previousMessageId) + throws DbException { + Message m = messageEncoder.encodeActivateMessage(contactGroupId, t, + previousMessageId); + sendMessage(txn, t, m, ACTIVATE); + return m; + } + + private void sendMessage(Transaction txn, TransportId t, Message m, + MessageType type) throws DbException { + BdfDictionary meta = + messageEncoder.encodeMessageMetadata(t, type, true); + try { + clientHelper.addLocalMessage(txn, m, meta, true, false); + } catch (FormatException e) { + throw new AssertionError(); + } + } + + private Group getContactGroup(Contact c) { + return contactGroupFactory.createContactGroup(CLIENT_ID, + MAJOR_VERSION, c); + } + + private boolean isLocalPartyAlice(Transaction txn, Contact c) + throws DbException { + Author local = identityManager.getLocalAuthor(txn); + Author remote = c.getAuthor(); + return compare(local.getId().getBytes(), remote.getId().getBytes()) < 0; + } + + private static class SavedSession { + + private final Session session; + private final MessageId storageId; + + private SavedSession(Session session, MessageId storageId) { + this.session = session; + this.storageId = storageId; + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementModule.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementModule.java new file mode 100644 index 000000000..8bff228fa --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementModule.java @@ -0,0 +1,83 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.data.MetadataEncoder; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.sync.validation.ValidationManager; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager; +import org.briarproject.bramble.api.versioning.ClientVersioningManager; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +import static org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager.CLIENT_ID; +import static org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager.MAJOR_VERSION; +import static org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager.MINOR_VERSION; + +@Module +public class TransportKeyAgreementModule { + + public static class EagerSingletons { + @Inject + TransportKeyAgreementManager transportKeyAgreementManager; + @Inject + TransportKeyAgreementValidator transportKeyAgreementValidator; + } + + @Provides + @Singleton + TransportKeyAgreementManager provideTransportKeyAgreementManager( + LifecycleManager lifecycleManager, + ValidationManager validationManager, + ContactManager contactManager, + ClientVersioningManager clientVersioningManager, + TransportKeyAgreementManagerImpl transportKeyAgreementManager) { + lifecycleManager.registerOpenDatabaseHook(transportKeyAgreementManager); + validationManager.registerIncomingMessageHook(CLIENT_ID, + MAJOR_VERSION, transportKeyAgreementManager); + contactManager.registerContactHook(transportKeyAgreementManager); + clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION, + MINOR_VERSION, transportKeyAgreementManager); + return transportKeyAgreementManager; + } + + @Provides + @Singleton + TransportKeyAgreementValidator provideTransportKeyAgreementValidator( + ClientHelper clientHelper, MetadataEncoder metadataEncoder, + Clock clock, MessageEncoder messageEncoder, + ValidationManager validationManager) { + TransportKeyAgreementValidator validator = + new TransportKeyAgreementValidator(clientHelper, + metadataEncoder, clock, messageEncoder); + validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION, + validator); + return validator; + } + + @Provides + MessageEncoder provideMessageEncoder(MessageEncoderImpl messageEncoder) { + return messageEncoder; + } + + @Provides + SessionEncoder provideSessionEncoder(SessionEncoderImpl sessionEncoder) { + return sessionEncoder; + } + + @Provides + SessionParser provideSessionParser(SessionParserImpl sessionParser) { + return sessionParser; + } + + @Provides + TransportKeyAgreementCrypto provideTransportKeyAgreementCrypto( + TransportKeyAgreementCryptoImpl transportKeyAgreementCrypto) { + return transportKeyAgreementCrypto; + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementValidator.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementValidator.java new file mode 100644 index 000000000..c371fca77 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementValidator.java @@ -0,0 +1,79 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.BdfMessageContext; +import org.briarproject.bramble.api.client.BdfMessageValidator; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.data.MetadataEncoder; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; + +import javax.annotation.concurrent.Immutable; + +import static java.util.Collections.singletonList; +import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES; +import static org.briarproject.bramble.api.plugin.TransportId.MAX_TRANSPORT_ID_LENGTH; +import static org.briarproject.bramble.api.system.Clock.MIN_REASONABLE_TIME_MS; +import static org.briarproject.bramble.transport.agreement.MessageType.ACTIVATE; +import static org.briarproject.bramble.transport.agreement.MessageType.KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_PUBLIC_KEY; +import static org.briarproject.bramble.util.ValidationUtils.checkLength; +import static org.briarproject.bramble.util.ValidationUtils.checkSize; + +@Immutable +@NotNullByDefault +class TransportKeyAgreementValidator extends BdfMessageValidator { + + private final MessageEncoder messageEncoder; + + TransportKeyAgreementValidator(ClientHelper clientHelper, + MetadataEncoder metadataEncoder, Clock clock, + MessageEncoder messageEncoder) { + super(clientHelper, metadataEncoder, clock); + this.messageEncoder = messageEncoder; + } + + @Override + protected BdfMessageContext validateMessage(Message m, Group g, + BdfList body) throws FormatException { + MessageType type = MessageType.fromValue(body.getLong(0).intValue()); + if (type == KEY) return validateKeyMessage(m.getTimestamp(), body); + else if (type == ACTIVATE) return validateActivateMessage(body); + else throw new AssertionError(); + } + + private BdfMessageContext validateKeyMessage(long timestamp, BdfList body) + throws FormatException { + if (timestamp < MIN_REASONABLE_TIME_MS) throw new FormatException(); + // Message type, transport ID, public key + checkSize(body, 3); + String transportId = body.getString(1); + checkLength(transportId, 1, MAX_TRANSPORT_ID_LENGTH); + byte[] publicKey = body.getRaw(2); + checkLength(publicKey, 1, MAX_AGREEMENT_PUBLIC_KEY_BYTES); + BdfDictionary meta = messageEncoder.encodeMessageMetadata( + new TransportId(transportId), KEY, false); + meta.put(MSG_KEY_PUBLIC_KEY, publicKey); + return new BdfMessageContext(meta); + } + + private BdfMessageContext validateActivateMessage(BdfList body) + throws FormatException { + // Message type, transport ID, previous message ID + checkSize(body, 3); + String transportId = body.getString(1); + checkLength(transportId, 1, MAX_TRANSPORT_ID_LENGTH); + byte[] previousMessageId = body.getRaw(2); + checkLength(previousMessageId, MessageId.LENGTH); + BdfDictionary meta = messageEncoder.encodeMessageMetadata( + new TransportId(transportId), ACTIVATE, false); + MessageId dependency = new MessageId(previousMessageId); + return new BdfMessageContext(meta, singletonList(dependency)); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java index 6aadf889a..fdaa6a80f 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java @@ -50,6 +50,7 @@ import static java.util.Collections.emptyList; import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION; @@ -173,8 +174,8 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, } @Override - public boolean incomingMessage(Transaction txn, Message m, Metadata meta) - throws DbException, InvalidMessageException { + public DeliveryAction incomingMessage(Transaction txn, Message m, + Metadata meta) throws DbException, InvalidMessageException { try { // Parse the new remote update Update newRemoteUpdate = parseUpdate(clientHelper.toList(m)); @@ -187,7 +188,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, && latest.remote.updateVersion > newRemoteUpdateVersion) { db.deleteMessage(txn, m.getId()); db.deleteMessageMetadata(txn, m.getId()); - return false; + return ACCEPT_DO_NOT_SHARE; } // Load and parse the latest local update if (latest.local == null) throw new DbException(); @@ -241,7 +242,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, } catch (FormatException e) { throw new InvalidMessageException(e); } - return false; + return ACCEPT_DO_NOT_SHARE; } private void storeClientVersions(Transaction txn, diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java index 39615b2e5..7d15a5284 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java @@ -120,7 +120,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { private final MessageId messageId, messageId1; private final Metadata metadata; private final TransportId transportId; - private final int maxLatency; + private final long maxLatency; private final ContactId contactId; private final Contact contact; private final KeySetId keySetId; @@ -298,11 +298,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { throws Exception { context.checking(new Expectations() {{ // Check whether the contact is in the DB (which it's not) - exactly(18).of(database).startTransaction(); + exactly(19).of(database).startTransaction(); will(returnValue(txn)); - exactly(18).of(database).containsContact(txn, contactId); + exactly(19).of(database).containsContact(txn, contactId); will(returnValue(false)); - exactly(18).of(database).abortTransaction(txn); + exactly(19).of(database).abortTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, eventExecutor, shutdownManager); @@ -349,7 +349,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { } try { - db.transaction(false, transaction -> + db.transaction(true, transaction -> db.getContact(transaction, contactId)); fail(); } catch (NoSuchContactException expected) { @@ -357,7 +357,15 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { } try { - db.transaction(false, transaction -> + db.transaction(true, transaction -> + db.getUnackedMessageBytesToSend(transaction, contactId)); + fail(); + } catch (NoSuchContactException expected) { + // Expected + } + + try { + db.transaction(true, transaction -> db.getMessageStatus(transaction, contactId, groupId)); fail(); } catch (NoSuchContactException expected) { @@ -365,7 +373,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { } try { - db.transaction(false, transaction -> + db.transaction(true, transaction -> db.getMessageStatus(transaction, contactId, messageId)); fail(); } catch (NoSuchContactException expected) { @@ -373,7 +381,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { } try { - db.transaction(false, transaction -> + db.transaction(true, transaction -> db.getGroupVisibility(transaction, contactId, groupId)); fail(); } catch (NoSuchContactException expected) { @@ -381,7 +389,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { } try { - db.transaction(false, transaction -> + db.transaction(true, transaction -> db.getSyncVersions(transaction, contactId)); fail(); } catch (NoSuchContactException expected) { diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/ExponentialBackoffTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/ExponentialBackoffTest.java index 6d1de7e36..aade99d68 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/db/ExponentialBackoffTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/ExponentialBackoffTest.java @@ -3,6 +3,7 @@ package org.briarproject.bramble.db; import org.briarproject.bramble.test.BrambleTestCase; import org.junit.Test; +import static org.briarproject.bramble.api.sync.SyncConstants.MAX_TRANSPORT_LATENCY; import static org.junit.Assert.assertEquals; public class ExponentialBackoffTest extends BrambleTestCase { @@ -36,28 +37,28 @@ public class ExponentialBackoffTest extends BrambleTestCase { @Test public void testTransmissionCountOverflow() { - int maxLatency = Integer.MAX_VALUE; // RTT will not overflow + long maxLatency = MAX_TRANSPORT_LATENCY; // RTT will not overflow long expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 0); - assertEquals(Integer.MAX_VALUE * 2L, expiry); // No overflow - expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 31); - assertEquals(Integer.MAX_VALUE * (2L << 31), expiry); // No overflow - expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 32); + assertEquals(MAX_TRANSPORT_LATENCY * 2L, expiry); // No overflow + expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 27); + assertEquals(MAX_TRANSPORT_LATENCY * (2L << 27), expiry); // No overflow + expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 28); assertEquals(Long.MAX_VALUE, expiry); // Overflow caught - expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 33); + expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 29); assertEquals(Long.MAX_VALUE, expiry); // Overflow caught } @Test public void testCurrentTimeOverflow() { - int maxLatency = Integer.MAX_VALUE; // RTT will not overflow - long now = Long.MAX_VALUE - (Integer.MAX_VALUE * (2L << 31)); + long maxLatency = MAX_TRANSPORT_LATENCY; // RTT will not overflow + long now = Long.MAX_VALUE - (MAX_TRANSPORT_LATENCY * (2L << 27)); long expiry = ExponentialBackoff.calculateExpiry(now, maxLatency, 0); - assertEquals(now + Integer.MAX_VALUE * 2L, expiry); // No overflow - expiry = ExponentialBackoff.calculateExpiry(now - 1, maxLatency, 31); + assertEquals(now + MAX_TRANSPORT_LATENCY * 2L, expiry); // No overflow + expiry = ExponentialBackoff.calculateExpiry(now - 1, maxLatency, 27); assertEquals(Long.MAX_VALUE - 1, expiry); // No overflow - expiry = ExponentialBackoff.calculateExpiry(now, maxLatency, 31); + expiry = ExponentialBackoff.calculateExpiry(now, maxLatency, 27); assertEquals(Long.MAX_VALUE, expiry); // No overflow - expiry = ExponentialBackoff.calculateExpiry(now + 1, maxLatency, 32); + expiry = ExponentialBackoff.calculateExpiry(now + 1, maxLatency, 27); assertEquals(Long.MAX_VALUE, expiry); // Overflow caught } } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java index 4dfec5fce..649b09369 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java @@ -57,6 +57,7 @@ import java.util.concurrent.atomic.AtomicLong; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static java.util.concurrent.TimeUnit.SECONDS; @@ -222,18 +223,13 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { db.addMessage(txn, message, DELIVERED, true, false, null); // The contact has not seen the message, so it should be sendable - Collection ids = - db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); - assertEquals(singletonList(messageId), ids); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertEquals(singletonList(messageId), ids); + assertOneMessageToSendEagerly(db, txn); + assertOneMessageToSendLazily(db, txn); // Changing the status to seen = true should make the message unsendable db.raiseSeenFlag(txn, contactId, messageId); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertTrue(ids.isEmpty()); + assertNothingToSendEagerly(db, txn); + assertNothingToSendLazily(db, txn); db.commitTransaction(txn); db.close(); @@ -253,32 +249,23 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { db.addMessage(txn, message, UNKNOWN, true, false, null); // The message has not been validated, so it should not be sendable - Collection ids = db.getMessagesToSend(txn, contactId, - ONE_MEGABYTE, MAX_LATENCY); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertTrue(ids.isEmpty()); + assertNothingToSendLazily(db, txn); + assertNothingToSendEagerly(db, txn); // Marking the message delivered should make it sendable db.setMessageState(txn, messageId, DELIVERED); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); - assertEquals(singletonList(messageId), ids); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertEquals(singletonList(messageId), ids); + assertOneMessageToSendLazily(db, txn); + assertOneMessageToSendEagerly(db, txn); // Marking the message invalid should make it unsendable db.setMessageState(txn, messageId, INVALID); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertTrue(ids.isEmpty()); + assertNothingToSendLazily(db, txn); + assertNothingToSendEagerly(db, txn); // Marking the message pending should make it unsendable db.setMessageState(txn, messageId, PENDING); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertTrue(ids.isEmpty()); + assertNothingToSendLazily(db, txn); + assertNothingToSendEagerly(db, txn); db.commitTransaction(txn); db.close(); @@ -297,39 +284,28 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { db.addMessage(txn, message, DELIVERED, true, false, null); // The group is invisible, so the message should not be sendable - Collection ids = db.getMessagesToSend(txn, contactId, - ONE_MEGABYTE, MAX_LATENCY); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertTrue(ids.isEmpty()); + assertNothingToSendLazily(db, txn); + assertNothingToSendEagerly(db, txn); // Making the group visible should not make the message sendable db.addGroupVisibility(txn, contactId, groupId, false); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertTrue(ids.isEmpty()); + assertNothingToSendLazily(db, txn); + assertNothingToSendEagerly(db, txn); // Sharing the group should make the message sendable db.setGroupVisibility(txn, contactId, groupId, true); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); - assertEquals(singletonList(messageId), ids); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertEquals(singletonList(messageId), ids); + assertOneMessageToSendEagerly(db, txn); + assertOneMessageToSendLazily(db, txn); // Unsharing the group should make the message unsendable db.setGroupVisibility(txn, contactId, groupId, false); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertTrue(ids.isEmpty()); + assertNothingToSendLazily(db, txn); + assertNothingToSendEagerly(db, txn); // Making the group invisible should make the message unsendable db.removeGroupVisibility(txn, contactId, groupId); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertTrue(ids.isEmpty()); + assertNothingToSendLazily(db, txn); + assertNothingToSendEagerly(db, txn); db.commitTransaction(txn); db.close(); @@ -349,18 +325,13 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { db.addMessage(txn, message, DELIVERED, false, false, null); // The message is not shared, so it should not be sendable - Collection ids = db.getMessagesToSend(txn, contactId, - ONE_MEGABYTE, MAX_LATENCY); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertTrue(ids.isEmpty()); + assertNothingToSendLazily(db, txn); + assertNothingToSendEagerly(db, txn); // Sharing the message should make it sendable db.setMessageShared(txn, messageId, true); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); - assertEquals(singletonList(messageId), ids); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertEquals(singletonList(messageId), ids); + assertOneMessageToSendLazily(db, txn); + assertOneMessageToSendEagerly(db, txn); db.commitTransaction(txn); db.close(); @@ -380,10 +351,13 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { db.addMessage(txn, message, DELIVERED, true, false, null); // The message is sendable, but too large to send + assertOneMessageToSendLazily(db, txn); + assertOneMessageToSendEagerly(db, txn); Collection ids = db.getMessagesToSend(txn, contactId, message.getRawLength() - 1, MAX_LATENCY); assertTrue(ids.isEmpty()); + // The message is just the right size to send ids = db.getMessagesToSend(txn, contactId, message.getRawLength(), MAX_LATENCY); @@ -405,6 +379,12 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { db.addGroup(txn, group); db.addGroupVisibility(txn, contactId, groupId, false); + // Initially there should be nothing to send + assertFalse( + db.containsAnythingToSend(txn, contactId, MAX_LATENCY, false)); + assertFalse( + db.containsAnythingToSend(txn, contactId, MAX_LATENCY, true)); + // Add some messages to ack Message message1 = getMessage(groupId); MessageId messageId1 = message1.getId(); @@ -412,6 +392,10 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { db.addMessage(txn, message1, DELIVERED, true, false, contactId); // Both message IDs should be returned + assertTrue( + db.containsAnythingToSend(txn, contactId, MAX_LATENCY, false)); + assertTrue( + db.containsAnythingToSend(txn, contactId, MAX_LATENCY, true)); Collection ids = db.getMessagesToAck(txn, contactId, 1234); assertEquals(asList(messageId, messageId1), ids); @@ -419,6 +403,10 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { db.lowerAckFlag(txn, contactId, asList(messageId, messageId1)); // Both message IDs should have been removed + assertFalse( + db.containsAnythingToSend(txn, contactId, MAX_LATENCY, false)); + assertFalse( + db.containsAnythingToSend(txn, contactId, MAX_LATENCY, true)); assertEquals(emptyList(), db.getMessagesToAck(txn, contactId, 1234)); @@ -427,6 +415,10 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { db.raiseAckFlag(txn, contactId, messageId1); // Both message IDs should be returned + assertTrue( + db.containsAnythingToSend(txn, contactId, MAX_LATENCY, false)); + assertTrue( + db.containsAnythingToSend(txn, contactId, MAX_LATENCY, true)); ids = db.getMessagesToAck(txn, contactId, 1234); assertEquals(asList(messageId, messageId1), ids); @@ -447,22 +439,25 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { db.addGroupVisibility(txn, contactId, groupId, true); db.addMessage(txn, message, DELIVERED, true, false, null); - // Retrieve the message from the database and mark it as sent - Collection ids = db.getMessagesToSend(txn, contactId, - ONE_MEGABYTE, MAX_LATENCY); - assertEquals(singletonList(messageId), ids); + // The message should be sendable via lazy or eager retransmission + assertOneMessageToSendLazily(db, txn); + assertOneMessageToSendEagerly(db, txn); + + // Mark the message as sent db.updateExpiryTimeAndEta(txn, contactId, messageId, MAX_LATENCY); - // The message should no longer be sendable - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); - assertTrue(ids.isEmpty()); + // The message should no longer be sendable via lazy retransmission, + // but it should still be sendable via eager retransmission + assertNothingToSendLazily(db, txn); + assertOneMessageToSendEagerly(db, txn); - // Pretend that the message was acked + // Mark the message as acked db.raiseSeenFlag(txn, contactId, messageId); - // The message still should not be sendable - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); - assertTrue(ids.isEmpty()); + // The message still should not be sendable via lazy or eager + // retransmission + assertNothingToSendLazily(db, txn); + assertNothingToSendEagerly(db, txn); db.commitTransaction(txn); db.close(); @@ -676,7 +671,9 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { Connection txn = db.startTransaction(); // Initially there should be no transport keys in the database + assertFalse(db.containsTransportKeys(txn, contactId, transportId)); assertEquals(emptyList(), db.getTransportKeys(txn, transportId)); + assertTrue(db.getTransportsWithKeys(txn).isEmpty()); // Add the contact, the transport and the transport keys db.addIdentity(txn, identity); @@ -687,6 +684,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { assertEquals(keySetId1, db.addTransportKeys(txn, contactId, keys1)); // Retrieve the transport keys + assertTrue(db.containsTransportKeys(txn, contactId, transportId)); Collection allKeys = db.getTransportKeys(txn, transportId); assertEquals(2, allKeys.size()); @@ -699,6 +697,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { assertKeysEquals(keys1, ks.getKeys()); } } + assertEquals(singletonMap(contactId, singletonList(transportId)), + db.getTransportsWithKeys(txn)); // Update the transport keys TransportKeys updated = createTransportKeys(timePeriod + 1, active); @@ -710,6 +710,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { null, updated1)); // Retrieve the transport keys again + assertTrue(db.containsTransportKeys(txn, contactId, transportId)); allKeys = db.getTransportKeys(txn, transportId); assertEquals(2, allKeys.size()); for (TransportKeySet ks : allKeys) { @@ -721,10 +722,14 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { assertKeysEquals(updated1, ks.getKeys()); } } + assertEquals(singletonMap(contactId, singletonList(transportId)), + db.getTransportsWithKeys(txn)); // Removing the contact should remove the transport keys db.removeContact(txn, contactId); + assertFalse(db.containsTransportKeys(txn, contactId, transportId)); assertEquals(emptyList(), db.getTransportKeys(txn, transportId)); + assertTrue(db.getTransportsWithKeys(txn).isEmpty()); db.commitTransaction(txn); db.close(); @@ -1925,11 +1930,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { assertTrue(db.containsVisibleMessage(txn, contactId, messageId)); // The message should be sendable - Collection ids = db.getMessagesToSend(txn, contactId, - ONE_MEGABYTE, MAX_LATENCY); - assertEquals(singletonList(messageId), ids); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertEquals(singletonList(messageId), ids); + assertOneMessageToSendLazily(db, txn); + assertOneMessageToSendEagerly(db, txn); // The message should be available Message m = db.getMessage(txn, messageId); @@ -1945,10 +1947,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { assertTrue(db.containsVisibleMessage(txn, contactId, messageId)); // The message should not be sendable - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); - assertTrue(ids.isEmpty()); + assertNothingToSendLazily(db, txn); + assertNothingToSendEagerly(db, txn); // Requesting the message should throw an exception try { @@ -2577,6 +2577,50 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase { deleteTestDirectory(testDir); } + private void assertNothingToSendLazily(Database db, + Connection txn) throws Exception { + assertFalse( + db.containsAnythingToSend(txn, contactId, MAX_LATENCY, false)); + Collection ids = + db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); + assertTrue(ids.isEmpty()); + } + + private void assertOneMessageToSendLazily(Database db, + Connection txn) throws Exception { + assertTrue( + db.containsAnythingToSend(txn, contactId, MAX_LATENCY, false)); + Collection ids = + db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); + assertEquals(singletonList(messageId), ids); + ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); + assertEquals(singletonList(messageId), ids); + } + + private void assertNothingToSendEagerly(Database db, + Connection txn) throws Exception { + assertFalse( + db.containsAnythingToSend(txn, contactId, MAX_LATENCY, true)); + Map unacked = + db.getUnackedMessagesToSend(txn, contactId); + assertTrue(unacked.isEmpty()); + assertEquals(0, db.getUnackedMessageBytesToSend(txn, contactId)); + } + + private void assertOneMessageToSendEagerly(Database db, + Connection txn) throws Exception { + assertTrue( + db.containsAnythingToSend(txn, contactId, MAX_LATENCY, true)); + Map unacked = + db.getUnackedMessagesToSend(txn, contactId); + assertEquals(singleton(messageId), unacked.keySet()); + assertEquals(message.getRawLength(), unacked.get(messageId).intValue()); + assertEquals(message.getRawLength(), + db.getUnackedMessageBytesToSend(txn, contactId)); + } + private static class StoppedClock implements Clock { private final long time; diff --git a/bramble-core/src/test/java/org/briarproject/bramble/lifecycle/LifecycleManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/lifecycle/LifecycleManagerImplTest.java index 09ce697bf..d057573fc 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/lifecycle/LifecycleManagerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/lifecycle/LifecycleManagerImplTest.java @@ -6,6 +6,7 @@ import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook; import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent; +import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.test.BrambleMockTestCase; import org.briarproject.bramble.test.DbExpectations; import org.junit.Before; @@ -14,7 +15,10 @@ import org.junit.Test; import java.util.concurrent.atomic.AtomicBoolean; import static junit.framework.TestCase.assertTrue; +import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.CLOCK_ERROR; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SUCCESS; +import static org.briarproject.bramble.api.system.Clock.MAX_REASONABLE_TIME_MS; +import static org.briarproject.bramble.api.system.Clock.MIN_REASONABLE_TIME_MS; import static org.briarproject.bramble.test.TestUtils.getSecretKey; import static org.junit.Assert.assertEquals; @@ -22,6 +26,7 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase { private final DatabaseComponent db = context.mock(DatabaseComponent.class); private final EventBus eventBus = context.mock(EventBus.class); + private final Clock clock = context.mock(Clock.class); private final SecretKey dbKey = getSecretKey(); @@ -29,16 +34,19 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase { @Before public void setUp() { - lifecycleManager = new LifecycleManagerImpl(db, eventBus); + lifecycleManager = new LifecycleManagerImpl(db, eventBus, clock); } @Test public void testOpenDatabaseHooksAreCalledAtStartup() throws Exception { + long now = System.currentTimeMillis(); Transaction txn = new Transaction(null, false); AtomicBoolean called = new AtomicBoolean(false); OpenDatabaseHook hook = transaction -> called.set(true); context.checking(new DbExpectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); oneOf(db).open(dbKey, lifecycleManager); will(returnValue(false)); oneOf(db).transaction(with(false), withDbRunnable(txn)); @@ -51,4 +59,26 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase { assertEquals(SUCCESS, lifecycleManager.startServices(dbKey)); assertTrue(called.get()); } + + @Test + public void testStartupFailsIfClockIsUnreasonablyBehind() { + + context.checking(new DbExpectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(MIN_REASONABLE_TIME_MS - 1)); + }}); + + assertEquals(CLOCK_ERROR, lifecycleManager.startServices(dbKey)); + } + + @Test + public void testStartupFailsIfClockIsUnreasonablyAhead() { + + context.checking(new DbExpectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(MAX_REASONABLE_TIME_MS + 1)); + }}); + + assertEquals(CLOCK_ERROR, lifecycleManager.startServices(dbKey)); + } } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTest.java new file mode 100644 index 000000000..b924beb5f --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTest.java @@ -0,0 +1,168 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.event.EventListener; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.Identity; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.file.RemovableDriveTask; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.sync.event.MessageStateChangedEvent; +import org.briarproject.bramble.test.BrambleTestCase; +import org.briarproject.bramble.test.TestDatabaseConfigModule; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.concurrent.CountDownLatch; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.briarproject.bramble.api.plugin.file.FileConstants.PROP_PATH; +import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED; +import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory; +import static org.briarproject.bramble.test.TestUtils.getSecretKey; +import static org.briarproject.bramble.test.TestUtils.getTestDirectory; +import static org.junit.Assert.assertTrue; + +public class RemovableDriveIntegrationTest extends BrambleTestCase { + + private static final int TIMEOUT_MS = 5_000; + + private final File testDir = getTestDirectory(); + private final File aliceDir = new File(testDir, "alice"); + private final File bobDir = new File(testDir, "bob"); + + private final SecretKey rootKey = getSecretKey(); + private final long timestamp = System.currentTimeMillis(); + + private RemovableDriveIntegrationTestComponent alice, bob; + + @Before + public void setUp() { + assertTrue(testDir.mkdirs()); + alice = DaggerRemovableDriveIntegrationTestComponent.builder() + .testDatabaseConfigModule( + new TestDatabaseConfigModule(aliceDir)).build(); + RemovableDriveIntegrationTestComponent.Helper + .injectEagerSingletons(alice); + bob = DaggerRemovableDriveIntegrationTestComponent.builder() + .testDatabaseConfigModule( + new TestDatabaseConfigModule(bobDir)).build(); + RemovableDriveIntegrationTestComponent.Helper + .injectEagerSingletons(bob); + } + + @Test + public void testWriteAndRead() throws Exception { + // Create the identities + Identity aliceIdentity = + alice.getIdentityManager().createIdentity("Alice"); + Identity bobIdentity = bob.getIdentityManager().createIdentity("Bob"); + // Set up the devices and get the contact IDs + ContactId bobId = setUp(alice, aliceIdentity, + bobIdentity.getLocalAuthor(), true); + ContactId aliceId = setUp(bob, bobIdentity, + aliceIdentity.getLocalAuthor(), false); + // Sync Alice's client versions and transport properties + read(bob, write(alice, bobId), 2); + // Sync Bob's client versions and transport properties + read(alice, write(bob, aliceId), 2); + } + + private ContactId setUp(RemovableDriveIntegrationTestComponent device, + Identity local, Author remote, boolean alice) throws Exception { + // Add an identity for the user + IdentityManager identityManager = device.getIdentityManager(); + identityManager.registerIdentity(local); + // Start the lifecycle manager + LifecycleManager lifecycleManager = device.getLifecycleManager(); + lifecycleManager.startServices(getSecretKey()); + lifecycleManager.waitForStartup(); + // Add the other user as a contact + ContactManager contactManager = device.getContactManager(); + return contactManager.addContact(remote, local.getId(), rootKey, + timestamp, alice, true, true); + } + + @SuppressWarnings("SameParameterValue") + private void read(RemovableDriveIntegrationTestComponent device, + File file, int deliveries) throws Exception { + // Listen for message deliveries + MessageDeliveryListener listener = + new MessageDeliveryListener(deliveries); + device.getEventBus().addListener(listener); + // Read the incoming stream + TransportProperties p = new TransportProperties(); + p.put(PROP_PATH, file.getAbsolutePath()); + RemovableDriveTask reader = + device.getRemovableDriveManager().startReaderTask(p); + CountDownLatch disposedLatch = new CountDownLatch(1); + reader.addObserver(state -> { + if (state.isFinished()) disposedLatch.countDown(); + }); + // Wait for the messages to be delivered + assertTrue(listener.delivered.await(TIMEOUT_MS, MILLISECONDS)); + // Clean up the listener + device.getEventBus().removeListener(listener); + // Wait for the reader to be disposed + disposedLatch.await(TIMEOUT_MS, MILLISECONDS); + } + + private File write(RemovableDriveIntegrationTestComponent device, + ContactId contactId) throws Exception { + // Write the outgoing stream to a file + File file = File.createTempFile("sync", ".tmp", testDir); + TransportProperties p = new TransportProperties(); + p.put(PROP_PATH, file.getAbsolutePath()); + RemovableDriveTask writer = device.getRemovableDriveManager() + .startWriterTask(contactId, p); + CountDownLatch disposedLatch = new CountDownLatch(1); + writer.addObserver(state -> { + if (state.isFinished()) disposedLatch.countDown(); + }); + // Wait for the writer to be disposed + disposedLatch.await(TIMEOUT_MS, MILLISECONDS); + // Return the file containing the stream + return file; + } + + private void tearDown(RemovableDriveIntegrationTestComponent device) + throws Exception { + // Stop the lifecycle manager + LifecycleManager lifecycleManager = device.getLifecycleManager(); + lifecycleManager.stopServices(); + lifecycleManager.waitForShutdown(); + } + + @After + public void tearDown() throws Exception { + // Tear down the devices + tearDown(alice); + tearDown(bob); + deleteTestDirectory(testDir); + } + + @NotNullByDefault + private static class MessageDeliveryListener implements EventListener { + + private final CountDownLatch delivered; + + private MessageDeliveryListener(int deliveries) { + delivered = new CountDownLatch(deliveries); + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent m = (MessageStateChangedEvent) e; + if (m.getState().equals(DELIVERED)) delivered.countDown(); + } + } + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestComponent.java b/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestComponent.java new file mode 100644 index 000000000..385e140da --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestComponent.java @@ -0,0 +1,55 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.BrambleCoreEagerSingletons; +import org.briarproject.bramble.BrambleCoreModule; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.plugin.file.RemovableDriveManager; +import org.briarproject.bramble.battery.DefaultBatteryManagerModule; +import org.briarproject.bramble.event.DefaultEventExecutorModule; +import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule; +import org.briarproject.bramble.system.TimeTravelModule; +import org.briarproject.bramble.test.TestDatabaseConfigModule; +import org.briarproject.bramble.test.TestFeatureFlagModule; +import org.briarproject.bramble.test.TestSecureRandomModule; + +import javax.inject.Singleton; + +import dagger.Component; + +@Singleton +@Component(modules = { + BrambleCoreModule.class, + DefaultBatteryManagerModule.class, + DefaultEventExecutorModule.class, + DefaultWakefulIoExecutorModule.class, + TestDatabaseConfigModule.class, + TestFeatureFlagModule.class, + RemovableDriveIntegrationTestModule.class, + RemovableDriveModule.class, + TestSecureRandomModule.class, + TimeTravelModule.class +}) +interface RemovableDriveIntegrationTestComponent + extends BrambleCoreEagerSingletons { + + ContactManager getContactManager(); + + EventBus getEventBus(); + + IdentityManager getIdentityManager(); + + LifecycleManager getLifecycleManager(); + + RemovableDriveManager getRemovableDriveManager(); + + class Helper { + + public static void injectEagerSingletons( + RemovableDriveIntegrationTestComponent c) { + BrambleCoreEagerSingletons.Helper.injectEagerSingletons(c); + } + } +} diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/DesktopPluginModule.java b/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestModule.java similarity index 52% rename from bramble-java/src/main/java/org/briarproject/bramble/plugin/DesktopPluginModule.java rename to bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestModule.java index 6a33b933a..5f3e19842 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/DesktopPluginModule.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestModule.java @@ -1,60 +1,53 @@ -package org.briarproject.bramble.plugin; +package org.briarproject.bramble.plugin.file; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.plugin.BluetoothConstants; -import org.briarproject.bramble.api.plugin.LanTcpConstants; import org.briarproject.bramble.api.plugin.PluginConfig; import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory; import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; -import org.briarproject.bramble.plugin.bluetooth.JavaBluetoothPluginFactory; -import org.briarproject.bramble.plugin.modem.ModemPluginFactory; -import org.briarproject.bramble.plugin.tcp.LanTcpPluginFactory; -import org.briarproject.bramble.plugin.tcp.WanTcpPluginFactory; import java.util.Collection; import java.util.List; import java.util.Map; +import javax.inject.Singleton; + import dagger.Module; import dagger.Provides; -import static java.util.Arrays.asList; import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; @Module -public class DesktopPluginModule extends PluginModule { +class RemovableDriveIntegrationTestModule { @Provides - PluginConfig getPluginConfig(JavaBluetoothPluginFactory bluetooth, - ModemPluginFactory modem, LanTcpPluginFactory lan, - WanTcpPluginFactory wan) { + @Singleton + PluginConfig providePluginConfig(RemovableDrivePluginFactory drive) { @NotNullByDefault PluginConfig pluginConfig = new PluginConfig() { @Override public Collection getDuplexFactories() { - return asList(bluetooth, modem, lan, wan); - } - - @Override - public Collection getSimplexFactories() { return emptyList(); } + @Override + public Collection getSimplexFactories() { + return singletonList(drive); + } + @Override public boolean shouldPoll() { - return true; + return false; } @Override public Map> getTransportPreferences() { - // Prefer LAN to Bluetooth - return singletonMap(BluetoothConstants.ID, - singletonList(LanTcpConstants.ID)); + return emptyMap(); } + }; return pluginConfig; } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/properties/TransportPropertyManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/properties/TransportPropertyManagerImplTest.java index 9073917a2..0f9b63663 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/properties/TransportPropertyManagerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/properties/TransportPropertyManagerImplTest.java @@ -43,6 +43,7 @@ import static org.briarproject.bramble.api.properties.TransportPropertyConstants import static org.briarproject.bramble.api.properties.TransportPropertyManager.CLIENT_ID; import static org.briarproject.bramble.api.properties.TransportPropertyManager.MAJOR_VERSION; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE; import static org.briarproject.bramble.test.TestUtils.getContact; import static org.briarproject.bramble.test.TestUtils.getGroup; import static org.briarproject.bramble.test.TestUtils.getMessage; @@ -230,7 +231,8 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase { }}); TransportPropertyManagerImpl t = createInstance(); - assertFalse(t.incomingMessage(txn, message, meta)); + assertEquals(ACCEPT_DO_NOT_SHARE, + t.incomingMessage(txn, message, meta)); assertTrue(hasEvent(txn, RemoteTransportPropertiesUpdatedEvent.class)); } @@ -269,7 +271,8 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase { }}); TransportPropertyManagerImpl t = createInstance(); - assertFalse(t.incomingMessage(txn, message, meta)); + assertEquals(ACCEPT_DO_NOT_SHARE, + t.incomingMessage(txn, message, meta)); assertTrue(hasEvent(txn, RemoteTransportPropertiesUpdatedEvent.class)); } @@ -308,7 +311,8 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase { }}); TransportPropertyManagerImpl t = createInstance(); - assertFalse(t.incomingMessage(txn, message, meta)); + assertEquals(ACCEPT_DO_NOT_SHARE, + t.incomingMessage(txn, message, meta)); assertFalse(hasEvent(txn, RemoteTransportPropertiesUpdatedEvent.class)); } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/sync/SimplexOutgoingSessionTest.java b/bramble-core/src/test/java/org/briarproject/bramble/sync/SimplexOutgoingSessionTest.java index df8dc7b87..9bcf7cc3f 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/sync/SimplexOutgoingSessionTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/sync/SimplexOutgoingSessionTest.java @@ -17,9 +17,14 @@ import org.briarproject.bramble.test.DbExpectations; import org.briarproject.bramble.test.ImmediateExecutor; import org.junit.Test; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.concurrent.Executor; +import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; +import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES; +import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS; import static org.briarproject.bramble.test.TestUtils.getContactId; import static org.briarproject.bramble.test.TestUtils.getMessage; @@ -39,14 +44,19 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase { private final Executor dbExecutor = new ImmediateExecutor(); private final ContactId contactId = getContactId(); private final TransportId transportId = getTransportId(); - private final Message message = getMessage(new GroupId(getRandomId())); - private final MessageId messageId = message.getId(); + private final Ack ack = + new Ack(singletonList(new MessageId(getRandomId()))); + private final Message message = getMessage(new GroupId(getRandomId()), + MAX_MESSAGE_BODY_LENGTH); + private final Message message1 = getMessage(new GroupId(getRandomId()), + MAX_MESSAGE_BODY_LENGTH); @Test public void testNothingToSend() throws Exception { SimplexOutgoingSession session = new SimplexOutgoingSession(db, dbExecutor, eventBus, contactId, transportId, MAX_LATENCY, - streamWriter, recordWriter); + false, streamWriter, recordWriter); + Transaction noAckTxn = new Transaction(null, false); Transaction noMsgTxn = new Transaction(null, false); @@ -63,8 +73,8 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase { // No messages to send oneOf(db).transactionWithNullableResult(with(false), withNullableDbCallable(noMsgTxn)); - oneOf(db).generateBatch(with(noMsgTxn), with(contactId), - with(any(int.class)), with(MAX_LATENCY)); + oneOf(db).generateBatch(noMsgTxn, contactId, + MAX_RECORD_PAYLOAD_BYTES, MAX_LATENCY); will(returnValue(null)); // Send the end of stream marker oneOf(streamWriter).sendEndOfStream(); @@ -76,11 +86,44 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase { } @Test - public void testSomethingToSend() throws Exception { - Ack ack = new Ack(singletonList(messageId)); + public void testNothingToSendEagerly() throws Exception { SimplexOutgoingSession session = new SimplexOutgoingSession(db, dbExecutor, eventBus, contactId, transportId, MAX_LATENCY, - streamWriter, recordWriter); + true, streamWriter, recordWriter); + + Transaction noAckTxn = new Transaction(null, false); + Transaction noIdsTxn = new Transaction(null, true); + + context.checking(new DbExpectations() {{ + // Add listener + oneOf(eventBus).addListener(session); + // Send the protocol versions + oneOf(recordWriter).writeVersions(with(any(Versions.class))); + // No acks to send + oneOf(db).transactionWithNullableResult(with(false), + withNullableDbCallable(noAckTxn)); + oneOf(db).generateAck(noAckTxn, contactId, MAX_MESSAGE_IDS); + will(returnValue(null)); + // No messages to send + oneOf(db).transactionWithResult(with(true), + withDbCallable(noIdsTxn)); + oneOf(db).getUnackedMessagesToSend(noIdsTxn, contactId); + will(returnValue(emptyMap())); + // Send the end of stream marker + oneOf(streamWriter).sendEndOfStream(); + // Remove listener + oneOf(eventBus).removeListener(session); + }}); + + session.run(); + } + + @Test + public void testSomethingToSend() throws Exception { + SimplexOutgoingSession session = new SimplexOutgoingSession(db, + dbExecutor, eventBus, contactId, transportId, MAX_LATENCY, + false, streamWriter, recordWriter); + Transaction ackTxn = new Transaction(null, false); Transaction noAckTxn = new Transaction(null, false); Transaction msgTxn = new Transaction(null, false); @@ -100,8 +143,8 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase { // One message to send oneOf(db).transactionWithNullableResult(with(false), withNullableDbCallable(msgTxn)); - oneOf(db).generateBatch(with(msgTxn), with(contactId), - with(any(int.class)), with(MAX_LATENCY)); + oneOf(db).generateBatch(msgTxn, contactId, + MAX_RECORD_PAYLOAD_BYTES, MAX_LATENCY); will(returnValue(singletonList(message))); oneOf(recordWriter).writeMessage(message); // No more acks @@ -112,8 +155,8 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase { // No more messages oneOf(db).transactionWithNullableResult(with(false), withNullableDbCallable(noMsgTxn)); - oneOf(db).generateBatch(with(noMsgTxn), with(contactId), - with(any(int.class)), with(MAX_LATENCY)); + oneOf(db).generateBatch(noMsgTxn, contactId, + MAX_RECORD_PAYLOAD_BYTES, MAX_LATENCY); will(returnValue(null)); // Send the end of stream marker oneOf(streamWriter).sendEndOfStream(); @@ -123,4 +166,63 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase { session.run(); } + + @Test + public void testSomethingToSendEagerly() throws Exception { + SimplexOutgoingSession session = new SimplexOutgoingSession(db, + dbExecutor, eventBus, contactId, transportId, MAX_LATENCY, + true, streamWriter, recordWriter); + + Map unacked = new LinkedHashMap<>(); + unacked.put(message.getId(), message.getRawLength()); + unacked.put(message1.getId(), message1.getRawLength()); + + Transaction ackTxn = new Transaction(null, false); + Transaction noAckTxn = new Transaction(null, false); + Transaction idsTxn = new Transaction(null, true); + Transaction msgTxn = new Transaction(null, false); + Transaction msgTxn1 = new Transaction(null, false); + + context.checking(new DbExpectations() {{ + // Add listener + oneOf(eventBus).addListener(session); + // Send the protocol versions + oneOf(recordWriter).writeVersions(with(any(Versions.class))); + // One ack to send + oneOf(db).transactionWithNullableResult(with(false), + withNullableDbCallable(ackTxn)); + oneOf(db).generateAck(ackTxn, contactId, MAX_MESSAGE_IDS); + will(returnValue(ack)); + oneOf(recordWriter).writeAck(ack); + // No more acks + oneOf(db).transactionWithNullableResult(with(false), + withNullableDbCallable(noAckTxn)); + oneOf(db).generateAck(noAckTxn, contactId, MAX_MESSAGE_IDS); + will(returnValue(null)); + // Two messages to send + oneOf(db).transactionWithResult(with(true), withDbCallable(idsTxn)); + oneOf(db).getUnackedMessagesToSend(idsTxn, contactId); + will(returnValue(unacked)); + // Send the first message + oneOf(db).transactionWithResult(with(false), + withDbCallable(msgTxn)); + oneOf(db).generateBatch(msgTxn, contactId, + singletonList(message.getId()), MAX_LATENCY); + will(returnValue(singletonList(message))); + oneOf(recordWriter).writeMessage(message); + // Send the second message + oneOf(db).transactionWithResult(with(false), + withDbCallable(msgTxn1)); + oneOf(db).generateBatch(msgTxn1, contactId, + singletonList(message1.getId()), MAX_LATENCY); + will(returnValue(singletonList(message1))); + oneOf(recordWriter).writeMessage(message1); + // Send the end of stream marker + oneOf(streamWriter).sendEndOfStream(); + // Remove listener + oneOf(eventBus).removeListener(session); + }}); + + session.run(); + } } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/sync/validation/ValidationManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/sync/validation/ValidationManagerImplTest.java index 09018c36e..fea01c120 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/sync/validation/ValidationManagerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/sync/validation/ValidationManagerImplTest.java @@ -31,6 +31,8 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_SHARE; import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED; import static org.briarproject.bramble.api.sync.validation.MessageState.INVALID; import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING; @@ -111,7 +113,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase { oneOf(db).mergeMessageMetadata(txn1, messageId, metadata); // Deliver the first message oneOf(hook).incomingMessage(txn1, message, metadata); - will(returnValue(false)); + will(returnValue(ACCEPT_DO_NOT_SHARE)); oneOf(db).setMessageState(txn1, messageId, DELIVERED); // Get any pending dependents oneOf(db).getMessageDependents(txn1, messageId); @@ -167,7 +169,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase { will(returnValue(new Metadata())); // Deliver the message oneOf(hook).incomingMessage(txn, message, metadata); - will(returnValue(false)); + will(returnValue(ACCEPT_DO_NOT_SHARE)); oneOf(db).setMessageState(txn, messageId, DELIVERED); // Get any pending dependents oneOf(db).getMessageDependents(txn, messageId); @@ -187,7 +189,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase { will(returnValue(metadata)); // Deliver the dependent oneOf(hook).incomingMessage(txn1, message2, metadata); - will(returnValue(false)); + will(returnValue(ACCEPT_DO_NOT_SHARE)); oneOf(db).setMessageState(txn1, messageId2, DELIVERED); // Get any pending dependents oneOf(db).getMessageDependents(txn1, messageId2); @@ -247,7 +249,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase { oneOf(db).mergeMessageMetadata(txn1, messageId, metadata); // Deliver the message oneOf(hook).incomingMessage(txn1, message, metadata); - will(returnValue(true)); + will(returnValue(ACCEPT_SHARE)); oneOf(db).setMessageState(txn1, messageId, DELIVERED); // Get any pending dependents oneOf(db).getMessageDependents(txn1, messageId); @@ -367,7 +369,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase { oneOf(db).mergeMessageMetadata(txn1, messageId, metadata); // Deliver the message oneOf(hook).incomingMessage(txn1, message, metadata); - will(returnValue(false)); + will(returnValue(ACCEPT_DO_NOT_SHARE)); oneOf(db).setMessageState(txn1, messageId, DELIVERED); // Get any pending dependents oneOf(db).getMessageDependents(txn1, messageId); @@ -432,7 +434,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase { oneOf(db).mergeMessageMetadata(txn1, messageId, metadata); // Deliver the message oneOf(hook).incomingMessage(txn1, message, metadata); - will(returnValue(false)); + will(returnValue(ACCEPT_DO_NOT_SHARE)); oneOf(db).setMessageState(txn1, messageId, DELIVERED); // Get any pending dependents oneOf(db).getMessageDependents(txn1, messageId); @@ -602,7 +604,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase { oneOf(db).mergeMessageMetadata(txn1, messageId, metadata); // Deliver the message oneOf(hook).incomingMessage(txn1, message, metadata); - will(returnValue(false)); + will(returnValue(ACCEPT_DO_NOT_SHARE)); oneOf(db).setMessageState(txn1, messageId, DELIVERED); // The message has two pending dependents: 1 and 2 oneOf(db).getMessageDependents(txn1, messageId); @@ -622,7 +624,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase { will(returnValue(metadata)); // Deliver message 1 oneOf(hook).incomingMessage(txn2, message1, metadata); - will(returnValue(false)); + will(returnValue(ACCEPT_DO_NOT_SHARE)); oneOf(db).setMessageState(txn2, messageId1, DELIVERED); // Message 1 has one pending dependent: 3 oneOf(db).getMessageDependents(txn2, messageId1); @@ -642,7 +644,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase { will(returnValue(metadata)); // Deliver message 2 oneOf(hook).incomingMessage(txn3, message2, metadata); - will(returnValue(false)); + will(returnValue(ACCEPT_DO_NOT_SHARE)); oneOf(db).setMessageState(txn3, messageId2, DELIVERED); // Message 2 has one pending dependent: 3 (same dependent as 1) oneOf(db).getMessageDependents(txn3, messageId2); @@ -662,6 +664,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase { will(returnValue(metadata)); // Deliver message 3 oneOf(hook).incomingMessage(txn4, message3, metadata); + will(returnValue(ACCEPT_DO_NOT_SHARE)); oneOf(db).setMessageState(txn4, messageId3, DELIVERED); // Message 3 has one pending dependent: 4 oneOf(db).getMessageDependents(txn4, messageId3); @@ -685,7 +688,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase { will(returnValue(metadata)); // Deliver message 4 oneOf(hook).incomingMessage(txn6, message4, metadata); - will(returnValue(false)); + will(returnValue(ACCEPT_DO_NOT_SHARE)); oneOf(db).setMessageState(txn6, messageId4, DELIVERED); // Message 4 has no pending dependents oneOf(db).getMessageDependents(txn6, messageId4); @@ -717,7 +720,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase { oneOf(db).mergeMessageMetadata(txn1, messageId, metadata); // Deliver the message oneOf(hook).incomingMessage(txn1, message, metadata); - will(returnValue(false)); + will(returnValue(ACCEPT_DO_NOT_SHARE)); oneOf(db).setMessageState(txn1, messageId, DELIVERED); // Get any pending dependents oneOf(db).getMessageDependents(txn1, messageId); diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java index cac2d562b..b735dab7d 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java @@ -1,53 +1,22 @@ package org.briarproject.bramble.test; -import org.briarproject.bramble.api.FeatureFlags; import org.briarproject.bramble.battery.DefaultBatteryManagerModule; import org.briarproject.bramble.event.DefaultEventExecutorModule; import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule; import org.briarproject.bramble.system.TimeTravelModule; import dagger.Module; -import dagger.Provides; @Module(includes = { DefaultBatteryManagerModule.class, DefaultEventExecutorModule.class, DefaultWakefulIoExecutorModule.class, TestDatabaseConfigModule.class, + TestFeatureFlagModule.class, TestPluginConfigModule.class, TestSecureRandomModule.class, TimeTravelModule.class }) public class BrambleCoreIntegrationTestModule { - @Provides - FeatureFlags provideFeatureFlags() { - return new FeatureFlags() { - - @Override - public boolean shouldEnableImageAttachments() { - return true; - } - - @Override - public boolean shouldEnableProfilePictures() { - return true; - } - - @Override - public boolean shouldEnableDisappearingMessages() { - return true; - } - - @Override - public boolean shouldEnableConnectViaBluetooth() { - return true; - } - - @Override - public boolean shouldEnableShareAppViaOfflineHotspot() { - return true; - } - }; - } } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTest.java new file mode 100644 index 000000000..68e9fe39b --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTest.java @@ -0,0 +1,336 @@ +package org.briarproject.bramble.test; + +import net.jodah.concurrentunit.Waiter; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.data.BdfStringUtils; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventListener; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.sync.event.MessageStateChangedEvent; +import org.briarproject.bramble.api.sync.event.MessagesAckedEvent; +import org.briarproject.bramble.api.sync.event.MessagesSentEvent; +import org.junit.After; +import org.junit.Before; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +import javax.annotation.Nonnull; + +import static java.util.concurrent.Executors.newSingleThreadExecutor; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED; +import static org.briarproject.bramble.api.sync.validation.MessageState.INVALID; +import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING; +import static org.briarproject.bramble.test.TestPluginConfigModule.SIMPLEX_TRANSPORT_ID; +import static org.briarproject.bramble.util.LogUtils.logException; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public abstract class BrambleIntegrationTest + extends BrambleTestCase { + + private static final Logger LOG = + getLogger(BrambleIntegrationTest.class.getName()); + + private static final boolean DEBUG = false; + + protected final static int TIMEOUT = 15000; + + // objects accessed from background threads need to be volatile + private volatile Waiter validationWaiter; + private volatile Waiter deliveryWaiter; + private volatile Waiter ackWaiter; + private volatile boolean expectAck = false; + + private final Semaphore messageSemaphore = new Semaphore(0); + private final AtomicInteger deliveryCounter = new AtomicInteger(0); + private final AtomicInteger validationCounter = new AtomicInteger(0); + private final AtomicInteger ackCounter = new AtomicInteger(0); + + protected final File testDir = TestUtils.getTestDirectory(); + + @Before + public void setUp() throws Exception { + assertTrue(testDir.mkdirs()); + + // initialize waiters fresh for each test + validationWaiter = new Waiter(); + deliveryWaiter = new Waiter(); + ackWaiter = new Waiter(); + deliveryCounter.set(0); + validationCounter.set(0); + ackCounter.set(0); + } + + @After + public void tearDown() throws Exception { + TestUtils.deleteTestDirectory(testDir); + } + + protected void addEventListener(C c) { + c.getEventBus().addListener(new Listener(c)); + } + + private class Listener implements EventListener { + + private final ClientHelper clientHelper; + private final Executor executor; + + private Listener(C c) { + clientHelper = c.getClientHelper(); + executor = newSingleThreadExecutor(); + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent event = (MessageStateChangedEvent) e; + if (!event.isLocal()) { + if (event.getState() == DELIVERED) { + LOG.info("Delivered new message " + + event.getMessageId()); + deliveryCounter.addAndGet(1); + loadAndLogMessage(event.getMessageId()); + deliveryWaiter.resume(); + } else if (event.getState() == INVALID || + event.getState() == PENDING) { + LOG.info("Validated new " + event.getState().name() + + " message " + event.getMessageId()); + validationCounter.addAndGet(1); + loadAndLogMessage(event.getMessageId()); + validationWaiter.resume(); + } + } + } else if (e instanceof MessagesAckedEvent && expectAck) { + MessagesAckedEvent event = (MessagesAckedEvent) e; + ackCounter.addAndGet(event.getMessageIds().size()); + for (MessageId m : event.getMessageIds()) { + loadAndLogMessage(m); + ackWaiter.resume(); + } + } + } + + private void loadAndLogMessage(MessageId id) { + executor.execute(() -> { + if (DEBUG) { + try { + BdfList body = clientHelper.getMessageAsList(id); + LOG.info("Contents of " + id + ":\n" + + BdfStringUtils.toString(body)); + } catch (DbException | FormatException e) { + logException(LOG, WARNING, e); + } + } + messageSemaphore.release(); + }); + } + } + + + protected void syncMessage(BrambleIntegrationTestComponent fromComponent, + BrambleIntegrationTestComponent toComponent, ContactId toId, + TransportId transportId, int num, boolean valid) throws Exception { + syncMessage(fromComponent, toComponent, toId, transportId, num, 0, + valid ? 0 : num, valid ? num : 0); + } + + protected void syncMessage(BrambleIntegrationTestComponent fromComponent, + BrambleIntegrationTestComponent toComponent, ContactId toId, + int num, boolean valid) throws Exception { + syncMessage(fromComponent, toComponent, toId, num, 0, valid ? 0 : num, + valid ? num : 0); + } + + protected void syncMessage(BrambleIntegrationTestComponent fromComponent, + BrambleIntegrationTestComponent toComponent, ContactId toId, + int numNew, int numDupes, int numPendingOrInvalid, int numDelivered) + throws Exception { + syncMessage(fromComponent, toComponent, toId, SIMPLEX_TRANSPORT_ID, + numNew, numDupes, numPendingOrInvalid, numDelivered); + } + + protected void syncMessage(BrambleIntegrationTestComponent fromComponent, + BrambleIntegrationTestComponent toComponent, ContactId toId, + TransportId transportId, int numNew, int numDupes, + int numPendingOrInvalid, int numDelivered) throws Exception { + // Debug output + String from = + fromComponent.getIdentityManager().getLocalAuthor().getName(); + String to = toComponent.getIdentityManager().getLocalAuthor().getName(); + LOG.info("TEST: Sending " + (numNew + numDupes) + " message(s) from " + + from + " to " + to); + + // Listen for messages being sent + waitForEvents(fromComponent); + SendListener sendListener = new SendListener(); + fromComponent.getEventBus().addListener(sendListener); + + // Write the messages to a transport stream + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestTransportConnectionWriter writer = + new TestTransportConnectionWriter(out, false); + fromComponent.getConnectionManager().manageOutgoingConnection(toId, + transportId, writer); + writer.getDisposedLatch().await(TIMEOUT, MILLISECONDS); + + // Check that the expected number of messages were sent + waitForEvents(fromComponent); + fromComponent.getEventBus().removeListener(sendListener); + assertEquals("Messages sent", numNew + numDupes, + sendListener.sent.size()); + + // Read the messages from the transport stream + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + TestTransportConnectionReader reader = + new TestTransportConnectionReader(in); + toComponent.getConnectionManager().manageIncomingConnection( + transportId, reader); + + if (numPendingOrInvalid > 0) { + validationWaiter.await(TIMEOUT, numPendingOrInvalid); + } + assertEquals("Messages validated", numPendingOrInvalid, + validationCounter.getAndSet(0)); + + if (numDelivered > 0) { + deliveryWaiter.await(TIMEOUT, numDelivered); + } + assertEquals("Messages delivered", numDelivered, + deliveryCounter.getAndSet(0)); + + try { + messageSemaphore.tryAcquire(numNew, TIMEOUT, MILLISECONDS); + } catch (InterruptedException e) { + LOG.info("Interrupted while waiting for messages"); + Thread.currentThread().interrupt(); + fail(); + } + } + + protected void awaitPendingMessageDelivery(int num) + throws TimeoutException { + deliveryWaiter.await(TIMEOUT, num); + assertEquals("Messages delivered", num, deliveryCounter.getAndSet(0)); + + try { + messageSemaphore.tryAcquire(num, TIMEOUT, MILLISECONDS); + } catch (InterruptedException e) { + LOG.info("Interrupted while waiting for messages"); + Thread.currentThread().interrupt(); + fail(); + } + } + + protected void sendAcks(BrambleIntegrationTestComponent fromComponent, + BrambleIntegrationTestComponent toComponent, ContactId toId, + int num) throws Exception { + // Debug output + String from = + fromComponent.getIdentityManager().getLocalAuthor().getName(); + String to = toComponent.getIdentityManager().getLocalAuthor().getName(); + LOG.info("TEST: Sending " + num + " ACKs from " + from + " to " + to); + + expectAck = true; + + // Listen for messages being sent (none should be sent) + waitForEvents(fromComponent); + SendListener sendListener = new SendListener(); + fromComponent.getEventBus().addListener(sendListener); + + // start outgoing connection + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestTransportConnectionWriter writer = + new TestTransportConnectionWriter(out, false); + fromComponent.getConnectionManager().manageOutgoingConnection(toId, + SIMPLEX_TRANSPORT_ID, writer); + writer.getDisposedLatch().await(TIMEOUT, MILLISECONDS); + + // Check that no messages were sent + waitForEvents(fromComponent); + fromComponent.getEventBus().removeListener(sendListener); + assertEquals("Messages sent", 0, sendListener.sent.size()); + + // handle incoming connection + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + TestTransportConnectionReader reader = + new TestTransportConnectionReader(in); + toComponent.getConnectionManager().manageIncomingConnection( + SIMPLEX_TRANSPORT_ID, reader); + + ackWaiter.await(TIMEOUT, num); + assertEquals("ACKs delivered", num, ackCounter.getAndSet(0)); + assertEquals("No messages delivered", 0, deliveryCounter.get()); + try { + messageSemaphore.tryAcquire(num, TIMEOUT, MILLISECONDS); + } catch (InterruptedException e) { + LOG.info("Interrupted while waiting for messages"); + Thread.currentThread().interrupt(); + fail(); + } finally { + expectAck = false; + } + } + + /** + * Broadcasts a marker event and waits for it to be delivered, which + * indicates that all previously broadcast events have been delivered. + */ + public static void waitForEvents(BrambleIntegrationTestComponent component) + throws Exception { + CountDownLatch latch = new CountDownLatch(1); + MarkerEvent marker = new MarkerEvent(); + EventBus eventBus = component.getEventBus(); + eventBus.addListener(new EventListener() { + @Override + public void eventOccurred(@Nonnull Event e) { + if (e == marker) { + latch.countDown(); + eventBus.removeListener(this); + } + } + }); + eventBus.broadcast(marker); + if (!latch.await(1, MINUTES)) fail(); + } + + private static class MarkerEvent extends Event { + } + + private static class SendListener implements EventListener { + + private final Set sent = new HashSet<>(); + + @Override + public void eventOccurred(Event e) { + if (e instanceof MessagesSentEvent) { + sent.addAll(((MessagesSentEvent) e).getMessageIds()); + } + } + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTestComponent.java b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTestComponent.java new file mode 100644 index 000000000..c7b0d80c5 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleIntegrationTestComponent.java @@ -0,0 +1,30 @@ +package org.briarproject.bramble.test; + +import org.briarproject.bramble.BrambleCoreIntegrationTestEagerSingletons; +import org.briarproject.bramble.BrambleCoreModule; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.connection.ConnectionManager; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.identity.IdentityManager; + +import javax.inject.Singleton; + +import dagger.Component; + +@Singleton +@Component(modules = { + BrambleCoreIntegrationTestModule.class, + BrambleCoreModule.class +}) +public interface BrambleIntegrationTestComponent + extends BrambleCoreIntegrationTestEagerSingletons { + + IdentityManager getIdentityManager(); + + EventBus getEventBus(); + + ConnectionManager getConnectionManager(); + + ClientHelper getClientHelper(); + +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/TestDuplexTransportConnection.java b/bramble-core/src/test/java/org/briarproject/bramble/test/TestDuplexTransportConnection.java index 0ed6f9045..83ec8129e 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/test/TestDuplexTransportConnection.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/TestDuplexTransportConnection.java @@ -25,7 +25,7 @@ public class TestDuplexTransportConnection @SuppressWarnings("WeakerAccess") public TestDuplexTransportConnection(InputStream in, OutputStream out) { reader = new TestTransportConnectionReader(in); - writer = new TestTransportConnectionWriter(out); + writer = new TestTransportConnectionWriter(out, false); } @Override diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/TestFeatureFlagModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/TestFeatureFlagModule.java new file mode 100644 index 000000000..5a076d178 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/TestFeatureFlagModule.java @@ -0,0 +1,44 @@ +package org.briarproject.bramble.test; + +import org.briarproject.bramble.api.FeatureFlags; + +import dagger.Module; +import dagger.Provides; + +@Module +public class TestFeatureFlagModule { + @Provides + FeatureFlags provideFeatureFlags() { + return new FeatureFlags() { + @Override + public boolean shouldEnableImageAttachments() { + return true; + } + + @Override + public boolean shouldEnableProfilePictures() { + return true; + } + + @Override + public boolean shouldEnableDisappearingMessages() { + return true; + } + + @Override + public boolean shouldEnableConnectViaBluetooth() { + return true; + } + + @Override + public boolean shouldEnableShareAppViaOfflineHotspot() { + return true; + } + + @Override + public boolean shouldEnableTransferData() { + return true; + } + }; + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/TestPluginConfigModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/TestPluginConfigModule.java index 4b3347884..efab0271e 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/test/TestPluginConfigModule.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/TestPluginConfigModule.java @@ -27,18 +27,30 @@ public class TestPluginConfigModule { public static final TransportId SIMPLEX_TRANSPORT_ID = getTransportId(); public static final TransportId DUPLEX_TRANSPORT_ID = getTransportId(); - public static final int MAX_LATENCY = 30_000; // 30 seconds + private static final int MAX_LATENCY = 30_000; // 30 seconds + + private final TransportId simplexTransportId, duplexTransportId; + + public TestPluginConfigModule() { + this(SIMPLEX_TRANSPORT_ID, DUPLEX_TRANSPORT_ID); + } + + public TestPluginConfigModule(TransportId simplexTransportId, + TransportId duplexTransportId) { + this.simplexTransportId = simplexTransportId; + this.duplexTransportId = duplexTransportId; + } @NotNullByDefault private final SimplexPluginFactory simplex = new SimplexPluginFactory() { @Override public TransportId getId() { - return SIMPLEX_TRANSPORT_ID; + return simplexTransportId; } @Override - public int getMaxLatency() { + public long getMaxLatency() { return MAX_LATENCY; } @@ -54,11 +66,11 @@ public class TestPluginConfigModule { @Override public TransportId getId() { - return DUPLEX_TRANSPORT_ID; + return duplexTransportId; } @Override - public int getMaxLatency() { + public long getMaxLatency() { return MAX_LATENCY; } @@ -70,7 +82,7 @@ public class TestPluginConfigModule { }; @Provides - PluginConfig providePluginConfig() { + public PluginConfig providePluginConfig() { @NotNullByDefault PluginConfig pluginConfig = new PluginConfig() { diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/TestTransportConnectionWriter.java b/bramble-core/src/test/java/org/briarproject/bramble/test/TestTransportConnectionWriter.java index 238208207..9c9cc48fd 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/test/TestTransportConnectionWriter.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/TestTransportConnectionWriter.java @@ -15,10 +15,13 @@ public class TestTransportConnectionWriter implements TransportConnectionWriter { private final OutputStream out; + private final boolean lossyAndCheap; private final CountDownLatch disposed = new CountDownLatch(1); - public TestTransportConnectionWriter(OutputStream out) { + public TestTransportConnectionWriter(OutputStream out, + boolean lossyAndCheap) { this.out = out; + this.lossyAndCheap = lossyAndCheap; } public CountDownLatch getDisposedLatch() { @@ -26,7 +29,7 @@ public class TestTransportConnectionWriter } @Override - public int getMaxLatency() { + public long getMaxLatency() { return 30_000; } @@ -35,6 +38,11 @@ public class TestTransportConnectionWriter return 60_000; } + @Override + public boolean isLossyAndCheap() { + return lossyAndCheap; + } + @Override public OutputStream getOutputStream() { return out; diff --git a/bramble-core/src/test/java/org/briarproject/bramble/transport/KeyManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/transport/KeyManagerImplTest.java index 142f06538..932653507 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/transport/KeyManagerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/transport/KeyManagerImplTest.java @@ -25,6 +25,7 @@ import java.util.Collection; import java.util.Map; import java.util.Random; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH; @@ -71,8 +72,7 @@ public class KeyManagerImplTest extends BrambleMockTestCase { private final SecretKey rootKey = getSecretKey(); private final Random random = new Random(); - private final KeyManagerImpl keyManager = new KeyManagerImpl(db, executor, - pluginConfig, transportKeyManagerFactory, transportCrypto); + private KeyManagerImpl keyManager; @Before public void testStartService() throws Exception { @@ -81,20 +81,27 @@ public class KeyManagerImplTest extends BrambleMockTestCase { context.mock(SimplexPluginFactory.class); Collection factories = singletonList(pluginFactory); - int maxLatency = 1337; + long maxLatency = 1337; - context.checking(new DbExpectations() {{ - oneOf(pluginConfig).getSimplexFactories(); + context.checking(new Expectations() {{ + allowing(pluginConfig).getSimplexFactories(); will(returnValue(factories)); - oneOf(pluginFactory).getId(); + allowing(pluginFactory).getId(); will(returnValue(transportId)); - oneOf(pluginFactory).getMaxLatency(); + allowing(pluginFactory).getMaxLatency(); will(returnValue(maxLatency)); - oneOf(db).addTransport(txn, transportId, maxLatency); + allowing(pluginConfig).getDuplexFactories(); + will(returnValue(emptyList())); oneOf(transportKeyManagerFactory) .createTransportKeyManager(transportId, maxLatency); will(returnValue(transportKeyManager)); - oneOf(pluginConfig).getDuplexFactories(); + }}); + + keyManager = new KeyManagerImpl(db, executor, + pluginConfig, transportCrypto, transportKeyManagerFactory); + + context.checking(new DbExpectations() {{ + oneOf(db).addTransport(txn, transportId, maxLatency); oneOf(db).transaction(with(false), withDbRunnable(txn)); oneOf(transportKeyManager).start(txn); }}); @@ -235,4 +242,37 @@ public class KeyManagerImplTest extends BrambleMockTestCase { keyManager.eventOccurred(event); executor.runUntilIdle(); } + + @Test + public void testAddMultipleRotationKeySets() throws Exception { + long timestamp = System.currentTimeMillis(); + boolean alice = random.nextBoolean(); + boolean active = random.nextBoolean(); + + context.checking(new Expectations() {{ + oneOf(transportKeyManager).addRotationKeys(txn, contactId, + rootKey, timestamp, alice, active); + will(returnValue(keySetId)); + }}); + + assertEquals(singletonMap(transportId, keySetId), + keyManager.addRotationKeys(txn, contactId, rootKey, timestamp, + alice, active)); + } + + @Test + public void testAddSingleRotationKeySet() throws Exception { + long timestamp = System.currentTimeMillis(); + boolean alice = random.nextBoolean(); + boolean active = random.nextBoolean(); + + context.checking(new Expectations() {{ + oneOf(transportKeyManager).addRotationKeys(txn, contactId, + rootKey, timestamp, alice, active); + will(returnValue(keySetId)); + }}); + + assertEquals(keySetId, keyManager.addRotationKeys(txn, contactId, + transportId, rootKey, timestamp, alice, active)); + } } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementIntegrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementIntegrationTest.java new file mode 100644 index 000000000..d16b6052c --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementIntegrationTest.java @@ -0,0 +1,418 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.BrambleCoreIntegrationTestEagerSingletons; +import org.briarproject.bramble.api.Pair; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.AuthorId; +import org.briarproject.bramble.api.identity.Identity; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.test.BrambleIntegrationTest; +import org.briarproject.bramble.test.TestDatabaseConfigModule; +import org.briarproject.bramble.test.TestPluginConfigModule; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.Map; + +import static org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager.CLIENT_ID; +import static org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager.MAJOR_VERSION; +import static org.briarproject.bramble.test.TestPluginConfigModule.DUPLEX_TRANSPORT_ID; +import static org.briarproject.bramble.test.TestPluginConfigModule.SIMPLEX_TRANSPORT_ID; +import static org.briarproject.bramble.test.TestUtils.getSecretKey; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_IS_SESSION; +import static org.briarproject.bramble.util.StringUtils.getRandomString; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class TransportKeyAgreementIntegrationTest + extends BrambleIntegrationTest { + + private final File aliceDir = new File(testDir, "alice"); + private final File bobDir = new File(testDir, "bob"); + private final SecretKey masterKey = getSecretKey(); + private final long timestamp = System.currentTimeMillis(); + private final TransportId newTransportId = + new TransportId(getRandomString(8)); + + private TransportKeyAgreementTestComponent alice, bob; + private Identity aliceIdentity, bobIdentity; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + // Create the devices + alice = createComponent(aliceDir, false); + bob = createComponent(bobDir, false); + + // Create identities + aliceIdentity = alice.getIdentityManager().createIdentity("Alice"); + bobIdentity = bob.getIdentityManager().createIdentity("Bob"); + + // Start both lifecycles + startLifecycle(alice, aliceIdentity); + startLifecycle(bob, bobIdentity); + } + + private TransportKeyAgreementTestComponent createComponent( + File dir, boolean useNewTransport) { + TestPluginConfigModule pluginConfigModule = useNewTransport ? + new TestPluginConfigModule(SIMPLEX_TRANSPORT_ID, newTransportId) + : new TestPluginConfigModule(); + TransportKeyAgreementTestComponent c = + DaggerTransportKeyAgreementTestComponent.builder() + .testDatabaseConfigModule( + new TestDatabaseConfigModule(dir)) + .testPluginConfigModule(pluginConfigModule) + .build(); + BrambleCoreIntegrationTestEagerSingletons.Helper + .injectEagerSingletons(c); + return c; + } + + private void startLifecycle( + TransportKeyAgreementTestComponent device, + Identity identity) throws Exception { + // Listen to message related events first to not miss early ones + addEventListener(device); + // Register identity before starting lifecycle + device.getIdentityManager().registerIdentity(identity); + // Start the lifecycle manager + LifecycleManager lifecycleManager = device.getLifecycleManager(); + lifecycleManager.startServices(masterKey); // re-using masterKey here + lifecycleManager.waitForStartup(); + } + + @After + @Override + public void tearDown() throws Exception { + tearDown(alice); + tearDown(bob); + super.tearDown(); + } + + private void tearDown(TransportKeyAgreementTestComponent device) + throws Exception { + // Stop the lifecycle manager + LifecycleManager lifecycleManager = device.getLifecycleManager(); + lifecycleManager.stopServices(); + lifecycleManager.waitForShutdown(); + } + + @Test + public void testBothAddTransportAtTheSameTime() throws Exception { + // Alice and Bob add each other. + Pair contactIds = addContacts(true); + ContactId aliceId = contactIds.getFirst(); + ContactId bobId = contactIds.getSecond(); + + // Alice and Bob restart and come back with the new transport. + alice = restartWithNewTransport(alice, aliceDir, aliceIdentity); + bob = restartWithNewTransport(bob, bobDir, bobIdentity); + + // They can still send via the old simplex, + // but not via the new duplex transport + assertTrue(alice.getKeyManager() + .canSendOutgoingStreams(bobId, SIMPLEX_TRANSPORT_ID)); + assertFalse(alice.getKeyManager() + .canSendOutgoingStreams(bobId, newTransportId)); + assertTrue(bob.getKeyManager() + .canSendOutgoingStreams(aliceId, SIMPLEX_TRANSPORT_ID)); + assertFalse(bob.getKeyManager() + .canSendOutgoingStreams(aliceId, newTransportId)); + + // Bobs has started a session and sends KEY message to Alice + syncMessage(bob, alice, aliceId, 1, true); + + // Alice now and sends her own KEY as well as her ACTIVATE message. + syncMessage(alice, bob, bobId, 2, true); + + // Bob can already send over the new transport while Alice still can't. + assertFalse(alice.getKeyManager() + .canSendOutgoingStreams(bobId, newTransportId)); + assertTrue(bob.getKeyManager() + .canSendOutgoingStreams(aliceId, newTransportId)); + + // Now Bob sends his ACTIVATE message to Alice. + syncMessage(bob, alice, aliceId, 1, true); + + // Now Alice can also send over the new transport. + assertTrue(alice.getKeyManager() + .canSendOutgoingStreams(bobId, newTransportId)); + assertTrue(bob.getKeyManager() + .canSendOutgoingStreams(aliceId, newTransportId)); + + // Ensure that private key is not stored anymore. + assertLocalKeyPairIsNull(alice, bobId); + assertLocalKeyPairIsNull(bob, aliceId); + + // Messages can be send over the new transport in both directions. + assertTransportMessageArrives(alice, bob, bobId, newTransportId); + assertTransportMessageArrives(bob, alice, aliceId, newTransportId); + } + + @Test + public void testAliceAddsTransportBeforeBob() throws Exception { + // Alice and Bob add each other. + Pair contactIds = addContacts(true); + ContactId aliceId = contactIds.getFirst(); + ContactId bobId = contactIds.getSecond(); + + // Alice restarts and comes back with the new transport. + alice = restartWithNewTransport(alice, aliceDir, aliceIdentity); + + // Alice can still send via the old simplex, + // but not via the new duplex transport + assertTrue(alice.getKeyManager() + .canSendOutgoingStreams(bobId, SIMPLEX_TRANSPORT_ID)); + assertFalse(alice.getKeyManager() + .canSendOutgoingStreams(bobId, newTransportId)); + + // Alice has started a session and sends KEY message to Bob + // which he can't read, as he doesn't support the new transport, yet. + syncMessage(alice, bob, bobId, 1, false); + + // Bob restarts and comes back with the new transport. + bob = restartWithNewTransport(bob, bobDir, bobIdentity); + + // Alice's pending KEY message now gets delivered async, so wait for it + awaitPendingMessageDelivery(1); + + // Bob now sends his own KEY as well as his ACTIVATE message. + syncMessage(bob, alice, aliceId, 2, true); + + // Alice can already send over the new transport while Bob still can't. + assertTrue(alice.getKeyManager() + .canSendOutgoingStreams(bobId, newTransportId)); + assertFalse(bob.getKeyManager() + .canSendOutgoingStreams(aliceId, newTransportId)); + + // Now Alice sends her ACTIVATE message to Bob. + syncMessage(alice, bob, bobId, 1, true); + + // Now Bob can also send over the new transport. + assertTrue(alice.getKeyManager() + .canSendOutgoingStreams(bobId, newTransportId)); + assertTrue(bob.getKeyManager() + .canSendOutgoingStreams(aliceId, newTransportId)); + + // Ensure that private key is not stored anymore. + assertLocalKeyPairIsNull(alice, bobId); + assertLocalKeyPairIsNull(bob, aliceId); + + // Messages can be send over the new transport in both directions. + assertTransportMessageArrives(alice, bob, bobId, newTransportId); + assertTransportMessageArrives(bob, alice, aliceId, newTransportId); + } + + @Test + public void testAliceAlreadyHasTransportWhenAddingBob() throws Exception { + // Alice restarts and comes back with the new transport. + alice = restartWithNewTransport(alice, aliceDir, aliceIdentity); + + // Alice and Bob add each other. + Pair contactIds = addContacts(false); + ContactId aliceId = contactIds.getFirst(); + ContactId bobId = contactIds.getSecond(); + + // Alice can still send via the old simplex, + // but not via the new duplex transport + assertTrue(alice.getKeyManager() + .canSendOutgoingStreams(bobId, SIMPLEX_TRANSPORT_ID)); + // Normally, Alice should not be able to send streams already. + // However, she does already derive keys for the transport. + // The UI checks RemovableDriveManager#isTransportSupportedByContact() + // in practice to prevent sending streams that Bob can't decrypt. + assertTrue(alice.getKeyManager() + .canSendOutgoingStreams(bobId, newTransportId)); + + // Bob restarts and comes back with the new transport. + bob = restartWithNewTransport(bob, bobDir, bobIdentity); + + // Bob sends his own KEY message. + syncMessage(bob, alice, aliceId, 1, true); + + // Alice can already send over the new transport while Bob still can't. + assertTrue(alice.getKeyManager() + .canSendOutgoingStreams(bobId, newTransportId)); + assertFalse(bob.getKeyManager() + .canSendOutgoingStreams(aliceId, newTransportId)); + + // Now Alice sends her KEY and her ACTIVATE message to Bob. + syncMessage(alice, bob, bobId, 2, true); + + // Now Bob can also send over the new transport. + assertTrue(alice.getKeyManager() + .canSendOutgoingStreams(bobId, newTransportId)); + assertTrue(bob.getKeyManager() + .canSendOutgoingStreams(aliceId, newTransportId)); + + // Ensure that private key is not stored anymore. + assertLocalKeyPairIsNull(alice, bobId); + assertLocalKeyPairIsNull(bob, aliceId); + + // Bobs still sends his ACTIVATE message. + syncMessage(bob, alice, aliceId, 1, true); + + // Messages can be send over the new transport in both directions. + assertTransportMessageArrives(alice, bob, bobId, newTransportId); + assertTransportMessageArrives(bob, alice, aliceId, newTransportId); + } + + @Test + public void testAliceActivatesKeysByIncomingMessage() throws Exception { + // Alice and Bob add each other. + Pair contactIds = addContacts(true); + ContactId aliceId = contactIds.getFirst(); + ContactId bobId = contactIds.getSecond(); + + // Alice and Bob restart and come back with the new transport. + alice = restartWithNewTransport(alice, aliceDir, aliceIdentity); + bob = restartWithNewTransport(bob, bobDir, bobIdentity); + + // They can still send via the old simplex, + // but not via the new duplex transport + assertTrue(alice.getKeyManager() + .canSendOutgoingStreams(bobId, SIMPLEX_TRANSPORT_ID)); + assertFalse(alice.getKeyManager() + .canSendOutgoingStreams(bobId, newTransportId)); + assertTrue(bob.getKeyManager() + .canSendOutgoingStreams(aliceId, SIMPLEX_TRANSPORT_ID)); + assertFalse(bob.getKeyManager() + .canSendOutgoingStreams(aliceId, newTransportId)); + + // Bobs has started a session and sends KEY message to Alice + syncMessage(bob, alice, aliceId, 1, true); + + // Alice now and sends her own KEY as well as her ACTIVATE message. + syncMessage(alice, bob, bobId, 2, true); + + // Bob can already send over the new transport while Alice still can't. + assertFalse(alice.getKeyManager() + .canSendOutgoingStreams(bobId, newTransportId)); + assertTrue(bob.getKeyManager() + .canSendOutgoingStreams(aliceId, newTransportId)); + + // Bob's database mysteriously loses the ACTIVATE message, + // so it won't be send to Alice. + Contact contact = bob.getContactManager().getContact(aliceId); + Group group = getContactGroup(bob, contact); + Map map = bob.getClientHelper() + .getMessageMetadataAsDictionary(group.getId()); + DatabaseComponent db = bob.getDatabaseComponent(); + for (Map.Entry e : map.entrySet()) { + if (e.getValue().getBoolean(MSG_KEY_IS_SESSION)) continue; + db.transaction(false, txn -> db.removeMessage(txn, e.getKey())); + } + + // Bob sends a message to Alice + assertTransportMessageArrives(bob, alice, aliceId, newTransportId); + + // Now without receiving the ACTIVATE, Alice can already send to Bob + assertTrue(alice.getKeyManager() + .canSendOutgoingStreams(bobId, newTransportId)); + assertTransportMessageArrives(alice, bob, bobId, newTransportId); + } + + private Pair addContacts( + boolean assertOldDuplexSending) throws Exception { + ContactId bobId = addContact(alice, bob, true); + ContactId aliceId = addContact(bob, alice, false); + + // Alice and Bob can send messages via the default test transports + if (assertOldDuplexSending) { + assertTrue(alice.getKeyManager() + .canSendOutgoingStreams(bobId, SIMPLEX_TRANSPORT_ID)); + assertTrue(alice.getKeyManager() + .canSendOutgoingStreams(bobId, DUPLEX_TRANSPORT_ID)); + assertTrue(bob.getKeyManager() + .canSendOutgoingStreams(aliceId, SIMPLEX_TRANSPORT_ID)); + assertTrue(bob.getKeyManager() + .canSendOutgoingStreams(aliceId, DUPLEX_TRANSPORT_ID)); + } + + // Sync initial client versioning updates + syncMessage(alice, bob, bobId, 1, true); + syncMessage(bob, alice, aliceId, 1, true); + syncMessage(alice, bob, bobId, 1, true); + sendAcks(bob, alice, aliceId, 1); + + return new Pair<>(aliceId, bobId); + } + + private ContactId addContact( + TransportKeyAgreementTestComponent device, + TransportKeyAgreementTestComponent remote, + boolean alice) throws Exception { + // Get remote Author + Author remoteAuthor = remote.getIdentityManager().getLocalAuthor(); + // Get ID of LocalAuthor + IdentityManager identityManager = device.getIdentityManager(); + AuthorId localAuthorId = identityManager.getLocalAuthor().getId(); + // Add the other user as a contact + ContactManager contactManager = device.getContactManager(); + return contactManager.addContact(remoteAuthor, localAuthorId, masterKey, + timestamp, alice, true, true); + } + + private TransportKeyAgreementTestComponent restartWithNewTransport( + TransportKeyAgreementTestComponent device, File dir, + Identity identity) throws Exception { + tearDown(device); + TransportKeyAgreementTestComponent newDevice = + createComponent(dir, true); + startLifecycle(newDevice, identity); + return newDevice; + } + + /** + * Asserts that the local key pair (specifically the private key) is removed + * from the session as intended when leaving the AWAIT_KEY state. + * If it remained on disk after the keys had been activated + * then we'd lose forward secrecy. + */ + private void assertLocalKeyPairIsNull( + TransportKeyAgreementTestComponent device, ContactId contactId) + throws Exception { + Contact contact = device.getContactManager().getContact(contactId); + Group group = getContactGroup(device, contact); + Map map = device.getClientHelper() + .getMessageMetadataAsDictionary(group.getId()); + for (Map.Entry e : map.entrySet()) { + if (!e.getValue().getBoolean(MSG_KEY_IS_SESSION)) continue; + Session s = device.getSessionParser().parseSession(e.getValue()); + assertNull(s.getLocalKeyPair()); + } + } + + private Group getContactGroup(TransportKeyAgreementTestComponent device, + Contact c) { + return device.getContactGroupFactory().createContactGroup(CLIENT_ID, + MAJOR_VERSION, c); + } + + private void assertTransportMessageArrives( + TransportKeyAgreementTestComponent from, + TransportKeyAgreementTestComponent to, ContactId toId, + TransportId transportId) throws Exception { + TransportProperties p = new TransportProperties(); + p.putBoolean("foo", true); + from.getTransportPropertyManager().mergeLocalProperties(transportId, p); + syncMessage(from, to, toId, transportId, 1, true); + } + +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementManagerImplTest.java new file mode 100644 index 000000000..e05f70d41 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementManagerImplTest.java @@ -0,0 +1,554 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.ContactGroupFactory; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfEntry; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.data.MetadataParser; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.Metadata; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.plugin.PluginConfig; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory; +import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.transport.KeyManager; +import org.briarproject.bramble.api.transport.KeySetId; +import org.briarproject.bramble.api.versioning.ClientVersioningManager; +import org.briarproject.bramble.test.BrambleMockTestCase; +import org.briarproject.bramble.test.CaptureArgumentAction; +import org.jmock.Expectations; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicReference; + +import static java.lang.Math.min; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.briarproject.bramble.api.Bytes.compare; +import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.DEFER; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.REJECT; +import static org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager.CLIENT_ID; +import static org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager.MAJOR_VERSION; +import static org.briarproject.bramble.test.TestUtils.getAgreementPrivateKey; +import static org.briarproject.bramble.test.TestUtils.getAgreementPublicKey; +import static org.briarproject.bramble.test.TestUtils.getContact; +import static org.briarproject.bramble.test.TestUtils.getGroup; +import static org.briarproject.bramble.test.TestUtils.getLocalAuthor; +import static org.briarproject.bramble.test.TestUtils.getMessage; +import static org.briarproject.bramble.test.TestUtils.getSecretKey; +import static org.briarproject.bramble.test.TestUtils.getTransportId; +import static org.briarproject.bramble.transport.agreement.MessageType.ACTIVATE; +import static org.briarproject.bramble.transport.agreement.MessageType.KEY; +import static org.briarproject.bramble.transport.agreement.State.ACTIVATED; +import static org.briarproject.bramble.transport.agreement.State.AWAIT_ACTIVATE; +import static org.briarproject.bramble.transport.agreement.State.AWAIT_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_MESSAGE_TYPE; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_PUBLIC_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_TRANSPORT_ID; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class TransportKeyAgreementManagerImplTest extends BrambleMockTestCase { + + private final DatabaseComponent db = context.mock(DatabaseComponent.class); + private final ClientHelper clientHelper = context.mock(ClientHelper.class); + private final MetadataParser metadataParser = + context.mock(MetadataParser.class); + private final ContactGroupFactory contactGroupFactory = + context.mock(ContactGroupFactory.class); + private final ClientVersioningManager clientVersioningManager = + context.mock(ClientVersioningManager.class); + private final IdentityManager identityManager = + context.mock(IdentityManager.class); + private final KeyManager keyManager = context.mock(KeyManager.class); + private final MessageEncoder messageEncoder = + context.mock(MessageEncoder.class); + private final SessionEncoder sessionEncoder = + context.mock(SessionEncoder.class); + private final SessionParser sessionParser = + context.mock(SessionParser.class); + private final TransportKeyAgreementCrypto crypto = + context.mock(TransportKeyAgreementCrypto.class); + private final PluginConfig pluginConfig = context.mock(PluginConfig.class); + private final SimplexPluginFactory simplexFactory = + context.mock(SimplexPluginFactory.class); + private final DuplexPluginFactory duplexFactory = + context.mock(DuplexPluginFactory.class); + + private final TransportId simplexTransportId = getTransportId(); + private final TransportId duplexTransportId = getTransportId(); + private final Group localGroup = getGroup(CLIENT_ID, MAJOR_VERSION); + private final Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION); + private final Contact contact = getContact(); + private final LocalAuthor localAuthor = getLocalAuthor(); + private final boolean alice = compare(localAuthor.getId().getBytes(), + contact.getAuthor().getId().getBytes()) < 0; + private final KeyPair localKeyPair = + new KeyPair(getAgreementPublicKey(), getAgreementPrivateKey()); + private final PublicKey remotePublicKey = getAgreementPublicKey(); + private final SecretKey rootKey = getSecretKey(); + private final KeySetId keySetId = new KeySetId(123); + + private final Message storageMessage = getMessage(contactGroup.getId()); + private final Message localKeyMessage = getMessage(contactGroup.getId()); + private final Message localActivateMessage = + getMessage(contactGroup.getId()); + private final Message remoteKeyMessage = getMessage(contactGroup.getId()); + private final Message remoteActivateMessage = + getMessage(contactGroup.getId()); + private final long localTimestamp = localKeyMessage.getTimestamp(); + private final long remoteTimestamp = remoteKeyMessage.getTimestamp(); + + // These query and metadata dictionaries are handled by the manager without + // inspecting their contents, so we can use empty dictionaries for testing + private final BdfDictionary sessionQuery = new BdfDictionary(); + private final BdfDictionary sessionMeta = new BdfDictionary(); + private final BdfDictionary localKeyMeta = new BdfDictionary(); + private final BdfDictionary localActivateMeta = new BdfDictionary(); + + // The manager doesn't use the incoming message body, so it can be empty + private final BdfList remoteMessageBody = new BdfList(); + + private final BdfDictionary remoteKeyMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_MESSAGE_TYPE, KEY.getValue()), + new BdfEntry(MSG_KEY_TRANSPORT_ID, + simplexTransportId.getString()), + new BdfEntry(MSG_KEY_PUBLIC_KEY, remotePublicKey.getEncoded())); + + private final BdfDictionary remoteActivateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_MESSAGE_TYPE, ACTIVATE.getValue()), + new BdfEntry(MSG_KEY_TRANSPORT_ID, + simplexTransportId.getString())); + + private TransportKeyAgreementManagerImpl manager; + + @Before + public void setUp() { + context.checking(new Expectations() {{ + oneOf(pluginConfig).getSimplexFactories(); + will(returnValue(singletonList(simplexFactory))); + oneOf(simplexFactory).getId(); + will(returnValue(simplexTransportId)); + oneOf(pluginConfig).getDuplexFactories(); + will(returnValue(singletonList(duplexFactory))); + oneOf(duplexFactory).getId(); + will(returnValue(duplexTransportId)); + oneOf(contactGroupFactory) + .createLocalGroup(CLIENT_ID, MAJOR_VERSION); + will(returnValue(localGroup)); + }}); + + manager = new TransportKeyAgreementManagerImpl(db, clientHelper, + metadataParser, contactGroupFactory, clientVersioningManager, + identityManager, keyManager, messageEncoder, sessionEncoder, + sessionParser, crypto, pluginConfig); + } + + @Test + public void testCreatesContactGroupAtStartupIfLocalGroupDoesNotExist() + throws Exception { + Transaction txn = new Transaction(null, false); + + context.checking(new Expectations() {{ + oneOf(db).getContacts(txn); + will(returnValue(singletonList(contact))); + // The local group doesn't exist so we need to create contact groups + oneOf(db).containsGroup(txn, localGroup.getId()); + will(returnValue(false)); + oneOf(db).addGroup(txn, localGroup); + // Create the contact group and set it up + oneOf(contactGroupFactory).createContactGroup(CLIENT_ID, + MAJOR_VERSION, contact); + will(returnValue(contactGroup)); + oneOf(db).addGroup(txn, contactGroup); + oneOf(clientHelper) + .setContactId(txn, contactGroup.getId(), contact.getId()); + oneOf(clientVersioningManager).getClientVisibility(txn, + contact.getId(), CLIENT_ID, MAJOR_VERSION); + will(returnValue(VISIBLE)); + oneOf(db).setGroupVisibility(txn, contact.getId(), + contactGroup.getId(), VISIBLE); + // We already have keys for both transports + oneOf(db).getTransportsWithKeys(txn); + will(returnValue(singletonMap(contact.getId(), + asList(simplexTransportId, duplexTransportId)))); + }}); + + manager.onDatabaseOpened(txn); + } + + @Test + public void testDoesNotCreateContactGroupAtStartupIfLocalGroupExists() + throws Exception { + Transaction txn = new Transaction(null, false); + + context.checking(new Expectations() {{ + oneOf(db).getContacts(txn); + will(returnValue(singletonList(contact))); + // The local group exists so we don't need to create contact groups + oneOf(db).containsGroup(txn, localGroup.getId()); + will(returnValue(true)); + // We already have keys for both transports + oneOf(db).getTransportsWithKeys(txn); + will(returnValue(singletonMap(contact.getId(), + asList(simplexTransportId, duplexTransportId)))); + }}); + + manager.onDatabaseOpened(txn); + } + + @Test + public void testStartsSessionAtStartup() throws Exception { + Transaction txn = new Transaction(null, false); + + context.checking(new Expectations() {{ + oneOf(db).getContacts(txn); + will(returnValue(singletonList(contact))); + // The local group exists so we don't need to create contact groups + oneOf(db).containsGroup(txn, localGroup.getId()); + will(returnValue(true)); + // We need keys for the simplex transport + oneOf(db).getTransportsWithKeys(txn); + will(returnValue(singletonMap(contact.getId(), + singletonList(duplexTransportId)))); + // Get the contact group ID + oneOf(contactGroupFactory) + .createContactGroup(CLIENT_ID, MAJOR_VERSION, contact); + will(returnValue(contactGroup)); + }}); + + // Check whether a session exists - it doesn't + expectSessionDoesNotExist(txn); + // Generate the local key pair + expectGenerateLocalKeyPair(); + // Send a key message + expectSendKeyMessage(txn); + // Save the session + expectCreateStorageMessage(txn); + AtomicReference savedSession = expectSaveSession(txn); + + manager.onDatabaseOpened(txn); + + assertEquals(AWAIT_KEY, savedSession.get().getState()); + assertEquals(localKeyMessage.getId(), + savedSession.get().getLastLocalMessageId()); + assertEquals(localKeyPair, savedSession.get().getLocalKeyPair()); + assertEquals(Long.valueOf(localTimestamp), + savedSession.get().getLocalTimestamp()); + assertNull(savedSession.get().getKeySetId()); + } + + @Test + public void testDefersMessageIfTransportIsNotSupported() throws Exception { + Transaction txn = new Transaction(null, false); + TransportId unknownTransportId = getTransportId(); + BdfDictionary meta = new BdfDictionary(remoteKeyMeta); + meta.put(MSG_KEY_TRANSPORT_ID, unknownTransportId.getString()); + + assertEquals(DEFER, manager.incomingMessage(txn, remoteKeyMessage, + remoteMessageBody, meta)); + } + + @Test + public void testAcceptsKeyMessageInAwaitKeyState() throws Exception { + Transaction txn = new Transaction(null, false); + Session loadedSession = new Session(AWAIT_KEY, + localKeyMessage.getId(), localKeyPair, localTimestamp, null); + + // Check whether a session exists - it does + expectLoadSession(txn, loadedSession); + // Load the contact ID + expectLoadContactId(txn); + // Check whether we already have keys - we don't + expectKeysExist(txn, false); + // Parse the remote public key + expectParseRemotePublicKey(); + // Derive and store the transport keys + expectDeriveAndStoreTransportKeys(txn); + // Send an activate message + expectSendActivateMessage(txn); + // Save the session + AtomicReference savedSession = expectSaveSession(txn); + + assertEquals(ACCEPT_DO_NOT_SHARE, manager.incomingMessage(txn, + remoteKeyMessage, remoteMessageBody, remoteKeyMeta)); + + assertEquals(AWAIT_ACTIVATE, savedSession.get().getState()); + assertEquals(localActivateMessage.getId(), + savedSession.get().getLastLocalMessageId()); + assertNull(savedSession.get().getLocalKeyPair()); + assertNull(savedSession.get().getLocalTimestamp()); + assertEquals(keySetId, savedSession.get().getKeySetId()); + } + + @Test + public void testAcceptsKeyMessageIfWeHaveTransportKeysButNoSession() + throws Exception { + Transaction txn = new Transaction(null, false); + + // Check whether a session exists - it doesn't + expectSessionDoesNotExist(txn); + // Load the contact ID + expectLoadContactId(txn); + // Check whether we already have keys - we do + expectKeysExist(txn, true); + // Generate the local key pair + expectGenerateLocalKeyPair(); + // Parse the remote public key + expectParseRemotePublicKey(); + // Send a key message + expectSendKeyMessage(txn); + // Derive and store the transport keys + expectDeriveAndStoreTransportKeys(txn); + // Send an activate message + expectSendActivateMessage(txn); + // Save the session + expectCreateStorageMessage(txn); + AtomicReference savedSession = expectSaveSession(txn); + + assertEquals(ACCEPT_DO_NOT_SHARE, manager.incomingMessage(txn, + remoteKeyMessage, remoteMessageBody, remoteKeyMeta)); + + assertEquals(AWAIT_ACTIVATE, savedSession.get().getState()); + assertEquals(localActivateMessage.getId(), + savedSession.get().getLastLocalMessageId()); + assertNull(savedSession.get().getLocalKeyPair()); + assertNull(savedSession.get().getLocalTimestamp()); + assertEquals(keySetId, savedSession.get().getKeySetId()); + } + + @Test + public void testRejectsKeyMessageInAwaitActivateState() throws Exception { + Session loadedSession = new Session(AWAIT_ACTIVATE, + localActivateMessage.getId(), null, null, keySetId); + testRejectsKeyMessageWithExistingSession(loadedSession); + } + + @Test + public void testRejectsKeyMessageInActivatedState() throws Exception { + Session loadedSession = new Session(ACTIVATED, + localActivateMessage.getId(), null, null, null); + testRejectsKeyMessageWithExistingSession(loadedSession); + } + + private void testRejectsKeyMessageWithExistingSession(Session loadedSession) + throws Exception { + Transaction txn = new Transaction(null, false); + + // Check whether a session exists - it does + expectLoadSession(txn, loadedSession); + // Load the contact ID + expectLoadContactId(txn); + // Check whether we already have keys - we don't + expectKeysExist(txn, false); + + assertEquals(REJECT, manager.incomingMessage(txn, + remoteKeyMessage, remoteMessageBody, remoteKeyMeta)); + } + + @Test + public void testAcceptsActivateMessageInAwaitActivateState() + throws Exception { + Transaction txn = new Transaction(null, false); + Session loadedSession = new Session(AWAIT_ACTIVATE, + localActivateMessage.getId(), null, null, keySetId); + + // Check whether a session exists - it does + expectLoadSession(txn, loadedSession); + + // Activate the transport keys + context.checking(new Expectations() {{ + oneOf(keyManager).activateKeys(txn, + singletonMap(simplexTransportId, keySetId)); + }}); + + // Save the session + AtomicReference savedSession = expectSaveSession(txn); + + assertEquals(ACCEPT_DO_NOT_SHARE, manager.incomingMessage(txn, + remoteActivateMessage, remoteMessageBody, remoteActivateMeta)); + + assertEquals(ACTIVATED, savedSession.get().getState()); + assertEquals(localActivateMessage.getId(), + savedSession.get().getLastLocalMessageId()); + assertNull(savedSession.get().getLocalKeyPair()); + assertNull(savedSession.get().getLocalTimestamp()); + assertNull(savedSession.get().getKeySetId()); + } + + @Test + public void testRejectsActivateMessageWithNoSession() throws Exception { + Transaction txn = new Transaction(null, false); + + // Check whether a session exists - it doesn't + expectSessionDoesNotExist(txn); + + assertEquals(REJECT, manager.incomingMessage(txn, + remoteActivateMessage, remoteMessageBody, remoteActivateMeta)); + } + + @Test + public void testRejectsActivateMessageInAwaitKeyState() throws Exception { + Session loadedSession = new Session(AWAIT_KEY, + localKeyMessage.getId(), localKeyPair, localTimestamp, null); + testRejectsActivateMessageWithExistingSession(loadedSession); + } + + @Test + public void testRejectsActivateMessageInActivatedState() throws Exception { + Session loadedSession = new Session(ACTIVATED, + localActivateMessage.getId(), null, null, null); + testRejectsActivateMessageWithExistingSession(loadedSession); + } + + private void testRejectsActivateMessageWithExistingSession( + Session loadedSession) throws Exception { + Transaction txn = new Transaction(null, false); + + // Check whether a session exists - it does + expectLoadSession(txn, loadedSession); + + assertEquals(REJECT, manager.incomingMessage(txn, + remoteActivateMessage, remoteMessageBody, remoteActivateMeta)); + } + + private void expectSessionDoesNotExist(Transaction txn) throws Exception { + context.checking(new Expectations() {{ + oneOf(sessionEncoder).getSessionQuery(simplexTransportId); + will(returnValue(sessionQuery)); + oneOf(clientHelper) + .getMessageIds(txn, contactGroup.getId(), sessionQuery); + will(returnValue(emptyList())); + }}); + } + + private void expectLoadSession(Transaction txn, Session loadedSession) + throws Exception { + context.checking(new Expectations() {{ + oneOf(sessionEncoder).getSessionQuery(simplexTransportId); + will(returnValue(sessionQuery)); + oneOf(clientHelper) + .getMessageIds(txn, contactGroup.getId(), sessionQuery); + will(returnValue(singletonList(storageMessage.getId()))); + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + storageMessage.getId()); + will(returnValue(sessionMeta)); + oneOf(sessionParser).parseSession(sessionMeta); + will(returnValue(loadedSession)); + }}); + } + + private void expectSendKeyMessage(Transaction txn) throws Exception { + context.checking(new Expectations() {{ + oneOf(messageEncoder).encodeKeyMessage(contactGroup.getId(), + simplexTransportId, localKeyPair.getPublic()); + will(returnValue(localKeyMessage)); + oneOf(messageEncoder) + .encodeMessageMetadata(simplexTransportId, KEY, true); + will(returnValue(localKeyMeta)); + oneOf(clientHelper).addLocalMessage(txn, localKeyMessage, + localKeyMeta, true, false); + }}); + } + + private void expectSendActivateMessage(Transaction txn) throws Exception { + context.checking(new Expectations() {{ + oneOf(messageEncoder).encodeActivateMessage(contactGroup.getId(), + simplexTransportId, localKeyMessage.getId()); + will(returnValue(localActivateMessage)); + oneOf(messageEncoder) + .encodeMessageMetadata(simplexTransportId, ACTIVATE, true); + will(returnValue(localActivateMeta)); + oneOf(clientHelper).addLocalMessage(txn, localActivateMessage, + localActivateMeta, true, false); + }}); + } + + private void expectCreateStorageMessage(Transaction txn) throws Exception { + context.checking(new Expectations() {{ + oneOf(clientHelper) + .createMessageForStoringMetadata(contactGroup.getId()); + will(returnValue(storageMessage)); + oneOf(db).addLocalMessage(txn, storageMessage, new Metadata(), + false, false); + }}); + } + + private AtomicReference expectSaveSession(Transaction txn) + throws Exception { + AtomicReference savedSession = new AtomicReference<>(); + + context.checking(new Expectations() {{ + oneOf(sessionEncoder).encodeSession(with(any(Session.class)), + with(simplexTransportId)); + will(doAll( + new CaptureArgumentAction<>(savedSession, Session.class, 0), + returnValue(sessionMeta))); + oneOf(clientHelper).mergeMessageMetadata(txn, + storageMessage.getId(), sessionMeta); + }}); + + return savedSession; + } + + private void expectLoadContactId(Transaction txn) throws Exception { + context.checking(new Expectations() {{ + oneOf(clientHelper).getContactId(txn, contactGroup.getId()); + will(returnValue(contact.getId())); + }}); + } + + private void expectGenerateLocalKeyPair() { + context.checking(new Expectations() {{ + oneOf(crypto).generateKeyPair(); + will(returnValue(localKeyPair)); + }}); + } + + private void expectParseRemotePublicKey() throws Exception { + context.checking(new Expectations() {{ + oneOf(crypto).parsePublicKey(remotePublicKey.getEncoded()); + will(returnValue(remotePublicKey)); + }}); + } + + private void expectDeriveAndStoreTransportKeys(Transaction txn) + throws Exception { + context.checking(new Expectations() {{ + oneOf(crypto).deriveRootKey(localKeyPair, remotePublicKey); + will(returnValue(rootKey)); + oneOf(db).getContact(txn, contact.getId()); + will(returnValue(contact)); + oneOf(identityManager).getLocalAuthor(txn); + will(returnValue(localAuthor)); + oneOf(keyManager).addRotationKeys(txn, contact.getId(), + simplexTransportId, rootKey, + min(localTimestamp, remoteTimestamp), alice, false); + will(returnValue(keySetId)); + }}); + } + + private void expectKeysExist(Transaction txn, boolean exist) + throws Exception { + context.checking(new Expectations() {{ + oneOf(db).containsTransportKeys(txn, contact.getId(), + simplexTransportId); + will(returnValue(exist)); + }}); + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementTestComponent.java b/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementTestComponent.java new file mode 100644 index 000000000..a0de3c5e4 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementTestComponent.java @@ -0,0 +1,40 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.BrambleCoreModule; +import org.briarproject.bramble.api.client.ContactGroupFactory; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.properties.TransportPropertyManager; +import org.briarproject.bramble.api.transport.KeyManager; +import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule; +import org.briarproject.bramble.test.BrambleIntegrationTestComponent; + +import javax.inject.Singleton; + +import dagger.Component; + +@Singleton +@Component(modules = { + BrambleCoreIntegrationTestModule.class, + BrambleCoreModule.class +}) +interface TransportKeyAgreementTestComponent + extends BrambleIntegrationTestComponent { + + KeyManager getKeyManager(); + + TransportKeyAgreementManagerImpl getTransportKeyAgreementManager(); + + ContactManager getContactManager(); + + LifecycleManager getLifecycleManager(); + + ContactGroupFactory getContactGroupFactory(); + + SessionParser getSessionParser(); + + TransportPropertyManager getTransportPropertyManager(); + + DatabaseComponent getDatabaseComponent(); +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementValidatorTest.java b/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementValidatorTest.java new file mode 100644 index 000000000..0a8be77ee --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementValidatorTest.java @@ -0,0 +1,301 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.BdfMessageContext; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.data.MetadataEncoder; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.test.BrambleMockTestCase; +import org.jmock.Expectations; +import org.junit.Test; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES; +import static org.briarproject.bramble.api.plugin.TransportId.MAX_TRANSPORT_ID_LENGTH; +import static org.briarproject.bramble.api.system.Clock.MIN_REASONABLE_TIME_MS; +import static org.briarproject.bramble.api.versioning.ClientVersioningManager.CLIENT_ID; +import static org.briarproject.bramble.api.versioning.ClientVersioningManager.MAJOR_VERSION; +import static org.briarproject.bramble.test.TestUtils.getGroup; +import static org.briarproject.bramble.test.TestUtils.getMessage; +import static org.briarproject.bramble.test.TestUtils.getRandomBytes; +import static org.briarproject.bramble.transport.agreement.MessageType.ACTIVATE; +import static org.briarproject.bramble.transport.agreement.MessageType.KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_PUBLIC_KEY; +import static org.briarproject.bramble.util.StringUtils.getRandomString; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class TransportKeyAgreementValidatorTest extends BrambleMockTestCase { + + private final ClientHelper clientHelper = context.mock(ClientHelper.class); + private final MetadataEncoder metadataEncoder = + context.mock(MetadataEncoder.class); + private final Clock clock = context.mock(Clock.class); + private final MessageEncoder messageEncoder = + context.mock(MessageEncoder.class); + private final TransportKeyAgreementValidator validator = + new TransportKeyAgreementValidator(clientHelper, metadataEncoder, + clock, messageEncoder); + + private final Group group = getGroup(CLIENT_ID, MAJOR_VERSION); + private final Message message = getMessage(group.getId()); + + @Test(expected = FormatException.class) + public void testRejectsEmptyMessage() throws Exception { + BdfList body = BdfList.of(); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsNullType() throws Exception { + BdfList body = BdfList.of((Object) null); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsNonLongType() throws Exception { + BdfList body = BdfList.of("123"); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsUnknownLongType() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue() + 1); + validator.validateMessage(message, group, body); + } + + @Test + public void testAcceptsKeyMsg() throws Exception { + TransportId transportId = new TransportId(getRandomString(1)); + context.checking(new Expectations() {{ + oneOf(messageEncoder) + .encodeMessageMetadata(transportId, KEY, false); + will(returnValue(new BdfDictionary())); + }}); + + byte[] publicKey = getRandomBytes(1); + BdfList body = + BdfList.of(KEY.getValue(), transportId.getString(), publicKey); + BdfMessageContext msgCtx = + validator.validateMessage(message, group, body); + assertEquals(emptyList(), msgCtx.getDependencies()); + BdfDictionary d = msgCtx.getDictionary(); + assertArrayEquals(publicKey, d.getRaw(MSG_KEY_PUBLIC_KEY)); + } + + @Test + public void testAcceptsKeyMsgMaxLengths() throws Exception { + TransportId transportId = + new TransportId(getRandomString(MAX_TRANSPORT_ID_LENGTH)); + context.checking(new Expectations() {{ + oneOf(messageEncoder) + .encodeMessageMetadata(transportId, KEY, false); + will(returnValue(new BdfDictionary())); + }}); + + byte[] publicKey = getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES); + BdfList body = + BdfList.of(KEY.getValue(), transportId.getString(), publicKey); + BdfMessageContext msgCtx = + validator.validateMessage(message, group, body); + assertEquals(emptyList(), msgCtx.getDependencies()); + BdfDictionary d = msgCtx.getDictionary(); + assertArrayEquals(publicKey, d.getRaw(MSG_KEY_PUBLIC_KEY)); + } + + @Test + public void testAcceptsMinTimestampKeyMsg() throws Exception { + Message message = + getMessage(group.getId(), 1234, MIN_REASONABLE_TIME_MS); + TransportId transportId = new TransportId(getRandomString(1)); + context.checking(new Expectations() {{ + oneOf(messageEncoder) + .encodeMessageMetadata(transportId, KEY, false); + will(returnValue(new BdfDictionary())); + }}); + + byte[] publicKey = getRandomBytes(1); + BdfList body = + BdfList.of(KEY.getValue(), transportId.getString(), publicKey); + BdfMessageContext msgCtx = + validator.validateMessage(message, group, body); + assertEquals(emptyList(), msgCtx.getDependencies()); + BdfDictionary d = msgCtx.getDictionary(); + assertArrayEquals(publicKey, d.getRaw(MSG_KEY_PUBLIC_KEY)); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongKeyMsg() throws Exception { + BdfList body = BdfList.of(KEY.getValue(), getRandomString(1), + getRandomBytes(1), 1); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooShortKeyMsg() throws Exception { + BdfList body = BdfList.of(KEY.getValue(), getRandomString(1)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsEmptyTransportIdKeyMsg() throws Exception { + BdfList body = BdfList.of(KEY.getValue(), "", getRandomBytes(1)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongTransportIdKeyMsg() throws Exception { + BdfList body = BdfList.of(KEY.getValue(), + getRandomString(MAX_TRANSPORT_ID_LENGTH + 1), + getRandomBytes(1)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsNonStringTransportIdKeyMsg() throws Exception { + BdfList body = BdfList.of(KEY.getValue(), + getRandomBytes(MAX_TRANSPORT_ID_LENGTH), + getRandomBytes(1)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsEmptyPublicKeyKeyMsg() throws Exception { + BdfList body = BdfList.of(KEY.getValue(), + getRandomString(1), + getRandomBytes(0)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongPublicKeyKeyMsg() throws Exception { + BdfList body = BdfList.of(KEY.getValue(), + getRandomString(1), + getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES + 1)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsNonBytesPublicKeyKeyMsg() throws Exception { + BdfList body = BdfList.of(KEY.getValue(), + getRandomString(1), + getRandomString(MAX_AGREEMENT_PUBLIC_KEY_BYTES)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooOldTimestampKeyMsg() throws Exception { + Message message = + getMessage(group.getId(), 1234, MIN_REASONABLE_TIME_MS - 1); + BdfList body = BdfList.of(KEY.getValue(), getRandomString(1), + getRandomBytes(1)); + validator.validateMessage(message, group, body); + } + + @Test + public void testAcceptsActivateMsg() throws Exception { + TransportId transportId = new TransportId(getRandomString(1)); + BdfDictionary meta = new BdfDictionary(); + context.checking(new Expectations() {{ + oneOf(messageEncoder) + .encodeMessageMetadata(transportId, ACTIVATE, false); + will(returnValue(meta)); + }}); + + MessageId msgId = new MessageId(getRandomBytes(MessageId.LENGTH)); + BdfList body = BdfList.of(ACTIVATE.getValue(), transportId.getString(), + msgId.getBytes()); + + BdfMessageContext msgCtx = + validator.validateMessage(message, group, body); + assertEquals(singletonList(msgId), msgCtx.getDependencies()); + assertEquals(meta, msgCtx.getDictionary()); + } + + @Test + public void testAcceptsActivateMsgMaxTransportIdLength() throws Exception { + TransportId transportId = + new TransportId(getRandomString(MAX_TRANSPORT_ID_LENGTH)); + BdfDictionary meta = new BdfDictionary(); + context.checking(new Expectations() {{ + oneOf(messageEncoder) + .encodeMessageMetadata(transportId, ACTIVATE, false); + will(returnValue(meta)); + }}); + + MessageId msgId = new MessageId(getRandomBytes(MessageId.LENGTH)); + BdfList body = BdfList.of(ACTIVATE.getValue(), transportId.getString(), + msgId.getBytes()); + + BdfMessageContext msgCtx = + validator.validateMessage(message, group, body); + assertEquals(singletonList(msgId), msgCtx.getDependencies()); + assertEquals(meta, msgCtx.getDictionary()); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongActivateMsg() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue(), getRandomString(1), + getRandomBytes(MessageId.LENGTH), 1); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooShortActivateMsg() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue(), getRandomString(1)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsEmptyTransportIdActivateMsg() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue(), "", + getRandomBytes(MessageId.LENGTH)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsNonStringTransportIdActivateMsg() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue(), 123, + getRandomBytes(MessageId.LENGTH)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongTransportIdActivateMsg() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue(), + getRandomString(MAX_TRANSPORT_ID_LENGTH + 1), + getRandomBytes(MessageId.LENGTH)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooShortMsgIdActivateMsg() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue(), + getRandomString(1), + getRandomBytes(MessageId.LENGTH - 1)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongMsgIdActivateMsg() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue(), + getRandomString(1), + getRandomBytes(MessageId.LENGTH + 1)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsNonByteMsgIdActivateMsg() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue(), + getRandomString(1), + getRandomString(MessageId.LENGTH)); + validator.validateMessage(message, group, body); + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java index 17d23087b..2a6d34405 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java @@ -31,6 +31,7 @@ import static java.util.Collections.singletonMap; import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE; import static org.briarproject.bramble.api.versioning.ClientVersioningManager.CLIENT_ID; import static org.briarproject.bramble.api.versioning.ClientVersioningManager.MAJOR_VERSION; import static org.briarproject.bramble.test.TestUtils.getClientId; @@ -41,7 +42,6 @@ import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; public class ClientVersioningManagerImplTest extends BrambleMockTestCase { @@ -419,7 +419,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase { ClientVersioningManagerImpl c = createInstance(); c.registerClient(clientId, 123, 234, hook); - assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata())); + assertEquals(ACCEPT_DO_NOT_SHARE, + c.incomingMessage(txn, newRemoteUpdate, new Metadata())); } @Test @@ -464,7 +465,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase { ClientVersioningManagerImpl c = createInstance(); c.registerClient(clientId, 123, 234, hook); - assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata())); + assertEquals(ACCEPT_DO_NOT_SHARE, + c.incomingMessage(txn, newRemoteUpdate, new Metadata())); } @Test @@ -496,7 +498,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase { ClientVersioningManagerImpl c = createInstance(); c.registerClient(clientId, 123, 234, hook); - assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata())); + assertEquals(ACCEPT_DO_NOT_SHARE, + c.incomingMessage(txn, newRemoteUpdate, new Metadata())); } @Test @@ -579,7 +582,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase { ClientVersioningManagerImpl c = createInstance(); c.registerClient(clientId, 123, 234, hook); - assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata())); + assertEquals(ACCEPT_DO_NOT_SHARE, + c.incomingMessage(txn, newRemoteUpdate, new Metadata())); } @Test @@ -649,7 +653,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase { ClientVersioningManagerImpl c = createInstance(); c.registerClient(clientId, 123, 234, hook); - assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata())); + assertEquals(ACCEPT_DO_NOT_SHARE, + c.incomingMessage(txn, newRemoteUpdate, new Metadata())); } @Test diff --git a/bramble-core/witness.gradle b/bramble-core/witness.gradle index 1b69ef5ff..8793a130e 100644 --- a/bramble-core/witness.gradle +++ b/bramble-core/witness.gradle @@ -23,6 +23,7 @@ dependencyVerification { 'net.bytebuddy:byte-buddy:1.9.12:byte-buddy-1.9.12.jar:3688c3d434bebc3edc5516296a2ed0f47b65e451071b4afecad84f902f0efc11', 'net.i2p.crypto:eddsa:0.2.0:eddsa-0.2.0.jar:a7cb1b85c16e2f0730b9204106929a1d9aaae1df728adc7041a8b8b605692140', 'net.jcip:jcip-annotations:1.0:jcip-annotations-1.0.jar:be5805392060c71474bf6c9a67a099471274d30b83eef84bfc4e0889a4f1dcc0', + 'net.jodah:concurrentunit:0.4.2:concurrentunit-0.4.2.jar:5583078e1acf91734939e985bc9e7ee947b0e93a8eef679da6bb07bbeb47ced3', 'net.ltgt.gradle.incap:incap:0.2:incap-0.2.jar:b625b9806b0f1e4bc7a2e3457119488de3cd57ea20feedd513db070a573a4ffd', 'org.apache-extras.beanshell:bsh:2.0b6:bsh-2.0b6.jar:a17955976070c0573235ee662f2794a78082758b61accffce8d3f8aedcd91047', 'org.bitlet:weupnp:0.1.4:weupnp-0.1.4.jar:88df7e6504929d00bdb832863761385c68ab92af945b04f0770b126270a444fb', diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java index cc80027fe..4a4e2e639 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java @@ -41,7 +41,7 @@ class JavaBluetoothPlugin extends SecureRandom secureRandom, Backoff backoff, PluginCallback callback, - int maxLatency, + long maxLatency, int maxIdleTime) { super(connectionManager, connectionFactory, ioExecutor, wakefulIoExecutor, secureRandom, backoff, callback, diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPluginFactory.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPluginFactory.java index 8f87c78bb..d7dbcfd11 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPluginFactory.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPluginFactory.java @@ -58,7 +58,7 @@ public class JavaBluetoothPluginFactory implements DuplexPluginFactory { } @Override - public int getMaxLatency() { + public long getMaxLatency() { return MAX_LATENCY; } diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java index 53a668f20..f93854f3e 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java @@ -50,14 +50,14 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback { private final ModemFactory modemFactory; private final SerialPortList serialPortList; private final PluginCallback callback; - private final int maxLatency; + private final long maxLatency; private final AtomicBoolean used = new AtomicBoolean(false); private final PluginState state = new PluginState(); private volatile Modem modem = null; ModemPlugin(ModemFactory modemFactory, SerialPortList serialPortList, - PluginCallback callback, int maxLatency) { + PluginCallback callback, long maxLatency) { this.modemFactory = modemFactory; this.serialPortList = serialPortList; this.callback = callback; @@ -70,7 +70,7 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback { } @Override - public int getMaxLatency() { + public long getMaxLatency() { return maxLatency; } diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPluginFactory.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPluginFactory.java index 8e2d29d92..459e9385a 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPluginFactory.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPluginFactory.java @@ -36,7 +36,7 @@ public class ModemPluginFactory implements DuplexPluginFactory { } @Override - public int getMaxLatency() { + public long getMaxLatency() { return MAX_LATENCY; } diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/JavaTorPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/JavaTorPlugin.java index d9d5ddf89..791d01896 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/JavaTorPlugin.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/JavaTorPlugin.java @@ -33,7 +33,7 @@ abstract class JavaTorPlugin extends TorPlugin { TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback, String architecture, - int maxLatency, + long maxLatency, int maxIdleTime, File torDirectory) { super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPlugin.java index f7ee9134e..568ba51c3 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPlugin.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPlugin.java @@ -33,7 +33,7 @@ class UnixTorPlugin extends JavaTorPlugin { TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback, String architecture, - int maxLatency, + long maxLatency, int maxIdleTime, File torDirectory) { super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java index e279d56be..c13b7e61b 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java @@ -56,7 +56,7 @@ public class UnixTorPluginFactory implements DuplexPluginFactory { private final File torDirectory; @Inject - public UnixTorPluginFactory(@IoExecutor Executor ioExecutor, + UnixTorPluginFactory(@IoExecutor Executor ioExecutor, @WakefulIoExecutor Executor wakefulIoExecutor, NetworkManager networkManager, LocationUtils locationUtils, @@ -88,7 +88,7 @@ public class UnixTorPluginFactory implements DuplexPluginFactory { } @Override - public int getMaxLatency() { + public long getMaxLatency() { return MAX_LATENCY; } diff --git a/briar-android/artwork/transfer_data.svg b/briar-android/artwork/transfer_data.svg new file mode 100644 index 000000000..af653ac7e --- /dev/null +++ b/briar-android/artwork/transfer_data.svg @@ -0,0 +1,35 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/briar-android/artwork/transfer_data_receive.svg b/briar-android/artwork/transfer_data_receive.svg new file mode 100644 index 000000000..2280b7453 --- /dev/null +++ b/briar-android/artwork/transfer_data_receive.svg @@ -0,0 +1,36 @@ + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/briar-android/artwork/transfer_data_send.svg b/briar-android/artwork/transfer_data_send.svg new file mode 100644 index 000000000..4cbdb88f9 --- /dev/null +++ b/briar-android/artwork/transfer_data_send.svg @@ -0,0 +1,37 @@ + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml index 4de40296e..e704879f3 100644 --- a/briar-android/src/main/AndroidManifest.xml +++ b/briar-android/src/main/AndroidManifest.xml @@ -437,6 +437,15 @@ android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" /> + + + + getSimplexFactories() { - return emptyList(); + return SDK_INT >= 19 ? singletonList(drive) : emptyList(); } @Override @@ -306,6 +311,11 @@ public class AppModule { return IS_DEBUG_BUILD; } + @Override + public boolean shouldEnableTransferData() { + return IS_DEBUG_BUILD; + } + @Override public boolean shouldEnableShareAppViaOfflineHotspot() { return IS_DEBUG_BUILD; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/StartupFailureActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/StartupFailureActivity.java index d92a2b58d..5f1e29fa2 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/StartupFailureActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/StartupFailureActivity.java @@ -51,26 +51,27 @@ public class StartupFailureActivity extends BaseActivity implements } // show proper error message - String errorMsg; + int errorRes; switch (result) { + case CLOCK_ERROR: + errorRes = R.string.startup_failed_clock_error; + break; case DATA_TOO_OLD_ERROR: - errorMsg = - getString(R.string.startup_failed_data_too_old_error); + errorRes = R.string.startup_failed_data_too_old_error; break; case DATA_TOO_NEW_ERROR: - errorMsg = - getString(R.string.startup_failed_data_too_new_error); + errorRes = R.string.startup_failed_data_too_new_error; break; case DB_ERROR: - errorMsg = getString(R.string.startup_failed_db_error); + errorRes = R.string.startup_failed_db_error; break; case SERVICE_ERROR: - errorMsg = getString(R.string.startup_failed_service_error); + errorRes = R.string.startup_failed_service_error; break; default: throw new IllegalArgumentException(); } - showInitialFragment(ErrorFragment.newInstance(errorMsg)); + showInitialFragment(ErrorFragment.newInstance(getString(errorRes))); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java index 974510b4d..683b484e8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java @@ -64,6 +64,7 @@ import org.briarproject.briar.android.privategroup.memberlist.GroupMemberModule; import org.briarproject.briar.android.privategroup.reveal.GroupRevealModule; import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity; import org.briarproject.briar.android.privategroup.reveal.RevealContactsFragment; +import org.briarproject.briar.android.removabledrive.RemovableDriveActivity; import org.briarproject.briar.android.reporting.CrashFragment; import org.briarproject.briar.android.reporting.CrashReportActivity; import org.briarproject.briar.android.reporting.ReportFormFragment; @@ -179,6 +180,8 @@ public interface ActivityComponent { void inject(HotspotActivity hotspotActivity); + void inject(RemovableDriveActivity activity); + // Fragments void inject(SetupFragment fragment); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java index 991aab447..160f054d7 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java @@ -11,8 +11,5 @@ public interface RequestCodes { int REQUEST_DOZE_WHITELISTING = 9; int REQUEST_UNLOCK = 11; int REQUEST_KEYGUARD_UNLOCK = 12; - int REQUEST_ATTACH_IMAGE = 13; - int REQUEST_SAVE_ATTACHMENT = 14; - int REQUEST_AVATAR_IMAGE = 15; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactActivity.java index 6dd6ca690..94b381b7f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactActivity.java @@ -17,7 +17,7 @@ import org.briarproject.briar.android.contact.add.nearby.AddContactState.Failed; import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactViewModel.BluetoothDecision; import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; -import org.briarproject.briar.android.util.RequestBluetoothDiscoverable; +import org.briarproject.briar.android.util.ActivityLaunchers.RequestBluetoothDiscoverable; import java.util.logging.Logger; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java index 955dc419f..f208d3bcf 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java @@ -14,7 +14,7 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.BaseActivity; import org.briarproject.briar.android.contact.ContactItem; -import org.briarproject.briar.android.util.RequestBluetoothDiscoverable; +import org.briarproject.briar.android.util.ActivityLaunchers.RequestBluetoothDiscoverable; import javax.inject.Inject; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 448d44b57..db2f8fb2a 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -4,6 +4,7 @@ import android.annotation.SuppressLint; import android.content.DialogInterface; import android.content.Intent; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.transition.Slide; @@ -34,7 +35,6 @@ import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; -import org.briarproject.bramble.api.plugin.BluetoothConstants; import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent; import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent; import org.briarproject.bramble.api.sync.ClientId; @@ -54,6 +54,9 @@ import org.briarproject.briar.android.forum.ForumActivity; import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; import org.briarproject.briar.android.introduction.IntroductionActivity; import org.briarproject.briar.android.privategroup.conversation.GroupActivity; +import org.briarproject.briar.android.removabledrive.RemovableDriveActivity; +import org.briarproject.briar.android.util.ActivityLaunchers.GetImageAdvanced; +import org.briarproject.briar.android.util.ActivityLaunchers.GetMultipleImagesAdvanced; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.ImagePreview; @@ -92,6 +95,7 @@ import java.util.logging.Logger; import javax.inject.Inject; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.appcompat.app.AlertDialog; @@ -121,6 +125,7 @@ import static android.widget.Toast.LENGTH_SHORT; import static androidx.core.app.ActivityOptionsCompat.makeSceneTransitionAnimation; import static androidx.lifecycle.Lifecycle.State.STARTED; import static androidx.recyclerview.widget.SortedList.INVALID_POSITION; +import static java.util.Collections.singletonList; import static java.util.Collections.sort; import static java.util.Objects.requireNonNull; import static java.util.logging.Level.INFO; @@ -132,7 +137,6 @@ import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.StringUtils.fromHexString; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; import static org.briarproject.bramble.util.StringUtils.join; -import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ATTACH_IMAGE; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION; import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENTS; import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION; @@ -192,6 +196,12 @@ public class ConversationActivity extends BriarActivity requireNonNull(name); loadMessages(); }; + private final ActivityResultLauncher launcher = SDK_INT >= 18 ? + registerForActivityResult(new GetMultipleImagesAdvanced(), + this::onImagesChosen) : + registerForActivityResult(new GetImageAdvanced(), uri -> { + if (uri != null) onImagesChosen(singletonList(uri)); + }); private AttachmentRetriever attachmentRetriever; private ConversationViewModel viewModel; @@ -314,9 +324,6 @@ public class ConversationActivity extends BriarActivity .make(list, R.string.introduction_sent, Snackbar.LENGTH_SHORT) .show(); - } else if (request == REQUEST_ATTACH_IMAGE && result == RESULT_OK) { - // TODO: remove cast when removing feature flag - ((TextAttachmentController) sendController).onImageReceived(data); } } @@ -375,6 +382,10 @@ public class ConversationActivity extends BriarActivity if (!featureFlags.shouldEnableConnectViaBluetooth()) { menu.findItem(R.id.action_connect_via_bluetooth).setVisible(false); } + // Transfer Data feature only supported on API 19+ + if (SDK_INT >= 19 && featureFlags.shouldEnableTransferData()) { + menu.findItem(R.id.action_transfer_data).setVisible(true); + } // enable alias and bluetooth action once available observeOnce(viewModel.getContactItem(), this, contact -> { menu.findItem(R.id.action_set_alias).setEnabled(true); @@ -415,6 +426,11 @@ public class ConversationActivity extends BriarActivity new BluetoothConnecterDialogFragment().show(fm, BluetoothConnecterDialogFragment.TAG); return true; + } else if (itemId == R.id.action_transfer_data) { + Intent intent = new Intent(this, RemovableDriveActivity.class); + intent.putExtra(CONTACT_ID, contactId.getInt()); + startActivity(intent); + return true; } else if (itemId == R.id.action_delete_all_messages) { askToDeleteAllMessages(); return true; @@ -760,8 +776,13 @@ public class ConversationActivity extends BriarActivity } @Override - public void onAttachImage(Intent intent) { - startActivityForResult(intent, REQUEST_ATTACH_IMAGE); + public void onAttachImageClicked() { + launcher.launch("image/*"); + } + + private void onImagesChosen(@Nullable List uris) { + // TODO: remove cast when removing feature flag + ((TextAttachmentController) sendController).onImageReceived(uris); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java index 9a90f0dde..3a0b289a0 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java @@ -2,6 +2,7 @@ package org.briarproject.briar.android.conversation; import android.content.DialogInterface.OnClickListener; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.transition.Fade; import android.transition.Transition; @@ -21,6 +22,7 @@ import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.attachment.AttachmentItem; +import org.briarproject.briar.android.util.ActivityLaunchers.CreateDocumentAdvanced; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.PullDownLayout; @@ -28,6 +30,7 @@ import java.util.List; import javax.inject.Inject; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog.Builder; @@ -37,9 +40,6 @@ import androidx.lifecycle.ViewModelProvider; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; -import static android.content.Intent.ACTION_CREATE_DOCUMENT; -import static android.content.Intent.CATEGORY_OPENABLE; -import static android.content.Intent.EXTRA_TITLE; import static android.graphics.Color.TRANSPARENT; import static android.os.Build.VERSION.SDK_INT; import static android.view.View.GONE; @@ -50,7 +50,6 @@ import static android.view.View.VISIBLE; import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; import static java.util.Objects.requireNonNull; -import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SAVE_ATTACHMENT; import static org.briarproject.briar.android.util.UiUtils.formatDateAbsolute; import static org.briarproject.briar.android.util.UiUtils.getDialogIcon; @@ -79,6 +78,10 @@ public class ImageActivity extends BriarActivity private List attachments; private MessageId conversationMessageId; + private final ActivityResultLauncher launcher = + registerForActivityResult(new CreateDocumentAdvanced(), + this::onImageUriSelected); + @Override public void injectActivity(ActivityComponent component) { component.inject(this); @@ -175,16 +178,6 @@ public class ImageActivity extends BriarActivity layout.getViewTreeObserver().removeOnGlobalLayoutListener(this); } - @Override - protected void onActivityResult(int request, int result, - @Nullable Intent data) { - super.onActivityResult(request, result, data); - if (request == REQUEST_SAVE_ATTACHMENT && result == RESULT_OK && - data != null) { - viewModel.saveImage(getVisibleAttachment(), data.getData()); - } - } - @Override public void onPullStart() { appBarLayout.animate() @@ -268,8 +261,9 @@ public class ImageActivity extends BriarActivity private void showSaveImageDialog() { OnClickListener okListener = (dialog, which) -> { if (SDK_INT >= 19) { - Intent intent = getCreationIntent(); - startActivityForResult(intent, REQUEST_SAVE_ATTACHMENT); + String name = viewModel.getFileName() + "." + + getVisibleAttachment().getExtension(); + launcher.launch(name); } else { viewModel.saveImage(getVisibleAttachment()); } @@ -283,13 +277,9 @@ public class ImageActivity extends BriarActivity builder.show(); } - @RequiresApi(api = 19) - private Intent getCreationIntent() { - Intent intent = new Intent(ACTION_CREATE_DOCUMENT); - intent.addCategory(CATEGORY_OPENABLE); - intent.setType(getVisibleAttachment().getMimeType()); - intent.putExtra(EXTRA_TITLE, viewModel.getFileName()); - return intent; + private void onImageUriSelected(@Nullable Uri uri) { + if (uri == null) return; + viewModel.saveImage(getVisibleAttachment(), uri); } private void onImageSaveStateChanged(@Nullable Boolean error) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java index 80ab00150..84e9af5d8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java @@ -33,7 +33,6 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -45,6 +44,7 @@ import androidx.annotation.UiThread; import static android.media.MediaScannerConnection.scanFile; import static android.os.Environment.DIRECTORY_PICTURES; import static android.os.Environment.getExternalStoragePublicDirectory; +import static java.util.Locale.US; import static java.util.Objects.requireNonNull; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; @@ -111,7 +111,7 @@ public class ImageViewModel extends DbViewModel implements EventListener { } @UiThread - public void expectAttachments(List attachments) { + void expectAttachments(List attachments) { for (AttachmentItem item : attachments) { // no need to track items that are in a final state already if (item.getState().isFinal()) continue; @@ -226,8 +226,7 @@ public class ImageViewModel extends DbViewModel implements EventListener { } String getFileName() { - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", - Locale.getDefault()); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HHmmss", US); return sdf.format(new Date()); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/fragment/FinalFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/fragment/FinalFragment.java new file mode 100644 index 000000000..0cb4b4a8a --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/fragment/FinalFragment.java @@ -0,0 +1,120 @@ +package org.briarproject.briar.android.fragment; + +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ScrollView; +import android.widget.TextView; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.widget.ImageViewCompat; +import androidx.fragment.app.Fragment; + +import static android.view.View.FOCUS_DOWN; + +/** + * A fragment to be used at the end of a user flow + * where the user should not have the option to go back. + * Here, we only show final information + * before finishing the related activity. + */ +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class FinalFragment extends Fragment { + + public static final String TAG = FinalFragment.class.getName(); + + public static final String ARG_TITLE = "title"; + public static final String ARG_ICON = "icon"; + public static final String ARG_ICON_TINT = "iconTint"; + public static final String ARG_TEXT = "text"; + + public static FinalFragment newInstance( + @StringRes int title, + @DrawableRes int icon, + @ColorRes int iconTint, + @StringRes int text) { + FinalFragment f = new FinalFragment(); + Bundle args = new Bundle(); + args.putInt(ARG_TITLE, title); + args.putInt(ARG_ICON, icon); + args.putInt(ARG_ICON_TINT, iconTint); + args.putInt(ARG_TEXT, text); + f.setArguments(args); + return f; + } + + private ScrollView scrollView; + protected Button buttonView; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View v = inflater + .inflate(R.layout.fragment_final, container, false); + + scrollView = (ScrollView) v; + ImageView iconView = v.findViewById(R.id.iconView); + TextView titleView = v.findViewById(R.id.titleView); + TextView textView = v.findViewById(R.id.textView); + buttonView = v.findViewById(R.id.button); + + Bundle args = requireArguments(); + titleView.setText(args.getInt(ARG_TITLE)); + iconView.setImageResource(args.getInt(ARG_ICON)); + int color = getResources().getColor(args.getInt(ARG_ICON_TINT)); + ColorStateList tint = ColorStateList.valueOf(color); + ImageViewCompat.setImageTintList(iconView, tint); + textView.setText(args.getInt(ARG_TEXT)); + + buttonView.setOnClickListener(view -> onBackButtonPressed()); + + AppCompatActivity a = (AppCompatActivity) requireActivity(); + a.setTitle(args.getInt(ARG_TITLE)); + ActionBar actionBar = a.getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(false); + actionBar.setHomeButtonEnabled(false); + } + a.getOnBackPressedDispatcher().addCallback( + getViewLifecycleOwner(), new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + onBackButtonPressed(); + } + }); + return v; + } + + @Override + public void onStart() { + super.onStart(); + // Scroll down in case the screen is small, so the button is visible + scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN)); + } + + /** + * This is the action that the system back button + * and the button at the bottom will perform. + */ + protected void onBackButtonPressed() { + requireActivity().supportFinishAfterTransition(); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/ChooserFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/ChooserFragment.java new file mode 100644 index 000000000..d4aa38173 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/ChooserFragment.java @@ -0,0 +1,78 @@ +package org.briarproject.briar.android.removabledrive; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ScrollView; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; + +import javax.inject.Inject; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.ViewModelProvider; + +import static android.view.View.FOCUS_DOWN; +import static org.briarproject.briar.android.AppModule.getAndroidComponent; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class ChooserFragment extends Fragment { + + public final static String TAG = ChooserFragment.class.getName(); + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private RemovableDriveViewModel viewModel; + private ScrollView scrollView; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + FragmentActivity activity = requireActivity(); + getAndroidComponent(activity).inject(this); + viewModel = new ViewModelProvider(activity, viewModelFactory) + .get(RemovableDriveViewModel.class); + } + + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_transfer_data_chooser, + container, false); + + scrollView = (ScrollView) v; + Button sendButton = v.findViewById(R.id.sendButton); + sendButton.setOnClickListener(i -> viewModel.startSendData()); + + Button receiveButton = v.findViewById(R.id.receiveButton); + receiveButton.setOnClickListener(i -> viewModel.startReceiveData()); + + return v; + } + + @Override + public void onStart() { + super.onStart(); + requireActivity().setTitle(R.string.removable_drive_menu_title); + TransferDataState state = viewModel.getState().getValue(); + if (state instanceof TransferDataState.TaskAvailable) { + // we can't come back here now to start another task + // as we only support one per ViewModel instance + requireActivity().supportFinishAfterTransition(); + } else { + // Scroll down in case the screen is small, so the button is visible + scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN)); + } + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/ErrorFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/ErrorFragment.java new file mode 100644 index 000000000..c1bf9441c --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/ErrorFragment.java @@ -0,0 +1,54 @@ +package org.briarproject.briar.android.removabledrive; + +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.fragment.FinalFragment; + +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class ErrorFragment extends FinalFragment { + + public static ErrorFragment newInstance(@StringRes int title, + @StringRes int text) { + ErrorFragment f = new ErrorFragment(); + Bundle args = new Bundle(); + args.putInt(ARG_TITLE, title); + args.putInt(ARG_ICON, R.drawable.alerts_and_states_error); + args.putInt(ARG_ICON_TINT, R.color.briar_red_500); + args.putInt(ARG_TEXT, text); + f.setArguments(args); + return f; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View v = super.onCreateView(inflater, container, savedInstanceState); + buttonView.setText(R.string.try_again_button); + return v; + } + + @Override + protected void onBackButtonPressed() { + // Re-create this activity when going back in failed state. + // This will also re-create the ViewModel, so we start fresh. + Intent i = requireActivity().getIntent(); + i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); + startActivity(i); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/ReceiveFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/ReceiveFragment.java new file mode 100644 index 000000000..76db99280 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/ReceiveFragment.java @@ -0,0 +1,110 @@ +package org.briarproject.briar.android.removabledrive; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.ScrollView; +import android.widget.Toast; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.util.ActivityLaunchers.GetContentAdvanced; + +import javax.inject.Inject; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.ViewModelProvider; + +import static android.view.View.FOCUS_DOWN; +import static android.view.View.VISIBLE; +import static android.widget.Toast.LENGTH_LONG; +import static org.briarproject.briar.android.AppModule.getAndroidComponent; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class ReceiveFragment extends Fragment { + + final static String TAG = ReceiveFragment.class.getName(); + + private final ActivityResultLauncher launcher = + registerForActivityResult(new GetContentAdvanced(), + this::onDocumentChosen); + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private RemovableDriveViewModel viewModel; + private ScrollView scrollView; + private Button button; + private ProgressBar progressBar; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + FragmentActivity activity = requireActivity(); + getAndroidComponent(activity).inject(this); + viewModel = new ViewModelProvider(activity, viewModelFactory) + .get(RemovableDriveViewModel.class); + } + + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_transfer_data_receive, + container, false); + + scrollView = (ScrollView) v; + progressBar = v.findViewById(R.id.progressBar); + button = v.findViewById(R.id.fileButton); + button.setOnClickListener(view -> + launcher.launch("*/*") + ); + viewModel.getOldTaskResumedEvent() + .observeEvent(getViewLifecycleOwner(), this::onOldTaskResumed); + viewModel.getState() + .observe(getViewLifecycleOwner(), this::onStateChanged); + return v; + } + + @Override + public void onStart() { + super.onStart(); + requireActivity().setTitle(R.string.removable_drive_title_receive); + // Scroll down in case the screen is small, so the button is visible + scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN)); + } + + private void onOldTaskResumed(boolean resumed) { + if (resumed) { + Toast.makeText(requireContext(), + R.string.removable_drive_ongoing, LENGTH_LONG).show(); + } + } + + private void onStateChanged(TransferDataState state) { + if (state instanceof TransferDataState.NoDataToSend) { + throw new IllegalStateException(); + } else if (state instanceof TransferDataState.Ready) { + button.setEnabled(true); + } else if (state instanceof TransferDataState.TaskAvailable) { + button.setEnabled(false); + progressBar.setVisibility(VISIBLE); + } + } + + private void onDocumentChosen(@Nullable Uri uri) { + if (uri == null) return; + viewModel.importData(uri); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/RemovableDriveActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/RemovableDriveActivity.java new file mode 100644 index 000000000..501f458fb --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/RemovableDriveActivity.java @@ -0,0 +1,144 @@ +package org.briarproject.briar.android.removabledrive; + +import android.content.Intent; +import android.os.Bundle; +import android.view.MenuItem; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.plugin.file.RemovableDriveTask; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.activity.BriarActivity; +import org.briarproject.briar.android.fragment.FinalFragment; +import org.briarproject.briar.android.removabledrive.RemovableDriveViewModel.Action; +import org.briarproject.briar.android.removabledrive.TransferDataState.TaskAvailable; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.StringRes; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.ViewModelProvider; + +import static java.util.Objects.requireNonNull; +import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID; +import static org.briarproject.briar.android.util.UiUtils.showFragment; + +@RequiresApi(19) +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class RemovableDriveActivity extends BriarActivity { + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private RemovableDriveViewModel viewModel; + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + + viewModel = new ViewModelProvider(this, viewModelFactory) + .get(RemovableDriveViewModel.class); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = requireNonNull(getIntent()); + int contactId = intent.getIntExtra(CONTACT_ID, -1); + if (contactId == -1) throw new IllegalArgumentException("ContactId"); + viewModel.setContactId(new ContactId(contactId)); + + setContentView(R.layout.activity_fragment_container); + + viewModel.getActionEvent().observeEvent(this, this::onActionReceived); + viewModel.getState().observe(this, this::onStateChanged); + + if (savedInstanceState == null) { + Fragment f = new ChooserFragment(); + String tag = ChooserFragment.TAG; + getSupportFragmentManager().beginTransaction() + .replace(R.id.fragmentContainer, f, tag) + .commit(); + } + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void onActionReceived(Action action) { + Fragment f; + String tag; + if (action == Action.SEND) { + f = new SendFragment(); + tag = SendFragment.TAG; + } else if (action == Action.RECEIVE) { + f = new ReceiveFragment(); + tag = ReceiveFragment.TAG; + } else throw new AssertionError(); + showFragment(getSupportFragmentManager(), f, tag); + } + + private void onStateChanged(TransferDataState state) { + if (!(state instanceof TaskAvailable)) return; + RemovableDriveTask.State s = ((TaskAvailable) state).state; + if (s.isFinished()) { + FragmentManager fm = getSupportFragmentManager(); + Action action; + // We can't simply rely on viewModel.getActionEvent() + // as that might have been destroyed in the meantime. + if (fm.findFragmentByTag(SendFragment.TAG) != null) { + action = Action.SEND; + } else if (fm.findFragmentByTag(ReceiveFragment.TAG) != null) { + action = Action.RECEIVE; + } else { + action = requireNonNull( + viewModel.getActionEvent().getLastValue()); + } + Fragment f; + if (s.isSuccess()) f = getSuccessFragment(action); + else f = getErrorFragment(action); + showFragment(getSupportFragmentManager(), f, FinalFragment.TAG); + } + } + + private Fragment getSuccessFragment(Action action) { + @StringRes int title, text; + if (action == Action.SEND) { + title = R.string.removable_drive_success_send_title; + text = R.string.removable_drive_success_send_text; + } else if (action == Action.RECEIVE) { + title = R.string.removable_drive_success_receive_title; + text = R.string.removable_drive_success_receive_text; + } else throw new AssertionError(); + return FinalFragment.newInstance(title, + R.drawable.ic_check_circle_outline, R.color.briar_brand_green, + text); + } + + private Fragment getErrorFragment(Action action) { + @StringRes int title, text; + if (action == Action.SEND) { + title = R.string.removable_drive_error_send_title; + text = R.string.removable_drive_error_send_text; + } else if (action == Action.RECEIVE) { + title = R.string.removable_drive_error_receive_title; + text = R.string.removable_drive_error_receive_text; + } else throw new AssertionError(); + return ErrorFragment.newInstance(title, text); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/RemovableDriveViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/RemovableDriveViewModel.java new file mode 100644 index 000000000..cd64932fb --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/RemovableDriveViewModel.java @@ -0,0 +1,190 @@ +package org.briarproject.briar.android.removabledrive; + +import android.app.Application; +import android.net.Uri; + +import org.briarproject.bramble.api.Consumer; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.db.DatabaseExecutor; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.TransactionManager; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.file.RemovableDriveManager; +import org.briarproject.bramble.api.plugin.file.RemovableDriveTask; +import org.briarproject.bramble.api.plugin.file.RemovableDriveTask.State; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.android.viewmodel.DbViewModel; +import org.briarproject.briar.android.viewmodel.LiveEvent; +import org.briarproject.briar.android.viewmodel.MutableLiveEvent; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.Executor; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import androidx.annotation.UiThread; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import static java.util.Locale.US; +import static java.util.Objects.requireNonNull; +import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.PROP_URI; + +@UiThread +@NotNullByDefault +class RemovableDriveViewModel extends DbViewModel { + + enum Action {SEND, RECEIVE} + + private final RemovableDriveManager manager; + + private final MutableLiveEvent action = new MutableLiveEvent<>(); + private final MutableLiveEvent oldTaskResumed = + new MutableLiveEvent<>(); + private final MutableLiveData state = + new MutableLiveData<>(); + @Nullable + private ContactId contactId = null; + @Nullable + private RemovableDriveTask task = null; + @Nullable + private Consumer taskObserver = null; + + @Inject + RemovableDriveViewModel( + Application app, + @DatabaseExecutor Executor dbExecutor, + LifecycleManager lifecycleManager, + TransactionManager db, + AndroidExecutor androidExecutor, + RemovableDriveManager removableDriveManager) { + super(app, dbExecutor, lifecycleManager, db, androidExecutor); + this.manager = removableDriveManager; + } + + @Override + protected void onCleared() { + if (task != null) { + // when we have a task, we must have an observer for it + Consumer observer = requireNonNull(taskObserver); + task.removeObserver(observer); + } + } + + /** + * Set this as soon as it becomes available. + */ + void setContactId(ContactId contactId) { + this.contactId = contactId; + } + + @UiThread + void startSendData() { + action.setEvent(Action.SEND); + + // check if there is already a send/write task + task = manager.getCurrentWriterTask(); + if (task == null) { + // check if there's even something to send + ContactId c = requireNonNull(contactId); + runOnDbThread(() -> { + try { + if (!manager.isTransportSupportedByContact(c)) { + state.postValue(new TransferDataState.NotSupported()); + } else if (manager.isWriterTaskNeeded(c)) { + state.postValue(new TransferDataState.Ready()); + } else { + state.postValue(new TransferDataState.NoDataToSend()); + } + } catch (DbException e) { + handleException(e); + } + }); + } else { + // observe old task + taskObserver = + s -> state.setValue(new TransferDataState.TaskAvailable(s)); + task.addObserver(taskObserver); + oldTaskResumed.setEvent(true); + } + } + + @UiThread + void startReceiveData() { + action.setEvent(Action.RECEIVE); + + // check if there is already a receive/read task + task = manager.getCurrentReaderTask(); + if (task == null) { + state.setValue(new TransferDataState.Ready()); + } else { + // observe old task + taskObserver = + s -> state.setValue(new TransferDataState.TaskAvailable(s)); + task.addObserver(taskObserver); + oldTaskResumed.setEvent(true); + } + } + + String getFileName() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", US); + return sdf.format(new Date()); + } + + /** + * Call this only when in {@link TransferDataState.Ready}. + */ + @UiThread + void exportData(Uri uri) { + // starting an action more than once is not supported for simplicity + if (task != null) throw new IllegalStateException(); + + // from now on, we are not re-usable + // (because gets a state update right away on the UiThread) + taskObserver = + s -> state.setValue(new TransferDataState.TaskAvailable(s)); + + // start the writer task for this contact and observe it + TransportProperties p = new TransportProperties(); + p.put(PROP_URI, uri.toString()); + ContactId c = requireNonNull(contactId); + task = manager.startWriterTask(c, p); + task.addObserver(taskObserver); + } + + /** + * Call this only when in {@link TransferDataState.Ready}. + */ + @UiThread + void importData(Uri uri) { + // starting an action more than once is not supported for simplicity + if (task != null) throw new IllegalStateException(); + + // from now on, we are not re-usable + // (because gets a state update right away on the UiThread) + taskObserver = + s -> state.setValue(new TransferDataState.TaskAvailable(s)); + + TransportProperties p = new TransportProperties(); + p.put(PROP_URI, uri.toString()); + task = manager.startReaderTask(p); + task.addObserver(taskObserver); + } + + LiveEvent getActionEvent() { + return action; + } + + LiveEvent getOldTaskResumedEvent() { + return oldTaskResumed; + } + + LiveData getState() { + return state; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/SendFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/SendFragment.java new file mode 100644 index 000000000..1d8b872cb --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/SendFragment.java @@ -0,0 +1,133 @@ +package org.briarproject.briar.android.removabledrive; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.plugin.file.RemovableDriveTask; +import org.briarproject.briar.R; +import org.briarproject.briar.android.util.ActivityLaunchers.CreateDocumentAdvanced; + +import javax.inject.Inject; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.ViewModelProvider; + +import static android.os.Build.VERSION.SDK_INT; +import static android.view.View.FOCUS_DOWN; +import static android.view.View.VISIBLE; +import static android.widget.Toast.LENGTH_LONG; +import static org.briarproject.briar.android.AppModule.getAndroidComponent; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class SendFragment extends Fragment { + + final static String TAG = SendFragment.class.getName(); + + private final ActivityResultLauncher launcher = + registerForActivityResult(new CreateDocumentAdvanced(), + this::onDocumentCreated); + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private RemovableDriveViewModel viewModel; + private ScrollView scrollView; + private TextView introTextView; + private Button button; + private ProgressBar progressBar; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + FragmentActivity activity = requireActivity(); + getAndroidComponent(activity).inject(this); + viewModel = new ViewModelProvider(activity, viewModelFactory) + .get(RemovableDriveViewModel.class); + } + + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_transfer_data_send, + container, false); + + scrollView = (ScrollView) v; + introTextView = v.findViewById(R.id.introTextView); + progressBar = v.findViewById(R.id.progressBar); + button = v.findViewById(R.id.fileButton); + button.setOnClickListener(view -> + launcher.launch(viewModel.getFileName()) + ); + + viewModel.getOldTaskResumedEvent() + .observeEvent(getViewLifecycleOwner(), this::onOldTaskResumed); + viewModel.getState() + .observe(getViewLifecycleOwner(), this::onStateChanged); + + return v; + } + + @Override + public void onStart() { + super.onStart(); + requireActivity().setTitle(R.string.removable_drive_title_send); + // Scroll down in case the screen is small, so the button is visible + scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN)); + } + + private void onOldTaskResumed(boolean resumed) { + if (resumed) { + Toast.makeText(requireContext(), + R.string.removable_drive_ongoing, LENGTH_LONG).show(); + } + } + + private void onStateChanged(TransferDataState state) { + if (state instanceof TransferDataState.NoDataToSend) { + introTextView.setText(R.string.removable_drive_send_no_data); + button.setEnabled(false); + } else if (state instanceof TransferDataState.NotSupported) { + introTextView.setText(R.string.removable_drive_send_not_supported); + button.setEnabled(false); + } else if (state instanceof TransferDataState.Ready) { + button.setEnabled(true); + } else if (state instanceof TransferDataState.TaskAvailable) { + button.setEnabled(false); + RemovableDriveTask.State s = + ((TransferDataState.TaskAvailable) state).state; + if (s.getTotal() > 0L && progressBar.getVisibility() != VISIBLE) { + progressBar.setVisibility(VISIBLE); + progressBar.setMax(100); + } + int progress = s.getTotal() == 0 ? 0 : // no div by null + (int) ((double) s.getDone() / s.getTotal() * 100); + if (SDK_INT >= 24) { + progressBar.setProgress(progress, true); + } else { + progressBar.setProgress(progress); + } + } + } + + private void onDocumentCreated(@Nullable Uri uri) { + if (uri == null) return; + viewModel.exportData(uri); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/TransferDataModule.java b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/TransferDataModule.java new file mode 100644 index 000000000..5e2d03c2b --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/TransferDataModule.java @@ -0,0 +1,19 @@ +package org.briarproject.briar.android.removabledrive; + +import org.briarproject.briar.android.viewmodel.ViewModelKey; + +import androidx.lifecycle.ViewModel; +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.IntoMap; + +@Module +public interface TransferDataModule { + + @Binds + @IntoMap + @ViewModelKey(RemovableDriveViewModel.class) + ViewModel bindRemovableDriveViewModel( + RemovableDriveViewModel removableDriveViewModel); + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/TransferDataState.java b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/TransferDataState.java new file mode 100644 index 000000000..e8ac85058 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/TransferDataState.java @@ -0,0 +1,40 @@ +package org.briarproject.briar.android.removabledrive; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.file.RemovableDriveTask; + +@NotNullByDefault +abstract class TransferDataState { + + /** + * There is nothing we can send to the chosen contact. + * This only applies to sending data, but not to receiving it. + */ + static class NoDataToSend extends TransferDataState { + } + + /** + * The chosen contact does not support the transport, yet. + * So we can't send them data this way. + */ + static class NotSupported extends TransferDataState { + } + + /** + * We are ready to let the user select a file for sending or receiving data. + */ + static class Ready extends TransferDataState { + } + + /** + * A task with state information is available and should be shown in the UI. + */ + static class TaskAvailable extends TransferDataState { + final RemovableDriveTask.State state; + + TaskAvailable(RemovableDriveTask.State state) { + this.state = state; + } + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java index 591f9031c..5f4fd2f16 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.settings; import android.content.Context; -import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; @@ -9,9 +8,11 @@ import android.view.View; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; +import org.briarproject.briar.android.util.ActivityLaunchers.GetImageAdvanced; import javax.inject.Inject; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; @@ -20,12 +21,9 @@ import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceGroup; -import static android.app.Activity.RESULT_OK; import static java.util.Objects.requireNonNull; import static org.briarproject.briar.android.AppModule.getAndroidComponent; import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; -import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_AVATAR_IMAGE; -import static org.briarproject.briar.android.util.UiUtils.createSelectImageIntent; import static org.briarproject.briar.android.util.UiUtils.triggerFeedback; @MethodsNotNullByDefault @@ -46,6 +44,10 @@ public class SettingsFragment extends PreferenceFragmentCompat { private SettingsViewModel viewModel; private AvatarPreference prefAvatar; + private final ActivityResultLauncher launcher = + registerForActivityResult(new GetImageAdvanced(), + this::onImageSelected); + @Override public void onAttach(@NonNull Context context) { super.onAttach(context); @@ -61,8 +63,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { prefAvatar = requireNonNull(findPreference(PREF_KEY_AVATAR)); if (viewModel.shouldEnableProfilePictures()) { prefAvatar.setOnPreferenceClickListener(preference -> { - Intent intent = createSelectImageIntent(false); - startActivityForResult(intent, REQUEST_AVATAR_IMAGE); + launcher.launch("image/*"); return true; }); } else { @@ -109,20 +110,11 @@ public class SettingsFragment extends PreferenceFragmentCompat { requireActivity().setTitle(R.string.settings_button); } - @Override - public void onActivityResult(int request, int result, - @Nullable Intent data) { - super.onActivityResult(request, result, data); - if (request == REQUEST_AVATAR_IMAGE && result == RESULT_OK) { - if (data == null) return; - Uri uri = data.getData(); - if (uri == null) return; - - DialogFragment dialog = - ConfirmAvatarDialogFragment.newInstance(uri); - dialog.show(getParentFragmentManager(), - ConfirmAvatarDialogFragment.TAG); - } + private void onImageSelected(@Nullable Uri uri) { + if (uri == null) return; + DialogFragment dialog = ConfirmAvatarDialogFragment.newInstance(uri); + dialog.show(getParentFragmentManager(), + ConfirmAvatarDialogFragment.TAG); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/ActivityLaunchers.java b/briar-android/src/main/java/org/briarproject/briar/android/util/ActivityLaunchers.java new file mode 100644 index 000000000..cd7456c89 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/ActivityLaunchers.java @@ -0,0 +1,88 @@ +package org.briarproject.briar.android.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import androidx.activity.result.contract.ActivityResultContract; +import androidx.activity.result.contract.ActivityResultContracts.CreateDocument; +import androidx.activity.result.contract.ActivityResultContracts.GetContent; +import androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents; +import androidx.annotation.Nullable; + +import static android.app.Activity.RESULT_CANCELED; +import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE; +import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION; +import static android.content.Intent.EXTRA_MIME_TYPES; +import static android.os.Build.VERSION.SDK_INT; +import static org.briarproject.bramble.util.AndroidUtils.getSupportedImageContentTypes; + +@NotNullByDefault +public class ActivityLaunchers { + + public static class CreateDocumentAdvanced extends CreateDocument { + @Override + public Intent createIntent(Context context, String input) { + Intent i = super.createIntent(context, input); + putShowAdvancedExtra(i); + return i; + } + } + + public static class GetContentAdvanced extends GetContent { + @Override + public Intent createIntent(Context context, String input) { + Intent i = super.createIntent(context, input); + putShowAdvancedExtra(i); + return i; + } + } + + public static class GetImageAdvanced extends GetContent { + @Override + public Intent createIntent(Context context, String input) { + Intent i = super.createIntent(context, input); + putShowAdvancedExtra(i); + i.setType("image/*"); + if (SDK_INT >= 19) + i.putExtra(EXTRA_MIME_TYPES, getSupportedImageContentTypes()); + return i; + } + } + + @TargetApi(18) + public static class GetMultipleImagesAdvanced extends GetMultipleContents { + @Override + public Intent createIntent(Context context, String input) { + Intent i = super.createIntent(context, input); + putShowAdvancedExtra(i); + i.setType("image/*"); + if (SDK_INT >= 19) + i.putExtra(EXTRA_MIME_TYPES, getSupportedImageContentTypes()); + return i; + } + } + + public static class RequestBluetoothDiscoverable + extends ActivityResultContract { + @Override + public Intent createIntent(Context context, Integer duration) { + Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE); + i.putExtra(EXTRA_DISCOVERABLE_DURATION, duration); + return i; + } + + @Override + public Boolean parseResult(int resultCode, @Nullable Intent intent) { + return resultCode != RESULT_CANCELED; + } + } + + private static void putShowAdvancedExtra(Intent i) { + i.putExtra(SDK_INT <= 28 ? "android.content.extra.SHOW_ADVANCED" : + "android.provider.extra.SHOW_ADVANCED", true); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/RequestBluetoothDiscoverable.java b/briar-android/src/main/java/org/briarproject/briar/android/util/RequestBluetoothDiscoverable.java deleted file mode 100644 index 8288aedac..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/RequestBluetoothDiscoverable.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.briarproject.briar.android.util; - - -import android.content.Context; -import android.content.Intent; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; - -import androidx.activity.result.contract.ActivityResultContract; -import androidx.annotation.Nullable; - -import static android.app.Activity.RESULT_CANCELED; -import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE; -import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION; - -@NotNullByDefault -public class RequestBluetoothDiscoverable - extends ActivityResultContract { - - @Override - public Intent createIntent(Context context, Integer duration) { - Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE); - i.putExtra(EXTRA_DISCOVERABLE_DURATION, duration); - return i; - } - - @Override - public Boolean parseResult(int resultCode, @Nullable Intent intent) { - return resultCode != RESULT_CANCELED; - } -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index 66378bf22..a27552486 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -62,6 +62,7 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; +import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; @@ -69,12 +70,7 @@ import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; import static android.content.Context.KEYGUARD_SERVICE; import static android.content.Context.POWER_SERVICE; -import static android.content.Intent.ACTION_GET_CONTENT; -import static android.content.Intent.ACTION_OPEN_DOCUMENT; import static android.content.Intent.CATEGORY_DEFAULT; -import static android.content.Intent.CATEGORY_OPENABLE; -import static android.content.Intent.EXTRA_ALLOW_MULTIPLE; -import static android.content.Intent.EXTRA_MIME_TYPES; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.os.Build.MANUFACTURER; import static android.os.Build.VERSION.SDK_INT; @@ -112,7 +108,6 @@ import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_RTL; import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.DAYS; import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.AndroidUtils.getSupportedImageContentTypes; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.briar.BuildConfig.APPLICATION_ID; import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE; @@ -321,18 +316,6 @@ public class UiUtils { }; } - public static Intent createSelectImageIntent(boolean allowMultiple) { - Intent intent = new Intent(SDK_INT >= 19 ? - ACTION_OPEN_DOCUMENT : ACTION_GET_CONTENT); - intent.setType("image/*"); - intent.addCategory(CATEGORY_OPENABLE); - if (SDK_INT >= 19) - intent.putExtra(EXTRA_MIME_TYPES, getSupportedImageContentTypes()); - if (allowMultiple && SDK_INT >= 18) - intent.putExtra(EXTRA_ALLOW_MULTIPLE, true); - return intent; - } - public static void showOnboardingDialog(Context ctx, String text) { new AlertDialog.Builder(ctx, R.style.OnboardingDialogTheme) .setMessage(text) @@ -358,6 +341,11 @@ public class UiUtils { return i; } + public static void putShowAdvancedExtra(Intent i) { + i.putExtra(SDK_INT <= 28 ? "android.content.extra.SHOW_ADVANCED" : + "android.provider.extra.SHOW_ADVANCED", true); + } + /** * @return true if location is enabled, * or it isn't required due to this being a SDK < 28 device. diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java b/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java index 9cb3210f3..f6b751412 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.view; import android.app.Activity; -import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; @@ -15,13 +14,13 @@ import org.briarproject.briar.R; import org.briarproject.briar.android.attachment.AttachmentItemResult; import org.briarproject.briar.android.attachment.AttachmentManager; import org.briarproject.briar.android.attachment.AttachmentResult; -import org.briarproject.briar.android.util.UiUtils; import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.appcompat.app.AlertDialog.Builder; @@ -31,7 +30,6 @@ import androidx.lifecycle.Observer; import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt; -import static android.os.Build.VERSION.SDK_INT; import static android.view.View.GONE; import static android.widget.Toast.LENGTH_LONG; import static androidx.core.content.ContextCompat.getColor; @@ -143,38 +141,23 @@ public class TextAttachmentController extends TextSendController builder.show(); return; } - Intent intent = UiUtils.createSelectImageIntent(true); if (attachmentListener.getLifecycle().getCurrentState() != DESTROYED) { - attachmentListener.onAttachImage(intent); + attachmentListener.onAttachImageClicked(); } } /** - * This is called with the result Intent returned by the Activity started - * with {@link UiUtils#createSelectImageIntent(boolean)}. - *

* This method must be called at most once per call to - * {@link AttachmentListener#onAttachImage(Intent)}. - * Normally, this is true if called from + * {@link AttachmentListener#onAttachImageClicked()}. + * Normally, this is true if called from the launcher equivalent of * {@link Activity#onActivityResult(int, int, Intent)} since this is called - * at most once per call to - * {@link Activity#startActivityForResult(Intent, int)}. + * at most once per call to {@link ActivityResultLauncher#launch(Object)}. */ @SuppressWarnings("JavadocReference") - public void onImageReceived(@Nullable Intent resultData) { - if (resultData == null) return; + public void onImageReceived(@Nullable List newUris) { + if (newUris == null) return; if (loadingUris || !imageUris.isEmpty()) throw new AssertionError(); - List newUris = new ArrayList<>(); - if (resultData.getData() != null) { - newUris.add(resultData.getData()); - onNewUris(false, newUris); - } else if (SDK_INT >= 18 && resultData.getClipData() != null) { - ClipData clipData = resultData.getClipData(); - for (int i = 0; i < clipData.getItemCount(); i++) { - newUris.add(clipData.getItemAt(i).getUri()); - } - onNewUris(false, newUris); - } + onNewUris(false, newUris); } private void onNewUris(boolean restart, List newUris) { @@ -329,7 +312,7 @@ public class TextAttachmentController extends TextSendController @UiThread public interface AttachmentListener extends SendListener { - void onAttachImage(Intent intent); + void onAttachImageClicked(); void onTooManyAttachments(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java index 087e95f64..6e39b347c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java @@ -23,7 +23,6 @@ import java.util.logging.Logger; import javax.annotation.concurrent.Immutable; import androidx.annotation.AnyThread; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.arch.core.util.Function; @@ -50,7 +49,7 @@ public abstract class DbViewModel extends AndroidViewModel { protected final AndroidExecutor androidExecutor; public DbViewModel( - @NonNull Application application, + Application application, @DatabaseExecutor Executor dbExecutor, LifecycleManager lifecycleManager, TransactionManager db, diff --git a/briar-android/src/main/res/drawable/ic_arrow_back.xml b/briar-android/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 000000000..3d25eefc0 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,11 @@ + + + diff --git a/briar-android/src/main/res/drawable/ic_check_circle_outline.xml b/briar-android/src/main/res/drawable/ic_check_circle_outline.xml new file mode 100644 index 000000000..bfac35d68 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_check_circle_outline.xml @@ -0,0 +1,10 @@ + + + diff --git a/briar-android/src/main/res/drawable/ic_transfer_data.xml b/briar-android/src/main/res/drawable/ic_transfer_data.xml new file mode 100644 index 000000000..68d313e52 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_transfer_data.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/briar-android/src/main/res/drawable/ic_transfer_data_receive.xml b/briar-android/src/main/res/drawable/ic_transfer_data_receive.xml new file mode 100644 index 000000000..87d43457f --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_transfer_data_receive.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/briar-android/src/main/res/drawable/ic_transfer_data_send.xml b/briar-android/src/main/res/drawable/ic_transfer_data_send.xml new file mode 100644 index 000000000..59b6f35ba --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_transfer_data_send.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/briar-android/src/main/res/layout/fragment_final.xml b/briar-android/src/main/res/layout/fragment_final.xml new file mode 100644 index 000000000..7bf37a536 --- /dev/null +++ b/briar-android/src/main/res/layout/fragment_final.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + +