mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-11 18:29:05 +01:00
Compare commits
137 Commits
1528-log-t
...
2343-mailb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdcacde915 | ||
|
|
e74021f571 | ||
|
|
6b6880c1ff | ||
|
|
5defd500ae | ||
|
|
37ff06d192 | ||
|
|
85aa21ebf6 | ||
|
|
e448699895 | ||
|
|
200f83bcfe | ||
|
|
89cce89650 | ||
|
|
8982964fbf | ||
|
|
f3a3fa0ea8 | ||
|
|
0865a06ac8 | ||
|
|
f2738c8bc4 | ||
|
|
1321f8775e | ||
|
|
9764aba47d | ||
|
|
913e5da2f5 | ||
|
|
f2ce7a386b | ||
|
|
7607b65e82 | ||
|
|
c13c2d62f5 | ||
|
|
8ea7204cf6 | ||
|
|
6ec382cfc4 | ||
|
|
ad0b28a684 | ||
|
|
0ae94e9579 | ||
|
|
57bd5789d4 | ||
|
|
f7dde1250c | ||
|
|
13d96651b4 | ||
|
|
65029982ce | ||
|
|
380921ce25 | ||
|
|
87ee8cd653 | ||
|
|
d4810a6f71 | ||
|
|
aa56aba1a5 | ||
|
|
35438dbac1 | ||
|
|
543b1178a1 | ||
|
|
7f1071f5cd | ||
|
|
e8c694fe00 | ||
|
|
b58b0c74a9 | ||
|
|
9e5029917e | ||
|
|
12ca74f86a | ||
|
|
622683f45e | ||
|
|
a5563ead28 | ||
|
|
e15f49fde7 | ||
|
|
e66f92f27e | ||
|
|
44acda2045 | ||
|
|
afd92dd916 | ||
|
|
2a969f8e0b | ||
|
|
ddc6606ccf | ||
|
|
1531a24b2d | ||
|
|
2298818af5 | ||
|
|
a19a4f36c6 | ||
|
|
6765de992d | ||
|
|
0ae5361281 | ||
|
|
d8e26eebbe | ||
|
|
692e353046 | ||
|
|
b9ba7aded5 | ||
|
|
4bca9decc1 | ||
|
|
7bbe9068bb | ||
|
|
63060679a3 | ||
|
|
ddb759dbb8 | ||
|
|
592daf9c20 | ||
|
|
3922270db1 | ||
|
|
feb8854678 | ||
|
|
4ba4e41e69 | ||
|
|
1f699238a9 | ||
|
|
b8e91a12e8 | ||
|
|
06eb01ab0a | ||
|
|
d82509f3ce | ||
|
|
b01c306500 | ||
|
|
61e7635b9f | ||
|
|
f2f356cbd4 | ||
|
|
28f3ab1310 | ||
|
|
1af52b21d5 | ||
|
|
8bb3a83ccb | ||
|
|
a742b007ef | ||
|
|
6bfd7bcc4f | ||
|
|
17f5fc7518 | ||
|
|
8dcf988399 | ||
|
|
05bf3833cf | ||
|
|
c39c2ce124 | ||
|
|
0b93af5d71 | ||
|
|
f8e3579a92 | ||
|
|
54e434d812 | ||
|
|
13c3974f73 | ||
|
|
aeb2a370e1 | ||
|
|
0aff23a067 | ||
|
|
a2a2da0260 | ||
|
|
4d7a3bca62 | ||
|
|
91d5698fe9 | ||
|
|
7266c6ee6b | ||
|
|
06b539b911 | ||
|
|
486ba4a3fc | ||
|
|
7f987667fe | ||
|
|
8d22a0ffaf | ||
|
|
43d28608f5 | ||
|
|
c84d3f7707 | ||
|
|
2843e15905 | ||
|
|
a2fb388aa6 | ||
|
|
b7b253cf24 | ||
|
|
f05e9dd746 | ||
|
|
b24a18b231 | ||
|
|
e2a63ee361 | ||
|
|
ff9f706670 | ||
|
|
10ab60569b | ||
|
|
d77d1d67aa | ||
|
|
924425522a | ||
|
|
356e0ee07b | ||
|
|
8e83743dd7 | ||
|
|
61658655ff | ||
|
|
40086ffde2 | ||
|
|
1551142e98 | ||
|
|
1c6fb6491a | ||
|
|
cfd4e85e77 | ||
|
|
4d6abfabf7 | ||
|
|
a38933df66 | ||
|
|
6a91d18003 | ||
|
|
e481a02126 | ||
|
|
825dff27fc | ||
|
|
de3a87fff5 | ||
|
|
85d1addd04 | ||
|
|
4993873ae2 | ||
|
|
02b805ce42 | ||
|
|
1a6ba16a59 | ||
|
|
654a05df8a | ||
|
|
ffe1876337 | ||
|
|
98963955b1 | ||
|
|
d83efce002 | ||
|
|
efb1b8c1ad | ||
|
|
3f36db8b3a | ||
|
|
a2f4e70a48 | ||
|
|
01e72eff40 | ||
|
|
6288577daa | ||
|
|
5d363496bd | ||
|
|
713be403eb | ||
|
|
2fd948b81d | ||
|
|
97d11cc602 | ||
|
|
79f41064e4 | ||
|
|
9aacd9d3d8 | ||
|
|
2b4a1cf54b |
10
CONTRIBUTING.md
Normal file
10
CONTRIBUTING.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Folder-Description:
|
||||
===================
|
||||
* `briar-*`: Specifically for the Briar app (Phone/Desktop/Headless)
|
||||
* `bramble-*`: The protocol stack - not necessarily Briar-dependent
|
||||
|
||||
---
|
||||
|
||||
* `*-api`: public stuff that can be referenced from other packages and modules - mostly interfaces + a few utility classes
|
||||
* `*-core`: implementations of api that are portable across Android/Desktop/Headless
|
||||
* `*-java`: implementations of api that are specific to Desktop & Headless
|
||||
@@ -15,8 +15,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
versionCode 10408
|
||||
versionName "1.4.8"
|
||||
versionCode 10410
|
||||
versionName "1.4.10"
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface ConnectionManager {
|
||||
@@ -45,6 +46,14 @@ public interface ConnectionManager {
|
||||
void manageOutgoingConnection(ContactId c, TransportId t,
|
||||
TransportConnectionWriter w);
|
||||
|
||||
/**
|
||||
* Manages an outgoing connection to a contact via a mailbox. The IDs of
|
||||
* any messages sent or acked are added to the given
|
||||
* {@link OutgoingSessionRecord}.
|
||||
*/
|
||||
void manageOutgoingConnection(ContactId c, TransportId t,
|
||||
TransportConnectionWriter w, OutgoingSessionRecord sessionRecord);
|
||||
|
||||
/**
|
||||
* Manages an outgoing connection to a contact over a duplex transport.
|
||||
*/
|
||||
|
||||
@@ -126,16 +126,11 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
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.
|
||||
* Returns true if there are any acks to send to the given contact.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*
|
||||
* @param eager True if messages that are not yet due for retransmission
|
||||
* should be included
|
||||
*/
|
||||
boolean containsAnythingToSend(Transaction txn, ContactId c,
|
||||
long maxLatency, boolean eager) throws DbException;
|
||||
boolean containsAcksToSend(Transaction txn, ContactId c) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if the database contains the given contact for the given
|
||||
@@ -161,6 +156,18 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
*/
|
||||
boolean containsIdentity(Transaction txn, AuthorId a) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if there are any 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 containsMessagesToSend(Transaction txn, ContactId c,
|
||||
long maxLatency, boolean eager) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if the database contains the given pending contact.
|
||||
* <p/>
|
||||
@@ -534,15 +541,18 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
*/
|
||||
long getNextCleanupDeadline(Transaction 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
|
||||
* be zero if a message is due to be sent immediately, or Long.MAX_VALUE if
|
||||
* no messages are scheduled to be sent.
|
||||
* message is due to be sent to the given contact over a transport with
|
||||
* the given latency.
|
||||
* <p>
|
||||
* The returned value may be zero if a message is due to be sent
|
||||
* immediately, or Long.MAX_VALUE if no messages are scheduled to be sent.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
long getNextSendTime(Transaction txn, ContactId c) throws DbException;
|
||||
long getNextSendTime(Transaction txn, ContactId c, long maxLatency)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the pending contact with the given ID.
|
||||
|
||||
@@ -37,8 +37,14 @@ public interface LifecycleManager {
|
||||
*/
|
||||
enum LifecycleState {
|
||||
|
||||
STARTING, MIGRATING_DATABASE, COMPACTING_DATABASE, STARTING_SERVICES,
|
||||
RUNNING, STOPPING;
|
||||
CREATED,
|
||||
STARTING,
|
||||
MIGRATING_DATABASE,
|
||||
COMPACTING_DATABASE,
|
||||
STARTING_SERVICES,
|
||||
RUNNING,
|
||||
STOPPING,
|
||||
STOPPED;
|
||||
|
||||
public boolean isAfter(LifecycleState state) {
|
||||
return ordinal() > state.ordinal();
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.concurrent.TimeUnit.DAYS;
|
||||
import static java.util.concurrent.TimeUnit.HOURS;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_FRAME_LENGTH;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_PAYLOAD_LENGTH;
|
||||
@@ -65,4 +66,8 @@ public interface MailboxConstants {
|
||||
*/
|
||||
long PROBLEM_MS_SINCE_LAST_SUCCESS = HOURS.toMillis(1);
|
||||
|
||||
/**
|
||||
* The maximum latency of the mailbox transport in milliseconds.
|
||||
*/
|
||||
long MAX_LATENCY = DAYS.toMillis(14);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.UniqueId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
@@ -32,7 +33,7 @@ public abstract class MailboxId extends UniqueId {
|
||||
}
|
||||
try {
|
||||
return fromHexString(token);
|
||||
} catch (IllegalArgumentException e) {
|
||||
} catch (FormatException e) {
|
||||
throw new InvalidMailboxIdException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import javax.annotation.concurrent.Immutable;
|
||||
@NotNullByDefault
|
||||
public class MailboxProperties {
|
||||
|
||||
private final String baseUrl;
|
||||
private final String onion;
|
||||
private final MailboxAuthToken authToken;
|
||||
private final boolean owner;
|
||||
private final List<MailboxVersion> serverSupports;
|
||||
@@ -23,9 +23,9 @@ public class MailboxProperties {
|
||||
/**
|
||||
* Constructor for properties used by the mailbox's owner.
|
||||
*/
|
||||
public MailboxProperties(String baseUrl, MailboxAuthToken authToken,
|
||||
public MailboxProperties(String onion, MailboxAuthToken authToken,
|
||||
List<MailboxVersion> serverSupports) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.onion = onion;
|
||||
this.authToken = authToken;
|
||||
this.owner = true;
|
||||
this.serverSupports = serverSupports;
|
||||
@@ -36,10 +36,10 @@ public class MailboxProperties {
|
||||
/**
|
||||
* Constructor for properties used by a contact of the mailbox's owner.
|
||||
*/
|
||||
public MailboxProperties(String baseUrl, MailboxAuthToken authToken,
|
||||
public MailboxProperties(String onion, MailboxAuthToken authToken,
|
||||
List<MailboxVersion> serverSupports, MailboxFolderId inboxId,
|
||||
MailboxFolderId outboxId) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.onion = onion;
|
||||
this.authToken = authToken;
|
||||
this.owner = false;
|
||||
this.serverSupports = serverSupports;
|
||||
@@ -47,13 +47,11 @@ public class MailboxProperties {
|
||||
this.outboxId = outboxId;
|
||||
}
|
||||
|
||||
public String getBaseUrl() {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the onion address of the mailbox, excluding the .onion suffix.
|
||||
*/
|
||||
public String getOnion() {
|
||||
return baseUrl.replaceFirst("^http://", "")
|
||||
.replaceFirst("\\.onion$", "");
|
||||
return onion;
|
||||
}
|
||||
|
||||
public MailboxAuthToken getAuthToken() {
|
||||
|
||||
@@ -35,6 +35,9 @@ public interface MailboxSettingsManager {
|
||||
void recordSuccessfulConnection(Transaction txn, long now)
|
||||
throws DbException;
|
||||
|
||||
void recordSuccessfulConnection(Transaction txn, long now,
|
||||
List<MailboxVersion> versions) throws DbException;
|
||||
|
||||
void recordFailedConnectionAttempt(Transaction txn, long now)
|
||||
throws DbException;
|
||||
|
||||
|
||||
@@ -29,19 +29,35 @@ public interface MailboxUpdateManager {
|
||||
|
||||
/**
|
||||
* The number of properties required for an update message with a mailbox.
|
||||
* <p>
|
||||
* The required properties are {@link #PROP_KEY_ONION},
|
||||
* {@link #PROP_KEY_AUTHTOKEN}, {@link #PROP_KEY_INBOXID} and
|
||||
* {@link #PROP_KEY_OUTBOXID}.
|
||||
*/
|
||||
int PROP_COUNT = 4;
|
||||
|
||||
/**
|
||||
* The required properties of an update message with a mailbox.
|
||||
* The onion address of the mailbox, excluding the .onion suffix.
|
||||
*/
|
||||
String PROP_KEY_ONION = "onion";
|
||||
|
||||
/**
|
||||
* A bearer token for accessing the mailbox (64 hex digits).
|
||||
*/
|
||||
String PROP_KEY_AUTHTOKEN = "authToken";
|
||||
|
||||
/**
|
||||
* A folder ID for downloading messages (64 hex digits).
|
||||
*/
|
||||
String PROP_KEY_INBOXID = "inboxId";
|
||||
|
||||
/**
|
||||
* A folder ID for uploading messages (64 hex digits).
|
||||
*/
|
||||
String PROP_KEY_OUTBOXID = "outboxId";
|
||||
|
||||
/**
|
||||
* Length of the Onion property.
|
||||
* Length of the {@link #PROP_KEY_ONION} property.
|
||||
*/
|
||||
int PROP_ONION_LENGTH = 56;
|
||||
|
||||
@@ -63,9 +79,26 @@ public interface MailboxUpdateManager {
|
||||
*/
|
||||
String GROUP_KEY_SENT_CLIENT_SUPPORTS = "sentClientSupports";
|
||||
|
||||
/**
|
||||
* Returns the latest {@link MailboxUpdate} sent to the given contact.
|
||||
* <p>
|
||||
* If we have our own mailbox then the update will be a
|
||||
* {@link MailboxUpdateWithMailbox} containing the
|
||||
* {@link MailboxProperties} the contact should use for communicating with
|
||||
* our mailbox.
|
||||
*/
|
||||
MailboxUpdate getLocalUpdate(Transaction txn, ContactId c)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the latest {@link MailboxUpdate} received from the given
|
||||
* contact, or null if no update has been received.
|
||||
* <p>
|
||||
* If the contact has a mailbox then the update will be a
|
||||
* {@link MailboxUpdateWithMailbox} containing the
|
||||
* {@link MailboxProperties} we should use for communicating with the
|
||||
* contact's mailbox.
|
||||
*/
|
||||
@Nullable
|
||||
MailboxUpdate getRemoteUpdate(Transaction txn, ContactId c)
|
||||
throws DbException;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package org.briarproject.bramble.api.sync;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* An interface for holding the IDs of messages sent and acked during an
|
||||
* outgoing {@link SyncSession} so they can be recorded in the DB as sent
|
||||
* or acked at some later time.
|
||||
*/
|
||||
public interface DeferredSendHandler {
|
||||
|
||||
void onAckSent(Collection<MessageId> acked);
|
||||
|
||||
void onMessageSent(MessageId sent);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.briarproject.bramble.api.sync;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* A container for holding the IDs of messages sent and acked during an
|
||||
* outgoing {@link SyncSession}, so they can be recorded in the DB as sent
|
||||
* or acked at some later time.
|
||||
*/
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
public class OutgoingSessionRecord {
|
||||
|
||||
private final Collection<MessageId> ackedIds = new CopyOnWriteArrayList<>();
|
||||
private final Collection<MessageId> sentIds = new CopyOnWriteArrayList<>();
|
||||
|
||||
public void onAckSent(Collection<MessageId> acked) {
|
||||
ackedIds.addAll(acked);
|
||||
}
|
||||
|
||||
public void onMessageSent(MessageId sent) {
|
||||
sentIds.add(sent);
|
||||
}
|
||||
|
||||
public Collection<MessageId> getAckedIds() {
|
||||
return ackedIds;
|
||||
}
|
||||
|
||||
public Collection<MessageId> getSentIds() {
|
||||
return sentIds;
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,30 @@ import javax.annotation.Nullable;
|
||||
@NotNullByDefault
|
||||
public interface SyncSessionFactory {
|
||||
|
||||
/**
|
||||
* Creates a session for receiving data from a contact.
|
||||
*/
|
||||
SyncSession createIncomingSession(ContactId c, InputStream in,
|
||||
PriorityHandler handler);
|
||||
|
||||
/**
|
||||
* Creates a session for sending data to a contact over a simplex transport.
|
||||
*
|
||||
* @param eager True if messages should be sent eagerly, ie regardless of
|
||||
* whether they're due for retransmission.
|
||||
*/
|
||||
SyncSession createSimplexOutgoingSession(ContactId c, TransportId t,
|
||||
long maxLatency, boolean eager, StreamWriter streamWriter);
|
||||
|
||||
/**
|
||||
* Creates a session for sending data to a contact via a mailbox. The IDs
|
||||
* of any messages sent or acked will be added to the given
|
||||
* {@link OutgoingSessionRecord}.
|
||||
*/
|
||||
SyncSession createSimplexOutgoingSession(ContactId c, TransportId t,
|
||||
long maxLatency, StreamWriter streamWriter,
|
||||
OutgoingSessionRecord sessionRecord);
|
||||
|
||||
SyncSession createDuplexOutgoingSession(ContactId c, TransportId t,
|
||||
long maxLatency, int maxIdleTime, StreamWriter streamWriter,
|
||||
@Nullable Priority priority);
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.briarproject.bramble.api.sync.event;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Group.Visibility;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@@ -15,12 +16,19 @@ import javax.annotation.concurrent.Immutable;
|
||||
@NotNullByDefault
|
||||
public class GroupVisibilityUpdatedEvent extends Event {
|
||||
|
||||
private final Visibility visibility;
|
||||
private final Collection<ContactId> affected;
|
||||
|
||||
public GroupVisibilityUpdatedEvent(Collection<ContactId> affected) {
|
||||
public GroupVisibilityUpdatedEvent(Visibility visibility,
|
||||
Collection<ContactId> affected) {
|
||||
this.visibility = visibility;
|
||||
this.affected = affected;
|
||||
}
|
||||
|
||||
public Visibility getVisibility() {
|
||||
return visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contacts affected by the update.
|
||||
*/
|
||||
|
||||
@@ -40,7 +40,7 @@ public class IoUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private static void delete(File f) {
|
||||
public static void delete(File f) {
|
||||
if (!f.delete() && LOG.isLoggable(WARNING))
|
||||
LOG.warning("Could not delete " + f.getAbsolutePath());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.util;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
@@ -95,10 +96,10 @@ public class StringUtils {
|
||||
/**
|
||||
* Converts the given hex string to a byte array.
|
||||
*/
|
||||
public static byte[] fromHexString(String hex) {
|
||||
public static byte[] fromHexString(String hex) throws FormatException {
|
||||
int len = hex.length();
|
||||
if (len % 2 != 0)
|
||||
throw new IllegalArgumentException("Not a hex string");
|
||||
throw new FormatException();
|
||||
byte[] bytes = new byte[len / 2];
|
||||
for (int i = 0, j = 0; i < len; i += 2, j++) {
|
||||
int high = hexDigitToInt(hex.charAt(i));
|
||||
@@ -108,11 +109,11 @@ public class StringUtils {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static int hexDigitToInt(char c) {
|
||||
private static int hexDigitToInt(char c) throws FormatException {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
throw new IllegalArgumentException("Not a hex digit: " + c);
|
||||
throw new FormatException();
|
||||
}
|
||||
|
||||
public static String trim(String s) {
|
||||
@@ -130,13 +131,13 @@ public class StringUtils {
|
||||
return MAC.matcher(mac).matches();
|
||||
}
|
||||
|
||||
public static byte[] macToBytes(String mac) {
|
||||
if (!MAC.matcher(mac).matches()) throw new IllegalArgumentException();
|
||||
public static byte[] macToBytes(String mac) throws FormatException {
|
||||
if (!MAC.matcher(mac).matches()) throw new FormatException();
|
||||
return fromHexString(mac.replaceAll(":", ""));
|
||||
}
|
||||
|
||||
public static String macToString(byte[] mac) {
|
||||
if (mac.length != 6) throw new IllegalArgumentException();
|
||||
public static String macToString(byte[] mac) throws FormatException {
|
||||
if (mac.length != 6) throw new FormatException();
|
||||
StringBuilder s = new StringBuilder();
|
||||
for (byte b : mac) {
|
||||
if (s.length() > 0) s.append(':');
|
||||
|
||||
@@ -230,14 +230,14 @@ public class TestUtils {
|
||||
|
||||
public static MailboxProperties getMailboxProperties(boolean owner,
|
||||
List<MailboxVersion> serverSupports) {
|
||||
String baseUrl = "http://" + getRandomString(56) + ".onion"; // TODO
|
||||
String onion = getRandomString(56);
|
||||
MailboxAuthToken authToken = new MailboxAuthToken(getRandomId());
|
||||
if (owner) {
|
||||
return new MailboxProperties(baseUrl, authToken, serverSupports);
|
||||
return new MailboxProperties(onion, authToken, serverSupports);
|
||||
}
|
||||
MailboxFolderId inboxId = new MailboxFolderId(getRandomId());
|
||||
MailboxFolderId outboxId = new MailboxFolderId(getRandomId());
|
||||
return new MailboxProperties(baseUrl, authToken, serverSupports,
|
||||
return new MailboxProperties(onion, authToken, serverSupports,
|
||||
inboxId, outboxId);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ dependencies {
|
||||
implementation 'org.bitlet:weupnp:0.1.4'
|
||||
implementation 'net.i2p.crypto:eddsa:0.2.0'
|
||||
implementation 'org.whispersystems:curve25519-java:0.5.0'
|
||||
implementation 'org.briarproject:jtorctl:0.4'
|
||||
implementation 'org.briarproject:jtorctl:0.5'
|
||||
|
||||
//noinspection GradleDependency
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.account;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.account.AccountManager;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.DecryptionException;
|
||||
@@ -209,7 +210,13 @@ class AccountManagerImpl implements AccountManager {
|
||||
LOG.warning("Failed to load encrypted database key");
|
||||
throw new DecryptionException(INVALID_CIPHERTEXT);
|
||||
}
|
||||
byte[] ciphertext = fromHexString(hex);
|
||||
byte[] ciphertext;
|
||||
try {
|
||||
ciphertext = fromHexString(hex);
|
||||
} catch (FormatException e) {
|
||||
LOG.warning("Encrypted database key has invalid format");
|
||||
throw new DecryptionException(INVALID_CIPHERTEXT);
|
||||
}
|
||||
KeyStrengthener keyStrengthener = databaseConfig.getKeyStrengthener();
|
||||
byte[] plaintext = crypto.decryptWithPassword(ciphertext, password,
|
||||
keyStrengthener);
|
||||
|
||||
@@ -456,8 +456,7 @@ class ClientHelperImpl implements ClientHelper {
|
||||
checkLength(inboxId, UniqueId.LENGTH);
|
||||
byte[] outboxId = properties.getRaw(PROP_KEY_OUTBOXID);
|
||||
checkLength(outboxId, UniqueId.LENGTH);
|
||||
String baseUrl = "http://" + onion + ".onion"; // TODO
|
||||
MailboxProperties props = new MailboxProperties(baseUrl,
|
||||
MailboxProperties props = new MailboxProperties(onion,
|
||||
new MailboxAuthToken(authToken), serverSupportsList,
|
||||
new MailboxFolderId(inboxId), new MailboxFolderId(outboxId));
|
||||
return new MailboxUpdateWithMailbox(clientSupportsList, props);
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.bramble.api.properties.TransportPropertyManager;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
import org.briarproject.bramble.api.sync.SyncSessionFactory;
|
||||
import org.briarproject.bramble.api.transport.KeyManager;
|
||||
import org.briarproject.bramble.api.transport.StreamReaderFactory;
|
||||
@@ -100,7 +101,16 @@ class ConnectionManagerImpl implements ConnectionManager {
|
||||
TransportConnectionWriter w) {
|
||||
ioExecutor.execute(new OutgoingSimplexSyncConnection(keyManager,
|
||||
connectionRegistry, streamReaderFactory, streamWriterFactory,
|
||||
syncSessionFactory, transportPropertyManager, c, t, w));
|
||||
syncSessionFactory, transportPropertyManager, c, t, w, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void manageOutgoingConnection(ContactId c, TransportId t,
|
||||
TransportConnectionWriter w, OutgoingSessionRecord sessionRecord) {
|
||||
ioExecutor.execute(new OutgoingSimplexSyncConnection(keyManager,
|
||||
connectionRegistry, streamReaderFactory, streamWriterFactory,
|
||||
syncSessionFactory, transportPropertyManager, c, t, w,
|
||||
sessionRecord));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.properties.TransportPropertyManager;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
import org.briarproject.bramble.api.sync.SyncSession;
|
||||
import org.briarproject.bramble.api.sync.SyncSessionFactory;
|
||||
import org.briarproject.bramble.api.transport.KeyManager;
|
||||
@@ -16,6 +17,8 @@ import org.briarproject.bramble.api.transport.StreamWriterFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
@@ -26,6 +29,8 @@ class OutgoingSimplexSyncConnection extends SyncConnection implements Runnable {
|
||||
private final ContactId contactId;
|
||||
private final TransportId transportId;
|
||||
private final TransportConnectionWriter writer;
|
||||
@Nullable
|
||||
private final OutgoingSessionRecord sessionRecord;
|
||||
|
||||
OutgoingSimplexSyncConnection(KeyManager keyManager,
|
||||
ConnectionRegistry connectionRegistry,
|
||||
@@ -34,13 +39,15 @@ class OutgoingSimplexSyncConnection extends SyncConnection implements Runnable {
|
||||
SyncSessionFactory syncSessionFactory,
|
||||
TransportPropertyManager transportPropertyManager,
|
||||
ContactId contactId, TransportId transportId,
|
||||
TransportConnectionWriter writer) {
|
||||
TransportConnectionWriter writer,
|
||||
@Nullable OutgoingSessionRecord sessionRecord) {
|
||||
super(keyManager, connectionRegistry, streamReaderFactory,
|
||||
streamWriterFactory, syncSessionFactory,
|
||||
transportPropertyManager);
|
||||
this.contactId = contactId;
|
||||
this.transportId = transportId;
|
||||
this.writer = writer;
|
||||
this.sessionRecord = sessionRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,10 +78,16 @@ 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(), w.isLossyAndCheap(),
|
||||
streamWriter);
|
||||
if (sessionRecord == null) {
|
||||
// Use eager retransmission if the transport is lossy and cheap
|
||||
return syncSessionFactory.createSimplexOutgoingSession(c,
|
||||
ctx.getTransportId(), w.getMaxLatency(),
|
||||
w.isLossyAndCheap(), streamWriter);
|
||||
} else {
|
||||
return syncSessionFactory.createSimplexOutgoingSession(c,
|
||||
ctx.getTransportId(), w.getMaxLatency(), streamWriter,
|
||||
sessionRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -163,16 +163,11 @@ interface Database<T> {
|
||||
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.
|
||||
* Returns true if there are any acks to send to the given contact.
|
||||
* <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;
|
||||
boolean containsAcksToSend(T txn, ContactId c) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if the database contains the given contact for the given
|
||||
@@ -212,6 +207,18 @@ interface Database<T> {
|
||||
*/
|
||||
boolean containsMessage(T txn, MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if there are any 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 containsMessagesToSend(T txn, ContactId c, long maxLatency,
|
||||
boolean eager) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if the database contains the given pending contact.
|
||||
* <p/>
|
||||
@@ -580,13 +587,16 @@ interface Database<T> {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* be zero if a message is due to be sent immediately, or Long.MAX_VALUE
|
||||
* if no messages are scheduled to be sent.
|
||||
* message is due to be sent to the given contact over a transport with
|
||||
* the given latency.
|
||||
* <p>
|
||||
* The returned value may be zero if a message is due to be sent
|
||||
* immediately, or Long.MAX_VALUE if no messages are scheduled to be sent.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
long getNextSendTime(T txn, ContactId c) throws DbException;
|
||||
long getNextSendTime(T txn, ContactId c, long maxLatency)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the pending contact with the given ID.
|
||||
|
||||
@@ -342,12 +342,12 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAnythingToSend(Transaction transaction, ContactId c,
|
||||
long maxLatency, boolean eager) throws DbException {
|
||||
public boolean containsAcksToSend(Transaction transaction, ContactId c)
|
||||
throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
return db.containsAnythingToSend(txn, c, maxLatency, eager);
|
||||
return db.containsAcksToSend(txn, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -373,6 +373,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
return db.containsIdentity(txn, a);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsMessagesToSend(Transaction transaction, ContactId c,
|
||||
long maxLatency, boolean eager) throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
return db.containsMessagesToSend(txn, c, maxLatency, eager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsPendingContact(Transaction transaction,
|
||||
PendingContactId p) throws DbException {
|
||||
@@ -805,10 +814,10 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextSendTime(Transaction transaction, ContactId c)
|
||||
throws DbException {
|
||||
public long getNextSendTime(Transaction transaction, ContactId c,
|
||||
long maxLatency) throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
return db.getNextSendTime(txn, c);
|
||||
return db.getNextSendTime(txn, c, maxLatency);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1016,7 +1025,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
db.getGroupVisibility(txn, id).keySet();
|
||||
db.removeGroup(txn, id);
|
||||
transaction.attach(new GroupRemovedEvent(g));
|
||||
transaction.attach(new GroupVisibilityUpdatedEvent(affected));
|
||||
transaction.attach(new GroupVisibilityUpdatedEvent(INVISIBLE,
|
||||
affected));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1141,7 +1151,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
else if (v == INVISIBLE) db.removeGroupVisibility(txn, c, g);
|
||||
else db.setGroupVisibility(txn, c, g, v == SHARED);
|
||||
List<ContactId> affected = singletonList(c);
|
||||
transaction.attach(new GroupVisibilityUpdatedEvent(affected));
|
||||
transaction.attach(new GroupVisibilityUpdatedEvent(v, affected));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1147,8 +1147,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAnythingToSend(Connection txn, ContactId c,
|
||||
long maxLatency, boolean eager) throws DbException {
|
||||
public boolean containsAcksToSend(Connection txn, ContactId c)
|
||||
throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
@@ -1160,34 +1160,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
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();
|
||||
sql = "SELECT NULL FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE"
|
||||
+ " AND (expiry <= ? OR maxLatency IS NULL"
|
||||
+ " OR ? < maxLatency)";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
ps.setLong(3, now);
|
||||
ps.setLong(4, maxLatency);
|
||||
}
|
||||
rs = ps.executeQuery();
|
||||
boolean messagesToSend = rs.next();
|
||||
rs.close();
|
||||
ps.close();
|
||||
return messagesToSend;
|
||||
return acksToSend;
|
||||
} catch (SQLException e) {
|
||||
tryToClose(rs, LOG, WARNING);
|
||||
tryToClose(ps, LOG, WARNING);
|
||||
@@ -1307,6 +1280,46 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsMessagesToSend(Connection txn, ContactId c,
|
||||
long maxLatency, boolean eager) throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
if (eager) {
|
||||
String 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();
|
||||
String sql = "SELECT NULL FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE"
|
||||
+ " AND (expiry <= ? OR maxLatency IS NULL"
|
||||
+ " OR ? < maxLatency)";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
ps.setLong(3, now);
|
||||
ps.setLong(4, maxLatency);
|
||||
}
|
||||
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 containsPendingContact(Connection txn, PendingContactId p)
|
||||
throws DbException {
|
||||
@@ -2477,12 +2490,28 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextSendTime(Connection txn, ContactId c)
|
||||
public long getNextSendTime(Connection txn, ContactId c, long maxLatency)
|
||||
throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT expiry FROM statuses"
|
||||
// Are any messages sendable immediately?
|
||||
String sql = "SELECT NULL FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE"
|
||||
+ " AND (maxLatency IS NULL OR ? < maxLatency)";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
ps.setLong(3, maxLatency);
|
||||
rs = ps.executeQuery();
|
||||
boolean found = rs.next();
|
||||
rs.close();
|
||||
ps.close();
|
||||
if (found) return 0;
|
||||
// When is the earliest expiry time (could be in the past)?
|
||||
sql = "SELECT expiry FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE"
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
@@ -29,10 +29,12 @@ 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.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.CREATED;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPED;
|
||||
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;
|
||||
@@ -60,12 +62,11 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
private final List<Service> services;
|
||||
private final List<OpenDatabaseHook> openDatabaseHooks;
|
||||
private final List<ExecutorService> executors;
|
||||
private final Semaphore startStopSemaphore = new Semaphore(1);
|
||||
private final CountDownLatch dbLatch = new CountDownLatch(1);
|
||||
private final CountDownLatch startupLatch = new CountDownLatch(1);
|
||||
private final CountDownLatch shutdownLatch = new CountDownLatch(1);
|
||||
|
||||
private volatile LifecycleState state = STARTING;
|
||||
private final AtomicReference<LifecycleState> state =
|
||||
new AtomicReference<>(CREATED);
|
||||
|
||||
@Inject
|
||||
LifecycleManagerImpl(DatabaseComponent db, EventBus eventBus,
|
||||
@@ -102,8 +103,8 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
|
||||
@Override
|
||||
public StartResult startServices(SecretKey dbKey) {
|
||||
if (!startStopSemaphore.tryAcquire()) {
|
||||
LOG.info("Already starting or stopping");
|
||||
if (!state.compareAndSet(CREATED, STARTING)) {
|
||||
LOG.warning("Already running");
|
||||
return ALREADY_RUNNING;
|
||||
}
|
||||
long now = clock.currentTimeMillis();
|
||||
@@ -135,7 +136,7 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
});
|
||||
|
||||
LOG.info("Starting services");
|
||||
state = STARTING_SERVICES;
|
||||
state.set(STARTING_SERVICES);
|
||||
dbLatch.countDown();
|
||||
eventBus.broadcast(new LifecycleEvent(STARTING_SERVICES));
|
||||
|
||||
@@ -148,7 +149,7 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
}
|
||||
}
|
||||
|
||||
state = RUNNING;
|
||||
state.set(RUNNING);
|
||||
startupLatch.countDown();
|
||||
eventBus.broadcast(new LifecycleEvent(RUNNING));
|
||||
return SUCCESS;
|
||||
@@ -164,63 +165,58 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
} catch (ServiceException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return SERVICE_ERROR;
|
||||
} finally {
|
||||
startStopSemaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDatabaseMigration() {
|
||||
state = MIGRATING_DATABASE;
|
||||
state.set(MIGRATING_DATABASE);
|
||||
eventBus.broadcast(new LifecycleEvent(MIGRATING_DATABASE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDatabaseCompaction() {
|
||||
state = COMPACTING_DATABASE;
|
||||
state.set(COMPACTING_DATABASE);
|
||||
eventBus.broadcast(new LifecycleEvent(COMPACTING_DATABASE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopServices() {
|
||||
try {
|
||||
startStopSemaphore.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
LOG.warning("Interrupted while waiting to stop services");
|
||||
if (!state.compareAndSet(RUNNING, STOPPING)) {
|
||||
LOG.warning("Not running");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (state == STOPPING) {
|
||||
LOG.info("Already stopped");
|
||||
return;
|
||||
}
|
||||
LOG.info("Stopping services");
|
||||
state = STOPPING;
|
||||
eventBus.broadcast(new LifecycleEvent(STOPPING));
|
||||
for (Service s : services) {
|
||||
LOG.info("Stopping services");
|
||||
eventBus.broadcast(new LifecycleEvent(STOPPING));
|
||||
for (Service s : services) {
|
||||
try {
|
||||
long start = now();
|
||||
s.stopService();
|
||||
if (LOG.isLoggable(FINE)) {
|
||||
logDuration(LOG, "Stopping service "
|
||||
+ s.getClass().getSimpleName(), start);
|
||||
}
|
||||
} catch (ServiceException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
for (ExecutorService e : executors) {
|
||||
if (LOG.isLoggable(FINE)) {
|
||||
LOG.fine("Stopping executor "
|
||||
+ e.getClass().getSimpleName());
|
||||
}
|
||||
e.shutdownNow();
|
||||
}
|
||||
for (ExecutorService e : executors) {
|
||||
if (LOG.isLoggable(FINE)) {
|
||||
LOG.fine("Stopping executor "
|
||||
+ e.getClass().getSimpleName());
|
||||
}
|
||||
e.shutdownNow();
|
||||
}
|
||||
try {
|
||||
long start = now();
|
||||
db.close();
|
||||
logDuration(LOG, "Closing database", start);
|
||||
shutdownLatch.countDown();
|
||||
} catch (DbException | ServiceException e) {
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
} finally {
|
||||
startStopSemaphore.release();
|
||||
}
|
||||
state.set(STOPPED);
|
||||
shutdownLatch.countDown();
|
||||
eventBus.broadcast(new LifecycleEvent(STOPPED));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -240,6 +236,6 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
|
||||
@Override
|
||||
public LifecycleState getLifecycleState() {
|
||||
return state;
|
||||
return state.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,21 @@ interface ConnectivityChecker {
|
||||
* the check succeeds. If a check is already running then the observer is
|
||||
* called when the check succeeds. If a connectivity check has recently
|
||||
* succeeded then the observer is called immediately.
|
||||
* <p>
|
||||
* Observers are removed after being called, or when the checker is
|
||||
* {@link #destroy() destroyed}.
|
||||
*/
|
||||
void checkConnectivity(MailboxProperties properties,
|
||||
ConnectivityObserver o);
|
||||
|
||||
/**
|
||||
* Removes an observer that was added via
|
||||
* {@link #checkConnectivity(MailboxProperties, ConnectivityObserver)}. If
|
||||
* there are no remaining observers and a connectivity check is running
|
||||
* then the check will be cancelled.
|
||||
*/
|
||||
void removeObserver(ConnectivityObserver o);
|
||||
|
||||
interface ConnectivityObserver {
|
||||
void onConnectivityCheckSucceeded();
|
||||
}
|
||||
|
||||
@@ -80,8 +80,7 @@ abstract class ConnectivityCheckerImpl implements ConnectivityChecker {
|
||||
> CONNECTIVITY_CHECK_FRESHNESS_MS) {
|
||||
// The last connectivity check is stale, start a new one
|
||||
connectivityObservers.add(o);
|
||||
ApiCall task =
|
||||
createConnectivityCheckTask(properties);
|
||||
ApiCall task = createConnectivityCheckTask(properties);
|
||||
connectivityCheck = mailboxApiCaller.retryWithBackoff(task);
|
||||
} else {
|
||||
// The last connectivity check is fresh
|
||||
@@ -108,4 +107,16 @@ abstract class ConnectivityCheckerImpl implements ConnectivityChecker {
|
||||
o.onConnectivityCheckSucceeded();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeObserver(ConnectivityObserver o) {
|
||||
synchronized (lock) {
|
||||
if (destroyed) return;
|
||||
connectivityObservers.remove(o);
|
||||
if (connectivityObservers.isEmpty() && connectivityCheck != null) {
|
||||
connectivityCheck.cancel();
|
||||
connectivityCheck = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class ContactMailboxClient implements MailboxClient {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(ContactMailboxClient.class.getName());
|
||||
|
||||
private final MailboxWorkerFactory workerFactory;
|
||||
private final ConnectivityChecker connectivityChecker;
|
||||
private final TorReachabilityMonitor reachabilityMonitor;
|
||||
private final Object lock = new Object();
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private MailboxWorker uploadWorker = null, downloadWorker = null;
|
||||
|
||||
ContactMailboxClient(MailboxWorkerFactory workerFactory,
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor reachabilityMonitor) {
|
||||
this.workerFactory = workerFactory;
|
||||
this.connectivityChecker = connectivityChecker;
|
||||
this.reachabilityMonitor = reachabilityMonitor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
LOG.info("Started");
|
||||
// Nothing to do until contact is assigned
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
LOG.info("Destroyed");
|
||||
MailboxWorker uploadWorker, downloadWorker;
|
||||
synchronized (lock) {
|
||||
uploadWorker = this.uploadWorker;
|
||||
this.uploadWorker = null;
|
||||
downloadWorker = this.downloadWorker;
|
||||
this.downloadWorker = null;
|
||||
}
|
||||
if (uploadWorker != null) uploadWorker.destroy();
|
||||
if (downloadWorker != null) downloadWorker.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assignContactForUpload(ContactId contactId,
|
||||
MailboxProperties properties, MailboxFolderId folderId) {
|
||||
LOG.info("Contact assigned for upload");
|
||||
if (properties.isOwner()) throw new IllegalArgumentException();
|
||||
// For a contact's mailbox we should always be uploading to the outbox
|
||||
// assigned to us by the contact
|
||||
if (!folderId.equals(properties.getOutboxId())) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
MailboxWorker uploadWorker = workerFactory.createUploadWorker(
|
||||
connectivityChecker, properties, folderId, contactId);
|
||||
synchronized (lock) {
|
||||
if (this.uploadWorker != null) throw new IllegalStateException();
|
||||
this.uploadWorker = uploadWorker;
|
||||
}
|
||||
uploadWorker.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deassignContactForUpload(ContactId contactId) {
|
||||
LOG.info("Contact deassigned for upload");
|
||||
MailboxWorker uploadWorker;
|
||||
synchronized (lock) {
|
||||
uploadWorker = this.uploadWorker;
|
||||
this.uploadWorker = null;
|
||||
}
|
||||
if (uploadWorker != null) uploadWorker.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assignContactForDownload(ContactId contactId,
|
||||
MailboxProperties properties, MailboxFolderId folderId) {
|
||||
LOG.info("Contact assigned for download");
|
||||
if (properties.isOwner()) throw new IllegalArgumentException();
|
||||
// For a contact's mailbox we should always be downloading from the
|
||||
// inbox assigned to us by the contact
|
||||
if (!folderId.equals(properties.getInboxId())) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
MailboxWorker downloadWorker =
|
||||
workerFactory.createDownloadWorkerForContactMailbox(
|
||||
connectivityChecker, reachabilityMonitor, properties);
|
||||
synchronized (lock) {
|
||||
if (this.downloadWorker != null) throw new IllegalStateException();
|
||||
this.downloadWorker = downloadWorker;
|
||||
}
|
||||
downloadWorker.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deassignContactForDownload(ContactId contactId) {
|
||||
LOG.info("Contact deassigned for download");
|
||||
MailboxWorker downloadWorker;
|
||||
synchronized (lock) {
|
||||
downloadWorker = this.downloadWorker;
|
||||
this.downloadWorker = null;
|
||||
}
|
||||
if (downloadWorker != null) downloadWorker.destroy();
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
@ThreadSafe
|
||||
@@ -24,16 +22,11 @@ class ContactMailboxConnectivityChecker extends ConnectivityCheckerImpl {
|
||||
@Override
|
||||
ApiCall createConnectivityCheckTask(MailboxProperties properties) {
|
||||
if (properties.isOwner()) throw new IllegalArgumentException();
|
||||
return new SimpleApiCall() {
|
||||
@Override
|
||||
void tryToCallApi() throws IOException, ApiException {
|
||||
if (!mailboxApi.checkStatus(properties)) {
|
||||
throw new ApiException();
|
||||
}
|
||||
// Call the observers and cache the result
|
||||
onConnectivityCheckSucceeded(clock.currentTimeMillis());
|
||||
}
|
||||
};
|
||||
return new SimpleApiCall(() -> {
|
||||
if (!mailboxApi.checkStatus(properties)) throw new ApiException();
|
||||
// Call the observers and cache the result
|
||||
onConnectivityCheckSucceeded(clock.currentTimeMillis());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class ContactMailboxDownloadWorker extends MailboxDownloadWorker {
|
||||
|
||||
ContactMailboxDownloadWorker(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor torReachabilityMonitor,
|
||||
MailboxApiCaller mailboxApiCaller,
|
||||
MailboxApi mailboxApi,
|
||||
MailboxFileManager mailboxFileManager,
|
||||
MailboxProperties mailboxProperties) {
|
||||
super(connectivityChecker, torReachabilityMonitor, mailboxApiCaller,
|
||||
mailboxApi, mailboxFileManager, mailboxProperties);
|
||||
if (mailboxProperties.isOwner()) throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ApiCall createApiCallForDownloadCycle() {
|
||||
return new SimpleApiCall(this::apiCallListInbox);
|
||||
}
|
||||
|
||||
private void apiCallListInbox() throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
}
|
||||
LOG.info("Listing inbox");
|
||||
MailboxFolderId folderId =
|
||||
requireNonNull(mailboxProperties.getInboxId());
|
||||
List<MailboxFile> files;
|
||||
try {
|
||||
files = mailboxApi.getFiles(mailboxProperties, folderId);
|
||||
} catch (TolerableFailureException e) {
|
||||
LOG.warning("Inbox folder does not exist");
|
||||
files = emptyList();
|
||||
}
|
||||
if (files.isEmpty()) {
|
||||
onDownloadCycleFinished();
|
||||
} else {
|
||||
Queue<FolderFile> queue = new LinkedList<>();
|
||||
for (MailboxFile file : files) {
|
||||
queue.add(new FolderFile(folderId, file.name));
|
||||
}
|
||||
downloadNextFile(queue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,9 +91,13 @@ interface MailboxApi {
|
||||
* Used by owner and contacts to list their files to retrieve.
|
||||
* <p>
|
||||
* Returns 200 OK with the list of files in JSON.
|
||||
*
|
||||
* @throws TolerableFailureException if response code is 404 (folder does
|
||||
* not exist or client is not authorised to download from it)
|
||||
*/
|
||||
List<MailboxFile> getFiles(MailboxProperties properties,
|
||||
MailboxFolderId folderId) throws IOException, ApiException;
|
||||
MailboxFolderId folderId)
|
||||
throws IOException, ApiException, TolerableFailureException;
|
||||
|
||||
/**
|
||||
* Used by owner and contacts to retrieve a file.
|
||||
@@ -102,17 +106,22 @@ interface MailboxApi {
|
||||
* in the response body.
|
||||
*
|
||||
* @param file the empty file the response bytes will be written into.
|
||||
* @throws TolerableFailureException if response code is 404 (folder does
|
||||
* not exist, client is not authorised to download from folder, or file
|
||||
* does not exist)
|
||||
*/
|
||||
void getFile(MailboxProperties properties, MailboxFolderId folderId,
|
||||
MailboxFileId fileId, File file) throws IOException, ApiException;
|
||||
MailboxFileId fileId, File file)
|
||||
throws IOException, ApiException, TolerableFailureException;
|
||||
|
||||
/**
|
||||
* Used by owner and contacts to delete files.
|
||||
* <p>
|
||||
* Returns 200 OK (no exception) if deletion was successful.
|
||||
*
|
||||
* @throws TolerableFailureException on 404 response,
|
||||
* because file was most likely deleted already.
|
||||
* @throws TolerableFailureException if response code is 404 (folder does
|
||||
* not exist, client is not authorised to download from folder, or file
|
||||
* does not exist)
|
||||
*/
|
||||
void deleteFile(MailboxProperties properties, MailboxFolderId folderId,
|
||||
MailboxFileId fileId)
|
||||
|
||||
@@ -24,6 +24,8 @@ interface MailboxApiCaller {
|
||||
* Asynchronously calls the given API call on the {@link IoExecutor},
|
||||
* automatically retrying at increasing intervals until the API call
|
||||
* returns false or retries are cancelled.
|
||||
* <p>
|
||||
* This method is safe to call while holding a lock.
|
||||
*
|
||||
* @return A {@link Cancellable} that can be used to cancel any future
|
||||
* retries.
|
||||
|
||||
@@ -42,18 +42,22 @@ import static org.briarproject.bramble.util.IoUtils.copyAndClose;
|
||||
@NotNullByDefault
|
||||
class MailboxApiImpl implements MailboxApi {
|
||||
|
||||
private final WeakSingletonProvider<OkHttpClient> httpClientProvider;
|
||||
private final JsonMapper mapper = JsonMapper.builder()
|
||||
.enable(BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES)
|
||||
.build();
|
||||
private static final MediaType JSON =
|
||||
requireNonNull(MediaType.parse("application/json; charset=utf-8"));
|
||||
private static final MediaType FILE =
|
||||
requireNonNull(MediaType.parse("application/octet-stream"));
|
||||
|
||||
private final WeakSingletonProvider<OkHttpClient> httpClientProvider;
|
||||
private final JsonMapper mapper = JsonMapper.builder()
|
||||
.enable(BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES)
|
||||
.build();
|
||||
private final UrlConverter urlConverter;
|
||||
|
||||
@Inject
|
||||
MailboxApiImpl(WeakSingletonProvider<OkHttpClient> httpClientProvider) {
|
||||
MailboxApiImpl(WeakSingletonProvider<OkHttpClient> httpClientProvider,
|
||||
UrlConverter urlConverter) {
|
||||
this.httpClientProvider = httpClientProvider;
|
||||
this.urlConverter = urlConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -78,7 +82,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
throws IOException, ApiException {
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
Request request = getRequestBuilder(properties.getAuthToken())
|
||||
.url(properties.getBaseUrl() + "/setup")
|
||||
.url(getBaseUrl(properties) + "/setup")
|
||||
.put(EMPTY_REQUEST)
|
||||
.build();
|
||||
OkHttpClient client = httpClientProvider.get();
|
||||
@@ -93,7 +97,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
if (tokenNode == null) {
|
||||
throw new ApiException();
|
||||
}
|
||||
return new MailboxProperties(properties.getBaseUrl(),
|
||||
return new MailboxProperties(properties.getOnion(),
|
||||
MailboxAuthToken.fromString(tokenNode.textValue()),
|
||||
parseServerSupports(node));
|
||||
} catch (JacksonException | InvalidMailboxIdException e) {
|
||||
@@ -137,7 +141,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
throws IOException, ApiException {
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
Request request = getRequestBuilder(properties.getAuthToken())
|
||||
.url(properties.getBaseUrl() + "/")
|
||||
.url(getBaseUrl(properties) + "/")
|
||||
.delete()
|
||||
.build();
|
||||
OkHttpClient client = httpClientProvider.get();
|
||||
@@ -162,7 +166,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
public void deleteContact(MailboxProperties properties, ContactId contactId)
|
||||
throws IOException, ApiException, TolerableFailureException {
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
String url = properties.getBaseUrl() + "/contacts/" +
|
||||
String url = getBaseUrl(properties) + "/contacts/" +
|
||||
contactId.getInt();
|
||||
Request request = getRequestBuilder(properties.getAuthToken())
|
||||
.delete()
|
||||
@@ -212,9 +216,11 @@ class MailboxApiImpl implements MailboxApi {
|
||||
|
||||
@Override
|
||||
public List<MailboxFile> getFiles(MailboxProperties properties,
|
||||
MailboxFolderId folderId) throws IOException, ApiException {
|
||||
MailboxFolderId folderId)
|
||||
throws IOException, ApiException, TolerableFailureException {
|
||||
String path = "/files/" + folderId;
|
||||
Response response = sendGetRequest(properties, path);
|
||||
if (response.code() == 404) throw new TolerableFailureException();
|
||||
if (response.code() != 200) throw new ApiException();
|
||||
|
||||
ResponseBody body = response.body();
|
||||
@@ -248,9 +254,11 @@ class MailboxApiImpl implements MailboxApi {
|
||||
|
||||
@Override
|
||||
public void getFile(MailboxProperties properties, MailboxFolderId folderId,
|
||||
MailboxFileId fileId, File file) throws IOException, ApiException {
|
||||
MailboxFileId fileId, File file)
|
||||
throws IOException, ApiException, TolerableFailureException {
|
||||
String path = "/files/" + folderId + "/" + fileId;
|
||||
Response response = sendGetRequest(properties, path);
|
||||
if (response.code() == 404) throw new TolerableFailureException();
|
||||
if (response.code() != 200) throw new ApiException();
|
||||
|
||||
ResponseBody body = response.body();
|
||||
@@ -266,7 +274,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
String path = "/files/" + folderId + "/" + fileId;
|
||||
Request request = getRequestBuilder(properties.getAuthToken())
|
||||
.delete()
|
||||
.url(properties.getBaseUrl() + path)
|
||||
.url(getBaseUrl(properties) + path)
|
||||
.build();
|
||||
OkHttpClient client = httpClientProvider.get();
|
||||
Response response = client.newCall(request).execute();
|
||||
@@ -308,7 +316,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
private Response sendGetRequest(MailboxProperties properties, String path)
|
||||
throws IOException {
|
||||
Request request = getRequestBuilder(properties.getAuthToken())
|
||||
.url(properties.getBaseUrl() + path)
|
||||
.url(getBaseUrl(properties) + path)
|
||||
.build();
|
||||
OkHttpClient client = httpClientProvider.get();
|
||||
return client.newCall(request).execute();
|
||||
@@ -317,7 +325,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
private Response sendPostRequest(MailboxProperties properties, String path,
|
||||
RequestBody body) throws IOException {
|
||||
Request request = getRequestBuilder(properties.getAuthToken())
|
||||
.url(properties.getBaseUrl() + path)
|
||||
.url(getBaseUrl(properties) + path)
|
||||
.post(body)
|
||||
.build();
|
||||
OkHttpClient client = httpClientProvider.get();
|
||||
@@ -339,4 +347,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
return (ArrayNode) arrayNode;
|
||||
}
|
||||
|
||||
private String getBaseUrl(MailboxProperties properties) {
|
||||
return urlConverter.convertOnionToBaseUrl(properties.getOnion());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
interface MailboxClient {
|
||||
|
||||
/**
|
||||
* Asynchronously starts the client.
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Destroys the client and its workers, cancelling any pending tasks or
|
||||
* retries.
|
||||
*/
|
||||
void destroy();
|
||||
|
||||
/**
|
||||
* Assigns a contact to the client for upload.
|
||||
*
|
||||
* @param properties Properties for communicating with the mailbox
|
||||
* managed by this client.
|
||||
* @param folderId The ID of the folder to which files will be uploaded.
|
||||
*/
|
||||
void assignContactForUpload(ContactId c, MailboxProperties properties,
|
||||
MailboxFolderId folderId);
|
||||
|
||||
/**
|
||||
* Deassigns a contact from the client for upload.
|
||||
*/
|
||||
void deassignContactForUpload(ContactId c);
|
||||
|
||||
/**
|
||||
* Assigns a contact to the client for download.
|
||||
*
|
||||
* @param properties Properties for communicating with the mailbox
|
||||
* managed by this client.
|
||||
* @param folderId The ID of the folder from which files will be
|
||||
* downloaded.
|
||||
*/
|
||||
void assignContactForDownload(ContactId c, MailboxProperties properties,
|
||||
MailboxFolderId folderId);
|
||||
|
||||
/**
|
||||
* Deassigns a contact from the client for download.
|
||||
*/
|
||||
void deassignContactForDownload(ContactId c);
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFileId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
import org.briarproject.bramble.mailbox.TorReachabilityMonitor.TorReachabilityObserver;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Queue;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
abstract class MailboxDownloadWorker implements MailboxWorker,
|
||||
ConnectivityObserver, TorReachabilityObserver {
|
||||
|
||||
/**
|
||||
* When the worker is started it waits for a connectivity check, then
|
||||
* starts its first download cycle: checking for files to download,
|
||||
* downloading and deleting the files, and checking again until all files
|
||||
* have been downloaded and deleted.
|
||||
* <p>
|
||||
* The worker then waits for our Tor hidden service to be reachable before
|
||||
* starting its second download cycle. This ensures that if a contact
|
||||
* tried and failed to connect to our hidden service before it was
|
||||
* reachable, and therefore uploaded a file to the mailbox instead, we'll
|
||||
* find the file in the second download cycle.
|
||||
*/
|
||||
protected enum State {
|
||||
CREATED,
|
||||
CONNECTIVITY_CHECK,
|
||||
DOWNLOAD_CYCLE_1,
|
||||
WAITING_FOR_TOR,
|
||||
DOWNLOAD_CYCLE_2,
|
||||
FINISHED,
|
||||
DESTROYED
|
||||
}
|
||||
|
||||
protected static final Logger LOG =
|
||||
getLogger(MailboxDownloadWorker.class.getName());
|
||||
|
||||
private final ConnectivityChecker connectivityChecker;
|
||||
private final TorReachabilityMonitor torReachabilityMonitor;
|
||||
protected final MailboxApiCaller mailboxApiCaller;
|
||||
protected final MailboxApi mailboxApi;
|
||||
private final MailboxFileManager mailboxFileManager;
|
||||
protected final MailboxProperties mailboxProperties;
|
||||
protected final Object lock = new Object();
|
||||
|
||||
@GuardedBy("lock")
|
||||
protected State state = State.CREATED;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
protected Cancellable apiCall = null;
|
||||
|
||||
/**
|
||||
* Creates the API call that starts the worker's download cycle.
|
||||
*/
|
||||
protected abstract ApiCall createApiCallForDownloadCycle();
|
||||
|
||||
MailboxDownloadWorker(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor torReachabilityMonitor,
|
||||
MailboxApiCaller mailboxApiCaller,
|
||||
MailboxApi mailboxApi,
|
||||
MailboxFileManager mailboxFileManager,
|
||||
MailboxProperties mailboxProperties) {
|
||||
this.connectivityChecker = connectivityChecker;
|
||||
this.torReachabilityMonitor = torReachabilityMonitor;
|
||||
this.mailboxApiCaller = mailboxApiCaller;
|
||||
this.mailboxApi = mailboxApi;
|
||||
this.mailboxFileManager = mailboxFileManager;
|
||||
this.mailboxProperties = mailboxProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
LOG.info("Started");
|
||||
synchronized (lock) {
|
||||
// Don't allow the worker to be reused
|
||||
if (state != State.CREATED) return;
|
||||
state = State.CONNECTIVITY_CHECK;
|
||||
}
|
||||
// Avoid leaking observer in case destroy() is called concurrently
|
||||
// before observer is added
|
||||
connectivityChecker.checkConnectivity(mailboxProperties, this);
|
||||
boolean destroyed;
|
||||
synchronized (lock) {
|
||||
destroyed = state == State.DESTROYED;
|
||||
}
|
||||
if (destroyed) connectivityChecker.removeObserver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
LOG.info("Destroyed");
|
||||
Cancellable apiCall;
|
||||
synchronized (lock) {
|
||||
state = State.DESTROYED;
|
||||
apiCall = this.apiCall;
|
||||
this.apiCall = null;
|
||||
}
|
||||
if (apiCall != null) apiCall.cancel();
|
||||
connectivityChecker.removeObserver(this);
|
||||
torReachabilityMonitor.removeObserver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectivityCheckSucceeded() {
|
||||
LOG.info("Connectivity check succeeded");
|
||||
synchronized (lock) {
|
||||
if (state != State.CONNECTIVITY_CHECK) return;
|
||||
state = State.DOWNLOAD_CYCLE_1;
|
||||
// Start first download cycle
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
createApiCallForDownloadCycle());
|
||||
}
|
||||
}
|
||||
|
||||
void onDownloadCycleFinished() {
|
||||
boolean addObserver = false;
|
||||
synchronized (lock) {
|
||||
if (state == State.DOWNLOAD_CYCLE_1) {
|
||||
LOG.info("First download cycle finished");
|
||||
state = State.WAITING_FOR_TOR;
|
||||
apiCall = null;
|
||||
addObserver = true;
|
||||
} else if (state == State.DOWNLOAD_CYCLE_2) {
|
||||
LOG.info("Second download cycle finished");
|
||||
state = State.FINISHED;
|
||||
apiCall = null;
|
||||
}
|
||||
}
|
||||
if (addObserver) {
|
||||
// Avoid leaking observer in case destroy() is called concurrently
|
||||
// before observer is added
|
||||
torReachabilityMonitor.addOneShotObserver(this);
|
||||
boolean destroyed;
|
||||
synchronized (lock) {
|
||||
destroyed = state == State.DESTROYED;
|
||||
}
|
||||
if (destroyed) torReachabilityMonitor.removeObserver(this);
|
||||
}
|
||||
}
|
||||
|
||||
void downloadNextFile(Queue<FolderFile> queue) {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
if (queue.isEmpty()) {
|
||||
// Check for files again, as new files may have arrived while
|
||||
// we were downloading
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
createApiCallForDownloadCycle());
|
||||
} else {
|
||||
FolderFile file = queue.remove();
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
new SimpleApiCall(() ->
|
||||
apiCallDownloadFile(file, queue)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void apiCallDownloadFile(FolderFile file, Queue<FolderFile> queue)
|
||||
throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
}
|
||||
LOG.info("Downloading file");
|
||||
File tempFile = mailboxFileManager.createTempFileForDownload();
|
||||
try {
|
||||
mailboxApi.getFile(mailboxProperties, file.folderId, file.fileId,
|
||||
tempFile);
|
||||
} catch (IOException | ApiException e) {
|
||||
if (!tempFile.delete()) {
|
||||
LOG.warning("Failed to delete temporary file");
|
||||
}
|
||||
throw e;
|
||||
} catch (TolerableFailureException e) {
|
||||
// File not found - continue to the next file
|
||||
LOG.warning("File does not exist");
|
||||
if (!tempFile.delete()) {
|
||||
LOG.warning("Failed to delete temporary file");
|
||||
}
|
||||
downloadNextFile(queue);
|
||||
return;
|
||||
}
|
||||
mailboxFileManager.handleDownloadedFile(tempFile);
|
||||
deleteFile(file, queue);
|
||||
}
|
||||
|
||||
private void deleteFile(FolderFile file, Queue<FolderFile> queue) {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
new SimpleApiCall(() -> apiCallDeleteFile(file, queue)));
|
||||
}
|
||||
}
|
||||
|
||||
private void apiCallDeleteFile(FolderFile file, Queue<FolderFile> queue)
|
||||
throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
}
|
||||
try {
|
||||
mailboxApi.deleteFile(mailboxProperties, file.folderId,
|
||||
file.fileId);
|
||||
} catch (TolerableFailureException e) {
|
||||
// File not found - continue to the next file
|
||||
LOG.warning("File does not exist");
|
||||
}
|
||||
downloadNextFile(queue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTorReachable() {
|
||||
LOG.info("Our Tor hidden service is reachable");
|
||||
synchronized (lock) {
|
||||
if (state != State.WAITING_FOR_TOR) return;
|
||||
state = State.DOWNLOAD_CYCLE_2;
|
||||
// Start second download cycle
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
createApiCallForDownloadCycle());
|
||||
}
|
||||
}
|
||||
|
||||
// Package access for testing
|
||||
static class FolderFile {
|
||||
|
||||
final MailboxFolderId folderId;
|
||||
final MailboxFileId fileId;
|
||||
|
||||
FolderFile(MailboxFolderId folderId, MailboxFileId fileId) {
|
||||
this.folderId = folderId;
|
||||
this.fileId = fileId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -16,6 +18,14 @@ interface MailboxFileManager {
|
||||
*/
|
||||
File createTempFileForDownload() throws IOException;
|
||||
|
||||
/**
|
||||
* Creates a file to be uploaded to the given contact and writes any
|
||||
* waiting data to the file. The IDs of any messages sent or acked will
|
||||
* be added to the given {@link OutgoingSessionRecord}.
|
||||
*/
|
||||
File createAndWriteTempFileForUpload(ContactId contactId,
|
||||
OutgoingSessionRecord sessionRecord) throws IOException;
|
||||
|
||||
/**
|
||||
* Handles a file that has been downloaded. The file should be created
|
||||
* with {@link #createTempFileForDownload()}.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.connection.ConnectionManager;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
@@ -10,13 +11,18 @@ import org.briarproject.bramble.api.mailbox.MailboxDirectory;
|
||||
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.plugin.TransportConnectionWriter;
|
||||
import org.briarproject.bramble.api.plugin.event.TransportActiveEvent;
|
||||
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
|
||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
@@ -30,6 +36,7 @@ import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleS
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.ID;
|
||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||
import static org.briarproject.bramble.api.plugin.file.FileConstants.PROP_PATH;
|
||||
import static org.briarproject.bramble.util.IoUtils.delete;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@ThreadSafe
|
||||
@@ -41,6 +48,7 @@ class MailboxFileManagerImpl implements MailboxFileManager, EventListener {
|
||||
|
||||
// Package access for testing
|
||||
static final String DOWNLOAD_DIR_NAME = "downloads";
|
||||
static final String UPLOAD_DIR_NAME = "uploads";
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final PluginManager pluginManager;
|
||||
@@ -67,14 +75,44 @@ class MailboxFileManagerImpl implements MailboxFileManager, EventListener {
|
||||
|
||||
@Override
|
||||
public File createTempFileForDownload() throws IOException {
|
||||
return createTempFile(DOWNLOAD_DIR_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File createAndWriteTempFileForUpload(ContactId contactId,
|
||||
OutgoingSessionRecord sessionRecord) throws IOException {
|
||||
File f = createTempFile(UPLOAD_DIR_NAME);
|
||||
// We shouldn't reach this point until the plugin has been started
|
||||
SimplexPlugin plugin =
|
||||
(SimplexPlugin) requireNonNull(pluginManager.getPlugin(ID));
|
||||
TransportProperties p = new TransportProperties();
|
||||
p.put(PROP_PATH, f.getAbsolutePath());
|
||||
TransportConnectionWriter writer = plugin.createWriter(p);
|
||||
if (writer == null) {
|
||||
delete(f);
|
||||
throw new IOException();
|
||||
}
|
||||
MailboxFileWriter decorated = new MailboxFileWriter(writer);
|
||||
LOG.info("Writing file for upload");
|
||||
connectionManager.manageOutgoingConnection(contactId, ID, decorated,
|
||||
sessionRecord);
|
||||
if (decorated.awaitDisposal()) {
|
||||
// An exception was thrown during the session - delete the file
|
||||
delete(f);
|
||||
throw new IOException();
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
private File createTempFile(String dirName) throws IOException {
|
||||
// Wait for orphaned files to be handled before creating new files
|
||||
try {
|
||||
orphanLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
File downloadDir = createDirectoryIfNeeded(DOWNLOAD_DIR_NAME);
|
||||
return File.createTempFile("mailbox", ".tmp", downloadDir);
|
||||
File dir = createDirectoryIfNeeded(dirName);
|
||||
return File.createTempFile("mailbox", ".tmp", dir);
|
||||
}
|
||||
|
||||
private File createDirectoryIfNeeded(String name) throws IOException {
|
||||
@@ -116,6 +154,8 @@ class MailboxFileManagerImpl implements MailboxFileManager, EventListener {
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
// Wait for the transport to become active before handling orphaned
|
||||
// files so that we can get the plugin from the plugin manager
|
||||
if (e instanceof TransportActiveEvent) {
|
||||
TransportActiveEvent t = (TransportActiveEvent) e;
|
||||
if (t.getTransportId().equals(ID)) {
|
||||
@@ -127,17 +167,25 @@ class MailboxFileManagerImpl implements MailboxFileManager, EventListener {
|
||||
|
||||
/**
|
||||
* This method is called at startup, as soon as the plugin is started, to
|
||||
* handle any files that were left in the download directory at the last
|
||||
* shutdown.
|
||||
* delete any files that were left in the upload directory at the last
|
||||
* shutdown and handle any files that were left in the download directory.
|
||||
*/
|
||||
@IoExecutor
|
||||
private void handleOrphanedFiles() {
|
||||
try {
|
||||
File uploadDir = createDirectoryIfNeeded(UPLOAD_DIR_NAME);
|
||||
File[] orphanedUploads = uploadDir.listFiles();
|
||||
if (orphanedUploads != null) {
|
||||
for (File f : orphanedUploads) delete(f);
|
||||
}
|
||||
File downloadDir = createDirectoryIfNeeded(DOWNLOAD_DIR_NAME);
|
||||
File[] orphans = downloadDir.listFiles();
|
||||
// Now that we've got the list of orphans, new files can be created
|
||||
File[] orphanedDownloads = downloadDir.listFiles();
|
||||
// Now that we've got the list of orphaned downloads, new files
|
||||
// can be created in the download directory
|
||||
orphanLatch.countDown();
|
||||
if (orphans != null) for (File f : orphans) handleDownloadedFile(f);
|
||||
if (orphanedDownloads != null) {
|
||||
for (File f : orphanedDownloads) handleDownloadedFile(f);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
@@ -165,9 +213,58 @@ class MailboxFileManagerImpl implements MailboxFileManager, EventListener {
|
||||
delegate.dispose(exception, recognised);
|
||||
if (isHandlingComplete(exception, recognised)) {
|
||||
LOG.info("Deleting downloaded file");
|
||||
if (!file.delete()) {
|
||||
LOG.warning("Failed to delete downloaded file");
|
||||
}
|
||||
delete(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class MailboxFileWriter
|
||||
implements TransportConnectionWriter {
|
||||
|
||||
private final TransportConnectionWriter delegate;
|
||||
private final BlockingQueue<Boolean> disposalResult =
|
||||
new ArrayBlockingQueue<>(1);
|
||||
|
||||
private MailboxFileWriter(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);
|
||||
disposalResult.add(exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the delegate to be disposed and returns true if an
|
||||
* exception occurred.
|
||||
*/
|
||||
private boolean awaitDisposal() {
|
||||
try {
|
||||
return disposalResult.take();
|
||||
} catch (InterruptedException e) {
|
||||
LOG.info("Interrupted while waiting for disposal");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@ import org.briarproject.bramble.api.mailbox.MailboxPairingTask;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxStatus;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -98,34 +100,35 @@ class MailboxManagerImpl implements MailboxManager {
|
||||
|
||||
@Override
|
||||
public boolean checkConnection() {
|
||||
boolean success;
|
||||
List<MailboxVersion> versions = null;
|
||||
try {
|
||||
MailboxProperties props = db.transactionWithNullableResult(true,
|
||||
mailboxSettingsManager::getOwnMailboxProperties);
|
||||
if (props == null) throw new DbException();
|
||||
success = api.checkStatus(props);
|
||||
versions = api.getServerSupports(props);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
// we don't treat this is a failure to record
|
||||
return false;
|
||||
} catch (IOException | MailboxApi.ApiException e) {
|
||||
// we record this as a failure
|
||||
success = false;
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
try {
|
||||
recordCheckResult(success);
|
||||
recordCheckResult(versions);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
return success;
|
||||
return versions != null;
|
||||
}
|
||||
|
||||
private void recordCheckResult(boolean success) throws DbException {
|
||||
private void recordCheckResult(@Nullable List<MailboxVersion> versions)
|
||||
throws DbException {
|
||||
long now = clock.currentTimeMillis();
|
||||
db.transaction(false, txn -> {
|
||||
if (success) {
|
||||
mailboxSettingsManager.recordSuccessfulConnection(txn, now);
|
||||
if (versions != null) {
|
||||
mailboxSettingsManager
|
||||
.recordSuccessfulConnection(txn, now, versions);
|
||||
} else {
|
||||
mailboxSettingsManager.recordFailedConnectionAttempt(txn, now);
|
||||
}
|
||||
|
||||
@@ -52,13 +52,19 @@ public class MailboxModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MailboxSettingsManager provideMailboxSettingsManager(
|
||||
MailboxSettingsManagerImpl mailboxSettingsManager) {
|
||||
return mailboxSettingsManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
MailboxApi providesMailboxApi(MailboxApiImpl mailboxApi) {
|
||||
UrlConverter provideUrlConverter(UrlConverterImpl urlConverter) {
|
||||
return urlConverter;
|
||||
}
|
||||
|
||||
@Provides
|
||||
MailboxApi provideMailboxApi(MailboxApiImpl mailboxApi) {
|
||||
return mailboxApi;
|
||||
}
|
||||
|
||||
@@ -114,4 +120,10 @@ public class MailboxModule {
|
||||
}
|
||||
return mailboxFileManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
MailboxWorkerFactory provideMailboxWorkerFactory(
|
||||
MailboxWorkerFactoryImpl mailboxWorkerFactory) {
|
||||
return mailboxWorkerFactory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,10 +177,9 @@ class MailboxPairingTaskImpl implements MailboxPairingTask {
|
||||
LOG.info("QR code is valid");
|
||||
byte[] onionPubKey = Arrays.copyOfRange(bytes, 1, 33);
|
||||
String onion = crypto.encodeOnion(onionPubKey);
|
||||
String baseUrl = "http://" + onion + ".onion"; // TODO
|
||||
byte[] tokenBytes = Arrays.copyOfRange(bytes, 33, 65);
|
||||
MailboxAuthToken setupToken = new MailboxAuthToken(tokenBytes);
|
||||
return new MailboxProperties(baseUrl, setupToken, new ArrayList<>());
|
||||
return new MailboxProperties(onion, setupToken, new ArrayList<>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@ import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
|
||||
@Immutable
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class MailboxSettingsManagerImpl implements MailboxSettingsManager {
|
||||
|
||||
@@ -74,16 +74,10 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
|
||||
public void setOwnMailboxProperties(Transaction txn, MailboxProperties p)
|
||||
throws DbException {
|
||||
Settings s = new Settings();
|
||||
s.put(SETTINGS_KEY_ONION, p.getBaseUrl());
|
||||
s.put(SETTINGS_KEY_ONION, p.getOnion());
|
||||
s.put(SETTINGS_KEY_TOKEN, p.getAuthToken().toString());
|
||||
List<MailboxVersion> serverSupports = p.getServerSupports();
|
||||
int[] ints = new int[serverSupports.size() * 2];
|
||||
int i = 0;
|
||||
for (MailboxVersion v : serverSupports) {
|
||||
ints[i++] = v.getMajor();
|
||||
ints[i++] = v.getMinor();
|
||||
}
|
||||
s.putIntArray(SETTINGS_KEY_SERVER_SUPPORTS, ints);
|
||||
encodeServerSupports(serverSupports, s);
|
||||
settingsManager.mergeSettings(txn, s, SETTINGS_NAMESPACE);
|
||||
for (MailboxHook hook : hooks) {
|
||||
hook.mailboxPaired(txn, p.getOnion(), p.getServerSupports());
|
||||
@@ -121,14 +115,30 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
|
||||
@Override
|
||||
public void recordSuccessfulConnection(Transaction txn, long now)
|
||||
throws DbException {
|
||||
Settings oldSettings =
|
||||
settingsManager.getSettings(txn, SETTINGS_NAMESPACE);
|
||||
recordSuccessfulConnection(txn, now, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordSuccessfulConnection(Transaction txn, long now,
|
||||
@Nullable List<MailboxVersion> versions) throws DbException {
|
||||
Settings s = new Settings();
|
||||
// fetch version that the server supports first
|
||||
List<MailboxVersion> serverSupports;
|
||||
if (versions == null) {
|
||||
Settings oldSettings =
|
||||
settingsManager.getSettings(txn, SETTINGS_NAMESPACE);
|
||||
serverSupports = parseServerSupports(oldSettings);
|
||||
} else {
|
||||
serverSupports = versions;
|
||||
// store new versions
|
||||
encodeServerSupports(serverSupports, s);
|
||||
}
|
||||
// now record the successful connection
|
||||
s.putLong(SETTINGS_KEY_LAST_ATTEMPT, now);
|
||||
s.putLong(SETTINGS_KEY_LAST_SUCCESS, now);
|
||||
s.putInt(SETTINGS_KEY_ATTEMPTS, 0);
|
||||
settingsManager.mergeSettings(txn, s, SETTINGS_NAMESPACE);
|
||||
List<MailboxVersion> serverSupports = parseServerSupports(oldSettings);
|
||||
// broadcast status event
|
||||
MailboxStatus status = new MailboxStatus(now, now, 0, serverSupports);
|
||||
txn.attach(new OwnMailboxConnectionStatusEvent(status));
|
||||
}
|
||||
@@ -171,6 +181,17 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
|
||||
return filename;
|
||||
}
|
||||
|
||||
private void encodeServerSupports(List<MailboxVersion> serverSupports,
|
||||
Settings s) {
|
||||
int[] ints = new int[serverSupports.size() * 2];
|
||||
int i = 0;
|
||||
for (MailboxVersion v : serverSupports) {
|
||||
ints[i++] = v.getMajor();
|
||||
ints[i++] = v.getMinor();
|
||||
}
|
||||
s.putIntArray(SETTINGS_KEY_SERVER_SUPPORTS, ints);
|
||||
}
|
||||
|
||||
private List<MailboxVersion> parseServerSupports(Settings s)
|
||||
throws DbException {
|
||||
if (!s.containsKey(SETTINGS_KEY_SERVER_SUPPORTS)) return emptyList();
|
||||
|
||||
@@ -242,8 +242,7 @@ class MailboxUpdateManagerImpl implements MailboxUpdateManager,
|
||||
private void createAndSendUpdateWithMailbox(Transaction txn, Contact c,
|
||||
List<MailboxVersion> serverSupports, String ownOnion)
|
||||
throws DbException {
|
||||
String baseUrl = "http://" + ownOnion + ".onion"; // TODO
|
||||
MailboxProperties properties = new MailboxProperties(baseUrl,
|
||||
MailboxProperties properties = new MailboxProperties(ownOnion,
|
||||
new MailboxAuthToken(crypto.generateUniqueId().getBytes()),
|
||||
serverSupports,
|
||||
new MailboxFolderId(crypto.generateUniqueId().getBytes()),
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
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.EventExecutor;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
import org.briarproject.bramble.api.sync.event.GroupVisibilityUpdatedEvent;
|
||||
import org.briarproject.bramble.api.sync.event.MessageSharedEvent;
|
||||
import org.briarproject.bramble.api.sync.event.MessageToAckEvent;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||
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.mailbox.MailboxConstants.MAX_LATENCY;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||
import static org.briarproject.bramble.util.IoUtils.delete;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class MailboxUploadWorker implements MailboxWorker, ConnectivityObserver,
|
||||
EventListener {
|
||||
|
||||
/**
|
||||
* When the worker is started it checks for data to send. If data is ready
|
||||
* to send, the worker waits for a connectivity check, then writes and
|
||||
* uploads a file and checks again for data to send.
|
||||
* <p>
|
||||
* If data is due to be sent at some time in the future, the worker
|
||||
* schedules a wakeup for that time and also listens for events indicating
|
||||
* that new data may be ready to send.
|
||||
* <p>
|
||||
* If there's no data to send, the worker listens for events indicating
|
||||
* that new data may be ready to send.
|
||||
*/
|
||||
private enum State {
|
||||
CREATED,
|
||||
CHECKING_FOR_DATA,
|
||||
WAITING_FOR_DATA,
|
||||
CONNECTIVITY_CHECK,
|
||||
WRITING_UPLOADING,
|
||||
DESTROYED
|
||||
}
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(MailboxUploadWorker.class.getName());
|
||||
|
||||
/**
|
||||
* When we're waiting for data to send and an event indicates that new data
|
||||
* may have become available, wait this long before checking the DB. This
|
||||
* should help to avoid creating lots of small files when several acks or
|
||||
* messages become available to send in a short period (eg when reading a
|
||||
* file downloaded from a mailbox).
|
||||
* <p>
|
||||
* Package access for testing.
|
||||
*/
|
||||
static final long CHECK_DELAY_MS = 5_000;
|
||||
|
||||
/**
|
||||
* How long to wait before retrying when an exception occurs while writing
|
||||
* a file.
|
||||
* <p>
|
||||
* Package access for testing.
|
||||
*/
|
||||
static final long RETRY_DELAY_MS = MINUTES.toMillis(1);
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final DatabaseComponent db;
|
||||
private final Clock clock;
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final EventBus eventBus;
|
||||
private final ConnectivityChecker connectivityChecker;
|
||||
private final MailboxApiCaller mailboxApiCaller;
|
||||
private final MailboxApi mailboxApi;
|
||||
private final MailboxFileManager mailboxFileManager;
|
||||
private final MailboxProperties mailboxProperties;
|
||||
private final MailboxFolderId folderId;
|
||||
private final ContactId contactId;
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
@GuardedBy("lock")
|
||||
private State state = State.CREATED;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private Cancellable wakeupTask = null, checkTask = null, apiCall = null;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private File file = null;
|
||||
|
||||
MailboxUploadWorker(@IoExecutor Executor ioExecutor,
|
||||
DatabaseComponent db,
|
||||
Clock clock,
|
||||
TaskScheduler taskScheduler,
|
||||
EventBus eventBus,
|
||||
ConnectivityChecker connectivityChecker,
|
||||
MailboxApiCaller mailboxApiCaller,
|
||||
MailboxApi mailboxApi,
|
||||
MailboxFileManager mailboxFileManager,
|
||||
MailboxProperties mailboxProperties,
|
||||
MailboxFolderId folderId,
|
||||
ContactId contactId) {
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.db = db;
|
||||
this.clock = clock;
|
||||
this.taskScheduler = taskScheduler;
|
||||
this.eventBus = eventBus;
|
||||
this.connectivityChecker = connectivityChecker;
|
||||
this.mailboxApiCaller = mailboxApiCaller;
|
||||
this.mailboxApi = mailboxApi;
|
||||
this.mailboxFileManager = mailboxFileManager;
|
||||
this.mailboxProperties = mailboxProperties;
|
||||
this.folderId = folderId;
|
||||
this.contactId = contactId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
LOG.info("Started");
|
||||
synchronized (lock) {
|
||||
// Don't allow the worker to be reused
|
||||
if (state != State.CREATED) return;
|
||||
state = State.CHECKING_FOR_DATA;
|
||||
}
|
||||
ioExecutor.execute(this::checkForDataToSend);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
LOG.info("Destroyed");
|
||||
Cancellable wakeupTask, checkTask, apiCall;
|
||||
File file;
|
||||
synchronized (lock) {
|
||||
state = State.DESTROYED;
|
||||
wakeupTask = this.wakeupTask;
|
||||
this.wakeupTask = null;
|
||||
checkTask = this.checkTask;
|
||||
this.checkTask = null;
|
||||
apiCall = this.apiCall;
|
||||
this.apiCall = null;
|
||||
file = this.file;
|
||||
this.file = null;
|
||||
}
|
||||
if (wakeupTask != null) wakeupTask.cancel();
|
||||
if (checkTask != null) checkTask.cancel();
|
||||
if (apiCall != null) apiCall.cancel();
|
||||
if (file != null) delete(file);
|
||||
connectivityChecker.removeObserver(this);
|
||||
eventBus.removeListener(this);
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void checkForDataToSend() {
|
||||
synchronized (lock) {
|
||||
checkTask = null;
|
||||
if (state != State.CHECKING_FOR_DATA) return;
|
||||
}
|
||||
LOG.info("Checking for data to send");
|
||||
try {
|
||||
db.transaction(true, txn -> {
|
||||
long nextSendTime;
|
||||
if (db.containsAcksToSend(txn, contactId)) {
|
||||
nextSendTime = 0L;
|
||||
} else {
|
||||
nextSendTime = db.getNextSendTime(txn, contactId,
|
||||
MAX_LATENCY);
|
||||
}
|
||||
// Handle the result on the event executor to avoid races with
|
||||
// incoming events
|
||||
txn.attach(() -> handleNextSendTime(nextSendTime));
|
||||
});
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void handleNextSendTime(long nextSendTime) {
|
||||
if (nextSendTime == Long.MAX_VALUE) {
|
||||
// Nothing is sendable now or due to be sent in the future. Wait
|
||||
// for an event indicating that new data may be ready to send
|
||||
waitForDataToSend();
|
||||
} else {
|
||||
// Work out the delay until data's ready to send (may be negative)
|
||||
long delay = nextSendTime - clock.currentTimeMillis();
|
||||
if (delay > 0) {
|
||||
// Schedule a wakeup when data will be ready to send. If an
|
||||
// event is received in the meantime indicating that new data
|
||||
// may be ready to send, we'll cancel the wakeup
|
||||
scheduleWakeup(delay);
|
||||
} else {
|
||||
// Data is ready to send now
|
||||
checkConnectivity();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void waitForDataToSend() {
|
||||
synchronized (lock) {
|
||||
if (state != State.CHECKING_FOR_DATA) return;
|
||||
state = State.WAITING_FOR_DATA;
|
||||
LOG.info("Waiting for data to send");
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void scheduleWakeup(long delay) {
|
||||
synchronized (lock) {
|
||||
if (state != State.CHECKING_FOR_DATA) return;
|
||||
state = State.WAITING_FOR_DATA;
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Scheduling wakeup in " + delay + " ms");
|
||||
}
|
||||
wakeupTask = taskScheduler.schedule(this::wakeUp, ioExecutor,
|
||||
delay, MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void wakeUp() {
|
||||
LOG.info("Woke up");
|
||||
synchronized (lock) {
|
||||
wakeupTask = null;
|
||||
if (state != State.WAITING_FOR_DATA) return;
|
||||
state = State.CHECKING_FOR_DATA;
|
||||
}
|
||||
checkForDataToSend();
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void checkConnectivity() {
|
||||
synchronized (lock) {
|
||||
if (state != State.CHECKING_FOR_DATA) return;
|
||||
state = State.CONNECTIVITY_CHECK;
|
||||
}
|
||||
LOG.info("Checking connectivity");
|
||||
// Avoid leaking observer in case destroy() is called concurrently
|
||||
// before observer is added
|
||||
connectivityChecker.checkConnectivity(mailboxProperties, this);
|
||||
boolean destroyed;
|
||||
synchronized (lock) {
|
||||
destroyed = state == State.DESTROYED;
|
||||
}
|
||||
if (destroyed) connectivityChecker.removeObserver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectivityCheckSucceeded() {
|
||||
LOG.info("Connectivity check succeeded");
|
||||
synchronized (lock) {
|
||||
if (state != State.CONNECTIVITY_CHECK) return;
|
||||
state = State.WRITING_UPLOADING;
|
||||
}
|
||||
ioExecutor.execute(this::writeAndUploadFile);
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void writeAndUploadFile() {
|
||||
synchronized (lock) {
|
||||
if (state != State.WRITING_UPLOADING) return;
|
||||
}
|
||||
OutgoingSessionRecord sessionRecord = new OutgoingSessionRecord();
|
||||
File file;
|
||||
try {
|
||||
file = mailboxFileManager.createAndWriteTempFileForUpload(
|
||||
contactId, sessionRecord);
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
// Try again after a delay
|
||||
synchronized (lock) {
|
||||
if (state != State.WRITING_UPLOADING) return;
|
||||
state = State.CHECKING_FOR_DATA;
|
||||
checkTask = taskScheduler.schedule(this::checkForDataToSend,
|
||||
ioExecutor, RETRY_DELAY_MS, MILLISECONDS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
boolean deleteFile = false;
|
||||
synchronized (lock) {
|
||||
if (state == State.WRITING_UPLOADING) {
|
||||
this.file = file;
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
new SimpleApiCall(() -> apiCallUploadFile(file,
|
||||
sessionRecord)));
|
||||
} else {
|
||||
deleteFile = true;
|
||||
}
|
||||
}
|
||||
if (deleteFile) delete(file);
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void apiCallUploadFile(File file,
|
||||
OutgoingSessionRecord sessionRecord)
|
||||
throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state != State.WRITING_UPLOADING) return;
|
||||
}
|
||||
LOG.info("Uploading file");
|
||||
mailboxApi.addFile(mailboxProperties, folderId, file);
|
||||
markMessagesSentOrAcked(sessionRecord);
|
||||
synchronized (lock) {
|
||||
if (state != State.WRITING_UPLOADING) return;
|
||||
state = State.CHECKING_FOR_DATA;
|
||||
apiCall = null;
|
||||
this.file = null;
|
||||
}
|
||||
delete(file);
|
||||
checkForDataToSend();
|
||||
}
|
||||
|
||||
private void markMessagesSentOrAcked(OutgoingSessionRecord sessionRecord) {
|
||||
Collection<MessageId> acked = sessionRecord.getAckedIds();
|
||||
Collection<MessageId> sent = sessionRecord.getSentIds();
|
||||
try {
|
||||
db.transaction(false, txn -> {
|
||||
if (!acked.isEmpty()) {
|
||||
db.setAckSent(txn, contactId, acked);
|
||||
}
|
||||
if (!sent.isEmpty()) {
|
||||
db.setMessagesSent(txn, contactId, sent, MAX_LATENCY);
|
||||
}
|
||||
});
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof MessageToAckEvent) {
|
||||
MessageToAckEvent m = (MessageToAckEvent) e;
|
||||
if (m.getContactId().equals(contactId)) {
|
||||
LOG.info("Message to ack");
|
||||
onDataToSend();
|
||||
}
|
||||
} else if (e instanceof MessageSharedEvent) {
|
||||
LOG.info("Message shared");
|
||||
onDataToSend();
|
||||
} else if (e instanceof GroupVisibilityUpdatedEvent) {
|
||||
GroupVisibilityUpdatedEvent g = (GroupVisibilityUpdatedEvent) e;
|
||||
if (g.getVisibility() == SHARED &&
|
||||
g.getAffectedContacts().contains(contactId)) {
|
||||
LOG.info("Group shared");
|
||||
onDataToSend();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onDataToSend() {
|
||||
Cancellable wakeupTask;
|
||||
synchronized (lock) {
|
||||
if (state != State.WAITING_FOR_DATA) return;
|
||||
state = State.CHECKING_FOR_DATA;
|
||||
wakeupTask = this.wakeupTask;
|
||||
this.wakeupTask = null;
|
||||
// Delay the check to avoid creating lots of small files
|
||||
checkTask = taskScheduler.schedule(this::checkForDataToSend,
|
||||
ioExecutor, CHECK_DELAY_MS, MILLISECONDS);
|
||||
}
|
||||
// If we had scheduled a wakeup when data was due to be sent, cancel it
|
||||
if (wakeupTask != null) wakeupTask.cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* A worker that downloads files from a contact's mailbox.
|
||||
*/
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
interface MailboxWorker {
|
||||
|
||||
/**
|
||||
* Asynchronously starts the worker.
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Destroys the worker and cancels any pending tasks or retries.
|
||||
*/
|
||||
void destroy();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
interface MailboxWorkerFactory {
|
||||
|
||||
MailboxWorker createUploadWorker(ConnectivityChecker connectivityChecker,
|
||||
MailboxProperties properties, MailboxFolderId folderId,
|
||||
ContactId contactId);
|
||||
|
||||
MailboxWorker createDownloadWorkerForContactMailbox(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor reachabilityMonitor,
|
||||
MailboxProperties properties);
|
||||
|
||||
MailboxWorker createDownloadWorkerForOwnMailbox(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor reachabilityMonitor,
|
||||
MailboxProperties properties);
|
||||
|
||||
MailboxWorker createContactListWorkerForOwnMailbox(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
MailboxProperties properties);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
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.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class MailboxWorkerFactoryImpl implements MailboxWorkerFactory {
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final DatabaseComponent db;
|
||||
private final Clock clock;
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final EventBus eventBus;
|
||||
private final MailboxApiCaller mailboxApiCaller;
|
||||
private final MailboxApi mailboxApi;
|
||||
private final MailboxFileManager mailboxFileManager;
|
||||
private final MailboxUpdateManager mailboxUpdateManager;
|
||||
|
||||
@Inject
|
||||
MailboxWorkerFactoryImpl(@IoExecutor Executor ioExecutor,
|
||||
DatabaseComponent db,
|
||||
Clock clock,
|
||||
TaskScheduler taskScheduler,
|
||||
EventBus eventBus,
|
||||
MailboxApiCaller mailboxApiCaller,
|
||||
MailboxApi mailboxApi,
|
||||
MailboxFileManager mailboxFileManager,
|
||||
MailboxUpdateManager mailboxUpdateManager) {
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.db = db;
|
||||
this.clock = clock;
|
||||
this.taskScheduler = taskScheduler;
|
||||
this.eventBus = eventBus;
|
||||
this.mailboxApiCaller = mailboxApiCaller;
|
||||
this.mailboxApi = mailboxApi;
|
||||
this.mailboxFileManager = mailboxFileManager;
|
||||
this.mailboxUpdateManager = mailboxUpdateManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxWorker createUploadWorker(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
MailboxProperties properties, MailboxFolderId folderId,
|
||||
ContactId contactId) {
|
||||
MailboxUploadWorker worker = new MailboxUploadWorker(ioExecutor, db,
|
||||
clock, taskScheduler, eventBus, connectivityChecker,
|
||||
mailboxApiCaller, mailboxApi, mailboxFileManager,
|
||||
properties, folderId, contactId);
|
||||
eventBus.addListener(worker);
|
||||
return worker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxWorker createDownloadWorkerForContactMailbox(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor reachabilityMonitor,
|
||||
MailboxProperties properties) {
|
||||
return new ContactMailboxDownloadWorker(connectivityChecker,
|
||||
reachabilityMonitor, mailboxApiCaller, mailboxApi,
|
||||
mailboxFileManager, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxWorker createDownloadWorkerForOwnMailbox(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor reachabilityMonitor,
|
||||
MailboxProperties properties) {
|
||||
return new OwnMailboxDownloadWorker(connectivityChecker,
|
||||
reachabilityMonitor, mailboxApiCaller, mailboxApi,
|
||||
mailboxFileManager, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxWorker createContactListWorkerForOwnMailbox(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
MailboxProperties properties) {
|
||||
return new OwnMailboxContactListWorker(ioExecutor, db, eventBus,
|
||||
connectivityChecker, mailboxApiCaller, mailboxApi,
|
||||
mailboxUpdateManager, properties);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class OwnMailboxClient implements MailboxClient {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(OwnMailboxClient.class.getName());
|
||||
|
||||
private final MailboxWorkerFactory workerFactory;
|
||||
private final ConnectivityChecker connectivityChecker;
|
||||
private final TorReachabilityMonitor reachabilityMonitor;
|
||||
private final MailboxWorker contactListWorker;
|
||||
private final Object lock = new Object();
|
||||
|
||||
/**
|
||||
* Upload workers: one worker per contact assigned for upload.
|
||||
*/
|
||||
@GuardedBy("lock")
|
||||
private final Map<ContactId, MailboxWorker> uploadWorkers = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Download worker: shared between all contacts assigned for download.
|
||||
* Null if no contacts are assigned for download.
|
||||
*/
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private MailboxWorker downloadWorker = null;
|
||||
|
||||
/**
|
||||
* IDs of contacts assigned for download, so that we know when to
|
||||
* create/destroy the download worker.
|
||||
*/
|
||||
@GuardedBy("lock")
|
||||
private final Set<ContactId> assignedForDownload = new HashSet<>();
|
||||
|
||||
OwnMailboxClient(MailboxWorkerFactory workerFactory,
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor reachabilityMonitor,
|
||||
MailboxProperties properties) {
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
this.workerFactory = workerFactory;
|
||||
this.connectivityChecker = connectivityChecker;
|
||||
this.reachabilityMonitor = reachabilityMonitor;
|
||||
contactListWorker = workerFactory.createContactListWorkerForOwnMailbox(
|
||||
connectivityChecker, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
LOG.info("Started");
|
||||
contactListWorker.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
LOG.info("Destroyed");
|
||||
List<MailboxWorker> uploadWorkers;
|
||||
MailboxWorker downloadWorker;
|
||||
synchronized (lock) {
|
||||
uploadWorkers = new ArrayList<>(this.uploadWorkers.values());
|
||||
this.uploadWorkers.clear();
|
||||
downloadWorker = this.downloadWorker;
|
||||
this.downloadWorker = null;
|
||||
}
|
||||
// Destroy the workers (with apologies to Mr Marx and Mr Engels)
|
||||
for (MailboxWorker worker : uploadWorkers) worker.destroy();
|
||||
if (downloadWorker != null) downloadWorker.destroy();
|
||||
contactListWorker.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assignContactForUpload(ContactId contactId,
|
||||
MailboxProperties properties, MailboxFolderId folderId) {
|
||||
LOG.info("Contact assigned for upload");
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
MailboxWorker uploadWorker = workerFactory.createUploadWorker(
|
||||
connectivityChecker, properties, folderId, contactId);
|
||||
synchronized (lock) {
|
||||
MailboxWorker old = uploadWorkers.put(contactId, uploadWorker);
|
||||
if (old != null) throw new IllegalStateException();
|
||||
}
|
||||
uploadWorker.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deassignContactForUpload(ContactId contactId) {
|
||||
LOG.info("Contact deassigned for upload");
|
||||
MailboxWorker uploadWorker;
|
||||
synchronized (lock) {
|
||||
uploadWorker = uploadWorkers.remove(contactId);
|
||||
}
|
||||
if (uploadWorker != null) uploadWorker.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assignContactForDownload(ContactId contactId,
|
||||
MailboxProperties properties, MailboxFolderId folderId) {
|
||||
LOG.info("Contact assigned for download");
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
// Create a download worker if we don't already have one. The worker
|
||||
// will use the API to discover which folders have files to download,
|
||||
// so it doesn't need to track the set of assigned contacts
|
||||
MailboxWorker toStart = null;
|
||||
synchronized (lock) {
|
||||
if (!assignedForDownload.add(contactId)) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (downloadWorker == null) {
|
||||
toStart = workerFactory.createDownloadWorkerForOwnMailbox(
|
||||
connectivityChecker, reachabilityMonitor, properties);
|
||||
downloadWorker = toStart;
|
||||
}
|
||||
}
|
||||
if (toStart != null) toStart.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deassignContactForDownload(ContactId contactId) {
|
||||
LOG.info("Contact deassigned for download");
|
||||
// If there are no more contacts assigned for download, destroy the
|
||||
// download worker
|
||||
MailboxWorker toDestroy = null;
|
||||
synchronized (lock) {
|
||||
if (!assignedForDownload.remove(contactId)) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (assignedForDownload.isEmpty()) {
|
||||
toDestroy = downloadWorker;
|
||||
downloadWorker = null;
|
||||
}
|
||||
}
|
||||
if (toDestroy != null) toDestroy.destroy();
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,13 @@ import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
@@ -55,11 +57,12 @@ class OwnMailboxConnectivityChecker extends ConnectivityCheckerImpl {
|
||||
private boolean checkConnectivityAndStoreResult(
|
||||
MailboxProperties properties) throws DbException {
|
||||
try {
|
||||
if (!mailboxApi.checkStatus(properties)) throw new ApiException();
|
||||
List<MailboxVersion> serverSupports =
|
||||
mailboxApi.getServerSupports(properties);
|
||||
LOG.info("Own mailbox is reachable");
|
||||
long now = clock.currentTimeMillis();
|
||||
db.transaction(false, txn -> mailboxSettingsManager
|
||||
.recordSuccessfulConnection(txn, now));
|
||||
.recordSuccessfulConnection(txn, now, serverSupports));
|
||||
// Call the observers and cache the result
|
||||
onConnectivityCheckSucceeded(now);
|
||||
return false; // Don't retry
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
|
||||
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
|
||||
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.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventExecutor;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdate;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateWithMailbox;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.MailboxContact;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
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.nullsafety.NullSafety.requireNonNull;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class OwnMailboxContactListWorker
|
||||
implements MailboxWorker, ConnectivityObserver, EventListener {
|
||||
|
||||
/**
|
||||
* When the worker is started it waits for a connectivity check, then
|
||||
* fetches the remote contact list and compares it to the local contact
|
||||
* list.
|
||||
* <p>
|
||||
* Any contacts that are missing from the remote list are added to the
|
||||
* mailbox's contact list, while any contacts that are missing from the
|
||||
* local list are removed from the mailbox's contact list.
|
||||
* <p>
|
||||
* Once the remote contact list has been brought up to date, the worker
|
||||
* waits for events indicating that contacts have been added or removed.
|
||||
* Each time an event is received, the worker updates the mailbox's
|
||||
* contact list and then goes back to waiting.
|
||||
*/
|
||||
private enum State {
|
||||
CREATED,
|
||||
CONNECTIVITY_CHECK,
|
||||
FETCHING_CONTACT_LIST,
|
||||
UPDATING_CONTACT_LIST,
|
||||
WAITING_FOR_CHANGES,
|
||||
DESTROYED
|
||||
}
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(OwnMailboxContactListWorker.class.getName());
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final DatabaseComponent db;
|
||||
private final EventBus eventBus;
|
||||
private final ConnectivityChecker connectivityChecker;
|
||||
private final MailboxApiCaller mailboxApiCaller;
|
||||
private final MailboxApi mailboxApi;
|
||||
private final MailboxUpdateManager mailboxUpdateManager;
|
||||
private final MailboxProperties mailboxProperties;
|
||||
private final Object lock = new Object();
|
||||
|
||||
@GuardedBy("lock")
|
||||
private State state = State.CREATED;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private Cancellable apiCall = null;
|
||||
|
||||
/**
|
||||
* A queue of updates waiting to be applied to the remote contact list.
|
||||
*/
|
||||
@GuardedBy("lock")
|
||||
private final Queue<Update> updates = new LinkedList<>();
|
||||
|
||||
OwnMailboxContactListWorker(@IoExecutor Executor ioExecutor,
|
||||
DatabaseComponent db,
|
||||
EventBus eventBus,
|
||||
ConnectivityChecker connectivityChecker,
|
||||
MailboxApiCaller mailboxApiCaller,
|
||||
MailboxApi mailboxApi,
|
||||
MailboxUpdateManager mailboxUpdateManager,
|
||||
MailboxProperties mailboxProperties) {
|
||||
if (!mailboxProperties.isOwner()) throw new IllegalArgumentException();
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.db = db;
|
||||
this.connectivityChecker = connectivityChecker;
|
||||
this.mailboxApiCaller = mailboxApiCaller;
|
||||
this.mailboxApi = mailboxApi;
|
||||
this.mailboxUpdateManager = mailboxUpdateManager;
|
||||
this.mailboxProperties = mailboxProperties;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
LOG.info("Started");
|
||||
synchronized (lock) {
|
||||
if (state != State.CREATED) return;
|
||||
state = State.CONNECTIVITY_CHECK;
|
||||
}
|
||||
// Avoid leaking observer in case destroy() is called concurrently
|
||||
// before observer is added
|
||||
connectivityChecker.checkConnectivity(mailboxProperties, this);
|
||||
boolean destroyed;
|
||||
synchronized (lock) {
|
||||
destroyed = state == State.DESTROYED;
|
||||
}
|
||||
if (destroyed) connectivityChecker.removeObserver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
LOG.info("Destroyed");
|
||||
Cancellable apiCall;
|
||||
synchronized (lock) {
|
||||
state = State.DESTROYED;
|
||||
apiCall = this.apiCall;
|
||||
this.apiCall = null;
|
||||
}
|
||||
if (apiCall != null) apiCall.cancel();
|
||||
connectivityChecker.removeObserver(this);
|
||||
eventBus.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectivityCheckSucceeded() {
|
||||
LOG.info("Connectivity check succeeded");
|
||||
synchronized (lock) {
|
||||
if (state != State.CONNECTIVITY_CHECK) return;
|
||||
state = State.FETCHING_CONTACT_LIST;
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
new SimpleApiCall(this::apiCallFetchContactList));
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void apiCallFetchContactList() throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state != State.FETCHING_CONTACT_LIST) return;
|
||||
}
|
||||
LOG.info("Fetching remote contact list");
|
||||
Collection<ContactId> remote =
|
||||
mailboxApi.getContacts(mailboxProperties);
|
||||
ioExecutor.execute(() -> loadLocalContactList(remote));
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void loadLocalContactList(Collection<ContactId> remote) {
|
||||
synchronized (lock) {
|
||||
if (state != State.FETCHING_CONTACT_LIST) return;
|
||||
apiCall = null;
|
||||
}
|
||||
LOG.info("Loading local contact list");
|
||||
try {
|
||||
db.transaction(true, txn -> {
|
||||
Collection<Contact> local = db.getContacts(txn);
|
||||
// Handle the result on the event executor to avoid races with
|
||||
// incoming events
|
||||
txn.attach(() -> reconcileContactLists(local, remote));
|
||||
});
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void reconcileContactLists(Collection<Contact> local,
|
||||
Collection<ContactId> remote) {
|
||||
Set<ContactId> localIds = new HashSet<>();
|
||||
for (Contact c : local) localIds.add(c.getId());
|
||||
remote = new HashSet<>(remote);
|
||||
synchronized (lock) {
|
||||
if (state != State.FETCHING_CONTACT_LIST) return;
|
||||
for (ContactId c : localIds) {
|
||||
if (!remote.contains(c)) updates.add(new Update(true, c));
|
||||
}
|
||||
for (ContactId c : remote) {
|
||||
if (!localIds.contains(c)) updates.add(new Update(false, c));
|
||||
}
|
||||
if (updates.isEmpty()) {
|
||||
LOG.info("Contact list is up to date");
|
||||
state = State.WAITING_FOR_CHANGES;
|
||||
} else {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info(updates.size() + " updates to apply");
|
||||
}
|
||||
state = State.UPDATING_CONTACT_LIST;
|
||||
ioExecutor.execute(this::updateContactList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void updateContactList() {
|
||||
Update update;
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST) return;
|
||||
update = updates.poll();
|
||||
if (update == null) {
|
||||
LOG.info("No more updates to process");
|
||||
state = State.WAITING_FOR_CHANGES;
|
||||
apiCall = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (update.add) loadMailboxProperties(update.contactId);
|
||||
else removeContact(update.contactId);
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void loadMailboxProperties(ContactId c) {
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST) return;
|
||||
}
|
||||
LOG.info("Loading mailbox properties for contact");
|
||||
try {
|
||||
MailboxUpdate mailboxUpdate = db.transactionWithResult(true, txn ->
|
||||
mailboxUpdateManager.getLocalUpdate(txn, c));
|
||||
if (mailboxUpdate instanceof MailboxUpdateWithMailbox) {
|
||||
addContact(c, (MailboxUpdateWithMailbox) mailboxUpdate);
|
||||
} else {
|
||||
// Our own mailbox was concurrently unpaired. This worker will
|
||||
// be destroyed soon, so we can stop here
|
||||
LOG.info("Own mailbox was unpaired");
|
||||
}
|
||||
} catch (NoSuchContactException e) {
|
||||
// Contact was removed concurrently. Move on to the next update.
|
||||
// Later we may process a removal update for this contact, which
|
||||
// was never added to the mailbox's contact list. The removal API
|
||||
// call should fail safely with a TolerableFailureException
|
||||
LOG.info("No such contact");
|
||||
updateContactList();
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void addContact(ContactId c, MailboxUpdateWithMailbox withMailbox) {
|
||||
MailboxProperties props = withMailbox.getMailboxProperties();
|
||||
MailboxContact contact = new MailboxContact(c, props.getAuthToken(),
|
||||
requireNonNull(props.getInboxId()),
|
||||
requireNonNull(props.getOutboxId()));
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST) return;
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(new SimpleApiCall(() ->
|
||||
apiCallAddContact(contact)));
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void apiCallAddContact(MailboxContact contact)
|
||||
throws IOException, ApiException, TolerableFailureException {
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST) return;
|
||||
}
|
||||
LOG.info("Adding contact to remote contact list");
|
||||
mailboxApi.addContact(mailboxProperties, contact);
|
||||
updateContactList();
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void removeContact(ContactId c) {
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST) return;
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(new SimpleApiCall(() ->
|
||||
apiCallRemoveContact(c)));
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void apiCallRemoveContact(ContactId c)
|
||||
throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST) return;
|
||||
}
|
||||
LOG.info("Removing contact from remote contact list");
|
||||
try {
|
||||
mailboxApi.deleteContact(mailboxProperties, c);
|
||||
} catch (TolerableFailureException e) {
|
||||
// Catch this so we can continue to the next update
|
||||
LOG.warning("Contact does not exist");
|
||||
}
|
||||
updateContactList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof ContactAddedEvent) {
|
||||
LOG.info("Contact added");
|
||||
onContactAdded(((ContactAddedEvent) e).getContactId());
|
||||
} else if (e instanceof ContactRemovedEvent) {
|
||||
LOG.info("Contact removed");
|
||||
onContactRemoved(((ContactRemovedEvent) e).getContactId());
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onContactAdded(ContactId c) {
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST &&
|
||||
state != State.WAITING_FOR_CHANGES) {
|
||||
return;
|
||||
}
|
||||
updates.add(new Update(true, c));
|
||||
if (state == State.WAITING_FOR_CHANGES) {
|
||||
state = State.UPDATING_CONTACT_LIST;
|
||||
ioExecutor.execute(this::updateContactList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onContactRemoved(ContactId c) {
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST &&
|
||||
state != State.WAITING_FOR_CHANGES) {
|
||||
return;
|
||||
}
|
||||
updates.add(new Update(false, c));
|
||||
if (state == State.WAITING_FOR_CHANGES) {
|
||||
state = State.UPDATING_CONTACT_LIST;
|
||||
ioExecutor.execute(this::updateContactList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An update that should be applied to the remote contact list.
|
||||
*/
|
||||
private static class Update {
|
||||
|
||||
/**
|
||||
* True if the contact should be added, false if the contact should be
|
||||
* removed.
|
||||
*/
|
||||
private final boolean add;
|
||||
private final ContactId contactId;
|
||||
|
||||
private Update(boolean add, ContactId contactId) {
|
||||
this.add = add;
|
||||
this.contactId = contactId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.Collections.shuffle;
|
||||
import static java.util.logging.Level.INFO;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class OwnMailboxDownloadWorker extends MailboxDownloadWorker {
|
||||
|
||||
/**
|
||||
* The maximum number of files that will be downloaded before checking
|
||||
* again for folders with available files. This ensures that if a file
|
||||
* arrives during a download cycle, its folder will be checked within a
|
||||
* reasonable amount of time even if some other folder has a very large
|
||||
* number of files.
|
||||
* <p>
|
||||
* Package access for testing.
|
||||
*/
|
||||
static final int MAX_ROUND_ROBIN_FILES = 1000;
|
||||
|
||||
OwnMailboxDownloadWorker(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor torReachabilityMonitor,
|
||||
MailboxApiCaller mailboxApiCaller,
|
||||
MailboxApi mailboxApi,
|
||||
MailboxFileManager mailboxFileManager,
|
||||
MailboxProperties mailboxProperties) {
|
||||
super(connectivityChecker, torReachabilityMonitor, mailboxApiCaller,
|
||||
mailboxApi, mailboxFileManager, mailboxProperties);
|
||||
if (!mailboxProperties.isOwner()) throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ApiCall createApiCallForDownloadCycle() {
|
||||
return new SimpleApiCall(this::apiCallListFolders);
|
||||
}
|
||||
|
||||
private void apiCallListFolders() throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
}
|
||||
LOG.info("Listing folders with available files");
|
||||
List<MailboxFolderId> folders =
|
||||
mailboxApi.getFolders(mailboxProperties);
|
||||
if (folders.isEmpty()) onDownloadCycleFinished();
|
||||
else listNextFolder(new LinkedList<>(folders), new HashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the next folder from `queue` and starts a task to list the
|
||||
* files in the folder and add them to `available`.
|
||||
*/
|
||||
private void listNextFolder(Queue<MailboxFolderId> queue,
|
||||
Map<MailboxFolderId, Queue<MailboxFile>> available) {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
MailboxFolderId folder = queue.remove();
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(new SimpleApiCall(() ->
|
||||
apiCallListFolder(folder, queue, available)));
|
||||
}
|
||||
}
|
||||
|
||||
private void apiCallListFolder(MailboxFolderId folder,
|
||||
Queue<MailboxFolderId> queue,
|
||||
Map<MailboxFolderId, Queue<MailboxFile>> available)
|
||||
throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
}
|
||||
LOG.info("Listing folder");
|
||||
try {
|
||||
List<MailboxFile> files =
|
||||
mailboxApi.getFiles(mailboxProperties, folder);
|
||||
if (!files.isEmpty()) {
|
||||
available.put(folder, new LinkedList<>(files));
|
||||
}
|
||||
} catch (TolerableFailureException e) {
|
||||
LOG.warning("Folder does not exist");
|
||||
}
|
||||
if (queue.isEmpty()) {
|
||||
LOG.info("Finished listing folders");
|
||||
if (available.isEmpty()) onDownloadCycleFinished();
|
||||
else createDownloadQueue(available);
|
||||
} else {
|
||||
listNextFolder(queue, available);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits the given folders in round-robin order to create a queue of up to
|
||||
* {@link #MAX_ROUND_ROBIN_FILES} to download.
|
||||
*/
|
||||
private void createDownloadQueue(
|
||||
Map<MailboxFolderId, Queue<MailboxFile>> available) {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
}
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info(available.size() + " folders have available files");
|
||||
}
|
||||
Queue<FolderFile> queue = createRoundRobinQueue(available);
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Downloading " + queue.size() + " files");
|
||||
}
|
||||
downloadNextFile(queue);
|
||||
}
|
||||
|
||||
// Package access for testing
|
||||
Queue<FolderFile> createRoundRobinQueue(
|
||||
Map<MailboxFolderId, Queue<MailboxFile>> available) {
|
||||
List<MailboxFolderId> roundRobin = new ArrayList<>(available.keySet());
|
||||
// Shuffle the folders so we don't always favour the same folders
|
||||
shuffle(roundRobin);
|
||||
Queue<FolderFile> queue = new LinkedList<>();
|
||||
while (queue.size() < MAX_ROUND_ROBIN_FILES && !available.isEmpty()) {
|
||||
Iterator<MailboxFolderId> it = roundRobin.iterator();
|
||||
while (queue.size() < MAX_ROUND_ROBIN_FILES && it.hasNext()) {
|
||||
MailboxFolderId folder = it.next();
|
||||
Queue<MailboxFile> files = available.get(folder);
|
||||
MailboxFile file = files.remove();
|
||||
queue.add(new FolderFile(folder, file.name));
|
||||
if (files.isEmpty()) {
|
||||
available.remove(folder);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
return queue;
|
||||
}
|
||||
}
|
||||
@@ -16,17 +16,20 @@ import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
* Convenience class for making simple API calls that don't return values.
|
||||
*/
|
||||
@NotNullByDefault
|
||||
public abstract class SimpleApiCall implements ApiCall {
|
||||
class SimpleApiCall implements ApiCall {
|
||||
|
||||
private static final Logger LOG = getLogger(SimpleApiCall.class.getName());
|
||||
|
||||
abstract void tryToCallApi()
|
||||
throws IOException, ApiException, TolerableFailureException;
|
||||
private final Attempt attempt;
|
||||
|
||||
SimpleApiCall(Attempt attempt) {
|
||||
this.attempt = attempt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean callApi() {
|
||||
try {
|
||||
tryToCallApi();
|
||||
attempt.tryToCallApi();
|
||||
return false; // Succeeded, don't retry
|
||||
} catch (IOException | ApiException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
@@ -36,4 +39,17 @@ public abstract class SimpleApiCall implements ApiCall {
|
||||
return false; // Failed tolerably, don't retry
|
||||
}
|
||||
}
|
||||
|
||||
interface Attempt {
|
||||
|
||||
/**
|
||||
* Makes a single attempt to call an API endpoint. If this method
|
||||
* throws an {@link IOException} or an {@link ApiException}, the call
|
||||
* will be retried. If it throws a {@link TolerableFailureException}
|
||||
* or returns without throwing an exception, the call will not be
|
||||
* retried.
|
||||
*/
|
||||
void tryToCallApi()
|
||||
throws IOException, ApiException, TolerableFailureException;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,12 @@ interface TorReachabilityMonitor {
|
||||
*/
|
||||
void addOneShotObserver(TorReachabilityObserver o);
|
||||
|
||||
/**
|
||||
* Removes an observer that was added via
|
||||
* {@link #addOneShotObserver(TorReachabilityObserver)}.
|
||||
*/
|
||||
void removeObserver(TorReachabilityObserver o);
|
||||
|
||||
interface TorReachabilityObserver {
|
||||
|
||||
void onTorReachable();
|
||||
|
||||
@@ -87,6 +87,14 @@ class TorReachabilityMonitorImpl
|
||||
if (callNow) o.onTorReachable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeObserver(TorReachabilityObserver o) {
|
||||
synchronized (lock) {
|
||||
if (destroyed) return;
|
||||
observers.remove(o);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof TransportActiveEvent) {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
/**
|
||||
* An interface for converting an onion address to an HTTP URL, allowing the
|
||||
* conversion to be customised for integration tests.
|
||||
*/
|
||||
@NotNullByDefault
|
||||
interface UrlConverter {
|
||||
|
||||
/**
|
||||
* Converts a raw onion address, excluding the .onion suffix, into an
|
||||
* HTTP URL.
|
||||
*/
|
||||
String convertOnionToBaseUrl(String onion);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@NotNullByDefault
|
||||
class UrlConverterImpl implements UrlConverter {
|
||||
|
||||
@Inject
|
||||
UrlConverterImpl() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertOnionToBaseUrl(String onion) {
|
||||
return "http://" + onion + ".onion";
|
||||
}
|
||||
}
|
||||
@@ -427,7 +427,13 @@ abstract class AbstractBluetoothPlugin<S, SS> implements BluetoothPlugin,
|
||||
BdfList descriptor = new BdfList();
|
||||
descriptor.add(TRANSPORT_ID_BLUETOOTH);
|
||||
String address = getBluetoothAddress();
|
||||
if (address != null) descriptor.add(macToBytes(address));
|
||||
if (address != null) {
|
||||
try {
|
||||
descriptor.add(macToBytes(address));
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
return new BluetoothKeyAgreementListener(descriptor, ss);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,14 +9,12 @@ import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.DAYS;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.ID;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.MAX_LATENCY;
|
||||
|
||||
@NotNullByDefault
|
||||
public class MailboxPluginFactory implements SimplexPluginFactory {
|
||||
|
||||
private static final long MAX_LATENCY = DAYS.toMillis(14);
|
||||
|
||||
@Inject
|
||||
MailboxPluginFactory() {
|
||||
}
|
||||
|
||||
@@ -106,7 +106,8 @@ class RemovableDriveManagerImpl
|
||||
@Override
|
||||
public boolean isWriterTaskNeeded(ContactId c) throws DbException {
|
||||
return db.transactionWithResult(true, txn ->
|
||||
db.containsAnythingToSend(txn, c, MAX_LATENCY, true));
|
||||
db.containsAcksToSend(txn, c) ||
|
||||
db.containsMessagesToSend(txn, c, MAX_LATENCY, true));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -285,7 +285,7 @@ class LanTcpPlugin extends TcpPlugin {
|
||||
if (ip.length == 16) addrs.add(InetAddress.getByAddress(ip));
|
||||
}
|
||||
return addrs;
|
||||
} catch (IllegalArgumentException | UnknownHostException e) {
|
||||
} catch (FormatException | UnknownHostException e) {
|
||||
return emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public interface CircumventionProvider {
|
||||
* Countries where bridge connections are likely to work.
|
||||
* Should be a subset of {@link #BLOCKED} and the union of
|
||||
* {@link #DEFAULT_BRIDGES}, {@link #NON_DEFAULT_BRIDGES} and
|
||||
* {@link #MEEK_BRIDGES}.
|
||||
* {@link #DPI_BRIDGES}.
|
||||
*/
|
||||
String[] BRIDGES = {"BY", "CN", "EG", "IR", "RU", "TM", "VE"};
|
||||
|
||||
@@ -44,10 +44,10 @@ public interface CircumventionProvider {
|
||||
String[] NON_DEFAULT_BRIDGES = {"BY", "RU", "TM"};
|
||||
|
||||
/**
|
||||
* Countries where obfs4 and vanilla bridges won't work and meek is needed.
|
||||
* Should be a subset of {@link #BRIDGES}.
|
||||
* Countries where vanilla bridges are blocked via DPI but non-default
|
||||
* obfs4 bridges and meek may work. Should be a subset of {@link #BRIDGES}.
|
||||
*/
|
||||
String[] MEEK_BRIDGES = {"CN", "IR"};
|
||||
String[] DPI_BRIDGES = {"CN", "IR"};
|
||||
|
||||
/**
|
||||
* Returns true if vanilla Tor connections are blocked in the given country.
|
||||
|
||||
@@ -14,7 +14,6 @@ import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.DEFAULT_OBFS4;
|
||||
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK;
|
||||
@@ -35,8 +34,8 @@ class CircumventionProviderImpl implements CircumventionProvider {
|
||||
new HashSet<>(asList(DEFAULT_BRIDGES));
|
||||
private static final Set<String> NON_DEFAULT_BRIDGE_COUNTRIES =
|
||||
new HashSet<>(asList(NON_DEFAULT_BRIDGES));
|
||||
private static final Set<String> MEEK_COUNTRIES =
|
||||
new HashSet<>(asList(MEEK_BRIDGES));
|
||||
private static final Set<String> DPI_COUNTRIES =
|
||||
new HashSet<>(asList(DPI_BRIDGES));
|
||||
|
||||
@Inject
|
||||
CircumventionProviderImpl() {
|
||||
@@ -58,8 +57,8 @@ class CircumventionProviderImpl implements CircumventionProvider {
|
||||
return asList(DEFAULT_OBFS4, VANILLA);
|
||||
} else if (NON_DEFAULT_BRIDGE_COUNTRIES.contains(countryCode)) {
|
||||
return asList(NON_DEFAULT_OBFS4, VANILLA);
|
||||
} else if (MEEK_COUNTRIES.contains(countryCode)) {
|
||||
return singletonList(MEEK);
|
||||
} else if (DPI_COUNTRIES.contains(countryCode)) {
|
||||
return asList(NON_DEFAULT_OBFS4, MEEK);
|
||||
} else {
|
||||
return asList(DEFAULT_OBFS4, VANILLA);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static java.util.logging.Level.INFO;
|
||||
@@ -93,9 +94,7 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V3;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_BATTERY;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_COUNTRY_BLOCKED;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_MOBILE_DATA;
|
||||
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.DEFAULT_OBFS4;
|
||||
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK;
|
||||
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4;
|
||||
import static org.briarproject.bramble.plugin.tor.TorRendezvousCrypto.SEED_BYTES;
|
||||
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
|
||||
import static org.briarproject.bramble.util.IoUtils.tryToClose;
|
||||
@@ -107,7 +106,7 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
@ParametersNotNullByDefault
|
||||
abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
|
||||
private static final Logger LOG = getLogger(TorPlugin.class.getName());
|
||||
protected static final Logger LOG = getLogger(TorPlugin.class.getName());
|
||||
|
||||
private static final String[] EVENTS = {
|
||||
"CIRC",
|
||||
@@ -124,7 +123,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
private static final int COOKIE_POLLING_INTERVAL_MS = 200;
|
||||
private static final Pattern ONION_V3 = Pattern.compile("[a-z2-7]{56}");
|
||||
|
||||
private final Executor ioExecutor, wakefulIoExecutor;
|
||||
protected final Executor ioExecutor;
|
||||
private final Executor wakefulIoExecutor;
|
||||
private final Executor connectionStatusExecutor;
|
||||
private final NetworkManager networkManager;
|
||||
private final LocationUtils locationUtils;
|
||||
@@ -238,8 +238,14 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
// Load the settings
|
||||
settings = callback.getSettings();
|
||||
// Install or update the assets if necessary
|
||||
if (!assetsAreUpToDate()) installAssets();
|
||||
try {
|
||||
// Install or update the assets if necessary
|
||||
if (!assetsAreUpToDate()) installAssets();
|
||||
// Start from the default config every time
|
||||
extract(getConfigInputStream(), configFile);
|
||||
} catch (IOException e) {
|
||||
throw new PluginException(e);
|
||||
}
|
||||
if (cookieFile.exists() && !cookieFile.delete())
|
||||
LOG.warning("Old auth cookie not deleted");
|
||||
// Start a new Tor process
|
||||
@@ -254,34 +260,15 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
Map<String, String> env = pb.environment();
|
||||
env.put("HOME", torDirectory.getAbsolutePath());
|
||||
pb.directory(torDirectory);
|
||||
pb.redirectErrorStream(true);
|
||||
try {
|
||||
torProcess = pb.start();
|
||||
} catch (SecurityException | IOException e) {
|
||||
throw new PluginException(e);
|
||||
}
|
||||
// Log the process's standard output until it detaches
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
Scanner stdout = new Scanner(torProcess.getInputStream());
|
||||
Scanner stderr = new Scanner(torProcess.getErrorStream());
|
||||
while (stdout.hasNextLine() || stderr.hasNextLine()) {
|
||||
if (stdout.hasNextLine()) {
|
||||
LOG.info(stdout.nextLine());
|
||||
}
|
||||
if (stderr.hasNextLine()) {
|
||||
LOG.info(stderr.nextLine());
|
||||
}
|
||||
}
|
||||
stdout.close();
|
||||
stderr.close();
|
||||
}
|
||||
try {
|
||||
// Wait for the process to detach or exit
|
||||
int exit = torProcess.waitFor();
|
||||
if (exit != 0) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.warning("Tor exited with value " + exit);
|
||||
throw new PluginException();
|
||||
}
|
||||
// Wait for the Tor process to start
|
||||
waitForTorToStart(torProcess);
|
||||
// Wait for the auth cookie file to be created/updated
|
||||
long start = clock.currentTimeMillis();
|
||||
while (cookieFile.length() < 32) {
|
||||
@@ -320,7 +307,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
info = controlConnection.getInfo("status/circuit-established");
|
||||
if ("1".equals(info)) {
|
||||
LOG.info("Tor has already built a circuit");
|
||||
state.getAndSetCircuitBuilt(true);
|
||||
state.setCircuitBuilt(true);
|
||||
}
|
||||
} catch (TorNotRunningException e) {
|
||||
throw new RuntimeException(e);
|
||||
@@ -339,25 +326,20 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
return doneFile.lastModified() > getLastUpdateTime();
|
||||
}
|
||||
|
||||
private void installAssets() throws PluginException {
|
||||
try {
|
||||
// The done file may already exist from a previous installation
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
doneFile.delete();
|
||||
// The GeoIP file may exist from a previous installation - we can
|
||||
// save some space by deleting it.
|
||||
// TODO: Remove after a reasonable migration period
|
||||
// (added 2022-03-29)
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
geoIpFile.delete();
|
||||
installTorExecutable();
|
||||
installObfs4Executable();
|
||||
extract(getConfigInputStream(), configFile);
|
||||
if (!doneFile.createNewFile())
|
||||
LOG.warning("Failed to create done file");
|
||||
} catch (IOException e) {
|
||||
throw new PluginException(e);
|
||||
}
|
||||
private void installAssets() throws IOException {
|
||||
// The done file may already exist from a previous installation
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
doneFile.delete();
|
||||
// The GeoIP file may exist from a previous installation - we can
|
||||
// save some space by deleting it.
|
||||
// TODO: Remove after a reasonable migration period
|
||||
// (added 2022-03-29)
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
geoIpFile.delete();
|
||||
installTorExecutable();
|
||||
installObfs4Executable();
|
||||
if (!doneFile.createNewFile())
|
||||
LOG.warning("Failed to create done file");
|
||||
}
|
||||
|
||||
protected void extract(InputStream in, File dest) throws IOException {
|
||||
@@ -397,7 +379,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
return zin;
|
||||
}
|
||||
|
||||
private static void append(StringBuilder strb, String name, int value) {
|
||||
private static void append(StringBuilder strb, String name, Object value) {
|
||||
strb.append(name);
|
||||
strb.append(" ");
|
||||
strb.append(value);
|
||||
@@ -405,13 +387,21 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
|
||||
private InputStream getConfigInputStream() {
|
||||
File dataDirectory = new File(torDirectory, ".tor");
|
||||
StringBuilder strb = new StringBuilder();
|
||||
append(strb, "ControlPort", torControlPort);
|
||||
append(strb, "CookieAuthentication", 1);
|
||||
append(strb, "DataDirectory", dataDirectory.getAbsolutePath());
|
||||
append(strb, "DisableNetwork", 1);
|
||||
append(strb, "RunAsDaemon", 1);
|
||||
append(strb, "SafeSocks", 1);
|
||||
append(strb, "SocksPort", torSocksPort);
|
||||
strb.append("GeoIPFile\n");
|
||||
strb.append("GeoIPv6File\n");
|
||||
append(strb, "ConnectionPadding", 0);
|
||||
String obfs4Path = getObfs4ExecutableFile().getAbsolutePath();
|
||||
append(strb, "ClientTransportPlugin obfs4 exec", obfs4Path);
|
||||
append(strb, "ClientTransportPlugin meek_lite exec", obfs4Path);
|
||||
//noinspection CharsetObjectCanBeUsed
|
||||
return new ByteArrayInputStream(
|
||||
strb.toString().getBytes(Charset.forName("UTF-8")));
|
||||
@@ -442,6 +432,23 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
}
|
||||
|
||||
protected void waitForTorToStart(Process torProcess)
|
||||
throws InterruptedException, PluginException {
|
||||
Scanner stdout = new Scanner(torProcess.getInputStream());
|
||||
// Log the first line of stdout (contains Tor and library versions)
|
||||
if (stdout.hasNextLine()) LOG.info(stdout.nextLine());
|
||||
// Read the process's stdout (and redirected stderr) until it detaches
|
||||
while (stdout.hasNextLine()) stdout.nextLine();
|
||||
stdout.close();
|
||||
// Wait for the process to detach or exit
|
||||
int exit = torProcess.waitFor();
|
||||
if (exit != 0) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.warning("Tor exited with value " + exit);
|
||||
throw new PluginException();
|
||||
}
|
||||
}
|
||||
|
||||
private void bind() {
|
||||
ioExecutor.execute(() -> {
|
||||
// If there's already a port number stored in config, reuse it
|
||||
@@ -544,7 +551,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
|
||||
protected void enableNetwork(boolean enable) throws IOException {
|
||||
state.enableNetwork(enable);
|
||||
if (!state.enableNetwork(enable)) return; // Unchanged
|
||||
try {
|
||||
controlConnection.setConf("DisableNetwork", enable ? "0" : "1");
|
||||
} catch (TorNotRunningException e) {
|
||||
@@ -552,28 +559,20 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
}
|
||||
|
||||
private void enableBridges(boolean enable, List<BridgeType> bridgeTypes)
|
||||
private void enableBridges(List<BridgeType> bridgeTypes)
|
||||
throws IOException {
|
||||
if (!state.setBridgeTypes(bridgeTypes)) return; // Unchanged
|
||||
try {
|
||||
if (enable) {
|
||||
if (bridgeTypes.isEmpty()) {
|
||||
controlConnection.setConf("UseBridges", "0");
|
||||
controlConnection.resetConf(singletonList("Bridge"));
|
||||
} else {
|
||||
Collection<String> conf = new ArrayList<>();
|
||||
conf.add("UseBridges 1");
|
||||
File obfs4File = getObfs4ExecutableFile();
|
||||
if (bridgeTypes.contains(MEEK)) {
|
||||
conf.add("ClientTransportPlugin meek_lite exec " +
|
||||
obfs4File.getAbsolutePath());
|
||||
}
|
||||
if (bridgeTypes.contains(DEFAULT_OBFS4) ||
|
||||
bridgeTypes.contains(NON_DEFAULT_OBFS4)) {
|
||||
conf.add("ClientTransportPlugin obfs4 exec " +
|
||||
obfs4File.getAbsolutePath());
|
||||
}
|
||||
for (BridgeType bridgeType : bridgeTypes) {
|
||||
conf.addAll(circumventionProvider.getBridges(bridgeType));
|
||||
}
|
||||
controlConnection.setConf(conf);
|
||||
} else {
|
||||
controlConnection.setConf("UseBridges", "0");
|
||||
}
|
||||
} catch (TorNotRunningException e) {
|
||||
throw new RuntimeException(e);
|
||||
@@ -587,7 +586,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
if (controlSocket != null && controlConnection != null) {
|
||||
try {
|
||||
LOG.info("Stopping Tor");
|
||||
controlConnection.setConf("DisableNetwork", "1");
|
||||
controlConnection.shutdownTor("TERM");
|
||||
controlSocket.close();
|
||||
} catch (TorNotRunningException e) {
|
||||
@@ -755,7 +753,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
public void circuitStatus(String status, String id, String path) {
|
||||
// In case of races between receiving CIRCUIT_ESTABLISHED and setting
|
||||
// DisableNetwork, set our circuitBuilt flag if not already set
|
||||
if (status.equals("BUILT") && !state.getAndSetCircuitBuilt(true)) {
|
||||
if (status.equals("BUILT") && state.setCircuitBuilt(true)) {
|
||||
LOG.info("Circuit built");
|
||||
backoff.reset();
|
||||
}
|
||||
@@ -802,6 +800,13 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void controlConnectionClosed() {
|
||||
if (state.isTorRunning()) {
|
||||
throw new RuntimeException("Control connection closed");
|
||||
}
|
||||
}
|
||||
|
||||
private String removeSeverity(String msg) {
|
||||
return msg.replaceFirst("[^ ]+ ", "");
|
||||
}
|
||||
@@ -812,12 +817,12 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
state.setBootstrapped();
|
||||
backoff.reset();
|
||||
} else if (msg.startsWith("CIRCUIT_ESTABLISHED")) {
|
||||
if (!state.getAndSetCircuitBuilt(true)) {
|
||||
if (state.setCircuitBuilt(true)) {
|
||||
LOG.info("Circuit built");
|
||||
backoff.reset();
|
||||
}
|
||||
} else if (msg.startsWith("CIRCUIT_NOT_ESTABLISHED")) {
|
||||
if (state.getAndSetCircuitBuilt(false)) {
|
||||
if (state.setCircuitBuilt(false)) {
|
||||
LOG.info("Circuit not built");
|
||||
// TODO: Disable and re-enable network to prompt Tor to rebuild
|
||||
// its guard/bridge connections? This will also close any
|
||||
@@ -908,10 +913,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
|
||||
int reasonsDisabled = 0;
|
||||
boolean enableNetwork = false, enableBridges = false;
|
||||
boolean enableConnectionPadding = false;
|
||||
List<BridgeType> bridgeTypes =
|
||||
circumventionProvider.getSuitableBridgeTypes(country);
|
||||
boolean enableNetwork = false, enableConnectionPadding = false;
|
||||
List<BridgeType> bridgeTypes = emptyList();
|
||||
|
||||
if (!online) {
|
||||
LOG.info("Disabling network, device is offline");
|
||||
@@ -940,8 +943,12 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
enableNetwork = true;
|
||||
if (network == PREF_TOR_NETWORK_WITH_BRIDGES ||
|
||||
(automatic && bridgesWork)) {
|
||||
if (ipv6Only) bridgeTypes = singletonList(MEEK);
|
||||
enableBridges = true;
|
||||
if (ipv6Only) {
|
||||
bridgeTypes = singletonList(MEEK);
|
||||
} else {
|
||||
bridgeTypes = circumventionProvider
|
||||
.getSuitableBridgeTypes(country);
|
||||
}
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Using bridge types " + bridgeTypes);
|
||||
}
|
||||
@@ -961,9 +968,9 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
|
||||
try {
|
||||
if (enableNetwork) {
|
||||
enableBridges(enableBridges, bridgeTypes);
|
||||
enableBridges(bridgeTypes);
|
||||
enableConnectionPadding(enableConnectionPadding);
|
||||
useIpv6(ipv6Only);
|
||||
enableIpv6(ipv6Only);
|
||||
}
|
||||
enableNetwork(enableNetwork);
|
||||
} catch (IOException e) {
|
||||
@@ -973,6 +980,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
|
||||
private void enableConnectionPadding(boolean enable) throws IOException {
|
||||
if (!state.enableConnectionPadding(enable)) return; // Unchanged
|
||||
try {
|
||||
controlConnection.setConf("ConnectionPadding", enable ? "1" : "0");
|
||||
} catch (TorNotRunningException e) {
|
||||
@@ -980,10 +988,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
}
|
||||
|
||||
private void useIpv6(boolean ipv6Only) throws IOException {
|
||||
private void enableIpv6(boolean enable) throws IOException {
|
||||
if (!state.enableIpv6(enable)) return; // Unchanged
|
||||
try {
|
||||
controlConnection.setConf("ClientUseIPv4", ipv6Only ? "0" : "1");
|
||||
controlConnection.setConf("ClientUseIPv6", ipv6Only ? "1" : "0");
|
||||
controlConnection.setConf("ClientUseIPv4", enable ? "0" : "1");
|
||||
controlConnection.setConf("ClientUseIPv6", enable ? "1" : "0");
|
||||
} catch (TorNotRunningException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -998,6 +1007,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
stopped = false,
|
||||
networkInitialised = false,
|
||||
networkEnabled = false,
|
||||
paddingEnabled = false,
|
||||
ipv6Enabled = false,
|
||||
bootstrapped = false,
|
||||
circuitBuilt = false,
|
||||
settingsChecked = false;
|
||||
@@ -1012,6 +1023,9 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
@GuardedBy("this")
|
||||
private int orConnectionsConnected = 0;
|
||||
|
||||
@GuardedBy("this")
|
||||
private List<BridgeType> bridgeTypes = emptyList();
|
||||
|
||||
private synchronized void setStarted() {
|
||||
started = true;
|
||||
callback.pluginStateChanged(getState());
|
||||
@@ -1032,28 +1046,66 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
|
||||
private synchronized void setBootstrapped() {
|
||||
boolean wasBootstrapped = bootstrapped;
|
||||
bootstrapped = true;
|
||||
callback.pluginStateChanged(getState());
|
||||
if (!wasBootstrapped) callback.pluginStateChanged(getState());
|
||||
}
|
||||
|
||||
private synchronized boolean getAndSetCircuitBuilt(boolean built) {
|
||||
boolean old = circuitBuilt;
|
||||
/**
|
||||
* Sets the `circuitBuilt` flag and returns true if the flag has
|
||||
* changed.
|
||||
*/
|
||||
private synchronized boolean setCircuitBuilt(boolean built) {
|
||||
if (built == circuitBuilt) return false; // Unchanged
|
||||
circuitBuilt = built;
|
||||
if (built != old) callback.pluginStateChanged(getState());
|
||||
return old;
|
||||
callback.pluginStateChanged(getState());
|
||||
return true; // Changed
|
||||
}
|
||||
|
||||
private synchronized void enableNetwork(boolean enable) {
|
||||
/**
|
||||
* Sets the `networkEnabled` flag and returns true if the flag has
|
||||
* changed.
|
||||
*/
|
||||
private synchronized boolean enableNetwork(boolean enable) {
|
||||
boolean wasInitialised = networkInitialised;
|
||||
boolean wasEnabled = networkEnabled;
|
||||
networkInitialised = true;
|
||||
networkEnabled = enable;
|
||||
if (!enable) circuitBuilt = false;
|
||||
callback.pluginStateChanged(getState());
|
||||
if (!wasInitialised || enable != wasEnabled) {
|
||||
callback.pluginStateChanged(getState());
|
||||
}
|
||||
return enable != wasEnabled;
|
||||
}
|
||||
|
||||
private synchronized void setReasonsDisabled(int reasonsDisabled) {
|
||||
/**
|
||||
* Sets the `paddingEnabled` flag and returns true if the flag has
|
||||
* changed. Doesn't affect getState().
|
||||
*/
|
||||
private synchronized boolean enableConnectionPadding(boolean enable) {
|
||||
if (enable == paddingEnabled) return false; // Unchanged
|
||||
paddingEnabled = enable;
|
||||
return true; // Changed
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the `ipv6Enabled` flag and returns true if the flag has
|
||||
* changed. Doesn't affect getState().
|
||||
*/
|
||||
private synchronized boolean enableIpv6(boolean enable) {
|
||||
if (enable == ipv6Enabled) return false; // Unchanged
|
||||
ipv6Enabled = enable;
|
||||
return true; // Changed
|
||||
}
|
||||
|
||||
private synchronized void setReasonsDisabled(int reasons) {
|
||||
boolean wasChecked = settingsChecked;
|
||||
settingsChecked = true;
|
||||
this.reasonsDisabled = reasonsDisabled;
|
||||
callback.pluginStateChanged(getState());
|
||||
int oldReasons = reasonsDisabled;
|
||||
reasonsDisabled = reasons;
|
||||
if (!wasChecked || reasons != oldReasons) {
|
||||
callback.pluginStateChanged(getState());
|
||||
}
|
||||
}
|
||||
|
||||
// Doesn't affect getState()
|
||||
@@ -1068,6 +1120,17 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
if (serverSocket == ss) serverSocket = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of bridge types being used and returns true if the
|
||||
* list has changed. The list is empty if bridges are disabled.
|
||||
* Doesn't affect getState().
|
||||
*/
|
||||
private synchronized boolean setBridgeTypes(List<BridgeType> types) {
|
||||
if (types.equals(bridgeTypes)) return false; // Unchanged
|
||||
bridgeTypes = types;
|
||||
return true; // Changed
|
||||
}
|
||||
|
||||
private synchronized State getState() {
|
||||
if (!started || stopped || !settingsChecked) {
|
||||
return STARTING_STOPPING;
|
||||
|
||||
@@ -50,6 +50,7 @@ import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
|
||||
import static org.briarproject.bramble.api.record.Record.RECORD_HEADER_BYTES;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_LENGTH;
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.SUPPORTED_VERSIONS;
|
||||
@@ -235,8 +236,10 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
||||
generateOffer();
|
||||
} else if (e instanceof GroupVisibilityUpdatedEvent) {
|
||||
GroupVisibilityUpdatedEvent g = (GroupVisibilityUpdatedEvent) e;
|
||||
if (g.getAffectedContacts().contains(contactId))
|
||||
if (g.getVisibility() == SHARED &&
|
||||
g.getAffectedContacts().contains(contactId)) {
|
||||
generateOffer();
|
||||
}
|
||||
} else if (e instanceof MessageRequestedEvent) {
|
||||
if (((MessageRequestedEvent) e).getContactId().equals(contactId))
|
||||
generateBatch();
|
||||
@@ -310,7 +313,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
||||
Collection<Message> batch =
|
||||
db.generateRequestedBatch(txn, contactId,
|
||||
BATCH_CAPACITY, maxLatency);
|
||||
setNextSendTime(db.getNextSendTime(txn, contactId));
|
||||
setNextSendTime(db.getNextSendTime(txn, contactId,
|
||||
maxLatency));
|
||||
return batch;
|
||||
});
|
||||
if (LOG.isLoggable(INFO))
|
||||
@@ -353,7 +357,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
||||
Offer o = db.transactionWithNullableResult(false, txn -> {
|
||||
Offer offer = db.generateOffer(txn, contactId,
|
||||
MAX_MESSAGE_IDS, maxLatency);
|
||||
setNextSendTime(db.getNextSendTime(txn, contactId));
|
||||
setNextSendTime(db.getNextSendTime(txn, contactId,
|
||||
maxLatency));
|
||||
return offer;
|
||||
});
|
||||
if (LOG.isLoggable(INFO))
|
||||
|
||||
@@ -7,9 +7,9 @@ import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.sync.Ack;
|
||||
import org.briarproject.bramble.api.sync.DeferredSendHandler;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
import org.briarproject.bramble.api.sync.SyncRecordWriter;
|
||||
import org.briarproject.bramble.api.transport.StreamWriter;
|
||||
|
||||
@@ -29,7 +29,7 @@ import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LEN
|
||||
|
||||
/**
|
||||
* A {@link SimplexOutgoingSession} for sending and acking messages via a
|
||||
* mailbox. The session uses a {@link DeferredSendHandler} to record the IDs
|
||||
* mailbox. The session uses a {@link OutgoingSessionRecord} to record the IDs
|
||||
* of the messages sent and acked during the session so that they can be
|
||||
* recorded in the DB as sent or acked after the file has been successfully
|
||||
* uploaded to the mailbox.
|
||||
@@ -41,7 +41,7 @@ class MailboxOutgoingSession extends SimplexOutgoingSession {
|
||||
private static final Logger LOG =
|
||||
getLogger(MailboxOutgoingSession.class.getName());
|
||||
|
||||
private final DeferredSendHandler deferredSendHandler;
|
||||
private final OutgoingSessionRecord sessionRecord;
|
||||
private final long initialCapacity;
|
||||
|
||||
MailboxOutgoingSession(DatabaseComponent db,
|
||||
@@ -51,11 +51,11 @@ class MailboxOutgoingSession extends SimplexOutgoingSession {
|
||||
long maxLatency,
|
||||
StreamWriter streamWriter,
|
||||
SyncRecordWriter recordWriter,
|
||||
DeferredSendHandler deferredSendHandler,
|
||||
OutgoingSessionRecord sessionRecord,
|
||||
long capacity) {
|
||||
super(db, eventBus, contactId, transportId, maxLatency, streamWriter,
|
||||
recordWriter);
|
||||
this.deferredSendHandler = deferredSendHandler;
|
||||
this.sessionRecord = sessionRecord;
|
||||
this.initialCapacity = capacity;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class MailboxOutgoingSession extends SimplexOutgoingSession {
|
||||
Collection<MessageId> idsToAck = loadMessageIdsToAck();
|
||||
if (idsToAck.isEmpty()) break;
|
||||
recordWriter.writeAck(new Ack(idsToAck));
|
||||
deferredSendHandler.onAckSent(idsToAck);
|
||||
sessionRecord.onAckSent(idsToAck);
|
||||
LOG.info("Sent ack");
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ class MailboxOutgoingSession extends SimplexOutgoingSession {
|
||||
db.getMessageToSend(txn, contactId, m, maxLatency, false));
|
||||
if (message == null) continue; // No longer shared
|
||||
recordWriter.writeMessage(message);
|
||||
deferredSendHandler.onMessageSent(m);
|
||||
sessionRecord.onMessageSent(m);
|
||||
LOG.info("Sent message");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
import org.briarproject.bramble.api.sync.Priority;
|
||||
import org.briarproject.bramble.api.sync.PriorityHandler;
|
||||
import org.briarproject.bramble.api.sync.SyncRecordReader;
|
||||
@@ -25,6 +26,8 @@ import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.MAX_FILE_PAYLOAD_BYTES;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class SyncSessionFactoryImpl implements SyncSessionFactory {
|
||||
@@ -73,6 +76,18 @@ class SyncSessionFactoryImpl implements SyncSessionFactory {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SyncSession createSimplexOutgoingSession(ContactId c, TransportId t,
|
||||
long maxLatency, StreamWriter streamWriter,
|
||||
OutgoingSessionRecord sessionRecord) {
|
||||
OutputStream out = streamWriter.getOutputStream();
|
||||
SyncRecordWriter recordWriter =
|
||||
recordWriterFactory.createRecordWriter(out);
|
||||
return new MailboxOutgoingSession(db, eventBus, c, t, maxLatency,
|
||||
streamWriter, recordWriter, sessionRecord,
|
||||
MAX_FILE_PAYLOAD_BYTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SyncSession createDuplexOutgoingSession(ContactId c, TransportId t,
|
||||
long maxLatency, int maxIdleTime, StreamWriter streamWriter,
|
||||
|
||||
@@ -15,16 +15,19 @@ n Bridge obfs4 46.226.107.197:10300 A38FD6BDFD902882F5F5B9B7CCC95602A20B0BC4 cer
|
||||
n Bridge obfs4 185.181.11.86:443 A961609729E7FDF520B4E81F1F1B8FA1045285C3 cert=e5faG9Zk4Ni+e7z2YgGfevyKPQlMvkVGi4ublSsHYjaBovKeNXpOhbeFxzbZZoAzxAoGUQ iat-mode=0
|
||||
n Bridge obfs4 85.242.211.221:8042 A36A938DD7FDB8BACC846BA326EE0BA0D89A9252 cert=1AN6Pt1eFca3Y/WYD2TGAU3Al9cO4eouXE9SX63s66Z/ks3tVmgQ5GeXi1B5DOvx6Il7Zw iat-mode=0
|
||||
n Bridge obfs4 74.104.165.202:9002 EF432018A6AA5D970B2F84E39CD30A147030141C cert=PhppfUusY85dHGvWtGTybZ1fED4DtbHmALkNMIOIYrAz1B4xN7/2a5gyiZe1epju1BOHVg iat-mode=0
|
||||
n Bridge obfs4 70.34.242.31:443 7F026956402CDFF4BCBA8E11EE9C50E3FE0A2B72 cert=hP/KU7JATSfWH3HwS5Er/YLT0J+bRO3+s2fWx2yirrgf37EyrWvm/BQshoNje8WfUm6CBw iat-mode=0
|
||||
n Bridge obfs4 93.95.226.151:41185 460B0CFFC0CF1D965F3DE064E08BA1915E7C916A cert=inluPzp5Jp5OzZar1eQb4dcQ/YlAj/v0kHAUCoCr3rmLt03+pVuVTjoH4mRy4+acXpn+Gw iat-mode=0
|
||||
n Bridge obfs4 120.29.217.52:5223 40FE3DB9800272F9CDC76422F8ED7883280EE96D cert=/71PS4l8c/XJ4DIItlH9xMqNvPFg2RUTrHvPlQWh48u5et8h/yyyjCcYphUadDsfBWpaGQ iat-mode=0
|
||||
n Bridge obfs4 83.97.179.29:1199 D83068BFAA28E71DB024B786E1E803BE14257127 cert=IduGtt05tM59Xmvo0oVNWgIRgY4OGPJjFP+y2oa6RMDHQBL/GRyFOOgX70iiazNAIJNkPw iat-mode=0
|
||||
n Bridge obfs4 207.181.244.13:40132 37FE8D782F5DD2BAEEDAAB8257B701344676B6DD cert=f5Hbfn3ToMzH170cV8DfLly3vRynveidfOfDcbradIDtbLDX15V2yQ8eEH2CPKQJmQR2Hg iat-mode=0
|
||||
n Bridge obfs4 76.255.201.112:8888 96CF36C2ECCFB7376AB6BE905BECD2C2AE8AEFCD cert=+q0pjaiM0JMqHL/BKqCRD+pjflaw/S406eUDF7CnFgamvQW3l2HVLJhQ6uX9P8zff0PLGg iat-mode=0
|
||||
n Bridge obfs4 65.108.159.114:14174 E1AD374BA9F34BD98862D128AC54D40C7DC138AE cert=YMkxMSBN2OOd99AkJpFaEUyKkdqZgJt9oVJEgO1QnT37n/Vc2yR4nhx4k4VkPLfEP1f4eg iat-mode=0
|
||||
n Bridge obfs4 185.177.207.138:8443 53716FE26F23C8C6A71A2BC5D9D8DC64747278C7 cert=6jcYVilMEzxdsWghSrQFpYYJlkkir/GPIXw/EnddUK3S8ToVpMG8u1SwMOEdEs735RrMHw iat-mode=0
|
||||
n Bridge obfs4 176.123.2.253:1933 B855D141CE6C4DE0F7EA4AAED83EBA8373FA8191 cert=1rOoSaRagc6PPR//paIl+ukv1N+xWKCdBXMFxK0k/moEwH0lk5bURBrUDzIX35fVzaiicQ iat-mode=0
|
||||
n Bridge obfs4 5.252.176.61:9418 3E61130100AD827AB9CB33DAC052D9BC49A39509 cert=/aMyBPnKbQFISithD3i1KHUdpWeMpWG3SvUpq1YWCf2EQohFxQfw+646gW1knm4BI/DLRA iat-mode=0
|
||||
n Bridge obfs4 202.61.224.111:6902 A4F91299763DB925AE3BD29A0FC1A9821E5D9BAE cert=NBKm2MJ83wMvYShkqpD5RrbDtW5YpIZrFNnMw7Dj1XOM3plU60Bh4eziaQXe8fGtb8ZqKg iat-mode=0
|
||||
n Bridge obfs4 87.121.72.109:9002 C8081D4731C953FA4AE166946E72B29153351E34 cert=bikAqxKV6Ch5gFCBTdPI28VeShYa1ZgkLmvc7YZNLWFsFZoaCULL/3AQKjpIfvSiJs5jGQ iat-mode=0
|
||||
n Bridge obfs4 172.104.17.96:17900 B6B37AC96E163D0A5ECE55826D17B50B70F0A7F8 cert=gUz7svhPxoALgeN4lMYrXK7NBnaDqwu6SKRJOhaO9IIMBpnB8UhMCMKzzMho3b0RxWzBVg iat-mode=0
|
||||
n Bridge obfs4 70.34.249.113:443 F441B16ABB1055794C2CE01821FC05047B2C8CFC cert=MauLNoyq8EwjY4Qe0oASYzs2hXdSjNgy+BtP9oo1naHhRsyKTtAZzeNv08RnzWjMJrTwcg iat-mode=0
|
||||
v Bridge 135.181.113.164:54444 74AF4CCA614C454B7D3E81FF8BACD78CEBC7D7DE
|
||||
v Bridge 92.243.15.235:9001 477EAD3C04036B48235F1F27FC91420A286A4B7F
|
||||
v Bridge 77.96.91.103:443 ED000A75B79A58F1D83A4D1675C2A9395B71BE8E
|
||||
m Bridge meek_lite 192.0.2.2:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com
|
||||
v Bridge 213.108.108.145:17674 A39C0FE47963B6E8CFE9815549864DE544935A31
|
||||
m Bridge meek_lite 192.0.2.2:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.connection;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.connection.ConnectionRegistry;
|
||||
import org.briarproject.bramble.api.connection.InterruptibleConnection;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
@@ -57,6 +58,10 @@ public class ConnectionRegistryImplTest extends BrambleMockTestCase {
|
||||
private final Priority high =
|
||||
new Priority(fromHexString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"));
|
||||
|
||||
public ConnectionRegistryImplTest() throws FormatException {
|
||||
// required for throws declaration
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterMultipleConnections() {
|
||||
context.checking(new Expectations() {{
|
||||
|
||||
@@ -17,7 +17,6 @@ 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.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -63,13 +62,9 @@ public class ContactManagerImplTest extends BrambleMockTestCase {
|
||||
private final long timestamp = System.currentTimeMillis();
|
||||
private final boolean alice = new Random().nextBoolean();
|
||||
|
||||
private ContactManagerImpl contactManager;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
contactManager = new ContactManagerImpl(db, keyManager,
|
||||
identityManager, pendingContactFactory);
|
||||
}
|
||||
private final ContactManagerImpl contactManager =
|
||||
new ContactManagerImpl(db, keyManager, identityManager,
|
||||
pendingContactFactory);
|
||||
|
||||
@Test
|
||||
public void testAddContact() throws Exception {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.briarproject.bramble.crypto;
|
||||
|
||||
import org.bouncycastle.crypto.digests.Blake2bDigest;
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.test.BrambleTestCase;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
import org.junit.Test;
|
||||
@@ -47,7 +48,7 @@ public class Blake2bDigestTest extends BrambleTestCase {
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testDigestWithKeyedTestVectors() {
|
||||
public void testDigestWithKeyedTestVectors() throws FormatException {
|
||||
for (String[] keyedTestVector : KEYED_TEST_VECTORS) {
|
||||
byte[] input = StringUtils.fromHexString(keyedTestVector[0]);
|
||||
byte[] key = StringUtils.fromHexString(keyedTestVector[1]);
|
||||
@@ -63,7 +64,8 @@ public class Blake2bDigestTest extends BrambleTestCase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDigestWithKeyedTestVectorsAndRandomUpdate() {
|
||||
public void testDigestWithKeyedTestVectorsAndRandomUpdate()
|
||||
throws FormatException {
|
||||
Random random = new Random();
|
||||
for (int i = 0; i < 100; i++) {
|
||||
for (String[] keyedTestVector : KEYED_TEST_VECTORS) {
|
||||
@@ -138,7 +140,7 @@ public class Blake2bDigestTest extends BrambleTestCase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runSelfTest() {
|
||||
public void runSelfTest() throws FormatException {
|
||||
Blake2bDigest testDigest = new Blake2bDigest(256);
|
||||
byte[] md = new byte[64];
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.crypto;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.crypto.AgreementPublicKey;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.KeyPair;
|
||||
@@ -83,7 +84,7 @@ public class KeyAgreementTest extends BrambleTestCase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRfc7748TestVector() {
|
||||
public void testRfc7748TestVector() throws FormatException {
|
||||
byte[] aPriv = parsePrivateKey(ALICE_PRIVATE);
|
||||
byte[] aPub = fromHexString(ALICE_PUBLIC);
|
||||
byte[] bPriv = parsePrivateKey(BOB_PRIVATE);
|
||||
@@ -97,7 +98,8 @@ public class KeyAgreementTest extends BrambleTestCase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDerivesSameSharedSecretFromEquivalentPublicKey() {
|
||||
public void testDerivesSameSharedSecretFromEquivalentPublicKey()
|
||||
throws FormatException {
|
||||
byte[] aPub = fromHexString(ALICE_PUBLIC);
|
||||
byte[] bPriv = parsePrivateKey(BOB_PRIVATE);
|
||||
byte[] sharedSecret = fromHexString(SHARED_SECRET);
|
||||
@@ -168,7 +170,7 @@ public class KeyAgreementTest extends BrambleTestCase {
|
||||
return crypto.deriveSharedSecret(label, publicKey, keyPair, inputs);
|
||||
}
|
||||
|
||||
private byte[] parsePrivateKey(String hex) {
|
||||
private byte[] parsePrivateKey(String hex) throws FormatException {
|
||||
// Private keys need to be clamped because curve25519-java does the
|
||||
// clamping at key generation time, not multiplication time
|
||||
return AgreementKeyParser.clamp(fromHexString(hex));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.crypto;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.test.BrambleTestCase;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
@@ -15,11 +16,11 @@ public class XSalsa20Poly1305AuthenticatedCipherTest extends BrambleTestCase {
|
||||
|
||||
// Test vectors from the NaCl paper
|
||||
// http://cr.yp.to/highspeed/naclcrypto-20090310.pdf
|
||||
private static final byte[] TEST_KEY = StringUtils.fromHexString(
|
||||
private final byte[] TEST_KEY = StringUtils.fromHexString(
|
||||
"1b27556473e985d462cd51197a9a46c76009549eac6474f206c4ee0844f68389");
|
||||
private static final byte[] TEST_IV = StringUtils.fromHexString(
|
||||
private final byte[] TEST_IV = StringUtils.fromHexString(
|
||||
"69696ee955b62b73cd62bda875fc73d68219e0036b7a0b37");
|
||||
private static final byte[] TEST_PLAINTEXT = StringUtils.fromHexString(
|
||||
private final byte[] TEST_PLAINTEXT = StringUtils.fromHexString(
|
||||
"be075fc53c81f2d5cf141316" +
|
||||
"ebeb0c7b5228c52a4c62cbd4" +
|
||||
"4b66849b64244ffce5ecbaaf" +
|
||||
@@ -31,7 +32,7 @@ public class XSalsa20Poly1305AuthenticatedCipherTest extends BrambleTestCase {
|
||||
"048977eb48f59ffd4924ca1c" +
|
||||
"60902e52f0a089bc76897040" +
|
||||
"e082f937763848645e0705");
|
||||
private static final byte[] TEST_CIPHERTEXT = StringUtils.fromHexString(
|
||||
private final byte[] TEST_CIPHERTEXT = StringUtils.fromHexString(
|
||||
"f3ffc7703f9400e52a7dfb4b" +
|
||||
"3d3305d98e993b9f48681273" +
|
||||
"c29650ba32fc76ce48332ea7" +
|
||||
@@ -46,6 +47,10 @@ public class XSalsa20Poly1305AuthenticatedCipherTest extends BrambleTestCase {
|
||||
"a43d14a6599b1f654cb45a74" +
|
||||
"e355a5");
|
||||
|
||||
public XSalsa20Poly1305AuthenticatedCipherTest() throws FormatException {
|
||||
// required for throws declaration
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncrypt() throws Exception {
|
||||
SecretKey k = new SecretKey(TEST_KEY);
|
||||
|
||||
@@ -618,11 +618,12 @@ public class BdfReaderImplTest extends BrambleTestCase {
|
||||
r.readDictionary();
|
||||
}
|
||||
|
||||
private void setContents(String hex) {
|
||||
private void setContents(String hex) throws FormatException {
|
||||
setContents(hex, DEFAULT_MAX_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
private void setContents(String hex, int maxBufferSize) {
|
||||
private void setContents(String hex, int maxBufferSize)
|
||||
throws FormatException {
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(fromHexString(hex));
|
||||
r = new BdfReaderImpl(in, DEFAULT_NESTED_LIMIT, maxBufferSize);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.briarproject.bramble.data;
|
||||
|
||||
import org.briarproject.bramble.test.BrambleTestCase;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -17,14 +16,8 @@ import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
public class BdfWriterImplTest extends BrambleTestCase {
|
||||
|
||||
private ByteArrayOutputStream out = null;
|
||||
private BdfWriterImpl w = null;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
out = new ByteArrayOutputStream();
|
||||
w = new BdfWriterImpl(out);
|
||||
}
|
||||
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
private final BdfWriterImpl w = new BdfWriterImpl(out);
|
||||
|
||||
@Test
|
||||
public void testWriteNull() throws IOException {
|
||||
|
||||
@@ -303,11 +303,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(25).of(database).startTransaction();
|
||||
exactly(27).of(database).startTransaction();
|
||||
will(returnValue(txn));
|
||||
exactly(25).of(database).containsContact(txn, contactId);
|
||||
exactly(27).of(database).containsContact(txn, contactId);
|
||||
will(returnValue(false));
|
||||
exactly(25).of(database).abortTransaction(txn);
|
||||
exactly(27).of(database).abortTransaction(txn);
|
||||
}});
|
||||
DatabaseComponent db = createDatabaseComponent(database, eventBus,
|
||||
eventExecutor, shutdownManager);
|
||||
@@ -321,6 +321,23 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
// Expected
|
||||
}
|
||||
|
||||
try {
|
||||
db.transaction(true, transaction ->
|
||||
db.containsAcksToSend(transaction, contactId));
|
||||
fail();
|
||||
} catch (NoSuchContactException expected) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
try {
|
||||
db.transaction(true, transaction ->
|
||||
db.containsMessagesToSend(transaction, contactId,
|
||||
123, true));
|
||||
fail();
|
||||
} catch (NoSuchContactException expected) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
try {
|
||||
db.transaction(false, transaction ->
|
||||
db.generateAck(transaction, contactId, 123));
|
||||
|
||||
@@ -378,9 +378,9 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Initially there should be nothing to send
|
||||
assertFalse(
|
||||
db.containsAnythingToSend(txn, contactId, MAX_LATENCY, false));
|
||||
db.containsMessagesToSend(txn, contactId, MAX_LATENCY, false));
|
||||
assertFalse(
|
||||
db.containsAnythingToSend(txn, contactId, MAX_LATENCY, true));
|
||||
db.containsMessagesToSend(txn, contactId, MAX_LATENCY, true));
|
||||
|
||||
// Add some messages to ack
|
||||
Message message1 = getMessage(groupId);
|
||||
@@ -389,10 +389,7 @@ 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));
|
||||
assertTrue(db.containsAcksToSend(txn, contactId));
|
||||
Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234);
|
||||
assertEquals(asList(messageId, messageId1), ids);
|
||||
|
||||
@@ -400,10 +397,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
||||
db.lowerAckFlag(txn, contactId, asList(messageId, messageId1));
|
||||
|
||||
// No message IDs should be returned
|
||||
assertFalse(
|
||||
db.containsAnythingToSend(txn, contactId, MAX_LATENCY, false));
|
||||
assertFalse(
|
||||
db.containsAnythingToSend(txn, contactId, MAX_LATENCY, true));
|
||||
assertFalse(db.containsAcksToSend(txn, contactId));
|
||||
assertEquals(emptyList(), db.getMessagesToAck(txn, contactId, 1234));
|
||||
|
||||
// Raise the ack flag again
|
||||
@@ -411,10 +405,7 @@ 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));
|
||||
assertTrue(db.containsAcksToSend(txn, contactId));
|
||||
ids = db.getMessagesToAck(txn, contactId, 1234);
|
||||
assertEquals(asList(messageId, messageId1), ids);
|
||||
|
||||
@@ -2029,37 +2020,51 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
||||
db.addMessage(txn, message, UNKNOWN, false, false, null);
|
||||
|
||||
// There should be no messages to send
|
||||
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
|
||||
assertEquals(Long.MAX_VALUE,
|
||||
db.getNextSendTime(txn, contactId, MAX_LATENCY));
|
||||
|
||||
// Share the group with the contact - still no messages to send
|
||||
db.addGroupVisibility(txn, contactId, groupId, true);
|
||||
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
|
||||
assertEquals(Long.MAX_VALUE,
|
||||
db.getNextSendTime(txn, contactId, MAX_LATENCY));
|
||||
|
||||
// Set the message's state to DELIVERED - still no messages to send
|
||||
db.setMessageState(txn, messageId, DELIVERED);
|
||||
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
|
||||
assertEquals(Long.MAX_VALUE,
|
||||
db.getNextSendTime(txn, contactId, MAX_LATENCY));
|
||||
|
||||
// Share the message - now it should be sendable immediately
|
||||
db.setMessageShared(txn, messageId, true);
|
||||
assertEquals(0, db.getNextSendTime(txn, contactId));
|
||||
assertEquals(0, db.getNextSendTime(txn, contactId, MAX_LATENCY));
|
||||
|
||||
// Mark the message as requested - it should still be sendable
|
||||
db.raiseRequestedFlag(txn, contactId, messageId);
|
||||
assertEquals(0, db.getNextSendTime(txn, contactId));
|
||||
assertEquals(0, db.getNextSendTime(txn, contactId, MAX_LATENCY));
|
||||
|
||||
// Update the message's expiry time as though we sent it - now the
|
||||
// message should be sendable after one round-trip
|
||||
db.updateRetransmissionData(txn, contactId, messageId, 1000);
|
||||
assertEquals(now + 2000, db.getNextSendTime(txn, contactId));
|
||||
db.updateRetransmissionData(txn, contactId, messageId, MAX_LATENCY);
|
||||
assertEquals(now + MAX_LATENCY * 2,
|
||||
db.getNextSendTime(txn, contactId, MAX_LATENCY));
|
||||
|
||||
// The message should be sendable immediately over a transport with
|
||||
// lower latency
|
||||
assertEquals(0L, db.getNextSendTime(txn, contactId, MAX_LATENCY - 1));
|
||||
|
||||
// Update the message's expiry time again - now it should be sendable
|
||||
// after two round-trips
|
||||
db.updateRetransmissionData(txn, contactId, messageId, 1000);
|
||||
assertEquals(now + 4000, db.getNextSendTime(txn, contactId));
|
||||
db.updateRetransmissionData(txn, contactId, messageId, MAX_LATENCY);
|
||||
assertEquals(now + MAX_LATENCY * 4,
|
||||
db.getNextSendTime(txn, contactId, MAX_LATENCY));
|
||||
|
||||
// The message should be sendable immediately over a transport with
|
||||
// lower latency
|
||||
assertEquals(0L, db.getNextSendTime(txn, contactId, MAX_LATENCY - 1));
|
||||
|
||||
// Delete the message - there should be no messages to send
|
||||
db.deleteMessage(txn, messageId);
|
||||
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
|
||||
assertEquals(Long.MAX_VALUE,
|
||||
db.getNextSendTime(txn, contactId, MAX_LATENCY));
|
||||
|
||||
db.commitTransaction(txn);
|
||||
db.close();
|
||||
@@ -2124,7 +2129,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
||||
db.updateRetransmissionData(txn, contactId, messageId, MAX_LATENCY);
|
||||
|
||||
// The message should expire after 2 * MAX_LATENCY
|
||||
assertEquals(now + MAX_LATENCY * 2, db.getNextSendTime(txn, contactId));
|
||||
assertEquals(now + MAX_LATENCY * 2,
|
||||
db.getNextSendTime(txn, contactId, MAX_LATENCY));
|
||||
|
||||
// Time: now + MAX_LATENCY * 2 - 1
|
||||
// The message should not yet be sendable
|
||||
@@ -2167,7 +2173,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
||||
db.updateRetransmissionData(txn, contactId, messageId, MAX_LATENCY);
|
||||
|
||||
// The message should expire after 2 * MAX_LATENCY
|
||||
assertEquals(now + MAX_LATENCY * 2, db.getNextSendTime(txn, contactId));
|
||||
assertEquals(now + MAX_LATENCY * 2,
|
||||
db.getNextSendTime(txn, contactId, MAX_LATENCY));
|
||||
|
||||
// The message should not be sendable via the same transport
|
||||
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
|
||||
@@ -2214,7 +2221,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
||||
db.updateRetransmissionData(txn, contactId, messageId, MAX_LATENCY);
|
||||
|
||||
// The message should expire after 2 * MAX_LATENCY
|
||||
assertEquals(now + MAX_LATENCY * 2, db.getNextSendTime(txn, contactId));
|
||||
assertEquals(now + MAX_LATENCY * 2,
|
||||
db.getNextSendTime(txn, contactId, MAX_LATENCY));
|
||||
|
||||
// Time: now + MAX_LATENCY * 2 - 1
|
||||
// The message should not yet be sendable
|
||||
@@ -2225,8 +2233,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
||||
// Reset the retransmission times
|
||||
db.resetUnackedMessagesToSend(txn, contactId);
|
||||
|
||||
// The message should have infinitely short expiry
|
||||
assertEquals(0, db.getNextSendTime(txn, contactId));
|
||||
// The message should be sendable immediately
|
||||
assertEquals(0, db.getNextSendTime(txn, contactId, MAX_LATENCY));
|
||||
|
||||
// The message should be sendable
|
||||
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
|
||||
@@ -2579,7 +2587,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
||||
private void assertNothingToSendLazily(Database<Connection> db,
|
||||
Connection txn) throws Exception {
|
||||
assertFalse(
|
||||
db.containsAnythingToSend(txn, contactId, MAX_LATENCY, false));
|
||||
db.containsMessagesToSend(txn, contactId, MAX_LATENCY, false));
|
||||
Collection<MessageId> ids =
|
||||
db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
|
||||
assertTrue(ids.isEmpty());
|
||||
@@ -2590,7 +2598,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
||||
private void assertOneMessageToSendLazily(Database<Connection> db,
|
||||
Connection txn) throws Exception {
|
||||
assertTrue(
|
||||
db.containsAnythingToSend(txn, contactId, MAX_LATENCY, false));
|
||||
db.containsMessagesToSend(txn, contactId, MAX_LATENCY, false));
|
||||
Collection<MessageId> ids =
|
||||
db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
|
||||
assertEquals(singletonList(messageId), ids);
|
||||
@@ -2601,7 +2609,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
||||
private void assertNothingToSendEagerly(Database<Connection> db,
|
||||
Connection txn) throws Exception {
|
||||
assertFalse(
|
||||
db.containsAnythingToSend(txn, contactId, MAX_LATENCY, true));
|
||||
db.containsMessagesToSend(txn, contactId, MAX_LATENCY, true));
|
||||
Collection<MessageId> unacked =
|
||||
db.getUnackedMessagesToSend(txn, contactId);
|
||||
assertTrue(unacked.isEmpty());
|
||||
@@ -2611,7 +2619,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
||||
private void assertOneMessageToSendEagerly(Database<Connection> db,
|
||||
Connection txn) throws Exception {
|
||||
assertTrue(
|
||||
db.containsAnythingToSend(txn, contactId, MAX_LATENCY, true));
|
||||
db.containsMessagesToSend(txn, contactId, MAX_LATENCY, true));
|
||||
Collection<MessageId> unacked =
|
||||
db.getUnackedMessagesToSend(txn, contactId);
|
||||
assertEquals(singletonList(messageId), unacked);
|
||||
|
||||
@@ -14,7 +14,6 @@ import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.DbExpectations;
|
||||
import org.jmock.Expectations;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
@@ -41,13 +40,8 @@ public class IdentityManagerImplTest extends BrambleMockTestCase {
|
||||
private final KeyPair handshakeKeyPair =
|
||||
new KeyPair(handshakePublicKey, handshakePrivateKey);
|
||||
|
||||
private IdentityManagerImpl identityManager;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
identityManager =
|
||||
new IdentityManagerImpl(db, crypto, authorFactory, clock);
|
||||
}
|
||||
private final IdentityManagerImpl identityManager =
|
||||
new IdentityManagerImpl(db, crypto, authorFactory, clock);
|
||||
|
||||
@Test
|
||||
public void testOpenDatabaseIdentityRegistered() throws Exception {
|
||||
|
||||
@@ -5,20 +5,18 @@ import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
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.Service;
|
||||
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.jmock.Expectations;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPED;
|
||||
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.SUCCESS;
|
||||
import static org.briarproject.bramble.api.system.Clock.MAX_REASONABLE_TIME_MS;
|
||||
@@ -31,22 +29,18 @@ 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 OpenDatabaseHook hook = context.mock(OpenDatabaseHook.class);
|
||||
private final Service service = context.mock(Service.class);
|
||||
|
||||
private final SecretKey dbKey = getSecretKey();
|
||||
|
||||
private LifecycleManagerImpl lifecycleManager;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
lifecycleManager = new LifecycleManagerImpl(db, eventBus, clock);
|
||||
}
|
||||
private final LifecycleManagerImpl 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();
|
||||
@@ -55,6 +49,7 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase {
|
||||
will(returnValue(false));
|
||||
oneOf(db).transaction(with(false), withDbRunnable(txn));
|
||||
oneOf(db).removeTemporaryMessages(txn);
|
||||
oneOf(hook).onDatabaseOpened(txn);
|
||||
allowing(eventBus).broadcast(with(any(LifecycleEvent.class)));
|
||||
}});
|
||||
|
||||
@@ -62,7 +57,38 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase {
|
||||
|
||||
assertEquals(SUCCESS, lifecycleManager.startServices(dbKey));
|
||||
assertEquals(RUNNING, lifecycleManager.getLifecycleState());
|
||||
assertTrue(called.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServicesAreStartedAndStopped() throws Exception {
|
||||
long now = System.currentTimeMillis();
|
||||
Transaction txn = new Transaction(null, false);
|
||||
|
||||
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));
|
||||
oneOf(db).removeTemporaryMessages(txn);
|
||||
oneOf(service).startService();
|
||||
allowing(eventBus).broadcast(with(any(LifecycleEvent.class)));
|
||||
}});
|
||||
|
||||
lifecycleManager.registerService(service);
|
||||
|
||||
assertEquals(SUCCESS, lifecycleManager.startServices(dbKey));
|
||||
assertEquals(RUNNING, lifecycleManager.getLifecycleState());
|
||||
context.assertIsSatisfied();
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(db).close();
|
||||
oneOf(service).stopService();
|
||||
allowing(eventBus).broadcast(with(any(LifecycleEvent.class)));
|
||||
}});
|
||||
|
||||
lifecycleManager.stopServices();
|
||||
assertEquals(STOPPED, lifecycleManager.getLifecycleState());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -89,6 +115,31 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase {
|
||||
assertEquals(STARTING, lifecycleManager.getLifecycleState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSecondCallToStartServicesReturnsEarly() throws Exception {
|
||||
long now = System.currentTimeMillis();
|
||||
Transaction txn = new Transaction(null, false);
|
||||
|
||||
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));
|
||||
oneOf(db).removeTemporaryMessages(txn);
|
||||
allowing(eventBus).broadcast(with(any(LifecycleEvent.class)));
|
||||
}});
|
||||
|
||||
assertEquals(SUCCESS, lifecycleManager.startServices(dbKey));
|
||||
assertEquals(RUNNING, lifecycleManager.getLifecycleState());
|
||||
context.assertIsSatisfied();
|
||||
|
||||
// Calling startServices() again should not try to open the DB or
|
||||
// start the services again
|
||||
assertEquals(ALREADY_RUNNING, lifecycleManager.startServices(dbKey));
|
||||
assertEquals(RUNNING, lifecycleManager.getLifecycleState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSecondCallToStopServicesReturnsEarly() throws Exception {
|
||||
long now = System.currentTimeMillis();
|
||||
@@ -101,7 +152,7 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase {
|
||||
will(returnValue(false));
|
||||
oneOf(db).transaction(with(false), withDbRunnable(txn));
|
||||
oneOf(db).removeTemporaryMessages(txn);
|
||||
exactly(2).of(eventBus).broadcast(with(any(LifecycleEvent.class)));
|
||||
allowing(eventBus).broadcast(with(any(LifecycleEvent.class)));
|
||||
}});
|
||||
|
||||
assertEquals(SUCCESS, lifecycleManager.startServices(dbKey));
|
||||
@@ -109,17 +160,17 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase {
|
||||
context.assertIsSatisfied();
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(eventBus).broadcast(with(any(LifecycleEvent.class)));
|
||||
oneOf(db).close();
|
||||
allowing(eventBus).broadcast(with(any(LifecycleEvent.class)));
|
||||
}});
|
||||
|
||||
lifecycleManager.stopServices();
|
||||
assertEquals(STOPPING, lifecycleManager.getLifecycleState());
|
||||
assertEquals(STOPPED, lifecycleManager.getLifecycleState());
|
||||
context.assertIsSatisfied();
|
||||
|
||||
// Calling stopServices() again should not broadcast another event or
|
||||
// try to close the DB again
|
||||
lifecycleManager.stopServices();
|
||||
assertEquals(STOPPING, lifecycleManager.getLifecycleState());
|
||||
assertEquals(STOPPED, lifecycleManager.getLifecycleState());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,32 @@ public class ConnectivityCheckerImplTest extends BrambleMockTestCase {
|
||||
checker.onConnectivityCheckSucceeded(now);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckIsCancelledWhenObserverIsRemoved() {
|
||||
ConnectivityCheckerImpl checker = createChecker();
|
||||
|
||||
// When checkConnectivity() is called a check should be started
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(now));
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(apiCall);
|
||||
will(returnValue(task));
|
||||
}});
|
||||
|
||||
checker.checkConnectivity(properties, observer1);
|
||||
|
||||
// When the observer is removed the check should be cancelled
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(task).cancel();
|
||||
}});
|
||||
|
||||
checker.removeObserver(observer1);
|
||||
|
||||
// If the check runs anyway (cancellation came too late) the observer
|
||||
// should not be called
|
||||
checker.onConnectivityCheckSucceeded(now);
|
||||
}
|
||||
|
||||
private ConnectivityCheckerImpl createChecker() {
|
||||
|
||||
return new ConnectivityCheckerImpl(clock, mailboxApiCaller) {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.jmock.Expectations;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
|
||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||
import static org.briarproject.bramble.test.TestUtils.getContactId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
|
||||
|
||||
public class ContactMailboxClientTest extends BrambleMockTestCase {
|
||||
|
||||
private final MailboxWorkerFactory workerFactory =
|
||||
context.mock(MailboxWorkerFactory.class);
|
||||
private final ConnectivityChecker connectivityChecker =
|
||||
context.mock(ConnectivityChecker.class);
|
||||
private final TorReachabilityMonitor reachabilityMonitor =
|
||||
context.mock(TorReachabilityMonitor.class);
|
||||
private final MailboxWorker uploadWorker =
|
||||
context.mock(MailboxWorker.class, "uploadWorker");
|
||||
private final MailboxWorker downloadWorker =
|
||||
context.mock(MailboxWorker.class, "downloadWorker");
|
||||
|
||||
private final MailboxProperties properties =
|
||||
getMailboxProperties(false, CLIENT_SUPPORTS);
|
||||
private final MailboxFolderId inboxId =
|
||||
requireNonNull(properties.getInboxId());
|
||||
private final MailboxFolderId outboxId =
|
||||
requireNonNull(properties.getOutboxId());
|
||||
private final ContactId contactId = getContactId();
|
||||
|
||||
private final ContactMailboxClient client =
|
||||
new ContactMailboxClient(workerFactory, connectivityChecker,
|
||||
reachabilityMonitor);
|
||||
|
||||
@Test
|
||||
public void testStartAndDestroyWithNoContactsAssigned() {
|
||||
client.start();
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAssignContactForUploadAndDestroyClient() {
|
||||
client.start();
|
||||
|
||||
// When the contact is assigned, the worker should be created and
|
||||
// started
|
||||
expectCreateAndStartUploadWorker();
|
||||
client.assignContactForUpload(contactId, properties, outboxId);
|
||||
|
||||
// When the client is destroyed, the worker should be destroyed
|
||||
expectDestroyWorker(uploadWorker);
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAssignAndDeassignContactForUpload() {
|
||||
client.start();
|
||||
|
||||
// When the contact is assigned, the worker should be created and
|
||||
// started
|
||||
expectCreateAndStartUploadWorker();
|
||||
client.assignContactForUpload(contactId, properties, outboxId);
|
||||
|
||||
// When the contact is deassigned, the worker should be destroyed
|
||||
expectDestroyWorker(uploadWorker);
|
||||
client.deassignContactForUpload(contactId);
|
||||
context.assertIsSatisfied();
|
||||
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAssignContactForDownloadAndDestroyClient() {
|
||||
client.start();
|
||||
|
||||
// When the contact is assigned, the worker should be created and
|
||||
// started
|
||||
expectCreateAndStartDownloadWorker();
|
||||
client.assignContactForDownload(contactId, properties, inboxId);
|
||||
|
||||
// When the client is destroyed, the worker should be destroyed
|
||||
expectDestroyWorker(downloadWorker);
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAssignAndDeassignContactForDownload() {
|
||||
client.start();
|
||||
|
||||
// When the contact is assigned, the worker should be created and
|
||||
// started
|
||||
expectCreateAndStartDownloadWorker();
|
||||
client.assignContactForDownload(contactId, properties, inboxId);
|
||||
|
||||
// When the contact is deassigned, the worker should be destroyed
|
||||
expectDestroyWorker(downloadWorker);
|
||||
client.deassignContactForDownload(contactId);
|
||||
context.assertIsSatisfied();
|
||||
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
private void expectCreateAndStartUploadWorker() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(workerFactory).createUploadWorker(connectivityChecker,
|
||||
properties, outboxId, contactId);
|
||||
will(returnValue(uploadWorker));
|
||||
oneOf(uploadWorker).start();
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectCreateAndStartDownloadWorker() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(workerFactory).createDownloadWorkerForContactMailbox(
|
||||
connectivityChecker, reachabilityMonitor, properties);
|
||||
will(returnValue(downloadWorker));
|
||||
oneOf(downloadWorker).start();
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectDestroyWorker(MailboxWorker worker) {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(worker).destroy();
|
||||
}});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class ContactMailboxDownloadWorkerTest
|
||||
extends MailboxDownloadWorkerTest<ContactMailboxDownloadWorker> {
|
||||
|
||||
public ContactMailboxDownloadWorkerTest() {
|
||||
mailboxProperties = getMailboxProperties(false, CLIENT_SUPPORTS);
|
||||
worker = new ContactMailboxDownloadWorker(connectivityChecker,
|
||||
torReachabilityMonitor, mailboxApiCaller, mailboxApi,
|
||||
mailboxFileManager, mailboxProperties);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChecksConnectivityWhenStartedAndRemovesObserverWhenDestroyed() {
|
||||
// When the worker is started it should start a connectivity check
|
||||
expectStartConnectivityCheck();
|
||||
worker.start();
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// and reachability observers
|
||||
expectRemoveObservers();
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChecksForFilesWhenConnectivityCheckSucceeds()
|
||||
throws Exception {
|
||||
// When the worker is started it should start a connectivity check
|
||||
expectStartConnectivityCheck();
|
||||
worker.start();
|
||||
|
||||
// When the connectivity check succeeds, a list-inbox task should be
|
||||
// started for the first download cycle
|
||||
AtomicReference<ApiCall> listTask = new AtomicReference<>();
|
||||
expectStartTask(listTask);
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the list-inbox tasks runs and finds no files to download,
|
||||
// it should add a Tor reachability observer
|
||||
expectCheckForFiles(mailboxProperties.getInboxId(), emptyList());
|
||||
expectAddReachabilityObserver();
|
||||
assertFalse(listTask.get().callApi());
|
||||
|
||||
// When the reachability observer is called, a list-inbox task should
|
||||
// be started for the second download cycle
|
||||
expectStartTask(listTask);
|
||||
worker.onTorReachable();
|
||||
|
||||
// When the list-inbox tasks runs and finds no files to download,
|
||||
// it should finish the second download cycle
|
||||
expectCheckForFiles(mailboxProperties.getInboxId(), emptyList());
|
||||
assertFalse(listTask.get().callApi());
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// and reachability observers
|
||||
expectRemoveObservers();
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadsFilesWhenConnectivityCheckSucceeds()
|
||||
throws Exception {
|
||||
// When the worker is started it should start a connectivity check
|
||||
expectStartConnectivityCheck();
|
||||
worker.start();
|
||||
|
||||
// When the connectivity check succeeds, a list-inbox task should be
|
||||
// started for the first download cycle
|
||||
AtomicReference<ApiCall> listTask = new AtomicReference<>();
|
||||
expectStartTask(listTask);
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the list-inbox tasks runs and finds some files to download,
|
||||
// it should start a download task for the first file
|
||||
AtomicReference<ApiCall> downloadTask = new AtomicReference<>();
|
||||
expectCheckForFiles(mailboxProperties.getInboxId(), files);
|
||||
expectStartTask(downloadTask);
|
||||
assertFalse(listTask.get().callApi());
|
||||
|
||||
// When the first download task runs it should download the file to the
|
||||
// location provided by the file manager and start a delete task
|
||||
AtomicReference<ApiCall> deleteTask = new AtomicReference<>();
|
||||
expectDownloadFile(mailboxProperties.getInboxId(), file1);
|
||||
expectStartTask(deleteTask);
|
||||
assertFalse(downloadTask.get().callApi());
|
||||
|
||||
// When the first delete task runs it should delete the file, ignore
|
||||
// the tolerable failure, and start a download task for the next file
|
||||
expectDeleteFile(mailboxProperties.getInboxId(), file1, true);
|
||||
expectStartTask(downloadTask);
|
||||
assertFalse(deleteTask.get().callApi());
|
||||
|
||||
// When the second download task runs it should download the file to
|
||||
// the location provided by the file manager and start a delete task
|
||||
expectDownloadFile(mailboxProperties.getInboxId(), file2);
|
||||
expectStartTask(deleteTask);
|
||||
assertFalse(downloadTask.get().callApi());
|
||||
|
||||
// When the second delete task runs it should delete the file and
|
||||
// start a list-inbox task to check for files that may have arrived
|
||||
// since the first download cycle started
|
||||
expectDeleteFile(mailboxProperties.getInboxId(), file2, false);
|
||||
expectStartTask(listTask);
|
||||
assertFalse(deleteTask.get().callApi());
|
||||
|
||||
// When the list-inbox tasks runs and finds no more files to download,
|
||||
// it should add a Tor reachability observer
|
||||
expectCheckForFiles(mailboxProperties.getInboxId(), emptyList());
|
||||
expectAddReachabilityObserver();
|
||||
assertFalse(listTask.get().callApi());
|
||||
|
||||
// When the reachability observer is called, a list-inbox task should
|
||||
// be started for the second download cycle
|
||||
expectStartTask(listTask);
|
||||
worker.onTorReachable();
|
||||
|
||||
// When the list-inbox tasks runs and finds no more files to download,
|
||||
// it should finish the second download cycle
|
||||
expectCheckForFiles(mailboxProperties.getInboxId(), emptyList());
|
||||
assertFalse(listTask.get().callApi());
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// and reachability observers
|
||||
expectRemoveObservers();
|
||||
worker.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.WeakSingletonProvider;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.mailbox.InvalidMailboxIdException;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFileId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
@@ -13,7 +11,6 @@ import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
import org.briarproject.bramble.test.BrambleTestCase;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -25,14 +22,11 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.SETUP_TOKEN;
|
||||
import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.URL_BASE;
|
||||
import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.createMailboxApi;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
|
||||
@@ -40,56 +34,31 @@ import static org.briarproject.bramble.test.TestUtils.readBytes;
|
||||
import static org.briarproject.bramble.test.TestUtils.writeBytes;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
public class MailboxIntegrationTest extends BrambleTestCase {
|
||||
public class MailboxApiIntegrationTest extends BrambleTestCase {
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder folder = new TemporaryFolder();
|
||||
|
||||
private final static String URL_BASE = "http://127.0.0.1:8000";
|
||||
private final static MailboxAuthToken SETUP_TOKEN;
|
||||
private static final MailboxApi api = createMailboxApi();
|
||||
|
||||
static {
|
||||
try {
|
||||
SETUP_TOKEN = MailboxAuthToken.fromString(
|
||||
"54686973206973206120736574757020746f6b656e20666f722042726961722e");
|
||||
} catch (InvalidMailboxIdException e) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
private static final OkHttpClient client = new OkHttpClient.Builder()
|
||||
.socketFactory(SocketFactory.getDefault())
|
||||
.connectTimeout(60_000, MILLISECONDS)
|
||||
.build();
|
||||
private static final WeakSingletonProvider<OkHttpClient>
|
||||
httpClientProvider =
|
||||
new WeakSingletonProvider<OkHttpClient>() {
|
||||
@Override
|
||||
@Nonnull
|
||||
public OkHttpClient createInstance() {
|
||||
return client;
|
||||
}
|
||||
};
|
||||
private final static MailboxApiImpl api =
|
||||
new MailboxApiImpl(httpClientProvider);
|
||||
// needs to be static to keep values across different tests
|
||||
private static MailboxProperties ownerProperties;
|
||||
|
||||
/**
|
||||
* Called before each test to make sure the mailbox is setup once
|
||||
* before starting with individual tests.
|
||||
* {@link BeforeClass} needs to be static, so we can't use the API class.
|
||||
*/
|
||||
@Before
|
||||
public void ensureSetup() throws IOException, ApiException {
|
||||
@BeforeClass
|
||||
public static void setUp() throws IOException, ApiException {
|
||||
// Skip this test unless it's explicitly enabled in the environment
|
||||
assumeTrue(isOptionalTestEnabled(MailboxIntegrationTest.class));
|
||||
assumeTrue(isOptionalTestEnabled(MailboxApiIntegrationTest.class));
|
||||
|
||||
if (ownerProperties != null) return;
|
||||
assertNull(ownerProperties);
|
||||
MailboxProperties setupProperties = new MailboxProperties(
|
||||
URL_BASE, SETUP_TOKEN, new ArrayList<>());
|
||||
ownerProperties = api.setup(setupProperties);
|
||||
@@ -98,7 +67,7 @@ public class MailboxIntegrationTest extends BrambleTestCase {
|
||||
@AfterClass
|
||||
// we can't test wiping as a regular test as it stops the mailbox
|
||||
public static void wipe() throws IOException, ApiException {
|
||||
if (!isOptionalTestEnabled(MailboxIntegrationTest.class)) return;
|
||||
if (!isOptionalTestEnabled(MailboxApiIntegrationTest.class)) return;
|
||||
|
||||
api.wipeMailbox(ownerProperties);
|
||||
|
||||
@@ -121,7 +90,7 @@ public class MailboxIntegrationTest extends BrambleTestCase {
|
||||
ContactId contactId = new ContactId(1);
|
||||
MailboxContact contact = getMailboxContact(contactId);
|
||||
MailboxProperties contactProperties = new MailboxProperties(
|
||||
ownerProperties.getBaseUrl(), contact.token,
|
||||
ownerProperties.getOnion(), contact.token,
|
||||
new ArrayList<>(), contact.inboxId, contact.outboxId);
|
||||
api.addContact(ownerProperties, contact);
|
||||
|
||||
@@ -167,7 +136,7 @@ public class MailboxIntegrationTest extends BrambleTestCase {
|
||||
ContactId contactId = new ContactId(1);
|
||||
MailboxContact contact = getMailboxContact(contactId);
|
||||
MailboxProperties contactProperties = new MailboxProperties(
|
||||
ownerProperties.getBaseUrl(), contact.token,
|
||||
ownerProperties.getOnion(), contact.token,
|
||||
new ArrayList<>(), contact.inboxId, contact.outboxId);
|
||||
api.addContact(ownerProperties, contact);
|
||||
|
||||
@@ -184,7 +153,7 @@ public class MailboxIntegrationTest extends BrambleTestCase {
|
||||
MailboxFileId fileName1 = files1.get(0).name;
|
||||
|
||||
// owner can't check files
|
||||
assertThrows(ApiException.class, () ->
|
||||
assertThrows(TolerableFailureException.class, () ->
|
||||
api.getFiles(ownerProperties, contact.inboxId));
|
||||
|
||||
// contact downloads file
|
||||
@@ -195,12 +164,13 @@ public class MailboxIntegrationTest extends BrambleTestCase {
|
||||
|
||||
// owner can't download file, even if knowing name
|
||||
File file1forbidden = folder.newFile();
|
||||
assertThrows(ApiException.class, () -> api.getFile(ownerProperties,
|
||||
contact.inboxId, fileName1, file1forbidden));
|
||||
assertThrows(TolerableFailureException.class, () ->
|
||||
api.getFile(ownerProperties, contact.inboxId, fileName1,
|
||||
file1forbidden));
|
||||
assertEquals(0, file1forbidden.length());
|
||||
|
||||
// owner can't delete file
|
||||
assertThrows(ApiException.class, () ->
|
||||
assertThrows(TolerableFailureException.class, () ->
|
||||
api.deleteFile(ownerProperties, contact.inboxId, fileName1));
|
||||
|
||||
// contact deletes file
|
||||
@@ -230,7 +200,7 @@ public class MailboxIntegrationTest extends BrambleTestCase {
|
||||
MailboxFileId file3name = files2.get(1).name;
|
||||
|
||||
// contact can't list files in contact's outbox
|
||||
assertThrows(ApiException.class, () ->
|
||||
assertThrows(TolerableFailureException.class, () ->
|
||||
api.getFiles(contactProperties, contact.outboxId));
|
||||
|
||||
// owner downloads both files from contact's outbox
|
||||
@@ -250,17 +220,19 @@ public class MailboxIntegrationTest extends BrambleTestCase {
|
||||
// contact can't download files again, even if knowing name
|
||||
File file2forbidden = folder.newFile();
|
||||
File file3forbidden = folder.newFile();
|
||||
assertThrows(ApiException.class, () -> api.getFile(contactProperties,
|
||||
contact.outboxId, file2name, file2forbidden));
|
||||
assertThrows(ApiException.class, () -> api.getFile(contactProperties,
|
||||
contact.outboxId, file3name, file3forbidden));
|
||||
assertThrows(TolerableFailureException.class, () ->
|
||||
api.getFile(contactProperties, contact.outboxId, file2name,
|
||||
file2forbidden));
|
||||
assertThrows(TolerableFailureException.class, () ->
|
||||
api.getFile(contactProperties, contact.outboxId, file3name,
|
||||
file3forbidden));
|
||||
assertEquals(0, file1forbidden.length());
|
||||
assertEquals(0, file2forbidden.length());
|
||||
|
||||
// contact can't delete files in outbox
|
||||
assertThrows(ApiException.class, () ->
|
||||
assertThrows(TolerableFailureException.class, () ->
|
||||
api.deleteFile(contactProperties, contact.outboxId, file2name));
|
||||
assertThrows(ApiException.class, () ->
|
||||
assertThrows(TolerableFailureException.class, () ->
|
||||
api.deleteFile(contactProperties, contact.outboxId, file3name));
|
||||
|
||||
// owner deletes files
|
||||
@@ -68,7 +68,10 @@ public class MailboxApiTest extends BrambleTestCase {
|
||||
return client;
|
||||
}
|
||||
};
|
||||
private final MailboxApiImpl api = new MailboxApiImpl(httpClientProvider);
|
||||
// We aren't using a real onion address, so use the given address verbatim
|
||||
private final UrlConverter urlConverter = onion -> onion;
|
||||
private final MailboxApiImpl api = new MailboxApiImpl(httpClientProvider,
|
||||
urlConverter);
|
||||
|
||||
private final MailboxAuthToken token = new MailboxAuthToken(getRandomId());
|
||||
private final MailboxAuthToken token2 = new MailboxAuthToken(getRandomId());
|
||||
@@ -627,6 +630,7 @@ public class MailboxApiTest extends BrambleTestCase {
|
||||
server.enqueue(new MockResponse().setBody(invalidResponse3));
|
||||
server.enqueue(new MockResponse().setBody(invalidResponse4));
|
||||
server.enqueue(new MockResponse().setResponseCode(401));
|
||||
server.enqueue(new MockResponse().setResponseCode(404));
|
||||
server.enqueue(new MockResponse().setResponseCode(500));
|
||||
server.start();
|
||||
String baseUrl = getBaseUrl(server);
|
||||
@@ -703,13 +707,21 @@ public class MailboxApiTest extends BrambleTestCase {
|
||||
assertEquals("GET", request8.getMethod());
|
||||
assertToken(request8, token);
|
||||
|
||||
// 500 internal server error
|
||||
assertThrows(ApiException.class,
|
||||
() -> api.getFiles(properties, contactInboxId));
|
||||
// 404 not found
|
||||
assertThrows(TolerableFailureException.class, () ->
|
||||
api.getFiles(properties, contactInboxId));
|
||||
RecordedRequest request9 = server.takeRequest();
|
||||
assertEquals("/files/" + contactInboxId, request9.getPath());
|
||||
assertEquals("GET", request9.getMethod());
|
||||
assertToken(request9, token);
|
||||
|
||||
// 500 internal server error
|
||||
assertThrows(ApiException.class,
|
||||
() -> api.getFiles(properties, contactInboxId));
|
||||
RecordedRequest request10 = server.takeRequest();
|
||||
assertEquals("/files/" + contactInboxId, request10.getPath());
|
||||
assertEquals("GET", request10.getMethod());
|
||||
assertToken(request10, token);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFileId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.CaptureArgumentAction;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.lib.action.DoAllAction;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
|
||||
|
||||
abstract class MailboxDownloadWorkerTest<W extends MailboxDownloadWorker>
|
||||
extends BrambleMockTestCase {
|
||||
|
||||
final ConnectivityChecker connectivityChecker =
|
||||
context.mock(ConnectivityChecker.class);
|
||||
final TorReachabilityMonitor torReachabilityMonitor =
|
||||
context.mock(TorReachabilityMonitor.class);
|
||||
final MailboxApiCaller mailboxApiCaller =
|
||||
context.mock(MailboxApiCaller.class);
|
||||
final MailboxApi mailboxApi = context.mock(MailboxApi.class);
|
||||
final MailboxFileManager mailboxFileManager =
|
||||
context.mock(MailboxFileManager.class);
|
||||
private final Cancellable apiCall = context.mock(Cancellable.class);
|
||||
|
||||
private final long now = System.currentTimeMillis();
|
||||
final MailboxFile file1 =
|
||||
new MailboxFile(new MailboxFileId(getRandomId()), now - 1);
|
||||
final MailboxFile file2 =
|
||||
new MailboxFile(new MailboxFileId(getRandomId()), now);
|
||||
final List<MailboxFile> files = asList(file1, file2);
|
||||
|
||||
private File testDir, tempFile;
|
||||
MailboxProperties mailboxProperties;
|
||||
W worker;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
testDir = getTestDirectory();
|
||||
tempFile = new File(testDir, "temp");
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
deleteTestDirectory(testDir);
|
||||
}
|
||||
|
||||
|
||||
void expectStartConnectivityCheck() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(connectivityChecker).checkConnectivity(
|
||||
with(mailboxProperties), with(worker));
|
||||
}});
|
||||
}
|
||||
|
||||
void expectStartTask(AtomicReference<ApiCall> task) {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(task, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
}
|
||||
|
||||
void expectCheckForFoldersWithAvailableFiles(
|
||||
List<MailboxFolderId> folderIds) throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxApi).getFolders(mailboxProperties);
|
||||
will(returnValue(folderIds));
|
||||
}});
|
||||
}
|
||||
|
||||
void expectCheckForFiles(MailboxFolderId folderId,
|
||||
List<MailboxFile> files) throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxApi).getFiles(mailboxProperties, folderId);
|
||||
will(returnValue(files));
|
||||
}});
|
||||
}
|
||||
|
||||
void expectDownloadFile(MailboxFolderId folderId,
|
||||
MailboxFile file)
|
||||
throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxFileManager).createTempFileForDownload();
|
||||
will(returnValue(tempFile));
|
||||
oneOf(mailboxApi).getFile(mailboxProperties, folderId, file.name,
|
||||
tempFile);
|
||||
oneOf(mailboxFileManager).handleDownloadedFile(tempFile);
|
||||
}});
|
||||
}
|
||||
|
||||
void expectDeleteFile(MailboxFolderId folderId, MailboxFile file,
|
||||
boolean tolerableFailure) throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxApi).deleteFile(mailboxProperties, folderId,
|
||||
file.name);
|
||||
if (tolerableFailure) {
|
||||
will(throwException(new TolerableFailureException()));
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
void expectAddReachabilityObserver() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(torReachabilityMonitor).addOneShotObserver(worker);
|
||||
}});
|
||||
}
|
||||
|
||||
void expectRemoveObservers() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(connectivityChecker).removeObserver(worker);
|
||||
oneOf(torReachabilityMonitor).removeObserver(worker);
|
||||
}});
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,20 @@ package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.connection.ConnectionManager;
|
||||
import org.briarproject.bramble.api.connection.ConnectionManager.TagController;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState;
|
||||
import org.briarproject.bramble.api.plugin.PluginManager;
|
||||
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
|
||||
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
|
||||
import org.briarproject.bramble.api.plugin.event.TransportActiveEvent;
|
||||
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
|
||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.CaptureArgumentAction;
|
||||
import org.briarproject.bramble.test.ConsumeArgumentAction;
|
||||
import org.briarproject.bramble.test.RunAction;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.lib.action.DoAllAction;
|
||||
@@ -20,6 +24,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@@ -28,11 +33,14 @@ import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleS
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.ID;
|
||||
import static org.briarproject.bramble.api.plugin.file.FileConstants.PROP_PATH;
|
||||
import static org.briarproject.bramble.mailbox.MailboxFileManagerImpl.DOWNLOAD_DIR_NAME;
|
||||
import static org.briarproject.bramble.mailbox.MailboxFileManagerImpl.UPLOAD_DIR_NAME;
|
||||
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
|
||||
import static org.briarproject.bramble.test.TestUtils.getContactId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
|
||||
@@ -47,6 +55,10 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
private final SimplexPlugin plugin = context.mock(SimplexPlugin.class);
|
||||
private final TransportConnectionReader transportConnectionReader =
|
||||
context.mock(TransportConnectionReader.class);
|
||||
private final TransportConnectionWriter transportConnectionWriter =
|
||||
context.mock(TransportConnectionWriter.class);
|
||||
|
||||
private final ContactId contactId = getContactId();
|
||||
|
||||
private File mailboxDir;
|
||||
private MailboxFileManagerImpl manager;
|
||||
@@ -65,17 +77,25 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
|
||||
@Test
|
||||
public void testHandlesOrphanedFilesAtStartup() throws Exception {
|
||||
// Create an orphaned file, left behind at the previous shutdown
|
||||
// Create an orphaned upload, left behind at the previous shutdown
|
||||
File uploadDir = new File(mailboxDir, UPLOAD_DIR_NAME);
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
uploadDir.mkdirs();
|
||||
File orphanedUpload = new File(uploadDir, "orphan");
|
||||
assertTrue(orphanedUpload.createNewFile());
|
||||
|
||||
// Create an orphaned download, left behind at the previous shutdown
|
||||
File downloadDir = new File(mailboxDir, DOWNLOAD_DIR_NAME);
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
downloadDir.mkdirs();
|
||||
File orphan = new File(downloadDir, "orphan");
|
||||
assertTrue(orphan.createNewFile());
|
||||
File orphanedDownload = new File(downloadDir, "orphan");
|
||||
assertTrue(orphanedDownload.createNewFile());
|
||||
|
||||
TransportProperties props = new TransportProperties();
|
||||
props.put(PROP_PATH, orphan.getAbsolutePath());
|
||||
props.put(PROP_PATH, orphanedDownload.getAbsolutePath());
|
||||
|
||||
// When the plugin becomes active the orphaned file should be handled
|
||||
// When the plugin becomes active the orphaned upload should be deleted
|
||||
// and the orphaned download should be handled
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(ioExecutor).execute(with(any(Runnable.class)));
|
||||
will(new RunAction());
|
||||
@@ -90,10 +110,12 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
}});
|
||||
|
||||
manager.eventOccurred(new TransportActiveEvent(ID));
|
||||
|
||||
assertFalse(orphanedUpload.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeletesFileWhenReadSucceeds() throws Exception {
|
||||
public void testDeletesDownloadedFileWhenReadSucceeds() throws Exception {
|
||||
expectCheckForOrphans();
|
||||
manager.eventOccurred(new TransportActiveEvent(ID));
|
||||
|
||||
@@ -102,7 +124,7 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
new AtomicReference<>(null);
|
||||
AtomicReference<TagController> controller = new AtomicReference<>(null);
|
||||
|
||||
expectPassFileToConnectionManager(f, reader, controller);
|
||||
expectPassDownloadedFileToConnectionManager(f, reader, controller);
|
||||
manager.handleDownloadedFile(f);
|
||||
|
||||
// The read is successful, so the tag controller should allow the tag
|
||||
@@ -117,29 +139,117 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeletesFileWhenTagIsNotRecognised() throws Exception {
|
||||
testDeletesFile(false, RUNNING, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeletesFileWhenReadFails() throws Exception {
|
||||
testDeletesFile(true, RUNNING, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoesNotDeleteFileWhenTagIsNotRecognisedAtShutdown()
|
||||
public void testDeletesDownloadedFileWhenTagIsNotRecognised()
|
||||
throws Exception {
|
||||
testDeletesFile(false, STOPPING, true);
|
||||
testDeletesDownloadedFile(false, RUNNING, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoesNotDeleteFileWhenReadFailsAtShutdown()
|
||||
throws Exception {
|
||||
testDeletesFile(true, STOPPING, true);
|
||||
public void testDeletesDownloadedFileWhenReadFails() throws Exception {
|
||||
testDeletesDownloadedFile(true, RUNNING, false);
|
||||
}
|
||||
|
||||
private void testDeletesFile(boolean recognised, LifecycleState state,
|
||||
boolean fileExists) throws Exception {
|
||||
@Test
|
||||
public void testDoesNotDeleteDownloadedFileWhenTagIsNotRecognisedAtShutdown()
|
||||
throws Exception {
|
||||
testDeletesDownloadedFile(false, STOPPING, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoesNotDeleteDownloadedFileWhenReadFailsAtShutdown()
|
||||
throws Exception {
|
||||
testDeletesDownloadedFile(true, STOPPING, true);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testThrowsExceptionIfPluginFailsToCreateWriter()
|
||||
throws Exception {
|
||||
OutgoingSessionRecord sessionRecord = new OutgoingSessionRecord();
|
||||
|
||||
expectCheckForOrphans();
|
||||
manager.eventOccurred(new TransportActiveEvent(ID));
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(pluginManager).getPlugin(ID);
|
||||
will(returnValue(plugin));
|
||||
oneOf(plugin).createWriter(with(any(TransportProperties.class)));
|
||||
will(returnValue(null));
|
||||
}});
|
||||
|
||||
manager.createAndWriteTempFileForUpload(contactId, sessionRecord);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testThrowsExceptionIfSessionFailsWithException()
|
||||
throws Exception {
|
||||
OutgoingSessionRecord sessionRecord = new OutgoingSessionRecord();
|
||||
|
||||
expectCheckForOrphans();
|
||||
manager.eventOccurred(new TransportActiveEvent(ID));
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(pluginManager).getPlugin(ID);
|
||||
will(returnValue(plugin));
|
||||
oneOf(plugin).createWriter(with(any(TransportProperties.class)));
|
||||
will(returnValue(transportConnectionWriter));
|
||||
oneOf(transportConnectionWriter).dispose(true);
|
||||
oneOf(connectionManager).manageOutgoingConnection(with(contactId),
|
||||
with(ID), with(any(TransportConnectionWriter.class)),
|
||||
with(sessionRecord));
|
||||
// The session fails with an exception. We need to use an action
|
||||
// for this, as createAndWriteTempFileForUpload() waits for it to
|
||||
// happen before returning
|
||||
will(new ConsumeArgumentAction<>(TransportConnectionWriter.class, 2,
|
||||
writer -> {
|
||||
try {
|
||||
writer.dispose(true);
|
||||
} catch (IOException e) {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
));
|
||||
}});
|
||||
|
||||
manager.createAndWriteTempFileForUpload(contactId, sessionRecord);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsFileIfSessionSucceeds() throws Exception {
|
||||
OutgoingSessionRecord sessionRecord = new OutgoingSessionRecord();
|
||||
|
||||
expectCheckForOrphans();
|
||||
manager.eventOccurred(new TransportActiveEvent(ID));
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(pluginManager).getPlugin(ID);
|
||||
will(returnValue(plugin));
|
||||
oneOf(plugin).createWriter(with(any(TransportProperties.class)));
|
||||
will(returnValue(transportConnectionWriter));
|
||||
oneOf(transportConnectionWriter).dispose(false);
|
||||
oneOf(connectionManager).manageOutgoingConnection(with(contactId),
|
||||
with(ID), with(any(TransportConnectionWriter.class)),
|
||||
with(sessionRecord));
|
||||
// The session succeeds. We need to use an action for this, as
|
||||
// createAndWriteTempFileForUpload() waits for it to happen before
|
||||
// returning
|
||||
will(new ConsumeArgumentAction<>(TransportConnectionWriter.class, 2,
|
||||
writer -> {
|
||||
try {
|
||||
writer.dispose(false);
|
||||
} catch (IOException e) {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
));
|
||||
}});
|
||||
|
||||
File f = manager.createAndWriteTempFileForUpload(contactId,
|
||||
sessionRecord);
|
||||
assertTrue(f.exists());
|
||||
}
|
||||
|
||||
private void testDeletesDownloadedFile(boolean recognised,
|
||||
LifecycleState state, boolean fileExists) throws Exception {
|
||||
expectCheckForOrphans();
|
||||
manager.eventOccurred(new TransportActiveEvent(ID));
|
||||
|
||||
@@ -148,7 +258,7 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
new AtomicReference<>(null);
|
||||
AtomicReference<TagController> controller = new AtomicReference<>(null);
|
||||
|
||||
expectPassFileToConnectionManager(f, reader, controller);
|
||||
expectPassDownloadedFileToConnectionManager(f, reader, controller);
|
||||
manager.handleDownloadedFile(f);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
@@ -169,7 +279,7 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectPassFileToConnectionManager(File f,
|
||||
private void expectPassDownloadedFileToConnectionManager(File f,
|
||||
AtomicReference<TransportConnectionReader> reader,
|
||||
AtomicReference<TagController> controller) {
|
||||
TransportProperties props = new TransportProperties();
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.BrambleCoreModule;
|
||||
import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
|
||||
import org.briarproject.bramble.test.BrambleIntegrationTestComponent;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Component;
|
||||
|
||||
@Singleton
|
||||
@Component(modules = {
|
||||
BrambleCoreIntegrationTestModule.class,
|
||||
BrambleCoreModule.class
|
||||
})
|
||||
interface MailboxIntegrationTestComponent extends
|
||||
BrambleIntegrationTestComponent {
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.BrambleCoreEagerSingletons;
|
||||
import org.briarproject.bramble.api.WeakSingletonProvider;
|
||||
import org.briarproject.bramble.api.mailbox.InvalidMailboxIdException;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
|
||||
import org.briarproject.bramble.io.IoModule;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.inject.Singleton;
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
class MailboxIntegrationTestUtils {
|
||||
|
||||
static final String URL_BASE = "http://127.0.0.1:8000";
|
||||
static final MailboxAuthToken SETUP_TOKEN;
|
||||
|
||||
static {
|
||||
try {
|
||||
SETUP_TOKEN = MailboxAuthToken.fromString(
|
||||
"54686973206973206120736574757020746f6b656e20666f722042726961722e");
|
||||
} catch (InvalidMailboxIdException e) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
static WeakSingletonProvider<OkHttpClient> createHttpClientProvider() {
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.socketFactory(SocketFactory.getDefault())
|
||||
.connectTimeout(60_000, MILLISECONDS)
|
||||
.build();
|
||||
return new WeakSingletonProvider<OkHttpClient>() {
|
||||
@Override
|
||||
@Nonnull
|
||||
public OkHttpClient createInstance() {
|
||||
return client;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static MailboxApi createMailboxApi() {
|
||||
return new MailboxApiImpl(createHttpClientProvider(), onion -> onion);
|
||||
}
|
||||
|
||||
static MailboxIntegrationTestComponent createTestComponent() {
|
||||
MailboxIntegrationTestComponent component =
|
||||
DaggerMailboxIntegrationTestComponent
|
||||
.builder()
|
||||
.ioModule(new TestIoModule())
|
||||
.mailboxModule(new TestMailboxModule())
|
||||
.build();
|
||||
BrambleCoreEagerSingletons.Helper.injectEagerSingletons(component);
|
||||
return component;
|
||||
}
|
||||
|
||||
@Module
|
||||
static class TestIoModule extends IoModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
WeakSingletonProvider<OkHttpClient> provideOkHttpClientProvider() {
|
||||
return createHttpClientProvider();
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
static class TestMailboxModule extends MailboxModule {
|
||||
|
||||
@Provides
|
||||
UrlConverter provideUrlConverter() {
|
||||
return onion -> onion;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
|
||||
import org.briarproject.bramble.api.mailbox.event.OwnMailboxConnectionStatusEvent;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.DbExpectations;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
|
||||
import static org.briarproject.bramble.test.TestUtils.hasEvent;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class MailboxManagerImplTest extends BrambleMockTestCase {
|
||||
|
||||
private final Executor ioExecutor = context.mock(Executor.class);
|
||||
private final MailboxApi api = context.mock(MailboxApi.class);
|
||||
private final TransactionManager db =
|
||||
context.mock(TransactionManager.class);
|
||||
private final MailboxSettingsManager mailboxSettingsManager =
|
||||
context.mock(MailboxSettingsManager.class);
|
||||
private final MailboxPairingTaskFactory pairingTaskFactory =
|
||||
context.mock(MailboxPairingTaskFactory.class);
|
||||
private final Clock clock =
|
||||
context.mock(Clock.class);
|
||||
|
||||
private final MailboxManagerImpl manager = new MailboxManagerImpl(
|
||||
ioExecutor, api, db, mailboxSettingsManager, pairingTaskFactory,
|
||||
clock);
|
||||
|
||||
@Test
|
||||
public void testDbExceptionDoesNotRecordFailure() throws Exception {
|
||||
Transaction txn = new Transaction(null, true);
|
||||
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).transactionWithNullableResult(with(true),
|
||||
withNullableDbCallable(txn));
|
||||
oneOf(mailboxSettingsManager).getOwnMailboxProperties(txn);
|
||||
will(throwException(new DbException()));
|
||||
}});
|
||||
|
||||
assertFalse(manager.checkConnection());
|
||||
assertFalse(hasEvent(txn, OwnMailboxConnectionStatusEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIOExceptionDoesRecordFailure() throws Exception {
|
||||
Transaction txn = new Transaction(null, true);
|
||||
Transaction txn2 = new Transaction(null, false);
|
||||
MailboxProperties props = getMailboxProperties(true, CLIENT_SUPPORTS);
|
||||
long now = new Random().nextLong();
|
||||
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).transactionWithNullableResult(with(true),
|
||||
withNullableDbCallable(txn));
|
||||
oneOf(mailboxSettingsManager).getOwnMailboxProperties(txn);
|
||||
will(returnValue(props));
|
||||
oneOf(api).getServerSupports(props);
|
||||
will(throwException(new IOException()));
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(now));
|
||||
oneOf(db).transaction(with(false), withDbRunnable(txn2));
|
||||
oneOf(mailboxSettingsManager)
|
||||
.recordFailedConnectionAttempt(txn2, now);
|
||||
}});
|
||||
|
||||
assertFalse(manager.checkConnection());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testApiExceptionDoesRecordFailure() throws Exception {
|
||||
Transaction txn = new Transaction(null, true);
|
||||
Transaction txn2 = new Transaction(null, false);
|
||||
MailboxProperties props = getMailboxProperties(true, CLIENT_SUPPORTS);
|
||||
long now = new Random().nextLong();
|
||||
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).transactionWithNullableResult(with(true),
|
||||
withNullableDbCallable(txn));
|
||||
oneOf(mailboxSettingsManager).getOwnMailboxProperties(txn);
|
||||
will(returnValue(props));
|
||||
oneOf(api).getServerSupports(props);
|
||||
will(throwException(new MailboxApi.ApiException()));
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(now));
|
||||
oneOf(db).transaction(with(false), withDbRunnable(txn2));
|
||||
oneOf(mailboxSettingsManager)
|
||||
.recordFailedConnectionAttempt(txn2, now);
|
||||
}});
|
||||
|
||||
assertFalse(manager.checkConnection());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectionSuccess() throws Exception {
|
||||
Transaction txn = new Transaction(null, true);
|
||||
Transaction txn2 = new Transaction(null, false);
|
||||
MailboxProperties props = getMailboxProperties(true, CLIENT_SUPPORTS);
|
||||
long now = new Random().nextLong();
|
||||
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).transactionWithNullableResult(with(true),
|
||||
withNullableDbCallable(txn));
|
||||
oneOf(mailboxSettingsManager).getOwnMailboxProperties(txn);
|
||||
will(returnValue(props));
|
||||
oneOf(api).getServerSupports(props);
|
||||
will(returnValue(CLIENT_SUPPORTS));
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(now));
|
||||
oneOf(db).transaction(with(false), withDbRunnable(txn2));
|
||||
oneOf(mailboxSettingsManager)
|
||||
.recordSuccessfulConnection(txn2, now, CLIENT_SUPPORTS);
|
||||
}});
|
||||
|
||||
assertTrue(manager.checkConnection());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdate;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.mailbox.event.OwnMailboxConnectionStatusEvent;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.DbExpectations;
|
||||
@@ -33,7 +32,6 @@ import static java.util.Collections.singletonList;
|
||||
import static org.briarproject.bramble.test.TestUtils.getContact;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.briarproject.bramble.test.TestUtils.hasEvent;
|
||||
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -57,7 +55,6 @@ public class MailboxPairingTaskImplTest extends BrambleMockTestCase {
|
||||
|
||||
private final String onion = getRandomString(56);
|
||||
private final byte[] onionBytes = getRandomBytes(32);
|
||||
private final String baseUrl = "http://" + onion + ".onion"; // TODO
|
||||
private final MailboxAuthToken setupToken =
|
||||
new MailboxAuthToken(getRandomId());
|
||||
private final MailboxAuthToken ownerToken =
|
||||
@@ -65,9 +62,9 @@ public class MailboxPairingTaskImplTest extends BrambleMockTestCase {
|
||||
private final String validPayload = getValidPayload();
|
||||
private final long time = System.currentTimeMillis();
|
||||
private final MailboxProperties setupProperties = new MailboxProperties(
|
||||
baseUrl, setupToken, new ArrayList<>());
|
||||
onion, setupToken, new ArrayList<>());
|
||||
private final MailboxProperties ownerProperties = new MailboxProperties(
|
||||
baseUrl, ownerToken, new ArrayList<>());
|
||||
onion, ownerToken, new ArrayList<>());
|
||||
|
||||
@Test
|
||||
public void testInitialQrCodeReceivedState() {
|
||||
@@ -138,7 +135,6 @@ public class MailboxPairingTaskImplTest extends BrambleMockTestCase {
|
||||
i.getAndIncrement();
|
||||
});
|
||||
task.run();
|
||||
hasEvent(txn, OwnMailboxConnectionStatusEvent.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -210,7 +206,7 @@ public class MailboxPairingTaskImplTest extends BrambleMockTestCase {
|
||||
private PredicateMatcher<MailboxProperties> matches(MailboxProperties p2) {
|
||||
return new PredicateMatcher<>(MailboxProperties.class, p1 ->
|
||||
p1.getAuthToken().equals(p2.getAuthToken()) &&
|
||||
p1.getBaseUrl().equals(p2.getBaseUrl()) &&
|
||||
p1.getOnion().equals(p2.getOnion()) &&
|
||||
p1.isOwner() == p2.isOwner() &&
|
||||
p1.getServerSupports().equals(p2.getServerSupports()));
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.briarproject.bramble.mailbox.MailboxSettingsManagerImpl.SETTINGS_KEY_ATTEMPTS;
|
||||
import static org.briarproject.bramble.mailbox.MailboxSettingsManagerImpl.SETTINGS_KEY_LAST_ATTEMPT;
|
||||
import static org.briarproject.bramble.mailbox.MailboxSettingsManagerImpl.SETTINGS_KEY_LAST_SUCCESS;
|
||||
@@ -83,7 +84,7 @@ public class MailboxSettingsManagerImplTest extends BrambleMockTestCase {
|
||||
|
||||
MailboxProperties properties = manager.getOwnMailboxProperties(txn);
|
||||
assertNotNull(properties);
|
||||
assertEquals(onion, properties.getBaseUrl());
|
||||
assertEquals(onion, properties.getOnion());
|
||||
assertEquals(token, properties.getAuthToken());
|
||||
assertEquals(serverSupports, properties.getServerSupports());
|
||||
assertTrue(properties.isOwner());
|
||||
@@ -162,6 +163,28 @@ public class MailboxSettingsManagerImplTest extends BrambleMockTestCase {
|
||||
}});
|
||||
|
||||
manager.recordSuccessfulConnection(txn, now);
|
||||
assertTrue(hasEvent(txn, OwnMailboxConnectionStatusEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecordsSuccessWithVersions() throws Exception {
|
||||
Transaction txn = new Transaction(null, false);
|
||||
List<MailboxVersion> versions = singletonList(new MailboxVersion(2, 1));
|
||||
Settings expectedSettings = new Settings();
|
||||
expectedSettings.putLong(SETTINGS_KEY_LAST_ATTEMPT, now);
|
||||
expectedSettings.putLong(SETTINGS_KEY_LAST_SUCCESS, now);
|
||||
expectedSettings.putInt(SETTINGS_KEY_ATTEMPTS, 0);
|
||||
expectedSettings.putInt(SETTINGS_KEY_SERVER_SUPPORTS, 0);
|
||||
int[] newVersionsInts = {2, 1};
|
||||
expectedSettings
|
||||
.putIntArray(SETTINGS_KEY_SERVER_SUPPORTS, newVersionsInts);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(settingsManager).mergeSettings(txn, expectedSettings,
|
||||
SETTINGS_NAMESPACE);
|
||||
}});
|
||||
|
||||
manager.recordSuccessfulConnection(txn, now, versions);
|
||||
hasEvent(txn, OwnMailboxConnectionStatusEvent.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ public class MailboxUpdateManagerImplTest extends BrambleMockTestCase {
|
||||
updateNoMailbox = new MailboxUpdate(someClientSupportsList);
|
||||
|
||||
updateProps = getMailboxProperties(false, someServerSupportsList);
|
||||
ownProps = new MailboxProperties(updateProps.getBaseUrl(),
|
||||
ownProps = new MailboxProperties(updateProps.getOnion(),
|
||||
updateProps.getAuthToken(), someServerSupportsList);
|
||||
updateWithMailbox = new MailboxUpdateWithMailbox(someClientSupportsList,
|
||||
updateProps);
|
||||
@@ -170,7 +170,7 @@ public class MailboxUpdateManagerImplTest extends BrambleMockTestCase {
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
expectStoreMessage(txn, contactGroup.getId(), 1, someClientSupports,
|
||||
emptyServerSupports, emptyPropsDict, true);
|
||||
emptyServerSupports, emptyPropsDict);
|
||||
|
||||
oneOf(clientHelper).mergeGroupMetadata(txn, localGroup.getId(),
|
||||
sentDict);
|
||||
@@ -225,7 +225,7 @@ public class MailboxUpdateManagerImplTest extends BrambleMockTestCase {
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
expectStoreMessage(txn, contactGroup.getId(), 1, someClientSupports,
|
||||
someServerSupports, propsDict, true);
|
||||
someServerSupports, propsDict);
|
||||
|
||||
oneOf(clientHelper).mergeGroupMetadata(txn, localGroup.getId(),
|
||||
sentDict);
|
||||
@@ -276,7 +276,7 @@ public class MailboxUpdateManagerImplTest extends BrambleMockTestCase {
|
||||
contactGroup.getId());
|
||||
will(returnValue(emptyMessageMetadata));
|
||||
expectStoreMessage(txn, contactGroup.getId(), 1, someClientSupports,
|
||||
emptyServerSupports, emptyPropsDict, true);
|
||||
emptyServerSupports, emptyPropsDict);
|
||||
|
||||
oneOf(clientHelper).mergeGroupMetadata(txn, localGroup.getId(),
|
||||
sentDict);
|
||||
@@ -341,7 +341,7 @@ public class MailboxUpdateManagerImplTest extends BrambleMockTestCase {
|
||||
contactGroup.getId());
|
||||
will(returnValue(emptyMessageMetadata));
|
||||
expectStoreMessage(txn, contactGroup.getId(), 1, someClientSupports,
|
||||
emptyServerSupports, emptyPropsDict, true);
|
||||
emptyServerSupports, emptyPropsDict);
|
||||
|
||||
oneOf(clientHelper).mergeGroupMetadata(txn, localGroup.getId(),
|
||||
sentDict);
|
||||
@@ -400,7 +400,7 @@ public class MailboxUpdateManagerImplTest extends BrambleMockTestCase {
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
expectStoreMessage(txn, contactGroup.getId(), 2,
|
||||
newerClientSupports, someServerSupports, propsDict, true);
|
||||
newerClientSupports, someServerSupports, propsDict);
|
||||
oneOf(db).removeMessage(txn, messageId);
|
||||
|
||||
oneOf(clientHelper).mergeGroupMetadata(txn, localGroup.getId(),
|
||||
@@ -441,7 +441,7 @@ public class MailboxUpdateManagerImplTest extends BrambleMockTestCase {
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
expectStoreMessage(txn, contactGroup.getId(), 1, someClientSupports,
|
||||
emptyServerSupports, emptyPropsDict, true);
|
||||
emptyServerSupports, emptyPropsDict);
|
||||
}});
|
||||
|
||||
MailboxUpdateManagerImpl t = createInstance(someClientSupportsList);
|
||||
@@ -484,7 +484,7 @@ public class MailboxUpdateManagerImplTest extends BrambleMockTestCase {
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
expectStoreMessage(txn, contactGroup.getId(), 1, someClientSupports,
|
||||
someServerSupports, propsDict, true);
|
||||
someServerSupports, propsDict);
|
||||
}});
|
||||
|
||||
MailboxUpdateManagerImpl t = createInstance(someClientSupportsList);
|
||||
@@ -674,7 +674,7 @@ public class MailboxUpdateManagerImplTest extends BrambleMockTestCase {
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
expectStoreMessage(txn, contactGroup.getId(), 2, someClientSupports,
|
||||
someServerSupports, propsDict, true);
|
||||
someServerSupports, propsDict);
|
||||
oneOf(db).removeMessage(txn, latestId);
|
||||
}});
|
||||
|
||||
@@ -712,7 +712,7 @@ public class MailboxUpdateManagerImplTest extends BrambleMockTestCase {
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
expectStoreMessage(txn, contactGroup.getId(), 2, someClientSupports,
|
||||
emptyServerSupports, emptyPropsDict, true);
|
||||
emptyServerSupports, emptyPropsDict);
|
||||
oneOf(db).removeMessage(txn, latestId);
|
||||
}});
|
||||
|
||||
@@ -890,15 +890,14 @@ public class MailboxUpdateManagerImplTest extends BrambleMockTestCase {
|
||||
|
||||
private void expectStoreMessage(Transaction txn, GroupId g,
|
||||
long version, BdfList clientSupports, BdfList serverSupports,
|
||||
BdfDictionary properties, boolean local)
|
||||
throws Exception {
|
||||
BdfDictionary properties) throws Exception {
|
||||
BdfList body = BdfList.of(version, clientSupports, serverSupports,
|
||||
properties);
|
||||
Message message = getMessage(g);
|
||||
long timestamp = message.getTimestamp();
|
||||
BdfDictionary meta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_VERSION, version),
|
||||
new BdfEntry(MSG_KEY_LOCAL, local)
|
||||
new BdfEntry(MSG_KEY_LOCAL, true)
|
||||
);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
import org.briarproject.bramble.api.sync.event.MessageSharedEvent;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.CaptureArgumentAction;
|
||||
import org.briarproject.bramble.test.ConsumeArgumentAction;
|
||||
import org.briarproject.bramble.test.DbExpectations;
|
||||
import org.briarproject.bramble.test.RunAction;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.lib.action.DoAllAction;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.MAX_LATENCY;
|
||||
import static org.briarproject.bramble.mailbox.MailboxUploadWorker.CHECK_DELAY_MS;
|
||||
import static org.briarproject.bramble.mailbox.MailboxUploadWorker.RETRY_DELAY_MS;
|
||||
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
|
||||
import static org.briarproject.bramble.test.TestUtils.getContactId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class MailboxUploadWorkerTest extends BrambleMockTestCase {
|
||||
|
||||
private final Executor ioExecutor = context.mock(Executor.class);
|
||||
private final DatabaseComponent db = context.mock(DatabaseComponent.class);
|
||||
private final Clock clock = context.mock(Clock.class);
|
||||
private final TaskScheduler taskScheduler =
|
||||
context.mock(TaskScheduler.class);
|
||||
private final EventBus eventBus = context.mock(EventBus.class);
|
||||
private final ConnectivityChecker connectivityChecker =
|
||||
context.mock(ConnectivityChecker.class);
|
||||
private final MailboxApiCaller mailboxApiCaller =
|
||||
context.mock(MailboxApiCaller.class);
|
||||
private final MailboxApi mailboxApi = context.mock(MailboxApi.class);
|
||||
private final MailboxFileManager mailboxFileManager =
|
||||
context.mock(MailboxFileManager.class);
|
||||
private final Cancellable apiCall =
|
||||
context.mock(Cancellable.class, "apiCall");
|
||||
private final Cancellable wakeupTask =
|
||||
context.mock(Cancellable.class, "wakeupTask");
|
||||
private final Cancellable checkTask =
|
||||
context.mock(Cancellable.class, "checkTask");
|
||||
|
||||
private final MailboxProperties mailboxProperties =
|
||||
getMailboxProperties(false, CLIENT_SUPPORTS);
|
||||
private final long now = System.currentTimeMillis();
|
||||
private final MailboxFolderId folderId = new MailboxFolderId(getRandomId());
|
||||
private final ContactId contactId = getContactId();
|
||||
private final MessageId ackedId = new MessageId(getRandomId());
|
||||
private final MessageId sentId = new MessageId(getRandomId());
|
||||
private final MessageId newMessageId = new MessageId(getRandomId());
|
||||
|
||||
private File testDir, tempFile;
|
||||
private MailboxUploadWorker worker;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
testDir = getTestDirectory();
|
||||
tempFile = new File(testDir, "temp");
|
||||
worker = new MailboxUploadWorker(ioExecutor, db, clock, taskScheduler,
|
||||
eventBus, connectivityChecker, mailboxApiCaller, mailboxApi,
|
||||
mailboxFileManager, mailboxProperties, folderId, contactId);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
deleteTestDirectory(testDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChecksForDataWhenStartedAndRemovesObserverWhenDestroyed()
|
||||
throws Exception {
|
||||
// When the worker is started it should check for data to send
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectCheckForDataToSendNoDataWaiting();
|
||||
|
||||
worker.start();
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChecksConnectivityWhenStartedIfDataIsReady()
|
||||
throws Exception {
|
||||
Transaction recordTxn = new Transaction(null, false);
|
||||
|
||||
// When the worker is started it should check for data to send. As
|
||||
// there's data ready to send immediately, the worker should start a
|
||||
// connectivity check
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectCheckForDataToSendAndStartConnectivityCheck();
|
||||
|
||||
worker.start();
|
||||
|
||||
// Create the temporary file so we can test that it gets deleted
|
||||
assertTrue(testDir.mkdirs());
|
||||
assertTrue(tempFile.createNewFile());
|
||||
|
||||
// When the connectivity check succeeds, the worker should write a file
|
||||
// and start an upload task
|
||||
expectRunTaskOnIoExecutor();
|
||||
AtomicReference<ApiCall> upload = new AtomicReference<>();
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxFileManager).createAndWriteTempFileForUpload(
|
||||
with(contactId), with(any(OutgoingSessionRecord.class)));
|
||||
will(new DoAllAction(
|
||||
// Record some IDs as acked and sent
|
||||
new ConsumeArgumentAction<>(OutgoingSessionRecord.class, 1,
|
||||
record -> {
|
||||
record.onAckSent(singletonList(ackedId));
|
||||
record.onMessageSent(sentId);
|
||||
}),
|
||||
returnValue(tempFile)
|
||||
));
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(upload, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the upload task runs, it should upload the file, record
|
||||
// the acked/sent messages in the DB, and check for more data to send
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(mailboxApi).addFile(mailboxProperties, folderId, tempFile);
|
||||
oneOf(db).transaction(with(false), withDbRunnable(recordTxn));
|
||||
oneOf(db).setAckSent(recordTxn, contactId, singletonList(ackedId));
|
||||
oneOf(db).setMessagesSent(recordTxn, contactId,
|
||||
singletonList(sentId), MAX_LATENCY);
|
||||
}});
|
||||
expectCheckForDataToSendNoDataWaiting();
|
||||
|
||||
assertFalse(upload.get().callApi());
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
|
||||
// The file should have been deleted
|
||||
assertFalse(tempFile.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelsApiCallWhenDestroyed() throws Exception {
|
||||
// When the worker is started it should check for data to send. As
|
||||
// there's data ready to send immediately, the worker should start a
|
||||
// connectivity check
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectCheckForDataToSendAndStartConnectivityCheck();
|
||||
|
||||
worker.start();
|
||||
|
||||
// Create the temporary file so we can test that it gets deleted
|
||||
assertTrue(testDir.mkdirs());
|
||||
assertTrue(tempFile.createNewFile());
|
||||
|
||||
// When the connectivity check succeeds, the worker should write a file
|
||||
// and start an upload task
|
||||
expectRunTaskOnIoExecutor();
|
||||
AtomicReference<ApiCall> upload = new AtomicReference<>();
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxFileManager).createAndWriteTempFileForUpload(
|
||||
with(contactId), with(any(OutgoingSessionRecord.class)));
|
||||
will(new DoAllAction(
|
||||
// Record some IDs as acked and sent
|
||||
new ConsumeArgumentAction<>(OutgoingSessionRecord.class, 1,
|
||||
record -> {
|
||||
record.onAckSent(singletonList(ackedId));
|
||||
record.onMessageSent(sentId);
|
||||
}),
|
||||
returnValue(tempFile)
|
||||
));
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(upload, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener and cancel the upload task
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(apiCall).cancel();
|
||||
}});
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
|
||||
// The file should have been deleted
|
||||
assertFalse(tempFile.exists());
|
||||
|
||||
// If the upload task runs anyway (cancellation came too late), it
|
||||
// should return early when it finds the state has changed
|
||||
assertFalse(upload.get().callApi());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchedulesWakeupWhenStartedIfDataIsNotReady()
|
||||
throws Exception {
|
||||
// When the worker is started it should check for data to send. As
|
||||
// the data isn't ready to send immediately, the worker should
|
||||
// schedule a wakeup
|
||||
expectRunTaskOnIoExecutor();
|
||||
AtomicReference<Runnable> wakeup = new AtomicReference<>();
|
||||
expectCheckForDataToSendAndScheduleWakeup(wakeup);
|
||||
|
||||
worker.start();
|
||||
|
||||
// When the wakeup task runs it should check for data to send
|
||||
expectCheckForDataToSendNoDataWaiting();
|
||||
|
||||
wakeup.get().run();
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelsWakeupIfDestroyedBeforeWakingUp() throws Exception {
|
||||
// When the worker is started it should check for data to send. As
|
||||
// the data isn't ready to send immediately, the worker should
|
||||
// schedule a wakeup
|
||||
expectRunTaskOnIoExecutor();
|
||||
AtomicReference<Runnable> wakeup = new AtomicReference<>();
|
||||
expectCheckForDataToSendAndScheduleWakeup(wakeup);
|
||||
|
||||
worker.start();
|
||||
|
||||
// When the worker is destroyed it should cancel the wakeup and
|
||||
// remove the connectivity observer and event listener
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(wakeupTask).cancel();
|
||||
}});
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
|
||||
// If the wakeup task runs anyway (cancellation came too late), it
|
||||
// should return early without doing anything
|
||||
wakeup.get().run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelsWakeupIfEventIsReceivedBeforeWakingUp()
|
||||
throws Exception {
|
||||
// When the worker is started it should check for data to send. As
|
||||
// the data isn't ready to send immediately, the worker should
|
||||
// schedule a wakeup
|
||||
expectRunTaskOnIoExecutor();
|
||||
AtomicReference<Runnable> wakeup = new AtomicReference<>();
|
||||
expectCheckForDataToSendAndScheduleWakeup(wakeup);
|
||||
|
||||
worker.start();
|
||||
|
||||
// Before the wakeup task runs, the worker receives an event that
|
||||
// indicates new data may be available. The worker should cancel the
|
||||
// wakeup task and schedule a check for new data after a short delay
|
||||
AtomicReference<Runnable> check = new AtomicReference<>();
|
||||
expectScheduleCheck(check, CHECK_DELAY_MS);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(wakeupTask).cancel();
|
||||
}});
|
||||
|
||||
worker.eventOccurred(new MessageSharedEvent(newMessageId));
|
||||
|
||||
// If the wakeup task runs anyway (cancellation came too late), it
|
||||
// should return early when it finds the state has changed
|
||||
wakeup.get().run();
|
||||
|
||||
// Before the check task runs, the worker receives another event that
|
||||
// indicates new data may be available. The event should be ignored,
|
||||
// as a check for new data has already been scheduled
|
||||
worker.eventOccurred(new MessageSharedEvent(newMessageId));
|
||||
|
||||
// When the check task runs, it should check for new data
|
||||
expectCheckForDataToSendNoDataWaiting();
|
||||
|
||||
check.get().run();
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelsCheckWhenDestroyed() throws Exception {
|
||||
// When the worker is started it should check for data to send
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectCheckForDataToSendNoDataWaiting();
|
||||
|
||||
worker.start();
|
||||
|
||||
// The worker receives an event that indicates new data may be
|
||||
// available. The worker should schedule a check for new data after
|
||||
// a short delay
|
||||
AtomicReference<Runnable> check = new AtomicReference<>();
|
||||
expectScheduleCheck(check, CHECK_DELAY_MS);
|
||||
|
||||
worker.eventOccurred(new MessageSharedEvent(newMessageId));
|
||||
|
||||
// When the worker is destroyed it should cancel the check and
|
||||
// remove the connectivity observer and event listener
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(checkTask).cancel();
|
||||
}});
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
|
||||
// If the check runs anyway (cancellation came too late), it should
|
||||
// return early when it finds the state has changed
|
||||
check.get().run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetriesAfterDelayIfExceptionOccursWhileWritingFile()
|
||||
throws Exception {
|
||||
// When the worker is started it should check for data to send. As
|
||||
// there's data ready to send immediately, the worker should start a
|
||||
// connectivity check
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectCheckForDataToSendAndStartConnectivityCheck();
|
||||
|
||||
worker.start();
|
||||
|
||||
// When the connectivity check succeeds, the worker should try to
|
||||
// write a file. This fails with an exception, so the worker should
|
||||
// retry by scheduling a check for new data after a short delay
|
||||
expectRunTaskOnIoExecutor();
|
||||
AtomicReference<Runnable> check = new AtomicReference<>();
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxFileManager).createAndWriteTempFileForUpload(
|
||||
with(contactId), with(any(OutgoingSessionRecord.class)));
|
||||
will(throwException(new IOException())); // Oh noes!
|
||||
}});
|
||||
expectScheduleCheck(check, RETRY_DELAY_MS);
|
||||
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the check task runs it should check for new data
|
||||
expectCheckForDataToSendNoDataWaiting();
|
||||
|
||||
check.get().run();
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
private void expectRunTaskOnIoExecutor() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(ioExecutor).execute(with(any(Runnable.class)));
|
||||
will(new RunAction());
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectCheckForDataToSendNoDataWaiting() throws Exception {
|
||||
Transaction txn = new Transaction(null, true);
|
||||
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).transaction(with(true), withDbRunnable(txn));
|
||||
oneOf(db).containsAcksToSend(txn, contactId);
|
||||
will(returnValue(false));
|
||||
oneOf(db).getNextSendTime(txn, contactId, MAX_LATENCY);
|
||||
will(returnValue(Long.MAX_VALUE)); // No data waiting
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectCheckForDataToSendAndScheduleWakeup(
|
||||
AtomicReference<Runnable> wakeup) throws Exception {
|
||||
Transaction txn = new Transaction(null, true);
|
||||
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).transaction(with(true), withDbRunnable(txn));
|
||||
oneOf(db).containsAcksToSend(txn, contactId);
|
||||
will(returnValue(false));
|
||||
oneOf(db).getNextSendTime(txn, contactId, MAX_LATENCY);
|
||||
will(returnValue(now + 1234L)); // Data waiting but not ready
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(now));
|
||||
oneOf(taskScheduler).schedule(with(any(Runnable.class)),
|
||||
with(ioExecutor), with(1234L), with(MILLISECONDS));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(wakeup, Runnable.class, 0),
|
||||
returnValue(wakeupTask)
|
||||
));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectCheckForDataToSendAndStartConnectivityCheck()
|
||||
throws Exception {
|
||||
Transaction txn = new Transaction(null, true);
|
||||
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).transaction(with(true), withDbRunnable(txn));
|
||||
oneOf(db).containsAcksToSend(txn, contactId);
|
||||
will(returnValue(false));
|
||||
oneOf(db).getNextSendTime(txn, contactId, MAX_LATENCY);
|
||||
will(returnValue(0L)); // Data ready to send
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(now));
|
||||
oneOf(connectivityChecker).checkConnectivity(mailboxProperties,
|
||||
worker);
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectScheduleCheck(AtomicReference<Runnable> check,
|
||||
long delay) {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(taskScheduler).schedule(with(any(Runnable.class)),
|
||||
with(ioExecutor), with(delay), with(MILLISECONDS));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(check, Runnable.class, 0),
|
||||
returnValue(checkTask)
|
||||
));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectRemoveObserverAndListener() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(connectivityChecker).removeObserver(worker);
|
||||
oneOf(eventBus).removeListener(worker);
|
||||
}});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.jmock.Expectations;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
|
||||
import static org.briarproject.bramble.test.TestUtils.getContactId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
|
||||
public class OwnMailboxClientTest extends BrambleMockTestCase {
|
||||
|
||||
private final MailboxWorkerFactory workerFactory =
|
||||
context.mock(MailboxWorkerFactory.class);
|
||||
private final ConnectivityChecker connectivityChecker =
|
||||
context.mock(ConnectivityChecker.class);
|
||||
private final TorReachabilityMonitor reachabilityMonitor =
|
||||
context.mock(TorReachabilityMonitor.class);
|
||||
private final MailboxWorker contactListWorker =
|
||||
context.mock(MailboxWorker.class, "contactListWorker");
|
||||
private final MailboxWorker uploadWorker1 =
|
||||
context.mock(MailboxWorker.class, "uploadWorker1");
|
||||
private final MailboxWorker uploadWorker2 =
|
||||
context.mock(MailboxWorker.class, "uploadWorker2");
|
||||
private final MailboxWorker downloadWorker =
|
||||
context.mock(MailboxWorker.class, "downloadWorker");
|
||||
|
||||
private final MailboxProperties properties =
|
||||
getMailboxProperties(true, CLIENT_SUPPORTS);
|
||||
private final MailboxFolderId folderId = new MailboxFolderId(getRandomId());
|
||||
private final ContactId contactId1 = getContactId();
|
||||
private final ContactId contactId2 = getContactId();
|
||||
|
||||
private final OwnMailboxClient client;
|
||||
|
||||
public OwnMailboxClientTest() {
|
||||
expectCreateContactListWorker();
|
||||
client = new OwnMailboxClient(workerFactory, connectivityChecker,
|
||||
reachabilityMonitor, properties);
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartAndDestroyWithNoContactsAssigned() {
|
||||
expectStartWorker(contactListWorker);
|
||||
client.start();
|
||||
|
||||
expectDestroyWorker(contactListWorker);
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAssignContactForUploadAndDestroyClient() {
|
||||
expectStartWorker(contactListWorker);
|
||||
client.start();
|
||||
|
||||
// When the contact is assigned, the worker should be created and
|
||||
// started
|
||||
expectCreateUploadWorker(contactId1, uploadWorker1);
|
||||
expectStartWorker(uploadWorker1);
|
||||
client.assignContactForUpload(contactId1, properties, folderId);
|
||||
|
||||
// When the client is destroyed, the worker should be destroyed
|
||||
expectDestroyWorker(uploadWorker1);
|
||||
expectDestroyWorker(contactListWorker);
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAssignAndDeassignContactForUpload() {
|
||||
expectStartWorker(contactListWorker);
|
||||
client.start();
|
||||
|
||||
// When the contact is assigned, the worker should be created and
|
||||
// started
|
||||
expectCreateUploadWorker(contactId1, uploadWorker1);
|
||||
expectStartWorker(uploadWorker1);
|
||||
client.assignContactForUpload(contactId1, properties, folderId);
|
||||
|
||||
// When the contact is deassigned, the worker should be destroyed
|
||||
expectDestroyWorker(uploadWorker1);
|
||||
client.deassignContactForUpload(contactId1);
|
||||
context.assertIsSatisfied();
|
||||
|
||||
expectDestroyWorker(contactListWorker);
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAssignAndDeassignTwoContactsForUpload() {
|
||||
expectStartWorker(contactListWorker);
|
||||
client.start();
|
||||
|
||||
// When the first contact is assigned, the first worker should be
|
||||
// created and started
|
||||
expectCreateUploadWorker(contactId1, uploadWorker1);
|
||||
expectStartWorker(uploadWorker1);
|
||||
client.assignContactForUpload(contactId1, properties, folderId);
|
||||
|
||||
// When the second contact is assigned, the second worker should be
|
||||
// created and started
|
||||
expectCreateUploadWorker(contactId2, uploadWorker2);
|
||||
expectStartWorker(uploadWorker2);
|
||||
client.assignContactForUpload(contactId2, properties, folderId);
|
||||
|
||||
// When the second contact is deassigned, the second worker should be
|
||||
// destroyed
|
||||
expectDestroyWorker(uploadWorker2);
|
||||
client.deassignContactForUpload(contactId2);
|
||||
context.assertIsSatisfied();
|
||||
|
||||
// When the first contact is deassigned, the first worker should be
|
||||
// destroyed
|
||||
expectDestroyWorker(uploadWorker1);
|
||||
client.deassignContactForUpload(contactId1);
|
||||
context.assertIsSatisfied();
|
||||
|
||||
expectDestroyWorker(contactListWorker);
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAssignContactForDownloadAndDestroyClient() {
|
||||
expectStartWorker(contactListWorker);
|
||||
client.start();
|
||||
|
||||
// When the contact is assigned, the worker should be created and
|
||||
// started
|
||||
expectCreateDownloadWorker();
|
||||
expectStartWorker(downloadWorker);
|
||||
client.assignContactForDownload(contactId1, properties, folderId);
|
||||
|
||||
// When the client is destroyed, the worker should be destroyed
|
||||
expectDestroyWorker(downloadWorker);
|
||||
expectDestroyWorker(contactListWorker);
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAssignAndDeassignTwoContactsForDownload() {
|
||||
expectStartWorker(contactListWorker);
|
||||
client.start();
|
||||
|
||||
// When the first contact is assigned, the worker should be created and
|
||||
// started
|
||||
expectCreateDownloadWorker();
|
||||
expectStartWorker(downloadWorker);
|
||||
client.assignContactForDownload(contactId1, properties, folderId);
|
||||
|
||||
// When the second contact is assigned, nothing should happen to the
|
||||
// worker
|
||||
client.assignContactForDownload(contactId2, properties, folderId);
|
||||
|
||||
// When the first contact is deassigned, nothing should happen to the
|
||||
// worker
|
||||
client.deassignContactForDownload(contactId1);
|
||||
|
||||
// When the second contact is deassigned, the worker should be
|
||||
// destroyed
|
||||
expectDestroyWorker(downloadWorker);
|
||||
client.deassignContactForDownload(contactId2);
|
||||
context.assertIsSatisfied();
|
||||
|
||||
expectDestroyWorker(contactListWorker);
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
private void expectCreateContactListWorker() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(workerFactory).createContactListWorkerForOwnMailbox(
|
||||
connectivityChecker, properties);
|
||||
will(returnValue(contactListWorker));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectCreateUploadWorker(ContactId contactId,
|
||||
MailboxWorker worker) {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(workerFactory).createUploadWorker(connectivityChecker,
|
||||
properties, folderId, contactId);
|
||||
will(returnValue(worker));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectCreateDownloadWorker() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(workerFactory).createDownloadWorkerForOwnMailbox(
|
||||
connectivityChecker, reachabilityMonitor, properties);
|
||||
will(returnValue(downloadWorker));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectStartWorker(MailboxWorker worker) {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(worker).start();
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectDestroyWorker(MailboxWorker worker) {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(worker).destroy();
|
||||
}});
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
@@ -15,8 +16,10 @@ import org.jmock.lib.action.DoAllAction;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -39,6 +42,8 @@ public class OwnMailboxConnectivityCheckerTest extends BrambleMockTestCase {
|
||||
private final MailboxProperties properties =
|
||||
getMailboxProperties(true, CLIENT_SUPPORTS);
|
||||
private final long now = System.currentTimeMillis();
|
||||
private final List<MailboxVersion> serverSupports =
|
||||
singletonList(new MailboxVersion(123, 456));
|
||||
|
||||
@Test
|
||||
public void testObserverIsCalledWhenCheckSucceeds() throws Exception {
|
||||
@@ -62,12 +67,13 @@ public class OwnMailboxConnectivityCheckerTest extends BrambleMockTestCase {
|
||||
// When the check succeeds, the success should be recorded in the DB
|
||||
// and the observer should be called
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(mailboxApi).checkStatus(properties);
|
||||
will(returnValue(true));
|
||||
oneOf(mailboxApi).getServerSupports(properties);
|
||||
will(returnValue(serverSupports));
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(now));
|
||||
oneOf(db).transaction(with(false), withDbRunnable(txn));
|
||||
oneOf(mailboxSettingsManager).recordSuccessfulConnection(txn, now);
|
||||
oneOf(mailboxSettingsManager).recordSuccessfulConnection(txn, now,
|
||||
serverSupports);
|
||||
oneOf(observer).onConnectivityCheckSucceeded();
|
||||
}});
|
||||
|
||||
@@ -97,7 +103,7 @@ public class OwnMailboxConnectivityCheckerTest extends BrambleMockTestCase {
|
||||
// When the check fails, the failure should be recorded in the DB and
|
||||
// the observer should not be called
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(mailboxApi).checkStatus(properties);
|
||||
oneOf(mailboxApi).getServerSupports(properties);
|
||||
will(throwException(new IOException()));
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(now));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.test.BrambleTestCase;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.createTestComponent;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class OwnMailboxContactListWorkerIntegrationTest
|
||||
extends BrambleTestCase {
|
||||
|
||||
private MailboxIntegrationTestComponent component;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
component = createTestComponent();
|
||||
}
|
||||
|
||||
// Just test that we can build the component. TODO: Write actual tests
|
||||
@Test
|
||||
public void testBuild() {
|
||||
assertTrue(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
|
||||
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.NoSuchContactException;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdate;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateWithMailbox;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.MailboxContact;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.CaptureArgumentAction;
|
||||
import org.briarproject.bramble.test.DbExpectations;
|
||||
import org.briarproject.bramble.test.RunAction;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.lib.action.DoAllAction;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
|
||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||
import static org.briarproject.bramble.test.TestUtils.getContact;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class OwnMailboxContactListWorkerTest extends BrambleMockTestCase {
|
||||
|
||||
private final Executor ioExecutor = context.mock(Executor.class);
|
||||
private final DatabaseComponent db = context.mock(DatabaseComponent.class);
|
||||
private final EventBus eventBus = context.mock(EventBus.class);
|
||||
private final ConnectivityChecker connectivityChecker =
|
||||
context.mock(ConnectivityChecker.class);
|
||||
private final MailboxApiCaller mailboxApiCaller =
|
||||
context.mock(MailboxApiCaller.class);
|
||||
private final MailboxApi mailboxApi = context.mock(MailboxApi.class);
|
||||
private final MailboxUpdateManager mailboxUpdateManager =
|
||||
context.mock(MailboxUpdateManager.class);
|
||||
private final Cancellable apiCall = context.mock(Cancellable.class);
|
||||
|
||||
private final MailboxProperties mailboxProperties =
|
||||
getMailboxProperties(true, CLIENT_SUPPORTS);
|
||||
private final Contact contact1 = getContact(), contact2 = getContact();
|
||||
private final MailboxProperties contactProperties1 =
|
||||
getMailboxProperties(false, CLIENT_SUPPORTS);
|
||||
private final MailboxProperties contactProperties2 =
|
||||
getMailboxProperties(false, CLIENT_SUPPORTS);
|
||||
private final MailboxUpdateWithMailbox update1 =
|
||||
new MailboxUpdateWithMailbox(CLIENT_SUPPORTS, contactProperties1);
|
||||
private final MailboxUpdateWithMailbox update2 =
|
||||
new MailboxUpdateWithMailbox(CLIENT_SUPPORTS, contactProperties2);
|
||||
|
||||
private final OwnMailboxContactListWorker worker =
|
||||
new OwnMailboxContactListWorker(ioExecutor, db, eventBus,
|
||||
connectivityChecker, mailboxApiCaller, mailboxApi,
|
||||
mailboxUpdateManager, mailboxProperties);
|
||||
|
||||
@Test
|
||||
public void testChecksConnectivityWhenStartedAndRemovesObserverWhenDestroyed() {
|
||||
// When the worker is started it should start a connectivity check
|
||||
expectStartConnectivityCheck();
|
||||
|
||||
worker.start();
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveConnectivityObserverAndEventListener();
|
||||
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdatesContactListWhenConnectivityCheckSucceeds()
|
||||
throws Exception {
|
||||
// When the worker is started it should start a connectivity check
|
||||
expectStartConnectivityCheck();
|
||||
|
||||
worker.start();
|
||||
|
||||
// When the connectivity check succeeds, the worker should start a
|
||||
// task to fetch the remote contact list
|
||||
AtomicReference<ApiCall> fetchList = new AtomicReference<>();
|
||||
expectStartTaskToFetchRemoteContactList(fetchList);
|
||||
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the fetch task runs it should fetch the remote contact list,
|
||||
// load the local contact list, and find the differences. Contact 2
|
||||
// needs to be added and contact 1 needs to be removed. The worker
|
||||
// should load the mailbox update for contact 2 and start a task to
|
||||
// add contact 2 to the mailbox
|
||||
expectFetchRemoteContactList(singletonList(contact1.getId()));
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectLoadLocalContactList(singletonList(contact2));
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectLoadMailboxUpdate(contact2, update2);
|
||||
AtomicReference<ApiCall> addContact = new AtomicReference<>();
|
||||
expectStartTaskToAddContact(addContact);
|
||||
|
||||
assertFalse(fetchList.get().callApi());
|
||||
|
||||
// When the add-contact task runs it should add contact 2 to the
|
||||
// mailbox, then continue with the next update
|
||||
AtomicReference<MailboxContact> added = new AtomicReference<>();
|
||||
expectAddContactToMailbox(added);
|
||||
AtomicReference<ApiCall> removeContact = new AtomicReference<>();
|
||||
expectStartTaskToRemoveContact(removeContact);
|
||||
|
||||
assertFalse(addContact.get().callApi());
|
||||
|
||||
// Check that the added contact has the expected properties
|
||||
MailboxContact expected = new MailboxContact(contact2.getId(),
|
||||
contactProperties2.getAuthToken(),
|
||||
requireNonNull(contactProperties2.getInboxId()),
|
||||
requireNonNull(contactProperties2.getOutboxId()));
|
||||
assertMailboxContactEquals(expected, added.get());
|
||||
|
||||
// When the remove-contact task runs it should remove contact 1 from
|
||||
// the mailbox
|
||||
expectRemoveContactFromMailbox(contact1);
|
||||
|
||||
assertFalse(removeContact.get().callApi());
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveConnectivityObserverAndEventListener();
|
||||
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlesEventsAfterMakingInitialUpdates() throws Exception {
|
||||
// When the worker is started it should start a connectivity check
|
||||
expectStartConnectivityCheck();
|
||||
|
||||
worker.start();
|
||||
|
||||
// When the connectivity check succeeds, the worker should start a
|
||||
// task to fetch the remote contact list
|
||||
AtomicReference<ApiCall> fetchList = new AtomicReference<>();
|
||||
expectStartTaskToFetchRemoteContactList(fetchList);
|
||||
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the fetch task runs it should fetch the remote contact list,
|
||||
// load the local contact list, and find the differences. The lists
|
||||
// are the same, so the worker should wait for changes
|
||||
expectFetchRemoteContactList(emptyList());
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectLoadLocalContactList(emptyList());
|
||||
|
||||
assertFalse(fetchList.get().callApi());
|
||||
|
||||
// When a contact is added, the worker should load the contact's
|
||||
// mailbox update and start a task to add the contact to the mailbox
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectLoadMailboxUpdate(contact1, update1);
|
||||
AtomicReference<ApiCall> addContact = new AtomicReference<>();
|
||||
expectStartTaskToAddContact(addContact);
|
||||
|
||||
worker.eventOccurred(new ContactAddedEvent(contact1.getId(), true));
|
||||
|
||||
// When the add-contact task runs it should add contact 1 to the
|
||||
// mailbox
|
||||
AtomicReference<MailboxContact> added = new AtomicReference<>();
|
||||
expectAddContactToMailbox(added);
|
||||
|
||||
assertFalse(addContact.get().callApi());
|
||||
|
||||
// Check that the added contact has the expected properties
|
||||
MailboxContact expected = new MailboxContact(contact1.getId(),
|
||||
contactProperties1.getAuthToken(),
|
||||
requireNonNull(contactProperties1.getInboxId()),
|
||||
requireNonNull(contactProperties1.getOutboxId()));
|
||||
assertMailboxContactEquals(expected, added.get());
|
||||
|
||||
// When the contact is removed again, the worker should start a task
|
||||
// to remove the contact from the mailbox
|
||||
expectRunTaskOnIoExecutor();
|
||||
AtomicReference<ApiCall> removeContact = new AtomicReference<>();
|
||||
expectStartTaskToRemoveContact(removeContact);
|
||||
|
||||
worker.eventOccurred(new ContactRemovedEvent(contact1.getId()));
|
||||
|
||||
// When the remove-contact task runs it should remove the contact from
|
||||
// the mailbox
|
||||
expectRemoveContactFromMailbox(contact1);
|
||||
|
||||
assertFalse(removeContact.get().callApi());
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveConnectivityObserverAndEventListener();
|
||||
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testHandlesNoSuchContactException() throws Exception {
|
||||
// When the worker is started it should start a connectivity check
|
||||
expectStartConnectivityCheck();
|
||||
|
||||
worker.start();
|
||||
|
||||
// When the connectivity check succeeds, the worker should start a
|
||||
// task to fetch the remote contact list
|
||||
AtomicReference<ApiCall> fetchList = new AtomicReference<>();
|
||||
expectStartTaskToFetchRemoteContactList(fetchList);
|
||||
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the fetch task runs it should fetch the remote contact list,
|
||||
// load the local contact list, and find the differences. Contact 1
|
||||
// needs to be added, so the worker should submit a task to the
|
||||
// IO executor to load the contact's mailbox update
|
||||
expectFetchRemoteContactList(emptyList());
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectLoadLocalContactList(singletonList(contact1));
|
||||
AtomicReference<Runnable> loadUpdate = new AtomicReference<>();
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(ioExecutor).execute(with(any(Runnable.class)));
|
||||
will(new CaptureArgumentAction<>(loadUpdate, Runnable.class, 0));
|
||||
}});
|
||||
|
||||
assertFalse(fetchList.get().callApi());
|
||||
|
||||
// Before the contact's mailbox update can be loaded, the contact
|
||||
// is removed
|
||||
worker.eventOccurred(new ContactRemovedEvent(contact1.getId()));
|
||||
|
||||
// When the load-update task runs, a NoSuchContactException is thrown.
|
||||
// The worker should abandon adding the contact and move on to the
|
||||
// next update, which is the removal of the same contact. The worker
|
||||
// should start a task to remove the contact from the mailbox
|
||||
Transaction txn = new Transaction(null, false);
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
|
||||
oneOf(mailboxUpdateManager).getLocalUpdate(txn, contact1.getId());
|
||||
will(throwException(new NoSuchContactException()));
|
||||
}});
|
||||
AtomicReference<ApiCall> removeContact = new AtomicReference<>();
|
||||
expectStartTaskToRemoveContact(removeContact);
|
||||
|
||||
loadUpdate.get().run();
|
||||
|
||||
// When the remove-contact task runs it should remove the contact from
|
||||
// the mailbox. The contact was never added, so the call throws a
|
||||
// TolerableFailureException
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxApi).deleteContact(mailboxProperties,
|
||||
contact1.getId());
|
||||
will(throwException(new TolerableFailureException()));
|
||||
}});
|
||||
|
||||
assertFalse(removeContact.get().callApi());
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveConnectivityObserverAndEventListener();
|
||||
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelsApiCallWhenDestroyed() {
|
||||
// When the worker is started it should start a connectivity check
|
||||
expectStartConnectivityCheck();
|
||||
|
||||
worker.start();
|
||||
|
||||
// When the connectivity check succeeds, the worker should start a
|
||||
// task to fetch the remote contact list
|
||||
AtomicReference<ApiCall> fetchList = new AtomicReference<>();
|
||||
expectStartTaskToFetchRemoteContactList(fetchList);
|
||||
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// The worker is destroyed before the task runs. The worker should
|
||||
// cancel the task remove the connectivity observer and event listener
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(apiCall).cancel();
|
||||
}});
|
||||
expectRemoveConnectivityObserverAndEventListener();
|
||||
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
private void expectStartConnectivityCheck() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(connectivityChecker).checkConnectivity(
|
||||
with(mailboxProperties), with(worker));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectRemoveConnectivityObserverAndEventListener() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(connectivityChecker).removeObserver(worker);
|
||||
oneOf(eventBus).removeListener(worker);
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectStartTaskToFetchRemoteContactList(
|
||||
AtomicReference<ApiCall> task) {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(task, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectFetchRemoteContactList(List<ContactId> remote)
|
||||
throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxApi).getContacts(mailboxProperties);
|
||||
will(returnValue(remote));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectLoadLocalContactList(List<Contact> local)
|
||||
throws Exception {
|
||||
Transaction txn = new Transaction(null, true);
|
||||
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).transaction(with(true), withDbRunnable(txn));
|
||||
oneOf(db).getContacts(txn);
|
||||
will(returnValue(local));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectLoadMailboxUpdate(Contact c, MailboxUpdate update)
|
||||
throws Exception {
|
||||
Transaction txn = new Transaction(null, true);
|
||||
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).transactionWithResult(with(true),
|
||||
withDbCallable(txn));
|
||||
oneOf(mailboxUpdateManager).getLocalUpdate(txn, c.getId());
|
||||
will(returnValue(update));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectStartTaskToAddContact(AtomicReference<ApiCall> task) {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(task, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectAddContactToMailbox(
|
||||
AtomicReference<MailboxContact> added) throws Exception {
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(mailboxApi).addContact(with(mailboxProperties),
|
||||
with(any(MailboxContact.class)));
|
||||
will(new CaptureArgumentAction<>(added, MailboxContact.class, 1));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectStartTaskToRemoveContact(AtomicReference<ApiCall> task) {
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(task, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectRemoveContactFromMailbox(Contact c) throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxApi).deleteContact(mailboxProperties, c.getId());
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectRunTaskOnIoExecutor() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(ioExecutor).execute(with(any(Runnable.class)));
|
||||
will(new RunAction());
|
||||
}});
|
||||
}
|
||||
|
||||
private void assertMailboxContactEquals(MailboxContact expected,
|
||||
MailboxContact actual) {
|
||||
assertEquals(expected.contactId, actual.contactId);
|
||||
assertEquals(expected.token, actual.token);
|
||||
assertEquals(expected.inboxId, actual.inboxId);
|
||||
assertEquals(expected.outboxId, actual.outboxId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFileId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
|
||||
import static org.briarproject.bramble.mailbox.MailboxDownloadWorker.FolderFile;
|
||||
import static org.briarproject.bramble.mailbox.OwnMailboxDownloadWorker.MAX_ROUND_ROBIN_FILES;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class OwnMailboxDownloadWorkerTest
|
||||
extends MailboxDownloadWorkerTest<OwnMailboxDownloadWorker> {
|
||||
|
||||
private final MailboxFolderId folderId1 =
|
||||
new MailboxFolderId(getRandomId());
|
||||
private final MailboxFolderId folderId2 =
|
||||
new MailboxFolderId(getRandomId());
|
||||
private final List<MailboxFolderId> folderIds =
|
||||
asList(folderId1, folderId2);
|
||||
|
||||
public OwnMailboxDownloadWorkerTest() {
|
||||
mailboxProperties = getMailboxProperties(true, CLIENT_SUPPORTS);
|
||||
worker = new OwnMailboxDownloadWorker(connectivityChecker,
|
||||
torReachabilityMonitor, mailboxApiCaller, mailboxApi,
|
||||
mailboxFileManager, mailboxProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() {
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChecksConnectivityWhenStartedAndRemovesObserverWhenDestroyed() {
|
||||
// When the worker is started it should start a connectivity check
|
||||
expectStartConnectivityCheck();
|
||||
worker.start();
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// and reachability observers
|
||||
expectRemoveObservers();
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChecksForFilesWhenConnectivityCheckSucceeds()
|
||||
throws Exception {
|
||||
// When the worker is started it should start a connectivity check
|
||||
expectStartConnectivityCheck();
|
||||
worker.start();
|
||||
|
||||
// When the connectivity check succeeds, a list-folders task should be
|
||||
// started for the first download cycle
|
||||
AtomicReference<ApiCall> listFoldersTask = new AtomicReference<>();
|
||||
expectStartTask(listFoldersTask);
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the list-folders tasks runs and finds no folders with files
|
||||
// to download, it should add a Tor reachability observer
|
||||
expectCheckForFoldersWithAvailableFiles(emptyList());
|
||||
expectAddReachabilityObserver();
|
||||
assertFalse(listFoldersTask.get().callApi());
|
||||
|
||||
// When the reachability observer is called, a list-folders task should
|
||||
// be started for the second download cycle
|
||||
expectStartTask(listFoldersTask);
|
||||
worker.onTorReachable();
|
||||
|
||||
// When the list-folders tasks runs and finds no folders with files
|
||||
// to download, it should finish the second download cycle
|
||||
expectCheckForFoldersWithAvailableFiles(emptyList());
|
||||
assertFalse(listFoldersTask.get().callApi());
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// and reachability observers
|
||||
expectRemoveObservers();
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadsFilesWhenConnectivityCheckSucceeds()
|
||||
throws Exception {
|
||||
// When the worker is started it should start a connectivity check
|
||||
expectStartConnectivityCheck();
|
||||
worker.start();
|
||||
|
||||
// When the connectivity check succeeds, a list-folders task should be
|
||||
// started for the first download cycle
|
||||
AtomicReference<ApiCall> listFoldersTask = new AtomicReference<>();
|
||||
expectStartTask(listFoldersTask);
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the list-folders tasks runs and finds some folders with files
|
||||
// to download, it should start a list-files task for the first folder
|
||||
AtomicReference<ApiCall> listFilesTask = new AtomicReference<>();
|
||||
expectCheckForFoldersWithAvailableFiles(folderIds);
|
||||
expectStartTask(listFilesTask);
|
||||
assertFalse(listFoldersTask.get().callApi());
|
||||
|
||||
// When the first list-files task runs and finds no files to download,
|
||||
// it should start a second list-files task for the next folder
|
||||
expectCheckForFiles(folderId1, emptyList());
|
||||
expectStartTask(listFilesTask);
|
||||
assertFalse(listFilesTask.get().callApi());
|
||||
|
||||
// When the second list-files task runs and finds some files to
|
||||
// download, it should create the round-robin queue and start a
|
||||
// download task for the first file
|
||||
AtomicReference<ApiCall> downloadTask = new AtomicReference<>();
|
||||
expectCheckForFiles(folderId2, files);
|
||||
expectStartTask(downloadTask);
|
||||
assertFalse(listFilesTask.get().callApi());
|
||||
|
||||
// When the first download task runs it should download the file to the
|
||||
// location provided by the file manager and start a delete task
|
||||
AtomicReference<ApiCall> deleteTask = new AtomicReference<>();
|
||||
expectDownloadFile(folderId2, file1);
|
||||
expectStartTask(deleteTask);
|
||||
assertFalse(downloadTask.get().callApi());
|
||||
|
||||
// When the first delete task runs it should delete the file, ignore
|
||||
// the tolerable failure, and start a download task for the next file
|
||||
expectDeleteFile(folderId2, file1, true); // Delete fails tolerably
|
||||
expectStartTask(downloadTask);
|
||||
assertFalse(deleteTask.get().callApi());
|
||||
|
||||
// When the second download task runs it should download the file to
|
||||
// the location provided by the file manager and start a delete task
|
||||
expectDownloadFile(folderId2, file2);
|
||||
expectStartTask(deleteTask);
|
||||
assertFalse(downloadTask.get().callApi());
|
||||
|
||||
// When the second delete task runs it should delete the file and
|
||||
// start a list-inbox task to check for files that may have arrived
|
||||
// since the first download cycle started
|
||||
expectDeleteFile(folderId2, file2, false); // Delete succeeds
|
||||
expectStartTask(listFoldersTask);
|
||||
assertFalse(deleteTask.get().callApi());
|
||||
|
||||
// When the list-inbox tasks runs and finds no more files to download,
|
||||
// it should add a Tor reachability observer
|
||||
expectCheckForFoldersWithAvailableFiles(emptyList());
|
||||
expectAddReachabilityObserver();
|
||||
assertFalse(listFoldersTask.get().callApi());
|
||||
|
||||
// When the reachability observer is called, a list-inbox task should
|
||||
// be started for the second download cycle
|
||||
expectStartTask(listFoldersTask);
|
||||
worker.onTorReachable();
|
||||
|
||||
// When the list-inbox tasks runs and finds no more files to download,
|
||||
// it should finish the second download cycle
|
||||
expectCheckForFoldersWithAvailableFiles(emptyList());
|
||||
assertFalse(listFoldersTask.get().callApi());
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// and reachability observers
|
||||
expectRemoveObservers();
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRoundRobinQueueVisitsAllFolders() {
|
||||
// Ten folders with two files each
|
||||
Map<MailboxFolderId, Queue<MailboxFile>> available =
|
||||
createAvailableFiles(10, 2);
|
||||
Queue<FolderFile> queue = worker.createRoundRobinQueue(available);
|
||||
// Check that all files were queued
|
||||
for (MailboxFolderId folderId : available.keySet()) {
|
||||
assertEquals(2, countFilesWithFolderId(queue, folderId));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSizeOfRoundRobinQueueIsLimited() {
|
||||
// Two folders with MAX_ROUND_ROBIN_FILES each
|
||||
Map<MailboxFolderId, Queue<MailboxFile>> available =
|
||||
createAvailableFiles(2, MAX_ROUND_ROBIN_FILES);
|
||||
Queue<FolderFile> queue = worker.createRoundRobinQueue(available);
|
||||
// Check that half the files in each folder were queued
|
||||
for (MailboxFolderId folderId : available.keySet()) {
|
||||
assertEquals(MAX_ROUND_ROBIN_FILES / 2,
|
||||
countFilesWithFolderId(queue, folderId));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<MailboxFolderId, Queue<MailboxFile>> createAvailableFiles(
|
||||
int numFolders, int numFiles) {
|
||||
Map<MailboxFolderId, Queue<MailboxFile>> available = new HashMap<>();
|
||||
List<MailboxFolderId> folderIds = createFolderIds(numFolders);
|
||||
for (MailboxFolderId folderId : folderIds) {
|
||||
available.put(folderId, createFiles(numFiles));
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
private List<MailboxFolderId> createFolderIds(int size) {
|
||||
List<MailboxFolderId> folderIds = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
folderIds.add(new MailboxFolderId(getRandomId()));
|
||||
}
|
||||
return folderIds;
|
||||
}
|
||||
|
||||
private Queue<MailboxFile> createFiles(int size) {
|
||||
Queue<MailboxFile> files = new LinkedList<>();
|
||||
for (int i = 0; i < size; i++) {
|
||||
files.add(new MailboxFile(new MailboxFileId(getRandomId()), i));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
private int countFilesWithFolderId(Queue<FolderFile> queue,
|
||||
MailboxFolderId folderId) {
|
||||
int count = 0;
|
||||
for (FolderFile file : queue) {
|
||||
if (file.folderId.equals(folderId)) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.CaptureArgumentAction;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.lib.action.DoAllAction;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
@@ -38,13 +37,9 @@ public class TorReachabilityMonitorImplTest extends BrambleMockTestCase {
|
||||
private final TorReachabilityObserver observer =
|
||||
context.mock(TorReachabilityObserver.class);
|
||||
|
||||
private TorReachabilityMonitorImpl monitor;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
monitor = new TorReachabilityMonitorImpl(ioExecutor, taskScheduler,
|
||||
pluginManager, eventBus);
|
||||
}
|
||||
private final TorReachabilityMonitorImpl monitor =
|
||||
new TorReachabilityMonitorImpl(ioExecutor, taskScheduler,
|
||||
pluginManager, eventBus);
|
||||
|
||||
@Test
|
||||
public void testSchedulesTaskWhenStartedIfTorIsActive() {
|
||||
|
||||
@@ -26,7 +26,6 @@ import org.briarproject.bramble.test.ImmediateExecutor;
|
||||
import org.briarproject.bramble.test.RunAction;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.imposters.ByteBuddyClassImposteriser;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
@@ -59,22 +58,18 @@ public class PollerImplTest extends BrambleMockTestCase {
|
||||
private final SecureRandom random;
|
||||
|
||||
private final Executor ioExecutor = new ImmediateExecutor();
|
||||
private final Executor wakefulIoExecutor = new ImmediateExecutor();
|
||||
private final TransportId transportId = getTransportId();
|
||||
private final ContactId contactId = getContactId();
|
||||
private final TransportProperties properties = new TransportProperties();
|
||||
private final int pollingInterval = 60 * 1000;
|
||||
private final long now = System.currentTimeMillis();
|
||||
|
||||
private PollerImpl poller;
|
||||
private final PollerImpl poller;
|
||||
|
||||
public PollerImplTest() {
|
||||
context.setImposteriser(ByteBuddyClassImposteriser.INSTANCE);
|
||||
random = context.mock(SecureRandom.class);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Executor wakefulIoExecutor = new ImmediateExecutor();
|
||||
poller = new PollerImpl(ioExecutor, wakefulIoExecutor, scheduler,
|
||||
connectionManager, connectionRegistry, pluginManager,
|
||||
transportPropertyManager, random, clock);
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||
import org.briarproject.bramble.api.settings.Settings;
|
||||
import org.briarproject.bramble.test.BrambleTestCase;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -46,13 +45,11 @@ public class LanTcpPluginTest extends BrambleTestCase {
|
||||
|
||||
private final Backoff backoff = new TestBackoff();
|
||||
private final ExecutorService ioExecutor = newCachedThreadPool();
|
||||
private final Callback callback = new Callback();
|
||||
|
||||
private Callback callback = null;
|
||||
private LanTcpPlugin plugin = null;
|
||||
private final LanTcpPlugin plugin;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
callback = new Callback();
|
||||
public LanTcpPluginTest() {
|
||||
plugin = new LanTcpPlugin(ioExecutor, ioExecutor, backoff, callback,
|
||||
0, 0, 1000) {
|
||||
@Override
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user