Merge tag 'release-1.3.6' into briar-as-subproject

release-1.3.6
This commit is contained in:
ialokim
2021-09-09 13:16:27 +02:00
1014 changed files with 42340 additions and 15502 deletions

View File

@@ -17,16 +17,17 @@ dependencies {
implementation 'org.whispersystems:curve25519-java:0.5.0'
implementation 'org.briarproject:jtorctl:0.3'
annotationProcessor 'com.google.dagger:dagger-compiler:2.24'
annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
testImplementation project(path: ':briar:bramble-api', configuration: 'testOutput')
testImplementation 'org.hsqldb:hsqldb:2.3.5' // The last version that supports Java 1.6
testImplementation 'junit:junit:4.12'
testImplementation "org.jmock:jmock:2.8.2"
testImplementation "org.jmock:jmock-junit4:2.8.2"
testImplementation "org.jmock:jmock-legacy:2.8.2"
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"
testImplementation "org.jmock:jmock-legacy:$jmock_version"
testAnnotationProcessor 'com.google.dagger:dagger-compiler:2.24'
testAnnotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
signature 'org.codehaus.mojo.signature:java16:1.1@signature'
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble;
import org.briarproject.bramble.cleanup.CleanupModule;
import org.briarproject.bramble.contact.ContactModule;
import org.briarproject.bramble.crypto.CryptoExecutorModule;
import org.briarproject.bramble.db.DatabaseExecutorModule;
@@ -10,10 +11,13 @@ 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 {
void inject(CleanupModule.EagerSingletons init);
void inject(ContactModule.EagerSingletons init);
void inject(CryptoExecutorModule.EagerSingletons init);
@@ -30,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);
@@ -39,6 +45,7 @@ public interface BrambleCoreEagerSingletons {
class Helper {
public static void injectEagerSingletons(BrambleCoreEagerSingletons c) {
c.inject(new CleanupModule.EagerSingletons());
c.inject(new ContactModule.EagerSingletons());
c.inject(new CryptoExecutorModule.EagerSingletons());
c.inject(new DatabaseExecutorModule.EagerSingletons());
@@ -47,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());

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble;
import org.briarproject.bramble.cleanup.CleanupModule;
import org.briarproject.bramble.client.ClientModule;
import org.briarproject.bramble.connection.ConnectionModule;
import org.briarproject.bramble.contact.ContactModule;
@@ -21,15 +22,15 @@ import org.briarproject.bramble.rendezvous.RendezvousModule;
import org.briarproject.bramble.settings.SettingsModule;
import org.briarproject.bramble.sync.SyncModule;
import org.briarproject.bramble.sync.validation.ValidationModule;
import org.briarproject.bramble.system.ClockModule;
import org.briarproject.bramble.transport.TransportModule;
import org.briarproject.bramble.transport.agreement.TransportKeyAgreementModule;
import org.briarproject.bramble.versioning.VersioningModule;
import dagger.Module;
@Module(includes = {
CleanupModule.class,
ClientModule.class,
ClockModule.class,
ConnectionModule.class,
ContactModule.class,
CryptoModule.class,
@@ -49,6 +50,7 @@ import dagger.Module;
RendezvousModule.class,
SettingsModule.class,
SyncModule.class,
TransportKeyAgreementModule.class,
TransportModule.class,
ValidationModule.class,
VersioningModule.class

View File

@@ -0,0 +1,159 @@
package org.briarproject.bramble.cleanup;
import org.briarproject.bramble.api.cleanup.CleanupHook;
import org.briarproject.bramble.api.cleanup.CleanupManager;
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.Service;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.TaskScheduler;
import org.briarproject.bramble.api.versioning.ClientMajorVersion;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import static java.lang.Math.max;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
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.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
import static org.briarproject.bramble.util.LogUtils.logException;
@ThreadSafe
@NotNullByDefault
class CleanupManagerImpl implements CleanupManager, Service, EventListener {
private static final Logger LOG =
getLogger(CleanupManagerImpl.class.getName());
private final Executor dbExecutor;
private final DatabaseComponent db;
private final TaskScheduler taskScheduler;
private final Clock clock;
private final Map<ClientMajorVersion, CleanupHook> hooks =
new ConcurrentHashMap<>();
private final Object lock = new Object();
@GuardedBy("lock")
private final Set<CleanupTask> pending = new HashSet<>();
@Inject
CleanupManagerImpl(@DatabaseExecutor Executor dbExecutor,
DatabaseComponent db, TaskScheduler taskScheduler, Clock clock) {
this.dbExecutor = dbExecutor;
this.db = db;
this.taskScheduler = taskScheduler;
this.clock = clock;
}
@Override
public void registerCleanupHook(ClientId c, int majorVersion,
CleanupHook hook) {
hooks.put(new ClientMajorVersion(c, majorVersion), hook);
}
@Override
public void startService() {
maybeScheduleTask(clock.currentTimeMillis());
}
@Override
public void stopService() {
}
@Override
public void eventOccurred(Event e) {
if (e instanceof CleanupTimerStartedEvent) {
CleanupTimerStartedEvent a = (CleanupTimerStartedEvent) e;
maybeScheduleTask(a.getCleanupDeadline());
}
}
private void maybeScheduleTask(long deadline) {
synchronized (lock) {
for (CleanupTask task : pending) {
if (task.deadline <= deadline) return;
}
CleanupTask task = new CleanupTask(deadline);
pending.add(task);
scheduleTask(task);
}
}
private void scheduleTask(CleanupTask task) {
long now = clock.currentTimeMillis();
long delay = max(0, task.deadline - now + BATCH_DELAY_MS);
if (LOG.isLoggable(INFO)) {
LOG.info("Scheduling cleanup task in " + delay + " ms");
}
taskScheduler.schedule(() -> deleteMessagesAndScheduleNextTask(task),
dbExecutor, delay, MILLISECONDS);
}
private void deleteMessagesAndScheduleNextTask(CleanupTask task) {
try {
synchronized (lock) {
pending.remove(task);
}
long deadline = db.transactionWithResult(false, txn -> {
deleteMessages(txn);
return db.getNextCleanupDeadline(txn);
});
if (deadline != NO_CLEANUP_DEADLINE) {
maybeScheduleTask(deadline);
}
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
private void deleteMessages(Transaction txn) throws DbException {
Map<GroupId, Collection<MessageId>> ids = db.getMessagesToDelete(txn);
for (Entry<GroupId, Collection<MessageId>> e : ids.entrySet()) {
GroupId groupId = e.getKey();
Collection<MessageId> messageIds = e.getValue();
if (LOG.isLoggable(INFO)) {
LOG.info(messageIds.size() + " messages to delete");
}
for (MessageId m : messageIds) db.stopCleanupTimer(txn, m);
Group group = db.getGroup(txn, groupId);
ClientMajorVersion cv = new ClientMajorVersion(group.getClientId(),
group.getMajorVersion());
CleanupHook hook = hooks.get(cv);
if (hook == null) {
throw new IllegalStateException("No cleanup hook for " + cv);
}
hook.deleteMessages(txn, groupId, messageIds);
}
}
private static class CleanupTask {
private final long deadline;
private CleanupTask(long deadline) {
this.deadline = deadline;
}
}
}

View File

@@ -0,0 +1,29 @@
package org.briarproject.bramble.cleanup;
import org.briarproject.bramble.api.cleanup.CleanupManager;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import javax.inject.Inject;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class CleanupModule {
public static class EagerSingletons {
@Inject
CleanupManager cleanupManager;
}
@Provides
@Singleton
CleanupManager provideCleanupManager(LifecycleManager lifecycleManager,
EventBus eventBus, CleanupManagerImpl cleanupManager) {
lifecycleManager.registerService(cleanupManager);
eventBus.addListener(cleanupManager);
return cleanupManager;
}
}

View File

@@ -2,11 +2,13 @@ package org.briarproject.bramble.client;
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.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.KeyParser;
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.data.BdfEntry;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.data.BdfReader;
import org.briarproject.bramble.api.data.BdfReaderFactory;
@@ -32,6 +34,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
@@ -39,6 +42,7 @@ import java.util.Map.Entry;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static org.briarproject.bramble.api.client.ContactGroupConstants.GROUP_KEY_CONTACT_ID;
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
@@ -151,6 +155,12 @@ class ClientHelperImpl implements ClientHelper {
return metadataParser.parse(metadata);
}
@Override
public Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
BdfDictionary query) throws DbException, FormatException {
return db.getMessageIds(txn, g, metadataEncoder.encode(query));
}
@Override
public BdfDictionary getMessageMetadataAsDictionary(MessageId m)
throws DbException, FormatException {
@@ -389,4 +399,27 @@ class ClientHelperImpl implements ClientHelper {
return tpMap;
}
@Override
public ContactId getContactId(Transaction txn, GroupId contactGroupId)
throws DbException {
try {
BdfDictionary meta =
getGroupMetadataAsDictionary(txn, contactGroupId);
return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
} catch (FormatException e) {
throw new DbException(e); // Invalid group metadata
}
}
@Override
public void setContactId(Transaction txn, GroupId contactGroupId,
ContactId c) throws DbException {
BdfDictionary meta = BdfDictionary.of(
new BdfEntry(GROUP_KEY_CONTACT_ID, c.getInt()));
try {
mergeGroupMetadata(txn, contactGroupId, meta);
} catch (FormatException e) {
throw new AssertionError(e);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -20,9 +20,7 @@ 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.AuthorId;
import org.briarproject.bramble.api.identity.AuthorInfo;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.transport.KeyManager;
@@ -40,10 +38,6 @@ import javax.inject.Inject;
import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.OURSELVES;
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNKNOWN;
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNVERIFIED;
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.VERIFIED;
import static org.briarproject.bramble.util.StringUtils.toUtf8;
@ThreadSafe
@@ -213,6 +207,11 @@ class ContactManagerImpl implements ContactManager, EventListener {
return db.transactionWithResult(true, db::getContacts);
}
@Override
public Collection<Contact> getContacts(Transaction txn) throws DbException {
return db.getContacts(txn);
}
@Override
public void removeContact(ContactId c) throws DbException {
db.transaction(false, txn -> removeContact(txn, c));
@@ -256,25 +255,6 @@ class ContactManagerImpl implements ContactManager, EventListener {
db.removeContact(txn, c);
}
@Override
public AuthorInfo getAuthorInfo(AuthorId a) throws DbException {
return db.transactionWithResult(true, txn -> getAuthorInfo(txn, a));
}
@Override
public AuthorInfo getAuthorInfo(Transaction txn, AuthorId authorId)
throws DbException {
LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
if (localAuthor.getId().equals(authorId))
return new AuthorInfo(OURSELVES);
Collection<Contact> contacts = db.getContactsByAuthorId(txn, authorId);
if (contacts.isEmpty()) return new AuthorInfo(UNKNOWN);
if (contacts.size() > 1) throw new AssertionError();
Contact c = contacts.iterator().next();
if (c.isVerified()) return new AuthorInfo(VERIFIED, c.getAlias());
else return new AuthorInfo(UNVERIFIED, c.getAlias());
}
@Override
public void eventOccurred(Event e) {
if (e instanceof PendingContactStateChangedEvent) {

View File

@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.util.Base32;
import java.security.GeneralSecurityException;
import java.util.Locale;
import java.util.regex.Matcher;
import javax.inject.Inject;
@@ -52,7 +53,7 @@ class PendingContactFactoryImpl implements PendingContactFactory {
byte[] raw = new byte[RAW_LINK_BYTES];
raw[0] = FORMAT_VERSION;
arraycopy(encoded, 0, raw, 1, encoded.length);
return "briar://" + Base32.encode(raw).toLowerCase();
return "briar://" + Base32.encode(raw).toLowerCase(Locale.US);
}
private PublicKey parseHandshakeLink(String link) throws FormatException {

View File

@@ -9,14 +9,16 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import static java.lang.Math.min;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.now;
class ScryptKdf implements PasswordBasedKdf {
private static final Logger LOG =
Logger.getLogger(ScryptKdf.class.getName());
getLogger(ScryptKdf.class.getName());
private static final int MIN_COST = 256; // Min parameter N
private static final int MAX_COST = 1024 * 1024; // Max parameter N
@@ -33,10 +35,20 @@ class ScryptKdf implements PasswordBasedKdf {
@Override
public int chooseCostParameter() {
// Scrypt uses at least 128 * N * r bytes of memory. Don't use more
// than half of the JVM's max heap size or we may run out of memory.
// https://blog.filippo.io/the-scrypt-parameters/
long maxMemory = Runtime.getRuntime().maxMemory();
long maxCost = min(MAX_COST, maxMemory / BLOCK_SIZE / 256);
if (LOG.isLoggable(INFO) && maxCost < MAX_COST) {
LOG.info("Max cost capped at " + maxCost
+ " due to max heap size " + maxMemory);
}
// Increase the cost from min to max while measuring performance
int cost = MIN_COST;
while (cost * 2 <= MAX_COST && measureDuration(cost) * 2 <= TARGET_MS)
while (cost * 2 <= maxCost && measureDuration(cost) * 2 <= TARGET_MS) {
cost *= 2;
}
if (LOG.isLoggable(INFO))
LOG.info("KDF cost parameter " + cost);
return cost;

View File

@@ -36,4 +36,10 @@ class StreamDecrypterFactoryImpl implements StreamDecrypterFactory {
SecretKey headerKey) {
return new StreamDecrypterImpl(in, cipherProvider.get(), 0, headerKey);
}
@Override
public StreamDecrypter createLogStreamDecrypter(InputStream in,
SecretKey headerKey) {
return createContactExchangeStreamDecrypter(in, headerKey);
}
}

View File

@@ -51,7 +51,7 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
}
@Override
public StreamEncrypter createContactExchangeStreamDecrypter(
public StreamEncrypter createContactExchangeStreamEncrypter(
OutputStream out, SecretKey headerKey) {
AuthenticatedCipher cipher = cipherProvider.get();
byte[] streamHeaderNonce = new byte[STREAM_HEADER_NONCE_LENGTH];
@@ -60,4 +60,10 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
return new StreamEncrypterImpl(out, cipher, 0, null, streamHeaderNonce,
headerKey, frameKey);
}
@Override
public StreamEncrypter createLogStreamEncrypter(OutputStream out,
SecretKey headerKey) {
return createContactExchangeStreamEncrypter(out, headerKey);
}
}

View File

@@ -68,6 +68,13 @@ interface Database<T> {
*/
void close() throws DbException;
/**
* Returns true if the dirty flag was set while opening the database,
* indicating that the database has not been shut down properly the last
* time it was closed and some data could be lost.
*/
boolean wasDirtyOnInitialisation();
/**
* Starts a new transaction and returns an object representing it.
*/
@@ -138,7 +145,7 @@ interface Database<T> {
/**
* Stores a transport.
*/
void addTransport(T txn, TransportId t, int maxLatency)
void addTransport(T txn, TransportId t, long maxLatency)
throws DbException;
/**
@@ -155,6 +162,18 @@ interface Database<T> {
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.
* <p/>
* 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.
@@ -208,6 +227,16 @@ interface Database<T> {
*/
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.
* <p/>
* 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
@@ -454,7 +483,7 @@ interface Database<T> {
* Read-only.
*/
Collection<MessageId> 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
@@ -469,10 +498,36 @@ interface Database<T> {
* Returns the IDs of some messages that are eligible to be sent to the
* given contact, up to the given total length.
* <p/>
* Unlike {@link #getUnackedMessagesToSend(Object, ContactId)} this method
* does not return messages that have already been sent unless they are
* due for retransmission.
* <p/>
* Read-only.
*/
Collection<MessageId> 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.
* <p/>
* Unlike {@link #getMessagesToSend(Object, ContactId, int, long)} this
* method may return messages that have already been sent and are not yet
* due for retransmission.
* <p/>
* Read-only.
*/
Map<MessageId, Integer> 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.
* <p/>
* Read-only.
*/
long getUnackedMessageBytesToSend(T txn, ContactId c) throws DbException;
/**
* Returns the IDs of any messages that need to be validated.
@@ -497,6 +552,25 @@ interface Database<T> {
*/
Collection<MessageId> getMessagesToShare(T txn) throws DbException;
/**
* Returns the IDs of any messages of any messages that are due for
* deletion, along with their group IDs.
* <p/>
* Read-only.
*/
Map<GroupId, Collection<MessageId>> getMessagesToDelete(T txn)
throws DbException;
/**
* Returns the next time (in milliseconds since the Unix epoch) when a
* message is due to be deleted, or
* {@link DatabaseComponent#NO_CLEANUP_DEADLINE} if no messages are
* scheduled to be deleted.
* <p/>
* Read-only.
*/
long getNextCleanupDeadline(T txn) throws DbException;
/**
* Returns the next time (in milliseconds since the Unix epoch) when a
* message is due to be sent to the given contact. The returned value may
@@ -530,7 +604,7 @@ interface Database<T> {
* Read-only.
*/
Collection<MessageId> getRequestedMessagesToSend(T txn, ContactId c,
int maxLength, int maxLatency) throws DbException;
int maxLength, long maxLatency) throws DbException;
/**
* Returns all settings in the given namespace.
@@ -554,6 +628,16 @@ interface Database<T> {
Collection<TransportKeySet> 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.
* <p/>
* Read-only.
*/
Map<ContactId, Collection<TransportId>> getTransportsWithKeys(T txn)
throws DbException;
/**
* Increments the outgoing stream counter for the given transport keys.
*/
@@ -606,8 +690,10 @@ interface Database<T> {
/**
* Marks a message as having been seen by the given contact.
*
* @return True if the message was not already marked as seen.
*/
void raiseSeenFlag(T txn, ContactId c, MessageId m) throws DbException;
boolean raiseSeenFlag(T txn, ContactId c, MessageId m) throws DbException;
/**
* Removes a contact from the database.
@@ -671,6 +757,13 @@ interface Database<T> {
*/
void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException;
/**
* Sets the cleanup timer duration for the given message. This does not
* start the message's cleanup timer.
*/
void setCleanupTimerDuration(T txn, MessageId m, long duration)
throws DbException;
/**
* Marks the given contact as verified.
*/
@@ -701,9 +794,10 @@ interface Database<T> {
void setMessagePermanent(T txn, MessageId m) throws DbException;
/**
* Marks the given message as shared.
* Marks the given message as shared or not.
*/
void setMessageShared(T txn, MessageId m) throws DbException;
void setMessageShared(T txn, MessageId m, boolean shared)
throws DbException;
/**
* Sets the validation and delivery state of the given message.
@@ -730,12 +824,28 @@ interface Database<T> {
void setTransportKeysActive(T txn, TransportId t, KeySetId k)
throws DbException;
/**
* Starts the cleanup timer for the given message, if a timer duration
* has been set and the timer has not already been started.
*
* @return The cleanup deadline, or
* {@link DatabaseComponent#TIMER_NOT_STARTED} if no timer duration has
* been set for this message or its timer has already been started.
*/
long startCleanupTimer(T txn, MessageId m) throws DbException;
/**
* Stops the cleanup timer for the given message, if the timer has been
* started.
*/
void stopCleanupTimer(T txn, MessageId m) throws DbException;
/**
* Updates the transmission count, expiry time and estimated time of arrival
* 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;
/**

View File

@@ -1,10 +1,12 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
import org.briarproject.bramble.api.contact.event.ContactAliasChangedEvent;
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.contact.event.ContactVerifiedEvent;
import org.briarproject.bramble.api.contact.event.PendingContactAddedEvent;
@@ -308,7 +310,7 @@ class DatabaseComponentImpl<T> 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))
@@ -339,6 +341,15 @@ class DatabaseComponentImpl<T> 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 {
@@ -369,6 +380,13 @@ class DatabaseComponentImpl<T> 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 {
@@ -406,28 +424,57 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
@Nullable
@Override
public Collection<Message> 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<MessageId> ids =
db.getMessagesToSend(txn, c, maxLength, maxLatency);
long totalLength = 0;
List<Message> 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<Message> generateBatch(Transaction transaction,
ContactId c, Collection<MessageId> 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<Message> messages = new ArrayList<>(ids.size());
List<MessageId> 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))
@@ -458,21 +505,24 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
@Nullable
@Override
public Collection<Message> 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<MessageId> ids =
db.getRequestedMessagesToSend(txn, c, maxLength, maxLatency);
long totalLength = 0;
List<Message> 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;
}
@@ -576,6 +626,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getMessageIds(txn, g);
}
@Override
public Collection<MessageId> getMessageIds(Transaction transaction,
GroupId g, Metadata query) throws DbException {
T txn = unbox(transaction);
if (!db.containsGroup(txn, g))
throw new NoSuchGroupException();
return db.getMessageIds(txn, g, query);
}
@Override
public Collection<MessageId> getMessagesToValidate(Transaction transaction)
throws DbException {
@@ -597,6 +656,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getMessagesToShare(txn);
}
@Override
public Map<GroupId, Collection<MessageId>> getMessagesToDelete(
Transaction transaction) throws DbException {
T txn = unbox(transaction);
return db.getMessagesToDelete(txn);
}
@Override
public Map<MessageId, Metadata> getMessageMetadata(Transaction transaction,
GroupId g) throws DbException {
@@ -674,6 +740,25 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return status;
}
@Override
public Map<MessageId, Integer> 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<MessageId, MessageState> getMessageDependencies(
Transaction transaction, MessageId m) throws DbException {
@@ -692,6 +777,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getMessageDependents(txn, m);
}
@Override
public long getNextCleanupDeadline(Transaction transaction)
throws DbException {
T txn = unbox(transaction);
return db.getNextCleanupDeadline(txn);
}
@Override
public long getNextSendTime(Transaction transaction, ContactId c)
throws DbException {
@@ -740,6 +832,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getTransportKeys(txn, t);
}
@Override
public Map<ContactId, Collection<TransportId>> 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 {
@@ -795,8 +894,17 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
Collection<MessageId> acked = new ArrayList<>();
for (MessageId m : a.getMessageIds()) {
if (db.containsVisibleMessage(txn, c, m)) {
db.raiseSeenFlag(txn, c, m);
acked.add(m);
if (db.raiseSeenFlag(txn, c, m)) {
// This is the first time the message has been acked by
// this contact. Start the cleanup timer (a no-op unless
// a cleanup deadline has been set for this message)
long deadline = db.startCleanupTimer(txn, m);
if (deadline != TIMER_NOT_STARTED) {
transaction.attach(new CleanupTimerStartedEvent(m,
deadline));
}
acked.add(m);
}
}
}
if (acked.size() > 0) {
@@ -952,6 +1060,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.removeTransportKeys(txn, t, k);
}
@Override
public void setCleanupTimerDuration(Transaction transaction, MessageId m,
long duration) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
db.setCleanupTimerDuration(txn, m, duration);
}
@Override
public void setContactVerified(Transaction transaction, ContactId c)
throws DbException {
@@ -970,6 +1088,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
T txn = unbox(transaction);
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
transaction.attach(new ContactAliasChangedEvent(c, alias));
db.setContactAlias(txn, c, alias);
}
@@ -1001,6 +1120,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.setMessagePermanent(txn, m);
}
@Override
public void setMessageNotShared(Transaction transaction, MessageId m)
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
db.setMessageShared(txn, m, false);
}
@Override
public void setMessageShared(Transaction transaction, MessageId m)
throws DbException {
@@ -1010,7 +1139,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
throw new NoSuchMessageException();
if (db.getMessageState(txn, m) != DELIVERED)
throw new IllegalArgumentException("Shared undelivered message");
db.setMessageShared(txn, m);
db.setMessageShared(txn, m, true);
transaction.attach(new MessageSharedEvent(m));
}
@@ -1082,6 +1211,30 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.setTransportKeysActive(txn, t, k);
}
@Override
public long startCleanupTimer(Transaction transaction, MessageId m)
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
long deadline = db.startCleanupTimer(txn, m);
if (deadline != TIMER_NOT_STARTED) {
transaction.attach(new CleanupTimerStartedEvent(m, deadline));
}
return deadline;
}
@Override
public void stopCleanupTimer(Transaction transaction, MessageId m)
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
db.stopCleanupTimer(txn, m);
}
@Override
public void updateTransportKeys(Transaction transaction,
Collection<TransportKeySet> keys) throws DbException {

View File

@@ -37,4 +37,10 @@ interface DatabaseConstants {
* has passed since the last compaction.
*/
long MAX_COMPACTION_INTERVAL_MS = DAYS.toMillis(30);
/**
* The {@link Settings} key under which the flag is stored indicating
* whether the database is marked as dirty.
*/
String DIRTY_KEY = "dirty";
}

View File

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

View File

@@ -84,9 +84,14 @@ class H2Database extends JdbcDatabase {
@Override
public void close() throws DbException {
// H2 will close the database when the last connection closes
Connection c = null;
try {
c = createConnection();
super.closeAllConnections();
setDirty(c, false);
c.close();
} catch (SQLException e) {
tryToClose(c, LOG, WARNING);
throw new DbException(e);
}
}

View File

@@ -81,6 +81,7 @@ class HyperSqlDatabase extends JdbcDatabase {
try {
super.closeAllConnections();
c = createConnection();
setDirty(c, false);
s = c.createStatement();
s.executeQuery("SHUTDOWN");
s.close();

View File

@@ -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;
@@ -72,6 +73,8 @@ import static java.util.Arrays.asList;
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.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
@@ -81,6 +84,7 @@ import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERE
import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING;
import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN;
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
import static org.briarproject.bramble.db.DatabaseConstants.DIRTY_KEY;
import static org.briarproject.bramble.db.DatabaseConstants.LAST_COMPACTED_KEY;
import static org.briarproject.bramble.db.DatabaseConstants.MAX_COMPACTION_INTERVAL_MS;
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
@@ -98,7 +102,7 @@ import static org.briarproject.bramble.util.LogUtils.now;
abstract class JdbcDatabase implements Database<Connection> {
// Package access for testing
static final int CODE_SCHEMA_VERSION = 47;
static final int CODE_SCHEMA_VERSION = 49;
// Time period offsets for incoming transport keys
private static final int OFFSET_PREV = -1;
@@ -180,6 +184,11 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " state INT NOT NULL,"
+ " shared BOOLEAN NOT NULL,"
+ " temporary BOOLEAN NOT NULL,"
// Null if no timer duration has been set
+ " cleanupTimerDuration BIGINT,"
// Null if no timer duration has been set or the timer
// hasn't started
+ " cleanupDeadline BIGINT,"
+ " length INT NOT NULL,"
+ " raw BLOB," // Null if message has been deleted
+ " PRIMARY KEY (messageId),"
@@ -258,7 +267,7 @@ abstract class JdbcDatabase implements Database<Connection> {
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 =
@@ -336,6 +345,15 @@ abstract class JdbcDatabase implements Database<Connection> {
"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)";
private static final Logger LOG =
getLogger(JdbcDatabase.class.getName());
@@ -354,9 +372,14 @@ abstract class JdbcDatabase implements Database<Connection> {
@GuardedBy("connectionsLock")
private boolean closed = false;
private volatile boolean wasDirtyOnInitialisation = false;
protected abstract Connection createConnection()
throws DbException, SQLException;
// Used exclusively during open to compact the database after schema
// migrations or after DatabaseConstants#MAX_COMPACTION_INTERVAL_MS has
// elapsed
protected abstract void compactAndClose() throws DbException;
JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory,
@@ -381,13 +404,19 @@ abstract class JdbcDatabase implements Database<Connection> {
try {
if (reopen) {
Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
wasDirtyOnInitialisation = isDirty(s);
compact = migrateSchema(txn, s, listener) || isCompactionDue(s);
} else {
wasDirtyOnInitialisation = false;
createTables(txn);
initialiseSettings(txn);
compact = false;
}
if (LOG.isLoggable(INFO)) {
LOG.info("db dirty? " + wasDirtyOnInitialisation);
}
createIndexes(txn);
setDirty(txn, true);
commitTransaction(txn);
} catch (DbException e) {
abortTransaction(txn);
@@ -414,6 +443,11 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public boolean wasDirtyOnInitialisation() {
return wasDirtyOnInitialisation;
}
/**
* Compares the schema version stored in the database with the schema
* version used by the current code and applies any suitable migrations to
@@ -463,7 +497,9 @@ abstract class JdbcDatabase implements Database<Connection> {
new Migration43_44(dbTypes),
new Migration44_45(),
new Migration45_46(),
new Migration46_47(dbTypes)
new Migration46_47(dbTypes),
new Migration47_48(),
new Migration48_49()
);
}
@@ -488,6 +524,16 @@ abstract class JdbcDatabase implements Database<Connection> {
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
}
private boolean isDirty(Settings s) {
return s.getBoolean(DIRTY_KEY, false);
}
protected void setDirty(Connection txn, boolean dirty) throws DbException {
Settings s = new Settings();
s.putBoolean(DIRTY_KEY, dirty);
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
}
private void initialiseSettings(Connection txn) throws DbException {
Settings s = new Settings();
s.putInt(SCHEMA_VERSION_KEY, CODE_SCHEMA_VERSION);
@@ -531,6 +577,8 @@ abstract class JdbcDatabase implements Database<Connection> {
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) {
tryToClose(s, LOG, WARNING);
@@ -959,7 +1007,7 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@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 {
@@ -1080,6 +1128,55 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@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 {
@@ -1237,6 +1334,29 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@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 {
@@ -1290,7 +1410,9 @@ abstract class JdbcDatabase implements Database<Connection> {
public void deleteMessage(Connection txn, MessageId m) throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE messages SET raw = NULL WHERE messageId = ?";
String sql = "UPDATE messages"
+ " SET raw = NULL, cleanupDeadline = NULL"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
int affected = ps.executeUpdate();
@@ -1769,7 +1891,6 @@ abstract class JdbcDatabase implements Database<Connection> {
// Return early if there are no matches
if (intersection.isEmpty()) return Collections.emptySet();
}
if (intersection == null) throw new AssertionError();
return intersection;
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
@@ -2068,7 +2189,7 @@ abstract class JdbcDatabase implements Database<Connection> {
@Override
public Collection<MessageId> 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;
@@ -2127,7 +2248,7 @@ abstract class JdbcDatabase implements Database<Connection> {
@Override
public Collection<MessageId> 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;
@@ -2164,6 +2285,63 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public Map<MessageId, Integer> 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<MessageId, Integer> 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.getLong(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<MessageId> getMessagesToValidate(Connection txn)
throws DbException {
@@ -2226,6 +2404,39 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public Map<GroupId, Collection<MessageId>> getMessagesToDelete(
Connection txn) throws DbException {
long now = clock.currentTimeMillis();
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT messageId, groupId FROM messages"
+ " WHERE cleanupDeadline <= ?";
ps = txn.prepareStatement(sql);
ps.setLong(1, now);
rs = ps.executeQuery();
Map<GroupId, Collection<MessageId>> ids = new HashMap<>();
while (rs.next()) {
MessageId m = new MessageId(rs.getBytes(1));
GroupId g = new GroupId(rs.getBytes(2));
Collection<MessageId> messageIds = ids.get(g);
if (messageIds == null) {
messageIds = new ArrayList<>();
ids.put(g, messageIds);
}
messageIds.add(m);
}
rs.close();
ps.close();
return ids;
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public long getNextSendTime(Connection txn, ContactId c)
throws DbException {
@@ -2256,6 +2467,31 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public long getNextCleanupDeadline(Connection txn) throws DbException {
Statement s = null;
ResultSet rs = null;
try {
String sql = "SELECT cleanupDeadline FROM messages"
+ " WHERE cleanupDeadline IS NOT NULL"
+ " ORDER BY cleanupDeadline LIMIT 1";
s = txn.createStatement();
rs = s.executeQuery(sql);
long nextDeadline = NO_CLEANUP_DEADLINE;
if (rs.next()) {
nextDeadline = rs.getLong(1);
if (rs.next()) throw new AssertionError();
}
rs.close();
s.close();
return nextDeadline;
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(s, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public PendingContact getPendingContact(Connection txn, PendingContactId p)
throws DbException {
@@ -2311,7 +2547,7 @@ abstract class JdbcDatabase implements Database<Connection> {
@Override
public Collection<MessageId> 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;
@@ -2475,6 +2711,38 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public Map<ContactId, Collection<TransportId>> 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<ContactId, Collection<TransportId>> ids = new HashMap<>();
while (rs.next()) {
ContactId c = new ContactId(rs.getInt(1));
TransportId t = new TransportId(rs.getString(2));
Collection<TransportId> 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 {
@@ -2776,7 +3044,7 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public void raiseSeenFlag(Connection txn, ContactId c, MessageId m)
public boolean raiseSeenFlag(Connection txn, ContactId c, MessageId m)
throws DbException {
PreparedStatement ps = null;
try {
@@ -2788,6 +3056,7 @@ abstract class JdbcDatabase implements Database<Connection> {
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
return affected == 1;
} catch (SQLException e) {
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
@@ -3021,6 +3290,25 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public void setCleanupTimerDuration(Connection txn, MessageId m,
long duration) throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE messages SET cleanupTimerDuration = ?"
+ " WHERE messageId = ? AND cleanupTimerDuration IS NULL";
ps = txn.prepareStatement(sql);
ps.setLong(1, duration);
ps.setBytes(2, m.getBytes());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public void setContactVerified(Connection txn, ContactId c)
throws DbException {
@@ -3128,22 +3416,24 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public void setMessageShared(Connection txn, MessageId m)
public void setMessageShared(Connection txn, MessageId m, boolean shared)
throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE messages SET shared = TRUE"
String sql = "UPDATE messages SET shared = ?"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setBoolean(1, shared);
ps.setBytes(2, m.getBytes());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
// Update denormalised column in statuses
sql = "UPDATE statuses SET messageShared = TRUE"
sql = "UPDATE statuses SET messageShared = ?"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setBoolean(1, shared);
ps.setBytes(2, m.getBytes());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
@@ -3272,9 +3562,63 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public long startCleanupTimer(Connection txn, MessageId m)
throws DbException {
long now = clock.currentTimeMillis();
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "UPDATE messages"
+ " SET cleanupDeadline = ? + cleanupTimerDuration"
+ " WHERE messageId = ?"
+ " AND cleanupTimerDuration IS NOT NULL"
+ " AND cleanupDeadline IS NULL";
ps = txn.prepareStatement(sql);
ps.setLong(1, now);
ps.setBytes(2, m.getBytes());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
if (affected == 0) return TIMER_NOT_STARTED;
sql = "SELECT cleanupDeadline FROM messages WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException();
long deadline = rs.getLong(1);
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
return deadline;
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public void stopCleanupTimer(Connection txn, MessageId m)
throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE messages SET cleanupDeadline = NULL"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@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 {

View File

@@ -0,0 +1,47 @@
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 Migration47_48 implements Migration<Connection> {
private static final Logger LOG = getLogger(Migration47_48.class.getName());
@Override
public int getStartVersion() {
return 47;
}
@Override
public int getEndVersion() {
return 48;
}
@Override
public void migrate(Connection txn) throws DbException {
Statement s = null;
try {
s = txn.createStatement();
// Null if no timer duration has been set
s.execute("ALTER TABLE messages"
+ " ADD COLUMN cleanupTimerDuration BIGINT");
// Null if no timer duration has been set or the timer
// hasn't started
s.execute("ALTER TABLE messages"
+ " ADD COLUMN cleanupDeadline BIGINT");
s.execute("CREATE INDEX IF NOT EXISTS messagesByCleanupDeadline"
+ " ON messages (cleanupDeadline)");
} catch (SQLException e) {
tryToClose(s, LOG, WARNING);
throw new DbException(e);
}
}
}

View File

@@ -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<Connection> {
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);
}
}
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.keyagreement;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.crypto.KeyAgreementCrypto;
import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.data.BdfList;
@@ -8,6 +9,8 @@ import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.keyagreement.Payload;
import org.briarproject.bramble.api.keyagreement.TransportDescriptor;
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.Plugin;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.TransportId;
@@ -19,7 +22,9 @@ import org.briarproject.bramble.api.record.RecordWriterFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
@@ -28,8 +33,10 @@ import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.util.Arrays.asList;
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.keyagreement.KeyAgreementConstants.CONNECTION_TIMEOUT;
import static org.briarproject.bramble.util.LogUtils.logException;
@@ -41,7 +48,10 @@ class KeyAgreementConnector {
}
private static final Logger LOG =
Logger.getLogger(KeyAgreementConnector.class.getName());
getLogger(KeyAgreementConnector.class.getName());
private static final List<TransportId> PREFERRED_TRANSPORTS =
asList(BluetoothConstants.ID, LanTcpConstants.ID);
private final Callbacks callbacks;
private final KeyAgreementCrypto keyAgreementCrypto;
@@ -105,24 +115,35 @@ class KeyAgreementConnector {
this.alice = alice;
aliceLatch.countDown();
// Start connecting over supported transports
// Start connecting over supported transports in order of preference
if (LOG.isLoggable(INFO)) {
LOG.info("Starting outgoing BQP connections as "
+ (alice ? "Alice" : "Bob"));
}
Map<TransportId, TransportDescriptor> descriptors = new HashMap<>();
for (TransportDescriptor d : remotePayload.getTransportDescriptors()) {
Plugin p = pluginManager.getPlugin(d.getId());
if (p instanceof DuplexPlugin) {
descriptors.put(d.getId(), d);
}
List<Pair<DuplexPlugin, BdfList>> transports = new ArrayList<>();
for (TransportId id : PREFERRED_TRANSPORTS) {
TransportDescriptor d = descriptors.get(id);
Plugin p = pluginManager.getPlugin(id);
if (d != null && p instanceof DuplexPlugin) {
if (LOG.isLoggable(INFO))
LOG.info("Connecting via " + d.getId());
DuplexPlugin plugin = (DuplexPlugin) p;
byte[] commitment = remotePayload.getCommitment();
BdfList descriptor = d.getDescriptor();
connectionChooser.submit(new ReadableTask(
new ConnectorTask(plugin, commitment, descriptor)));
LOG.info("Connecting via " + id);
transports.add(new Pair<>((DuplexPlugin) p, d.getDescriptor()));
}
}
// TODO: If we don't have any transports in common with the peer,
// warn the user and give up (#1224)
if (!transports.isEmpty()) {
byte[] commitment = remotePayload.getCommitment();
connectionChooser.submit(new ReadableTask(new ConnectorTask(
transports, commitment)));
}
// Get chosen connection
try {
KeyAgreementConnection chosen =
@@ -148,15 +169,13 @@ class KeyAgreementConnector {
private class ConnectorTask implements Callable<KeyAgreementConnection> {
private final List<Pair<DuplexPlugin, BdfList>> transports;
private final byte[] commitment;
private final BdfList descriptor;
private final DuplexPlugin plugin;
private ConnectorTask(DuplexPlugin plugin, byte[] commitment,
BdfList descriptor) {
this.plugin = plugin;
private ConnectorTask(List<Pair<DuplexPlugin, BdfList>> transports,
byte[] commitment) {
this.transports = transports;
this.commitment = commitment;
this.descriptor = descriptor;
}
@Nullable
@@ -164,13 +183,18 @@ class KeyAgreementConnector {
public KeyAgreementConnection call() throws Exception {
// Repeat attempts until we connect, get stopped, or get interrupted
while (!stopped) {
DuplexTransportConnection conn =
plugin.createKeyAgreementConnection(commitment,
descriptor);
if (conn != null) {
if (LOG.isLoggable(INFO))
LOG.info(plugin.getId() + ": Outgoing connection");
return new KeyAgreementConnection(conn, plugin.getId());
for (Pair<DuplexPlugin, BdfList> pair : transports) {
if (stopped) return null;
DuplexPlugin plugin = pair.getFirst();
BdfList descriptor = pair.getSecond();
DuplexTransportConnection conn =
plugin.createKeyAgreementConnection(commitment,
descriptor);
if (conn != null) {
if (LOG.isLoggable(INFO))
LOG.info(plugin.getId() + ": Outgoing connection");
return new KeyAgreementConnection(conn, plugin.getId());
}
}
// Wait 2s before retry (to circumvent transient failures)
Thread.sleep(2000);

View File

@@ -34,12 +34,12 @@ class KeyAgreementTransport {
Logger.getLogger(KeyAgreementTransport.class.getName());
// Accept records with current protocol version, known record type
private static Predicate<Record> ACCEPT = r ->
private static final Predicate<Record> ACCEPT = r ->
r.getProtocolVersion() == PROTOCOL_VERSION &&
isKnownRecordType(r.getRecordType());
// Ignore records with current protocol version, unknown record type
private static Predicate<Record> IGNORE = r ->
private static final Predicate<Record> IGNORE = r ->
r.getProtocolVersion() == PROTOCOL_VERSION &&
!isKnownRecordType(r.getRecordType());

View File

@@ -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<Service> services;
private final List<OpenDatabaseHook> openDatabaseHooks;
private final List<ExecutorService> 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();

View File

@@ -0,0 +1,634 @@
package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Multiset;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStoppedListeningEvent;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.ConnectionHandler;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.properties.event.RemoteTransportPropertiesUpdatedEvent;
import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
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.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_ADDRESS_IS_REFLECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_EVER_CONNECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_ADDRESS_IS_REFLECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_EVER_CONNECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.REFLECTED_PROPERTY_PREFIX;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.bramble.util.StringUtils.macToBytes;
import static org.briarproject.bramble.util.StringUtils.macToString;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
abstract class AbstractBluetoothPlugin<S, SS> implements BluetoothPlugin,
EventListener {
private static final Logger LOG =
getLogger(AbstractBluetoothPlugin.class.getName());
private final BluetoothConnectionLimiter connectionLimiter;
final BluetoothConnectionFactory<S> connectionFactory;
private final Executor ioExecutor, wakefulIoExecutor;
private final SecureRandom secureRandom;
private final Backoff backoff;
private final PluginCallback callback;
private final long maxLatency;
private final int maxIdleTime;
private final AtomicBoolean used = new AtomicBoolean(false);
private final AtomicBoolean everConnected = new AtomicBoolean(false);
protected final PluginState state = new PluginState();
protected final Semaphore discoverSemaphore = new Semaphore(1);
private volatile String contactConnectionsUuid = null;
abstract void initialiseAdapter() throws IOException;
abstract boolean isAdapterEnabled();
/**
* Returns the local Bluetooth address, or null if no valid address can
* be found.
*/
@Nullable
abstract String getBluetoothAddress();
abstract SS openServerSocket(String uuid) throws IOException;
abstract void tryToClose(@Nullable SS ss);
abstract DuplexTransportConnection acceptConnection(SS ss)
throws IOException;
abstract boolean isValidAddress(String address);
abstract DuplexTransportConnection connectTo(String address, String uuid)
throws IOException;
@Nullable
abstract DuplexTransportConnection discoverAndConnect(String uuid);
AbstractBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
BluetoothConnectionFactory<S> connectionFactory,
Executor ioExecutor,
Executor wakefulIoExecutor,
SecureRandom secureRandom,
Backoff backoff,
PluginCallback callback,
long maxLatency,
int maxIdleTime) {
this.connectionLimiter = connectionLimiter;
this.connectionFactory = connectionFactory;
this.ioExecutor = ioExecutor;
this.wakefulIoExecutor = wakefulIoExecutor;
this.secureRandom = secureRandom;
this.backoff = backoff;
this.callback = callback;
this.maxLatency = maxLatency;
this.maxIdleTime = maxIdleTime;
}
void onAdapterEnabled() {
LOG.info("Bluetooth enabled");
// We may not have been able to get the local address before
ioExecutor.execute(this::updateProperties);
if (getState() == INACTIVE) bind();
}
void onAdapterDisabled() {
LOG.info("Bluetooth disabled");
connectionLimiter.allConnectionsClosed();
// The server socket may not have been closed automatically
SS ss = state.clearServerSocket();
if (ss != null) {
LOG.info("Closing server socket");
tryToClose(ss);
}
}
@Override
public TransportId getId() {
return ID;
}
@Override
public long getMaxLatency() {
return maxLatency;
}
@Override
public int getMaxIdleTime() {
return maxIdleTime;
}
@Override
public void start() throws PluginException {
if (used.getAndSet(true)) throw new IllegalStateException();
Settings settings = callback.getSettings();
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
DEFAULT_PREF_PLUGIN_ENABLE);
everConnected.set(settings.getBoolean(PREF_EVER_CONNECTED,
DEFAULT_PREF_EVER_CONNECTED));
state.setStarted(enabledByUser);
try {
initialiseAdapter();
} catch (IOException e) {
throw new PluginException(e);
}
updateProperties();
if (enabledByUser && isAdapterEnabled()) bind();
}
private void bind() {
ioExecutor.execute(() -> {
if (getState() != INACTIVE) return;
// Bind a server socket to accept connections from contacts
SS ss;
try {
ss = openServerSocket(contactConnectionsUuid);
} catch (IOException e) {
logException(LOG, WARNING, e);
return;
}
if (!state.setServerSocket(ss)) {
LOG.info("Closing redundant server socket");
tryToClose(ss);
return;
}
backoff.reset();
acceptContactConnections(ss);
});
}
private void updateProperties() {
TransportProperties p = callback.getLocalProperties();
String address = p.get(PROP_ADDRESS);
String uuid = p.get(PROP_UUID);
Settings s = callback.getSettings();
boolean isReflected = s.getBoolean(PREF_ADDRESS_IS_REFLECTED,
DEFAULT_PREF_ADDRESS_IS_REFLECTED);
boolean changed = false;
if (address == null || isReflected) {
address = getBluetoothAddress();
if (LOG.isLoggable(INFO)) {
LOG.info("Local address " + scrubMacAddress(address));
}
if (address == null) {
if (everConnected.get()) {
address = getReflectedAddress();
if (LOG.isLoggable(INFO)) {
LOG.info("Reflected address " +
scrubMacAddress(address));
}
if (address != null) {
changed = true;
isReflected = true;
}
}
} else {
changed = true;
isReflected = false;
}
}
if (uuid == null) {
byte[] random = new byte[UUID_BYTES];
secureRandom.nextBytes(random);
uuid = UUID.nameUUIDFromBytes(random).toString();
changed = true;
}
contactConnectionsUuid = uuid;
if (changed) {
p = new TransportProperties();
// If we previously used a reflected address and there's no longer
// a reflected address with enough votes to be used, we'll continue
// to use the old reflected address until there's a new winner
if (address != null) p.put(PROP_ADDRESS, address);
p.put(PROP_UUID, uuid);
callback.mergeLocalProperties(p);
s = new Settings();
s.putBoolean(PREF_ADDRESS_IS_REFLECTED, isReflected);
callback.mergeSettings(s);
}
}
@Nullable
private String getReflectedAddress() {
// Count the number of votes for each reflected address
String key = REFLECTED_PROPERTY_PREFIX + PROP_ADDRESS;
Multiset<String> votes = new Multiset<>();
for (TransportProperties p : callback.getRemoteProperties()) {
String address = p.get(key);
if (address != null && isValidAddress(address)) votes.add(address);
}
// If an address gets more than half of the votes, accept it
int total = votes.getTotal();
for (String address : votes.keySet()) {
if (votes.getCount(address) * 2 > total) return address;
}
return null;
}
private void acceptContactConnections(SS ss) {
while (true) {
DuplexTransportConnection conn;
try {
conn = acceptConnection(ss);
} catch (IOException e) {
// This is expected when the server socket is closed
LOG.info("Server socket closed");
state.clearServerSocket();
return;
}
LOG.info("Connection received");
connectionLimiter.connectionOpened(conn);
backoff.reset();
setEverConnected();
callback.handleConnection(conn);
}
}
private void setEverConnected() {
if (!everConnected.getAndSet(true)) {
ioExecutor.execute(() -> {
Settings s = new Settings();
s.putBoolean(PREF_EVER_CONNECTED, true);
callback.mergeSettings(s);
// Contacts may already have sent a reflected address
updateProperties();
});
}
}
@Override
public void stop() {
SS ss = state.setStopped();
tryToClose(ss);
}
@Override
public State getState() {
return state.getState();
}
@Override
public int getReasonsDisabled() {
return state.getReasonsDisabled();
}
@Override
public boolean shouldPoll() {
return true;
}
@Override
public int getPollingInterval() {
return backoff.getPollingInterval();
}
@Override
public void poll(Collection<Pair<TransportProperties, ConnectionHandler>>
properties) {
if (getState() != ACTIVE) return;
backoff.increment();
for (Pair<TransportProperties, ConnectionHandler> p : properties) {
connect(p.getFirst(), p.getSecond());
}
}
private void connect(TransportProperties p, ConnectionHandler h) {
String address = p.get(PROP_ADDRESS);
if (isNullOrEmpty(address)) return;
String uuid = p.get(PROP_UUID);
if (isNullOrEmpty(uuid)) return;
wakefulIoExecutor.execute(() -> {
DuplexTransportConnection d = createConnection(p);
if (d != null) {
backoff.reset();
setEverConnected();
h.handleConnection(d);
}
});
}
@Nullable
private DuplexTransportConnection connect(String address, String uuid) {
// Validate the address
if (!isValidAddress(address)) {
if (LOG.isLoggable(WARNING))
// Not scrubbing here to be able to figure out the problem
LOG.warning("Invalid address " + address);
return null;
}
// Validate the UUID
try {
//noinspection ResultOfMethodCallIgnored
UUID.fromString(uuid);
} catch (IllegalArgumentException e) {
if (LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
return null;
}
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubMacAddress(address));
try {
DuplexTransportConnection conn = connectTo(address, uuid);
if (LOG.isLoggable(INFO))
LOG.info("Connected to " + scrubMacAddress(address));
return conn;
} catch (IOException e) {
if (LOG.isLoggable(INFO))
LOG.info("Could not connect to " + scrubMacAddress(address));
return null;
}
}
@Override
public DuplexTransportConnection createConnection(TransportProperties p) {
if (getState() != ACTIVE) return null;
if (!connectionLimiter.canOpenContactConnection()) return null;
String address = p.get(PROP_ADDRESS);
if (isNullOrEmpty(address)) return null;
String uuid = p.get(PROP_UUID);
if (isNullOrEmpty(uuid)) return null;
DuplexTransportConnection conn = connect(address, uuid);
if (conn != null) connectionLimiter.connectionOpened(conn);
return conn;
}
@Override
public boolean supportsKeyAgreement() {
return true;
}
@Override
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
if (getState() != ACTIVE) return null;
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
// Bind a server socket for receiving key agreement connections
SS ss;
try {
ss = openServerSocket(uuid);
} catch (IOException e) {
logException(LOG, WARNING, e);
return null;
}
if (getState() != ACTIVE) {
tryToClose(ss);
return null;
}
BdfList descriptor = new BdfList();
descriptor.add(TRANSPORT_ID_BLUETOOTH);
String address = getBluetoothAddress();
if (address != null) descriptor.add(macToBytes(address));
return new BluetoothKeyAgreementListener(descriptor, ss);
}
@Override
public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor) {
if (getState() != ACTIVE) return null;
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
DuplexTransportConnection conn;
if (descriptor.size() == 1) {
if (LOG.isLoggable(INFO)) {
LOG.info("Discovering address for key agreement UUID " +
uuid);
}
conn = discoverAndConnect(uuid);
} else {
String address;
try {
address = parseAddress(descriptor);
} catch (FormatException e) {
LOG.info("Invalid address in key agreement descriptor");
return null;
}
if (LOG.isLoggable(INFO))
LOG.info("Connecting to key agreement UUID " + uuid);
conn = connect(address, uuid);
}
if (conn != null) {
connectionLimiter.connectionOpened(conn);
setEverConnected();
}
return conn;
}
private String parseAddress(BdfList descriptor) throws FormatException {
byte[] mac = descriptor.getRaw(1);
if (mac.length != 6) throw new FormatException();
return macToString(mac);
}
@Override
public boolean isDiscovering() {
return discoverSemaphore.availablePermits() == 0;
}
@Override
public void disablePolling() {
connectionLimiter.startLimiting();
}
@Override
public void enablePolling() {
connectionLimiter.endLimiting();
}
@Override
public DuplexTransportConnection discoverAndConnectForSetup(String uuid) {
DuplexTransportConnection conn = discoverAndConnect(uuid);
if (conn != null) {
connectionLimiter.connectionOpened(conn);
setEverConnected();
}
return conn;
}
@Override
public boolean supportsRendezvous() {
return false;
}
@Override
public RendezvousEndpoint createRendezvousEndpoint(KeyMaterialSource k,
boolean alice, ConnectionHandler incoming) {
throw new UnsupportedOperationException();
}
@Override
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
if (s.getNamespace().equals(ID.getString()))
ioExecutor.execute(() -> onSettingsUpdated(s.getSettings()));
} else if (e instanceof KeyAgreementListeningEvent) {
connectionLimiter.startLimiting();
} else if (e instanceof KeyAgreementStoppedListeningEvent) {
connectionLimiter.endLimiting();
} else if (e instanceof RemoteTransportPropertiesUpdatedEvent) {
RemoteTransportPropertiesUpdatedEvent r =
(RemoteTransportPropertiesUpdatedEvent) e;
if (r.getTransportId().equals(ID)) {
ioExecutor.execute(this::updateProperties);
}
}
}
@IoExecutor
private void onSettingsUpdated(Settings settings) {
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
DEFAULT_PREF_PLUGIN_ENABLE);
SS ss = state.setEnabledByUser(enabledByUser);
State s = getState();
if (ss != null) {
LOG.info("Disabled by user, closing server socket");
tryToClose(ss);
} else if (s == INACTIVE) {
if (isAdapterEnabled()) {
LOG.info("Enabled by user, opening server socket");
bind();
} else {
LOG.info("Enabled by user but adapter is disabled");
}
}
}
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
private final SS ss;
private BluetoothKeyAgreementListener(BdfList descriptor, SS ss) {
super(descriptor);
this.ss = ss;
}
@Override
public KeyAgreementConnection accept() throws IOException {
DuplexTransportConnection conn = acceptConnection(ss);
if (LOG.isLoggable(INFO)) LOG.info(ID + ": Incoming connection");
connectionLimiter.connectionOpened(conn);
return new KeyAgreementConnection(conn, ID);
}
@Override
public void close() {
tryToClose(ss);
}
}
@ThreadSafe
@NotNullByDefault
private class PluginState {
@GuardedBy("this")
private boolean started = false,
stopped = false,
enabledByUser = false;
@GuardedBy("this")
@Nullable
private SS serverSocket = null;
private synchronized void setStarted(boolean enabledByUser) {
started = true;
this.enabledByUser = enabledByUser;
callback.pluginStateChanged(getState());
}
@Nullable
private synchronized SS setStopped() {
stopped = true;
SS ss = serverSocket;
serverSocket = null;
callback.pluginStateChanged(getState());
return ss;
}
@Nullable
private synchronized SS setEnabledByUser(boolean enabledByUser) {
this.enabledByUser = enabledByUser;
SS ss = null;
if (!enabledByUser) {
ss = serverSocket;
serverSocket = null;
}
callback.pluginStateChanged(getState());
return ss;
}
private synchronized boolean setServerSocket(SS ss) {
if (stopped || serverSocket != null) return false;
serverSocket = ss;
callback.pluginStateChanged(getState());
return true;
}
@Nullable
private synchronized SS clearServerSocket() {
SS ss = serverSocket;
serverSocket = null;
callback.pluginStateChanged(getState());
return ss;
}
private synchronized State getState() {
if (!started || stopped) return STARTING_STOPPING;
if (!enabledByUser) return DISABLED;
return serverSocket == null ? INACTIVE : ACTIVE;
}
private synchronized int getReasonsDisabled() {
return getState() == DISABLED ? REASON_USER : 0;
}
}
}

View File

@@ -7,14 +7,15 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
interface BluetoothConnectionLimiter {
/**
* Informs the limiter that key agreement has started.
* Tells the limiter to not allow regular polling connections (because we
* are about to do key agreement, or connect via BT for setup).
*/
void keyAgreementStarted();
void startLimiting();
/**
* Informs the limiter that key agreement has ended.
* Tells the limiter to no longer limit regular polling connections.
*/
void keyAgreementEnded();
void endLimiting();
/**
* Returns true if a contact connection can be opened. This method does not

View File

@@ -30,34 +30,37 @@ class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter {
private final List<DuplexTransportConnection> connections =
new LinkedList<>();
@GuardedBy("lock")
private boolean keyAgreementInProgress = false;
private int limitingInProgress = 0;
BluetoothConnectionLimiterImpl(EventBus eventBus) {
this.eventBus = eventBus;
}
@Override
public void keyAgreementStarted() {
public void startLimiting() {
synchronized (lock) {
keyAgreementInProgress = true;
limitingInProgress++;
}
LOG.info("Key agreement started");
LOG.info("Limiting started");
eventBus.broadcast(new CloseSyncConnectionsEvent(ID));
}
@Override
public void keyAgreementEnded() {
public void endLimiting() {
synchronized (lock) {
keyAgreementInProgress = false;
limitingInProgress--;
if (limitingInProgress < 0) {
throw new IllegalStateException();
}
}
LOG.info("Key agreement ended");
LOG.info("Limiting ended");
}
@Override
public boolean canOpenContactConnection() {
synchronized (lock) {
if (keyAgreementInProgress) {
LOG.info("Can't open contact connection during key agreement");
if (limitingInProgress > 0) {
LOG.info("Can't open contact connection while limiting");
return false;
} else {
LOG.info("Can open contact connection");

View File

@@ -1,604 +1,22 @@
package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Multiset;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStoppedListeningEvent;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.ConnectionHandler;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.properties.event.RemoteTransportPropertiesUpdatedEvent;
import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
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.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_ADDRESS_IS_REFLECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_EVER_CONNECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_ADDRESS_IS_REFLECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_EVER_CONNECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.REFLECTED_PROPERTY_PREFIX;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.bramble.util.StringUtils.macToBytes;
import static org.briarproject.bramble.util.StringUtils.macToString;
@NotNullByDefault
public interface BluetoothPlugin extends DuplexPlugin {
@MethodsNotNullByDefault
@ParametersNotNullByDefault
abstract class BluetoothPlugin<S, SS> implements DuplexPlugin, EventListener {
boolean isDiscovering();
private static final Logger LOG =
getLogger(BluetoothPlugin.class.getName());
void disablePolling();
final BluetoothConnectionLimiter connectionLimiter;
final BluetoothConnectionFactory<S> connectionFactory;
private final Executor ioExecutor, wakefulIoExecutor;
private final SecureRandom secureRandom;
private final Backoff backoff;
private final PluginCallback callback;
private final int maxLatency, maxIdleTime;
private final AtomicBoolean used = new AtomicBoolean(false);
private final AtomicBoolean everConnected = new AtomicBoolean(false);
protected final PluginState state = new PluginState();
private volatile String contactConnectionsUuid = null;
abstract void initialiseAdapter() throws IOException;
abstract boolean isAdapterEnabled();
/**
* Returns the local Bluetooth address, or null if no valid address can
* be found.
*/
@Nullable
abstract String getBluetoothAddress();
abstract SS openServerSocket(String uuid) throws IOException;
abstract void tryToClose(@Nullable SS ss);
abstract DuplexTransportConnection acceptConnection(SS ss)
throws IOException;
abstract boolean isValidAddress(String address);
abstract DuplexTransportConnection connectTo(String address, String uuid)
throws IOException;
void enablePolling();
@Nullable
abstract DuplexTransportConnection discoverAndConnect(String uuid);
DuplexTransportConnection discoverAndConnectForSetup(String uuid);
BluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
BluetoothConnectionFactory<S> connectionFactory,
Executor ioExecutor,
Executor wakefulIoExecutor,
SecureRandom secureRandom,
Backoff backoff,
PluginCallback callback,
int maxLatency,
int maxIdleTime) {
this.connectionLimiter = connectionLimiter;
this.connectionFactory = connectionFactory;
this.ioExecutor = ioExecutor;
this.wakefulIoExecutor = wakefulIoExecutor;
this.secureRandom = secureRandom;
this.backoff = backoff;
this.callback = callback;
this.maxLatency = maxLatency;
this.maxIdleTime = maxIdleTime;
}
void onAdapterEnabled() {
LOG.info("Bluetooth enabled");
// We may not have been able to get the local address before
ioExecutor.execute(this::updateProperties);
if (getState() == INACTIVE) bind();
}
void onAdapterDisabled() {
LOG.info("Bluetooth disabled");
connectionLimiter.allConnectionsClosed();
// The server socket may not have been closed automatically
SS ss = state.clearServerSocket();
if (ss != null) {
LOG.info("Closing server socket");
tryToClose(ss);
}
}
@Override
public TransportId getId() {
return ID;
}
@Override
public int getMaxLatency() {
return maxLatency;
}
@Override
public int getMaxIdleTime() {
return maxIdleTime;
}
@Override
public void start() throws PluginException {
if (used.getAndSet(true)) throw new IllegalStateException();
Settings settings = callback.getSettings();
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
DEFAULT_PREF_PLUGIN_ENABLE);
everConnected.set(settings.getBoolean(PREF_EVER_CONNECTED,
DEFAULT_PREF_EVER_CONNECTED));
state.setStarted(enabledByUser);
try {
initialiseAdapter();
} catch (IOException e) {
throw new PluginException(e);
}
updateProperties();
if (enabledByUser && isAdapterEnabled()) bind();
}
private void bind() {
ioExecutor.execute(() -> {
if (getState() != INACTIVE) return;
// Bind a server socket to accept connections from contacts
SS ss;
try {
ss = openServerSocket(contactConnectionsUuid);
} catch (IOException e) {
logException(LOG, WARNING, e);
return;
}
if (!state.setServerSocket(ss)) {
LOG.info("Closing redundant server socket");
tryToClose(ss);
return;
}
backoff.reset();
acceptContactConnections(ss);
});
}
private void updateProperties() {
TransportProperties p = callback.getLocalProperties();
String address = p.get(PROP_ADDRESS);
String uuid = p.get(PROP_UUID);
Settings s = callback.getSettings();
boolean isReflected = s.getBoolean(PREF_ADDRESS_IS_REFLECTED,
DEFAULT_PREF_ADDRESS_IS_REFLECTED);
boolean changed = false;
if (address == null || isReflected) {
address = getBluetoothAddress();
if (LOG.isLoggable(INFO)) {
LOG.info("Local address " + scrubMacAddress(address));
}
if (address == null) {
if (everConnected.get()) {
address = getReflectedAddress();
if (LOG.isLoggable(INFO)) {
LOG.info("Reflected address " +
scrubMacAddress(address));
}
if (address != null) {
changed = true;
isReflected = true;
}
}
} else {
changed = true;
isReflected = false;
}
}
if (uuid == null) {
byte[] random = new byte[UUID_BYTES];
secureRandom.nextBytes(random);
uuid = UUID.nameUUIDFromBytes(random).toString();
changed = true;
}
contactConnectionsUuid = uuid;
if (changed) {
p = new TransportProperties();
// If we previously used a reflected address and there's no longer
// a reflected address with enough votes to be used, we'll continue
// to use the old reflected address until there's a new winner
if (address != null) p.put(PROP_ADDRESS, address);
p.put(PROP_UUID, uuid);
callback.mergeLocalProperties(p);
s = new Settings();
s.putBoolean(PREF_ADDRESS_IS_REFLECTED, isReflected);
callback.mergeSettings(s);
}
}
@Nullable
private String getReflectedAddress() {
// Count the number of votes for each reflected address
String key = REFLECTED_PROPERTY_PREFIX + PROP_ADDRESS;
Multiset<String> votes = new Multiset<>();
for (TransportProperties p : callback.getRemoteProperties()) {
String address = p.get(key);
if (address != null && isValidAddress(address)) votes.add(address);
}
// If an address gets more than half of the votes, accept it
int total = votes.getTotal();
for (String address : votes.keySet()) {
if (votes.getCount(address) * 2 > total) return address;
}
return null;
}
private void acceptContactConnections(SS ss) {
while (true) {
DuplexTransportConnection conn;
try {
conn = acceptConnection(ss);
} catch (IOException e) {
// This is expected when the server socket is closed
LOG.info("Server socket closed");
state.clearServerSocket();
return;
}
LOG.info("Connection received");
connectionLimiter.connectionOpened(conn);
backoff.reset();
setEverConnected();
callback.handleConnection(conn);
}
}
private void setEverConnected() {
if (!everConnected.getAndSet(true)) {
ioExecutor.execute(() -> {
Settings s = new Settings();
s.putBoolean(PREF_EVER_CONNECTED, true);
callback.mergeSettings(s);
// Contacts may already have sent a reflected address
updateProperties();
});
}
}
@Override
public void stop() {
SS ss = state.setStopped();
tryToClose(ss);
}
@Override
public State getState() {
return state.getState();
}
@Override
public int getReasonsDisabled() {
return state.getReasonsDisabled();
}
@Override
public boolean shouldPoll() {
return true;
}
@Override
public int getPollingInterval() {
return backoff.getPollingInterval();
}
@Override
public void poll(Collection<Pair<TransportProperties, ConnectionHandler>>
properties) {
if (getState() != ACTIVE) return;
backoff.increment();
for (Pair<TransportProperties, ConnectionHandler> p : properties) {
connect(p.getFirst(), p.getSecond());
}
}
private void connect(TransportProperties p, ConnectionHandler h) {
String address = p.get(PROP_ADDRESS);
if (isNullOrEmpty(address)) return;
String uuid = p.get(PROP_UUID);
if (isNullOrEmpty(uuid)) return;
wakefulIoExecutor.execute(() -> {
DuplexTransportConnection d = createConnection(p);
if (d != null) {
backoff.reset();
setEverConnected();
h.handleConnection(d);
}
});
}
@Nullable
private DuplexTransportConnection connect(String address, String uuid) {
// Validate the address
if (!isValidAddress(address)) {
if (LOG.isLoggable(WARNING))
// Not scrubbing here to be able to figure out the problem
LOG.warning("Invalid address " + address);
return null;
}
// Validate the UUID
try {
//noinspection ResultOfMethodCallIgnored
UUID.fromString(uuid);
} catch (IllegalArgumentException e) {
if (LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
return null;
}
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubMacAddress(address));
try {
DuplexTransportConnection conn = connectTo(address, uuid);
if (LOG.isLoggable(INFO))
LOG.info("Connected to " + scrubMacAddress(address));
return conn;
} catch (IOException e) {
if (LOG.isLoggable(INFO))
LOG.info("Could not connect to " + scrubMacAddress(address));
return null;
}
}
@Override
public DuplexTransportConnection createConnection(TransportProperties p) {
if (getState() != ACTIVE) return null;
if (!connectionLimiter.canOpenContactConnection()) return null;
String address = p.get(PROP_ADDRESS);
if (isNullOrEmpty(address)) return null;
String uuid = p.get(PROP_UUID);
if (isNullOrEmpty(uuid)) return null;
DuplexTransportConnection conn = connect(address, uuid);
if (conn != null) connectionLimiter.connectionOpened(conn);
return conn;
}
@Override
public boolean supportsKeyAgreement() {
return true;
}
@Override
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
if (getState() != ACTIVE) return null;
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
// Bind a server socket for receiving key agreement connections
SS ss;
try {
ss = openServerSocket(uuid);
} catch (IOException e) {
logException(LOG, WARNING, e);
return null;
}
if (getState() != ACTIVE) {
tryToClose(ss);
return null;
}
BdfList descriptor = new BdfList();
descriptor.add(TRANSPORT_ID_BLUETOOTH);
String address = getBluetoothAddress();
if (address != null) descriptor.add(macToBytes(address));
return new BluetoothKeyAgreementListener(descriptor, ss);
}
@Override
public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor) {
if (getState() != ACTIVE) return null;
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
DuplexTransportConnection conn;
if (descriptor.size() == 1) {
if (LOG.isLoggable(INFO))
LOG.info("Discovering address for key agreement UUID " + uuid);
conn = discoverAndConnect(uuid);
} else {
String address;
try {
address = parseAddress(descriptor);
} catch (FormatException e) {
LOG.info("Invalid address in key agreement descriptor");
return null;
}
if (LOG.isLoggable(INFO))
LOG.info("Connecting to key agreement UUID " + uuid);
conn = connect(address, uuid);
}
if (conn != null) {
connectionLimiter.connectionOpened(conn);
setEverConnected();
}
return conn;
}
private String parseAddress(BdfList descriptor) throws FormatException {
byte[] mac = descriptor.getRaw(1);
if (mac.length != 6) throw new FormatException();
return macToString(mac);
}
@Override
public boolean supportsRendezvous() {
return false;
}
@Override
public RendezvousEndpoint createRendezvousEndpoint(KeyMaterialSource k,
boolean alice, ConnectionHandler incoming) {
throw new UnsupportedOperationException();
}
@Override
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
if (s.getNamespace().equals(ID.getString()))
ioExecutor.execute(() -> onSettingsUpdated(s.getSettings()));
} else if (e instanceof KeyAgreementListeningEvent) {
ioExecutor.execute(connectionLimiter::keyAgreementStarted);
} else if (e instanceof KeyAgreementStoppedListeningEvent) {
ioExecutor.execute(connectionLimiter::keyAgreementEnded);
} else if (e instanceof RemoteTransportPropertiesUpdatedEvent) {
RemoteTransportPropertiesUpdatedEvent r =
(RemoteTransportPropertiesUpdatedEvent) e;
if (r.getTransportId().equals(ID)) {
ioExecutor.execute(this::updateProperties);
}
}
}
@IoExecutor
private void onSettingsUpdated(Settings settings) {
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
DEFAULT_PREF_PLUGIN_ENABLE);
SS ss = state.setEnabledByUser(enabledByUser);
State s = getState();
if (ss != null) {
LOG.info("Disabled by user, closing server socket");
tryToClose(ss);
} else if (s == INACTIVE) {
if (isAdapterEnabled()) {
LOG.info("Enabled by user, opening server socket");
bind();
} else {
LOG.info("Enabled by user but adapter is disabled");
}
}
}
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
private final SS ss;
private BluetoothKeyAgreementListener(BdfList descriptor, SS ss) {
super(descriptor);
this.ss = ss;
}
@Override
public KeyAgreementConnection accept() throws IOException {
DuplexTransportConnection conn = acceptConnection(ss);
if (LOG.isLoggable(INFO)) LOG.info(ID + ": Incoming connection");
connectionLimiter.connectionOpened(conn);
return new KeyAgreementConnection(conn, ID);
}
@Override
public void close() {
tryToClose(ss);
}
}
@ThreadSafe
@NotNullByDefault
protected class PluginState {
@GuardedBy("this")
private boolean started = false,
stopped = false,
enabledByUser = false;
@GuardedBy("this")
@Nullable
private SS serverSocket = null;
synchronized void setStarted(boolean enabledByUser) {
started = true;
this.enabledByUser = enabledByUser;
callback.pluginStateChanged(getState());
}
@Nullable
synchronized SS setStopped() {
stopped = true;
SS ss = serverSocket;
serverSocket = null;
callback.pluginStateChanged(getState());
return ss;
}
@Nullable
synchronized SS setEnabledByUser(boolean enabledByUser) {
this.enabledByUser = enabledByUser;
SS ss = null;
if (!enabledByUser) {
ss = serverSocket;
serverSocket = null;
}
callback.pluginStateChanged(getState());
return ss;
}
synchronized boolean setServerSocket(SS ss) {
if (stopped || serverSocket != null) return false;
serverSocket = ss;
callback.pluginStateChanged(getState());
return true;
}
@Nullable
synchronized SS clearServerSocket() {
SS ss = serverSocket;
serverSocket = null;
callback.pluginStateChanged(getState());
return ss;
}
synchronized State getState() {
if (!started || stopped) return STARTING_STOPPING;
if (!enabledByUser) return DISABLED;
return serverSocket == null ? INACTIVE : ACTIVE;
}
synchronized int getReasonsDisabled() {
return getState() == DISABLED ? REASON_USER : 0;
}
}
void stopDiscoverAndConnect();
}

View File

@@ -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<Pair<TransportProperties, ConnectionHandler>> 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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Consumer<State>> 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<State> o) {
State state;
synchronized (lock) {
observers.add(o);
state = this.state;
eventExecutor.execute(() -> o.accept(state));
}
}
@Override
public void removeObserver(Consumer<State> 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<Consumer<State>> observers = new ArrayList<>(this.observers);
State state = this.state;
eventExecutor.execute(() -> {
for (Consumer<State> o : observers) o.accept(state);
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,7 +50,7 @@ public class LanTcpPluginFactory implements DuplexPluginFactory {
}
@Override
public int getMaxLatency() {
public long getMaxLatency() {
return MAX_LATENCY;
}

View File

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

View File

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

View File

@@ -54,7 +54,7 @@ public class WanTcpPluginFactory implements DuplexPluginFactory {
}
@Override
public int getMaxLatency() {
public long getMaxLatency() {
return MAX_LATENCY;
}

View File

@@ -131,8 +131,9 @@ 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 File torDirectory, torFile, geoIpFile, obfs4File, configFile;
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;
@@ -181,9 +182,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
socketTimeout = Integer.MAX_VALUE;
else socketTimeout = maxIdleTime * 2;
this.torDirectory = torDirectory;
torFile = new File(torDirectory, "tor");
geoIpFile = new File(torDirectory, "geoip");
obfs4File = new File(torDirectory, "obfs4proxy");
configFile = new File(torDirectory, "torrc");
doneFile = new File(torDirectory, "done");
cookieFile = new File(torDirectory, ".tor/control_auth_cookie");
@@ -192,13 +191,21 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
new PoliteExecutor("TorPlugin", ioExecutor, 1);
}
protected File getTorExecutableFile() {
return new File(torDirectory, "tor");
}
protected File getObfs4ExecutableFile() {
return new File(torDirectory, "obfs4proxy");
}
@Override
public TransportId getId() {
return TorConstants.ID;
}
@Override
public int getMaxLatency() {
public long getMaxLatency() {
return maxLatency;
}
@@ -224,6 +231,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
LOG.warning("Old auth cookie not deleted");
// Start a new Tor process
LOG.info("Starting Tor");
File torFile = getTorExecutableFile();
String torPath = torFile.getAbsolutePath();
String configPath = configFile.getAbsolutePath();
String pid = String.valueOf(getProcessId());
@@ -322,44 +330,43 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
private void installAssets() throws PluginException {
InputStream in = null;
OutputStream out = null;
try {
// The done file may already exist from a previous installation
//noinspection ResultOfMethodCallIgnored
doneFile.delete();
// Unzip the Tor binary to the filesystem
in = getTorInputStream();
out = new FileOutputStream(torFile);
copyAndClose(in, out);
// Make the Tor binary executable
if (!torFile.setExecutable(true, true)) throw new IOException();
// Unzip the GeoIP database to the filesystem
in = getGeoIpInputStream();
out = new FileOutputStream(geoIpFile);
copyAndClose(in, out);
// Unzip the Obfs4 proxy to the filesystem
in = getObfs4InputStream();
out = new FileOutputStream(obfs4File);
copyAndClose(in, out);
// Make the Obfs4 proxy executable
if (!obfs4File.setExecutable(true, true)) throw new IOException();
// Copy the config file to the filesystem
in = getConfigInputStream();
out = new FileOutputStream(configFile);
copyAndClose(in, out);
installTorExecutable();
installObfs4Executable();
extract(getGeoIpInputStream(), geoIpFile);
extract(getConfigInputStream(), configFile);
if (!doneFile.createNewFile())
LOG.warning("Failed to create done file");
} catch (IOException e) {
tryToClose(in, LOG, WARNING);
tryToClose(out, LOG, WARNING);
throw new PluginException(e);
}
}
private InputStream getTorInputStream() throws IOException {
protected void extract(InputStream in, File dest) throws IOException {
OutputStream out = new FileOutputStream(dest);
copyAndClose(in, out);
}
protected void installTorExecutable() throws IOException {
if (LOG.isLoggable(INFO))
LOG.info("Installing Tor binary for " + architecture);
File torFile = getTorExecutableFile();
extract(getTorInputStream(), torFile);
if (!torFile.setExecutable(true, true)) throw new IOException();
}
protected void installObfs4Executable() throws IOException {
if (LOG.isLoggable(INFO))
LOG.info("Installing obfs4proxy binary for " + architecture);
File obfs4File = getObfs4ExecutableFile();
extract(getObfs4InputStream(), obfs4File);
if (!obfs4File.setExecutable(true, true)) throw new IOException();
}
private InputStream getTorInputStream() throws IOException {
InputStream in = resourceProvider
.getResourceInputStream("tor_" + architecture, ".zip");
ZipInputStream zin = new ZipInputStream(in);
@@ -376,8 +383,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
private InputStream getObfs4InputStream() throws IOException {
if (LOG.isLoggable(INFO))
LOG.info("Installing obfs4proxy binary for " + architecture);
InputStream in = resourceProvider
.getResourceInputStream("obfs4proxy_" + architecture, ".zip");
ZipInputStream zin = new ZipInputStream(in);
@@ -569,6 +574,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (enable) {
Collection<String> conf = new ArrayList<>();
conf.add("UseBridges 1");
File obfs4File = getObfs4ExecutableFile();
if (needsMeek) {
conf.add("ClientTransportPlugin meek_lite exec " +
obfs4File.getAbsolutePath());
@@ -858,6 +864,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (!state.isTorRunning()) return;
boolean online = status.isConnected();
boolean wifi = status.isWifi();
boolean ipv6Only = status.isIpv6Only();
String country = locationUtils.getCurrentCountry();
boolean blocked =
circumventionProvider.isTorProbablyBlocked(country);
@@ -874,7 +881,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
boolean automatic = network == PREF_TOR_NETWORK_AUTOMATIC;
if (LOG.isLoggable(INFO)) {
LOG.info("Online: " + online + ", wifi: " + wifi);
LOG.info("Online: " + online + ", wifi: " + wifi
+ ", IPv6 only: " + ipv6Only);
if (country.isEmpty()) LOG.info("Country code unknown");
else LOG.info("Country code: " + country);
LOG.info("Charging: " + charging);
@@ -911,7 +919,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
enableNetwork = true;
if (network == PREF_TOR_NETWORK_WITH_BRIDGES ||
(automatic && bridgesWork)) {
if (circumventionProvider.needsMeek(country)) {
if (ipv6Only ||
circumventionProvider.needsMeek(country)) {
LOG.info("Using meek bridges");
enableBridges = true;
useMeek = true;
@@ -937,6 +946,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (enableNetwork) {
enableBridges(enableBridges, useMeek);
enableConnectionPadding(enableConnectionPadding);
useIpv6(ipv6Only);
}
enableNetwork(enableNetwork);
} catch (IOException e) {
@@ -949,6 +959,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
controlConnection.setConf("ConnectionPadding", enable ? "1" : "0");
}
private void useIpv6(boolean ipv6Only) throws IOException {
controlConnection.setConf("ClientUseIPv4", ipv6Only ? "0" : "1");
controlConnection.setConf("ClientUseIPv6", ipv6Only ? "1" : "0");
}
@ThreadSafe
@NotNullByDefault
protected class PluginState {

View File

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

View File

@@ -15,6 +15,9 @@ import org.briarproject.bramble.api.system.Clock;
import javax.annotation.concurrent.Immutable;
import static org.briarproject.bramble.api.plugin.TransportId.MAX_TRANSPORT_ID_LENGTH;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_LOCAL;
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.util.ValidationUtils.checkLength;
import static org.briarproject.bramble.util.ValidationUtils.checkSize;
@@ -43,9 +46,9 @@ class TransportPropertyValidator extends BdfMessageValidator {
clientHelper.parseAndValidateTransportProperties(dictionary);
// Return the metadata
BdfDictionary meta = new BdfDictionary();
meta.put("transportId", transportId);
meta.put("version", version);
meta.put("local", false);
meta.put(MSG_KEY_TRANSPORT_ID, transportId);
meta.put(MSG_KEY_VERSION, version);
meta.put(MSG_KEY_LOCAL, false);
return new BdfMessageContext(meta);
}
}

View File

@@ -29,6 +29,7 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import javax.net.SocketFactory;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
@@ -100,11 +101,12 @@ class DevReporterImpl implements DevReporter, EventListener {
}
@Override
public void sendReports() {
public int sendReports() {
File reportDir = devConfig.getReportDir();
File[] reports = reportDir.listFiles();
int reportsSent = 0;
if (reports == null || reports.length == 0)
return; // No reports to send
return reportsSent; // No reports to send
LOG.info("Sending reports to developers");
for (File f : reports) {
@@ -116,13 +118,15 @@ class DevReporterImpl implements DevReporter, EventListener {
in = new FileInputStream(f);
IoUtils.copyAndClose(in, out);
f.delete();
reportsSent++;
} catch (IOException e) {
LOG.log(WARNING, "Failed to send reports", e);
tryToClose(out, LOG, WARNING);
tryToClose(in, LOG, WARNING);
return;
return reportsSent;
}
}
LOG.info("Reports sent");
if (LOG.isLoggable(INFO)) LOG.info(reportsSent + " report(s) sent");
return reportsSent;
}
}

View File

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

View File

@@ -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<MessageId, Integer> 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<IOException> {
private final Ack ack;
private WriteAck(Ack ack) {
this.ack = ack;
@DatabaseExecutor
private void generateEagerBatch(Map<MessageId, Integer> ids) {
if (interrupted) return;
// Take some message IDs from `ids` to form a batch
Collection<MessageId> batchIds = new ArrayList<>();
long totalLength = 0;
Iterator<Entry<MessageId, Integer>> it = ids.entrySet().iterator();
while (it.hasNext()) {
// Check whether the next message will fit in the batch
Entry<MessageId, Integer> 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<Message> 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<Message> batch,
Map<MessageId, Integer> 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<Message> 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<IOException> {
@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<Message> batch;
private WriteBatch(Collection<Message> batch) {
this.batch = batch;
@DatabaseExecutor
private void generateBatch() {
if (interrupted) return;
try {
Collection<Message> 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<Message> batch) throws IOException {
if (interrupted) return;
for (Message m : batch) recordWriter.writeMessage(m);
LOG.info("Sent batch");
dbExecutor.execute(this::generateBatch);
}
}

View File

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

View File

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

View File

@@ -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<TransportId, TransportKeyManager> 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<TransportId, Integer> 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<TransportId, Integer> e : transports.entrySet())
db.addTransport(txn, e.getKey(), e.getValue());
for (Entry<TransportId, Integer> 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<TransportId, KeySetId> 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<TransportId, KeySetId> addRotationKeys(Transaction txn,
ContactId c, SecretKey rootKey, long timestamp, boolean alice,
boolean active) throws DbException {
Map<TransportId, KeySetId> ids = new HashMap<>();
for (Entry<TransportId, TransportKeyManager> 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);

View File

@@ -24,15 +24,21 @@ class StreamReaderFactoryImpl implements StreamReaderFactory {
@Override
public InputStream createStreamReader(InputStream in, StreamContext ctx) {
return new StreamReaderImpl(
streamDecrypterFactory.createStreamDecrypter(in, ctx));
return new StreamReaderImpl(streamDecrypterFactory
.createStreamDecrypter(in, ctx));
}
@Override
public InputStream createContactExchangeStreamReader(InputStream in,
SecretKey headerKey) {
return new StreamReaderImpl(
streamDecrypterFactory.createContactExchangeStreamDecrypter(in,
headerKey));
return new StreamReaderImpl(streamDecrypterFactory
.createContactExchangeStreamDecrypter(in, headerKey));
}
@Override
public InputStream createLogStreamReader(InputStream in,
SecretKey headerKey) {
return new StreamReaderImpl(streamDecrypterFactory
.createLogStreamDecrypter(in, headerKey));
}
}

View File

@@ -26,15 +26,21 @@ class StreamWriterFactoryImpl implements StreamWriterFactory {
@Override
public StreamWriter createStreamWriter(OutputStream out,
StreamContext ctx) {
return new StreamWriterImpl(
streamEncrypterFactory.createStreamEncrypter(out, ctx));
return new StreamWriterImpl(streamEncrypterFactory
.createStreamEncrypter(out, ctx));
}
@Override
public StreamWriter createContactExchangeStreamWriter(OutputStream out,
SecretKey headerKey) {
return new StreamWriterImpl(
streamEncrypterFactory.createContactExchangeStreamDecrypter(out,
headerKey));
return new StreamWriterImpl(streamEncrypterFactory
.createContactExchangeStreamEncrypter(out, headerKey));
}
}
@Override
public StreamWriter createLogStreamWriter(OutputStream out,
SecretKey headerKey) {
return new StreamWriterImpl(streamEncrypterFactory
.createLogStreamEncrypter(out, headerKey));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<TransportId> 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<Contact> 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<ContactId, Collection<TransportId>> transportsWithKeys =
db.getTransportsWithKeys(txn);
for (Contact c : contacts) {
Collection<TransportId> 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<MessageId> 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;
}
}
}

View File

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

View File

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

View File

@@ -5,6 +5,5 @@ interface ClientVersioningConstants {
// Metadata keys
String MSG_KEY_UPDATE_VERSION = "version";
String MSG_KEY_LOCAL = "local";
String GROUP_KEY_CONTACT_ID = "contactId";
}

View File

@@ -50,7 +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.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
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;
@@ -161,13 +161,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
db.addGroup(txn, g);
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
// Attach the contact ID to the group
BdfDictionary meta = new BdfDictionary();
meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
try {
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
} catch (FormatException e) {
throw new AssertionError(e);
}
clientHelper.setContactId(txn, g.getId(), c.getId());
// Create and store the first local update
List<ClientVersion> versions = new ArrayList<>(clients);
Collections.sort(versions);
@@ -180,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));
@@ -194,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();
@@ -229,7 +223,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
Map<ClientMajorVersion, Visibility> after =
getVisibilities(newLocalStates, newRemoteStates);
// Call hooks for any visibilities that have changed
ContactId c = getContactId(txn, m.getGroupId());
ContactId c = clientHelper.getContactId(txn, m.getGroupId());
if (!before.equals(after)) {
Contact contact = db.getContact(txn, c);
callVisibilityHooks(txn, contact, before, after);
@@ -248,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,
@@ -521,17 +515,6 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
storeUpdate(txn, g, states, 1);
}
private ContactId getContactId(Transaction txn, GroupId g)
throws DbException {
try {
BdfDictionary meta =
clientHelper.getGroupMetadataAsDictionary(txn, g);
return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
} catch (FormatException e) {
throw new DbException(e);
}
}
private List<ClientState> updateStatesFromRemoteStates(
List<ClientState> oldLocalStates, List<ClientState> remoteStates) {
Set<ClientMajorVersion> remoteSet = new HashSet<>();

View File

@@ -1,5 +1,10 @@
Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0
Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0
Bridge obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0
Bridge obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0
Bridge obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0
Bridge obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0
Bridge obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0
Bridge obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0
Bridge obfs4 78.46.188.239:37356 5A2D2F4158D0453E00C7C176978D3F41D69C45DB cert=3c0SwxpOisbohNxEc4tb875RVW8eOu1opRTVXJhafaKA/PNNtI7ElQIVOVZg1AdL5bxGCw iat-mode=0
Bridge obfs4 52.15.78.72:9443 02069A3C5362476936B62BA6F5ACC41ABD573A9B cert=ijYG/OKc7kqu2YzKNFfeXN7/BG2BOgfEP2KyYEiGDQthnHbsOiTWHeIG0WJVW+BckzDgKw iat-mode=0
Bridge obfs4 13.58.29.242:9443 0C58939A77DA6B6B29D4B5236A75865659607AE0 cert=OylWIEHb/ezpq1zWxW0sgKRn+9ARH2eOcQOZ8/Gew+4l+oKOhQ2jUX/Y+FSl61JorXZUWA iat-mode=0
Bridge obfs4 45.33.37.112:9443 60A609BB4ABE8D46E634AE81ED29ADAB7776B399 cert=t5v19WmNv5Sc2YPNr8RQids365W7MY8zJwQVkOxBjUMFomMWARDzsbYpcWLLcw0J9Gm+BQ iat-mode=0
Bridge meek_lite 0.0.2.0:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com
Bridge meek_lite 192.0.2.2:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com

View File

@@ -1,18 +1,18 @@
package org.briarproject.bramble;
import org.briarproject.bramble.system.DefaultTaskSchedulerModule;
import org.briarproject.bramble.system.TimeTravelModule;
public interface BrambleCoreIntegrationTestEagerSingletons
extends BrambleCoreEagerSingletons {
void inject(DefaultTaskSchedulerModule.EagerSingletons init);
void inject(TimeTravelModule.EagerSingletons init);
class Helper {
public static void injectEagerSingletons(
BrambleCoreIntegrationTestEagerSingletons c) {
BrambleCoreEagerSingletons.Helper.injectEagerSingletons(c);
c.inject(new DefaultTaskSchedulerModule.EagerSingletons());
c.inject(new TimeTravelModule.EagerSingletons());
}
}
}

View File

@@ -8,18 +8,15 @@ import org.briarproject.bramble.api.contact.PendingContactState;
import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.identity.AuthorInfo;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.DbExpectations;
import org.jmock.Expectations;
import org.junit.Before;
import org.junit.Test;
@@ -31,10 +28,6 @@ import static java.util.Collections.singletonList;
import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.BASE32_LINK_BYTES;
import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.OURSELVES;
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNKNOWN;
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNVERIFIED;
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.VERIFIED;
import static org.briarproject.bramble.test.TestUtils.getAgreementPrivateKey;
import static org.briarproject.bramble.test.TestUtils.getAgreementPublicKey;
import static org.briarproject.bramble.test.TestUtils.getAuthor;
@@ -46,7 +39,6 @@ import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.briarproject.bramble.util.StringUtils.getRandomBase32String;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class ContactManagerImplTest extends BrambleMockTestCase {
@@ -212,75 +204,6 @@ public class ContactManagerImplTest extends BrambleMockTestCase {
assertTrue(contactManager.contactExists(remote.getId(), local));
}
@Test
public void testGetAuthorInfo() throws Exception {
Transaction txn = new Transaction(null, true);
context.checking(new DbExpectations() {{
oneOf(identityManager).getLocalAuthor(txn);
will(returnValue(localAuthor));
oneOf(db).getContactsByAuthorId(txn, remote.getId());
will(returnValue(singletonList(contact)));
}});
AuthorInfo authorInfo =
contactManager.getAuthorInfo(txn, remote.getId());
assertEquals(UNVERIFIED, authorInfo.getStatus());
assertEquals(contact.getAlias(), authorInfo.getAlias());
}
@Test
public void testGetAuthorInfoTransaction() throws DbException {
Transaction txn = new Transaction(null, true);
// check unknown author
context.checking(new Expectations() {{
oneOf(identityManager).getLocalAuthor(txn);
will(returnValue(localAuthor));
oneOf(db).getContactsByAuthorId(txn, remote.getId());
will(returnValue(emptyList()));
}});
AuthorInfo authorInfo =
contactManager.getAuthorInfo(txn, remote.getId());
assertEquals(UNKNOWN, authorInfo.getStatus());
assertNull(authorInfo.getAlias());
// check unverified contact
checkAuthorInfoContext(txn, remote.getId(), singletonList(contact));
authorInfo = contactManager.getAuthorInfo(txn, remote.getId());
assertEquals(UNVERIFIED, authorInfo.getStatus());
assertEquals(contact.getAlias(), authorInfo.getAlias());
// check verified contact
Contact verified = getContact(remote, local, true);
checkAuthorInfoContext(txn, remote.getId(), singletonList(verified));
authorInfo = contactManager.getAuthorInfo(txn, remote.getId());
assertEquals(VERIFIED, authorInfo.getStatus());
assertEquals(verified.getAlias(), authorInfo.getAlias());
// check ourselves
context.checking(new Expectations() {{
oneOf(identityManager).getLocalAuthor(txn);
will(returnValue(localAuthor));
never(db).getContactsByAuthorId(txn, remote.getId());
}});
authorInfo = contactManager.getAuthorInfo(txn, localAuthor.getId());
assertEquals(OURSELVES, authorInfo.getStatus());
assertNull(authorInfo.getAlias());
}
private void checkAuthorInfoContext(Transaction txn, AuthorId authorId,
Collection<Contact> contacts) throws DbException {
context.checking(new Expectations() {{
oneOf(identityManager).getLocalAuthor(txn);
will(returnValue(localAuthor));
oneOf(db).getContactsByAuthorId(txn, authorId);
will(returnValue(contacts));
}});
}
@Test
public void testGetHandshakeLink() throws Exception {
Transaction txn = new Transaction(null, true);

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContactId;
@@ -69,6 +70,8 @@ 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.concurrent.TimeUnit.HOURS;
import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
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;
@@ -117,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;
@@ -295,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);
@@ -346,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) {
@@ -354,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) {
@@ -362,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) {
@@ -370,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) {
@@ -378,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) {
@@ -510,11 +521,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception {
context.checking(new Expectations() {{
// Check whether the group is in the DB (which it's not)
exactly(8).of(database).startTransaction();
exactly(10).of(database).startTransaction();
will(returnValue(txn));
exactly(8).of(database).containsGroup(txn, groupId);
exactly(10).of(database).containsGroup(txn, groupId);
will(returnValue(false));
exactly(8).of(database).abortTransaction(txn);
exactly(10).of(database).abortTransaction(txn);
// Allow other checks to pass
allowing(database).containsContact(txn, contactId);
will(returnValue(true));
@@ -523,7 +534,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
eventExecutor, shutdownManager);
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getGroup(transaction, groupId));
fail();
} catch (NoSuchGroupException expected) {
@@ -531,7 +542,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getGroupMetadata(transaction, groupId));
fail();
} catch (NoSuchGroupException expected) {
@@ -539,7 +550,23 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageIds(transaction, groupId));
fail();
} catch (NoSuchGroupException expected) {
// Expected
}
try {
db.transaction(true, transaction ->
db.getMessageIds(transaction, groupId, new Metadata()));
fail();
} catch (NoSuchGroupException expected) {
// Expected
}
try {
db.transaction(true, transaction ->
db.getMessageMetadata(transaction, groupId));
fail();
} catch (NoSuchGroupException expected) {
@@ -547,7 +574,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageMetadata(transaction, groupId,
new Metadata()));
fail();
@@ -556,7 +583,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageStatus(transaction, contactId, groupId));
fail();
} catch (NoSuchGroupException expected) {
@@ -594,11 +621,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception {
context.checking(new Expectations() {{
// Check whether the message is in the DB (which it's not)
exactly(12).of(database).startTransaction();
exactly(15).of(database).startTransaction();
will(returnValue(txn));
exactly(12).of(database).containsMessage(txn, messageId);
exactly(15).of(database).containsMessage(txn, messageId);
will(returnValue(false));
exactly(12).of(database).abortTransaction(txn);
exactly(15).of(database).abortTransaction(txn);
// Allow other checks to pass
allowing(database).containsContact(txn, contactId);
will(returnValue(true));
@@ -623,7 +650,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessage(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -631,7 +658,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageMetadata(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -639,7 +666,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageState(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -647,7 +674,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageStatus(transaction, contactId, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -662,6 +689,15 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
// Expected
}
try {
db.transaction(false, transaction ->
db.setCleanupTimerDuration(transaction, message.getId(),
HOURS.toMillis(1)));
fail();
} catch (NoSuchMessageException expected) {
// Expected
}
try {
db.transaction(false, transaction ->
db.setMessagePermanent(transaction, message.getId()));
@@ -687,7 +723,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageDependencies(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -695,12 +731,28 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageDependents(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
// Expected
}
try {
db.transaction(false, transaction ->
db.startCleanupTimer(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
// Expected
}
try {
db.transaction(false, transaction ->
db.stopCleanupTimer(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
// Expected
}
}
@Test
@@ -981,6 +1033,9 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
will(returnValue(true));
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
will(returnValue(true));
oneOf(database).startCleanupTimer(txn, messageId);
will(returnValue(TIMER_NOT_STARTED)); // No cleanup duration was set
oneOf(database).commitTransaction(txn);
oneOf(eventBus).broadcast(with(any(MessagesAckedEvent.class)));
}});
@@ -993,6 +1048,56 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
});
}
@Test
public void testReceiveDuplicateAck() throws Exception {
context.checking(new Expectations() {{
oneOf(database).startTransaction();
will(returnValue(txn));
oneOf(database).containsContact(txn, contactId);
will(returnValue(true));
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
will(returnValue(true));
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
will(returnValue(false)); // Already acked
oneOf(database).commitTransaction(txn);
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager);
db.transaction(false, transaction -> {
Ack a = new Ack(singletonList(messageId));
db.receiveAck(transaction, contactId, a);
});
}
@Test
public void testReceiveAckWithCleanupTimer() throws Exception {
long deadline = System.currentTimeMillis();
context.checking(new Expectations() {{
oneOf(database).startTransaction();
will(returnValue(txn));
oneOf(database).containsContact(txn, contactId);
will(returnValue(true));
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
will(returnValue(true));
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
will(returnValue(true));
oneOf(database).startCleanupTimer(txn, messageId);
will(returnValue(deadline));
oneOf(database).commitTransaction(txn);
oneOf(eventBus).broadcast(with(any(
CleanupTimerStartedEvent.class)));
oneOf(eventBus).broadcast(with(any(MessagesAckedEvent.class)));
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager);
db.transaction(false, transaction -> {
Ack a = new Ack(singletonList(messageId));
db.receiveAck(transaction, contactId, a);
});
}
@Test
public void testReceiveMessage() throws Exception {
context.checking(new Expectations() {{

View File

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

View File

@@ -41,8 +41,12 @@ import org.junit.Test;
import java.io.File;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -53,10 +57,12 @@ import java.util.concurrent.atomic.AtomicLong;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
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;
import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
@@ -217,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<MessageId> 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();
@@ -248,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<MessageId> 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();
@@ -292,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<MessageId> 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();
@@ -344,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<MessageId> 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);
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);
db.setMessageShared(txn, messageId, true);
assertOneMessageToSendLazily(db, txn);
assertOneMessageToSendEagerly(db, txn);
db.commitTransaction(txn);
db.close();
@@ -375,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<MessageId> 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);
@@ -400,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();
@@ -407,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<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234);
assertEquals(asList(messageId, messageId1), ids);
@@ -414,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));
@@ -422,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);
@@ -442,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<MessageId> 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();
@@ -631,8 +631,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// The group should not be visible to the contact
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(emptyMap(),
db.getGroupVisibility(txn, groupId));
assertTrue(db.getGroupVisibility(txn, groupId).isEmpty());
// Make the group visible to the contact
db.addGroupVisibility(txn, contactId, groupId, false);
@@ -655,8 +654,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Make the group invisible again
db.removeGroupVisibility(txn, contactId, groupId);
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(emptyMap(),
db.getGroupVisibility(txn, groupId));
assertTrue(db.getGroupVisibility(txn, groupId).isEmpty());
db.commitTransaction(txn);
db.close();
@@ -673,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);
@@ -684,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<TransportKeySet> allKeys =
db.getTransportKeys(txn, transportId);
assertEquals(2, allKeys.size());
@@ -696,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);
@@ -707,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) {
@@ -718,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();
@@ -1922,11 +1930,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(db.containsVisibleMessage(txn, contactId, messageId));
// The message should be sendable
Collection<MessageId> 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);
@@ -1942,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 {
@@ -2040,7 +2043,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
// Share the message - now it should be sendable immediately
db.setMessageShared(txn, messageId);
db.setMessageShared(txn, messageId, true);
assertEquals(0, db.getNextSendTime(txn, contactId));
// Mark the message as requested - it should still be sendable
@@ -2347,6 +2350,148 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.close();
}
@Test
public void testShutdownGracefully() throws Exception {
Database<Connection> db = open(false);
db.close();
open(true);
assertFalse(db.wasDirtyOnInitialisation());
}
@Test
public void testShutdownDirty() throws Exception {
Database<Connection> db = open(false);
// We want to simulate a dirty shutdown here which would normally be
// caused by an empty battery or by force closing the Android app.
// As there is no obvious way to simulate this, we're artificially
// causing an SqlException during close() here by unloading the JDBC
// drivers.
List<String> unloadedDrivers = unloadDrivers();
try {
db.close();
fail();
} catch (Exception e) {
// continue
e.printStackTrace();
}
// Reloading drivers to continue so that we're able to work with the
// database again.
reloadDrivers(unloadedDrivers);
db = open(true);
assertTrue(db.wasDirtyOnInitialisation());
}
@Test
public void testShutdownDirtyThenGracefully() throws Exception {
Database<Connection> db = open(false);
// Simulating a dirty shutdown here, look at #testShutdownDirty for
// details.
List<String> unloadedDrivers = unloadDrivers();
try {
db.close();
fail();
} catch (Exception e) {
// continue
}
reloadDrivers(unloadedDrivers);
db = open(true);
assertTrue(db.wasDirtyOnInitialisation());
db.close();
db = open(true);
assertFalse(db.wasDirtyOnInitialisation());
}
@Test
public void testCleanupTimer() throws Exception {
long duration = 60_000;
long now = System.currentTimeMillis();
AtomicLong time = new AtomicLong(now);
Database<Connection> db =
open(false, new TestMessageFactory(), new SettableClock(time));
Connection txn = db.startTransaction();
// No messages should be due or scheduled for deletion
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
// Add a group and a message
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, false, false, null);
// No messages should be due or scheduled for deletion
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
// Set the message's cleanup timer duration
db.setCleanupTimerDuration(txn, messageId, duration);
// No messages should be due or scheduled for deletion
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
// Start the message's cleanup timer
assertEquals(now + duration, db.startCleanupTimer(txn, messageId));
// The timer can't be started again
assertEquals(TIMER_NOT_STARTED, db.startCleanupTimer(txn, messageId));
// No messages should be due for deletion, but the message should be
// scheduled for deletion
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
// Stop the timer
db.stopCleanupTimer(txn, messageId);
// No messages should be due or scheduled for deletion
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
// Start the timer again
assertEquals(now + duration, db.startCleanupTimer(txn, messageId));
// No messages should be due for deletion, but the message should be
// scheduled for deletion
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
// 1 ms before the timer expires, no messages should be due for
// deletion but the message should be scheduled for deletion
time.set(now + duration - 1);
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
// When the timer expires, the message should be due and scheduled for
// deletion
time.set(now + duration);
assertEquals(singletonMap(groupId, singletonList(messageId)),
db.getMessagesToDelete(txn));
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
// 1 ms after the timer expires, the message should be due and
// scheduled for deletion
time.set(now + duration + 1);
assertEquals(singletonMap(groupId, singletonList(messageId)),
db.getMessagesToDelete(txn));
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
// Once the message has been deleted, it should no longer be due
// or scheduled for deletion
db.deleteMessage(txn, messageId);
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
}
private Database<Connection> open(boolean resume) throws Exception {
return open(resume, new TestMessageFactory(), new SystemClock());
}
@@ -2402,11 +2547,80 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
rootKey, alice);
}
private List<String> unloadDrivers() {
Enumeration<Driver> drivers = DriverManager.getDrivers();
List<String> unloaded = new ArrayList<>();
while (drivers.hasMoreElements()) {
Driver d = drivers.nextElement();
try {
DriverManager.deregisterDriver(d);
unloaded.add(d.getClass().getName());
} catch (SQLException e) {
e.printStackTrace();
fail();
}
}
return unloaded;
}
private void reloadDrivers(List<String> unloadedDrivers)
throws ClassNotFoundException, IllegalAccessException,
InstantiationException, SQLException {
for (String driverName : unloadedDrivers) {
DriverManager.registerDriver(
(Driver) Class.forName(driverName).newInstance());
}
}
@After
public void tearDown() {
deleteTestDirectory(testDir);
}
private void assertNothingToSendLazily(Database<Connection> db,
Connection txn) throws Exception {
assertFalse(
db.containsAnythingToSend(txn, contactId, MAX_LATENCY, false));
Collection<MessageId> 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<Connection> db,
Connection txn) throws Exception {
assertTrue(
db.containsAnythingToSend(txn, contactId, MAX_LATENCY, false));
Collection<MessageId> 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<Connection> db,
Connection txn) throws Exception {
assertFalse(
db.containsAnythingToSend(txn, contactId, MAX_LATENCY, true));
Map<MessageId, Integer> unacked =
db.getUnackedMessagesToSend(txn, contactId);
assertTrue(unacked.isEmpty());
assertEquals(0, db.getUnackedMessageBytesToSend(txn, contactId));
}
private void assertOneMessageToSendEagerly(Database<Connection> db,
Connection txn) throws Exception {
assertTrue(
db.containsAnythingToSend(txn, contactId, MAX_LATENCY, true));
Map<MessageId, Integer> 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;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
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 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.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
@Module
class RemovableDriveIntegrationTestModule {
@Provides
@Singleton
PluginConfig providePluginConfig(RemovableDrivePluginFactory drive) {
@NotNullByDefault
PluginConfig pluginConfig = new PluginConfig() {
@Override
public Collection<DuplexPluginFactory> getDuplexFactories() {
return emptyList();
}
@Override
public Collection<SimplexPluginFactory> getSimplexFactories() {
return singletonList(drive);
}
@Override
public boolean shouldPoll() {
return false;
}
@Override
public Map<TransportId, List<TransportId>> getTransportPreferences() {
return emptyMap();
}
};
return pluginConfig;
}
}

View File

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

View File

@@ -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<MessageId, Integer> 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();
}
}

View File

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

View File

@@ -0,0 +1,122 @@
package org.briarproject.bramble.system;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.TaskScheduler;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.junit.Assert.fail;
/**
* A {@link TaskScheduler} for use in tests. The scheduler keeps all scheduled
* tasks in a queue until {@link #runTasks()} is called.
*/
@NotNullByDefault
class TestTaskScheduler implements TaskScheduler {
private final Queue<Task> queue = new PriorityBlockingQueue<>();
private final Clock clock;
TestTaskScheduler(Clock clock) {
this.clock = clock;
}
@Override
public Cancellable schedule(Runnable task, Executor executor, long delay,
TimeUnit unit) {
AtomicBoolean cancelled = new AtomicBoolean(false);
return schedule(task, executor, delay, unit, cancelled);
}
@Override
public Cancellable scheduleWithFixedDelay(Runnable task, Executor executor,
long delay, long interval, TimeUnit unit) {
AtomicBoolean cancelled = new AtomicBoolean(false);
return scheduleWithFixedDelay(task, executor, delay, interval, unit,
cancelled);
}
private Cancellable schedule(Runnable task, Executor executor, long delay,
TimeUnit unit, AtomicBoolean cancelled) {
long delayMillis = MILLISECONDS.convert(delay, unit);
long dueMillis = clock.currentTimeMillis() + delayMillis;
Task t = new Task(task, executor, dueMillis, cancelled);
queue.add(t);
return t;
}
private Cancellable scheduleWithFixedDelay(Runnable task, Executor executor,
long delay, long interval, TimeUnit unit, AtomicBoolean cancelled) {
// All executions of this periodic task share a cancelled flag
Runnable wrapped = () -> {
task.run();
scheduleWithFixedDelay(task, executor, interval, interval, unit,
cancelled);
};
return schedule(wrapped, executor, delay, unit, cancelled);
}
/**
* Runs any scheduled tasks that are due.
*/
void runTasks() throws InterruptedException {
long now = clock.currentTimeMillis();
while (true) {
Task t = queue.peek();
if (t == null || t.dueMillis > now) return;
t = queue.poll();
// Submit the task to its executor and wait for it to finish
if (!t.run().await(1, MINUTES)) fail();
}
}
private static class Task
implements Cancellable, Comparable<Task> {
private final Runnable task;
private final Executor executor;
private final long dueMillis;
private final AtomicBoolean cancelled;
private Task(Runnable task, Executor executor, long dueMillis,
AtomicBoolean cancelled) {
this.task = task;
this.executor = executor;
this.dueMillis = dueMillis;
this.cancelled = cancelled;
}
@SuppressWarnings("UseCompareMethod") // Animal Sniffer
@Override
public int compareTo(Task task) {
return Long.valueOf(dueMillis).compareTo(task.dueMillis);
}
/**
* Submits the task to its executor and returns a latch that will be
* released when the task finishes.
*/
public CountDownLatch run() {
if (cancelled.get()) return new CountDownLatch(0);
CountDownLatch latch = new CountDownLatch(1);
executor.execute(() -> {
task.run();
latch.countDown();
});
return latch;
}
@Override
public void cancel() {
cancelled.set(true);
}
}
}

View File

@@ -0,0 +1,98 @@
package org.briarproject.bramble.system;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.TaskScheduler;
import org.briarproject.bramble.test.SettableClock;
import org.briarproject.bramble.test.TimeTravel;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicLong;
import javax.inject.Inject;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class TimeTravelModule {
public static class EagerSingletons {
@Inject
TaskScheduler scheduler;
}
private final ScheduledExecutorService scheduledExecutorService;
private final Clock clock;
private final TaskScheduler taskScheduler;
private final TimeTravel timeTravel;
public TimeTravelModule() {
this(false);
}
public TimeTravelModule(boolean travel) {
// Discard tasks that are submitted during shutdown
RejectedExecutionHandler policy =
new ScheduledThreadPoolExecutor.DiscardPolicy();
scheduledExecutorService =
new ScheduledThreadPoolExecutor(1, policy);
if (travel) {
// Use a SettableClock and TestTaskScheduler to allow time travel
AtomicLong time = new AtomicLong(System.currentTimeMillis());
clock = new SettableClock(time);
TestTaskScheduler testTaskScheduler = new TestTaskScheduler(clock);
taskScheduler = testTaskScheduler;
timeTravel = new TimeTravel() {
@Override
public void setCurrentTimeMillis(long now)
throws InterruptedException {
time.set(now);
testTaskScheduler.runTasks();
}
@Override
public void addCurrentTimeMillis(long add)
throws InterruptedException {
time.addAndGet(add);
testTaskScheduler.runTasks();
}
};
} else {
// Use the default clock and task scheduler
clock = new SystemClock();
taskScheduler = new TaskSchedulerImpl(scheduledExecutorService);
timeTravel = new TimeTravel() {
@Override
public void setCurrentTimeMillis(long now) {
throw new UnsupportedOperationException();
}
@Override
public void addCurrentTimeMillis(long add) {
throw new UnsupportedOperationException();
}
};
}
}
@Provides
Clock provideClock() {
return clock;
}
@Provides
@Singleton
TaskScheduler provideTaskScheduler(LifecycleManager lifecycleManager) {
lifecycleManager.registerForShutdown(scheduledExecutorService);
return taskScheduler;
}
@Provides
TimeTravel provideTimeTravel() {
return timeTravel;
}
}

View File

@@ -1,27 +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.DefaultTaskSchedulerModule;
import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
import org.briarproject.bramble.system.TimeTravelModule;
import dagger.Module;
import dagger.Provides;
@Module(includes = {
DefaultBatteryManagerModule.class,
DefaultEventExecutorModule.class,
DefaultTaskSchedulerModule.class,
DefaultWakefulIoExecutorModule.class,
TestDatabaseConfigModule.class,
TestFeatureFlagModule.class,
TestPluginConfigModule.class,
TestSecureRandomModule.class
TestSecureRandomModule.class,
TimeTravelModule.class
})
public class BrambleCoreIntegrationTestModule {
@Provides
FeatureFlags provideFeatureFlags() {
return () -> true;
}
}

View File

@@ -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<C extends BrambleIntegrationTestComponent>
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<MessageId> sent = new HashSet<>();
@Override
public void eventOccurred(Event e) {
if (e instanceof MessagesSentEvent) {
sent.addAll(((MessagesSentEvent) e).getMessageIds());
}
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
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 shouldEnableTransferData() {
return true;
}
};
}
}

View File

@@ -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() {

View File

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

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