mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-11 18:29:05 +01:00
Compare commits
254 Commits
release-1.
...
1528-log-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
580be7cfdc | ||
|
|
dbcea3e1d1 | ||
|
|
75b5c92495 | ||
|
|
bcc98cc4c9 | ||
|
|
2d605089bc | ||
|
|
01f8be1b66 | ||
|
|
eac6d0aa40 | ||
|
|
62af5e858c | ||
|
|
2201585a34 | ||
|
|
78f4dee43d | ||
|
|
bb71de1a78 | ||
|
|
08bf13e44f | ||
|
|
cc7de2c70a | ||
|
|
0f4aa8027a | ||
|
|
b161a5e115 | ||
|
|
e112f69c4e | ||
|
|
4623d03c93 | ||
|
|
b128220be3 | ||
|
|
6aa24af94c | ||
|
|
de63a50662 | ||
|
|
5517ac14ed | ||
|
|
2672d82a40 | ||
|
|
63c0210047 | ||
|
|
6e61827fe6 | ||
|
|
2be93f6a49 | ||
|
|
5eb994d3e8 | ||
|
|
f0c9819332 | ||
|
|
971dbf5df2 | ||
|
|
43a83df342 | ||
|
|
0092f38bab | ||
|
|
285a5f2928 | ||
|
|
804049209d | ||
|
|
2b1aed6caa | ||
|
|
44b0955b9d | ||
|
|
d43ef463a6 | ||
|
|
34337486e9 | ||
|
|
3ebbb2a8cf | ||
|
|
54339afab8 | ||
|
|
6c19b22aab | ||
|
|
6b790b59fa | ||
|
|
8b61a0279b | ||
|
|
94ce6bbb2c | ||
|
|
845d505d2b | ||
|
|
6358518f88 | ||
|
|
ef6e3bb2a7 | ||
|
|
8ec998f645 | ||
|
|
f75d63fc46 | ||
|
|
0c22c25995 | ||
|
|
7e249ecf70 | ||
|
|
274963d9d1 | ||
|
|
18b3865a86 | ||
|
|
f08688708a | ||
|
|
c37f6069c7 | ||
|
|
c8caae49f1 | ||
|
|
670cc34b12 | ||
|
|
f387c3801b | ||
|
|
aa759a636e | ||
|
|
0b85aca932 | ||
|
|
d4cdedeed7 | ||
|
|
9b10c12f23 | ||
|
|
2bf490b973 | ||
|
|
d2f25f2ebe | ||
|
|
b3dcde9187 | ||
|
|
241e5e9f6e | ||
|
|
c59524df65 | ||
|
|
4467f9e260 | ||
|
|
7e215e7f84 | ||
|
|
601ff50294 | ||
|
|
9f839d9d12 | ||
|
|
1e4c28a30a | ||
|
|
bc0f9a984c | ||
|
|
15e0abffb0 | ||
|
|
5254efb630 | ||
|
|
df22df22a0 | ||
|
|
23681ff7f7 | ||
|
|
57bebc0b87 | ||
|
|
82057da962 | ||
|
|
00b7518e49 | ||
|
|
418ab99a3c | ||
|
|
49c14af0dc | ||
|
|
3f7aed7886 | ||
|
|
d2728dd29b | ||
|
|
84afc6d934 | ||
|
|
a42d9eec1c | ||
|
|
5d5d8d206c | ||
|
|
5237df32e3 | ||
|
|
72e376f152 | ||
|
|
4d685a2617 | ||
|
|
16ab48d009 | ||
|
|
095bebf524 | ||
|
|
b67d9935c7 | ||
|
|
34aea945cb | ||
|
|
a82666b8bd | ||
|
|
e614046662 | ||
|
|
0691354952 | ||
|
|
aa997a9c64 | ||
|
|
f05cbac20a | ||
|
|
39c74f1363 | ||
|
|
2411c82d9c | ||
|
|
f43839dbb3 | ||
|
|
3138213f39 | ||
|
|
d080af4b7a | ||
|
|
9d19761dbe | ||
|
|
fa3a5be083 | ||
|
|
fa3db0f888 | ||
|
|
4b7ee62190 | ||
|
|
9d3c33fdbc | ||
|
|
37d4ca84f7 | ||
|
|
1b58d986ae | ||
|
|
784c7416ec | ||
|
|
7536f16c61 | ||
|
|
ab628c1921 | ||
|
|
85e53479f2 | ||
|
|
116ee97056 | ||
|
|
78938f1ac6 | ||
|
|
afff66eaff | ||
|
|
8c33ea5a6b | ||
|
|
96228c1fd0 | ||
|
|
eb6a5fe63e | ||
|
|
a8624cd507 | ||
|
|
e7fc37d81e | ||
|
|
7bd220f18d | ||
|
|
7f581fee15 | ||
|
|
383056d37e | ||
|
|
23316f5e9c | ||
|
|
dea05c85a2 | ||
|
|
b36066514b | ||
|
|
f9403782a2 | ||
|
|
174ca3cfb8 | ||
|
|
961af66c8e | ||
|
|
a86ea454d0 | ||
|
|
a7877bf7ee | ||
|
|
62ae0f745b | ||
|
|
f83abbe63d | ||
|
|
e0b6b8435d | ||
|
|
d3c7832245 | ||
|
|
cc4978c2b1 | ||
|
|
a043e8b1cf | ||
|
|
97ba18cfb2 | ||
|
|
bc013296f6 | ||
|
|
c1fabcd46b | ||
|
|
3c08e86822 | ||
|
|
de2c9670d5 | ||
|
|
9632754274 | ||
|
|
b275a0ffff | ||
|
|
74a3f54d28 | ||
|
|
edcb234b93 | ||
|
|
dae00c7e4e | ||
|
|
29b16c4d74 | ||
|
|
edd270abf3 | ||
|
|
47d412dd0a | ||
|
|
5d952ff68e | ||
|
|
9304a6b266 | ||
|
|
a99ec5ed51 | ||
|
|
40d58a9359 | ||
|
|
60a1a4d2d1 | ||
|
|
238aeb3abd | ||
|
|
62c16fad09 | ||
|
|
68e57bda0d | ||
|
|
0df73dbf0a | ||
|
|
5b648cbd35 | ||
|
|
5e7891d78a | ||
|
|
d5e17c8201 | ||
|
|
d572ae71e7 | ||
|
|
2e9d9dac84 | ||
|
|
573817c4c9 | ||
|
|
4f00f39d3f | ||
|
|
c7d3628ecb | ||
|
|
b198bef5f8 | ||
|
|
cff94009a1 | ||
|
|
44f9f0bbc5 | ||
|
|
5fdb43ce9b | ||
|
|
725d11d960 | ||
|
|
7cf2c2faa7 | ||
|
|
4b3c26feb6 | ||
|
|
2fbeb29195 | ||
|
|
5892fba237 | ||
|
|
cc9f04980a | ||
|
|
44fb2a5c59 | ||
|
|
68e534348f | ||
|
|
795a8f1e70 | ||
|
|
bf968b227e | ||
|
|
8b94dad01f | ||
|
|
fa0610fff1 | ||
|
|
1d94db8d60 | ||
|
|
1d4f450960 | ||
|
|
7f6b31d36c | ||
|
|
05737d858d | ||
|
|
2c8e2ab6b8 | ||
|
|
97f64fb31c | ||
|
|
e66152e812 | ||
|
|
101ffa2f08 | ||
|
|
13eebe393a | ||
|
|
5bc5791ddb | ||
|
|
a35e9af1de | ||
|
|
ade89c14c4 | ||
|
|
16cfb89310 | ||
|
|
78f00863dd | ||
|
|
bd50a109cd | ||
|
|
38c91aea32 | ||
|
|
92517ae7c0 | ||
|
|
dd1c8c8301 | ||
|
|
edc1029e92 | ||
|
|
27e9338a12 | ||
|
|
243df3096a | ||
|
|
50f9718037 | ||
|
|
88c8bd32a5 | ||
|
|
3e597ceff8 | ||
|
|
3d6972fd73 | ||
|
|
288f3331ec | ||
|
|
a14ee55f12 | ||
|
|
2a85907565 | ||
|
|
1fe7b2f451 | ||
|
|
585ceb626b | ||
|
|
5da782cf18 | ||
|
|
ec6b999d30 | ||
|
|
d8a925a94f | ||
|
|
3de4386e63 | ||
|
|
8c60787866 | ||
|
|
fa8ca8e6cf | ||
|
|
07814d43de | ||
|
|
d80ba0f556 | ||
|
|
d70e1ed32e | ||
|
|
eec2c87797 | ||
|
|
a256027916 | ||
|
|
bf0f99277a | ||
|
|
2d62deb2db | ||
|
|
e3682bb331 | ||
|
|
6805040ac4 | ||
|
|
4198e1f22a | ||
|
|
ee11d2a28d | ||
|
|
f3718e496c | ||
|
|
414c296abd | ||
|
|
79051439c5 | ||
|
|
32b62d3e30 | ||
|
|
e3f2a30120 | ||
|
|
58a122ee28 | ||
|
|
f5f7b3eb51 | ||
|
|
098128c8a8 | ||
|
|
27d566df7a | ||
|
|
9469825f4f | ||
|
|
5ce90422c6 | ||
|
|
256662e094 | ||
|
|
dc7f1e0c86 | ||
|
|
a54e1d424c | ||
|
|
9fa3ee18a4 | ||
|
|
84be347695 | ||
|
|
952ac2c922 | ||
|
|
4390c810d1 | ||
|
|
a567301e49 | ||
|
|
5e8d5c96fc | ||
|
|
80d804d280 | ||
|
|
7fad299cf0 | ||
|
|
47085722da |
@@ -98,6 +98,7 @@ bridge test:
|
||||
allow_failure: true
|
||||
script:
|
||||
- OPTIONAL_TESTS=org.briarproject.bramble.plugin.tor.BridgeTest ./gradlew --info bramble-java:test --tests BridgeTest
|
||||
timeout: 3h
|
||||
|
||||
mailbox integration test:
|
||||
extends: .optional_tests
|
||||
@@ -117,10 +118,3 @@ mailbox integration test:
|
||||
- cd "$CI_PROJECT_DIR"
|
||||
- bramble-core/src/test/bash/wait-for-mailbox.sh
|
||||
- OPTIONAL_TESTS=org.briarproject.bramble.mailbox.MailboxIntegrationTest ./gradlew --info bramble-core:test --tests MailboxIntegrationTest
|
||||
|
||||
pre_release_tests:
|
||||
extends: .optional_tests
|
||||
script:
|
||||
- OPTIONAL_TESTS=org.briarproject.bramble.plugin.tor.BridgeTest ./gradlew --info bramble-java:test --tests BridgeTest
|
||||
only:
|
||||
- tags
|
||||
|
||||
@@ -15,8 +15,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
versionCode 10405
|
||||
versionName "1.4.5"
|
||||
versionCode 10408
|
||||
versionName "1.4.8"
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -43,7 +43,7 @@ configurations {
|
||||
dependencies {
|
||||
implementation project(path: ':bramble-core', configuration: 'default')
|
||||
tor "org.briarproject:tor-android:$tor_version"
|
||||
tor "org.briarproject:obfs4proxy-android:$obfs4proxy_version@zip"
|
||||
tor "org.briarproject:obfs4proxy-android:$obfs4proxy_version"
|
||||
|
||||
annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
|
||||
|
||||
@@ -70,11 +70,6 @@ clean.dependsOn cleanTorBinaries
|
||||
|
||||
task unpackTorBinaries {
|
||||
doLast {
|
||||
copy {
|
||||
from configurations.tor.collect { zipTree(it) }
|
||||
into torBinariesDir
|
||||
include 'geoip.zip'
|
||||
}
|
||||
configurations.tor.each { outer ->
|
||||
zipTree(outer).each { inner ->
|
||||
if (inner.name.endsWith('_arm_pie.zip')) {
|
||||
|
||||
@@ -11,7 +11,10 @@ import android.net.LinkAddress;
|
||||
import android.net.LinkProperties;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.wifi.WifiInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventExecutor;
|
||||
import org.briarproject.bramble.api.lifecycle.Service;
|
||||
@@ -21,7 +24,6 @@ import org.briarproject.bramble.api.network.event.NetworkStatusEvent;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler.Cancellable;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
@@ -38,6 +40,7 @@ import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.content.Context.CONNECTIVITY_SERVICE;
|
||||
import static android.content.Context.WIFI_SERVICE;
|
||||
import static android.content.Intent.ACTION_SCREEN_OFF;
|
||||
import static android.content.Intent.ACTION_SCREEN_ON;
|
||||
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
|
||||
@@ -111,15 +114,37 @@ class AndroidNetworkManager implements NetworkManager, Service {
|
||||
|
||||
@Override
|
||||
public NetworkStatus getNetworkStatus() {
|
||||
NetworkInfo net = connectivityManager.getActiveNetworkInfo();
|
||||
boolean connected = net != null && net.isConnected();
|
||||
boolean wifi = false, ipv6Only = false;
|
||||
if (connected) {
|
||||
wifi = net.getType() == TYPE_WIFI;
|
||||
if (SDK_INT >= 23) ipv6Only = isActiveNetworkIpv6Only();
|
||||
else ipv6Only = areAllAvailableNetworksIpv6Only();
|
||||
// https://issuetracker.google.com/issues/175055271
|
||||
try {
|
||||
NetworkInfo net = connectivityManager.getActiveNetworkInfo();
|
||||
boolean connected = net != null && net.isConnected();
|
||||
boolean wifi = false, ipv6Only = false;
|
||||
if (connected) {
|
||||
wifi = net.getType() == TYPE_WIFI;
|
||||
if (SDK_INT >= 23) ipv6Only = isActiveNetworkIpv6Only();
|
||||
else ipv6Only = areAllAvailableNetworksIpv6Only();
|
||||
}
|
||||
return new NetworkStatus(connected, wifi, ipv6Only);
|
||||
} catch (SecurityException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
// Without the ConnectivityManager we can't detect whether we have
|
||||
// internet access. Assume we do, which is probably less harmful
|
||||
// than assuming we don't. Likewise, assume the connection is
|
||||
// IPv6-only. Fall back to the WifiManager to detect whether we
|
||||
// have a wifi connection.
|
||||
LOG.info("ConnectivityManager is broken, guessing connectivity");
|
||||
boolean connected = true, wifi = false, ipv6Only = true;
|
||||
WifiManager wm = (WifiManager) app.getSystemService(WIFI_SERVICE);
|
||||
if (wm != null) {
|
||||
WifiInfo info = wm.getConnectionInfo();
|
||||
if (info != null && info.getIpAddress() != 0) {
|
||||
LOG.info("Connected to wifi");
|
||||
wifi = true;
|
||||
ipv6Only = false;
|
||||
}
|
||||
}
|
||||
return new NetworkStatus(connected, wifi, ipv6Only);
|
||||
}
|
||||
return new NetworkStatus(connected, wifi, ipv6Only);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,23 +155,29 @@ class AndroidNetworkManager implements NetworkManager, Service {
|
||||
*/
|
||||
@TargetApi(23)
|
||||
private boolean isActiveNetworkIpv6Only() {
|
||||
Network net = connectivityManager.getActiveNetwork();
|
||||
if (net == null) {
|
||||
LOG.info("No active network");
|
||||
// https://issuetracker.google.com/issues/175055271
|
||||
try {
|
||||
Network net = connectivityManager.getActiveNetwork();
|
||||
if (net == null) {
|
||||
LOG.info("No active network");
|
||||
return false;
|
||||
}
|
||||
LinkProperties props = connectivityManager.getLinkProperties(net);
|
||||
if (props == null) {
|
||||
LOG.info("No link properties for active network");
|
||||
return false;
|
||||
}
|
||||
boolean hasIpv6Unicast = false;
|
||||
for (LinkAddress linkAddress : props.getLinkAddresses()) {
|
||||
InetAddress addr = linkAddress.getAddress();
|
||||
if (addr instanceof Inet4Address) return false;
|
||||
if (!addr.isMulticastAddress()) hasIpv6Unicast = true;
|
||||
}
|
||||
return hasIpv6Unicast;
|
||||
} catch (SecurityException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return false;
|
||||
}
|
||||
LinkProperties props = connectivityManager.getLinkProperties(net);
|
||||
if (props == null) {
|
||||
LOG.info("No link properties for active network");
|
||||
return false;
|
||||
}
|
||||
boolean hasIpv6Unicast = false;
|
||||
for (LinkAddress linkAddress : props.getLinkAddresses()) {
|
||||
InetAddress addr = linkAddress.getAddress();
|
||||
if (addr instanceof Inet4Address) return false;
|
||||
if (!addr.isMulticastAddress()) hasIpv6Unicast = true;
|
||||
}
|
||||
return hasIpv6Unicast;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,13 +32,22 @@ class AndroidRemovableDrivePlugin extends RemovableDrivePlugin {
|
||||
InputStream openInputStream(TransportProperties p) throws IOException {
|
||||
String uri = p.get(PROP_URI);
|
||||
if (isNullOrEmpty(uri)) throw new IllegalArgumentException();
|
||||
return app.getContentResolver().openInputStream(Uri.parse(uri));
|
||||
try {
|
||||
return app.getContentResolver().openInputStream(Uri.parse(uri));
|
||||
} catch (SecurityException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
OutputStream openOutputStream(TransportProperties p) throws IOException {
|
||||
String uri = p.get(PROP_URI);
|
||||
if (isNullOrEmpty(uri)) throw new IllegalArgumentException();
|
||||
return app.getContentResolver().openOutputStream(Uri.parse(uri));
|
||||
try {
|
||||
return app.getContentResolver()
|
||||
.openOutputStream(Uri.parse(uri), "wt");
|
||||
} catch (SecurityException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,16 +175,24 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
|
||||
@TargetApi(21)
|
||||
@Nullable
|
||||
private InetAddress getWifiClientIpv6Address() {
|
||||
for (Network net : connectivityManager.getAllNetworks()) {
|
||||
NetworkCapabilities caps =
|
||||
connectivityManager.getNetworkCapabilities(net);
|
||||
if (caps == null || !caps.hasTransport(TRANSPORT_WIFI)) continue;
|
||||
LinkProperties props = connectivityManager.getLinkProperties(net);
|
||||
if (props == null) continue;
|
||||
for (LinkAddress linkAddress : props.getLinkAddresses()) {
|
||||
InetAddress addr = linkAddress.getAddress();
|
||||
if (isIpv6LinkLocalAddress(addr)) return addr;
|
||||
// https://issuetracker.google.com/issues/175055271
|
||||
try {
|
||||
for (Network net : connectivityManager.getAllNetworks()) {
|
||||
NetworkCapabilities caps =
|
||||
connectivityManager.getNetworkCapabilities(net);
|
||||
if (caps == null || !caps.hasTransport(TRANSPORT_WIFI)) {
|
||||
continue;
|
||||
}
|
||||
LinkProperties props =
|
||||
connectivityManager.getLinkProperties(net);
|
||||
if (props == null) continue;
|
||||
for (LinkAddress linkAddress : props.getLinkAddresses()) {
|
||||
InetAddress addr = linkAddress.getAddress();
|
||||
if (isIpv6LinkLocalAddress(addr)) return addr;
|
||||
}
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -227,12 +235,17 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
|
||||
// network's socket factory may try to connect via another network
|
||||
private SocketFactory getSocketFactory() {
|
||||
if (SDK_INT < 21) return SocketFactory.getDefault();
|
||||
for (Network net : connectivityManager.getAllNetworks()) {
|
||||
NetworkCapabilities caps =
|
||||
connectivityManager.getNetworkCapabilities(net);
|
||||
if (caps != null && caps.hasTransport(TRANSPORT_WIFI)) {
|
||||
return net.getSocketFactory();
|
||||
// https://issuetracker.google.com/issues/175055271
|
||||
try {
|
||||
for (Network net : connectivityManager.getAllNetworks()) {
|
||||
NetworkCapabilities caps =
|
||||
connectivityManager.getNetworkCapabilities(net);
|
||||
if (caps != null && caps.hasTransport(TRANSPORT_WIFI)) {
|
||||
return net.getSocketFactory();
|
||||
}
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
LOG.warning("Could not find suitable socket factory");
|
||||
return SocketFactory.getDefault();
|
||||
|
||||
@@ -11,62 +11,35 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.Backoff;
|
||||
import org.briarproject.bramble.api.plugin.BackoffFactory;
|
||||
import org.briarproject.bramble.api.plugin.PluginCallback;
|
||||
import org.briarproject.bramble.api.plugin.TorConstants;
|
||||
import org.briarproject.bramble.api.plugin.TorControlPort;
|
||||
import org.briarproject.bramble.api.plugin.TorDirectory;
|
||||
import org.briarproject.bramble.api.plugin.TorSocksPort;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
|
||||
import org.briarproject.bramble.api.system.AndroidWakeLockManager;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.system.LocationUtils;
|
||||
import org.briarproject.bramble.api.system.ResourceProvider;
|
||||
import org.briarproject.bramble.api.system.WakefulIoExecutor;
|
||||
import org.briarproject.bramble.util.AndroidUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
import static org.briarproject.bramble.util.AndroidUtils.getSupportedArchitectures;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class AndroidTorPluginFactory implements DuplexPluginFactory {
|
||||
public class AndroidTorPluginFactory extends TorPluginFactory {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(AndroidTorPluginFactory.class.getName());
|
||||
|
||||
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
|
||||
private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds
|
||||
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute
|
||||
private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins
|
||||
private static final double BACKOFF_BASE = 1.2;
|
||||
|
||||
private final Executor ioExecutor, wakefulIoExecutor;
|
||||
private final Application app;
|
||||
private final NetworkManager networkManager;
|
||||
private final LocationUtils locationUtils;
|
||||
private final EventBus eventBus;
|
||||
private final SocketFactory torSocketFactory;
|
||||
private final BackoffFactory backoffFactory;
|
||||
private final ResourceProvider resourceProvider;
|
||||
private final CircumventionProvider circumventionProvider;
|
||||
private final BatteryManager batteryManager;
|
||||
private final AndroidWakeLockManager wakeLockManager;
|
||||
private final Clock clock;
|
||||
private final File torDirectory;
|
||||
private int torSocksPort;
|
||||
private int torControlPort;
|
||||
private final CryptoComponent crypto;
|
||||
|
||||
@Inject
|
||||
AndroidTorPluginFactory(@IoExecutor Executor ioExecutor,
|
||||
@WakefulIoExecutor Executor wakefulIoExecutor,
|
||||
Application app,
|
||||
NetworkManager networkManager,
|
||||
LocationUtils locationUtils,
|
||||
EventBus eventBus,
|
||||
@@ -75,80 +48,43 @@ public class AndroidTorPluginFactory implements DuplexPluginFactory {
|
||||
ResourceProvider resourceProvider,
|
||||
CircumventionProvider circumventionProvider,
|
||||
BatteryManager batteryManager,
|
||||
AndroidWakeLockManager wakeLockManager,
|
||||
Clock clock,
|
||||
CryptoComponent crypto,
|
||||
@TorDirectory File torDirectory,
|
||||
@TorSocksPort int torSocksPort,
|
||||
@TorControlPort int torControlPort,
|
||||
CryptoComponent crypto) {
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.wakefulIoExecutor = wakefulIoExecutor;
|
||||
Application app,
|
||||
AndroidWakeLockManager wakeLockManager) {
|
||||
super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils,
|
||||
eventBus, torSocketFactory, backoffFactory, resourceProvider,
|
||||
circumventionProvider, batteryManager, clock, crypto,
|
||||
torDirectory, torSocksPort, torControlPort);
|
||||
this.app = app;
|
||||
this.networkManager = networkManager;
|
||||
this.locationUtils = locationUtils;
|
||||
this.eventBus = eventBus;
|
||||
this.torSocketFactory = torSocketFactory;
|
||||
this.backoffFactory = backoffFactory;
|
||||
this.resourceProvider = resourceProvider;
|
||||
this.circumventionProvider = circumventionProvider;
|
||||
this.batteryManager = batteryManager;
|
||||
this.wakeLockManager = wakeLockManager;
|
||||
this.clock = clock;
|
||||
this.torDirectory = torDirectory;
|
||||
this.torSocksPort = torSocksPort;
|
||||
this.torControlPort = torControlPort;
|
||||
this.crypto = crypto;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public TransportId getId() {
|
||||
return TorConstants.ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaxLatency() {
|
||||
return MAX_LATENCY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DuplexPlugin createPlugin(PluginCallback callback) {
|
||||
|
||||
// Check that we have a Tor binary for this architecture
|
||||
String architecture = null;
|
||||
for (String abi : AndroidUtils.getSupportedArchitectures()) {
|
||||
if (abi.startsWith("x86_64")) {
|
||||
architecture = "x86_64";
|
||||
break;
|
||||
} else if (abi.startsWith("x86")) {
|
||||
architecture = "x86";
|
||||
break;
|
||||
} else if (abi.startsWith("arm64")) {
|
||||
architecture = "arm64";
|
||||
break;
|
||||
} else if (abi.startsWith("armeabi")) {
|
||||
architecture = "arm";
|
||||
break;
|
||||
}
|
||||
String getArchitectureForTorBinary() {
|
||||
for (String abi : getSupportedArchitectures()) {
|
||||
if (abi.startsWith("x86_64")) return "x86_64_pie";
|
||||
else if (abi.startsWith("x86")) return "x86_pie";
|
||||
else if (abi.startsWith("arm64")) return "arm64_pie";
|
||||
else if (abi.startsWith("armeabi")) return "arm_pie";
|
||||
}
|
||||
if (architecture == null) {
|
||||
LOG.info("Tor is not supported on this architecture");
|
||||
return null;
|
||||
}
|
||||
// Use position-independent executable
|
||||
architecture += "_pie";
|
||||
return null;
|
||||
}
|
||||
|
||||
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
|
||||
MAX_POLLING_INTERVAL, BACKOFF_BASE);
|
||||
TorRendezvousCrypto torRendezvousCrypto =
|
||||
new TorRendezvousCryptoImpl(crypto);
|
||||
AndroidTorPlugin plugin = new AndroidTorPlugin(ioExecutor,
|
||||
@Override
|
||||
TorPlugin createPluginInstance(Backoff backoff,
|
||||
TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback,
|
||||
String architecture) {
|
||||
return new AndroidTorPlugin(ioExecutor,
|
||||
wakefulIoExecutor, app, networkManager, locationUtils,
|
||||
torSocketFactory, clock, resourceProvider,
|
||||
circumventionProvider, batteryManager, wakeLockManager,
|
||||
backoff, torRendezvousCrypto, callback, architecture,
|
||||
MAX_LATENCY, MAX_IDLE_TIME, torDirectory, torSocksPort,
|
||||
torControlPort);
|
||||
eventBus.addListener(plugin);
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.Intent;
|
||||
import android.os.Process;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.lifecycle.Service;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.AlarmListener;
|
||||
@@ -116,10 +117,12 @@ class AndroidTaskScheduler implements TaskScheduler, Service, AlarmListener {
|
||||
long dueMillis = now + MILLISECONDS.convert(delay, unit);
|
||||
Runnable wakeful = () ->
|
||||
wakeLockManager.executeWakefully(task, executor, "TaskHandoff");
|
||||
Future<?> check = scheduleCheckForDueTasks(delay, unit);
|
||||
ScheduledTask s = new ScheduledTask(wakeful, dueMillis, check,
|
||||
cancelled);
|
||||
// Acquire the lock before scheduling the check to ensure the check
|
||||
// doesn't access the task queue before the task has been added
|
||||
ScheduledTask s;
|
||||
synchronized (lock) {
|
||||
Future<?> check = scheduleCheckForDueTasks(delay, unit);
|
||||
s = new ScheduledTask(wakeful, dueMillis, check, cancelled);
|
||||
tasks.add(s);
|
||||
}
|
||||
return s;
|
||||
@@ -136,6 +139,7 @@ class AndroidTaskScheduler implements TaskScheduler, Service, AlarmListener {
|
||||
return schedule(wrapped, executor, delay, unit, cancelled);
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private Future<?> scheduleCheckForDueTasks(long delay, TimeUnit unit) {
|
||||
Runnable wakeful = () -> wakeLockManager.runWakefully(
|
||||
this::runDueTasks, "TaskScheduler");
|
||||
@@ -206,7 +210,7 @@ class AndroidTaskScheduler implements TaskScheduler, Service, AlarmListener {
|
||||
private final Future<?> check;
|
||||
private final AtomicBoolean cancelled;
|
||||
|
||||
public ScheduledTask(Runnable task, long dueMillis,
|
||||
private ScheduledTask(Runnable task, long dueMillis,
|
||||
Future<?> check, AtomicBoolean cancelled) {
|
||||
this.task = task;
|
||||
this.dueMillis = dueMillis;
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Looper;
|
||||
import android.provider.Settings;
|
||||
|
||||
import org.briarproject.bramble.api.Pair;
|
||||
@@ -134,4 +135,8 @@ public class AndroidUtils {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isUiThread() {
|
||||
return Looper.myLooper() == Looper.getMainLooper();
|
||||
}
|
||||
}
|
||||
|
||||
0
bramble-android/src/main/res/raw/.gitkeep
Normal file
0
bramble-android/src/main/res/raw/.gitkeep
Normal file
@@ -87,8 +87,8 @@ dependencyVerification {
|
||||
'org.apache.httpcomponents:httpmime:4.5.6:httpmime-4.5.6.jar:0b2b1102c18d3c7e05a77214b9b7501a6f6056174ae5604e0e256776eda7553e',
|
||||
'org.bouncycastle:bcpkix-jdk15on:1.56:bcpkix-jdk15on-1.56.jar:7043dee4e9e7175e93e0b36f45b1ec1ecb893c5f755667e8b916eb8dd201c6ca',
|
||||
'org.bouncycastle:bcprov-jdk15on:1.56:bcprov-jdk15on-1.56.jar:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349',
|
||||
'org.briarproject:obfs4proxy-android:0.0.12-dev-40245c4a:obfs4proxy-android-0.0.12-dev-40245c4a.zip:8ab05a8f8391be2cb5ab2b665c281a06d9e3a756bd0f95a40a36ca927866ea82',
|
||||
'org.briarproject:tor-android:0.3.5.17:tor-android-0.3.5.17.jar:1888afc10a26b93d00a010ea27bf0b1b162a6d524688b08b98d70d14dc363b54',
|
||||
'org.briarproject:obfs4proxy-android:0.0.12:obfs4proxy-android-0.0.12.jar:84159d2a4668abc40e3fccaa1f6fa0c04892863f9eb80a866ac8928d9f9a7e89',
|
||||
'org.briarproject:tor-android:0.4.5.12-2:tor-android-0.4.5.12-2.jar:8545dbcef2bb6aa89c32bb6f8ac51f7a64bce3ae85845b3578ffdeb9b206feb9',
|
||||
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
|
||||
'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
|
||||
'org.checkerframework:checker-qual:3.5.0:checker-qual-3.5.0.jar:729990b3f18a95606fc2573836b6958bcdb44cb52bfbd1b7aa9c339cff35a5a4',
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.briarproject.bramble.api;
|
||||
|
||||
public interface Cancellable {
|
||||
|
||||
void cancel();
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
package org.briarproject.bramble.api;
|
||||
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public abstract class StringMap extends Hashtable<String, String> {
|
||||
|
||||
protected StringMap(Map<String, String> m) {
|
||||
@@ -52,4 +58,31 @@ public abstract class StringMap extends Hashtable<String, String> {
|
||||
public void putLong(String key, long value) {
|
||||
put(key, String.valueOf(value));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public int[] getIntArray(String key) {
|
||||
String s = get(key);
|
||||
if (s == null) return null;
|
||||
// Handle empty string because "".split(",") returns {""}
|
||||
if (s.length() == 0) return new int[0];
|
||||
String[] intStrings = s.split(",");
|
||||
int[] ints = new int[intStrings.length];
|
||||
try {
|
||||
for (int i = 0; i < ints.length; i++) {
|
||||
ints[i] = Integer.parseInt(intStrings[i]);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
return ints;
|
||||
}
|
||||
|
||||
public void putIntArray(String key, int[] value) {
|
||||
List<String> intStrings = new ArrayList<>();
|
||||
for (int integer : value) {
|
||||
intStrings.add(String.valueOf(integer));
|
||||
}
|
||||
// Puts empty string if input array value is empty
|
||||
put(key, StringUtils.join(intStrings, ","));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
public abstract class UniqueId extends Bytes {
|
||||
public class UniqueId extends Bytes {
|
||||
|
||||
/**
|
||||
* The length of a unique identifier in bytes.
|
||||
*/
|
||||
public static final int LENGTH = 32;
|
||||
|
||||
protected UniqueId(byte[] id) {
|
||||
public UniqueId(byte[] id) {
|
||||
super(id);
|
||||
if (id.length != LENGTH) throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdate;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||
@@ -18,6 +20,7 @@ import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@NotNullByDefault
|
||||
@@ -123,6 +126,19 @@ public interface ClientHelper {
|
||||
Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap(
|
||||
BdfDictionary properties) throws FormatException;
|
||||
|
||||
/**
|
||||
* Parse and validate the elements of a Mailbox update message.
|
||||
*
|
||||
* @return the parsed update message
|
||||
* @throws FormatException if the message elements are invalid
|
||||
*/
|
||||
MailboxUpdate parseAndValidateMailboxUpdate(BdfList clientSupports,
|
||||
BdfList serverSupports, BdfDictionary properties)
|
||||
throws FormatException;
|
||||
|
||||
List<MailboxVersion> parseMailboxVersionList(BdfList bdfList)
|
||||
throws FormatException;
|
||||
|
||||
/**
|
||||
* Retrieves the contact ID from the group metadata of the given contact
|
||||
* group.
|
||||
|
||||
@@ -16,6 +16,17 @@ public interface ConnectionManager {
|
||||
*/
|
||||
void manageIncomingConnection(TransportId t, TransportConnectionReader r);
|
||||
|
||||
/**
|
||||
* Manages an incoming connection from a contact via a mailbox.
|
||||
* <p>
|
||||
* This method does not mark the tag as recognised until after the data
|
||||
* has been read from the {@link TransportConnectionReader}, at which
|
||||
* point the {@link TagController} is called to decide whether the tag
|
||||
* should be marked as recognised.
|
||||
*/
|
||||
void manageIncomingConnection(TransportId t, TransportConnectionReader r,
|
||||
TagController c);
|
||||
|
||||
/**
|
||||
* Manages an incoming connection from a contact over a duplex transport.
|
||||
*/
|
||||
@@ -46,4 +57,21 @@ public interface ConnectionManager {
|
||||
*/
|
||||
void manageOutgoingConnection(PendingContactId p, TransportId t,
|
||||
DuplexTransportConnection d);
|
||||
|
||||
/**
|
||||
* An interface for controlling whether a tag should be marked as
|
||||
* recognised.
|
||||
*/
|
||||
interface TagController {
|
||||
/**
|
||||
* This method is only called if a tag was read from the corresponding
|
||||
* {@link TransportConnectionReader} and recognised.
|
||||
*
|
||||
* @param exception True if an exception was thrown while reading from
|
||||
* the {@link TransportConnectionReader}, after successfully reading
|
||||
* and recognising the tag.
|
||||
* @return True if the tag should be marked as recognised.
|
||||
*/
|
||||
boolean shouldMarkTagAsRecognised(boolean exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.briarproject.bramble.api.contact;
|
||||
import org.briarproject.bramble.api.UniqueId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
@@ -17,9 +16,4 @@ public class PendingContactId extends UniqueId {
|
||||
public PendingContactId(byte[] id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
return o instanceof PendingContactId && super.equals(o);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.api.crypto;
|
||||
|
||||
import org.briarproject.bramble.api.UniqueId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
@@ -10,6 +11,8 @@ import javax.annotation.Nullable;
|
||||
@NotNullByDefault
|
||||
public interface CryptoComponent {
|
||||
|
||||
UniqueId generateUniqueId();
|
||||
|
||||
SecretKey generateSecretKey();
|
||||
|
||||
SecureRandom getSecureRandom();
|
||||
@@ -172,9 +175,11 @@ public interface CryptoComponent {
|
||||
String asciiArmour(byte[] b, int lineLength);
|
||||
|
||||
/**
|
||||
* Encode the onion/hidden service address given its public key. As
|
||||
* specified here: https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt?id=29245fd5#n2135
|
||||
* Encode the Onion given its public key. Specified here:
|
||||
* https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt?id=29245fd5#n2135
|
||||
*
|
||||
* @return the encoded onion, base32 chars
|
||||
*/
|
||||
String encodeOnionAddress(byte[] publicKey);
|
||||
String encodeOnion(byte[] publicKey);
|
||||
|
||||
}
|
||||
|
||||
@@ -33,11 +33,18 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* Encapsulates the database implementation and exposes high-level operations
|
||||
* to other components.
|
||||
* <p>
|
||||
* With the exception of the {@link #open(SecretKey, MigrationListener)} and
|
||||
* {@link #close()} methods, which must not be called concurrently, the
|
||||
* database can be accessed from any thread. See {@link TransactionManager}
|
||||
* for locking behaviour.
|
||||
*/
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
public interface DatabaseComponent extends TransactionManager {
|
||||
|
||||
@@ -193,26 +200,15 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns a batch of messages for the given contact, with a total length
|
||||
* less than or equal to the given length, for transmission over a
|
||||
* transport with the given maximum latency. Returns null if there are no
|
||||
* sendable messages that fit in the given length.
|
||||
* Returns a batch of messages for the given contact, for transmission over
|
||||
* a transport with the given maximum latency. The total length of the
|
||||
* messages, including record headers, will be no more than the given
|
||||
* capacity. Returns null if there are no sendable messages that would fit
|
||||
* in the given capacity.
|
||||
*/
|
||||
@Nullable
|
||||
Collection<Message> generateBatch(Transaction txn, ContactId c,
|
||||
int maxLength, long maxLatency) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns a batch of messages for the given contact containing the
|
||||
* messages with the given IDs, for transmission over a transport with
|
||||
* the given maximum latency.
|
||||
* <p/>
|
||||
* If any of the given messages are not in the database or are not visible
|
||||
* to the contact, they are omitted from the batch without throwing an
|
||||
* exception.
|
||||
*/
|
||||
Collection<Message> generateBatch(Transaction txn, ContactId c,
|
||||
Collection<MessageId> ids, long maxLatency) throws DbException;
|
||||
long capacity, long maxLatency) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns an offer for the given contact for transmission over a
|
||||
@@ -232,15 +228,16 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns a batch of messages for the given contact, with a total length
|
||||
* less than or equal to the given length, for transmission over a
|
||||
* transport with the given maximum latency. Only messages that have been
|
||||
* requested by the contact are returned. Returns null if there are no
|
||||
* sendable messages that fit in the given length.
|
||||
* Returns a batch of messages for the given contact, for transmission over
|
||||
* a transport with the given maximum latency. Only messages that have been
|
||||
* requested by the contact are returned. The total length of the messages,
|
||||
* including record headers, will be no more than the given capacity.
|
||||
* Returns null if there are no sendable messages that have been requested
|
||||
* by the contact and would fit in the given capacity.
|
||||
*/
|
||||
@Nullable
|
||||
Collection<Message> generateRequestedBatch(Transaction txn, ContactId c,
|
||||
int maxLength, long maxLatency) throws DbException;
|
||||
long capacity, long maxLatency) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the contact with the given ID.
|
||||
@@ -344,6 +341,30 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
|
||||
Metadata query) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the IDs of some messages received from the given contact that
|
||||
* need to be acknowledged, up to the given number of messages.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Collection<MessageId> getMessagesToAck(Transaction txn, ContactId c,
|
||||
int maxMessages) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the IDs of some messages that are eligible to be sent to the
|
||||
* given contact over a transport with the given maximum latency. The total
|
||||
* length of the messages including record headers will be no more than the
|
||||
* given capacity.
|
||||
* <p/>
|
||||
* Unlike {@link #getUnackedMessagesToSend(Transaction, ContactId)} this
|
||||
* method does not return messages that have already been sent unless they
|
||||
* are due for retransmission.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Collection<MessageId> getMessagesToSend(Transaction txn, ContactId c,
|
||||
long capacity, long maxLatency) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the IDs of any messages that need to be validated.
|
||||
* <p/>
|
||||
@@ -460,21 +481,36 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the message with the given ID for transmission to the given
|
||||
* contact over a transport with the given maximum latency. Returns null
|
||||
* if the message is no longer visible to the contact.
|
||||
*
|
||||
* @param markAsSent True if the message should be marked as sent.
|
||||
* If false it can be marked as sent by calling
|
||||
* {@link #setMessagesSent(Transaction, ContactId, Collection, long)}.
|
||||
*/
|
||||
@Nullable
|
||||
Message getMessageToSend(Transaction txn, ContactId c, MessageId m,
|
||||
long maxLatency, boolean markAsSent) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the IDs of all messages that are eligible to be sent to the
|
||||
* given contact, together with their raw lengths. This may include
|
||||
* messages that have already been sent and are not yet due for
|
||||
* retransmission.
|
||||
* given contact.
|
||||
* <p>
|
||||
* Unlike {@link #getMessagesToSend(Transaction, ContactId, long, long)}
|
||||
* this method may return messages that have already been sent and are
|
||||
* not yet due for retransmission.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Map<MessageId, Integer> getUnackedMessagesToSend(Transaction txn,
|
||||
Collection<MessageId> getUnackedMessagesToSend(Transaction txn,
|
||||
ContactId c) throws DbException;
|
||||
|
||||
/**
|
||||
* Reset the transmission count, expiry time and ETA of all messages that
|
||||
* are eligible to be sent to the given contact. This includes messages that
|
||||
* have already been sent and are not yet due for retransmission.
|
||||
* Resets the transmission count, expiry time and max latency of all messages
|
||||
* that are eligible to be sent to the given contact. This includes messages
|
||||
* that have already been sent and are not yet due for retransmission.
|
||||
*/
|
||||
void resetUnackedMessagesToSend(Transaction txn, ContactId c)
|
||||
throws DbException;
|
||||
@@ -648,6 +684,13 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
void removeTransportKeys(Transaction txn, TransportId t, KeySetId k)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Records an ack for the given messages as having been sent to the given
|
||||
* contact.
|
||||
*/
|
||||
void setAckSent(Transaction txn, ContactId c, Collection<MessageId> acked)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Sets the cleanup timer duration for the given message. This does not
|
||||
* start the message's cleanup timer.
|
||||
@@ -694,6 +737,13 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
void setMessageState(Transaction txn, MessageId m, MessageState state)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Records the given messages as having been sent to the given contact
|
||||
* over a transport with the given maximum latency.
|
||||
*/
|
||||
void setMessagesSent(Transaction txn, ContactId c,
|
||||
Collection<MessageId> sent, long maxLatency) throws DbException;
|
||||
|
||||
/**
|
||||
* Adds dependencies for a message
|
||||
*/
|
||||
|
||||
@@ -18,6 +18,10 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
* submitted, tasks are not run concurrently, and submitting a task will never
|
||||
* block. Tasks must not run indefinitely. Tasks submitted during shutdown are
|
||||
* discarded.
|
||||
* <p>
|
||||
* It is not mandatory to use this executor for database tasks. The database
|
||||
* can be accessed from any thread, but this executor's guarantee that tasks
|
||||
* are run in the order they're submitted may be useful in some cases.
|
||||
*/
|
||||
@Qualifier
|
||||
@Target({FIELD, METHOD, PARAMETER})
|
||||
|
||||
@@ -45,6 +45,9 @@ public class Transaction {
|
||||
/**
|
||||
* Attaches an event to be broadcast when the transaction has been
|
||||
* committed. The event will be broadcast on the {@link EventExecutor}.
|
||||
* Events and {@link #attach(Runnable) tasks} are submitted to the
|
||||
* {@link EventExecutor} in the order they were attached to the
|
||||
* transaction.
|
||||
*/
|
||||
public void attach(Event e) {
|
||||
if (actions == null) actions = new ArrayList<>();
|
||||
@@ -54,6 +57,9 @@ public class Transaction {
|
||||
/**
|
||||
* Attaches a task to be executed when the transaction has been
|
||||
* committed. The task will be run on the {@link EventExecutor}.
|
||||
* {@link #attach(Event) Events} and tasks are submitted to the
|
||||
* {@link EventExecutor} in the order they were attached to the
|
||||
* transaction.
|
||||
*/
|
||||
public void attach(Runnable r) {
|
||||
if (actions == null) actions = new ArrayList<>();
|
||||
|
||||
@@ -1,51 +1,95 @@
|
||||
package org.briarproject.bramble.api.db;
|
||||
|
||||
import org.briarproject.bramble.api.event.EventExecutor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* An interface for managing database transactions.
|
||||
* <p>
|
||||
* Read-only transactions may access the database concurrently. Read-write
|
||||
* transactions access the database exclusively, so starting a read-only or
|
||||
* read-write transaction will block until there are no read-write
|
||||
* transactions in progress.
|
||||
* <p>
|
||||
* Failing to {@link #endTransaction(Transaction) end} a transaction will
|
||||
* prevent other callers from accessing the database, so it is recommended to
|
||||
* use the {@link #transaction(boolean, DbRunnable)},
|
||||
* {@link #transactionWithResult(boolean, DbCallable)} and
|
||||
* {@link #transactionWithNullableResult(boolean, NullableDbCallable)} methods
|
||||
* where possible, which handle committing or aborting the transaction on the
|
||||
* caller's behalf.
|
||||
* <p>
|
||||
* Transactions are not reentrant, i.e. it is not permitted to start a
|
||||
* transaction on a thread that already has a transaction in progress.
|
||||
*/
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
public interface TransactionManager {
|
||||
|
||||
/**
|
||||
* Starts a new transaction and returns an object representing it.
|
||||
* <p/>
|
||||
* This method acquires locks, so it must not be called while holding a
|
||||
* lock.
|
||||
* Starts a new transaction and returns an object representing it. This
|
||||
* method acquires the database lock, which is held until
|
||||
* {@link #endTransaction(Transaction)} is called.
|
||||
*
|
||||
* @param readOnly true if the transaction will only be used for reading.
|
||||
* @param readOnly True if the transaction will only be used for reading,
|
||||
* in which case the database lock can be shared with other read-only
|
||||
* transactions.
|
||||
*/
|
||||
Transaction startTransaction(boolean readOnly) throws DbException;
|
||||
|
||||
/**
|
||||
* Commits a transaction to the database.
|
||||
* {@link #endTransaction(Transaction)} must be called to release the
|
||||
* database lock.
|
||||
*/
|
||||
void commitTransaction(Transaction txn) throws DbException;
|
||||
|
||||
/**
|
||||
* Ends a transaction. If the transaction has not been committed,
|
||||
* it will be aborted. If the transaction has been committed,
|
||||
* any events attached to the transaction are broadcast.
|
||||
* The database lock will be released in either case.
|
||||
* Ends a transaction. If the transaction has not been committed by
|
||||
* calling {@link #commitTransaction(Transaction)}, it is aborted and the
|
||||
* database lock is released.
|
||||
* <p>
|
||||
* If the transaction has been committed, any
|
||||
* {@link Transaction#attach events} attached to the transaction are
|
||||
* broadcast and any {@link Transaction#attach(Runnable) tasks} attached
|
||||
* to the transaction are submitted to the {@link EventExecutor}. The
|
||||
* database lock is then released.
|
||||
*/
|
||||
void endTransaction(Transaction txn);
|
||||
|
||||
/**
|
||||
* Runs the given task within a transaction.
|
||||
* Runs the given task within a transaction. The database lock is held
|
||||
* while running the task.
|
||||
*
|
||||
* @param readOnly True if the transaction will only be used for reading,
|
||||
* in which case the database lock can be shared with other read-only
|
||||
* transactions.
|
||||
*/
|
||||
<E extends Exception> void transaction(boolean readOnly,
|
||||
DbRunnable<E> task) throws DbException, E;
|
||||
|
||||
/**
|
||||
* Runs the given task within a transaction and returns the result of the
|
||||
* task.
|
||||
* task. The database lock is held while running the task.
|
||||
*
|
||||
* @param readOnly True if the transaction will only be used for reading,
|
||||
* in which case the database lock can be shared with other read-only
|
||||
* transactions.
|
||||
*/
|
||||
<R, E extends Exception> R transactionWithResult(boolean readOnly,
|
||||
DbCallable<R, E> task) throws DbException, E;
|
||||
|
||||
/**
|
||||
* Runs the given task within a transaction and returns the result of the
|
||||
* task, which may be null.
|
||||
* task, which may be null. The database lock is held while running the
|
||||
* task.
|
||||
*
|
||||
* @param readOnly True if the transaction will only be used for reading,
|
||||
* in which case the database lock can be shared with other read-only
|
||||
* transactions.
|
||||
*/
|
||||
@Nullable
|
||||
<R, E extends Exception> R transactionWithNullableResult(boolean readOnly,
|
||||
|
||||
@@ -21,9 +21,4 @@ public class AuthorId extends UniqueId {
|
||||
public AuthorId(byte[] id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof AuthorId && super.equals(o);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
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;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.STREAM_HEADER_LENGTH;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
|
||||
|
||||
public interface MailboxConstants {
|
||||
|
||||
/**
|
||||
* The transport ID of the mailbox plugin.
|
||||
*/
|
||||
TransportId ID = new TransportId("org.briarproject.bramble.mailbox");
|
||||
|
||||
/**
|
||||
* Mailbox API versions that we support as a client. This is reported to our
|
||||
* contacts by {@link MailboxUpdateManager}.
|
||||
*/
|
||||
List<MailboxVersion> CLIENT_SUPPORTS = singletonList(
|
||||
new MailboxVersion(1, 0));
|
||||
|
||||
/**
|
||||
* The constant returned by
|
||||
* {@link MailboxHelper#getHighestCommonMajorVersion(List, List)}
|
||||
* when the server is too old to support our major version.
|
||||
*/
|
||||
int API_SERVER_TOO_OLD = -1;
|
||||
|
||||
/**
|
||||
* The constant returned by
|
||||
* {@link MailboxHelper#getHighestCommonMajorVersion(List, List)}
|
||||
* when we as a client are too old to support the server's major version.
|
||||
*/
|
||||
int API_CLIENT_TOO_OLD = -2;
|
||||
|
||||
/**
|
||||
* The maximum length of a file that can be uploaded to or downloaded from
|
||||
* a mailbox.
|
||||
*/
|
||||
int MAX_FILE_BYTES = 1024 * 1024;
|
||||
|
||||
/**
|
||||
* The maximum length of the plaintext payload of a file, such that the
|
||||
* ciphertext is no more than {@link #MAX_FILE_BYTES}.
|
||||
*/
|
||||
int MAX_FILE_PAYLOAD_BYTES =
|
||||
(MAX_FILE_BYTES - TAG_LENGTH - STREAM_HEADER_LENGTH)
|
||||
/ MAX_FRAME_LENGTH * MAX_PAYLOAD_LENGTH;
|
||||
|
||||
/**
|
||||
* The number of connection failures
|
||||
* that indicate a problem with the mailbox.
|
||||
*/
|
||||
int PROBLEM_NUM_CONNECTION_FAILURES = 5;
|
||||
|
||||
/**
|
||||
* The time in milliseconds since the last connection success
|
||||
* that need to pass to indicates a problem with the mailbox.
|
||||
*/
|
||||
long PROBLEM_MS_SINCE_LAST_SUCCESS = HOURS.toMillis(1);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import javax.inject.Qualifier;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.ElementType.PARAMETER;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* Annotation for injecting the {@link File directory} where the Mailbox plugin
|
||||
* should store its state.
|
||||
*/
|
||||
@Qualifier
|
||||
@Target({FIELD, METHOD, PARAMETER})
|
||||
@Retention(RUNTIME)
|
||||
public @interface MailboxDirectory {
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.API_CLIENT_TOO_OLD;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.API_SERVER_TOO_OLD;
|
||||
|
||||
class MailboxHelper {
|
||||
|
||||
/**
|
||||
* Returns the highest major version that both client and server support
|
||||
* or {@link MailboxConstants#API_SERVER_TOO_OLD} if the server is too old
|
||||
* or {@link MailboxConstants#API_CLIENT_TOO_OLD} if the client is too old.
|
||||
*/
|
||||
static int getHighestCommonMajorVersion(
|
||||
List<MailboxVersion> client, List<MailboxVersion> server) {
|
||||
TreeSet<Integer> clientVersions = new TreeSet<>();
|
||||
for (MailboxVersion version : client) {
|
||||
clientVersions.add(version.getMajor());
|
||||
}
|
||||
TreeSet<Integer> serverVersions = new TreeSet<>();
|
||||
for (MailboxVersion version : server) {
|
||||
serverVersions.add(version.getMajor());
|
||||
}
|
||||
for (int clientVersion : clientVersions.descendingSet()) {
|
||||
if (serverVersions.contains(clientVersion)) return clientVersion;
|
||||
}
|
||||
if (clientVersions.last() < serverVersions.last()) {
|
||||
return API_CLIENT_TOO_OLD;
|
||||
}
|
||||
return API_SERVER_TOO_OLD;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.mailbox.event.OwnMailboxConnectionStatusEvent;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@@ -32,4 +34,23 @@ public interface MailboxManager {
|
||||
*/
|
||||
MailboxPairingTask startPairingTask(String qrCodePayload);
|
||||
|
||||
/**
|
||||
* Can be used by the UI to test the mailbox connection.
|
||||
*
|
||||
* @return true (success) or false (error).
|
||||
* A {@link OwnMailboxConnectionStatusEvent} might be broadcast with a new
|
||||
* {@link MailboxStatus}.
|
||||
*/
|
||||
boolean checkConnection();
|
||||
|
||||
/**
|
||||
* Unpairs the owner's mailbox and tries to wipe it.
|
||||
* As this makes a network call, it should be run on the {@link IoExecutor}.
|
||||
*
|
||||
* @return true if we could wipe the mailbox, false if we couldn't.
|
||||
* It is advised to inform the user to wipe the mailbox themselves,
|
||||
* if we failed to wipe it.
|
||||
*/
|
||||
@IoExecutor
|
||||
boolean unPair() throws DbException;
|
||||
}
|
||||
|
||||
@@ -1,59 +1,25 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public abstract class MailboxPairingState {
|
||||
|
||||
/**
|
||||
* The QR code payload that was scanned by the user.
|
||||
* This is null if the code should not be re-used anymore in this state.
|
||||
*/
|
||||
@Nullable
|
||||
public final String qrCodePayload;
|
||||
|
||||
MailboxPairingState(@Nullable String qrCodePayload) {
|
||||
this.qrCodePayload = qrCodePayload;
|
||||
}
|
||||
|
||||
public static class QrCodeReceived extends MailboxPairingState {
|
||||
public QrCodeReceived(String qrCodePayload) {
|
||||
super(qrCodePayload);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Pairing extends MailboxPairingState {
|
||||
public Pairing(String qrCodePayload) {
|
||||
super(qrCodePayload);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Paired extends MailboxPairingState {
|
||||
public Paired() {
|
||||
super(null);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidQrCode extends MailboxPairingState {
|
||||
public InvalidQrCode() {
|
||||
super(null);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MailboxAlreadyPaired extends MailboxPairingState {
|
||||
public MailboxAlreadyPaired() {
|
||||
super(null);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConnectionError extends MailboxPairingState {
|
||||
public ConnectionError(String qrCodePayload) {
|
||||
super(qrCodePayload);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnexpectedError extends MailboxPairingState {
|
||||
public UnexpectedError(String qrCodePayload) {
|
||||
super(qrCodePayload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@@ -11,18 +14,48 @@ public class MailboxProperties {
|
||||
private final String baseUrl;
|
||||
private final MailboxAuthToken authToken;
|
||||
private final boolean owner;
|
||||
private final List<MailboxVersion> serverSupports;
|
||||
@Nullable
|
||||
private final MailboxFolderId inboxId; // Null for own mailbox
|
||||
@Nullable
|
||||
private final MailboxFolderId outboxId; // Null for own mailbox
|
||||
|
||||
/**
|
||||
* Constructor for properties used by the mailbox's owner.
|
||||
*/
|
||||
public MailboxProperties(String baseUrl, MailboxAuthToken authToken,
|
||||
boolean owner) {
|
||||
List<MailboxVersion> serverSupports) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.authToken = authToken;
|
||||
this.owner = owner;
|
||||
this.owner = true;
|
||||
this.serverSupports = serverSupports;
|
||||
this.inboxId = null;
|
||||
this.outboxId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for properties used by a contact of the mailbox's owner.
|
||||
*/
|
||||
public MailboxProperties(String baseUrl, MailboxAuthToken authToken,
|
||||
List<MailboxVersion> serverSupports, MailboxFolderId inboxId,
|
||||
MailboxFolderId outboxId) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.authToken = authToken;
|
||||
this.owner = false;
|
||||
this.serverSupports = serverSupports;
|
||||
this.inboxId = inboxId;
|
||||
this.outboxId = outboxId;
|
||||
}
|
||||
|
||||
public String getBaseUrl() {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
public String getOnion() {
|
||||
return baseUrl.replaceFirst("^http://", "")
|
||||
.replaceFirst("\\.onion$", "");
|
||||
}
|
||||
|
||||
public MailboxAuthToken getAuthToken() {
|
||||
return authToken;
|
||||
}
|
||||
@@ -30,4 +63,18 @@ public class MailboxProperties {
|
||||
public boolean isOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public List<MailboxVersion> getServerSupports() {
|
||||
return serverSupports;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public MailboxFolderId getInboxId() {
|
||||
return inboxId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public MailboxFolderId getOutboxId() {
|
||||
return outboxId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface MailboxSettingsManager {
|
||||
|
||||
/**
|
||||
* Registers a hook to be called when a mailbox has been paired or unpaired.
|
||||
* This method should be called before
|
||||
* {@link LifecycleManager#startServices(SecretKey)}.
|
||||
*/
|
||||
void registerMailboxHook(MailboxHook hook);
|
||||
|
||||
@Nullable
|
||||
MailboxProperties getOwnMailboxProperties(Transaction txn)
|
||||
throws DbException;
|
||||
@@ -17,6 +28,8 @@ public interface MailboxSettingsManager {
|
||||
void setOwnMailboxProperties(Transaction txn, MailboxProperties p)
|
||||
throws DbException;
|
||||
|
||||
void removeOwnMailboxProperties(Transaction txn) throws DbException;
|
||||
|
||||
MailboxStatus getOwnMailboxStatus(Transaction txn) throws DbException;
|
||||
|
||||
void recordSuccessfulConnection(Transaction txn, long now)
|
||||
@@ -30,4 +43,23 @@ public interface MailboxSettingsManager {
|
||||
|
||||
@Nullable
|
||||
String getPendingUpload(Transaction txn, ContactId id) throws DbException;
|
||||
|
||||
interface MailboxHook {
|
||||
/**
|
||||
* Called when Briar is paired with a mailbox
|
||||
*
|
||||
* @param txn A read-write transaction
|
||||
* @param ownOnion Our new mailbox's onion (56 base32 chars)
|
||||
*/
|
||||
void mailboxPaired(Transaction txn, String ownOnion,
|
||||
List<MailboxVersion> serverSupports)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Called when the mailbox is unpaired
|
||||
*
|
||||
* @param txn A read-write transaction
|
||||
*/
|
||||
void mailboxUnpaired(Transaction txn) throws DbException;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,30 @@ package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.PROBLEM_MS_SINCE_LAST_SUCCESS;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.PROBLEM_NUM_CONNECTION_FAILURES;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxHelper.getHighestCommonMajorVersion;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class MailboxStatus {
|
||||
|
||||
private final long lastAttempt, lastSuccess;
|
||||
private final int attemptsSinceSuccess;
|
||||
private final List<MailboxVersion> serverSupports;
|
||||
|
||||
public MailboxStatus(long lastAttempt, long lastSuccess,
|
||||
int attemptsSinceSuccess) {
|
||||
int attemptsSinceSuccess,
|
||||
List<MailboxVersion> serverSupports) {
|
||||
this.lastAttempt = lastAttempt;
|
||||
this.lastSuccess = lastSuccess;
|
||||
this.attemptsSinceSuccess = attemptsSinceSuccess;
|
||||
this.serverSupports = serverSupports;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,4 +66,21 @@ public class MailboxStatus {
|
||||
public int getAttemptsSinceSuccess() {
|
||||
return attemptsSinceSuccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this status indicates a problem with the mailbox.
|
||||
*/
|
||||
public boolean hasProblem(long now) {
|
||||
return attemptsSinceSuccess >= PROBLEM_NUM_CONNECTION_FAILURES &&
|
||||
(now - lastSuccess) >= PROBLEM_MS_SINCE_LAST_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a positive integer if the mailbox is compatible. Same result as
|
||||
* {@link MailboxHelper#getHighestCommonMajorVersion(List, List)}.
|
||||
*/
|
||||
public int getMailboxCompatibility() {
|
||||
return getHighestCommonMajorVersion(CLIENT_SUPPORTS, serverSupports);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class MailboxUpdate {
|
||||
private final boolean hasMailbox;
|
||||
private final List<MailboxVersion> clientSupports;
|
||||
|
||||
public MailboxUpdate(List<MailboxVersion> clientSupports) {
|
||||
this(clientSupports, false);
|
||||
}
|
||||
|
||||
MailboxUpdate(List<MailboxVersion> clientSupports, boolean hasMailbox) {
|
||||
this.clientSupports = clientSupports;
|
||||
this.hasMailbox = hasMailbox;
|
||||
}
|
||||
|
||||
public List<MailboxVersion> getClientSupports() {
|
||||
return clientSupports;
|
||||
}
|
||||
|
||||
public boolean hasMailbox() {
|
||||
return hasMailbox;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.ClientId;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface MailboxUpdateManager {
|
||||
|
||||
/**
|
||||
* The unique ID of the mailbox update (properties) client.
|
||||
*/
|
||||
ClientId CLIENT_ID =
|
||||
new ClientId("org.briarproject.bramble.mailbox.properties");
|
||||
|
||||
/**
|
||||
* The current major version of the mailbox update (properties) client.
|
||||
*/
|
||||
int MAJOR_VERSION = 2;
|
||||
|
||||
/**
|
||||
* The current minor version of the mailbox update (properties) client.
|
||||
*/
|
||||
int MINOR_VERSION = 0;
|
||||
|
||||
/**
|
||||
* The number of properties required for an update message with a mailbox.
|
||||
*/
|
||||
int PROP_COUNT = 4;
|
||||
|
||||
/**
|
||||
* The required properties of an update message with a mailbox.
|
||||
*/
|
||||
String PROP_KEY_ONION = "onion";
|
||||
String PROP_KEY_AUTHTOKEN = "authToken";
|
||||
String PROP_KEY_INBOXID = "inboxId";
|
||||
String PROP_KEY_OUTBOXID = "outboxId";
|
||||
|
||||
/**
|
||||
* Length of the Onion property.
|
||||
*/
|
||||
int PROP_ONION_LENGTH = 56;
|
||||
|
||||
/**
|
||||
* Message metadata key for the version number of a local or remote update,
|
||||
* as a BDF long.
|
||||
*/
|
||||
String MSG_KEY_VERSION = "version";
|
||||
|
||||
/**
|
||||
* Message metadata key for whether an update is local or remote, as a BDF
|
||||
* boolean.
|
||||
*/
|
||||
String MSG_KEY_LOCAL = "local";
|
||||
|
||||
/**
|
||||
* Key in the client's local group for storing the clientSupports list that
|
||||
* was last sent out.
|
||||
*/
|
||||
String GROUP_KEY_SENT_CLIENT_SUPPORTS = "sentClientSupports";
|
||||
|
||||
MailboxUpdate getLocalUpdate(Transaction txn, ContactId c)
|
||||
throws DbException;
|
||||
|
||||
@Nullable
|
||||
MailboxUpdate getRemoteUpdate(Transaction txn, ContactId c)
|
||||
throws DbException;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class MailboxUpdateWithMailbox extends MailboxUpdate {
|
||||
|
||||
private final MailboxProperties properties;
|
||||
|
||||
public MailboxUpdateWithMailbox(List<MailboxVersion> clientSupports,
|
||||
MailboxProperties properties) {
|
||||
super(clientSupports, true);
|
||||
if (properties.isOwner()) throw new IllegalArgumentException();
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public MailboxUpdateWithMailbox(MailboxUpdateWithMailbox o,
|
||||
List<MailboxVersion> newClientSupports) {
|
||||
this(newClientSupports, o.getMailboxProperties());
|
||||
}
|
||||
|
||||
public MailboxProperties getMailboxProperties() {
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class MailboxVersion implements Comparable<MailboxVersion> {
|
||||
|
||||
private final int major;
|
||||
private final int minor;
|
||||
|
||||
public MailboxVersion(int major, int minor) {
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
}
|
||||
|
||||
public int getMajor() {
|
||||
return major;
|
||||
}
|
||||
|
||||
public int getMinor() {
|
||||
return minor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof MailboxVersion) {
|
||||
MailboxVersion v = (MailboxVersion) o;
|
||||
return major == v.major && minor == v.minor;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(MailboxVersion v) {
|
||||
int c = major - v.major;
|
||||
if (c != 0) {
|
||||
return c;
|
||||
}
|
||||
return minor - v.minor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.briarproject.bramble.api.mailbox.event;
|
||||
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* An event that is broadcast by {@link MailboxSettingsManager} when
|
||||
* recording a connection failure for own Mailbox
|
||||
* that has persistent for long enough for the mailbox owner to become active
|
||||
* and fix the problem with the mailbox.
|
||||
*/
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class MailboxProblemEvent extends Event {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.briarproject.bramble.api.mailbox.event;
|
||||
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxStatus;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* An event that is broadcast by {@link MailboxSettingsManager} when
|
||||
* recording the connection status of own Mailbox.
|
||||
*/
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class OwnMailboxConnectionStatusEvent extends Event {
|
||||
|
||||
private final MailboxStatus status;
|
||||
|
||||
public OwnMailboxConnectionStatusEvent(MailboxStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public MailboxStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.briarproject.bramble.api.mailbox.event;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdate;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* An event that is broadcast when {@link MailboxUpdate} are received
|
||||
* from a contact.
|
||||
*/
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class RemoteMailboxUpdateEvent extends Event {
|
||||
|
||||
private final ContactId contactId;
|
||||
private final MailboxUpdate mailboxUpdate;
|
||||
|
||||
public RemoteMailboxUpdateEvent(ContactId contactId,
|
||||
MailboxUpdate mailboxUpdate) {
|
||||
this.contactId = contactId;
|
||||
this.mailboxUpdate = mailboxUpdate;
|
||||
}
|
||||
|
||||
public ContactId getContact() {
|
||||
return contactId;
|
||||
}
|
||||
|
||||
public MailboxUpdate getMailboxUpdate() {
|
||||
return mailboxUpdate;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.briarproject.bramble.api.plugin;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
public interface TorConstants {
|
||||
|
||||
TransportId ID = new TransportId("org.briarproject.bramble.tor");
|
||||
@@ -10,8 +12,9 @@ public interface TorConstants {
|
||||
int DEFAULT_SOCKS_PORT = 59050;
|
||||
int DEFAULT_CONTROL_PORT = 59051;
|
||||
|
||||
int CONNECT_TO_PROXY_TIMEOUT = 5000; // Milliseconds
|
||||
int EXTRA_SOCKET_TIMEOUT = 30000; // Milliseconds
|
||||
int CONNECT_TO_PROXY_TIMEOUT = (int) SECONDS.toMillis(5);
|
||||
int EXTRA_CONNECT_TIMEOUT = (int) SECONDS.toMillis(120);
|
||||
int EXTRA_SOCKET_TIMEOUT = (int) SECONDS.toMillis(30);
|
||||
|
||||
// Local settings (not shared with contacts)
|
||||
String PREF_TOR_NETWORK = "network2";
|
||||
|
||||
@@ -12,4 +12,6 @@ public interface RecordWriter {
|
||||
void flush() throws IOException;
|
||||
|
||||
void close() throws IOException;
|
||||
|
||||
long getBytesWritten();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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);
|
||||
}
|
||||
@@ -20,9 +20,4 @@ public class GroupId extends UniqueId {
|
||||
public GroupId(byte[] id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof GroupId && super.equals(o);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,4 @@ public class MessageId extends UniqueId {
|
||||
public MessageId(byte[] id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof MessageId && super.equals(o);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,6 @@ public interface SyncRecordWriter {
|
||||
void writePriority(Priority p) throws IOException;
|
||||
|
||||
void flush() throws IOException;
|
||||
|
||||
long getBytesWritten();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.api.system;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
@@ -16,6 +17,8 @@ public interface TaskScheduler {
|
||||
* <p>
|
||||
* If the platform supports wake locks, a wake lock will be held while
|
||||
* submitting and running the task.
|
||||
*
|
||||
* @return A {@link Cancellable} for cancelling the task.
|
||||
*/
|
||||
Cancellable schedule(Runnable task, Executor executor, long delay,
|
||||
TimeUnit unit);
|
||||
@@ -27,17 +30,11 @@ public interface TaskScheduler {
|
||||
* <p>
|
||||
* If the platform supports wake locks, a wake lock will be held while
|
||||
* submitting and running the task.
|
||||
*
|
||||
* @return A {@link Cancellable} for cancelling all future executions of
|
||||
* the task.
|
||||
*/
|
||||
Cancellable scheduleWithFixedDelay(Runnable task, Executor executor,
|
||||
long delay, long interval, TimeUnit unit);
|
||||
|
||||
interface Cancellable {
|
||||
|
||||
/**
|
||||
* Cancels the task if it has not already started running. If the task
|
||||
* is {@link #scheduleWithFixedDelay(Runnable, Executor, long, long, TimeUnit) periodic},
|
||||
* all future executions of the task are cancelled.
|
||||
*/
|
||||
void cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public class NetworkUtils {
|
||||
// Despite what the docs say, the return value can be null
|
||||
//noinspection ConstantConditions
|
||||
return ifaces == null ? emptyList() : list(ifaces);
|
||||
} catch (SocketException e) {
|
||||
} catch (SocketException | NullPointerException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.API_CLIENT_TOO_OLD;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.API_SERVER_TOO_OLD;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxHelper.getHighestCommonMajorVersion;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class MailboxHelperTest {
|
||||
|
||||
private final Random random = new Random();
|
||||
|
||||
@Test
|
||||
public void testGetHighestCommonMajorVersion() {
|
||||
assertEquals(2, getHighestCommonMajorVersion(v(2), v(2)));
|
||||
assertEquals(2, getHighestCommonMajorVersion(v(1, 2), v(2, 3, 4)));
|
||||
assertEquals(2, getHighestCommonMajorVersion(v(2, 3, 4), v(2)));
|
||||
assertEquals(2, getHighestCommonMajorVersion(v(2), v(2, 3, 4)));
|
||||
|
||||
assertEquals(API_CLIENT_TOO_OLD,
|
||||
getHighestCommonMajorVersion(v(2), v(3, 4)));
|
||||
assertEquals(API_CLIENT_TOO_OLD,
|
||||
getHighestCommonMajorVersion(v(2), v(1, 3)));
|
||||
assertEquals(API_SERVER_TOO_OLD,
|
||||
getHighestCommonMajorVersion(v(3, 4, 5), v(2)));
|
||||
assertEquals(API_SERVER_TOO_OLD,
|
||||
getHighestCommonMajorVersion(v(1, 3), v(2)));
|
||||
}
|
||||
|
||||
private List<MailboxVersion> v(int... ints) {
|
||||
List<MailboxVersion> versions = new ArrayList<>(ints.length);
|
||||
for (int v : ints) {
|
||||
// minor versions should not matter
|
||||
versions.add(new MailboxVersion(v, random.nextInt(42)));
|
||||
}
|
||||
return versions;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +1,46 @@
|
||||
package org.briarproject.bramble.test;
|
||||
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
public abstract class BrambleTestCase {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(BrambleTestCase.class.getName());
|
||||
|
||||
@Nullable
|
||||
protected volatile Throwable exceptionInBackgroundThread = null;
|
||||
|
||||
public BrambleTestCase() {
|
||||
// Ensure exceptions thrown on worker threads cause tests to fail
|
||||
UncaughtExceptionHandler fail = (thread, throwable) -> {
|
||||
throwable.printStackTrace();
|
||||
fail();
|
||||
LOG.log(WARNING, "Caught unhandled exception", throwable);
|
||||
exceptionInBackgroundThread = throwable;
|
||||
};
|
||||
Thread.setDefaultUncaughtExceptionHandler(fail);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void beforeBrambleTestCase() {
|
||||
exceptionInBackgroundThread = null;
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterBrambleTestCase() {
|
||||
Throwable thrown = exceptionInBackgroundThread;
|
||||
if (thrown != null) {
|
||||
LOG.log(WARNING,
|
||||
"Background thread has thrown an exception unexpectedly",
|
||||
thrown);
|
||||
throw new AssertionError(thrown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,20 @@ import org.briarproject.bramble.api.crypto.PublicKey;
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.crypto.SignaturePrivateKey;
|
||||
import org.briarproject.bramble.api.crypto.SignaturePublicKey;
|
||||
import org.briarproject.bramble.api.db.CommitAction;
|
||||
import org.briarproject.bramble.api.db.EventAction;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.identity.Identity;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdate;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateWithMailbox;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||
import org.briarproject.bramble.api.sync.ClientId;
|
||||
@@ -30,6 +40,7 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -39,6 +50,9 @@ import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES;
|
||||
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_PUBLIC_KEY_BYTES;
|
||||
@@ -214,6 +228,19 @@ public class TestUtils {
|
||||
getAgreementPublicKey(), verified);
|
||||
}
|
||||
|
||||
public static MailboxProperties getMailboxProperties(boolean owner,
|
||||
List<MailboxVersion> serverSupports) {
|
||||
String baseUrl = "http://" + getRandomString(56) + ".onion"; // TODO
|
||||
MailboxAuthToken authToken = new MailboxAuthToken(getRandomId());
|
||||
if (owner) {
|
||||
return new MailboxProperties(baseUrl, authToken, serverSupports);
|
||||
}
|
||||
MailboxFolderId inboxId = new MailboxFolderId(getRandomId());
|
||||
MailboxFolderId outboxId = new MailboxFolderId(getRandomId());
|
||||
return new MailboxProperties(baseUrl, authToken, serverSupports,
|
||||
inboxId, outboxId);
|
||||
}
|
||||
|
||||
public static void writeBytes(File file, byte[] bytes)
|
||||
throws IOException {
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
@@ -266,9 +293,57 @@ public class TestUtils {
|
||||
return Math.sqrt(getVariance(samples));
|
||||
}
|
||||
|
||||
public static boolean isOptionalTestEnabled(Class testClass) {
|
||||
public static boolean isOptionalTestEnabled(Class<?> testClass) {
|
||||
String optionalTests = System.getenv("OPTIONAL_TESTS");
|
||||
return optionalTests != null &&
|
||||
asList(optionalTests.split(",")).contains(testClass.getName());
|
||||
}
|
||||
|
||||
public static boolean mailboxUpdateEqual(@Nullable MailboxUpdate a,
|
||||
@Nullable MailboxUpdate b) {
|
||||
if (a == null || b == null) {
|
||||
return a == b;
|
||||
}
|
||||
if (!a.hasMailbox() && !b.hasMailbox()) {
|
||||
return a.getClientSupports().equals(b.getClientSupports());
|
||||
} else if (a.hasMailbox() && b.hasMailbox()) {
|
||||
MailboxUpdateWithMailbox am = (MailboxUpdateWithMailbox) a;
|
||||
MailboxUpdateWithMailbox bm = (MailboxUpdateWithMailbox) b;
|
||||
return am.getClientSupports().equals(bm.getClientSupports()) &&
|
||||
mailboxPropertiesEqual(am.getMailboxProperties(),
|
||||
bm.getMailboxProperties());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean mailboxPropertiesEqual(@Nullable MailboxProperties a,
|
||||
@Nullable MailboxProperties b) {
|
||||
if (a == null || b == null) {
|
||||
return a == b;
|
||||
}
|
||||
return a.getOnion().equals(b.getOnion()) &&
|
||||
a.getAuthToken().equals(b.getAuthToken()) &&
|
||||
a.isOwner() == b.isOwner() &&
|
||||
a.getServerSupports().equals(b.getServerSupports());
|
||||
}
|
||||
|
||||
public static boolean hasEvent(Transaction txn,
|
||||
Class<? extends Event> eventClass) {
|
||||
for (CommitAction action : txn.getActions()) {
|
||||
if (action instanceof EventAction) {
|
||||
Event event = ((EventAction) action).getEvent();
|
||||
if (eventClass.isInstance(event)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isCryptoStrengthUnlimited() {
|
||||
try {
|
||||
return Cipher.getMaxAllowedKeyLength("AES/CBC/PKCS5Padding")
|
||||
== Integer.MAX_VALUE;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.briarproject.bramble.test;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class ThreadExceptionTest extends BrambleTestCase {
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void testAssertionErrorMakesTestCaseFail() {
|
||||
// This is what BrambleTestCase does, too:
|
||||
fail();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExceptionInThreadMakesTestCaseFail() {
|
||||
Thread t = new Thread(() -> {
|
||||
System.out.println("thread before exception");
|
||||
throw new RuntimeException("boom");
|
||||
});
|
||||
|
||||
t.start();
|
||||
try {
|
||||
t.join();
|
||||
System.out.println("joined thread");
|
||||
} catch (InterruptedException e) {
|
||||
System.out.println("interrupted while joining thread");
|
||||
fail();
|
||||
}
|
||||
|
||||
assertNotNull(exceptionInBackgroundThread);
|
||||
exceptionInBackgroundThread = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
dependencyVerification {
|
||||
verify = [
|
||||
'cglib:cglib:3.2.8:cglib-3.2.8.jar:3f64de999ecc5595dc84ca8ff0879d8a34c8623f9ef3c517a53ed59023fcb9db',
|
||||
'com.fasterxml.jackson.core:jackson-annotations:2.13.0:jackson-annotations-2.13.0.jar:81f9724d8843e8b08f8f6c0609e7a2b030d00c34861c4ac7e2099a7235047d6f',
|
||||
'com.google.code.findbugs:annotations:3.0.1:annotations-3.0.1.jar:6b47ff0a6de0ce17cbedc3abb0828ca5bce3009d53ea47b3723ff023c4742f79',
|
||||
'com.google.code.findbugs:jsr305:3.0.2:jsr305-3.0.2.jar:766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7',
|
||||
'com.google.dagger:dagger:2.33:dagger-2.33.jar:d8798c5b8cf6b125234e33af5c6293bb9f2208ce29b57924c35b8c0be7b6bdcb',
|
||||
|
||||
@@ -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.3'
|
||||
implementation 'org.briarproject:jtorctl:0.4'
|
||||
|
||||
//noinspection GradleDependency
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.briarproject.bramble.crypto.CryptoExecutorModule;
|
||||
import org.briarproject.bramble.db.DatabaseExecutorModule;
|
||||
import org.briarproject.bramble.identity.IdentityModule;
|
||||
import org.briarproject.bramble.lifecycle.LifecycleModule;
|
||||
import org.briarproject.bramble.mailbox.MailboxModule;
|
||||
import org.briarproject.bramble.plugin.PluginModule;
|
||||
import org.briarproject.bramble.properties.PropertiesModule;
|
||||
import org.briarproject.bramble.rendezvous.RendezvousModule;
|
||||
@@ -28,6 +29,8 @@ public interface BrambleCoreEagerSingletons {
|
||||
|
||||
void inject(LifecycleModule.EagerSingletons init);
|
||||
|
||||
void inject(MailboxModule.EagerSingletons init);
|
||||
|
||||
void inject(PluginModule.EagerSingletons init);
|
||||
|
||||
void inject(PropertiesModule.EagerSingletons init);
|
||||
@@ -51,6 +54,7 @@ public interface BrambleCoreEagerSingletons {
|
||||
c.inject(new DatabaseExecutorModule.EagerSingletons());
|
||||
c.inject(new IdentityModule.EagerSingletons());
|
||||
c.inject(new LifecycleModule.EagerSingletons());
|
||||
c.inject(new MailboxModule.EagerSingletons());
|
||||
c.inject(new RendezvousModule.EagerSingletons());
|
||||
c.inject(new PluginModule.EagerSingletons());
|
||||
c.inject(new PropertiesModule.EagerSingletons());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.briarproject.bramble.client;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.UniqueId;
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
@@ -22,6 +23,12 @@ import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.identity.AuthorFactory;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdate;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateWithMailbox;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||
@@ -29,13 +36,16 @@ import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageFactory;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.util.Base32;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
@@ -46,6 +56,12 @@ import static org.briarproject.bramble.api.client.ContactGroupConstants.GROUP_KE
|
||||
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxUpdateManager.PROP_COUNT;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxUpdateManager.PROP_KEY_AUTHTOKEN;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxUpdateManager.PROP_KEY_INBOXID;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxUpdateManager.PROP_KEY_ONION;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxUpdateManager.PROP_KEY_OUTBOXID;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxUpdateManager.PROP_ONION_LENGTH;
|
||||
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT;
|
||||
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
|
||||
import static org.briarproject.bramble.util.ValidationUtils.checkLength;
|
||||
@@ -399,6 +415,69 @@ class ClientHelperImpl implements ClientHelper {
|
||||
return tpMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxUpdate parseAndValidateMailboxUpdate(BdfList clientSupports,
|
||||
BdfList serverSupports, BdfDictionary properties)
|
||||
throws FormatException {
|
||||
List<MailboxVersion> clientSupportsList =
|
||||
parseMailboxVersionList(clientSupports);
|
||||
List<MailboxVersion> serverSupportsList =
|
||||
parseMailboxVersionList(serverSupports);
|
||||
|
||||
// We must always learn what Mailbox API version(s) the client supports
|
||||
if (clientSupports.isEmpty()) {
|
||||
throw new FormatException();
|
||||
}
|
||||
if (properties.isEmpty()) {
|
||||
// No mailbox -- cannot claim to support any API versions!
|
||||
if (!serverSupports.isEmpty()) {
|
||||
throw new FormatException();
|
||||
}
|
||||
return new MailboxUpdate(clientSupportsList);
|
||||
}
|
||||
// Mailbox must be accompanied by the Mailbox API version(s) it supports
|
||||
if (serverSupports.isEmpty()) {
|
||||
throw new FormatException();
|
||||
}
|
||||
// Accepting more props than we need, for forward compatibility
|
||||
if (properties.size() < PROP_COUNT) {
|
||||
throw new FormatException();
|
||||
}
|
||||
String onion = properties.getString(PROP_KEY_ONION);
|
||||
checkLength(onion, PROP_ONION_LENGTH);
|
||||
try {
|
||||
Base32.decode(onion, true);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new FormatException();
|
||||
}
|
||||
byte[] authToken = properties.getRaw(PROP_KEY_AUTHTOKEN);
|
||||
checkLength(authToken, UniqueId.LENGTH);
|
||||
byte[] inboxId = properties.getRaw(PROP_KEY_INBOXID);
|
||||
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,
|
||||
new MailboxAuthToken(authToken), serverSupportsList,
|
||||
new MailboxFolderId(inboxId), new MailboxFolderId(outboxId));
|
||||
return new MailboxUpdateWithMailbox(clientSupportsList, props);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MailboxVersion> parseMailboxVersionList(BdfList bdfList)
|
||||
throws FormatException {
|
||||
List<MailboxVersion> list = new ArrayList<>();
|
||||
for (int i = 0; i < bdfList.size(); i++) {
|
||||
BdfList element = bdfList.getList(i);
|
||||
if (element.size() != 2) {
|
||||
throw new FormatException();
|
||||
}
|
||||
list.add(new MailboxVersion(element.getLong(0).intValue(),
|
||||
element.getLong(1).intValue()));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContactId getContactId(Transaction txn, GroupId contactGroupId)
|
||||
throws DbException {
|
||||
|
||||
@@ -54,7 +54,7 @@ abstract class Connection {
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] readTag(InputStream in) throws IOException {
|
||||
byte[] readTag(InputStream in) throws IOException {
|
||||
byte[] tag = new byte[TAG_LENGTH];
|
||||
read(in, tag);
|
||||
return tag;
|
||||
|
||||
@@ -67,7 +67,15 @@ class ConnectionManagerImpl implements ConnectionManager {
|
||||
TransportConnectionReader r) {
|
||||
ioExecutor.execute(new IncomingSimplexSyncConnection(keyManager,
|
||||
connectionRegistry, streamReaderFactory, streamWriterFactory,
|
||||
syncSessionFactory, transportPropertyManager, t, r));
|
||||
syncSessionFactory, transportPropertyManager, t, r, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void manageIncomingConnection(TransportId t,
|
||||
TransportConnectionReader r, TagController c) {
|
||||
ioExecutor.execute(new IncomingSimplexSyncConnection(keyManager,
|
||||
connectionRegistry, streamReaderFactory, streamWriterFactory,
|
||||
syncSessionFactory, transportPropertyManager, t, r, c));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.briarproject.bramble.connection;
|
||||
|
||||
import org.briarproject.bramble.api.connection.ConnectionManager.TagController;
|
||||
import org.briarproject.bramble.api.connection.ConnectionRegistry;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
@@ -15,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.util.LogUtils.logException;
|
||||
|
||||
@@ -23,6 +27,8 @@ class IncomingSimplexSyncConnection extends SyncConnection implements Runnable {
|
||||
|
||||
private final TransportId transportId;
|
||||
private final TransportConnectionReader reader;
|
||||
@Nullable
|
||||
private final TagController tagController;
|
||||
|
||||
IncomingSimplexSyncConnection(KeyManager keyManager,
|
||||
ConnectionRegistry connectionRegistry,
|
||||
@@ -30,33 +36,50 @@ class IncomingSimplexSyncConnection extends SyncConnection implements Runnable {
|
||||
StreamWriterFactory streamWriterFactory,
|
||||
SyncSessionFactory syncSessionFactory,
|
||||
TransportPropertyManager transportPropertyManager,
|
||||
TransportId transportId, TransportConnectionReader reader) {
|
||||
TransportId transportId,
|
||||
TransportConnectionReader reader,
|
||||
@Nullable TagController tagController) {
|
||||
super(keyManager, connectionRegistry, streamReaderFactory,
|
||||
streamWriterFactory, syncSessionFactory,
|
||||
transportPropertyManager);
|
||||
this.transportId = transportId;
|
||||
this.reader = reader;
|
||||
this.tagController = tagController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Read and recognise the tag
|
||||
StreamContext ctx = recogniseTag(reader, transportId);
|
||||
byte[] tag;
|
||||
StreamContext ctx;
|
||||
try {
|
||||
tag = readTag(reader.getInputStream());
|
||||
// If we have a tag controller, defer marking the tag as recognised
|
||||
if (tagController == null) {
|
||||
ctx = keyManager.getStreamContext(transportId, tag);
|
||||
} else {
|
||||
ctx = keyManager.getStreamContextOnly(transportId, tag);
|
||||
}
|
||||
} catch (IOException | DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
onError();
|
||||
return;
|
||||
}
|
||||
if (ctx == null) {
|
||||
LOG.info("Unrecognised tag");
|
||||
onError(false);
|
||||
onError();
|
||||
return;
|
||||
}
|
||||
ContactId contactId = ctx.getContactId();
|
||||
if (contactId == null) {
|
||||
LOG.warning("Received rendezvous stream, expected contact");
|
||||
onError(true);
|
||||
onError(tag);
|
||||
return;
|
||||
}
|
||||
if (ctx.isHandshakeMode()) {
|
||||
// TODO: Support handshake mode for contacts
|
||||
LOG.warning("Received handshake tag, expected rotation mode");
|
||||
onError(true);
|
||||
onError(tag);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -65,15 +88,33 @@ class IncomingSimplexSyncConnection extends SyncConnection implements Runnable {
|
||||
LOG.info("Ignoring priority for simplex connection");
|
||||
// Create and run the incoming session
|
||||
createIncomingSession(ctx, reader, handler).run();
|
||||
// Success
|
||||
markTagAsRecognisedIfRequired(false, tag);
|
||||
reader.dispose(false, true);
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
onError(true);
|
||||
onError(tag);
|
||||
}
|
||||
}
|
||||
|
||||
private void onError(boolean recognised) {
|
||||
disposeOnError(reader, recognised);
|
||||
private void onError() {
|
||||
disposeOnError(reader, false);
|
||||
}
|
||||
|
||||
private void onError(byte[] tag) {
|
||||
markTagAsRecognisedIfRequired(true, tag);
|
||||
disposeOnError(reader, true);
|
||||
}
|
||||
|
||||
private void markTagAsRecognisedIfRequired(boolean exception, byte[] tag) {
|
||||
if (tagController != null &&
|
||||
tagController.shouldMarkTagAsRecognised(exception)) {
|
||||
try {
|
||||
keyManager.markTagAsRecognised(transportId, tag);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.bouncycastle.crypto.CryptoException;
|
||||
import org.bouncycastle.crypto.Digest;
|
||||
import org.bouncycastle.crypto.digests.Blake2bDigest;
|
||||
import org.bouncycastle.crypto.digests.SHA3Digest;
|
||||
import org.briarproject.bramble.api.UniqueId;
|
||||
import org.briarproject.bramble.api.crypto.AgreementPrivateKey;
|
||||
import org.briarproject.bramble.api.crypto.AgreementPublicKey;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
@@ -41,6 +42,7 @@ import javax.inject.Inject;
|
||||
|
||||
import static java.lang.System.arraycopy;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_AGREEMENT;
|
||||
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_SIGNATURE;
|
||||
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
|
||||
@@ -54,7 +56,7 @@ import static org.briarproject.bramble.util.LogUtils.now;
|
||||
class CryptoComponentImpl implements CryptoComponent {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(CryptoComponentImpl.class.getName());
|
||||
getLogger(CryptoComponentImpl.class.getName());
|
||||
|
||||
private static final int SIGNATURE_KEY_PAIR_BITS = 256;
|
||||
private static final int STORAGE_IV_BYTES = 24; // 196 bits
|
||||
@@ -128,6 +130,13 @@ class CryptoComponentImpl implements CryptoComponent {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UniqueId generateUniqueId() {
|
||||
byte[] b = new byte[UniqueId.LENGTH];
|
||||
secureRandom.nextBytes(b);
|
||||
return new UniqueId(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecretKey generateSecretKey() {
|
||||
byte[] b = new byte[SecretKey.LENGTH];
|
||||
@@ -449,7 +458,7 @@ class CryptoComponentImpl implements CryptoComponent {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encodeOnionAddress(byte[] publicKey) {
|
||||
public String encodeOnion(byte[] publicKey) {
|
||||
Digest digest = new SHA3Digest(256);
|
||||
byte[] label = ".onion checksum".getBytes(Charset.forName("US-ASCII"));
|
||||
digest.update(label, 0, label.length);
|
||||
|
||||
@@ -406,6 +406,12 @@ interface Database<T> {
|
||||
Collection<MessageId> getMessageIds(T txn, GroupId g, Metadata query)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the length of the given message in bytes, including the
|
||||
* message header.
|
||||
*/
|
||||
int getMessageLength(T txn, MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the metadata for all delivered messages in the given group.
|
||||
* <p/>
|
||||
@@ -496,7 +502,8 @@ interface Database<T> {
|
||||
|
||||
/**
|
||||
* Returns the IDs of some messages that are eligible to be sent to the
|
||||
* given contact, up to the given total length.
|
||||
* given contact. The total length of the messages including record headers
|
||||
* will be no more than the given capacity.
|
||||
* <p/>
|
||||
* Unlike {@link #getUnackedMessagesToSend(Object, ContactId)} this method
|
||||
* does not return messages that have already been sent unless they are
|
||||
@@ -504,20 +511,20 @@ interface Database<T> {
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Collection<MessageId> getMessagesToSend(T txn, ContactId c, int maxLength,
|
||||
Collection<MessageId> getMessagesToSend(T txn, ContactId c, long capacity,
|
||||
long maxLatency) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the IDs of all messages that are eligible to be sent to the
|
||||
* given contact, together with their raw lengths.
|
||||
* given contact.
|
||||
* <p/>
|
||||
* Unlike {@link #getMessagesToSend(Object, ContactId, int, long)} this
|
||||
* Unlike {@link #getMessagesToSend(Object, ContactId, long, long)} this
|
||||
* method may return messages that have already been sent and are not yet
|
||||
* due for retransmission.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Map<MessageId, Integer> getUnackedMessagesToSend(T txn, ContactId c)
|
||||
Collection<MessageId> getUnackedMessagesToSend(T txn, ContactId c)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
@@ -598,13 +605,14 @@ interface Database<T> {
|
||||
|
||||
/**
|
||||
* Returns the IDs of some messages that are eligible to be sent to the
|
||||
* given contact and have been requested by the contact, up to the given
|
||||
* total length.
|
||||
* given contact and have been requested by the contact. The total length
|
||||
* of the messages including record headers will be no more than the given
|
||||
* capacity.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Collection<MessageId> getRequestedMessagesToSend(T txn, ContactId c,
|
||||
int maxLength, long maxLatency) throws DbException;
|
||||
long capacity, long maxLatency) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns all settings in the given namespace.
|
||||
@@ -758,9 +766,10 @@ interface Database<T> {
|
||||
void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Resets the transmission count, expiry time and ETA of all messages that
|
||||
* are eligible to be sent to the given contact. This includes messages that
|
||||
* have already been sent and are not yet due for retransmission.
|
||||
* Resets the transmission count, expiry time and max latency of all
|
||||
* messages that are eligible to be sent to the given contact. This includes
|
||||
* messages that have already been sent and are not yet due for
|
||||
* retransmission.
|
||||
*/
|
||||
void resetUnackedMessagesToSend(T txn, ContactId c) throws DbException;
|
||||
|
||||
@@ -848,11 +857,13 @@ interface Database<T> {
|
||||
void stopCleanupTimer(T txn, MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Updates the transmission count, expiry time and estimated time of arrival
|
||||
* of the given message with respect to the given contact, using the latency
|
||||
* of the transport over which it was sent.
|
||||
* Updates the transmission count, expiry time and max latency of the given
|
||||
* message with respect to the given contact.
|
||||
*
|
||||
* @param maxLatency latency of the transport over which the message was
|
||||
* sent.
|
||||
*/
|
||||
void updateExpiryTimeAndEta(T txn, ContactId c, MessageId m,
|
||||
void updateRetransmissionData(T txn, ContactId c, MessageId m,
|
||||
long maxLatency) throws DbException;
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,7 +75,6 @@ import org.briarproject.bramble.api.transport.TransportKeys;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
@@ -87,6 +86,7 @@ import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||
@@ -424,53 +424,27 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
@Nullable
|
||||
@Override
|
||||
public Collection<Message> generateBatch(Transaction transaction,
|
||||
ContactId c, int maxLength, long maxLatency) throws DbException {
|
||||
ContactId c, long capacity, long maxLatency) throws DbException {
|
||||
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
Collection<MessageId> ids =
|
||||
db.getMessagesToSend(txn, c, maxLength, maxLatency);
|
||||
db.getMessagesToSend(txn, c, capacity, maxLatency);
|
||||
if (ids.isEmpty()) return null;
|
||||
long totalLength = 0;
|
||||
List<Message> messages = new ArrayList<>(ids.size());
|
||||
for (MessageId m : ids) {
|
||||
Message message = db.getMessage(txn, m);
|
||||
totalLength += message.getRawLength();
|
||||
messages.add(message);
|
||||
db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
|
||||
db.updateRetransmissionData(txn, c, m, maxLatency);
|
||||
}
|
||||
if (ids.isEmpty()) return null;
|
||||
db.lowerRequestedFlag(txn, c, ids);
|
||||
transaction.attach(new MessagesSentEvent(c, ids, totalLength));
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Message> generateBatch(Transaction transaction,
|
||||
ContactId c, Collection<MessageId> ids, long maxLatency)
|
||||
throws DbException {
|
||||
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
long totalLength = 0;
|
||||
List<Message> messages = new ArrayList<>(ids.size());
|
||||
List<MessageId> sentIds = new ArrayList<>(ids.size());
|
||||
for (MessageId m : ids) {
|
||||
if (db.containsVisibleMessage(txn, c, m)) {
|
||||
Message message = db.getMessage(txn, m);
|
||||
totalLength += message.getRawLength();
|
||||
messages.add(message);
|
||||
sentIds.add(m);
|
||||
db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
|
||||
}
|
||||
}
|
||||
if (messages.isEmpty()) return messages;
|
||||
db.lowerRequestedFlag(txn, c, sentIds);
|
||||
transaction.attach(new MessagesSentEvent(c, sentIds, totalLength));
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Offer generateOffer(Transaction transaction, ContactId c,
|
||||
@@ -483,7 +457,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
db.getMessagesToOffer(txn, c, maxMessages, maxLatency);
|
||||
if (ids.isEmpty()) return null;
|
||||
for (MessageId m : ids)
|
||||
db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
|
||||
db.updateRetransmissionData(txn, c, m, maxLatency);
|
||||
return new Offer(ids);
|
||||
}
|
||||
|
||||
@@ -505,22 +479,22 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
@Nullable
|
||||
@Override
|
||||
public Collection<Message> generateRequestedBatch(Transaction transaction,
|
||||
ContactId c, int maxLength, long maxLatency) throws DbException {
|
||||
ContactId c, long capacity, long maxLatency) throws DbException {
|
||||
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
Collection<MessageId> ids =
|
||||
db.getRequestedMessagesToSend(txn, c, maxLength, maxLatency);
|
||||
db.getRequestedMessagesToSend(txn, c, capacity, maxLatency);
|
||||
if (ids.isEmpty()) return null;
|
||||
long totalLength = 0;
|
||||
List<Message> messages = new ArrayList<>(ids.size());
|
||||
for (MessageId m : ids) {
|
||||
Message message = db.getMessage(txn, m);
|
||||
totalLength += message.getRawLength();
|
||||
messages.add(message);
|
||||
db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
|
||||
db.updateRetransmissionData(txn, c, m, maxLatency);
|
||||
}
|
||||
if (ids.isEmpty()) return null;
|
||||
db.lowerRequestedFlag(txn, c, ids);
|
||||
transaction.attach(new MessagesSentEvent(c, ids, totalLength));
|
||||
return messages;
|
||||
@@ -635,6 +609,24 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
return db.getMessageIds(txn, g, query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MessageId> getMessagesToAck(Transaction transaction,
|
||||
ContactId c, int maxMessages) throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
return db.getMessagesToAck(txn, c, maxMessages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MessageId> getMessagesToSend(Transaction transaction,
|
||||
ContactId c, long capacity, long maxLatency) throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
return db.getMessagesToSend(txn, c, capacity, maxLatency);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MessageId> getMessagesToValidate(Transaction transaction)
|
||||
throws DbException {
|
||||
@@ -740,10 +732,29 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
return status;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Map<MessageId, Integer> getUnackedMessagesToSend(
|
||||
Transaction transaction,
|
||||
ContactId c) throws DbException {
|
||||
public Message getMessageToSend(Transaction transaction, ContactId c,
|
||||
MessageId m, long maxLatency, boolean markAsSent)
|
||||
throws DbException {
|
||||
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
if (!db.containsVisibleMessage(txn, c, m)) return null;
|
||||
Message message = db.getMessage(txn, m);
|
||||
if (markAsSent) {
|
||||
db.updateRetransmissionData(txn, c, m, maxLatency);
|
||||
db.lowerRequestedFlag(txn, c, singletonList(m));
|
||||
transaction.attach(new MessagesSentEvent(c, singletonList(m),
|
||||
message.getRawLength()));
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MessageId> getUnackedMessagesToSend(
|
||||
Transaction transaction, ContactId c) throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
@@ -1069,6 +1080,20 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
db.removeTransportKeys(txn, t, k);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAckSent(Transaction transaction, ContactId c,
|
||||
Collection<MessageId> acked) throws DbException {
|
||||
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
List<MessageId> visible = new ArrayList<>(acked.size());
|
||||
for (MessageId m : acked) {
|
||||
if (db.containsVisibleMessage(txn, c, m)) visible.add(m);
|
||||
}
|
||||
db.lowerAckFlag(txn, c, visible);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCleanupTimerDuration(Transaction transaction, MessageId m,
|
||||
long duration) throws DbException {
|
||||
@@ -1115,7 +1140,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
if (old == INVISIBLE) db.addGroupVisibility(txn, c, g, v == SHARED);
|
||||
else if (v == INVISIBLE) db.removeGroupVisibility(txn, c, g);
|
||||
else db.setGroupVisibility(txn, c, g, v == SHARED);
|
||||
List<ContactId> affected = Collections.singletonList(c);
|
||||
List<ContactId> affected = singletonList(c);
|
||||
transaction.attach(new GroupVisibilityUpdatedEvent(affected));
|
||||
}
|
||||
|
||||
@@ -1163,6 +1188,28 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
transaction.attach(new MessageStateChangedEvent(m, false, state));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMessagesSent(Transaction transaction, ContactId c,
|
||||
Collection<MessageId> sent, long maxLatency) throws DbException {
|
||||
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
long totalLength = 0;
|
||||
List<MessageId> visible = new ArrayList<>(sent.size());
|
||||
for (MessageId m : sent) {
|
||||
if (db.containsVisibleMessage(txn, c, m)) {
|
||||
visible.add(m);
|
||||
totalLength += db.getMessageLength(txn, m);
|
||||
db.updateRetransmissionData(txn, c, m, maxLatency);
|
||||
}
|
||||
}
|
||||
db.lowerRequestedFlag(txn, c, visible);
|
||||
if (!visible.isEmpty()) {
|
||||
transaction.attach(new MessagesSentEvent(c, visible, totalLength));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMessageDependencies(Transaction transaction,
|
||||
Message dependent, Collection<MessageId> dependencies)
|
||||
|
||||
@@ -2,8 +2,6 @@ package org.briarproject.bramble.db;
|
||||
|
||||
import org.briarproject.bramble.api.settings.Settings;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.DAYS;
|
||||
|
||||
interface DatabaseConstants {
|
||||
|
||||
/**
|
||||
@@ -25,19 +23,6 @@ interface DatabaseConstants {
|
||||
*/
|
||||
String SCHEMA_VERSION_KEY = "schemaVersion";
|
||||
|
||||
/**
|
||||
* The {@link Settings} key under which the time of the last database
|
||||
* compaction is stored.
|
||||
*/
|
||||
String LAST_COMPACTED_KEY = "lastCompacted";
|
||||
|
||||
/**
|
||||
* The maximum time between database compactions in milliseconds. When the
|
||||
* database is opened it will be compacted if more than this amount of time
|
||||
* has passed since the last compaction.
|
||||
*/
|
||||
long MAX_COMPACTION_INTERVAL_MS = DAYS.toMillis(30);
|
||||
|
||||
/**
|
||||
* The {@link Settings} key under which the flag is stored indicating
|
||||
* whether the database is marked as dirty.
|
||||
|
||||
@@ -3,15 +3,16 @@ package org.briarproject.bramble.db;
|
||||
class DatabaseTypes {
|
||||
|
||||
private final String hashType, secretType, binaryType;
|
||||
private final String counterType, stringType;
|
||||
private final String counterType, stringType, schemaQuery;
|
||||
|
||||
public DatabaseTypes(String hashType, String secretType, String binaryType,
|
||||
String counterType, String stringType) {
|
||||
DatabaseTypes(String hashType, String secretType, String binaryType,
|
||||
String counterType, String stringType, String schemaQuery) {
|
||||
this.hashType = hashType;
|
||||
this.secretType = secretType;
|
||||
this.binaryType = binaryType;
|
||||
this.counterType = counterType;
|
||||
this.stringType = stringType;
|
||||
this.schemaQuery = schemaQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,4 +32,8 @@ class DatabaseTypes {
|
||||
s = s.replaceAll("_STRING", stringType);
|
||||
return s;
|
||||
}
|
||||
|
||||
String getSchemaQuery() {
|
||||
return schemaQuery;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,9 @@ class H2Database extends JdbcDatabase {
|
||||
private static final String BINARY_TYPE = "BINARY";
|
||||
private static final String COUNTER_TYPE = "INT NOT NULL AUTO_INCREMENT";
|
||||
private static final String STRING_TYPE = "VARCHAR";
|
||||
private static final String SCHEMA_QUERY = "SHOW TABLES";
|
||||
private static final DatabaseTypes dbTypes = new DatabaseTypes(HASH_TYPE,
|
||||
SECRET_TYPE, BINARY_TYPE, COUNTER_TYPE, STRING_TYPE);
|
||||
SECRET_TYPE, BINARY_TYPE, COUNTER_TYPE, STRING_TYPE, SCHEMA_QUERY);
|
||||
|
||||
private final DatabaseConfig config;
|
||||
private final String url;
|
||||
@@ -85,12 +86,17 @@ class H2Database extends JdbcDatabase {
|
||||
public void close() throws DbException {
|
||||
// H2 will close the database when the last connection closes
|
||||
Connection c = null;
|
||||
Statement s = null;
|
||||
try {
|
||||
c = createConnection();
|
||||
super.closeAllConnections();
|
||||
closeAllConnections();
|
||||
setDirty(c, false);
|
||||
s = c.createStatement();
|
||||
s.execute("SHUTDOWN COMPACT");
|
||||
s.close();
|
||||
c.close();
|
||||
} catch (SQLException e) {
|
||||
tryToClose(s, LOG, WARNING);
|
||||
tryToClose(c, LOG, WARNING);
|
||||
throw new DbException(e);
|
||||
}
|
||||
|
||||
@@ -41,8 +41,11 @@ class HyperSqlDatabase extends JdbcDatabase {
|
||||
private static final String COUNTER_TYPE =
|
||||
"INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY(START WITH 1)";
|
||||
private static final String STRING_TYPE = "VARCHAR";
|
||||
private static final String SCHEMA_QUERY =
|
||||
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.SYSTEM_TABLES"
|
||||
+ " WHERE TABLE_TYPE='TABLE'";
|
||||
private static final DatabaseTypes dbTypes = new DatabaseTypes(HASH_TYPE,
|
||||
SECRET_TYPE, BINARY_TYPE, COUNTER_TYPE, STRING_TYPE);
|
||||
SECRET_TYPE, BINARY_TYPE, COUNTER_TYPE, STRING_TYPE, SCHEMA_QUERY);
|
||||
|
||||
private final DatabaseConfig config;
|
||||
private final String url;
|
||||
@@ -79,11 +82,11 @@ class HyperSqlDatabase extends JdbcDatabase {
|
||||
Connection c = null;
|
||||
Statement s = null;
|
||||
try {
|
||||
super.closeAllConnections();
|
||||
closeAllConnections();
|
||||
c = createConnection();
|
||||
setDirty(c, false);
|
||||
s = c.createStatement();
|
||||
s.executeQuery("SHUTDOWN");
|
||||
s.executeQuery("SHUTDOWN COMPACT");
|
||||
s.close();
|
||||
c.close();
|
||||
} catch (SQLException e) {
|
||||
@@ -106,7 +109,7 @@ class HyperSqlDatabase extends JdbcDatabase {
|
||||
Connection c = null;
|
||||
Statement s = null;
|
||||
try {
|
||||
super.closeAllConnections();
|
||||
closeAllConnections();
|
||||
c = createConnection();
|
||||
s = c.createStatement();
|
||||
s.executeQuery("SHUTDOWN COMPACT");
|
||||
|
||||
@@ -51,7 +51,6 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -70,12 +69,14 @@ import static java.sql.Types.BOOLEAN;
|
||||
import static java.sql.Types.INTEGER;
|
||||
import static java.sql.Types.VARCHAR;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.logging.Level.FINE;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
|
||||
import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
|
||||
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
|
||||
import static org.briarproject.bramble.api.record.Record.RECORD_HEADER_BYTES;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
||||
@@ -85,8 +86,6 @@ import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING;
|
||||
import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN;
|
||||
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
|
||||
import static org.briarproject.bramble.db.DatabaseConstants.DIRTY_KEY;
|
||||
import static org.briarproject.bramble.db.DatabaseConstants.LAST_COMPACTED_KEY;
|
||||
import static org.briarproject.bramble.db.DatabaseConstants.MAX_COMPACTION_INTERVAL_MS;
|
||||
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
|
||||
import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
|
||||
import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
|
||||
@@ -102,7 +101,12 @@ import static org.briarproject.bramble.util.LogUtils.now;
|
||||
abstract class JdbcDatabase implements Database<Connection> {
|
||||
|
||||
// Package access for testing
|
||||
static final int CODE_SCHEMA_VERSION = 49;
|
||||
static final int CODE_SCHEMA_VERSION = 50;
|
||||
|
||||
/**
|
||||
* The maximum number of idle connections to keep open.
|
||||
*/
|
||||
private static final int MAX_CONNECTION_POOL_SIZE = 1;
|
||||
|
||||
// Time period offsets for incoming transport keys
|
||||
private static final int OFFSET_PREV = -1;
|
||||
@@ -252,7 +256,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
+ " requested BOOLEAN NOT NULL,"
|
||||
+ " expiry BIGINT NOT NULL,"
|
||||
+ " txCount INT NOT NULL,"
|
||||
+ " eta BIGINT NOT NULL,"
|
||||
+ " maxLatency BIGINT," // Null if latency was reset
|
||||
+ " PRIMARY KEY (messageId, contactId),"
|
||||
+ " FOREIGN KEY (messageId)"
|
||||
+ " REFERENCES messages (messageId)"
|
||||
@@ -365,7 +369,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
private final Condition connectionsChanged = connectionsLock.newCondition();
|
||||
|
||||
@GuardedBy("connectionsLock")
|
||||
private final LinkedList<Connection> connections = new LinkedList<>();
|
||||
private final LinkedList<Connection> connectionPool = new LinkedList<>();
|
||||
|
||||
@GuardedBy("connectionsLock")
|
||||
private int openConnections = 0;
|
||||
@@ -378,8 +382,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
throws DbException, SQLException;
|
||||
|
||||
// Used exclusively during open to compact the database after schema
|
||||
// migrations or after DatabaseConstants#MAX_COMPACTION_INTERVAL_MS has
|
||||
// elapsed
|
||||
// migrations or if the database was not shut down cleanly
|
||||
protected abstract void compactAndClose() throws DbException;
|
||||
|
||||
JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory,
|
||||
@@ -402,10 +405,12 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
boolean compact;
|
||||
Connection txn = startTransaction();
|
||||
try {
|
||||
if (LOG.isLoggable(INFO)) logSchema(txn);
|
||||
if (reopen) {
|
||||
Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
|
||||
wasDirtyOnInitialisation = isDirty(s);
|
||||
compact = migrateSchema(txn, s, listener) || isCompactionDue(s);
|
||||
boolean migrated = migrateSchema(txn, s, listener);
|
||||
compact = wasDirtyOnInitialisation || migrated;
|
||||
} else {
|
||||
wasDirtyOnInitialisation = false;
|
||||
createTables(txn);
|
||||
@@ -435,14 +440,6 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
} finally {
|
||||
connectionsLock.unlock();
|
||||
}
|
||||
txn = startTransaction();
|
||||
try {
|
||||
storeLastCompacted(txn);
|
||||
commitTransaction(txn);
|
||||
} catch (DbException e) {
|
||||
abortTransaction(txn);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,6 +448,24 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
return wasDirtyOnInitialisation;
|
||||
}
|
||||
|
||||
private void logSchema(Connection txn) throws DbException {
|
||||
Statement s = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
s = txn.createStatement();
|
||||
rs = s.executeQuery(dbTypes.getSchemaQuery());
|
||||
StringBuilder sb = new StringBuilder("Tables:");
|
||||
while (rs.next()) sb.append('\n').append(rs.getString(1));
|
||||
LOG.info(sb.toString());
|
||||
rs.close();
|
||||
s.close();
|
||||
} catch (SQLException e) {
|
||||
tryToClose(rs, LOG, WARNING);
|
||||
tryToClose(s, LOG, WARNING);
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the schema version stored in the database with the schema
|
||||
* version used by the current code and applies any suitable migrations to
|
||||
@@ -502,18 +517,11 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
new Migration45_46(),
|
||||
new Migration46_47(dbTypes),
|
||||
new Migration47_48(),
|
||||
new Migration48_49()
|
||||
new Migration48_49(),
|
||||
new Migration49_50()
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isCompactionDue(Settings s) {
|
||||
long lastCompacted = s.getLong(LAST_COMPACTED_KEY, 0);
|
||||
long elapsed = clock.currentTimeMillis() - lastCompacted;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(elapsed + " ms since last compaction");
|
||||
return elapsed > MAX_COMPACTION_INTERVAL_MS;
|
||||
}
|
||||
|
||||
private void storeSchemaVersion(Connection txn, int version)
|
||||
throws DbException {
|
||||
Settings s = new Settings();
|
||||
@@ -521,12 +529,6 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
|
||||
}
|
||||
|
||||
private void storeLastCompacted(Connection txn) throws DbException {
|
||||
Settings s = new Settings();
|
||||
s.putLong(LAST_COMPACTED_KEY, clock.currentTimeMillis());
|
||||
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
|
||||
}
|
||||
|
||||
private boolean isDirty(Settings s) {
|
||||
return s.getBoolean(DIRTY_KEY, false);
|
||||
}
|
||||
@@ -540,7 +542,6 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
private void initialiseSettings(Connection txn) throws DbException {
|
||||
Settings s = new Settings();
|
||||
s.putInt(SCHEMA_VERSION_KEY, CODE_SCHEMA_VERSION);
|
||||
s.putLong(LAST_COMPACTED_KEY, clock.currentTimeMillis());
|
||||
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
|
||||
}
|
||||
|
||||
@@ -595,7 +596,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
connectionsLock.lock();
|
||||
try {
|
||||
if (closed) throw new DbClosedException();
|
||||
txn = connections.poll();
|
||||
txn = connectionPool.poll();
|
||||
logConnectionCounts();
|
||||
} finally {
|
||||
connectionsLock.unlock();
|
||||
}
|
||||
@@ -606,7 +608,14 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
txn.setAutoCommit(false);
|
||||
connectionsLock.lock();
|
||||
try {
|
||||
// The DB may have been closed since the check above
|
||||
if (closed) {
|
||||
tryToClose(txn, LOG, WARNING);
|
||||
throw new DbClosedException();
|
||||
}
|
||||
openConnections++;
|
||||
logConnectionCounts();
|
||||
connectionsChanged.signalAll();
|
||||
} finally {
|
||||
connectionsLock.unlock();
|
||||
}
|
||||
@@ -617,67 +626,91 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
return txn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void abortTransaction(Connection txn) {
|
||||
try {
|
||||
txn.rollback();
|
||||
connectionsLock.lock();
|
||||
try {
|
||||
connections.add(txn);
|
||||
connectionsChanged.signalAll();
|
||||
} finally {
|
||||
connectionsLock.unlock();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
// Try to close the connection
|
||||
logException(LOG, WARNING, e);
|
||||
tryToClose(txn, LOG, WARNING);
|
||||
// Whatever happens, allow the database to close
|
||||
connectionsLock.lock();
|
||||
try {
|
||||
openConnections--;
|
||||
connectionsChanged.signalAll();
|
||||
} finally {
|
||||
connectionsLock.unlock();
|
||||
}
|
||||
@GuardedBy("connectionsLock")
|
||||
private void logConnectionCounts() {
|
||||
if (LOG.isLoggable(FINE)) {
|
||||
LOG.fine(openConnections + " connections open, "
|
||||
+ connectionPool.size() + " in pool");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commitTransaction(Connection txn) throws DbException {
|
||||
public void abortTransaction(Connection txn) {
|
||||
// The transaction may have been aborted due to an earlier exception,
|
||||
// so close the connection rather than returning it to the pool
|
||||
try {
|
||||
txn.commit();
|
||||
txn.rollback();
|
||||
} catch (SQLException e) {
|
||||
throw new DbException(e);
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
closeConnection(txn);
|
||||
}
|
||||
|
||||
private void closeConnection(Connection txn) {
|
||||
tryToClose(txn, LOG, WARNING);
|
||||
connectionsLock.lock();
|
||||
try {
|
||||
connections.add(txn);
|
||||
openConnections--;
|
||||
logConnectionCounts();
|
||||
connectionsChanged.signalAll();
|
||||
} finally {
|
||||
connectionsLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void closeAllConnections() throws SQLException {
|
||||
@Override
|
||||
public void commitTransaction(Connection txn) throws DbException {
|
||||
// If the transaction commits successfully then return the connection
|
||||
// to the pool, otherwise close it
|
||||
try {
|
||||
txn.commit();
|
||||
returnConnectionToPool(txn);
|
||||
} catch (SQLException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
closeConnection(txn);
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void returnConnectionToPool(Connection txn) {
|
||||
boolean shouldClose;
|
||||
connectionsLock.lock();
|
||||
try {
|
||||
shouldClose = connectionPool.size() >= MAX_CONNECTION_POOL_SIZE;
|
||||
if (shouldClose) openConnections--;
|
||||
else connectionPool.add(txn);
|
||||
logConnectionCounts();
|
||||
connectionsChanged.signalAll();
|
||||
} finally {
|
||||
connectionsLock.unlock();
|
||||
}
|
||||
if (shouldClose) tryToClose(txn, LOG, WARNING);
|
||||
}
|
||||
|
||||
void closeAllConnections() {
|
||||
boolean interrupted = false;
|
||||
connectionsLock.lock();
|
||||
try {
|
||||
closed = true;
|
||||
for (Connection c : connections) c.close();
|
||||
openConnections -= connections.size();
|
||||
connections.clear();
|
||||
for (Connection c : connectionPool) tryToClose(c, LOG, WARNING);
|
||||
openConnections -= connectionPool.size();
|
||||
connectionPool.clear();
|
||||
while (openConnections > 0) {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Waiting for " + openConnections
|
||||
+ " connections to be closed");
|
||||
}
|
||||
try {
|
||||
connectionsChanged.await();
|
||||
} catch (InterruptedException e) {
|
||||
LOG.warning("Interrupted while closing connections");
|
||||
interrupted = true;
|
||||
}
|
||||
for (Connection c : connections) c.close();
|
||||
openConnections -= connections.size();
|
||||
connections.clear();
|
||||
for (Connection c : connectionPool) tryToClose(c, LOG, WARNING);
|
||||
openConnections -= connectionPool.size();
|
||||
connectionPool.clear();
|
||||
}
|
||||
LOG.info("All connections closed");
|
||||
} finally {
|
||||
connectionsLock.unlock();
|
||||
}
|
||||
@@ -920,9 +953,10 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
try {
|
||||
String sql = "INSERT INTO statuses (messageId, contactId, groupId,"
|
||||
+ " timestamp, length, state, groupShared, messageShared,"
|
||||
+ " deleted, ack, seen, requested, expiry, txCount, eta)"
|
||||
+ " deleted, ack, seen, requested, expiry, txCount,"
|
||||
+ " maxLatency)"
|
||||
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE, 0, 0,"
|
||||
+ " 0)";
|
||||
+ " NULL)";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setBytes(1, m.getBytes());
|
||||
ps.setInt(2, c.getInt());
|
||||
@@ -1156,17 +1190,17 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
} else {
|
||||
long now = clock.currentTimeMillis();
|
||||
long eta = now + maxLatency;
|
||||
sql = "SELECT NULL FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE"
|
||||
+ " AND (expiry <= ? OR eta > ?)";
|
||||
+ " 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, eta);
|
||||
ps.setLong(4, maxLatency);
|
||||
}
|
||||
rs = ps.executeQuery();
|
||||
boolean messagesToSend = rs.next();
|
||||
@@ -1902,6 +1936,31 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMessageLength(Connection txn, MessageId m)
|
||||
throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT length from messages"
|
||||
+ " WHERE messageId = ? AND state = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setBytes(1, m.getBytes());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
rs = ps.executeQuery();
|
||||
if (!rs.next()) throw new DbStateException();
|
||||
int length = rs.getInt(1);
|
||||
if (rs.next()) throw new DbStateException();
|
||||
rs.close();
|
||||
ps.close();
|
||||
return length;
|
||||
} catch (SQLException e) {
|
||||
tryToClose(rs, LOG, WARNING);
|
||||
tryToClose(ps, LOG, WARNING);
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<MessageId, Metadata> getMessageMetadata(Connection txn,
|
||||
GroupId g) throws DbException {
|
||||
@@ -2194,7 +2253,6 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
public Collection<MessageId> getMessagesToOffer(Connection txn,
|
||||
ContactId c, int maxMessages, long maxLatency) throws DbException {
|
||||
long now = clock.currentTimeMillis();
|
||||
long eta = now + maxLatency;
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
@@ -2203,13 +2261,14 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE"
|
||||
+ " AND seen = FALSE AND requested = FALSE"
|
||||
+ " AND (expiry <= ? OR eta > ?)"
|
||||
+ " AND (expiry <= ? OR maxLatency IS NULL"
|
||||
+ " OR ? < maxLatency)"
|
||||
+ " ORDER BY timestamp LIMIT ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
ps.setLong(3, now);
|
||||
ps.setLong(4, eta);
|
||||
ps.setLong(4, maxLatency);
|
||||
ps.setInt(5, maxMessages);
|
||||
rs = ps.executeQuery();
|
||||
List<MessageId> ids = new ArrayList<>();
|
||||
@@ -2250,10 +2309,9 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MessageId> getMessagesToSend(Connection txn, ContactId c,
|
||||
int maxLength, long maxLatency) throws DbException {
|
||||
public Collection<MessageId> getMessagesToSend(Connection txn,
|
||||
ContactId c, long capacity, long maxLatency) throws DbException {
|
||||
long now = clock.currentTimeMillis();
|
||||
long eta = now + maxLatency;
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
@@ -2262,21 +2320,21 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE"
|
||||
+ " AND seen = FALSE"
|
||||
+ " AND (expiry <= ? OR eta > ?)"
|
||||
+ " AND (expiry <= ? OR maxLatency IS NULL"
|
||||
+ " OR ? < maxLatency)"
|
||||
+ " ORDER BY timestamp";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
ps.setLong(3, now);
|
||||
ps.setLong(4, eta);
|
||||
ps.setLong(4, maxLatency);
|
||||
rs = ps.executeQuery();
|
||||
List<MessageId> ids = new ArrayList<>();
|
||||
int total = 0;
|
||||
while (rs.next()) {
|
||||
int length = rs.getInt(1);
|
||||
if (total + length > maxLength) break;
|
||||
if (capacity < RECORD_HEADER_BYTES + length) break;
|
||||
ids.add(new MessageId(rs.getBytes(2)));
|
||||
total += length;
|
||||
capacity -= RECORD_HEADER_BYTES + length;
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
@@ -2289,12 +2347,12 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<MessageId, Integer> getUnackedMessagesToSend(Connection txn,
|
||||
public Collection<MessageId> getUnackedMessagesToSend(Connection txn,
|
||||
ContactId c) throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT length, messageId FROM statuses"
|
||||
String sql = "SELECT messageId FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE"
|
||||
@@ -2303,15 +2361,11 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
rs = ps.executeQuery();
|
||||
Map<MessageId, Integer> results = new LinkedHashMap<>();
|
||||
while (rs.next()) {
|
||||
int length = rs.getInt(1);
|
||||
MessageId id = new MessageId(rs.getBytes(2));
|
||||
results.put(id, length);
|
||||
}
|
||||
List<MessageId> ids = new ArrayList<>();
|
||||
while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
|
||||
rs.close();
|
||||
ps.close();
|
||||
return results;
|
||||
return ids;
|
||||
} catch (SQLException e) {
|
||||
tryToClose(rs, LOG, WARNING);
|
||||
tryToClose(ps, LOG, WARNING);
|
||||
@@ -2424,6 +2478,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
MessageId m = new MessageId(rs.getBytes(1));
|
||||
GroupId g = new GroupId(rs.getBytes(2));
|
||||
Collection<MessageId> messageIds = ids.get(g);
|
||||
//noinspection Java8MapApi
|
||||
if (messageIds == null) {
|
||||
messageIds = new ArrayList<>();
|
||||
ids.put(g, messageIds);
|
||||
@@ -2550,9 +2605,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
|
||||
@Override
|
||||
public Collection<MessageId> getRequestedMessagesToSend(Connection txn,
|
||||
ContactId c, int maxLength, long maxLatency) throws DbException {
|
||||
ContactId c, long capacity, long maxLatency) throws DbException {
|
||||
long now = clock.currentTimeMillis();
|
||||
long eta = now + maxLatency;
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
@@ -2561,21 +2615,21 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE"
|
||||
+ " AND seen = FALSE AND requested = TRUE"
|
||||
+ " AND (expiry <= ? OR eta > ?)"
|
||||
+ " AND (expiry <= ? OR maxLatency IS NULL"
|
||||
+ " OR ? < maxLatency)"
|
||||
+ " ORDER BY timestamp";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
ps.setLong(3, now);
|
||||
ps.setLong(4, eta);
|
||||
ps.setLong(4, maxLatency);
|
||||
rs = ps.executeQuery();
|
||||
List<MessageId> ids = new ArrayList<>();
|
||||
int total = 0;
|
||||
while (rs.next()) {
|
||||
int length = rs.getInt(1);
|
||||
if (total + length > maxLength) break;
|
||||
if (capacity < RECORD_HEADER_BYTES + length) break;
|
||||
ids.add(new MessageId(rs.getBytes(2)));
|
||||
total += length;
|
||||
capacity -= RECORD_HEADER_BYTES + length;
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
@@ -2729,6 +2783,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
ContactId c = new ContactId(rs.getInt(1));
|
||||
TransportId t = new TransportId(rs.getString(2));
|
||||
Collection<TransportId> transportIds = ids.get(c);
|
||||
//noinspection Java8MapApi
|
||||
if (transportIds == null) {
|
||||
transportIds = new ArrayList<>();
|
||||
ids.put(c, transportIds);
|
||||
@@ -3298,7 +3353,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
try {
|
||||
String sql = "UPDATE statuses SET expiry = 0, txCount = 0, eta = 0"
|
||||
String sql = "UPDATE statuses SET expiry = 0, txCount = 0,"
|
||||
+ " maxLatency = NULL"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE";
|
||||
@@ -3643,8 +3699,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateExpiryTimeAndEta(Connection txn, ContactId c, MessageId m,
|
||||
long maxLatency) throws DbException {
|
||||
public void updateRetransmissionData(Connection txn, ContactId c,
|
||||
MessageId m, long maxLatency) throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
@@ -3660,13 +3716,12 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
rs.close();
|
||||
ps.close();
|
||||
sql = "UPDATE statuses"
|
||||
+ " SET expiry = ?, txCount = txCount + 1, eta = ?"
|
||||
+ " SET expiry = ?, txCount = txCount + 1, maxLatency = ?"
|
||||
+ " WHERE messageId = ? AND contactId = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
long now = clock.currentTimeMillis();
|
||||
long eta = now + maxLatency;
|
||||
ps.setLong(1, calculateExpiry(now, maxLatency, txCount));
|
||||
ps.setLong(2, eta);
|
||||
ps.setLong(2, maxLatency);
|
||||
ps.setBytes(3, m.getBytes());
|
||||
ps.setInt(4, c.getInt());
|
||||
int affected = ps.executeUpdate();
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.briarproject.bramble.db;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
|
||||
|
||||
class Migration49_50 implements Migration<Connection> {
|
||||
|
||||
private static final Logger LOG = getLogger(Migration49_50.class.getName());
|
||||
|
||||
@Override
|
||||
public int getStartVersion() {
|
||||
return 49;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEndVersion() {
|
||||
return 50;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void migrate(Connection txn) throws DbException {
|
||||
Statement s = null;
|
||||
try {
|
||||
s = txn.createStatement();
|
||||
s.execute("ALTER TABLE statuses"
|
||||
+ " ALTER COLUMN eta"
|
||||
+ " RENAME TO maxLatency");
|
||||
s.execute("ALTER TABLE statuses"
|
||||
+ " ALTER COLUMN maxLatency"
|
||||
+ " SET NULL");
|
||||
s.execute("UPDATE statuses SET maxLatency = NULL");
|
||||
} catch (SQLException e) {
|
||||
tryToClose(s, LOG, WARNING);
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.briarproject.bramble.io;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.io.TimeoutMonitor;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler.Cancellable;
|
||||
import org.briarproject.bramble.api.system.Wakeful;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -190,6 +190,10 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (state == STOPPING) {
|
||||
LOG.info("Already stopped");
|
||||
return;
|
||||
}
|
||||
LOG.info("Stopping services");
|
||||
state = STOPPING;
|
||||
eventBus.broadcast(new LifecycleEvent(STOPPING));
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
|
||||
/**
|
||||
* An interface for calling an API endpoint with the option to retry the call.
|
||||
*/
|
||||
interface ApiCall {
|
||||
|
||||
/**
|
||||
* This method makes a synchronous call to an API endpoint and returns
|
||||
* true if the call should be retried, in which case the method may be
|
||||
* called again on the same {@link ApiCall} instance after a delay.
|
||||
*
|
||||
* @return True if the API call needs to be retried, or false if the API
|
||||
* call succeeded or {@link TolerableFailureException failed tolerably}.
|
||||
*/
|
||||
@IoExecutor
|
||||
boolean callApi();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* An interface for checking whether a mailbox is reachable.
|
||||
*/
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
interface ConnectivityChecker {
|
||||
|
||||
/**
|
||||
* Destroys the checker. Any current connectivity check is cancelled.
|
||||
*/
|
||||
void destroy();
|
||||
|
||||
/**
|
||||
* Starts a connectivity check if needed and calls the given observer when
|
||||
* 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.
|
||||
*/
|
||||
void checkConnectivity(MailboxProperties properties,
|
||||
ConnectivityObserver o);
|
||||
|
||||
interface ConnectivityObserver {
|
||||
void onConnectivityCheckSucceeded();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
abstract class ConnectivityCheckerImpl implements ConnectivityChecker {
|
||||
|
||||
/**
|
||||
* If no more than this much time has elapsed since the last connectivity
|
||||
* check succeeded, consider the result to be fresh and don't check again.
|
||||
* <p>
|
||||
* Package access for testing.
|
||||
*/
|
||||
static final long CONNECTIVITY_CHECK_FRESHNESS_MS = 10_000;
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
protected final Clock clock;
|
||||
private final MailboxApiCaller mailboxApiCaller;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private boolean destroyed = false;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private Cancellable connectivityCheck = null;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private long lastConnectivityCheckSucceeded = 0;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private final List<ConnectivityObserver> connectivityObservers =
|
||||
new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Creates an {@link ApiCall} for checking whether the mailbox is
|
||||
* reachable. The {@link ApiCall} should call
|
||||
* {@link #onConnectivityCheckSucceeded(long)} if the check succeeds.
|
||||
*/
|
||||
abstract ApiCall createConnectivityCheckTask(MailboxProperties properties);
|
||||
|
||||
ConnectivityCheckerImpl(Clock clock, MailboxApiCaller mailboxApiCaller) {
|
||||
this.clock = clock;
|
||||
this.mailboxApiCaller = mailboxApiCaller;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
synchronized (lock) {
|
||||
destroyed = true;
|
||||
connectivityObservers.clear();
|
||||
if (connectivityCheck != null) {
|
||||
connectivityCheck.cancel();
|
||||
connectivityCheck = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkConnectivity(MailboxProperties properties,
|
||||
ConnectivityObserver o) {
|
||||
boolean callNow = false;
|
||||
synchronized (lock) {
|
||||
if (destroyed) return;
|
||||
if (connectivityCheck == null) {
|
||||
// No connectivity check is running
|
||||
long now = clock.currentTimeMillis();
|
||||
if (now - lastConnectivityCheckSucceeded
|
||||
> CONNECTIVITY_CHECK_FRESHNESS_MS) {
|
||||
// The last connectivity check is stale, start a new one
|
||||
connectivityObservers.add(o);
|
||||
ApiCall task =
|
||||
createConnectivityCheckTask(properties);
|
||||
connectivityCheck = mailboxApiCaller.retryWithBackoff(task);
|
||||
} else {
|
||||
// The last connectivity check is fresh
|
||||
callNow = true;
|
||||
}
|
||||
} else {
|
||||
// A connectivity check is running, wait for it to succeed
|
||||
connectivityObservers.add(o);
|
||||
}
|
||||
}
|
||||
if (callNow) o.onConnectivityCheckSucceeded();
|
||||
}
|
||||
|
||||
protected void onConnectivityCheckSucceeded(long now) {
|
||||
List<ConnectivityObserver> observers;
|
||||
synchronized (lock) {
|
||||
if (destroyed) return;
|
||||
connectivityCheck = null;
|
||||
lastConnectivityCheckSucceeded = now;
|
||||
observers = new ArrayList<>(connectivityObservers);
|
||||
connectivityObservers.clear();
|
||||
}
|
||||
for (ConnectivityObserver o : observers) {
|
||||
o.onConnectivityCheckSucceeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
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
|
||||
@NotNullByDefault
|
||||
class ContactMailboxConnectivityChecker extends ConnectivityCheckerImpl {
|
||||
|
||||
private final MailboxApi mailboxApi;
|
||||
|
||||
ContactMailboxConnectivityChecker(Clock clock,
|
||||
MailboxApiCaller mailboxApiCaller, MailboxApi mailboxApi) {
|
||||
super(clock, mailboxApiCaller);
|
||||
this.mailboxApi = mailboxApi;
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
|
||||
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.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -16,8 +18,12 @@ import java.util.List;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@NotNullByDefault
|
||||
interface MailboxApi {
|
||||
|
||||
List<MailboxVersion> getServerSupports(MailboxProperties properties)
|
||||
throws IOException, ApiException;
|
||||
|
||||
/**
|
||||
* Sets up the mailbox with the setup token.
|
||||
*
|
||||
@@ -25,7 +31,7 @@ interface MailboxApi {
|
||||
* @return the owner token
|
||||
* @throws ApiException for 401 response.
|
||||
*/
|
||||
MailboxAuthToken setup(MailboxProperties properties)
|
||||
MailboxProperties setup(MailboxProperties properties)
|
||||
throws IOException, ApiException;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.DAYS;
|
||||
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||
|
||||
@NotNullByDefault
|
||||
interface MailboxApiCaller {
|
||||
|
||||
/**
|
||||
* The minimum interval between retries in milliseconds.
|
||||
*/
|
||||
long MIN_RETRY_INTERVAL_MS = MINUTES.toMillis(1);
|
||||
|
||||
/**
|
||||
* The maximum interval between retries in milliseconds.
|
||||
*/
|
||||
long MAX_RETRY_INTERVAL_MS = DAYS.toMillis(1);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return A {@link Cancellable} that can be used to cancel any future
|
||||
* retries.
|
||||
*/
|
||||
Cancellable retryWithBackoff(ApiCall apiCall);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class MailboxApiCallerImpl implements MailboxApiCaller {
|
||||
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final Executor ioExecutor;
|
||||
|
||||
@Inject
|
||||
MailboxApiCallerImpl(TaskScheduler taskScheduler,
|
||||
@IoExecutor Executor ioExecutor) {
|
||||
this.taskScheduler = taskScheduler;
|
||||
this.ioExecutor = ioExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cancellable retryWithBackoff(ApiCall apiCall) {
|
||||
Task task = new Task(apiCall);
|
||||
task.start();
|
||||
return task;
|
||||
}
|
||||
|
||||
private class Task implements Cancellable {
|
||||
|
||||
private final ApiCall apiCall;
|
||||
private final Object lock = new Object();
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private Cancellable scheduledTask = null;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private boolean cancelled = false;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private long retryIntervalMs = MIN_RETRY_INTERVAL_MS;
|
||||
|
||||
private Task(ApiCall apiCall) {
|
||||
this.apiCall = apiCall;
|
||||
}
|
||||
|
||||
private void start() {
|
||||
synchronized (lock) {
|
||||
if (cancelled) throw new AssertionError();
|
||||
ioExecutor.execute(this::callApi);
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void callApi() {
|
||||
synchronized (lock) {
|
||||
if (cancelled) return;
|
||||
}
|
||||
// The call returns true if we should retry
|
||||
if (apiCall.callApi()) {
|
||||
synchronized (lock) {
|
||||
if (cancelled) return;
|
||||
scheduledTask = taskScheduler.schedule(this::callApi,
|
||||
ioExecutor, retryIntervalMs, MILLISECONDS);
|
||||
// Increase the retry interval each time we retry
|
||||
retryIntervalMs =
|
||||
min(MAX_RETRY_INTERVAL_MS, retryIntervalMs * 2);
|
||||
}
|
||||
} else {
|
||||
synchronized (lock) {
|
||||
scheduledTask = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
Cancellable scheduledTask;
|
||||
synchronized (lock) {
|
||||
cancelled = true;
|
||||
scheduledTask = this.scheduledTask;
|
||||
this.scheduledTask = null;
|
||||
}
|
||||
if (scheduledTask != null) scheduledTask.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import org.briarproject.bramble.api.mailbox.MailboxFileId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.io.File;
|
||||
@@ -56,7 +57,24 @@ class MailboxApiImpl implements MailboxApi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxAuthToken setup(MailboxProperties properties)
|
||||
public List<MailboxVersion> getServerSupports(MailboxProperties properties)
|
||||
throws IOException, ApiException {
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
Response response = sendGetRequest(properties, "/versions");
|
||||
if (response.code() != 200) throw new ApiException();
|
||||
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) throw new ApiException();
|
||||
try {
|
||||
JsonNode node = mapper.readTree(body.string());
|
||||
return parseServerSupports(node);
|
||||
} catch (JacksonException e) {
|
||||
throw new ApiException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxProperties setup(MailboxProperties properties)
|
||||
throws IOException, ApiException {
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
Request request = getRequestBuilder(properties.getAuthToken())
|
||||
@@ -75,17 +93,40 @@ class MailboxApiImpl implements MailboxApi {
|
||||
if (tokenNode == null) {
|
||||
throw new ApiException();
|
||||
}
|
||||
String ownerToken = tokenNode.textValue();
|
||||
return MailboxAuthToken.fromString(ownerToken);
|
||||
return new MailboxProperties(properties.getBaseUrl(),
|
||||
MailboxAuthToken.fromString(tokenNode.textValue()),
|
||||
parseServerSupports(node));
|
||||
} catch (JacksonException | InvalidMailboxIdException e) {
|
||||
throw new ApiException();
|
||||
}
|
||||
}
|
||||
|
||||
private List<MailboxVersion> parseServerSupports(JsonNode node)
|
||||
throws ApiException {
|
||||
List<MailboxVersion> serverSupports = new ArrayList<>();
|
||||
ArrayNode serverSupportsNode = getArray(node, "serverSupports");
|
||||
for (JsonNode versionNode : serverSupportsNode) {
|
||||
if (!versionNode.isObject()) throw new ApiException();
|
||||
ObjectNode objectNode = (ObjectNode) versionNode;
|
||||
JsonNode majorNode = objectNode.get("major");
|
||||
JsonNode minorNode = objectNode.get("minor");
|
||||
if (majorNode == null || !majorNode.isNumber()) {
|
||||
throw new ApiException();
|
||||
}
|
||||
if (minorNode == null || !minorNode.isNumber()) {
|
||||
throw new ApiException();
|
||||
}
|
||||
int major = majorNode.asInt();
|
||||
int minor = minorNode.asInt();
|
||||
if (major < 0 || minor < 0) throw new ApiException();
|
||||
serverSupports.add(new MailboxVersion(major, minor));
|
||||
}
|
||||
return serverSupports;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkStatus(MailboxProperties properties)
|
||||
throws IOException, ApiException {
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
Response response = sendGetRequest(properties, "/status");
|
||||
if (response.code() == 401) throw new ApiException();
|
||||
return response.isSuccessful();
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
interface MailboxFileManager {
|
||||
|
||||
/**
|
||||
* Creates an empty file for storing a download.
|
||||
*/
|
||||
File createTempFileForDownload() throws IOException;
|
||||
|
||||
/**
|
||||
* Handles a file that has been downloaded. The file should be created
|
||||
* with {@link #createTempFileForDownload()}.
|
||||
*/
|
||||
void handleDownloadedFile(File f);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.connection.ConnectionManager;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
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.event.TransportActiveEvent;
|
||||
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
|
||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
|
||||
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.LogUtils.logException;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class MailboxFileManagerImpl implements MailboxFileManager, EventListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(MailboxFileManagerImpl.class.getName());
|
||||
|
||||
// Package access for testing
|
||||
static final String DOWNLOAD_DIR_NAME = "downloads";
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final PluginManager pluginManager;
|
||||
private final ConnectionManager connectionManager;
|
||||
private final LifecycleManager lifecycleManager;
|
||||
private final File mailboxDir;
|
||||
private final EventBus eventBus;
|
||||
private final CountDownLatch orphanLatch = new CountDownLatch(1);
|
||||
|
||||
@Inject
|
||||
MailboxFileManagerImpl(@IoExecutor Executor ioExecutor,
|
||||
PluginManager pluginManager,
|
||||
ConnectionManager connectionManager,
|
||||
LifecycleManager lifecycleManager,
|
||||
@MailboxDirectory File mailboxDir,
|
||||
EventBus eventBus) {
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.pluginManager = pluginManager;
|
||||
this.connectionManager = connectionManager;
|
||||
this.lifecycleManager = lifecycleManager;
|
||||
this.mailboxDir = mailboxDir;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File createTempFileForDownload() 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);
|
||||
}
|
||||
|
||||
private File createDirectoryIfNeeded(String name) throws IOException {
|
||||
File dir = new File(mailboxDir, name);
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
dir.mkdirs();
|
||||
if (!dir.isDirectory()) {
|
||||
throw new IOException("Failed to create directory '" + name + "'");
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleDownloadedFile(File f) {
|
||||
// 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());
|
||||
TransportConnectionReader reader = plugin.createReader(p);
|
||||
if (reader == null) {
|
||||
LOG.warning("Failed to create reader for downloaded file");
|
||||
return;
|
||||
}
|
||||
TransportConnectionReader decorated = new MailboxFileReader(reader, f);
|
||||
LOG.info("Reading downloaded file");
|
||||
connectionManager.manageIncomingConnection(ID, decorated,
|
||||
exception -> isHandlingComplete(exception, true));
|
||||
}
|
||||
|
||||
private boolean isHandlingComplete(boolean exception, boolean recognised) {
|
||||
// If we've successfully read the file then we're done
|
||||
if (!exception && recognised) return true;
|
||||
// If the app is shutting down we may get spurious IO exceptions
|
||||
// due to executors being shut down. Leave the file in the download
|
||||
// directory and we'll try to read it again at the next startup
|
||||
return !lifecycleManager.getLifecycleState().isAfter(RUNNING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof TransportActiveEvent) {
|
||||
TransportActiveEvent t = (TransportActiveEvent) e;
|
||||
if (t.getTransportId().equals(ID)) {
|
||||
ioExecutor.execute(this::handleOrphanedFiles);
|
||||
eventBus.removeListener(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@IoExecutor
|
||||
private void handleOrphanedFiles() {
|
||||
try {
|
||||
File downloadDir = createDirectoryIfNeeded(DOWNLOAD_DIR_NAME);
|
||||
File[] orphans = downloadDir.listFiles();
|
||||
// Now that we've got the list of orphans, new files can be created
|
||||
orphanLatch.countDown();
|
||||
if (orphans != null) for (File f : orphans) handleDownloadedFile(f);
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
}
|
||||
|
||||
private class MailboxFileReader implements TransportConnectionReader {
|
||||
|
||||
private final TransportConnectionReader delegate;
|
||||
private final File file;
|
||||
|
||||
private MailboxFileReader(TransportConnectionReader delegate,
|
||||
File file) {
|
||||
this.delegate = delegate;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return delegate.getInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(boolean exception, boolean recognised)
|
||||
throws IOException {
|
||||
delegate.dispose(exception, recognised);
|
||||
if (isHandlingComplete(exception, recognised)) {
|
||||
LOG.info("Deleting downloaded file");
|
||||
if (!file.delete()) {
|
||||
LOG.warning("Failed to delete downloaded file");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,42 @@ 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.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxManager;
|
||||
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.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class MailboxManagerImpl implements MailboxManager {
|
||||
|
||||
private static final String TAG = MailboxManagerImpl.class.getName();
|
||||
private final static Logger LOG = getLogger(TAG);
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final MailboxApi api;
|
||||
private final TransactionManager db;
|
||||
private final MailboxSettingsManager mailboxSettingsManager;
|
||||
private final MailboxPairingTaskFactory pairingTaskFactory;
|
||||
private final Clock clock;
|
||||
private final Object lock = new Object();
|
||||
|
||||
@Nullable
|
||||
@@ -32,11 +47,17 @@ class MailboxManagerImpl implements MailboxManager {
|
||||
@Inject
|
||||
MailboxManagerImpl(
|
||||
@IoExecutor Executor ioExecutor,
|
||||
MailboxApi api,
|
||||
TransactionManager db,
|
||||
MailboxSettingsManager mailboxSettingsManager,
|
||||
MailboxPairingTaskFactory pairingTaskFactory) {
|
||||
MailboxPairingTaskFactory pairingTaskFactory,
|
||||
Clock clock) {
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.api = api;
|
||||
this.db = db;
|
||||
this.mailboxSettingsManager = mailboxSettingsManager;
|
||||
this.pairingTaskFactory = pairingTaskFactory;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,4 +96,61 @@ class MailboxManagerImpl implements MailboxManager {
|
||||
return created;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkConnection() {
|
||||
boolean success;
|
||||
try {
|
||||
MailboxProperties props = db.transactionWithNullableResult(true,
|
||||
mailboxSettingsManager::getOwnMailboxProperties);
|
||||
if (props == null) throw new DbException();
|
||||
success = api.checkStatus(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);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
private void recordCheckResult(boolean success) throws DbException {
|
||||
long now = clock.currentTimeMillis();
|
||||
db.transaction(false, txn -> {
|
||||
if (success) {
|
||||
mailboxSettingsManager.recordSuccessfulConnection(txn, now);
|
||||
} else {
|
||||
mailboxSettingsManager.recordFailedConnectionAttempt(txn, now);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unPair() throws DbException {
|
||||
MailboxProperties properties = db.transactionWithNullableResult(true,
|
||||
mailboxSettingsManager::getOwnMailboxProperties);
|
||||
if (properties == null) {
|
||||
// no more mailbox, that's strange but possible if called in quick
|
||||
// succession, so let's return true this time
|
||||
return true;
|
||||
}
|
||||
boolean wasWiped;
|
||||
try {
|
||||
api.wipeMailbox(properties);
|
||||
wasWiped = true;
|
||||
} catch (IOException | MailboxApi.ApiException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
wasWiped = false;
|
||||
}
|
||||
db.transaction(false,
|
||||
mailboxSettingsManager::removeOwnMailboxProperties);
|
||||
return wasWiped;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.FeatureFlags;
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
import org.briarproject.bramble.api.data.MetadataEncoder;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.sync.validation.ValidationManager;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.versioning.ClientVersioningManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxUpdateManager.CLIENT_ID;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxUpdateManager.MAJOR_VERSION;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxUpdateManager.MINOR_VERSION;
|
||||
|
||||
@Module
|
||||
public class MailboxModule {
|
||||
|
||||
public static class EagerSingletons {
|
||||
@Inject
|
||||
MailboxUpdateValidator mailboxUpdateValidator;
|
||||
@Inject
|
||||
MailboxUpdateManager mailboxUpdateManager;
|
||||
@Inject
|
||||
MailboxFileManager mailboxFileManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MailboxManager providesMailboxManager(MailboxManagerImpl mailboxManager) {
|
||||
@@ -33,4 +61,57 @@ public class MailboxModule {
|
||||
MailboxApi providesMailboxApi(MailboxApiImpl mailboxApi) {
|
||||
return mailboxApi;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MailboxUpdateValidator provideMailboxUpdateValidator(
|
||||
ValidationManager validationManager,
|
||||
ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder,
|
||||
Clock clock,
|
||||
FeatureFlags featureFlags) {
|
||||
MailboxUpdateValidator validator = new MailboxUpdateValidator(
|
||||
clientHelper, metadataEncoder, clock);
|
||||
if (featureFlags.shouldEnableMailbox()) {
|
||||
validationManager.registerMessageValidator(CLIENT_ID,
|
||||
MAJOR_VERSION, validator);
|
||||
}
|
||||
return validator;
|
||||
}
|
||||
|
||||
@Provides
|
||||
List<MailboxVersion> provideClientSupports() {
|
||||
return CLIENT_SUPPORTS;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MailboxUpdateManager provideMailboxUpdateManager(
|
||||
FeatureFlags featureFlags,
|
||||
LifecycleManager lifecycleManager,
|
||||
ValidationManager validationManager, ContactManager contactManager,
|
||||
ClientVersioningManager clientVersioningManager,
|
||||
MailboxSettingsManager mailboxSettingsManager,
|
||||
MailboxUpdateManagerImpl mailboxUpdateManager) {
|
||||
if (featureFlags.shouldEnableMailbox()) {
|
||||
lifecycleManager.registerOpenDatabaseHook(mailboxUpdateManager);
|
||||
validationManager.registerIncomingMessageHook(CLIENT_ID,
|
||||
MAJOR_VERSION, mailboxUpdateManager);
|
||||
contactManager.registerContactHook(mailboxUpdateManager);
|
||||
clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION,
|
||||
MINOR_VERSION, mailboxUpdateManager);
|
||||
mailboxSettingsManager.registerMailboxHook(mailboxUpdateManager);
|
||||
}
|
||||
return mailboxUpdateManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MailboxFileManager provideMailboxFileManager(FeatureFlags featureFlags,
|
||||
EventBus eventBus, MailboxFileManagerImpl mailboxFileManager) {
|
||||
if (featureFlags.shouldEnableMailbox()) {
|
||||
eventBus.addListener(mailboxFileManager);
|
||||
}
|
||||
return mailboxFileManager;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.event.EventExecutor;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxPairingTask;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
@@ -18,31 +19,35 @@ import javax.inject.Inject;
|
||||
class MailboxPairingTaskFactoryImpl implements MailboxPairingTaskFactory {
|
||||
|
||||
private final Executor eventExecutor;
|
||||
private final TransactionManager db;
|
||||
private final DatabaseComponent db;
|
||||
private final CryptoComponent crypto;
|
||||
private final Clock clock;
|
||||
private final MailboxApi api;
|
||||
private final MailboxSettingsManager mailboxSettingsManager;
|
||||
private final MailboxUpdateManager mailboxUpdateManager;
|
||||
|
||||
@Inject
|
||||
MailboxPairingTaskFactoryImpl(
|
||||
@EventExecutor Executor eventExecutor,
|
||||
TransactionManager db,
|
||||
DatabaseComponent db,
|
||||
CryptoComponent crypto,
|
||||
Clock clock,
|
||||
MailboxApi api,
|
||||
MailboxSettingsManager mailboxSettingsManager) {
|
||||
MailboxSettingsManager mailboxSettingsManager,
|
||||
MailboxUpdateManager mailboxUpdateManager) {
|
||||
this.eventExecutor = eventExecutor;
|
||||
this.db = db;
|
||||
this.crypto = crypto;
|
||||
this.clock = clock;
|
||||
this.api = api;
|
||||
this.mailboxSettingsManager = mailboxSettingsManager;
|
||||
this.mailboxUpdateManager = mailboxUpdateManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxPairingTask createPairingTask(String qrCodePayload) {
|
||||
return new MailboxPairingTaskImpl(qrCodePayload, eventExecutor, db,
|
||||
crypto, clock, api, mailboxSettingsManager);
|
||||
crypto, clock, api, mailboxSettingsManager,
|
||||
mailboxUpdateManager);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,18 @@ package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Consumer;
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.event.EventExecutor;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxPairingState;
|
||||
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.MailboxUpdate;
|
||||
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.mailbox.MailboxApi.ApiException;
|
||||
@@ -43,11 +46,12 @@ class MailboxPairingTaskImpl implements MailboxPairingTask {
|
||||
|
||||
private final String payload;
|
||||
private final Executor eventExecutor;
|
||||
private final TransactionManager db;
|
||||
private final DatabaseComponent db;
|
||||
private final CryptoComponent crypto;
|
||||
private final Clock clock;
|
||||
private final MailboxApi api;
|
||||
private final MailboxSettingsManager mailboxSettingsManager;
|
||||
private final MailboxUpdateManager mailboxUpdateManager;
|
||||
|
||||
private final Object lock = new Object();
|
||||
@GuardedBy("lock")
|
||||
@@ -59,11 +63,12 @@ class MailboxPairingTaskImpl implements MailboxPairingTask {
|
||||
MailboxPairingTaskImpl(
|
||||
String payload,
|
||||
@EventExecutor Executor eventExecutor,
|
||||
TransactionManager db,
|
||||
DatabaseComponent db,
|
||||
CryptoComponent crypto,
|
||||
Clock clock,
|
||||
MailboxApi api,
|
||||
MailboxSettingsManager mailboxSettingsManager) {
|
||||
MailboxSettingsManager mailboxSettingsManager,
|
||||
MailboxUpdateManager mailboxUpdateManager) {
|
||||
this.payload = payload;
|
||||
this.eventExecutor = eventExecutor;
|
||||
this.db = db;
|
||||
@@ -71,7 +76,8 @@ class MailboxPairingTaskImpl implements MailboxPairingTask {
|
||||
this.clock = clock;
|
||||
this.api = api;
|
||||
this.mailboxSettingsManager = mailboxSettingsManager;
|
||||
state = new MailboxPairingState.QrCodeReceived(payload);
|
||||
this.mailboxUpdateManager = mailboxUpdateManager;
|
||||
state = new MailboxPairingState.QrCodeReceived();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -100,23 +106,31 @@ class MailboxPairingTaskImpl implements MailboxPairingTask {
|
||||
} catch (MailboxAlreadyPairedException e) {
|
||||
onMailboxError(e, new MailboxPairingState.MailboxAlreadyPaired());
|
||||
} catch (IOException e) {
|
||||
onMailboxError(e, new MailboxPairingState.ConnectionError(payload));
|
||||
onMailboxError(e, new MailboxPairingState.ConnectionError());
|
||||
} catch (ApiException | DbException e) {
|
||||
onMailboxError(e, new MailboxPairingState.UnexpectedError(payload));
|
||||
onMailboxError(e, new MailboxPairingState.UnexpectedError());
|
||||
}
|
||||
}
|
||||
|
||||
private void pairMailbox() throws IOException, ApiException, DbException {
|
||||
MailboxProperties mailboxProperties = decodeQrCodePayload(payload);
|
||||
setState(new MailboxPairingState.Pairing(payload));
|
||||
MailboxAuthToken ownerToken = api.setup(mailboxProperties);
|
||||
MailboxProperties ownerProperties = new MailboxProperties(
|
||||
mailboxProperties.getBaseUrl(), ownerToken, true);
|
||||
setState(new MailboxPairingState.Pairing());
|
||||
MailboxProperties ownerProperties = api.setup(mailboxProperties);
|
||||
long time = clock.currentTimeMillis();
|
||||
db.transaction(false, txn -> {
|
||||
mailboxSettingsManager
|
||||
.setOwnMailboxProperties(txn, ownerProperties);
|
||||
mailboxSettingsManager.recordSuccessfulConnection(txn, time);
|
||||
// A (possibly new) mailbox is paired. Reset message retransmission
|
||||
// timers for contacts who doesn't have their own mailbox. This way,
|
||||
// data stranded on our old mailbox will be re-uploaded to our new.
|
||||
for (Contact c : db.getContacts(txn)) {
|
||||
MailboxUpdate update = mailboxUpdateManager.getRemoteUpdate(
|
||||
txn, c.getId());
|
||||
if (update == null || !update.hasMailbox()) {
|
||||
db.resetUnackedMessagesToSend(txn, c.getId());
|
||||
}
|
||||
}
|
||||
});
|
||||
setState(new MailboxPairingState.Paired());
|
||||
}
|
||||
@@ -162,11 +176,11 @@ class MailboxPairingTaskImpl implements MailboxPairingTask {
|
||||
}
|
||||
LOG.info("QR code is valid");
|
||||
byte[] onionPubKey = Arrays.copyOfRange(bytes, 1, 33);
|
||||
String onionAddress = crypto.encodeOnionAddress(onionPubKey);
|
||||
String baseUrl = "http://" + onionAddress + ".onion";
|
||||
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, true);
|
||||
return new MailboxProperties(baseUrl, setupToken, new ArrayList<>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,14 +8,22 @@ import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
|
||||
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.mailbox.event.MailboxProblemEvent;
|
||||
import org.briarproject.bramble.api.mailbox.event.OwnMailboxConnectionStatusEvent;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.settings.Settings;
|
||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
|
||||
@Immutable
|
||||
@@ -24,20 +32,28 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
|
||||
|
||||
// Package access for testing
|
||||
static final String SETTINGS_NAMESPACE = "mailbox";
|
||||
// TODO: This currently stores the base URL, not the 56-char onion address
|
||||
static final String SETTINGS_KEY_ONION = "onion";
|
||||
static final String SETTINGS_KEY_TOKEN = "token";
|
||||
static final String SETTINGS_KEY_SERVER_SUPPORTS = "serverSupports";
|
||||
static final String SETTINGS_KEY_LAST_ATTEMPT = "lastAttempt";
|
||||
static final String SETTINGS_KEY_LAST_SUCCESS = "lastSuccess";
|
||||
static final String SETTINGS_KEY_ATTEMPTS = "attempts";
|
||||
static final String SETTINGS_UPLOADS_NAMESPACE = "mailbox-uploads";
|
||||
|
||||
private final SettingsManager settingsManager;
|
||||
private final List<MailboxHook> hooks = new CopyOnWriteArrayList<>();
|
||||
|
||||
@Inject
|
||||
MailboxSettingsManagerImpl(SettingsManager settingsManager) {
|
||||
this.settingsManager = settingsManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerMailboxHook(MailboxHook hook) {
|
||||
hooks.add(hook);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxProperties getOwnMailboxProperties(Transaction txn)
|
||||
throws DbException {
|
||||
@@ -45,9 +61,10 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
|
||||
String onion = s.get(SETTINGS_KEY_ONION);
|
||||
String token = s.get(SETTINGS_KEY_TOKEN);
|
||||
if (isNullOrEmpty(onion) || isNullOrEmpty(token)) return null;
|
||||
List<MailboxVersion> serverSupports = parseServerSupports(s);
|
||||
try {
|
||||
MailboxAuthToken tokenId = MailboxAuthToken.fromString(token);
|
||||
return new MailboxProperties(onion, tokenId, true);
|
||||
return new MailboxProperties(onion, tokenId, serverSupports);
|
||||
} catch (InvalidMailboxIdException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
@@ -59,7 +76,29 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
|
||||
Settings s = new Settings();
|
||||
s.put(SETTINGS_KEY_ONION, p.getBaseUrl());
|
||||
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);
|
||||
settingsManager.mergeSettings(txn, s, SETTINGS_NAMESPACE);
|
||||
for (MailboxHook hook : hooks) {
|
||||
hook.mailboxPaired(txn, p.getOnion(), p.getServerSupports());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeOwnMailboxProperties(Transaction txn) throws DbException {
|
||||
Settings s = new Settings();
|
||||
s.put(SETTINGS_KEY_ONION, "");
|
||||
s.put(SETTINGS_KEY_TOKEN, "");
|
||||
settingsManager.mergeSettings(txn, s, SETTINGS_NAMESPACE);
|
||||
for (MailboxHook hook : hooks) {
|
||||
hook.mailboxUnpaired(txn);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,17 +108,29 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
|
||||
long lastAttempt = s.getLong(SETTINGS_KEY_LAST_ATTEMPT, -1);
|
||||
long lastSuccess = s.getLong(SETTINGS_KEY_LAST_SUCCESS, -1);
|
||||
int attempts = s.getInt(SETTINGS_KEY_ATTEMPTS, 0);
|
||||
return new MailboxStatus(lastAttempt, lastSuccess, attempts);
|
||||
List<MailboxVersion> serverSupports;
|
||||
try {
|
||||
serverSupports = parseServerSupports(s);
|
||||
} catch (DbException e) {
|
||||
serverSupports = emptyList();
|
||||
}
|
||||
return new MailboxStatus(lastAttempt, lastSuccess, attempts,
|
||||
serverSupports);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordSuccessfulConnection(Transaction txn, long now)
|
||||
throws DbException {
|
||||
Settings oldSettings =
|
||||
settingsManager.getSettings(txn, SETTINGS_NAMESPACE);
|
||||
Settings s = new Settings();
|
||||
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);
|
||||
MailboxStatus status = new MailboxStatus(now, now, 0, serverSupports);
|
||||
txn.attach(new OwnMailboxConnectionStatusEvent(status));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,11 +138,17 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
|
||||
throws DbException {
|
||||
Settings oldSettings =
|
||||
settingsManager.getSettings(txn, SETTINGS_NAMESPACE);
|
||||
int attempts = oldSettings.getInt(SETTINGS_KEY_ATTEMPTS, 0);
|
||||
int newAttempts = 1 + oldSettings.getInt(SETTINGS_KEY_ATTEMPTS, 0);
|
||||
long lastSuccess = oldSettings.getLong(SETTINGS_KEY_LAST_SUCCESS, 0);
|
||||
Settings newSettings = new Settings();
|
||||
newSettings.putLong(SETTINGS_KEY_LAST_ATTEMPT, now);
|
||||
newSettings.putInt(SETTINGS_KEY_ATTEMPTS, attempts + 1);
|
||||
newSettings.putInt(SETTINGS_KEY_ATTEMPTS, newAttempts);
|
||||
settingsManager.mergeSettings(txn, newSettings, SETTINGS_NAMESPACE);
|
||||
List<MailboxVersion> serverSupports = parseServerSupports(oldSettings);
|
||||
MailboxStatus status = new MailboxStatus(now, lastSuccess, newAttempts,
|
||||
serverSupports);
|
||||
txn.attach(new OwnMailboxConnectionStatusEvent(status));
|
||||
if (status.hasProblem(now)) txn.attach(new MailboxProblemEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -113,4 +170,19 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
|
||||
if (isNullOrEmpty(filename)) return null;
|
||||
return filename;
|
||||
}
|
||||
|
||||
private List<MailboxVersion> parseServerSupports(Settings s)
|
||||
throws DbException {
|
||||
if (!s.containsKey(SETTINGS_KEY_SERVER_SUPPORTS)) return emptyList();
|
||||
int[] ints = s.getIntArray(SETTINGS_KEY_SERVER_SUPPORTS);
|
||||
// We know we were paired, so we must have proper serverSupports
|
||||
if (ints == null || ints.length == 0 || ints.length % 2 != 0) {
|
||||
throw new DbException();
|
||||
}
|
||||
List<MailboxVersion> serverSupports = new ArrayList<>();
|
||||
for (int i = 0; i < ints.length - 1; i += 2) {
|
||||
serverSupports.add(new MailboxVersion(ints[i], ints[i + 1]));
|
||||
}
|
||||
return serverSupports;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.client.ContactGroupFactory;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.ContactManager.ContactHook;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
import org.briarproject.bramble.api.data.BdfEntry;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.data.MetadataParser;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager.MailboxHook;
|
||||
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.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.mailbox.event.RemoteMailboxUpdateEvent;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Group;
|
||||
import org.briarproject.bramble.api.sync.Group.Visibility;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.InvalidMessageException;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.validation.IncomingMessageHook;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.versioning.ClientVersioningManager;
|
||||
import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE;
|
||||
|
||||
@NotNullByDefault
|
||||
class MailboxUpdateManagerImpl implements MailboxUpdateManager,
|
||||
OpenDatabaseHook, ContactHook, ClientVersioningHook,
|
||||
IncomingMessageHook, MailboxHook {
|
||||
|
||||
private final List<MailboxVersion> clientSupports;
|
||||
private final DatabaseComponent db;
|
||||
private final ClientHelper clientHelper;
|
||||
private final ClientVersioningManager clientVersioningManager;
|
||||
private final MetadataParser metadataParser;
|
||||
private final ContactGroupFactory contactGroupFactory;
|
||||
private final Clock clock;
|
||||
private final MailboxSettingsManager mailboxSettingsManager;
|
||||
private final CryptoComponent crypto;
|
||||
private final Group localGroup;
|
||||
|
||||
@Inject
|
||||
MailboxUpdateManagerImpl(List<MailboxVersion> clientSupports,
|
||||
DatabaseComponent db, ClientHelper clientHelper,
|
||||
ClientVersioningManager clientVersioningManager,
|
||||
MetadataParser metadataParser,
|
||||
ContactGroupFactory contactGroupFactory, Clock clock,
|
||||
MailboxSettingsManager mailboxSettingsManager,
|
||||
CryptoComponent crypto) {
|
||||
this.clientSupports = clientSupports;
|
||||
this.db = db;
|
||||
this.clientHelper = clientHelper;
|
||||
this.clientVersioningManager = clientVersioningManager;
|
||||
this.metadataParser = metadataParser;
|
||||
this.contactGroupFactory = contactGroupFactory;
|
||||
this.clock = clock;
|
||||
this.mailboxSettingsManager = mailboxSettingsManager;
|
||||
this.crypto = crypto;
|
||||
localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID,
|
||||
MAJOR_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDatabaseOpened(Transaction txn) throws DbException {
|
||||
if (db.containsGroup(txn, localGroup.getId())) {
|
||||
try {
|
||||
BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(
|
||||
txn, localGroup.getId());
|
||||
BdfList sent = meta.getList(GROUP_KEY_SENT_CLIENT_SUPPORTS);
|
||||
if (clientHelper.parseMailboxVersionList(sent)
|
||||
.equals(clientSupports)) {
|
||||
return;
|
||||
}
|
||||
} catch (FormatException e) {
|
||||
throw new DbException();
|
||||
}
|
||||
// Our current clientSupports list has changed compared to what we
|
||||
// last sent out.
|
||||
for (Contact c : db.getContacts(txn)) {
|
||||
MailboxUpdate latest = getLocalUpdate(txn, c.getId());
|
||||
MailboxUpdate updated;
|
||||
if (latest.hasMailbox()) {
|
||||
updated = new MailboxUpdateWithMailbox(
|
||||
(MailboxUpdateWithMailbox) latest, clientSupports);
|
||||
} else {
|
||||
updated = new MailboxUpdate(clientSupports);
|
||||
}
|
||||
Group g = getContactGroup(c);
|
||||
storeMessageReplaceLatest(txn, g.getId(), updated);
|
||||
}
|
||||
} else {
|
||||
db.addGroup(txn, localGroup);
|
||||
// Set things up for any pre-existing contacts
|
||||
for (Contact c : db.getContacts(txn)) {
|
||||
addingContact(txn, c);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
BdfDictionary meta = BdfDictionary.of(new BdfEntry(
|
||||
GROUP_KEY_SENT_CLIENT_SUPPORTS,
|
||||
encodeSupportsList(clientSupports)));
|
||||
clientHelper.mergeGroupMetadata(txn, localGroup.getId(), meta);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addingContact(Transaction txn, Contact c) throws DbException {
|
||||
// Create a group to share with the contact
|
||||
Group g = getContactGroup(c);
|
||||
db.addGroup(txn, g);
|
||||
// Apply the client's visibility to the contact group
|
||||
Visibility client = clientVersioningManager
|
||||
.getClientVisibility(txn, c.getId(), CLIENT_ID, MAJOR_VERSION);
|
||||
db.setGroupVisibility(txn, c.getId(), g.getId(), client);
|
||||
// Attach the contact ID to the group
|
||||
clientHelper.setContactId(txn, g.getId(), c.getId());
|
||||
MailboxProperties ownProps =
|
||||
mailboxSettingsManager.getOwnMailboxProperties(txn);
|
||||
if (ownProps != null) {
|
||||
// We are paired, create and send props to the newly added contact
|
||||
createAndSendUpdateWithMailbox(txn, c, ownProps.getServerSupports(),
|
||||
ownProps.getOnion());
|
||||
} else {
|
||||
// Not paired, but we still want to get our clientSupports sent
|
||||
sendUpdateNoMailbox(txn, c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removingContact(Transaction txn, Contact c) throws DbException {
|
||||
db.removeGroup(txn, getContactGroup(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mailboxPaired(Transaction txn, String ownOnion,
|
||||
List<MailboxVersion> serverSupports) throws DbException {
|
||||
for (Contact c : db.getContacts(txn)) {
|
||||
createAndSendUpdateWithMailbox(txn, c, serverSupports, ownOnion);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mailboxUnpaired(Transaction txn) throws DbException {
|
||||
for (Contact c : db.getContacts(txn)) {
|
||||
sendUpdateNoMailbox(txn, c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClientVisibilityChanging(Transaction txn, Contact c,
|
||||
Visibility v) throws DbException {
|
||||
// Apply the client's visibility to the contact group
|
||||
Group g = getContactGroup(c);
|
||||
db.setGroupVisibility(txn, c.getId(), g.getId(), v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeliveryAction incomingMessage(Transaction txn, Message m,
|
||||
Metadata meta) throws DbException, InvalidMessageException {
|
||||
try {
|
||||
BdfDictionary d = metadataParser.parse(meta);
|
||||
// Get latest non-local update in the same group (from same contact)
|
||||
LatestUpdate latest = findLatest(txn, m.getGroupId(), false);
|
||||
if (latest != null) {
|
||||
if (d.getLong(MSG_KEY_VERSION) > latest.version) {
|
||||
db.deleteMessage(txn, latest.messageId);
|
||||
db.deleteMessageMetadata(txn, latest.messageId);
|
||||
} else {
|
||||
// Delete this update, we already have a newer one
|
||||
db.deleteMessage(txn, m.getId());
|
||||
db.deleteMessageMetadata(txn, m.getId());
|
||||
return ACCEPT_DO_NOT_SHARE;
|
||||
}
|
||||
}
|
||||
ContactId c = clientHelper.getContactId(txn, m.getGroupId());
|
||||
BdfList body = clientHelper.getMessageAsList(txn, m.getId());
|
||||
MailboxUpdate u = parseUpdate(body);
|
||||
txn.attach(new RemoteMailboxUpdateEvent(c, u));
|
||||
// Reset message retransmission timers for the contact. Avoiding
|
||||
// messages getting stranded:
|
||||
// - on our mailbox, if they now have a mailbox but didn't before
|
||||
// - on the contact's old mailbox, if they removed their mailbox
|
||||
// - on the contact's old mailbox, if they replaced their mailbox
|
||||
db.resetUnackedMessagesToSend(txn, c);
|
||||
} catch (FormatException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
return ACCEPT_DO_NOT_SHARE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxUpdate getLocalUpdate(Transaction txn, ContactId c)
|
||||
throws DbException {
|
||||
MailboxUpdate local = getUpdate(txn, db.getContact(txn, c), true);
|
||||
// An update (with or without mailbox) is created when contact is added
|
||||
if (local == null) {
|
||||
throw new DbException();
|
||||
}
|
||||
return local;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public MailboxUpdate getRemoteUpdate(Transaction txn, ContactId c)
|
||||
throws DbException {
|
||||
return getUpdate(txn, db.getContact(txn, c), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and sends an update message to the given contact. The message
|
||||
* holds our own mailbox's onion, generated unique properties, and lists of
|
||||
* supported Mailbox API version(s). All of which the contact needs to
|
||||
* communicate with our Mailbox.
|
||||
*/
|
||||
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,
|
||||
new MailboxAuthToken(crypto.generateUniqueId().getBytes()),
|
||||
serverSupports,
|
||||
new MailboxFolderId(crypto.generateUniqueId().getBytes()),
|
||||
new MailboxFolderId(crypto.generateUniqueId().getBytes()));
|
||||
MailboxUpdate u =
|
||||
new MailboxUpdateWithMailbox(clientSupports, properties);
|
||||
Group g = getContactGroup(c);
|
||||
storeMessageReplaceLatest(txn, g.getId(), u);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an update message with empty properties to the given contact. The
|
||||
* empty update indicates for the receiving contact that we don't have any
|
||||
* Mailbox that they can use. It still includes the list of Mailbox API
|
||||
* version(s) that we support as a client.
|
||||
*/
|
||||
private void sendUpdateNoMailbox(Transaction txn, Contact c)
|
||||
throws DbException {
|
||||
Group g = getContactGroup(c);
|
||||
MailboxUpdate u = new MailboxUpdate(clientSupports);
|
||||
storeMessageReplaceLatest(txn, g.getId(), u);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MailboxUpdate getUpdate(Transaction txn, Contact c, boolean local)
|
||||
throws DbException {
|
||||
MailboxUpdate u = null;
|
||||
Group g = getContactGroup(c);
|
||||
try {
|
||||
LatestUpdate latest = findLatest(txn, g.getId(), local);
|
||||
if (latest != null) {
|
||||
BdfList body =
|
||||
clientHelper.getMessageAsList(txn, latest.messageId);
|
||||
u = parseUpdate(body);
|
||||
}
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
return u;
|
||||
}
|
||||
|
||||
private void storeMessageReplaceLatest(Transaction txn, GroupId g,
|
||||
MailboxUpdate u) throws DbException {
|
||||
try {
|
||||
LatestUpdate latest = findLatest(txn, g, true);
|
||||
long version = latest == null ? 1 : latest.version + 1;
|
||||
Message m = clientHelper.createMessage(g, clock.currentTimeMillis(),
|
||||
encodeProperties(version, u));
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(MSG_KEY_VERSION, version);
|
||||
meta.put(MSG_KEY_LOCAL, true);
|
||||
clientHelper.addLocalMessage(txn, m, meta, true, false);
|
||||
if (latest != null) {
|
||||
db.removeMessage(txn, latest.messageId);
|
||||
}
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private LatestUpdate findLatest(Transaction txn, GroupId g, boolean local)
|
||||
throws DbException, FormatException {
|
||||
Map<MessageId, BdfDictionary> metadata =
|
||||
clientHelper.getMessageMetadataAsDictionary(txn, g);
|
||||
// We should have at most 1 local and 1 remote
|
||||
if (metadata.size() > 2) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
|
||||
BdfDictionary meta = e.getValue();
|
||||
if (meta.getBoolean(MSG_KEY_LOCAL) == local) {
|
||||
return new LatestUpdate(e.getKey(),
|
||||
meta.getLong(MSG_KEY_VERSION));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private BdfList encodeProperties(long version, MailboxUpdate u) {
|
||||
BdfDictionary dict = new BdfDictionary();
|
||||
BdfList serverSupports = new BdfList();
|
||||
if (u.hasMailbox()) {
|
||||
MailboxUpdateWithMailbox um = (MailboxUpdateWithMailbox) u;
|
||||
MailboxProperties properties = um.getMailboxProperties();
|
||||
serverSupports = encodeSupportsList(properties.getServerSupports());
|
||||
dict.put(PROP_KEY_ONION, properties.getOnion());
|
||||
dict.put(PROP_KEY_AUTHTOKEN, properties.getAuthToken());
|
||||
dict.put(PROP_KEY_INBOXID, properties.getInboxId());
|
||||
dict.put(PROP_KEY_OUTBOXID, properties.getOutboxId());
|
||||
}
|
||||
return BdfList.of(version, encodeSupportsList(u.getClientSupports()),
|
||||
serverSupports, dict);
|
||||
}
|
||||
|
||||
private BdfList encodeSupportsList(List<MailboxVersion> supportsList) {
|
||||
BdfList supports = new BdfList();
|
||||
for (MailboxVersion version : supportsList) {
|
||||
supports.add(BdfList.of(version.getMajor(), version.getMinor()));
|
||||
}
|
||||
return supports;
|
||||
}
|
||||
|
||||
private MailboxUpdate parseUpdate(BdfList body) throws FormatException {
|
||||
BdfList clientSupports = body.getList(1);
|
||||
BdfList serverSupports = body.getList(2);
|
||||
BdfDictionary dict = body.getDictionary(3);
|
||||
return clientHelper.parseAndValidateMailboxUpdate(clientSupports,
|
||||
serverSupports, dict);
|
||||
}
|
||||
|
||||
private Group getContactGroup(Contact c) {
|
||||
return contactGroupFactory.createContactGroup(CLIENT_ID, MAJOR_VERSION,
|
||||
c);
|
||||
}
|
||||
|
||||
private static class LatestUpdate {
|
||||
|
||||
private final MessageId messageId;
|
||||
private final long version;
|
||||
|
||||
private LatestUpdate(MessageId messageId, long version) {
|
||||
this.messageId = messageId;
|
||||
this.version = version;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.client.BdfMessageContext;
|
||||
import org.briarproject.bramble.api.client.BdfMessageValidator;
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.data.MetadataEncoder;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Group;
|
||||
import org.briarproject.bramble.api.sync.InvalidMessageException;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxUpdateManager.MSG_KEY_LOCAL;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxUpdateManager.MSG_KEY_VERSION;
|
||||
import static org.briarproject.bramble.util.ValidationUtils.checkSize;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class MailboxUpdateValidator extends BdfMessageValidator {
|
||||
|
||||
MailboxUpdateValidator(ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock) {
|
||||
super(clientHelper, metadataEncoder, clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BdfMessageContext validateMessage(Message m, Group g,
|
||||
BdfList body) throws InvalidMessageException, FormatException {
|
||||
// Version, Properties, clientSupports, serverSupports
|
||||
checkSize(body, 4);
|
||||
// Version
|
||||
long version = body.getLong(0);
|
||||
if (version < 0) throw new FormatException();
|
||||
// clientSupports
|
||||
BdfList clientSupports = body.getList(1);
|
||||
// serverSupports
|
||||
BdfList serverSupports = body.getList(2);
|
||||
// Properties
|
||||
BdfDictionary dictionary = body.getDictionary(3);
|
||||
clientHelper.parseAndValidateMailboxUpdate(clientSupports,
|
||||
serverSupports, dictionary);
|
||||
// Return the metadata
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(MSG_KEY_VERSION, version);
|
||||
meta.put(MSG_KEY_LOCAL, false);
|
||||
return new BdfMessageContext(meta);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
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.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class OwnMailboxConnectivityChecker extends ConnectivityCheckerImpl {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(OwnMailboxConnectivityChecker.class.getName());
|
||||
|
||||
private final MailboxApi mailboxApi;
|
||||
private final TransactionManager db;
|
||||
private final MailboxSettingsManager mailboxSettingsManager;
|
||||
|
||||
OwnMailboxConnectivityChecker(Clock clock,
|
||||
MailboxApiCaller mailboxApiCaller,
|
||||
MailboxApi mailboxApi,
|
||||
TransactionManager db,
|
||||
MailboxSettingsManager mailboxSettingsManager) {
|
||||
super(clock, mailboxApiCaller);
|
||||
this.mailboxApi = mailboxApi;
|
||||
this.db = db;
|
||||
this.mailboxSettingsManager = mailboxSettingsManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
ApiCall createConnectivityCheckTask(MailboxProperties properties) {
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
return () -> {
|
||||
try {
|
||||
return checkConnectivityAndStoreResult(properties);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return true; // Retry
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private boolean checkConnectivityAndStoreResult(
|
||||
MailboxProperties properties) throws DbException {
|
||||
try {
|
||||
if (!mailboxApi.checkStatus(properties)) throw new ApiException();
|
||||
LOG.info("Own mailbox is reachable");
|
||||
long now = clock.currentTimeMillis();
|
||||
db.transaction(false, txn -> mailboxSettingsManager
|
||||
.recordSuccessfulConnection(txn, now));
|
||||
// Call the observers and cache the result
|
||||
onConnectivityCheckSucceeded(now);
|
||||
return false; // Don't retry
|
||||
} catch (IOException | ApiException e) {
|
||||
LOG.warning("Own mailbox is unreachable");
|
||||
logException(LOG, WARNING, e);
|
||||
long now = clock.currentTimeMillis();
|
||||
db.transaction(false, txn -> mailboxSettingsManager
|
||||
.recordFailedConnectionAttempt(txn, now));
|
||||
}
|
||||
return true; // Retry
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
/**
|
||||
* Convenience class for making simple API calls that don't return values.
|
||||
*/
|
||||
@NotNullByDefault
|
||||
public abstract class SimpleApiCall implements ApiCall {
|
||||
|
||||
private static final Logger LOG = getLogger(SimpleApiCall.class.getName());
|
||||
|
||||
abstract void tryToCallApi()
|
||||
throws IOException, ApiException, TolerableFailureException;
|
||||
|
||||
@Override
|
||||
public boolean callApi() {
|
||||
try {
|
||||
tryToCallApi();
|
||||
return false; // Succeeded, don't retry
|
||||
} catch (IOException | ApiException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return true; // Failed, retry with backoff
|
||||
} catch (TolerableFailureException e) {
|
||||
logException(LOG, INFO, e);
|
||||
return false; // Failed tolerably, don't retry
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.Plugin;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
interface TorReachabilityMonitor {
|
||||
|
||||
/**
|
||||
* How long the Tor plugin needs to be continuously
|
||||
* {@link Plugin.State#ACTIVE active} before we assume our contacts can
|
||||
* reach our hidden service.
|
||||
*/
|
||||
long REACHABILITY_PERIOD_MS = MINUTES.toMillis(10);
|
||||
|
||||
/**
|
||||
* Starts the monitor.
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Destroys the monitor.
|
||||
*/
|
||||
void destroy();
|
||||
|
||||
/**
|
||||
* Adds an observer that will be called when our Tor hidden service becomes
|
||||
* reachable. If our hidden service is already reachable, the observer is
|
||||
* called immediately.
|
||||
* <p>
|
||||
* Observers are removed after being called, or when the monitor is
|
||||
* {@link #destroy() destroyed}.
|
||||
*/
|
||||
void addOneShotObserver(TorReachabilityObserver o);
|
||||
|
||||
interface TorReachabilityObserver {
|
||||
|
||||
void onTorReachable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.Plugin;
|
||||
import org.briarproject.bramble.api.plugin.PluginManager;
|
||||
import org.briarproject.bramble.api.plugin.event.TransportActiveEvent;
|
||||
import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.ID;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class TorReachabilityMonitorImpl
|
||||
implements TorReachabilityMonitor, EventListener {
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final PluginManager pluginManager;
|
||||
private final EventBus eventBus;
|
||||
private final Object lock = new Object();
|
||||
|
||||
@GuardedBy("lock")
|
||||
private boolean reachable = false, destroyed = false;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private final List<TorReachabilityObserver> observers = new ArrayList<>();
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private Cancellable task = null;
|
||||
|
||||
@Inject
|
||||
TorReachabilityMonitorImpl(
|
||||
@IoExecutor Executor ioExecutor,
|
||||
TaskScheduler taskScheduler,
|
||||
PluginManager pluginManager,
|
||||
EventBus eventBus) {
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.taskScheduler = taskScheduler;
|
||||
this.pluginManager = pluginManager;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
eventBus.addListener(this);
|
||||
Plugin plugin = pluginManager.getPlugin(ID);
|
||||
if (plugin != null && plugin.getState() == ACTIVE) onTorActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
eventBus.removeListener(this);
|
||||
synchronized (lock) {
|
||||
destroyed = true;
|
||||
if (task != null) task.cancel();
|
||||
task = null;
|
||||
observers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addOneShotObserver(TorReachabilityObserver o) {
|
||||
boolean callNow = false;
|
||||
synchronized (lock) {
|
||||
if (destroyed) return;
|
||||
if (reachable) callNow = true;
|
||||
else observers.add(o);
|
||||
}
|
||||
if (callNow) o.onTorReachable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof TransportActiveEvent) {
|
||||
TransportActiveEvent t = (TransportActiveEvent) e;
|
||||
if (t.getTransportId().equals(ID)) onTorActive();
|
||||
} else if (e instanceof TransportInactiveEvent) {
|
||||
TransportInactiveEvent t = (TransportInactiveEvent) e;
|
||||
if (t.getTransportId().equals(ID)) onTorInactive();
|
||||
}
|
||||
}
|
||||
|
||||
private void onTorActive() {
|
||||
synchronized (lock) {
|
||||
if (destroyed || task != null) return;
|
||||
task = taskScheduler.schedule(this::onTorReachable, ioExecutor,
|
||||
REACHABILITY_PERIOD_MS, MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private void onTorInactive() {
|
||||
synchronized (lock) {
|
||||
reachable = false;
|
||||
if (task != null) task.cancel();
|
||||
task = null;
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void onTorReachable() {
|
||||
List<TorReachabilityObserver> observers;
|
||||
synchronized (lock) {
|
||||
if (destroyed) return;
|
||||
reachable = true;
|
||||
observers = new ArrayList<>(this.observers);
|
||||
this.observers.clear();
|
||||
task = null;
|
||||
}
|
||||
for (TorReachabilityObserver o : observers) o.onTorReachable();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.plugin;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.Pair;
|
||||
import org.briarproject.bramble.api.connection.ConnectionManager;
|
||||
import org.briarproject.bramble.api.connection.ConnectionRegistry;
|
||||
@@ -27,7 +28,6 @@ import org.briarproject.bramble.api.properties.TransportProperties;
|
||||
import org.briarproject.bramble.api.properties.TransportPropertyManager;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler.Cancellable;
|
||||
import org.briarproject.bramble.api.system.Wakeful;
|
||||
import org.briarproject.bramble.api.system.WakefulIoExecutor;
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ abstract class AbstractRemovableDrivePlugin implements SimplexPlugin {
|
||||
public void start() {
|
||||
callback.mergeLocalProperties(
|
||||
new TransportProperties(singletonMap(PROP_SUPPORTED, "true")));
|
||||
callback.pluginStateChanged(ACTIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
@@ -29,11 +30,6 @@ abstract class FilePlugin implements SimplexPlugin {
|
||||
protected final PluginCallback callback;
|
||||
protected final long maxLatency;
|
||||
|
||||
protected abstract void writerFinished(File f, boolean exception);
|
||||
|
||||
protected abstract void readerFinished(File f, boolean exception,
|
||||
boolean recognised);
|
||||
|
||||
FilePlugin(PluginCallback callback, long maxLatency) {
|
||||
this.callback = callback;
|
||||
this.maxLatency = maxLatency;
|
||||
@@ -50,9 +46,8 @@ abstract class FilePlugin implements SimplexPlugin {
|
||||
String path = p.get(PROP_PATH);
|
||||
if (isNullOrEmpty(path)) return null;
|
||||
try {
|
||||
File file = new File(path);
|
||||
FileInputStream in = new FileInputStream(file);
|
||||
return new FileTransportReader(file, in, this);
|
||||
FileInputStream in = new FileInputStream(path);
|
||||
return new TransportInputStreamReader(in);
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return null;
|
||||
@@ -70,8 +65,8 @@ abstract class FilePlugin implements SimplexPlugin {
|
||||
LOG.info("Failed to create file");
|
||||
return null;
|
||||
}
|
||||
FileOutputStream out = new FileOutputStream(file);
|
||||
return new FileTransportWriter(file, out, this);
|
||||
OutputStream out = new FileOutputStream(file);
|
||||
return new TransportOutputStreamWriter(this, out);
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return null;
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package org.briarproject.bramble.plugin.file;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.util.IoUtils.tryToClose;
|
||||
|
||||
@NotNullByDefault
|
||||
class FileTransportReader implements TransportConnectionReader {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(FileTransportReader.class.getName());
|
||||
|
||||
private final File file;
|
||||
private final InputStream in;
|
||||
private final FilePlugin plugin;
|
||||
|
||||
FileTransportReader(File file, InputStream in, FilePlugin plugin) {
|
||||
this.file = file;
|
||||
this.in = in;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return in;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(boolean exception, boolean recognised) {
|
||||
tryToClose(in, LOG, WARNING);
|
||||
plugin.readerFinished(file, exception, recognised);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package org.briarproject.bramble.plugin.file;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.OutputStream;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.util.IoUtils.tryToClose;
|
||||
|
||||
@NotNullByDefault
|
||||
class FileTransportWriter implements TransportConnectionWriter {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(FileTransportWriter.class.getName());
|
||||
|
||||
private final File file;
|
||||
private final OutputStream out;
|
||||
private final FilePlugin plugin;
|
||||
|
||||
FileTransportWriter(File file, OutputStream out, FilePlugin plugin) {
|
||||
this.file = file;
|
||||
this.out = out;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaxLatency() {
|
||||
return plugin.getMaxLatency();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxIdleTime() {
|
||||
return plugin.getMaxIdleTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLossyAndCheap() {
|
||||
return plugin.isLossyAndCheap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(boolean exception) {
|
||||
tryToClose(out, LOG, WARNING);
|
||||
plugin.writerFinished(file, exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.briarproject.bramble.plugin.file;
|
||||
|
||||
import org.briarproject.bramble.api.Pair;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.ConnectionHandler;
|
||||
import org.briarproject.bramble.api.plugin.PluginCallback;
|
||||
import org.briarproject.bramble.api.plugin.PluginException;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.ID;
|
||||
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
|
||||
|
||||
@NotNullByDefault
|
||||
class MailboxPlugin extends FilePlugin {
|
||||
|
||||
MailboxPlugin(PluginCallback callback, long maxLatency) {
|
||||
super(callback, maxLatency);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportId getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxIdleTime() {
|
||||
// Unused for simplex transports
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws PluginException {
|
||||
callback.pluginStateChanged(ACTIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws PluginException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState() {
|
||||
return ACTIVE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getReasonsDisabled() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldPoll() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPollingInterval() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void poll(
|
||||
Collection<Pair<TransportProperties, ConnectionHandler>> properties) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLossyAndCheap() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.briarproject.bramble.plugin.file;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.PluginCallback;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
|
||||
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;
|
||||
|
||||
@NotNullByDefault
|
||||
public class MailboxPluginFactory implements SimplexPluginFactory {
|
||||
|
||||
private static final long MAX_LATENCY = DAYS.toMillis(14);
|
||||
|
||||
@Inject
|
||||
MailboxPluginFactory() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportId getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaxLatency() {
|
||||
return MAX_LATENCY;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SimplexPlugin createPlugin(PluginCallback callback) {
|
||||
return new MailboxPlugin(callback, MAX_LATENCY);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ public interface CircumventionProvider {
|
||||
enum BridgeType {
|
||||
DEFAULT_OBFS4,
|
||||
NON_DEFAULT_OBFS4,
|
||||
VANILLA,
|
||||
MEEK
|
||||
}
|
||||
|
||||
@@ -20,30 +21,30 @@ public interface CircumventionProvider {
|
||||
* See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
||||
* and https://trac.torproject.org/projects/tor/wiki/doc/OONI/censorshipwiki
|
||||
*/
|
||||
String[] BLOCKED = {"CN", "IR", "EG", "BY", "TR", "SY", "VE", "RU"};
|
||||
String[] BLOCKED = {"BY", "CN", "EG", "IR", "RU", "TM", "VE"};
|
||||
|
||||
/**
|
||||
* Countries where obfs4 or meek bridge connections are likely to work.
|
||||
* Countries where bridge connections are likely to work.
|
||||
* Should be a subset of {@link #BLOCKED} and the union of
|
||||
* {@link #DEFAULT_OBFS4_BRIDGES}, {@link #NON_DEFAULT_OBFS4_BRIDGES} and
|
||||
* {@link #DEFAULT_BRIDGES}, {@link #NON_DEFAULT_BRIDGES} and
|
||||
* {@link #MEEK_BRIDGES}.
|
||||
*/
|
||||
String[] BRIDGES = {"CN", "IR", "EG", "BY", "TR", "SY", "VE", "RU"};
|
||||
String[] BRIDGES = {"BY", "CN", "EG", "IR", "RU", "TM", "VE"};
|
||||
|
||||
/**
|
||||
* Countries where default obfs4 bridges are likely to work.
|
||||
* Countries where default obfs4 or vanilla bridges are likely to work.
|
||||
* Should be a subset of {@link #BRIDGES}.
|
||||
*/
|
||||
String[] DEFAULT_OBFS4_BRIDGES = {"EG", "BY", "TR", "SY", "VE"};
|
||||
String[] DEFAULT_BRIDGES = {"EG", "VE"};
|
||||
|
||||
/**
|
||||
* Countries where non-default obfs4 bridges are likely to work.
|
||||
* Countries where non-default obfs4 or vanilla bridges are likely to work.
|
||||
* Should be a subset of {@link #BRIDGES}.
|
||||
*/
|
||||
String[] NON_DEFAULT_OBFS4_BRIDGES = {"RU"};
|
||||
String[] NON_DEFAULT_BRIDGES = {"BY", "RU", "TM"};
|
||||
|
||||
/**
|
||||
* Countries where obfs4 bridges won't work and meek is needed.
|
||||
* Countries where obfs4 and vanilla bridges won't work and meek is needed.
|
||||
* Should be a subset of {@link #BRIDGES}.
|
||||
*/
|
||||
String[] MEEK_BRIDGES = {"CN", "IR"};
|
||||
@@ -60,10 +61,11 @@ public interface CircumventionProvider {
|
||||
boolean doBridgesWork(String countryCode);
|
||||
|
||||
/**
|
||||
* Returns the best type of bridge connection for the given country, or
|
||||
* {@link #DEFAULT_OBFS4_BRIDGES} if no bridge type is known to work.
|
||||
* Returns the types of bridge connection that are suitable for the given
|
||||
* country, or {@link #DEFAULT_BRIDGES} if no bridge type is known
|
||||
* to work.
|
||||
*/
|
||||
BridgeType getBestBridgeType(String countryCode);
|
||||
List<BridgeType> getSuitableBridgeTypes(String countryCode);
|
||||
|
||||
@IoExecutor
|
||||
List<String> getBridges(BridgeType type);
|
||||
|
||||
@@ -14,10 +14,12 @@ 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;
|
||||
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4;
|
||||
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.VANILLA;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
@@ -30,9 +32,9 @@ class CircumventionProviderImpl implements CircumventionProvider {
|
||||
private static final Set<String> BRIDGE_COUNTRIES =
|
||||
new HashSet<>(asList(BRIDGES));
|
||||
private static final Set<String> DEFAULT_OBFS4_BRIDGE_COUNTRIES =
|
||||
new HashSet<>(asList(DEFAULT_OBFS4_BRIDGES));
|
||||
private static final Set<String> NON_DEFAULT_OBFS4_BRIDGE_COUNTRIES =
|
||||
new HashSet<>(asList(NON_DEFAULT_OBFS4_BRIDGES));
|
||||
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));
|
||||
|
||||
@@ -51,15 +53,15 @@ class CircumventionProviderImpl implements CircumventionProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BridgeType getBestBridgeType(String countryCode) {
|
||||
public List<BridgeType> getSuitableBridgeTypes(String countryCode) {
|
||||
if (DEFAULT_OBFS4_BRIDGE_COUNTRIES.contains(countryCode)) {
|
||||
return DEFAULT_OBFS4;
|
||||
} else if (NON_DEFAULT_OBFS4_BRIDGE_COUNTRIES.contains(countryCode)) {
|
||||
return NON_DEFAULT_OBFS4;
|
||||
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 MEEK;
|
||||
return singletonList(MEEK);
|
||||
} else {
|
||||
return DEFAULT_OBFS4;
|
||||
return asList(DEFAULT_OBFS4, VANILLA);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,12 +75,10 @@ class CircumventionProviderImpl implements CircumventionProvider {
|
||||
List<String> bridges = new ArrayList<>();
|
||||
while (scanner.hasNextLine()) {
|
||||
String line = scanner.nextLine();
|
||||
boolean isDefaultObfs4 = line.startsWith("d ");
|
||||
boolean isNonDefaultObfs4 = line.startsWith("n ");
|
||||
boolean isMeek = line.startsWith("m ");
|
||||
if ((type == DEFAULT_OBFS4 && isDefaultObfs4) ||
|
||||
(type == NON_DEFAULT_OBFS4 && isNonDefaultObfs4) ||
|
||||
(type == MEEK && isMeek)) {
|
||||
if ((type == DEFAULT_OBFS4 && line.startsWith("d ")) ||
|
||||
(type == NON_DEFAULT_OBFS4 && line.startsWith("n ")) ||
|
||||
(type == VANILLA && line.startsWith("v ")) ||
|
||||
(type == MEEK && line.startsWith("m "))) {
|
||||
bridges.add(line.substring(2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.briarproject.bramble.plugin.tor;
|
||||
|
||||
import net.freehaven.tor.control.EventHandler;
|
||||
import net.freehaven.tor.control.TorControlConnection;
|
||||
import net.freehaven.tor.control.TorNotRunningException;
|
||||
|
||||
import org.briarproject.bramble.PoliteExecutor;
|
||||
import org.briarproject.bramble.api.Pair;
|
||||
@@ -85,7 +86,6 @@ import static org.briarproject.bramble.api.plugin.TorConstants.ID;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_MOBILE;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_AUTOMATIC;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_NEVER;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_WITH_BRIDGES;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHEN_CHARGING;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_PORT;
|
||||
@@ -93,7 +93,9 @@ 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;
|
||||
@@ -108,7 +110,14 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
private static final Logger LOG = getLogger(TorPlugin.class.getName());
|
||||
|
||||
private static final String[] EVENTS = {
|
||||
"CIRC", "ORCONN", "HS_DESC", "NOTICE", "WARN", "ERR"
|
||||
"CIRC",
|
||||
"ORCONN",
|
||||
"STATUS_GENERAL",
|
||||
"STATUS_CLIENT",
|
||||
"HS_DESC",
|
||||
"NOTICE",
|
||||
"WARN",
|
||||
"ERR"
|
||||
};
|
||||
private static final String OWNER = "__OwningControllerProcess";
|
||||
private static final int COOKIE_TIMEOUT_MS = 3000;
|
||||
@@ -228,7 +237,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
}
|
||||
// Load the settings
|
||||
settings = migrateSettings(callback.getSettings());
|
||||
settings = callback.getSettings();
|
||||
// Install or update the assets if necessary
|
||||
if (!assetsAreUpToDate()) installAssets();
|
||||
if (cookieFile.exists() && !cookieFile.delete())
|
||||
@@ -302,11 +311,19 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
controlConnection.setEventHandler(this);
|
||||
controlConnection.setEvents(asList(EVENTS));
|
||||
// Check whether Tor has already bootstrapped
|
||||
String phase = controlConnection.getInfo("status/bootstrap-phase");
|
||||
if (phase != null && phase.contains("PROGRESS=100")) {
|
||||
String info = controlConnection.getInfo("status/bootstrap-phase");
|
||||
if (info != null && info.contains("PROGRESS=100")) {
|
||||
LOG.info("Tor has already bootstrapped");
|
||||
state.setBootstrapped();
|
||||
}
|
||||
// Check whether Tor has already built a circuit
|
||||
info = controlConnection.getInfo("status/circuit-established");
|
||||
if ("1".equals(info)) {
|
||||
LOG.info("Tor has already built a circuit");
|
||||
state.getAndSetCircuitBuilt(true);
|
||||
}
|
||||
} catch (TorNotRunningException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
throw new PluginException(e);
|
||||
}
|
||||
@@ -318,18 +335,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
bind();
|
||||
}
|
||||
|
||||
// TODO: Remove after a reasonable migration period (added 2020-06-25)
|
||||
private Settings migrateSettings(Settings settings) {
|
||||
int network = settings.getInt(PREF_TOR_NETWORK,
|
||||
DEFAULT_PREF_TOR_NETWORK);
|
||||
if (network == PREF_TOR_NETWORK_NEVER) {
|
||||
settings.putInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK);
|
||||
settings.putBoolean(PREF_PLUGIN_ENABLE, false);
|
||||
callback.mergeSettings(settings);
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
private boolean assetsAreUpToDate() {
|
||||
return doneFile.lastModified() > getLastUpdateTime();
|
||||
}
|
||||
@@ -339,9 +344,14 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
// 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(getGeoIpInputStream(), geoIpFile);
|
||||
extract(getConfigInputStream(), configFile);
|
||||
if (!doneFile.createNewFile())
|
||||
LOG.warning("Failed to create done file");
|
||||
@@ -379,14 +389,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
return zin;
|
||||
}
|
||||
|
||||
private InputStream getGeoIpInputStream() throws IOException {
|
||||
InputStream in = resourceProvider.getResourceInputStream("geoip",
|
||||
".zip");
|
||||
ZipInputStream zin = new ZipInputStream(in);
|
||||
if (zin.getNextEntry() == null) throw new IOException();
|
||||
return zin;
|
||||
}
|
||||
|
||||
private InputStream getObfs4InputStream() throws IOException {
|
||||
InputStream in = resourceProvider
|
||||
.getResourceInputStream("obfs4proxy_" + architecture, ".zip");
|
||||
@@ -493,6 +495,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
} else {
|
||||
response = controlConnection.addOnion(privKey, portLines);
|
||||
}
|
||||
} catch (TorNotRunningException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return;
|
||||
@@ -541,26 +545,38 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
|
||||
protected void enableNetwork(boolean enable) throws IOException {
|
||||
state.enableNetwork(enable);
|
||||
controlConnection.setConf("DisableNetwork", enable ? "0" : "1");
|
||||
try {
|
||||
controlConnection.setConf("DisableNetwork", enable ? "0" : "1");
|
||||
} catch (TorNotRunningException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void enableBridges(boolean enable, BridgeType bridgeType)
|
||||
private void enableBridges(boolean enable, List<BridgeType> bridgeTypes)
|
||||
throws IOException {
|
||||
if (enable) {
|
||||
Collection<String> conf = new ArrayList<>();
|
||||
conf.add("UseBridges 1");
|
||||
File obfs4File = getObfs4ExecutableFile();
|
||||
if (bridgeType == MEEK) {
|
||||
conf.add("ClientTransportPlugin meek_lite exec " +
|
||||
obfs4File.getAbsolutePath());
|
||||
try {
|
||||
if (enable) {
|
||||
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 {
|
||||
conf.add("ClientTransportPlugin obfs4 exec " +
|
||||
obfs4File.getAbsolutePath());
|
||||
controlConnection.setConf("UseBridges", "0");
|
||||
}
|
||||
conf.addAll(circumventionProvider.getBridges(bridgeType));
|
||||
controlConnection.setConf(conf);
|
||||
} else {
|
||||
controlConnection.setConf("UseBridges", "0");
|
||||
} catch (TorNotRunningException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,6 +590,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
controlConnection.setConf("DisableNetwork", "1");
|
||||
controlConnection.shutdownTor("TERM");
|
||||
controlSocket.close();
|
||||
} catch (TorNotRunningException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
@@ -646,7 +664,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Could not connect to v3 "
|
||||
+ scrubOnion(onion3) + ": " + e.toString());
|
||||
+ scrubOnion(onion3) + ": " + e);
|
||||
}
|
||||
tryToClose(s, LOG, WARNING);
|
||||
return null;
|
||||
@@ -682,8 +700,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
byte[] localSeed = alice ? aliceSeed : bobSeed;
|
||||
byte[] remoteSeed = alice ? bobSeed : aliceSeed;
|
||||
String blob = torRendezvousCrypto.getPrivateKeyBlob(localSeed);
|
||||
String localOnion = torRendezvousCrypto.getOnionAddress(localSeed);
|
||||
String remoteOnion = torRendezvousCrypto.getOnionAddress(remoteSeed);
|
||||
String localOnion = torRendezvousCrypto.getOnion(localSeed);
|
||||
String remoteOnion = torRendezvousCrypto.getOnion(remoteSeed);
|
||||
TransportProperties remoteProperties = new TransportProperties();
|
||||
remoteProperties.put(PROP_ONION_V3, remoteOnion);
|
||||
try {
|
||||
@@ -705,7 +723,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
});
|
||||
Map<Integer, String> portLines =
|
||||
singletonMap(80, "127.0.0.1:" + port);
|
||||
controlConnection.addOnion(blob, portLines);
|
||||
try {
|
||||
controlConnection.addOnion(blob, portLines);
|
||||
} catch (TorNotRunningException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return new RendezvousEndpoint() {
|
||||
|
||||
@Override
|
||||
@@ -715,7 +737,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
controlConnection.delOnion(localOnion);
|
||||
try {
|
||||
controlConnection.delOnion(localOnion);
|
||||
} catch (TorNotRunningException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
tryToClose(ss, LOG, WARNING);
|
||||
}
|
||||
};
|
||||
@@ -727,9 +753,10 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
|
||||
@Override
|
||||
public void circuitStatus(String status, String id, String path) {
|
||||
if (status.equals("BUILT") &&
|
||||
state.getAndSetCircuitBuilt()) {
|
||||
LOG.info("First circuit built");
|
||||
// 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)) {
|
||||
LOG.info("Circuit built");
|
||||
backoff.reset();
|
||||
}
|
||||
}
|
||||
@@ -740,13 +767,10 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
|
||||
@Override
|
||||
public void orConnStatus(String status, String orName) {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("OR connection " + status + " " + orName);
|
||||
if (status.equals("CLOSED") || status.equals("FAILED")) {
|
||||
// Check whether we've lost connectivity
|
||||
updateConnectionStatus(networkManager.getNetworkStatus(),
|
||||
batteryManager.isCharging());
|
||||
}
|
||||
if (LOG.isLoggable(INFO)) LOG.info("OR connection " + status);
|
||||
|
||||
if (status.equals("CONNECTED")) state.onOrConnectionConnected();
|
||||
else if (status.equals("CLOSED")) state.onOrConnectionClosed();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -760,24 +784,78 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
@Override
|
||||
public void message(String severity, String msg) {
|
||||
if (LOG.isLoggable(INFO)) LOG.info(severity + " " + msg);
|
||||
if (severity.equals("NOTICE") && msg.startsWith("Bootstrapped 100%")) {
|
||||
state.setBootstrapped();
|
||||
backoff.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unrecognized(String type, String msg) {
|
||||
if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
String[] words = msg.split(" ");
|
||||
if (words.length > 1 && ONION_V3.matcher(words[1]).matches()) {
|
||||
LOG.info("V3 descriptor uploaded");
|
||||
} else {
|
||||
LOG.info("V2 descriptor uploaded");
|
||||
if (type.equals("STATUS_CLIENT")) {
|
||||
handleClientStatus(removeSeverity(msg));
|
||||
} else if (type.equals("STATUS_GENERAL")) {
|
||||
handleGeneralStatus(removeSeverity(msg));
|
||||
} else if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) {
|
||||
String[] parts = msg.split(" ");
|
||||
if (parts.length < 2) {
|
||||
LOG.warning("Failed to parse HS_DESC UPLOADED event");
|
||||
} else if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("V3 descriptor uploaded for " + scrubOnion(parts[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String removeSeverity(String msg) {
|
||||
return msg.replaceFirst("[^ ]+ ", "");
|
||||
}
|
||||
|
||||
private void handleClientStatus(String msg) {
|
||||
if (msg.startsWith("BOOTSTRAP PROGRESS=100")) {
|
||||
LOG.info("Bootstrapped");
|
||||
state.setBootstrapped();
|
||||
backoff.reset();
|
||||
} else if (msg.startsWith("CIRCUIT_ESTABLISHED")) {
|
||||
if (!state.getAndSetCircuitBuilt(true)) {
|
||||
LOG.info("Circuit built");
|
||||
backoff.reset();
|
||||
}
|
||||
} else if (msg.startsWith("CIRCUIT_NOT_ESTABLISHED")) {
|
||||
if (state.getAndSetCircuitBuilt(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
|
||||
// established circuits, which might still be functioning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleGeneralStatus(String msg) {
|
||||
if (msg.startsWith("CLOCK_JUMPED")) {
|
||||
Long time = parseLongArgument(msg, "TIME");
|
||||
if (time != null && LOG.isLoggable(WARNING)) {
|
||||
LOG.warning("Clock jumped " + time + " seconds");
|
||||
}
|
||||
} else if (msg.startsWith("CLOCK_SKEW")) {
|
||||
Long skew = parseLongArgument(msg, "SKEW");
|
||||
if (skew != null && LOG.isLoggable(WARNING)) {
|
||||
LOG.warning("Clock is skewed by " + skew + " seconds");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Long parseLongArgument(String msg, String argName) {
|
||||
String[] args = msg.split(" ");
|
||||
for (String arg : args) {
|
||||
if (arg.startsWith(argName + "=")) {
|
||||
try {
|
||||
return Long.parseLong(arg.substring(argName.length() + 1));
|
||||
} catch (NumberFormatException e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (LOG.isLoggable(WARNING)) {
|
||||
LOG.warning("Failed to parse " + argName + " from '" + msg + "'");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -787,11 +865,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
if (s.getNamespace().equals(ID.getString())) {
|
||||
LOG.info("Tor settings updated");
|
||||
settings = s.getSettings();
|
||||
// Works around a bug introduced in Tor 0.3.4.8.
|
||||
// https://trac.torproject.org/projects/tor/ticket/28027
|
||||
// Could be replaced with callback.transportDisabled()
|
||||
// when fixed.
|
||||
disableNetwork();
|
||||
updateConnectionStatus(networkManager.getNetworkStatus(),
|
||||
batteryManager.isCharging());
|
||||
}
|
||||
@@ -804,16 +877,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
}
|
||||
|
||||
private void disableNetwork() {
|
||||
connectionStatusExecutor.execute(() -> {
|
||||
try {
|
||||
if (state.isTorRunning()) enableNetwork(false);
|
||||
} catch (IOException ex) {
|
||||
logException(LOG, WARNING, ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateConnectionStatus(NetworkStatus status,
|
||||
boolean charging) {
|
||||
connectionStatusExecutor.execute(() -> {
|
||||
@@ -847,8 +910,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
int reasonsDisabled = 0;
|
||||
boolean enableNetwork = false, enableBridges = false;
|
||||
boolean enableConnectionPadding = false;
|
||||
BridgeType bridgeType =
|
||||
circumventionProvider.getBestBridgeType(country);
|
||||
List<BridgeType> bridgeTypes =
|
||||
circumventionProvider.getSuitableBridgeTypes(country);
|
||||
|
||||
if (!online) {
|
||||
LOG.info("Disabling network, device is offline");
|
||||
@@ -877,10 +940,10 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
enableNetwork = true;
|
||||
if (network == PREF_TOR_NETWORK_WITH_BRIDGES ||
|
||||
(automatic && bridgesWork)) {
|
||||
if (ipv6Only) bridgeType = MEEK;
|
||||
if (ipv6Only) bridgeTypes = singletonList(MEEK);
|
||||
enableBridges = true;
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Using bridge type " + bridgeType);
|
||||
LOG.info("Using bridge types " + bridgeTypes);
|
||||
}
|
||||
} else {
|
||||
LOG.info("Not using bridges");
|
||||
@@ -898,7 +961,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
|
||||
try {
|
||||
if (enableNetwork) {
|
||||
enableBridges(enableBridges, bridgeType);
|
||||
enableBridges(enableBridges, bridgeTypes);
|
||||
enableConnectionPadding(enableConnectionPadding);
|
||||
useIpv6(ipv6Only);
|
||||
}
|
||||
@@ -910,12 +973,20 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
|
||||
private void enableConnectionPadding(boolean enable) throws IOException {
|
||||
controlConnection.setConf("ConnectionPadding", enable ? "1" : "0");
|
||||
try {
|
||||
controlConnection.setConf("ConnectionPadding", enable ? "1" : "0");
|
||||
} catch (TorNotRunningException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void useIpv6(boolean ipv6Only) throws IOException {
|
||||
controlConnection.setConf("ClientUseIPv4", ipv6Only ? "0" : "1");
|
||||
controlConnection.setConf("ClientUseIPv6", ipv6Only ? "1" : "0");
|
||||
try {
|
||||
controlConnection.setConf("ClientUseIPv4", ipv6Only ? "0" : "1");
|
||||
controlConnection.setConf("ClientUseIPv6", ipv6Only ? "1" : "0");
|
||||
} catch (TorNotRunningException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
@@ -938,11 +1009,15 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
@Nullable
|
||||
private ServerSocket serverSocket = null;
|
||||
|
||||
@GuardedBy("this")
|
||||
private int orConnectionsConnected = 0;
|
||||
|
||||
private synchronized void setStarted() {
|
||||
started = true;
|
||||
callback.pluginStateChanged(getState());
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
private synchronized boolean isTorRunning() {
|
||||
return started && !stopped;
|
||||
}
|
||||
@@ -961,11 +1036,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
callback.pluginStateChanged(getState());
|
||||
}
|
||||
|
||||
private synchronized boolean getAndSetCircuitBuilt() {
|
||||
boolean firstCircuit = !circuitBuilt;
|
||||
circuitBuilt = true;
|
||||
callback.pluginStateChanged(getState());
|
||||
return firstCircuit;
|
||||
private synchronized boolean getAndSetCircuitBuilt(boolean built) {
|
||||
boolean old = circuitBuilt;
|
||||
circuitBuilt = built;
|
||||
if (built != old) callback.pluginStateChanged(getState());
|
||||
return old;
|
||||
}
|
||||
|
||||
private synchronized void enableNetwork(boolean enable) {
|
||||
@@ -1000,11 +1075,39 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
if (reasonsDisabled != 0) return DISABLED;
|
||||
if (!networkInitialised) return ENABLING;
|
||||
if (!networkEnabled) return INACTIVE;
|
||||
return bootstrapped && circuitBuilt ? ACTIVE : ENABLING;
|
||||
return bootstrapped && circuitBuilt && orConnectionsConnected > 0
|
||||
? ACTIVE : ENABLING;
|
||||
}
|
||||
|
||||
private synchronized int getReasonsDisabled() {
|
||||
return getState() == DISABLED ? reasonsDisabled : 0;
|
||||
}
|
||||
|
||||
private synchronized void onOrConnectionConnected() {
|
||||
int oldConnected = orConnectionsConnected;
|
||||
orConnectionsConnected++;
|
||||
logOrConnections();
|
||||
if (oldConnected == 0) callback.pluginStateChanged(getState());
|
||||
}
|
||||
|
||||
private synchronized void onOrConnectionClosed() {
|
||||
int oldConnected = orConnectionsConnected;
|
||||
orConnectionsConnected--;
|
||||
if (orConnectionsConnected < 0) {
|
||||
LOG.warning("Count was zero before connection closed");
|
||||
orConnectionsConnected = 0;
|
||||
}
|
||||
logOrConnections();
|
||||
if (orConnectionsConnected == 0 && oldConnected != 0) {
|
||||
callback.pluginStateChanged(getState());
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("this")
|
||||
private void logOrConnections() {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info(orConnectionsConnected + " OR connections connected");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package org.briarproject.bramble.plugin.tor;
|
||||
|
||||
import org.briarproject.bramble.api.battery.BatteryManager;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.network.NetworkManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.Backoff;
|
||||
import org.briarproject.bramble.api.plugin.BackoffFactory;
|
||||
import org.briarproject.bramble.api.plugin.PluginCallback;
|
||||
import org.briarproject.bramble.api.plugin.TorConstants;
|
||||
import org.briarproject.bramble.api.plugin.TorControlPort;
|
||||
import org.briarproject.bramble.api.plugin.TorDirectory;
|
||||
import org.briarproject.bramble.api.plugin.TorSocksPort;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.system.LocationUtils;
|
||||
import org.briarproject.bramble.api.system.ResourceProvider;
|
||||
import org.briarproject.bramble.api.system.WakefulIoExecutor;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
abstract class TorPluginFactory implements DuplexPluginFactory {
|
||||
|
||||
protected static final Logger LOG =
|
||||
getLogger(TorPluginFactory.class.getName());
|
||||
|
||||
protected static final int MAX_LATENCY = 30 * 1000; // 30 seconds
|
||||
protected static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds
|
||||
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute
|
||||
private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins
|
||||
private static final double BACKOFF_BASE = 1.2;
|
||||
|
||||
protected final Executor ioExecutor, wakefulIoExecutor;
|
||||
protected final NetworkManager networkManager;
|
||||
protected final LocationUtils locationUtils;
|
||||
protected final EventBus eventBus;
|
||||
protected final SocketFactory torSocketFactory;
|
||||
protected final BackoffFactory backoffFactory;
|
||||
protected final ResourceProvider resourceProvider;
|
||||
protected final CircumventionProvider circumventionProvider;
|
||||
protected final BatteryManager batteryManager;
|
||||
protected final Clock clock;
|
||||
protected final CryptoComponent crypto;
|
||||
protected final File torDirectory;
|
||||
protected final int torSocksPort;
|
||||
protected final int torControlPort;
|
||||
|
||||
TorPluginFactory(@IoExecutor Executor ioExecutor,
|
||||
@WakefulIoExecutor Executor wakefulIoExecutor,
|
||||
NetworkManager networkManager,
|
||||
LocationUtils locationUtils,
|
||||
EventBus eventBus,
|
||||
SocketFactory torSocketFactory,
|
||||
BackoffFactory backoffFactory,
|
||||
ResourceProvider resourceProvider,
|
||||
CircumventionProvider circumventionProvider,
|
||||
BatteryManager batteryManager,
|
||||
Clock clock,
|
||||
CryptoComponent crypto,
|
||||
@TorDirectory File torDirectory,
|
||||
@TorSocksPort int torSocksPort,
|
||||
@TorControlPort int torControlPort) {
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.wakefulIoExecutor = wakefulIoExecutor;
|
||||
this.networkManager = networkManager;
|
||||
this.locationUtils = locationUtils;
|
||||
this.eventBus = eventBus;
|
||||
this.torSocketFactory = torSocketFactory;
|
||||
this.backoffFactory = backoffFactory;
|
||||
this.resourceProvider = resourceProvider;
|
||||
this.circumventionProvider = circumventionProvider;
|
||||
this.batteryManager = batteryManager;
|
||||
this.clock = clock;
|
||||
this.crypto = crypto;
|
||||
this.torDirectory = torDirectory;
|
||||
this.torSocksPort = torSocksPort;
|
||||
this.torControlPort = torControlPort;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
abstract String getArchitectureForTorBinary();
|
||||
|
||||
abstract TorPlugin createPluginInstance(Backoff backoff,
|
||||
TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback,
|
||||
String architecture);
|
||||
|
||||
@Override
|
||||
public TransportId getId() {
|
||||
return TorConstants.ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaxLatency() {
|
||||
return MAX_LATENCY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DuplexPlugin createPlugin(PluginCallback callback) {
|
||||
// Check that we have a Tor binary for this architecture
|
||||
String architecture = getArchitectureForTorBinary();
|
||||
if (architecture == null) {
|
||||
LOG.warning("Tor is not supported on this architecture");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("The selected architecture for Tor is " + architecture);
|
||||
}
|
||||
|
||||
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
|
||||
MAX_POLLING_INTERVAL, BACKOFF_BASE);
|
||||
TorRendezvousCrypto torRendezvousCrypto =
|
||||
new TorRendezvousCryptoImpl(crypto);
|
||||
TorPlugin plugin = createPluginInstance(backoff, torRendezvousCrypto,
|
||||
callback, architecture);
|
||||
eventBus.addListener(plugin);
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ interface TorRendezvousCrypto {
|
||||
|
||||
static final int SEED_BYTES = 32;
|
||||
|
||||
String getOnionAddress(byte[] seed);
|
||||
String getOnion(byte[] seed);
|
||||
|
||||
String getPrivateKeyBlob(byte[] seed);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user