mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 10:49:06 +01:00
Compare commits
339 Commits
alpha-1.4.
...
beta-1.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
559138c5b6 | ||
|
|
f90aef7767 | ||
|
|
ee417fc8d2 | ||
|
|
b424d6f98e | ||
|
|
32205ca6d3 | ||
|
|
dd3a9aa71b | ||
|
|
4e59836dd0 | ||
|
|
264b2ca2f3 | ||
|
|
23f5de66a8 | ||
|
|
79aa42c0f8 | ||
|
|
f00c3a47f5 | ||
|
|
04011e50bc | ||
|
|
7d20a844ff | ||
|
|
43581cc339 | ||
|
|
34815eb1a5 | ||
|
|
13d9e93758 | ||
|
|
98c1dca602 | ||
|
|
5ceba8f508 | ||
|
|
8e5ec347f2 | ||
|
|
f3afcb8469 | ||
|
|
3a317a9144 | ||
|
|
480a4b5901 | ||
|
|
6d9a241820 | ||
|
|
1c656d217c | ||
|
|
a503aa6ed2 | ||
|
|
85361b0099 | ||
|
|
4efdb7b75b | ||
|
|
787200d03f | ||
|
|
3ac05e4b88 | ||
|
|
7aafbdd715 | ||
|
|
617a6db84c | ||
|
|
2c295fb096 | ||
|
|
4af895d124 | ||
|
|
3cd388decd | ||
|
|
08551d16cd | ||
|
|
d905cb6cda | ||
|
|
bcc7a4b93b | ||
|
|
4fe9fa3315 | ||
|
|
079ef5b3c0 | ||
|
|
de76986ee4 | ||
|
|
96630e1b34 | ||
|
|
4eddf625d8 | ||
|
|
28ad66a03d | ||
|
|
0af371d026 | ||
|
|
a57c784b47 | ||
|
|
ab360e1e25 | ||
|
|
2aa39e43ef | ||
|
|
efb294de53 | ||
|
|
99755619c5 | ||
|
|
9990fb3b8f | ||
|
|
6d26db3d66 | ||
|
|
51301968a5 | ||
|
|
feb1c1b655 | ||
|
|
148f61a6b5 | ||
|
|
24d4debde0 | ||
|
|
a1f25c8101 | ||
|
|
62883b4bde | ||
|
|
42243f73f4 | ||
|
|
f4365330cb | ||
|
|
d3a06cf2c0 | ||
|
|
15d29f6189 | ||
|
|
339e4daded | ||
|
|
217a6dbf1c | ||
|
|
46352f664c | ||
|
|
dfcd626081 | ||
|
|
347895f6b2 | ||
|
|
7a6d075984 | ||
|
|
68ab3b0e97 | ||
|
|
16fc4f4527 | ||
|
|
8657216345 | ||
|
|
42e2926d61 | ||
|
|
a261b8e739 | ||
|
|
1699d6b5f8 | ||
|
|
848872a803 | ||
|
|
04ed3a652a | ||
|
|
d20457f338 | ||
|
|
ab29aacce0 | ||
|
|
46bb2b8ec2 | ||
|
|
6b6880c1ff | ||
|
|
5defd500ae | ||
|
|
7a888a6114 | ||
|
|
37ff06d192 | ||
|
|
85aa21ebf6 | ||
|
|
e448699895 | ||
|
|
200f83bcfe | ||
|
|
89cce89650 | ||
|
|
8982964fbf | ||
|
|
f3a3fa0ea8 | ||
|
|
0865a06ac8 | ||
|
|
f2738c8bc4 | ||
|
|
1321f8775e | ||
|
|
9764aba47d | ||
|
|
913e5da2f5 | ||
|
|
f2ce7a386b | ||
|
|
7607b65e82 | ||
|
|
c13c2d62f5 | ||
|
|
8ea7204cf6 | ||
|
|
6ec382cfc4 | ||
|
|
ad0b28a684 | ||
|
|
0ae94e9579 | ||
|
|
57bd5789d4 | ||
|
|
f7dde1250c | ||
|
|
13d96651b4 | ||
|
|
65029982ce | ||
|
|
380921ce25 | ||
|
|
87ee8cd653 | ||
|
|
d4810a6f71 | ||
|
|
aa56aba1a5 | ||
|
|
35438dbac1 | ||
|
|
543b1178a1 | ||
|
|
7f1071f5cd | ||
|
|
e8c694fe00 | ||
|
|
b58b0c74a9 | ||
|
|
9e5029917e | ||
|
|
12ca74f86a | ||
|
|
622683f45e | ||
|
|
a5563ead28 | ||
|
|
e15f49fde7 | ||
|
|
e66f92f27e | ||
|
|
44acda2045 | ||
|
|
afd92dd916 | ||
|
|
2a969f8e0b | ||
|
|
ddc6606ccf | ||
|
|
1531a24b2d | ||
|
|
2298818af5 | ||
|
|
a19a4f36c6 | ||
|
|
6765de992d | ||
|
|
0ae5361281 | ||
|
|
d8e26eebbe | ||
|
|
692e353046 | ||
|
|
b9ba7aded5 | ||
|
|
4bca9decc1 | ||
|
|
7bbe9068bb | ||
|
|
63060679a3 | ||
|
|
ddb759dbb8 | ||
|
|
592daf9c20 | ||
|
|
3922270db1 | ||
|
|
feb8854678 | ||
|
|
4ba4e41e69 | ||
|
|
1f699238a9 | ||
|
|
b8e91a12e8 | ||
|
|
06eb01ab0a | ||
|
|
d82509f3ce | ||
|
|
b01c306500 | ||
|
|
61e7635b9f | ||
|
|
f2f356cbd4 | ||
|
|
28f3ab1310 | ||
|
|
1af52b21d5 | ||
|
|
8bb3a83ccb | ||
|
|
a742b007ef | ||
|
|
6bfd7bcc4f | ||
|
|
17f5fc7518 | ||
|
|
8dcf988399 | ||
|
|
05bf3833cf | ||
|
|
c39c2ce124 | ||
|
|
0b93af5d71 | ||
|
|
f8e3579a92 | ||
|
|
54e434d812 | ||
|
|
13c3974f73 | ||
|
|
aeb2a370e1 | ||
|
|
0aff23a067 | ||
|
|
a2a2da0260 | ||
|
|
4d7a3bca62 | ||
|
|
91d5698fe9 | ||
|
|
7266c6ee6b | ||
|
|
06b539b911 | ||
|
|
486ba4a3fc | ||
|
|
7f987667fe | ||
|
|
8d22a0ffaf | ||
|
|
43d28608f5 | ||
|
|
c84d3f7707 | ||
|
|
2843e15905 | ||
|
|
a2fb388aa6 | ||
|
|
b7b253cf24 | ||
|
|
f05e9dd746 | ||
|
|
b24a18b231 | ||
|
|
e2a63ee361 | ||
|
|
ff9f706670 | ||
|
|
10ab60569b | ||
|
|
d77d1d67aa | ||
|
|
924425522a | ||
|
|
356e0ee07b | ||
|
|
8e83743dd7 | ||
|
|
61658655ff | ||
|
|
40086ffde2 | ||
|
|
1551142e98 | ||
|
|
1c6fb6491a | ||
|
|
cfd4e85e77 | ||
|
|
4d6abfabf7 | ||
|
|
a38933df66 | ||
|
|
6a91d18003 | ||
|
|
e481a02126 | ||
|
|
825dff27fc | ||
|
|
de3a87fff5 | ||
|
|
85d1addd04 | ||
|
|
4993873ae2 | ||
|
|
02b805ce42 | ||
|
|
1a6ba16a59 | ||
|
|
654a05df8a | ||
|
|
ffe1876337 | ||
|
|
98963955b1 | ||
|
|
d83efce002 | ||
|
|
efb1b8c1ad | ||
|
|
3f36db8b3a | ||
|
|
a2f4e70a48 | ||
|
|
01e72eff40 | ||
|
|
dbcea3e1d1 | ||
|
|
6288577daa | ||
|
|
5d363496bd | ||
|
|
75b5c92495 | ||
|
|
bcc98cc4c9 | ||
|
|
2d605089bc | ||
|
|
01f8be1b66 | ||
|
|
eac6d0aa40 | ||
|
|
713be403eb | ||
|
|
2fd948b81d | ||
|
|
62af5e858c | ||
|
|
2201585a34 | ||
|
|
97d11cc602 | ||
|
|
79f41064e4 | ||
|
|
9aacd9d3d8 | ||
|
|
78f4dee43d | ||
|
|
2b4a1cf54b | ||
|
|
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 | ||
|
|
8c33ea5a6b | ||
|
|
96228c1fd0 | ||
|
|
eb6a5fe63e | ||
|
|
7f581fee15 | ||
|
|
383056d37e | ||
|
|
23316f5e9c | ||
|
|
b36066514b | ||
|
|
f9403782a2 | ||
|
|
cc4978c2b1 | ||
|
|
97ba18cfb2 | ||
|
|
edd270abf3 | ||
|
|
47d412dd0a | ||
|
|
5d952ff68e | ||
|
|
9304a6b266 | ||
|
|
a99ec5ed51 | ||
|
|
47085722da |
@@ -98,7 +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
|
||||
timeout: 4h
|
||||
|
||||
mailbox integration test:
|
||||
extends: .optional_tests
|
||||
@@ -118,11 +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
|
||||
timeout: 3h
|
||||
only:
|
||||
- tags
|
||||
|
||||
10
CONTRIBUTING.md
Normal file
10
CONTRIBUTING.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Folder-Description:
|
||||
===================
|
||||
* `briar-*`: Specifically for the Briar app (Phone/Desktop/Headless)
|
||||
* `bramble-*`: The protocol stack - not necessarily Briar-dependent
|
||||
|
||||
---
|
||||
|
||||
* `*-api`: public stuff that can be referenced from other packages and modules - mostly interfaces + a few utility classes
|
||||
* `*-core`: implementations of api that are portable across Android/Desktop/Headless
|
||||
* `*-java`: implementations of api that are specific to Desktop & Headless
|
||||
@@ -15,8 +15,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
versionCode 10407
|
||||
versionName "1.4.7"
|
||||
versionCode 10412
|
||||
versionName "1.4.12"
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -42,8 +42,10 @@ configurations {
|
||||
|
||||
dependencies {
|
||||
implementation project(path: ':bramble-core', configuration: 'default')
|
||||
implementation 'androidx.annotation:annotation:1.5.0'
|
||||
tor "org.briarproject:tor-android:$tor_version"
|
||||
tor "org.briarproject:obfs4proxy-android:$obfs4proxy_version"
|
||||
tor "org.briarproject:snowflake-android:$snowflake_version"
|
||||
|
||||
annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
|
||||
|
||||
|
||||
@@ -18,3 +18,7 @@
|
||||
-dontnote com.google.common.**
|
||||
|
||||
-dontwarn com.fasterxml.jackson.databind.ext.Java7SupportImpl
|
||||
|
||||
# Keep all Jackson-serialisable classes and their members
|
||||
-keep interface com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
-keep @com.fasterxml.jackson.databind.annotation.JsonSerialize class * { *; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.briarproject.bramble;
|
||||
|
||||
import org.briarproject.bramble.battery.AndroidBatteryModule;
|
||||
import org.briarproject.bramble.io.DnsModule;
|
||||
import org.briarproject.bramble.network.AndroidNetworkModule;
|
||||
import org.briarproject.bramble.plugin.tor.CircumventionModule;
|
||||
import org.briarproject.bramble.reporting.ReportingModule;
|
||||
@@ -18,6 +19,7 @@ import dagger.Module;
|
||||
AndroidTaskSchedulerModule.class,
|
||||
AndroidWakefulIoExecutorModule.class,
|
||||
CircumventionModule.class,
|
||||
DnsModule.class,
|
||||
ReportingModule.class,
|
||||
SocksModule.class
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ 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;
|
||||
@@ -23,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;
|
||||
|
||||
@@ -31,6 +31,8 @@ import java.util.zip.ZipInputStream;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
import androidx.annotation.ChecksSdkIntAtLeast;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.logging.Level.INFO;
|
||||
@@ -45,13 +47,14 @@ class AndroidTorPlugin extends TorPlugin {
|
||||
|
||||
private static final String TOR_LIB_NAME = "libtor.so";
|
||||
private static final String OBFS4_LIB_NAME = "libobfs4proxy.so";
|
||||
private static final String SNOWFLAKE_LIB_NAME = "libsnowflake.so";
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(AndroidTorPlugin.class.getName());
|
||||
|
||||
private final Application app;
|
||||
private final AndroidWakeLock wakeLock;
|
||||
private final File torLib, obfs4Lib;
|
||||
private final File torLib, obfs4Lib, snowflakeLib;
|
||||
|
||||
AndroidTorPlugin(Executor ioExecutor,
|
||||
Executor wakefulIoExecutor,
|
||||
@@ -83,6 +86,7 @@ class AndroidTorPlugin extends TorPlugin {
|
||||
String nativeLibDir = app.getApplicationInfo().nativeLibraryDir;
|
||||
torLib = new File(nativeLibDir, TOR_LIB_NAME);
|
||||
obfs4Lib = new File(nativeLibDir, OBFS4_LIB_NAME);
|
||||
snowflakeLib = new File(nativeLibDir, SNOWFLAKE_LIB_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -108,6 +112,12 @@ class AndroidTorPlugin extends TorPlugin {
|
||||
if (!enable) wakeLock.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ChecksSdkIntAtLeast(api = 25)
|
||||
protected boolean canVerifyLetsEncryptCerts() {
|
||||
return SDK_INT >= 25;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
@@ -124,39 +134,43 @@ class AndroidTorPlugin extends TorPlugin {
|
||||
return obfs4Lib.exists() ? obfs4Lib : super.getObfs4ExecutableFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected File getSnowflakeExecutableFile() {
|
||||
return snowflakeLib.exists()
|
||||
? snowflakeLib : super.getSnowflakeExecutableFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void installTorExecutable() throws IOException {
|
||||
File extracted = super.getTorExecutableFile();
|
||||
if (torLib.exists()) {
|
||||
// If an older version left behind a Tor binary, delete it
|
||||
if (extracted.exists()) {
|
||||
if (extracted.delete()) LOG.info("Deleted Tor binary");
|
||||
else LOG.info("Failed to delete Tor binary");
|
||||
}
|
||||
} else if (SDK_INT < 29) {
|
||||
// The binary wasn't extracted at install time. Try to extract it
|
||||
extractLibraryFromApk(TOR_LIB_NAME, extracted);
|
||||
} else {
|
||||
// No point extracting the binary, we won't be allowed to execute it
|
||||
throw new FileNotFoundException(torLib.getAbsolutePath());
|
||||
}
|
||||
installExecutable(super.getTorExecutableFile(), torLib, TOR_LIB_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void installObfs4Executable() throws IOException {
|
||||
File extracted = super.getObfs4ExecutableFile();
|
||||
if (obfs4Lib.exists()) {
|
||||
// If an older version left behind an obfs4 binary, delete it
|
||||
installExecutable(super.getObfs4ExecutableFile(), obfs4Lib,
|
||||
OBFS4_LIB_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void installSnowflakeExecutable() throws IOException {
|
||||
installExecutable(super.getSnowflakeExecutableFile(), snowflakeLib,
|
||||
SNOWFLAKE_LIB_NAME);
|
||||
}
|
||||
|
||||
private void installExecutable(File extracted, File lib, String libName)
|
||||
throws IOException {
|
||||
if (lib.exists()) {
|
||||
// If an older version left behind a binary, delete it
|
||||
if (extracted.exists()) {
|
||||
if (extracted.delete()) LOG.info("Deleted obfs4 binary");
|
||||
else LOG.info("Failed to delete obfs4 binary");
|
||||
if (extracted.delete()) LOG.info("Deleted old binary");
|
||||
else LOG.info("Failed to delete old binary");
|
||||
}
|
||||
} else if (SDK_INT < 29) {
|
||||
// The binary wasn't extracted at install time. Try to extract it
|
||||
extractLibraryFromApk(OBFS4_LIB_NAME, extracted);
|
||||
extractLibraryFromApk(libName, extracted);
|
||||
} else {
|
||||
// No point extracting the binary, we won't be allowed to execute it
|
||||
throw new FileNotFoundException(obfs4Lib.getAbsolutePath());
|
||||
throw new FileNotFoundException(lib.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
dependencyVerification {
|
||||
verify = [
|
||||
'androidx.annotation:annotation:1.5.0:annotation-1.5.0.jar:261fb7c0210858500bab66d34354972a75166ab4182add283780b05513d6ec4a',
|
||||
'cglib:cglib:3.2.8:cglib-3.2.8.jar:3f64de999ecc5595dc84ca8ff0879d8a34c8623f9ef3c517a53ed59023fcb9db',
|
||||
'com.android.tools.analytics-library:protos:30.0.3:protos-30.0.3.jar:f62b89dcd9de719c6a7b7e15fb1dd20e45b57222e675cf633607bd0ed6bca7e7',
|
||||
'com.android.tools.analytics-library:shared:30.0.3:shared-30.0.3.jar:05aa9ba3cc890354108521fdf99802565aae5dd6ca44a6ac8bb8d594d1c1cd15',
|
||||
@@ -87,8 +88,9 @@ 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: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.briarproject:obfs4proxy-android:0.0.14:obfs4proxy-android-0.0.14.jar:ad9b1ee4757b05867a19e993147bbb018bddd1f26ce3da746d5f037d5991a8c8',
|
||||
'org.briarproject:snowflake-android:2.3.1:snowflake-android-2.3.1.jar:1f83c9a070f87b7074af13627709a8b5aced5460104be7166af736b1bb73c293',
|
||||
'org.briarproject:tor-android:0.4.5.14:tor-android-0.4.5.14.jar:7cf1beaa6c1db51fc8fac263aba9624ef289c3db29772509efcbc59f7057330a',
|
||||
'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',
|
||||
@@ -129,10 +131,12 @@ dependencyVerification {
|
||||
'org.jetbrains.kotlin:kotlin-reflect:1.4.32:kotlin-reflect-1.4.32.jar:dbf19e9cdaa9c3c170f3f6f6ce3922f38dfc1d7fa1cab5b7c23a19da8b5eec5b',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib-common:1.4.20:kotlin-stdlib-common-1.4.20.jar:a7112c9b3cefee418286c9c9372f7af992bd1e6e030691d52f60cb36dbec8320',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib-common:1.4.32:kotlin-stdlib-common-1.4.32.jar:e1ff6f55ee9e7591dcc633f7757bac25a7edb1cc7f738b37ec652f10f66a4145',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10:kotlin-stdlib-common-1.7.10.jar:19f102efe9629f8eabc63853ad15c533e47c47f91fca09285c5bde86e59f91d4',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.32:kotlin-stdlib-jdk7-1.4.32.jar:5f801e75ca27d8791c14b07943c608da27620d910a8093022af57f543d5d98b6',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32:kotlin-stdlib-jdk8-1.4.32.jar:adc43e54757b106e0cd7b3b7aa257dff471b61efdabe067fc02b2f57e2396262',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib:1.4.20:kotlin-stdlib-1.4.20.jar:b8ab1da5cdc89cb084d41e1f28f20a42bd431538642a5741c52bbfae3fa3e656',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib:1.4.32:kotlin-stdlib-1.4.32.jar:13e9fd3e69dc7230ce0fc873a92a4e5d521d179bcf1bef75a6705baac3bfecba',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib:1.7.10:kotlin-stdlib-1.7.10.jar:e771fe74250a943e8f6346713201ff1d8cb95c3a5d1a91a22b65a9e04f6a8901',
|
||||
'org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.1.0:kotlinx-metadata-jvm-0.1.0.jar:9753bb39efef35957c5c15df9a3cb769aabf2cdfa74b47afcb7760e5146be3b5',
|
||||
'org.jetbrains:annotations:13.0:annotations-13.0.jar:ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478',
|
||||
'org.jmock:jmock-imposters:2.12.0:jmock-imposters-2.12.0.jar:3b836269745a137c9b2347e8d7c2104845b126ef04f012d6bfd94f1a7dea7b09',
|
||||
|
||||
@@ -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, ","));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +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.MailboxPropertiesUpdate;
|
||||
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;
|
||||
@@ -19,10 +20,9 @@ import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface ClientHelper {
|
||||
|
||||
@@ -127,16 +127,17 @@ public interface ClientHelper {
|
||||
BdfDictionary properties) throws FormatException;
|
||||
|
||||
/**
|
||||
* Parse and validate the property dictionary of a Mailbox property update
|
||||
* message.
|
||||
* Parse and validate the elements of a Mailbox update message.
|
||||
*
|
||||
* @return the properties for using the Mailbox, or null if there is no
|
||||
* Mailbox available
|
||||
* @throws FormatException if the properties are not valid
|
||||
* @return the parsed update message
|
||||
* @throws FormatException if the message elements are invalid
|
||||
*/
|
||||
@Nullable
|
||||
MailboxPropertiesUpdate parseAndValidateMailboxPropertiesUpdate(
|
||||
BdfDictionary properties) throws FormatException;
|
||||
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
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.briarproject.bramble.api.plugin.TransportConnectionReader;
|
||||
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface ConnectionManager {
|
||||
@@ -16,6 +17,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.
|
||||
*/
|
||||
@@ -34,6 +46,14 @@ public interface ConnectionManager {
|
||||
void manageOutgoingConnection(ContactId c, TransportId t,
|
||||
TransportConnectionWriter w);
|
||||
|
||||
/**
|
||||
* Manages an outgoing connection to a contact via a mailbox. The IDs of
|
||||
* any messages sent or acked are added to the given
|
||||
* {@link OutgoingSessionRecord}.
|
||||
*/
|
||||
void manageOutgoingConnection(ContactId c, TransportId t,
|
||||
TransportConnectionWriter w, OutgoingSessionRecord sessionRecord);
|
||||
|
||||
/**
|
||||
* Manages an outgoing connection to a contact over a duplex transport.
|
||||
*/
|
||||
@@ -46,4 +66,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -119,16 +126,11 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
TransportKeys k) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if there are any acks or messages to send to the given
|
||||
* contact over a transport with the given maximum latency.
|
||||
* Returns true if there are any acks to send to the given contact.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*
|
||||
* @param eager True if messages that are not yet due for retransmission
|
||||
* should be included
|
||||
*/
|
||||
boolean containsAnythingToSend(Transaction txn, ContactId c,
|
||||
long maxLatency, boolean eager) throws DbException;
|
||||
boolean containsAcksToSend(Transaction txn, ContactId c) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if the database contains the given contact for the given
|
||||
@@ -154,6 +156,18 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
*/
|
||||
boolean containsIdentity(Transaction txn, AuthorId a) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if there are any messages to send to the given contact
|
||||
* over a transport with the given maximum latency.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*
|
||||
* @param eager True if messages that are not yet due for retransmission
|
||||
* should be included
|
||||
*/
|
||||
boolean containsMessagesToSend(Transaction txn, ContactId c,
|
||||
long maxLatency, boolean eager) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if the database contains the given pending contact.
|
||||
* <p/>
|
||||
@@ -193,26 +207,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 +235,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.
|
||||
@@ -279,6 +283,13 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
*/
|
||||
Group getGroup(Transaction txn, GroupId g) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the ID of the group containing the given message.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
GroupId getGroupId(Transaction txn, MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the metadata for the given group.
|
||||
* <p/>
|
||||
@@ -344,6 +355,30 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
|
||||
Metadata query) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the IDs of all messages received from the given contact that
|
||||
* need to be acknowledged.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Collection<MessageId> getMessagesToAck(Transaction txn, ContactId c)
|
||||
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,15 +495,32 @@ 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.
|
||||
* <p/>
|
||||
* Read-only if {@code markAsSent} is false.
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
@@ -498,15 +550,18 @@ public interface DatabaseComponent extends TransactionManager {
|
||||
*/
|
||||
long getNextCleanupDeadline(Transaction txn) throws DbException;
|
||||
|
||||
/*
|
||||
/**
|
||||
* Returns the next time (in milliseconds since the Unix epoch) when a
|
||||
* message is due to be sent to the given contact. The returned value may
|
||||
* be zero if a message is due to be sent immediately, or Long.MAX_VALUE if
|
||||
* no messages are scheduled to be sent.
|
||||
* message is due to be sent to the given contact over a transport with
|
||||
* the given latency.
|
||||
* <p>
|
||||
* The returned value may be zero if a message is due to be sent
|
||||
* immediately, or Long.MAX_VALUE if no messages are scheduled to be sent.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
long getNextSendTime(Transaction txn, ContactId c) throws DbException;
|
||||
long getNextSendTime(Transaction txn, ContactId c, long maxLatency)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the pending contact with the given ID.
|
||||
@@ -648,6 +703,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 +756,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,
|
||||
|
||||
@@ -37,8 +37,14 @@ public interface LifecycleManager {
|
||||
*/
|
||||
enum LifecycleState {
|
||||
|
||||
STARTING, MIGRATING_DATABASE, COMPACTING_DATABASE, STARTING_SERVICES,
|
||||
RUNNING, STOPPING;
|
||||
CREATED,
|
||||
STARTING,
|
||||
MIGRATING_DATABASE,
|
||||
COMPACTING_DATABASE,
|
||||
STARTING_SERVICES,
|
||||
RUNNING,
|
||||
STOPPING,
|
||||
STOPPED;
|
||||
|
||||
public boolean isAfter(LifecycleState state) {
|
||||
return ordinal() > state.ordinal();
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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.DAYS;
|
||||
import static java.util.concurrent.TimeUnit.HOURS;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_FRAME_LENGTH;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_PAYLOAD_LENGTH;
|
||||
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);
|
||||
|
||||
/**
|
||||
* The maximum latency of the mailbox transport in milliseconds.
|
||||
*/
|
||||
long MAX_LATENCY = DAYS.toMillis(14);
|
||||
}
|
||||
@@ -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,47 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
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;
|
||||
|
||||
@NotNullByDefault
|
||||
public 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.
|
||||
*/
|
||||
public 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a client and server with the given API versions can
|
||||
* communicate with each other (ie, have any major API versions in common).
|
||||
*/
|
||||
public static boolean isClientCompatibleWithServer(
|
||||
List<MailboxVersion> client, List<MailboxVersion> server) {
|
||||
int common = getHighestCommonMajorVersion(client, server);
|
||||
return common != API_CLIENT_TOO_OLD && common != API_SERVER_TOO_OLD;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.UniqueId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
@@ -32,7 +33,7 @@ public abstract class MailboxId extends UniqueId {
|
||||
}
|
||||
try {
|
||||
return fromHexString(token);
|
||||
} catch (IllegalArgumentException e) {
|
||||
} catch (FormatException e) {
|
||||
throw new InvalidMailboxIdException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -41,4 +43,14 @@ public interface MailboxManager {
|
||||
*/
|
||||
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,31 +1,58 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.NullSafety;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class MailboxProperties {
|
||||
|
||||
private final String baseUrl;
|
||||
private final String onion;
|
||||
private final MailboxAuthToken authToken;
|
||||
private final boolean owner;
|
||||
private final List<MailboxVersion> serverSupports;
|
||||
@Nullable
|
||||
private final MailboxFolderId inboxId; // Null for own mailbox
|
||||
@Nullable
|
||||
private final MailboxFolderId outboxId; // Null for own mailbox
|
||||
|
||||
public MailboxProperties(String baseUrl, MailboxAuthToken authToken,
|
||||
boolean owner) {
|
||||
this.baseUrl = baseUrl;
|
||||
/**
|
||||
* Constructor for properties used by the mailbox's owner.
|
||||
*/
|
||||
public MailboxProperties(String onion, MailboxAuthToken authToken,
|
||||
List<MailboxVersion> serverSupports) {
|
||||
this.onion = onion;
|
||||
this.authToken = authToken;
|
||||
this.owner = owner;
|
||||
this.owner = true;
|
||||
this.serverSupports = serverSupports;
|
||||
this.inboxId = null;
|
||||
this.outboxId = null;
|
||||
}
|
||||
|
||||
public String getBaseUrl() {
|
||||
return baseUrl;
|
||||
/**
|
||||
* Constructor for properties used by a contact of the mailbox's owner.
|
||||
*/
|
||||
public MailboxProperties(String onion, MailboxAuthToken authToken,
|
||||
List<MailboxVersion> serverSupports, MailboxFolderId inboxId,
|
||||
MailboxFolderId outboxId) {
|
||||
this.onion = onion;
|
||||
this.authToken = authToken;
|
||||
this.owner = false;
|
||||
this.serverSupports = serverSupports;
|
||||
this.inboxId = inboxId;
|
||||
this.outboxId = outboxId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the onion address of the mailbox, excluding the .onion suffix.
|
||||
*/
|
||||
public String getOnion() {
|
||||
return baseUrl.replaceFirst("^http://", "")
|
||||
.replaceFirst("\\.onion$", "");
|
||||
return onion;
|
||||
}
|
||||
|
||||
public MailboxAuthToken getAuthToken() {
|
||||
@@ -35,4 +62,37 @@ 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof MailboxProperties) {
|
||||
MailboxProperties m = (MailboxProperties) o;
|
||||
return owner == m.owner &&
|
||||
onion.equals(m.onion) &&
|
||||
authToken.equals(m.authToken) &&
|
||||
NullSafety.equals(inboxId, m.inboxId) &&
|
||||
NullSafety.equals(outboxId, m.outboxId) &&
|
||||
serverSupports.equals(m.serverSupports);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return authToken.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class MailboxPropertiesUpdate {
|
||||
|
||||
private final String onion;
|
||||
private final MailboxAuthToken authToken;
|
||||
private final MailboxFolderId inboxId;
|
||||
private final MailboxFolderId outboxId;
|
||||
|
||||
public MailboxPropertiesUpdate(String onion,
|
||||
MailboxAuthToken authToken, MailboxFolderId inboxId,
|
||||
MailboxFolderId outboxId) {
|
||||
this.onion = onion;
|
||||
this.authToken = authToken;
|
||||
this.inboxId = inboxId;
|
||||
this.outboxId = outboxId;
|
||||
}
|
||||
|
||||
public String getOnion() {
|
||||
return onion;
|
||||
}
|
||||
|
||||
public MailboxAuthToken getAuthToken() {
|
||||
return authToken;
|
||||
}
|
||||
|
||||
public MailboxFolderId getInboxId() {
|
||||
return inboxId;
|
||||
}
|
||||
|
||||
public MailboxFolderId getOutboxId() {
|
||||
return outboxId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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 MailboxPropertyManager {
|
||||
|
||||
/**
|
||||
* The unique ID of the mailbox property client.
|
||||
*/
|
||||
ClientId CLIENT_ID =
|
||||
new ClientId("org.briarproject.bramble.mailbox.properties");
|
||||
|
||||
/**
|
||||
* The current major version of the mailbox property client.
|
||||
*/
|
||||
int MAJOR_VERSION = 0;
|
||||
|
||||
/**
|
||||
* The current minor version of the mailbox property client.
|
||||
*/
|
||||
int MINOR_VERSION = 0;
|
||||
|
||||
/**
|
||||
* The number of properties required for a (non-empty) update message.
|
||||
*/
|
||||
int PROP_COUNT = 4;
|
||||
|
||||
/**
|
||||
* The required properties of a non-empty update message.
|
||||
*/
|
||||
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";
|
||||
|
||||
@Nullable
|
||||
MailboxPropertiesUpdate getLocalProperties(Transaction txn, ContactId c)
|
||||
throws DbException;
|
||||
|
||||
@Nullable
|
||||
MailboxPropertiesUpdate getRemoteProperties(Transaction txn, ContactId c)
|
||||
throws DbException;
|
||||
}
|
||||
@@ -7,6 +7,8 @@ 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
|
||||
@@ -26,10 +28,12 @@ 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)
|
||||
throws DbException;
|
||||
void recordSuccessfulConnection(Transaction txn, long now,
|
||||
List<MailboxVersion> versions) throws DbException;
|
||||
|
||||
void recordFailedConnectionAttempt(Transaction txn, long now)
|
||||
throws DbException;
|
||||
@@ -42,19 +46,28 @@ public interface MailboxSettingsManager {
|
||||
|
||||
interface MailboxHook {
|
||||
/**
|
||||
* Called when Briar is paired with a mailbox
|
||||
* 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)
|
||||
void mailboxPaired(Transaction txn, MailboxProperties p)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Called when the mailbox is unpaired
|
||||
* Called when the mailbox is unpaired.
|
||||
*
|
||||
* @param txn A read-write transaction
|
||||
*/
|
||||
void mailboxUnpaired(Transaction txn) throws DbException;
|
||||
|
||||
/**
|
||||
* Called when we receive our mailbox's server-supported API versions.
|
||||
* This happens whenever we successfully check the connectivity of
|
||||
* our mailbox, so this hook may be called frequently.
|
||||
*
|
||||
* @param txn A read-write transaction
|
||||
*/
|
||||
void serverSupportedVersionsReceived(Transaction txn,
|
||||
List<MailboxVersion> serverSupports) 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,28 @@ public class MailboxStatus {
|
||||
public int getAttemptsSinceSuccess() {
|
||||
return attemptsSinceSuccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mailbox's supported API versions.
|
||||
*/
|
||||
public List<MailboxVersion> getServerSupports() {
|
||||
return serverSupports;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,111 @@
|
||||
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.
|
||||
* <p>
|
||||
* The required properties are {@link #PROP_KEY_ONION},
|
||||
* {@link #PROP_KEY_AUTHTOKEN}, {@link #PROP_KEY_INBOXID} and
|
||||
* {@link #PROP_KEY_OUTBOXID}.
|
||||
*/
|
||||
int PROP_COUNT = 4;
|
||||
|
||||
/**
|
||||
* The onion address of the mailbox, excluding the .onion suffix.
|
||||
*/
|
||||
String PROP_KEY_ONION = "onion";
|
||||
|
||||
/**
|
||||
* A bearer token for accessing the mailbox (64 hex digits).
|
||||
*/
|
||||
String PROP_KEY_AUTHTOKEN = "authToken";
|
||||
|
||||
/**
|
||||
* A folder ID for downloading messages (64 hex digits).
|
||||
*/
|
||||
String PROP_KEY_INBOXID = "inboxId";
|
||||
|
||||
/**
|
||||
* A folder ID for uploading messages (64 hex digits).
|
||||
*/
|
||||
String PROP_KEY_OUTBOXID = "outboxId";
|
||||
|
||||
/**
|
||||
* Length of the {@link #PROP_KEY_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";
|
||||
|
||||
/**
|
||||
* Key in the client's local group for storing the serverSupports list that
|
||||
* was last sent out, if any.
|
||||
*/
|
||||
String GROUP_KEY_SENT_SERVER_SUPPORTS = "sentServerSupports";
|
||||
|
||||
/**
|
||||
* Returns the latest {@link MailboxUpdate} sent to the given contact.
|
||||
* <p>
|
||||
* If we have our own mailbox then the update will be a
|
||||
* {@link MailboxUpdateWithMailbox} containing the
|
||||
* {@link MailboxProperties} the contact should use for communicating with
|
||||
* our mailbox.
|
||||
*/
|
||||
MailboxUpdate getLocalUpdate(Transaction txn, ContactId c)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the latest {@link MailboxUpdate} received from the given
|
||||
* contact, or null if no update has been received.
|
||||
* <p>
|
||||
* If the contact has a mailbox then the update will be a
|
||||
* {@link MailboxUpdateWithMailbox} containing the
|
||||
* {@link MailboxProperties} we should use for communicating with the
|
||||
* contact's mailbox.
|
||||
*/
|
||||
@Nullable
|
||||
MailboxUpdate getRemoteUpdate(Transaction txn, ContactId c)
|
||||
throws DbException;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* An event that is broadcast when {@link MailboxPropertiesUpdate} are received
|
||||
* from a contact.
|
||||
*/
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class RemoteMailboxPropertiesUpdateEvent extends Event {
|
||||
|
||||
private final ContactId contactId;
|
||||
@Nullable
|
||||
private final MailboxPropertiesUpdate mailboxPropertiesUpdate;
|
||||
|
||||
public RemoteMailboxPropertiesUpdateEvent(ContactId contactId,
|
||||
@Nullable MailboxPropertiesUpdate mailboxPropertiesUpdate) {
|
||||
this.contactId = contactId;
|
||||
this.mailboxPropertiesUpdate = mailboxPropertiesUpdate;
|
||||
}
|
||||
|
||||
public ContactId getContact() {
|
||||
return contactId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public MailboxPropertiesUpdate getMailboxPropertiesUpdate() {
|
||||
return mailboxPropertiesUpdate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateWithMailbox;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* An event that is broadcast when a mailbox is paired.
|
||||
*/
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class MailboxPairedEvent extends Event {
|
||||
|
||||
private final MailboxProperties properties;
|
||||
private final Map<ContactId, MailboxUpdateWithMailbox> localUpdates;
|
||||
|
||||
public MailboxPairedEvent(MailboxProperties properties,
|
||||
Map<ContactId, MailboxUpdateWithMailbox> localUpdates) {
|
||||
this.properties = properties;
|
||||
this.localUpdates = localUpdates;
|
||||
}
|
||||
|
||||
public MailboxProperties getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
public Map<ContactId, MailboxUpdateWithMailbox> getLocalUpdates() {
|
||||
return localUpdates;
|
||||
}
|
||||
}
|
||||
@@ -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,28 @@
|
||||
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 java.util.Map;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* An event that is broadcast when a mailbox is unpaired.
|
||||
*/
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class MailboxUnpairedEvent extends Event {
|
||||
|
||||
private final Map<ContactId, MailboxUpdate> localUpdates;
|
||||
|
||||
public MailboxUnpairedEvent(Map<ContactId, MailboxUpdate> localUpdates) {
|
||||
this.localUpdates = localUpdates;
|
||||
}
|
||||
|
||||
public Map<ContactId, MailboxUpdate> getLocalUpdates() {
|
||||
return localUpdates;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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.mailbox.MailboxUpdateManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* An event that is broadcast when the first mailbox update is sent to a
|
||||
* newly added contact, which happens in the same transaction in which the
|
||||
* contact is added.
|
||||
* <p>
|
||||
* This event is not broadcast when the first mailbox update is sent to an
|
||||
* existing contact when setting up the
|
||||
* {@link MailboxUpdateManager mailbox update client}.
|
||||
*/
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class MailboxUpdateSentToNewContactEvent extends Event {
|
||||
|
||||
private final ContactId contactId;
|
||||
private final MailboxUpdate mailboxUpdate;
|
||||
|
||||
public MailboxUpdateSentToNewContactEvent(ContactId contactId,
|
||||
MailboxUpdate mailboxUpdate) {
|
||||
this.contactId = contactId;
|
||||
this.mailboxUpdate = mailboxUpdate;
|
||||
}
|
||||
|
||||
public ContactId getContactId() {
|
||||
return contactId;
|
||||
}
|
||||
|
||||
public MailboxUpdate getMailboxUpdate() {
|
||||
return mailboxUpdate;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.briarproject.bramble.api.mailbox;
|
||||
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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,6 @@ public interface RecordWriter {
|
||||
void flush() throws IOException;
|
||||
|
||||
void close() throws IOException;
|
||||
|
||||
long getBytesWritten();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.briarproject.bramble.api.sync;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* A container for holding the IDs of messages sent and acked during an
|
||||
* outgoing {@link SyncSession}, so they can be recorded in the DB as sent
|
||||
* or acked at some later time.
|
||||
*/
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
public class OutgoingSessionRecord {
|
||||
|
||||
private final Collection<MessageId> ackedIds = new CopyOnWriteArrayList<>();
|
||||
private final Collection<MessageId> sentIds = new CopyOnWriteArrayList<>();
|
||||
|
||||
public void onAckSent(Collection<MessageId> acked) {
|
||||
ackedIds.addAll(acked);
|
||||
}
|
||||
|
||||
public void onMessageSent(MessageId sent) {
|
||||
sentIds.add(sent);
|
||||
}
|
||||
|
||||
public Collection<MessageId> getAckedIds() {
|
||||
return ackedIds;
|
||||
}
|
||||
|
||||
public Collection<MessageId> getSentIds() {
|
||||
return sentIds;
|
||||
}
|
||||
}
|
||||
@@ -20,4 +20,6 @@ public interface SyncRecordWriter {
|
||||
void writePriority(Priority p) throws IOException;
|
||||
|
||||
void flush() throws IOException;
|
||||
|
||||
long getBytesWritten();
|
||||
}
|
||||
|
||||
@@ -12,12 +12,30 @@ import javax.annotation.Nullable;
|
||||
@NotNullByDefault
|
||||
public interface SyncSessionFactory {
|
||||
|
||||
/**
|
||||
* Creates a session for receiving data from a contact.
|
||||
*/
|
||||
SyncSession createIncomingSession(ContactId c, InputStream in,
|
||||
PriorityHandler handler);
|
||||
|
||||
/**
|
||||
* Creates a session for sending data to a contact over a simplex transport.
|
||||
*
|
||||
* @param eager True if messages should be sent eagerly, ie regardless of
|
||||
* whether they're due for retransmission.
|
||||
*/
|
||||
SyncSession createSimplexOutgoingSession(ContactId c, TransportId t,
|
||||
long maxLatency, boolean eager, StreamWriter streamWriter);
|
||||
|
||||
/**
|
||||
* Creates a session for sending data to a contact via a mailbox. The IDs
|
||||
* of any messages sent or acked will be added to the given
|
||||
* {@link OutgoingSessionRecord}.
|
||||
*/
|
||||
SyncSession createSimplexOutgoingSession(ContactId c, TransportId t,
|
||||
long maxLatency, StreamWriter streamWriter,
|
||||
OutgoingSessionRecord sessionRecord);
|
||||
|
||||
SyncSession createDuplexOutgoingSession(ContactId c, TransportId t,
|
||||
long maxLatency, int maxIdleTime, StreamWriter streamWriter,
|
||||
@Nullable Priority priority);
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.briarproject.bramble.api.sync.event;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Group.Visibility;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@@ -15,12 +16,19 @@ import javax.annotation.concurrent.Immutable;
|
||||
@NotNullByDefault
|
||||
public class GroupVisibilityUpdatedEvent extends Event {
|
||||
|
||||
private final Visibility visibility;
|
||||
private final Collection<ContactId> affected;
|
||||
|
||||
public GroupVisibilityUpdatedEvent(Collection<ContactId> affected) {
|
||||
public GroupVisibilityUpdatedEvent(Visibility visibility,
|
||||
Collection<ContactId> affected) {
|
||||
this.visibility = visibility;
|
||||
this.affected = affected;
|
||||
}
|
||||
|
||||
public Visibility getVisibility() {
|
||||
return visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contacts affected by the update.
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package org.briarproject.bramble.api.sync.event;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Group.Visibility;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
@@ -14,12 +19,32 @@ import javax.annotation.concurrent.Immutable;
|
||||
public class MessageSharedEvent extends Event {
|
||||
|
||||
private final MessageId messageId;
|
||||
private final GroupId groupId;
|
||||
private final Map<ContactId, Boolean> groupVisibility;
|
||||
|
||||
public MessageSharedEvent(MessageId message) {
|
||||
public MessageSharedEvent(MessageId message, GroupId groupId,
|
||||
Map<ContactId, Boolean> groupVisibility) {
|
||||
this.messageId = message;
|
||||
this.groupId = groupId;
|
||||
this.groupVisibility = groupVisibility;
|
||||
}
|
||||
|
||||
public MessageId getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public GroupId getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the IDs of all contacts for which the visibility of the
|
||||
* message's group is either {@link Visibility#SHARED shared} or
|
||||
* {@link Visibility#VISIBLE visible}. The value in the map is true if the
|
||||
* group is {@link Visibility#SHARED shared} or false if the group is
|
||||
* {@link Visibility#VISIBLE visible}.
|
||||
*/
|
||||
public Map<ContactId, Boolean> getGroupVisibility() {
|
||||
return groupVisibility;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public class IoUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private static void delete(File f) {
|
||||
public static void delete(File f) {
|
||||
if (!f.delete() && LOG.isLoggable(WARNING))
|
||||
LOG.warning("Could not delete " + f.getAbsolutePath());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.util;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
@@ -95,10 +96,10 @@ public class StringUtils {
|
||||
/**
|
||||
* Converts the given hex string to a byte array.
|
||||
*/
|
||||
public static byte[] fromHexString(String hex) {
|
||||
public static byte[] fromHexString(String hex) throws FormatException {
|
||||
int len = hex.length();
|
||||
if (len % 2 != 0)
|
||||
throw new IllegalArgumentException("Not a hex string");
|
||||
throw new FormatException();
|
||||
byte[] bytes = new byte[len / 2];
|
||||
for (int i = 0, j = 0; i < len; i += 2, j++) {
|
||||
int high = hexDigitToInt(hex.charAt(i));
|
||||
@@ -108,11 +109,11 @@ public class StringUtils {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static int hexDigitToInt(char c) {
|
||||
private static int hexDigitToInt(char c) throws FormatException {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
throw new IllegalArgumentException("Not a hex digit: " + c);
|
||||
throw new FormatException();
|
||||
}
|
||||
|
||||
public static String trim(String s) {
|
||||
@@ -130,13 +131,13 @@ public class StringUtils {
|
||||
return MAC.matcher(mac).matches();
|
||||
}
|
||||
|
||||
public static byte[] macToBytes(String mac) {
|
||||
if (!MAC.matcher(mac).matches()) throw new IllegalArgumentException();
|
||||
public static byte[] macToBytes(String mac) throws FormatException {
|
||||
if (!MAC.matcher(mac).matches()) throw new FormatException();
|
||||
return fromHexString(mac.replaceAll(":", ""));
|
||||
}
|
||||
|
||||
public static String macToString(byte[] mac) {
|
||||
if (mac.length != 6) throw new IllegalArgumentException();
|
||||
public static String macToString(byte[] mac) throws FormatException {
|
||||
if (mac.length != 6) throw new FormatException();
|
||||
StringBuilder s = new StringBuilder();
|
||||
for (byte b : mac) {
|
||||
if (s.length() > 0) s.append(':');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,7 +20,12 @@ 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.MailboxPropertiesUpdate;
|
||||
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;
|
||||
@@ -35,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;
|
||||
@@ -45,6 +51,7 @@ 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;
|
||||
@@ -221,6 +228,19 @@ public class TestUtils {
|
||||
getAgreementPublicKey(), verified);
|
||||
}
|
||||
|
||||
public static MailboxProperties getMailboxProperties(boolean owner,
|
||||
List<MailboxVersion> serverSupports) {
|
||||
String onion = getRandomString(56);
|
||||
MailboxAuthToken authToken = new MailboxAuthToken(getRandomId());
|
||||
if (owner) {
|
||||
return new MailboxProperties(onion, authToken, serverSupports);
|
||||
}
|
||||
MailboxFolderId inboxId = new MailboxFolderId(getRandomId());
|
||||
MailboxFolderId outboxId = new MailboxFolderId(getRandomId());
|
||||
return new MailboxProperties(onion, authToken, serverSupports,
|
||||
inboxId, outboxId);
|
||||
}
|
||||
|
||||
public static void writeBytes(File file, byte[] bytes)
|
||||
throws IOException {
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
@@ -273,22 +293,38 @@ 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 mailboxPropertiesUpdateEqual(
|
||||
@Nullable MailboxPropertiesUpdate a,
|
||||
@Nullable MailboxPropertiesUpdate b) {
|
||||
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.getInboxId().equals(b.getInboxId()) &&
|
||||
a.getOutboxId().equals(b.getOutboxId());
|
||||
a.isOwner() == b.isOwner() &&
|
||||
a.getServerSupports().equals(b.getServerSupports());
|
||||
}
|
||||
|
||||
public static boolean hasEvent(Transaction txn,
|
||||
@@ -301,4 +337,24 @@ public class TestUtils {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static <E extends Event> E getEvent(Transaction txn,
|
||||
Class<E> eventClass) {
|
||||
for (CommitAction action : txn.getActions()) {
|
||||
if (action instanceof EventAction) {
|
||||
Event event = ((EventAction) action).getEvent();
|
||||
if (eventClass.isInstance(event)) return eventClass.cast(event);
|
||||
}
|
||||
}
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public static boolean isCryptoStrengthUnlimited() {
|
||||
try {
|
||||
return Cipher.getMaxAllowedKeyLength("AES/CBC/PKCS5Padding")
|
||||
== Integer.MAX_VALUE;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.5'
|
||||
|
||||
//noinspection GradleDependency
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.account;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.account.AccountManager;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.DecryptionException;
|
||||
@@ -209,7 +210,13 @@ class AccountManagerImpl implements AccountManager {
|
||||
LOG.warning("Failed to load encrypted database key");
|
||||
throw new DecryptionException(INVALID_CIPHERTEXT);
|
||||
}
|
||||
byte[] ciphertext = fromHexString(hex);
|
||||
byte[] ciphertext;
|
||||
try {
|
||||
ciphertext = fromHexString(hex);
|
||||
} catch (FormatException e) {
|
||||
LOG.warning("Encrypted database key has invalid format");
|
||||
throw new DecryptionException(INVALID_CIPHERTEXT);
|
||||
}
|
||||
KeyStrengthener keyStrengthener = databaseConfig.getKeyStrengthener();
|
||||
byte[] plaintext = crypto.decryptWithPassword(ciphertext, password,
|
||||
keyStrengthener);
|
||||
|
||||
@@ -25,7 +25,10 @@ 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.MailboxPropertiesUpdate;
|
||||
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;
|
||||
@@ -39,25 +42,27 @@ 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;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.Collections.sort;
|
||||
import static org.briarproject.bramble.api.client.ContactGroupConstants.GROUP_KEY_CONTACT_ID;
|
||||
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_COUNT;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_KEY_AUTHTOKEN;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_KEY_INBOXID;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_KEY_ONION;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_KEY_OUTBOXID;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_ONION_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;
|
||||
@@ -412,11 +417,28 @@ class ClientHelperImpl implements ClientHelper {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public MailboxPropertiesUpdate parseAndValidateMailboxPropertiesUpdate(
|
||||
BdfDictionary properties) throws FormatException {
|
||||
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()) {
|
||||
return null;
|
||||
// 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) {
|
||||
@@ -435,9 +457,27 @@ class ClientHelperImpl implements ClientHelper {
|
||||
checkLength(inboxId, UniqueId.LENGTH);
|
||||
byte[] outboxId = properties.getRaw(PROP_KEY_OUTBOXID);
|
||||
checkLength(outboxId, UniqueId.LENGTH);
|
||||
return new MailboxPropertiesUpdate(onion,
|
||||
new MailboxAuthToken(authToken), new MailboxFolderId(inboxId),
|
||||
new MailboxFolderId(outboxId));
|
||||
MailboxProperties props = new MailboxProperties(onion,
|
||||
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()));
|
||||
}
|
||||
// Sort the list of versions for easier comparison
|
||||
sort(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.bramble.api.properties.TransportPropertyManager;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
import org.briarproject.bramble.api.sync.SyncSessionFactory;
|
||||
import org.briarproject.bramble.api.transport.KeyManager;
|
||||
import org.briarproject.bramble.api.transport.StreamReaderFactory;
|
||||
@@ -67,7 +68,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
|
||||
@@ -92,7 +101,16 @@ class ConnectionManagerImpl implements ConnectionManager {
|
||||
TransportConnectionWriter w) {
|
||||
ioExecutor.execute(new OutgoingSimplexSyncConnection(keyManager,
|
||||
connectionRegistry, streamReaderFactory, streamWriterFactory,
|
||||
syncSessionFactory, transportPropertyManager, c, t, w));
|
||||
syncSessionFactory, transportPropertyManager, c, t, w, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void manageOutgoingConnection(ContactId c, TransportId t,
|
||||
TransportConnectionWriter w, OutgoingSessionRecord sessionRecord) {
|
||||
ioExecutor.execute(new OutgoingSimplexSyncConnection(keyManager,
|
||||
connectionRegistry, streamReaderFactory, streamWriterFactory,
|
||||
syncSessionFactory, transportPropertyManager, c, t, w,
|
||||
sessionRecord));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.properties.TransportPropertyManager;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
import org.briarproject.bramble.api.sync.SyncSession;
|
||||
import org.briarproject.bramble.api.sync.SyncSessionFactory;
|
||||
import org.briarproject.bramble.api.transport.KeyManager;
|
||||
@@ -16,6 +17,8 @@ import org.briarproject.bramble.api.transport.StreamWriterFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
@@ -26,6 +29,8 @@ class OutgoingSimplexSyncConnection extends SyncConnection implements Runnable {
|
||||
private final ContactId contactId;
|
||||
private final TransportId transportId;
|
||||
private final TransportConnectionWriter writer;
|
||||
@Nullable
|
||||
private final OutgoingSessionRecord sessionRecord;
|
||||
|
||||
OutgoingSimplexSyncConnection(KeyManager keyManager,
|
||||
ConnectionRegistry connectionRegistry,
|
||||
@@ -34,13 +39,15 @@ class OutgoingSimplexSyncConnection extends SyncConnection implements Runnable {
|
||||
SyncSessionFactory syncSessionFactory,
|
||||
TransportPropertyManager transportPropertyManager,
|
||||
ContactId contactId, TransportId transportId,
|
||||
TransportConnectionWriter writer) {
|
||||
TransportConnectionWriter writer,
|
||||
@Nullable OutgoingSessionRecord sessionRecord) {
|
||||
super(keyManager, connectionRegistry, streamReaderFactory,
|
||||
streamWriterFactory, syncSessionFactory,
|
||||
transportPropertyManager);
|
||||
this.contactId = contactId;
|
||||
this.transportId = transportId;
|
||||
this.writer = writer;
|
||||
this.sessionRecord = sessionRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,10 +78,16 @@ class OutgoingSimplexSyncConnection extends SyncConnection implements Runnable {
|
||||
StreamWriter streamWriter = streamWriterFactory.createStreamWriter(
|
||||
w.getOutputStream(), ctx);
|
||||
ContactId c = requireNonNull(ctx.getContactId());
|
||||
// Use eager retransmission if the transport is lossy and cheap
|
||||
return syncSessionFactory.createSimplexOutgoingSession(c,
|
||||
ctx.getTransportId(), w.getMaxLatency(), w.isLossyAndCheap(),
|
||||
streamWriter);
|
||||
if (sessionRecord == null) {
|
||||
// Use eager retransmission if the transport is lossy and cheap
|
||||
return syncSessionFactory.createSimplexOutgoingSession(c,
|
||||
ctx.getTransportId(), w.getMaxLatency(),
|
||||
w.isLossyAndCheap(), streamWriter);
|
||||
} else {
|
||||
return syncSessionFactory.createSimplexOutgoingSession(c,
|
||||
ctx.getTransportId(), w.getMaxLatency(), streamWriter,
|
||||
sessionRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -163,16 +163,11 @@ interface Database<T> {
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if there are any acks or messages to send to the given
|
||||
* contact over a transport with the given maximum latency.
|
||||
* Returns true if there are any acks to send to the given contact.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*
|
||||
* @param eager True if messages that are not yet due for retransmission
|
||||
* should be included
|
||||
*/
|
||||
boolean containsAnythingToSend(T txn, ContactId c, long maxLatency,
|
||||
boolean eager) throws DbException;
|
||||
boolean containsAcksToSend(T txn, ContactId c) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if the database contains the given contact for the given
|
||||
@@ -212,6 +207,18 @@ interface Database<T> {
|
||||
*/
|
||||
boolean containsMessage(T txn, MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if there are any messages to send to the given
|
||||
* contact over a transport with the given maximum latency.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*
|
||||
* @param eager True if messages that are not yet due for retransmission
|
||||
* should be included
|
||||
*/
|
||||
boolean containsMessagesToSend(T txn, ContactId c, long maxLatency,
|
||||
boolean eager) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if the database contains the given pending contact.
|
||||
* <p/>
|
||||
@@ -313,6 +320,13 @@ interface Database<T> {
|
||||
*/
|
||||
Group getGroup(T txn, GroupId g) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the ID of the group containing the given message.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
GroupId getGroupId(T txn, MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the metadata for the given group.
|
||||
* <p/>
|
||||
@@ -338,8 +352,11 @@ interface Database<T> {
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the IDs of all contacts to which the given group's visibility is
|
||||
* either {@link Visibility VISIBLE} or {@link Visibility SHARED}.
|
||||
* Returns the IDs of all contacts for which the given group's visibility
|
||||
* is either {@link Visibility#SHARED shared} or
|
||||
* {@link Visibility#VISIBLE visible}. The value in the map is true if the
|
||||
* group is {@link Visibility#SHARED shared} or false if the group is
|
||||
* {@link Visibility#VISIBLE visible}.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
@@ -406,6 +423,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 +519,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 +528,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;
|
||||
|
||||
/**
|
||||
@@ -573,13 +597,16 @@ interface Database<T> {
|
||||
|
||||
/**
|
||||
* Returns the next time (in milliseconds since the Unix epoch) when a
|
||||
* message is due to be sent to the given contact. The returned value may
|
||||
* be zero if a message is due to be sent immediately, or Long.MAX_VALUE
|
||||
* if no messages are scheduled to be sent.
|
||||
* message is due to be sent to the given contact over a transport with
|
||||
* the given latency.
|
||||
* <p>
|
||||
* The returned value may be zero if a message is due to be sent
|
||||
* immediately, or Long.MAX_VALUE if no messages are scheduled to be sent.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
long getNextSendTime(T txn, ContactId c) throws DbException;
|
||||
long getNextSendTime(T txn, ContactId c, long maxLatency)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the pending contact with the given ID.
|
||||
@@ -598,13 +625,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.
|
||||
|
||||
@@ -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;
|
||||
@@ -287,7 +287,12 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
transaction.attach(new MessageAddedEvent(m, null));
|
||||
transaction.attach(new MessageStateChangedEvent(m.getId(), true,
|
||||
DELIVERED));
|
||||
if (shared) transaction.attach(new MessageSharedEvent(m.getId()));
|
||||
if (shared) {
|
||||
Map<ContactId, Boolean> visibility =
|
||||
db.getGroupVisibility(txn, m.getGroupId());
|
||||
transaction.attach(new MessageSharedEvent(m.getId(),
|
||||
m.getGroupId(), visibility));
|
||||
}
|
||||
}
|
||||
db.mergeMessageMetadata(txn, m.getId(), meta);
|
||||
}
|
||||
@@ -342,12 +347,12 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAnythingToSend(Transaction transaction, ContactId c,
|
||||
long maxLatency, boolean eager) throws DbException {
|
||||
public boolean containsAcksToSend(Transaction transaction, ContactId c)
|
||||
throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
return db.containsAnythingToSend(txn, c, maxLatency, eager);
|
||||
return db.containsAcksToSend(txn, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -373,6 +378,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
return db.containsIdentity(txn, a);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsMessagesToSend(Transaction transaction, ContactId c,
|
||||
long maxLatency, boolean eager) throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
return db.containsMessagesToSend(txn, c, maxLatency, eager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsPendingContact(Transaction transaction,
|
||||
PendingContactId p) throws DbException {
|
||||
@@ -424,13 +438,14 @@ 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) {
|
||||
@@ -439,38 +454,11 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
messages.add(message);
|
||||
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.updateRetransmissionData(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,
|
||||
@@ -505,13 +493,14 @@ 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) {
|
||||
@@ -520,7 +509,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
messages.add(message);
|
||||
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;
|
||||
@@ -567,6 +555,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
return db.getGroup(txn, g);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupId getGroupId(Transaction transaction, MessageId m)
|
||||
throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsMessage(txn, m))
|
||||
throw new NoSuchMessageException();
|
||||
return db.getGroupId(txn, m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Metadata getGroupMetadata(Transaction transaction, GroupId g)
|
||||
throws DbException {
|
||||
@@ -635,6 +632,24 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
return db.getMessageIds(txn, g, query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MessageId> getMessagesToAck(Transaction transaction,
|
||||
ContactId c) throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsContact(txn, c))
|
||||
throw new NoSuchContactException();
|
||||
return db.getMessagesToAck(txn, c, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@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 +755,31 @@ 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 (markAsSent && 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();
|
||||
@@ -794,10 +830,10 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextSendTime(Transaction transaction, ContactId c)
|
||||
throws DbException {
|
||||
public long getNextSendTime(Transaction transaction, ContactId c,
|
||||
long maxLatency) throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
return db.getNextSendTime(txn, c);
|
||||
return db.getNextSendTime(txn, c, maxLatency);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1005,7 +1041,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
db.getGroupVisibility(txn, id).keySet();
|
||||
db.removeGroup(txn, id);
|
||||
transaction.attach(new GroupRemovedEvent(g));
|
||||
transaction.attach(new GroupVisibilityUpdatedEvent(affected));
|
||||
transaction.attach(new GroupVisibilityUpdatedEvent(INVISIBLE,
|
||||
affected));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1069,6 +1106,16 @@ 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();
|
||||
db.lowerAckFlag(txn, c, acked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCleanupTimerDuration(Transaction transaction, MessageId m,
|
||||
long duration) throws DbException {
|
||||
@@ -1115,8 +1162,8 @@ 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);
|
||||
transaction.attach(new GroupVisibilityUpdatedEvent(affected));
|
||||
List<ContactId> affected = singletonList(c);
|
||||
transaction.attach(new GroupVisibilityUpdatedEvent(v, affected));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1149,7 +1196,9 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
if (db.getMessageState(txn, m) != DELIVERED)
|
||||
throw new IllegalArgumentException("Shared undelivered message");
|
||||
db.setMessageShared(txn, m, true);
|
||||
transaction.attach(new MessageSharedEvent(m));
|
||||
GroupId g = db.getGroupId(txn, m);
|
||||
Map<ContactId, Boolean> visibility = db.getGroupVisibility(txn, g);
|
||||
transaction.attach(new MessageSharedEvent(m, g, visibility));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1163,6 +1212,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)
|
||||
|
||||
@@ -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;
|
||||
@@ -102,6 +103,11 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
// Package access for testing
|
||||
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;
|
||||
private static final int OFFSET_CURR = 0;
|
||||
@@ -363,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;
|
||||
@@ -571,7 +577,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();
|
||||
}
|
||||
@@ -582,7 +589,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();
|
||||
}
|
||||
@@ -593,67 +607,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();
|
||||
}
|
||||
@@ -1109,8 +1147,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAnythingToSend(Connection txn, ContactId c,
|
||||
long maxLatency, boolean eager) throws DbException {
|
||||
public boolean containsAcksToSend(Connection txn, ContactId c)
|
||||
throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
@@ -1122,34 +1160,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
boolean acksToSend = rs.next();
|
||||
rs.close();
|
||||
ps.close();
|
||||
if (acksToSend) return true;
|
||||
if (eager) {
|
||||
sql = "SELECT NULL from statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
} else {
|
||||
long now = clock.currentTimeMillis();
|
||||
sql = "SELECT NULL FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE"
|
||||
+ " AND (expiry <= ? OR maxLatency IS NULL"
|
||||
+ " OR ? < maxLatency)";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
ps.setLong(3, now);
|
||||
ps.setLong(4, maxLatency);
|
||||
}
|
||||
rs = ps.executeQuery();
|
||||
boolean messagesToSend = rs.next();
|
||||
rs.close();
|
||||
ps.close();
|
||||
return messagesToSend;
|
||||
return acksToSend;
|
||||
} catch (SQLException e) {
|
||||
tryToClose(rs, LOG, WARNING);
|
||||
tryToClose(ps, LOG, WARNING);
|
||||
@@ -1269,6 +1280,46 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsMessagesToSend(Connection txn, ContactId c,
|
||||
long maxLatency, boolean eager) throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
if (eager) {
|
||||
String sql = "SELECT NULL from statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
} else {
|
||||
long now = clock.currentTimeMillis();
|
||||
String sql = "SELECT NULL FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE"
|
||||
+ " AND (expiry <= ? OR maxLatency IS NULL"
|
||||
+ " OR ? < maxLatency)";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
ps.setLong(3, now);
|
||||
ps.setLong(4, maxLatency);
|
||||
}
|
||||
rs = ps.executeQuery();
|
||||
boolean messagesToSend = rs.next();
|
||||
rs.close();
|
||||
ps.close();
|
||||
return messagesToSend;
|
||||
} catch (SQLException e) {
|
||||
tryToClose(rs, LOG, WARNING);
|
||||
tryToClose(ps, LOG, WARNING);
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsPendingContact(Connection txn, PendingContactId p)
|
||||
throws DbException {
|
||||
@@ -1632,6 +1683,27 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupId getGroupId(Connection txn, MessageId m) throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT groupId FROM messages WHERE messageId = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setBytes(1, m.getBytes());
|
||||
rs = ps.executeQuery();
|
||||
if (!rs.next()) throw new DbStateException();
|
||||
GroupId g = new GroupId(rs.getBytes(1));
|
||||
rs.close();
|
||||
ps.close();
|
||||
return g;
|
||||
} catch (SQLException e) {
|
||||
tryToClose(rs, LOG, WARNING);
|
||||
tryToClose(ps, LOG, WARNING);
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Group> getGroups(Connection txn, ClientId c,
|
||||
int majorVersion) throws DbException {
|
||||
@@ -1879,6 +1951,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 {
|
||||
@@ -2227,8 +2324,8 @@ 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();
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
@@ -2248,12 +2345,11 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
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();
|
||||
@@ -2266,12 +2362,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"
|
||||
@@ -2280,15 +2376,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);
|
||||
@@ -2401,6 +2493,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);
|
||||
@@ -2418,12 +2511,28 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextSendTime(Connection txn, ContactId c)
|
||||
public long getNextSendTime(Connection txn, ContactId c, long maxLatency)
|
||||
throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT expiry FROM statuses"
|
||||
// Are any messages sendable immediately?
|
||||
String sql = "SELECT NULL FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE"
|
||||
+ " AND (maxLatency IS NULL OR ? < maxLatency)";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
ps.setLong(3, maxLatency);
|
||||
rs = ps.executeQuery();
|
||||
boolean found = rs.next();
|
||||
rs.close();
|
||||
ps.close();
|
||||
if (found) return 0;
|
||||
// When is the earliest expiry time (could be in the past)?
|
||||
sql = "SELECT expiry FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE"
|
||||
@@ -2527,7 +2636,7 @@ 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();
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
@@ -2547,12 +2656,11 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
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();
|
||||
@@ -2706,6 +2814,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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.briarproject.briar.feed;
|
||||
package org.briarproject.bramble.io;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
@@ -1,4 +1,6 @@
|
||||
package org.briarproject.briar.feed;
|
||||
package org.briarproject.bramble.io;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
@@ -9,6 +11,7 @@ import javax.inject.Inject;
|
||||
|
||||
import okhttp3.Dns;
|
||||
|
||||
@NotNullByDefault
|
||||
class NoDns implements Dns {
|
||||
|
||||
private static final byte[] UNSPECIFIED_ADDRESS = new byte[4];
|
||||
@@ -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;
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
@@ -29,10 +29,12 @@ import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.CREATED;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPED;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.CLOCK_ERROR;
|
||||
@@ -60,12 +62,11 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
private final List<Service> services;
|
||||
private final List<OpenDatabaseHook> openDatabaseHooks;
|
||||
private final List<ExecutorService> executors;
|
||||
private final Semaphore startStopSemaphore = new Semaphore(1);
|
||||
private final CountDownLatch dbLatch = new CountDownLatch(1);
|
||||
private final CountDownLatch startupLatch = new CountDownLatch(1);
|
||||
private final CountDownLatch shutdownLatch = new CountDownLatch(1);
|
||||
|
||||
private volatile LifecycleState state = STARTING;
|
||||
private final AtomicReference<LifecycleState> state =
|
||||
new AtomicReference<>(CREATED);
|
||||
|
||||
@Inject
|
||||
LifecycleManagerImpl(DatabaseComponent db, EventBus eventBus,
|
||||
@@ -102,8 +103,8 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
|
||||
@Override
|
||||
public StartResult startServices(SecretKey dbKey) {
|
||||
if (!startStopSemaphore.tryAcquire()) {
|
||||
LOG.info("Already starting or stopping");
|
||||
if (!state.compareAndSet(CREATED, STARTING)) {
|
||||
LOG.warning("Already running");
|
||||
return ALREADY_RUNNING;
|
||||
}
|
||||
long now = clock.currentTimeMillis();
|
||||
@@ -135,7 +136,7 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
});
|
||||
|
||||
LOG.info("Starting services");
|
||||
state = STARTING_SERVICES;
|
||||
state.set(STARTING_SERVICES);
|
||||
dbLatch.countDown();
|
||||
eventBus.broadcast(new LifecycleEvent(STARTING_SERVICES));
|
||||
|
||||
@@ -148,7 +149,7 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
}
|
||||
}
|
||||
|
||||
state = RUNNING;
|
||||
state.set(RUNNING);
|
||||
startupLatch.countDown();
|
||||
eventBus.broadcast(new LifecycleEvent(RUNNING));
|
||||
return SUCCESS;
|
||||
@@ -164,59 +165,58 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
} catch (ServiceException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return SERVICE_ERROR;
|
||||
} finally {
|
||||
startStopSemaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDatabaseMigration() {
|
||||
state = MIGRATING_DATABASE;
|
||||
state.set(MIGRATING_DATABASE);
|
||||
eventBus.broadcast(new LifecycleEvent(MIGRATING_DATABASE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDatabaseCompaction() {
|
||||
state = COMPACTING_DATABASE;
|
||||
state.set(COMPACTING_DATABASE);
|
||||
eventBus.broadcast(new LifecycleEvent(COMPACTING_DATABASE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopServices() {
|
||||
try {
|
||||
startStopSemaphore.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
LOG.warning("Interrupted while waiting to stop services");
|
||||
if (!state.compareAndSet(RUNNING, STOPPING)) {
|
||||
LOG.warning("Not running");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
LOG.info("Stopping services");
|
||||
state = STOPPING;
|
||||
eventBus.broadcast(new LifecycleEvent(STOPPING));
|
||||
for (Service s : services) {
|
||||
LOG.info("Stopping services");
|
||||
eventBus.broadcast(new LifecycleEvent(STOPPING));
|
||||
for (Service s : services) {
|
||||
try {
|
||||
long start = now();
|
||||
s.stopService();
|
||||
if (LOG.isLoggable(FINE)) {
|
||||
logDuration(LOG, "Stopping service "
|
||||
+ s.getClass().getSimpleName(), start);
|
||||
}
|
||||
} catch (ServiceException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
for (ExecutorService e : executors) {
|
||||
if (LOG.isLoggable(FINE)) {
|
||||
LOG.fine("Stopping executor "
|
||||
+ e.getClass().getSimpleName());
|
||||
}
|
||||
e.shutdownNow();
|
||||
}
|
||||
for (ExecutorService e : executors) {
|
||||
if (LOG.isLoggable(FINE)) {
|
||||
LOG.fine("Stopping executor "
|
||||
+ e.getClass().getSimpleName());
|
||||
}
|
||||
e.shutdownNow();
|
||||
}
|
||||
try {
|
||||
long start = now();
|
||||
db.close();
|
||||
logDuration(LOG, "Closing database", start);
|
||||
shutdownLatch.countDown();
|
||||
} catch (DbException | ServiceException e) {
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
} finally {
|
||||
startStopSemaphore.release();
|
||||
}
|
||||
state.set(STOPPED);
|
||||
shutdownLatch.countDown();
|
||||
eventBus.broadcast(new LifecycleEvent(STOPPED));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -236,6 +236,6 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
|
||||
@Override
|
||||
public LifecycleState getLifecycleState() {
|
||||
return state;
|
||||
return state.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,43 @@
|
||||
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.
|
||||
* <p>
|
||||
* Observers are removed after being called, or when the checker is
|
||||
* {@link #destroy() destroyed}.
|
||||
*/
|
||||
void checkConnectivity(MailboxProperties properties,
|
||||
ConnectivityObserver o);
|
||||
|
||||
/**
|
||||
* Removes an observer that was added via
|
||||
* {@link #checkConnectivity(MailboxProperties, ConnectivityObserver)}. If
|
||||
* there are no remaining observers and a connectivity check is running
|
||||
* then the check will be cancelled.
|
||||
*/
|
||||
void removeObserver(ConnectivityObserver o);
|
||||
|
||||
interface ConnectivityObserver {
|
||||
void onConnectivityCheckSucceeded();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeObserver(ConnectivityObserver o) {
|
||||
synchronized (lock) {
|
||||
if (destroyed) return;
|
||||
connectivityObservers.remove(o);
|
||||
if (connectivityObservers.isEmpty() && connectivityCheck != null) {
|
||||
connectivityCheck.cancel();
|
||||
connectivityCheck = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class ContactMailboxClient implements MailboxClient {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(ContactMailboxClient.class.getName());
|
||||
|
||||
private final MailboxWorkerFactory workerFactory;
|
||||
private final ConnectivityChecker connectivityChecker;
|
||||
private final TorReachabilityMonitor reachabilityMonitor;
|
||||
private final Object lock = new Object();
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private MailboxWorker uploadWorker = null, downloadWorker = null;
|
||||
|
||||
ContactMailboxClient(MailboxWorkerFactory workerFactory,
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor reachabilityMonitor) {
|
||||
this.workerFactory = workerFactory;
|
||||
this.connectivityChecker = connectivityChecker;
|
||||
this.reachabilityMonitor = reachabilityMonitor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
LOG.info("Started");
|
||||
// Nothing to do until contact is assigned
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
LOG.info("Destroyed");
|
||||
MailboxWorker uploadWorker, downloadWorker;
|
||||
synchronized (lock) {
|
||||
uploadWorker = this.uploadWorker;
|
||||
this.uploadWorker = null;
|
||||
downloadWorker = this.downloadWorker;
|
||||
this.downloadWorker = null;
|
||||
}
|
||||
if (uploadWorker != null) uploadWorker.destroy();
|
||||
if (downloadWorker != null) downloadWorker.destroy();
|
||||
// The connectivity checker belongs to the client, so it should be
|
||||
// destroyed. The Tor reachability monitor is shared between clients,
|
||||
// so it should not be destroyed
|
||||
connectivityChecker.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assignContactForUpload(ContactId contactId,
|
||||
MailboxProperties properties, MailboxFolderId folderId) {
|
||||
LOG.info("Contact assigned for upload");
|
||||
if (properties.isOwner()) throw new IllegalArgumentException();
|
||||
// For a contact's mailbox we should always be uploading to the outbox
|
||||
// assigned to us by the contact
|
||||
if (!folderId.equals(properties.getOutboxId())) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
MailboxWorker uploadWorker = workerFactory.createUploadWorker(
|
||||
connectivityChecker, properties, folderId, contactId);
|
||||
synchronized (lock) {
|
||||
if (this.uploadWorker != null) throw new IllegalStateException();
|
||||
this.uploadWorker = uploadWorker;
|
||||
}
|
||||
uploadWorker.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deassignContactForUpload(ContactId contactId) {
|
||||
LOG.info("Contact deassigned for upload");
|
||||
MailboxWorker uploadWorker;
|
||||
synchronized (lock) {
|
||||
uploadWorker = this.uploadWorker;
|
||||
this.uploadWorker = null;
|
||||
}
|
||||
if (uploadWorker != null) uploadWorker.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assignContactForDownload(ContactId contactId,
|
||||
MailboxProperties properties, MailboxFolderId folderId) {
|
||||
LOG.info("Contact assigned for download");
|
||||
if (properties.isOwner()) throw new IllegalArgumentException();
|
||||
// For a contact's mailbox we should always be downloading from the
|
||||
// inbox assigned to us by the contact
|
||||
if (!folderId.equals(properties.getInboxId())) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
MailboxWorker downloadWorker =
|
||||
workerFactory.createDownloadWorkerForContactMailbox(
|
||||
connectivityChecker, reachabilityMonitor, properties);
|
||||
synchronized (lock) {
|
||||
if (this.downloadWorker != null) throw new IllegalStateException();
|
||||
this.downloadWorker = downloadWorker;
|
||||
}
|
||||
downloadWorker.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deassignContactForDownload(ContactId contactId) {
|
||||
LOG.info("Contact deassigned for download");
|
||||
MailboxWorker downloadWorker;
|
||||
synchronized (lock) {
|
||||
downloadWorker = this.downloadWorker;
|
||||
this.downloadWorker = null;
|
||||
}
|
||||
if (downloadWorker != null) downloadWorker.destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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.util.logging.Logger;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class ContactMailboxConnectivityChecker extends ConnectivityCheckerImpl {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(ContactMailboxConnectivityChecker.class.getName());
|
||||
|
||||
private final MailboxApi mailboxApi;
|
||||
|
||||
@Inject
|
||||
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(() -> {
|
||||
LOG.info("Checking connectivity of contact's mailbox");
|
||||
if (!mailboxApi.checkStatus(properties)) throw new ApiException();
|
||||
LOG.info("Contact's mailbox is reachable");
|
||||
// Call the observers and cache the result
|
||||
onConnectivityCheckSucceeded(clock.currentTimeMillis());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class ContactMailboxDownloadWorker extends MailboxDownloadWorker {
|
||||
|
||||
ContactMailboxDownloadWorker(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor torReachabilityMonitor,
|
||||
MailboxApiCaller mailboxApiCaller,
|
||||
MailboxApi mailboxApi,
|
||||
MailboxFileManager mailboxFileManager,
|
||||
MailboxProperties mailboxProperties) {
|
||||
super(connectivityChecker, torReachabilityMonitor, mailboxApiCaller,
|
||||
mailboxApi, mailboxFileManager, mailboxProperties);
|
||||
if (mailboxProperties.isOwner()) throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ApiCall createApiCallForDownloadCycle() {
|
||||
return new SimpleApiCall(this::apiCallListInbox);
|
||||
}
|
||||
|
||||
private void apiCallListInbox() throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
}
|
||||
LOG.info("Listing inbox");
|
||||
MailboxFolderId folderId =
|
||||
requireNonNull(mailboxProperties.getInboxId());
|
||||
List<MailboxFile> files;
|
||||
try {
|
||||
files = mailboxApi.getFiles(mailboxProperties, folderId);
|
||||
} catch (TolerableFailureException e) {
|
||||
LOG.warning("Inbox folder does not exist");
|
||||
files = emptyList();
|
||||
}
|
||||
if (files.isEmpty()) {
|
||||
onDownloadCycleFinished();
|
||||
} else {
|
||||
Queue<FolderFile> queue = new LinkedList<>();
|
||||
for (MailboxFile file : files) {
|
||||
queue.add(new FolderFile(folderId, file.name));
|
||||
}
|
||||
downloadNextFile(queue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -85,9 +91,13 @@ interface MailboxApi {
|
||||
* Used by owner and contacts to list their files to retrieve.
|
||||
* <p>
|
||||
* Returns 200 OK with the list of files in JSON.
|
||||
*
|
||||
* @throws TolerableFailureException if response code is 404 (folder does
|
||||
* not exist or client is not authorised to download from it)
|
||||
*/
|
||||
List<MailboxFile> getFiles(MailboxProperties properties,
|
||||
MailboxFolderId folderId) throws IOException, ApiException;
|
||||
MailboxFolderId folderId)
|
||||
throws IOException, ApiException, TolerableFailureException;
|
||||
|
||||
/**
|
||||
* Used by owner and contacts to retrieve a file.
|
||||
@@ -96,17 +106,22 @@ interface MailboxApi {
|
||||
* in the response body.
|
||||
*
|
||||
* @param file the empty file the response bytes will be written into.
|
||||
* @throws TolerableFailureException if response code is 404 (folder does
|
||||
* not exist, client is not authorised to download from folder, or file
|
||||
* does not exist)
|
||||
*/
|
||||
void getFile(MailboxProperties properties, MailboxFolderId folderId,
|
||||
MailboxFileId fileId, File file) throws IOException, ApiException;
|
||||
MailboxFileId fileId, File file)
|
||||
throws IOException, ApiException, TolerableFailureException;
|
||||
|
||||
/**
|
||||
* Used by owner and contacts to delete files.
|
||||
* <p>
|
||||
* Returns 200 OK (no exception) if deletion was successful.
|
||||
*
|
||||
* @throws TolerableFailureException on 404 response,
|
||||
* because file was most likely deleted already.
|
||||
* @throws TolerableFailureException if response code is 404 (folder does
|
||||
* not exist, client is not authorised to download from folder, or file
|
||||
* does not exist)
|
||||
*/
|
||||
void deleteFile(MailboxProperties properties, MailboxFolderId folderId,
|
||||
MailboxFileId fileId)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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.
|
||||
* <p>
|
||||
* This method is safe to call while holding a lock.
|
||||
*
|
||||
* @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;
|
||||
@@ -21,7 +22,6 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -34,6 +34,7 @@ import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
import static com.fasterxml.jackson.databind.MapperFeature.BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES;
|
||||
import static java.util.Collections.sort;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static okhttp3.internal.Util.EMPTY_REQUEST;
|
||||
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
|
||||
@@ -41,26 +42,47 @@ import static org.briarproject.bramble.util.IoUtils.copyAndClose;
|
||||
@NotNullByDefault
|
||||
class MailboxApiImpl implements MailboxApi {
|
||||
|
||||
private final WeakSingletonProvider<OkHttpClient> httpClientProvider;
|
||||
private final JsonMapper mapper = JsonMapper.builder()
|
||||
.enable(BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES)
|
||||
.build();
|
||||
private static final MediaType JSON =
|
||||
requireNonNull(MediaType.parse("application/json; charset=utf-8"));
|
||||
private static final MediaType FILE =
|
||||
requireNonNull(MediaType.parse("application/octet-stream"));
|
||||
|
||||
private final WeakSingletonProvider<OkHttpClient> httpClientProvider;
|
||||
private final JsonMapper mapper = JsonMapper.builder()
|
||||
.enable(BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES)
|
||||
.build();
|
||||
private final UrlConverter urlConverter;
|
||||
|
||||
@Inject
|
||||
MailboxApiImpl(WeakSingletonProvider<OkHttpClient> httpClientProvider) {
|
||||
MailboxApiImpl(WeakSingletonProvider<OkHttpClient> httpClientProvider,
|
||||
UrlConverter urlConverter) {
|
||||
this.httpClientProvider = httpClientProvider;
|
||||
this.urlConverter = urlConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
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())
|
||||
.url(properties.getBaseUrl() + "/setup")
|
||||
.url(getBaseUrl(properties) + "/setup")
|
||||
.put(EMPTY_REQUEST)
|
||||
.build();
|
||||
OkHttpClient client = httpClientProvider.get();
|
||||
@@ -75,17 +97,42 @@ class MailboxApiImpl implements MailboxApi {
|
||||
if (tokenNode == null) {
|
||||
throw new ApiException();
|
||||
}
|
||||
String ownerToken = tokenNode.textValue();
|
||||
return MailboxAuthToken.fromString(ownerToken);
|
||||
return new MailboxProperties(properties.getOnion(),
|
||||
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));
|
||||
}
|
||||
// Sort the list of versions for easier comparison
|
||||
sort(serverSupports);
|
||||
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();
|
||||
@@ -96,7 +143,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
throws IOException, ApiException {
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
Request request = getRequestBuilder(properties.getAuthToken())
|
||||
.url(properties.getBaseUrl() + "/")
|
||||
.url(getBaseUrl(properties) + "/")
|
||||
.delete()
|
||||
.build();
|
||||
OkHttpClient client = httpClientProvider.get();
|
||||
@@ -121,7 +168,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
public void deleteContact(MailboxProperties properties, ContactId contactId)
|
||||
throws IOException, ApiException, TolerableFailureException {
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
String url = properties.getBaseUrl() + "/contacts/" +
|
||||
String url = getBaseUrl(properties) + "/contacts/" +
|
||||
contactId.getInt();
|
||||
Request request = getRequestBuilder(properties.getAuthToken())
|
||||
.delete()
|
||||
@@ -171,9 +218,11 @@ class MailboxApiImpl implements MailboxApi {
|
||||
|
||||
@Override
|
||||
public List<MailboxFile> getFiles(MailboxProperties properties,
|
||||
MailboxFolderId folderId) throws IOException, ApiException {
|
||||
MailboxFolderId folderId)
|
||||
throws IOException, ApiException, TolerableFailureException {
|
||||
String path = "/files/" + folderId;
|
||||
Response response = sendGetRequest(properties, path);
|
||||
if (response.code() == 404) throw new TolerableFailureException();
|
||||
if (response.code() != 200) throw new ApiException();
|
||||
|
||||
ResponseBody body = response.body();
|
||||
@@ -198,7 +247,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
if (time < 1) throw new ApiException();
|
||||
list.add(new MailboxFile(MailboxFileId.fromString(name), time));
|
||||
}
|
||||
Collections.sort(list);
|
||||
sort(list);
|
||||
return list;
|
||||
} catch (JacksonException | InvalidMailboxIdException e) {
|
||||
throw new ApiException();
|
||||
@@ -207,9 +256,11 @@ class MailboxApiImpl implements MailboxApi {
|
||||
|
||||
@Override
|
||||
public void getFile(MailboxProperties properties, MailboxFolderId folderId,
|
||||
MailboxFileId fileId, File file) throws IOException, ApiException {
|
||||
MailboxFileId fileId, File file)
|
||||
throws IOException, ApiException, TolerableFailureException {
|
||||
String path = "/files/" + folderId + "/" + fileId;
|
||||
Response response = sendGetRequest(properties, path);
|
||||
if (response.code() == 404) throw new TolerableFailureException();
|
||||
if (response.code() != 200) throw new ApiException();
|
||||
|
||||
ResponseBody body = response.body();
|
||||
@@ -225,7 +276,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
String path = "/files/" + folderId + "/" + fileId;
|
||||
Request request = getRequestBuilder(properties.getAuthToken())
|
||||
.delete()
|
||||
.url(properties.getBaseUrl() + path)
|
||||
.url(getBaseUrl(properties) + path)
|
||||
.build();
|
||||
OkHttpClient client = httpClientProvider.get();
|
||||
Response response = client.newCall(request).execute();
|
||||
@@ -267,7 +318,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
private Response sendGetRequest(MailboxProperties properties, String path)
|
||||
throws IOException {
|
||||
Request request = getRequestBuilder(properties.getAuthToken())
|
||||
.url(properties.getBaseUrl() + path)
|
||||
.url(getBaseUrl(properties) + path)
|
||||
.build();
|
||||
OkHttpClient client = httpClientProvider.get();
|
||||
return client.newCall(request).execute();
|
||||
@@ -276,7 +327,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
private Response sendPostRequest(MailboxProperties properties, String path,
|
||||
RequestBody body) throws IOException {
|
||||
Request request = getRequestBuilder(properties.getAuthToken())
|
||||
.url(properties.getBaseUrl() + path)
|
||||
.url(getBaseUrl(properties) + path)
|
||||
.post(body)
|
||||
.build();
|
||||
OkHttpClient client = httpClientProvider.get();
|
||||
@@ -298,4 +349,7 @@ class MailboxApiImpl implements MailboxApi {
|
||||
return (ArrayNode) arrayNode;
|
||||
}
|
||||
|
||||
private String getBaseUrl(MailboxProperties properties) {
|
||||
return urlConverter.convertOnionToBaseUrl(properties.getOnion());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
interface MailboxClient {
|
||||
|
||||
/**
|
||||
* Asynchronously starts the client.
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Destroys the client and its workers, cancelling any pending tasks or
|
||||
* retries.
|
||||
*/
|
||||
void destroy();
|
||||
|
||||
/**
|
||||
* Assigns a contact to the client for upload.
|
||||
*
|
||||
* @param properties Properties for communicating with the mailbox
|
||||
* managed by this client.
|
||||
* @param folderId The ID of the folder to which files will be uploaded.
|
||||
*/
|
||||
void assignContactForUpload(ContactId c, MailboxProperties properties,
|
||||
MailboxFolderId folderId);
|
||||
|
||||
/**
|
||||
* Deassigns a contact from the client for upload.
|
||||
*/
|
||||
void deassignContactForUpload(ContactId c);
|
||||
|
||||
/**
|
||||
* Assigns a contact to the client for download.
|
||||
*
|
||||
* @param properties Properties for communicating with the mailbox
|
||||
* managed by this client.
|
||||
* @param folderId The ID of the folder from which files will be
|
||||
* downloaded.
|
||||
*/
|
||||
void assignContactForDownload(ContactId c, MailboxProperties properties,
|
||||
MailboxFolderId folderId);
|
||||
|
||||
/**
|
||||
* Deassigns a contact from the client for download.
|
||||
*/
|
||||
void deassignContactForDownload(ContactId c);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
@NotNullByDefault
|
||||
interface MailboxClientFactory {
|
||||
|
||||
/**
|
||||
* Creates a client for communicating with a contact's mailbox.
|
||||
*/
|
||||
MailboxClient createContactMailboxClient(
|
||||
TorReachabilityMonitor reachabilityMonitor);
|
||||
|
||||
/**
|
||||
* Creates a client for communicating with our own mailbox.
|
||||
*/
|
||||
MailboxClient createOwnMailboxClient(
|
||||
TorReachabilityMonitor reachabilityMonitor,
|
||||
MailboxProperties properties);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
@NotNullByDefault
|
||||
class MailboxClientFactoryImpl implements MailboxClientFactory {
|
||||
|
||||
private final MailboxWorkerFactory workerFactory;
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final Executor ioExecutor;
|
||||
private final Provider<ContactMailboxConnectivityChecker>
|
||||
contactCheckerProvider;
|
||||
private final Provider<OwnMailboxConnectivityChecker> ownCheckerProvider;
|
||||
|
||||
@Inject
|
||||
MailboxClientFactoryImpl(MailboxWorkerFactory workerFactory,
|
||||
TaskScheduler taskScheduler,
|
||||
@IoExecutor Executor ioExecutor,
|
||||
Provider<ContactMailboxConnectivityChecker> contactCheckerProvider,
|
||||
Provider<OwnMailboxConnectivityChecker> ownCheckerProvider) {
|
||||
this.workerFactory = workerFactory;
|
||||
this.taskScheduler = taskScheduler;
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.contactCheckerProvider = contactCheckerProvider;
|
||||
this.ownCheckerProvider = ownCheckerProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxClient createContactMailboxClient(
|
||||
TorReachabilityMonitor reachabilityMonitor) {
|
||||
ConnectivityChecker connectivityChecker = contactCheckerProvider.get();
|
||||
return new ContactMailboxClient(workerFactory, connectivityChecker,
|
||||
reachabilityMonitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxClient createOwnMailboxClient(
|
||||
TorReachabilityMonitor reachabilityMonitor,
|
||||
MailboxProperties properties) {
|
||||
ConnectivityChecker connectivityChecker = ownCheckerProvider.get();
|
||||
return new OwnMailboxClient(workerFactory, connectivityChecker,
|
||||
reachabilityMonitor, taskScheduler, ioExecutor, properties);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,667 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventExecutor;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.Service;
|
||||
import org.briarproject.bramble.api.lifecycle.ServiceException;
|
||||
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.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.MailboxPairedEvent;
|
||||
import org.briarproject.bramble.api.mailbox.event.MailboxUnpairedEvent;
|
||||
import org.briarproject.bramble.api.mailbox.event.MailboxUpdateSentToNewContactEvent;
|
||||
import org.briarproject.bramble.api.mailbox.event.OwnMailboxConnectionStatusEvent;
|
||||
import org.briarproject.bramble.api.mailbox.event.RemoteMailboxUpdateEvent;
|
||||
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 java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
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.mailbox.MailboxConstants.CLIENT_SUPPORTS;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxHelper.isClientCompatibleWithServer;
|
||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.ID;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
/**
|
||||
* This component manages a {@link MailboxClient} for each mailbox we know
|
||||
* about and are able to use (our own mailbox and/or contacts' mailboxes).
|
||||
* The clients are created when we come online (i.e. when the Tor plugin
|
||||
* becomes {@link Plugin.State#ACTIVE active}) and destroyed when we go
|
||||
* offline.
|
||||
* <p/>
|
||||
* The manager keeps track of the latest {@link MailboxUpdate} sent to and
|
||||
* received from each contact. These updates are used to decide which
|
||||
* mailboxes the manager needs clients for, and which mailbox (if any) should
|
||||
* be used for uploading data to and/or downloading data from each contact.
|
||||
* <p/>
|
||||
* The assignments of contacts to mailboxes for upload and/or download may
|
||||
* change when the following events happen:
|
||||
* <ul>
|
||||
* <li> A mailbox is paired or unpaired </li>
|
||||
* <li> A contact is added or removed </li>
|
||||
* <li> A {@link MailboxUpdate} is received from a contact </li>
|
||||
* <li> We discover that our own mailbox's supported API versions have
|
||||
* changed </li>
|
||||
* </ul>
|
||||
* <p/>
|
||||
* The manager keeps its mutable state consistent with the state in the DB by
|
||||
* using commit actions and events to update the manager's state on the event
|
||||
* thread.
|
||||
*/
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class MailboxClientManager implements Service, EventListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(MailboxClientManager.class.getName());
|
||||
|
||||
private final Executor eventExecutor, dbExecutor;
|
||||
private final TransactionManager db;
|
||||
private final ContactManager contactManager;
|
||||
private final PluginManager pluginManager;
|
||||
private final MailboxSettingsManager mailboxSettingsManager;
|
||||
private final MailboxUpdateManager mailboxUpdateManager;
|
||||
private final MailboxClientFactory mailboxClientFactory;
|
||||
private final TorReachabilityMonitor reachabilityMonitor;
|
||||
private final AtomicBoolean used = new AtomicBoolean(false);
|
||||
|
||||
// All of the following mutable state must only be accessed on the
|
||||
// event thread
|
||||
private final Map<ContactId, Updates> contactUpdates = new HashMap<>();
|
||||
private final Map<ContactId, MailboxClient> contactClients =
|
||||
new HashMap<>();
|
||||
@Nullable
|
||||
private MailboxProperties ownProperties = null;
|
||||
@Nullable
|
||||
private MailboxClient ownClient = null;
|
||||
private boolean online = false, handleEvents = false;
|
||||
|
||||
@Inject
|
||||
MailboxClientManager(@EventExecutor Executor eventExecutor,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
TransactionManager db,
|
||||
ContactManager contactManager,
|
||||
PluginManager pluginManager,
|
||||
MailboxSettingsManager mailboxSettingsManager,
|
||||
MailboxUpdateManager mailboxUpdateManager,
|
||||
MailboxClientFactory mailboxClientFactory,
|
||||
TorReachabilityMonitor reachabilityMonitor) {
|
||||
this.eventExecutor = eventExecutor;
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.db = db;
|
||||
this.contactManager = contactManager;
|
||||
this.pluginManager = pluginManager;
|
||||
this.mailboxSettingsManager = mailboxSettingsManager;
|
||||
this.mailboxUpdateManager = mailboxUpdateManager;
|
||||
this.mailboxClientFactory = mailboxClientFactory;
|
||||
this.reachabilityMonitor = reachabilityMonitor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startService() throws ServiceException {
|
||||
if (used.getAndSet(true)) throw new IllegalStateException();
|
||||
reachabilityMonitor.start();
|
||||
dbExecutor.execute(this::loadMailboxProperties);
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
private void loadMailboxProperties() {
|
||||
LOG.info("Loading mailbox properties");
|
||||
try {
|
||||
db.transaction(true, txn -> {
|
||||
Map<ContactId, Updates> updates = new HashMap<>();
|
||||
for (Contact c : contactManager.getContacts(txn)) {
|
||||
MailboxUpdate local = mailboxUpdateManager
|
||||
.getLocalUpdate(txn, c.getId());
|
||||
MailboxUpdate remote = mailboxUpdateManager
|
||||
.getRemoteUpdate(txn, c.getId());
|
||||
updates.put(c.getId(), new Updates(local, remote));
|
||||
}
|
||||
MailboxProperties ownProps =
|
||||
mailboxSettingsManager.getOwnMailboxProperties(txn);
|
||||
// Use a commit action so the state in memory remains
|
||||
// consistent with the state in the DB
|
||||
txn.attach(() -> initialiseState(updates, ownProps));
|
||||
});
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void initialiseState(Map<ContactId, Updates> updates,
|
||||
@Nullable MailboxProperties ownProps) {
|
||||
contactUpdates.putAll(updates);
|
||||
ownProperties = ownProps;
|
||||
Plugin tor = pluginManager.getPlugin(ID);
|
||||
if (tor != null && tor.getState() == ACTIVE) {
|
||||
LOG.info("Online");
|
||||
online = true;
|
||||
createClients();
|
||||
}
|
||||
// Now that the mutable state has been initialised we can start
|
||||
// handling events. This is done in a commit action so that we don't
|
||||
// miss any changes to the DB state or handle events for any changes
|
||||
// that were already reflected in the initial load
|
||||
handleEvents = true;
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void createClients() {
|
||||
LOG.info("Creating clients");
|
||||
for (Entry<ContactId, Updates> e : contactUpdates.entrySet()) {
|
||||
ContactId c = e.getKey();
|
||||
Updates u = e.getValue();
|
||||
if (isContactMailboxUsable(u.remote)) {
|
||||
// Create and start a client for the contact's mailbox
|
||||
MailboxClient contactClient = createAndStartClient(c);
|
||||
// Assign the contact to the contact's mailbox for upload
|
||||
assignContactToContactMailboxForUpload(c, contactClient, u);
|
||||
if (!isOwnMailboxUsable(ownProperties, u.remote)) {
|
||||
// We don't have a usable mailbox, so assign the contact to
|
||||
// the contact's mailbox for download too
|
||||
assignContactToContactMailboxForDownload(c,
|
||||
contactClient, u);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ownProperties == null) return;
|
||||
if (!isOwnMailboxUsable(ownProperties)) {
|
||||
LOG.warning("We have a mailbox but we can't use it");
|
||||
return;
|
||||
}
|
||||
// Create and start a client for our mailbox
|
||||
createAndStartClientForOwnMailbox();
|
||||
// Assign contacts to our mailbox for upload/download
|
||||
for (Entry<ContactId, Updates> e : contactUpdates.entrySet()) {
|
||||
ContactId c = e.getKey();
|
||||
Updates u = e.getValue();
|
||||
if (isOwnMailboxUsable(ownProperties, u.remote)) {
|
||||
// Assign the contact to our mailbox for download
|
||||
assignContactToOwnMailboxForDownload(c, u);
|
||||
if (!isContactMailboxUsable(u.remote)) {
|
||||
// The contact doesn't have a usable mailbox, so assign
|
||||
// the contact to our mailbox for upload too
|
||||
assignContactToOwnMailboxForUpload(c, u);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopService() throws ServiceException {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
eventExecutor.execute(() -> {
|
||||
handleEvents = false;
|
||||
if (online) destroyClients();
|
||||
latch.countDown();
|
||||
});
|
||||
reachabilityMonitor.destroy();
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
throw new ServiceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void destroyClients() {
|
||||
LOG.info("Destroying clients");
|
||||
for (MailboxClient client : contactClients.values()) {
|
||||
client.destroy();
|
||||
}
|
||||
contactClients.clear();
|
||||
destroyOwnClient();
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void destroyOwnClient() {
|
||||
if (ownClient != null) {
|
||||
ownClient.destroy();
|
||||
ownClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (!handleEvents) return;
|
||||
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();
|
||||
} else if (e instanceof MailboxPairedEvent) {
|
||||
LOG.info("Mailbox paired");
|
||||
MailboxPairedEvent m = (MailboxPairedEvent) e;
|
||||
onMailboxPaired(m.getProperties(), m.getLocalUpdates());
|
||||
} else if (e instanceof MailboxUnpairedEvent) {
|
||||
LOG.info("Mailbox unpaired");
|
||||
MailboxUnpairedEvent m = (MailboxUnpairedEvent) e;
|
||||
onMailboxUnpaired(m.getLocalUpdates());
|
||||
} else if (e instanceof MailboxUpdateSentToNewContactEvent) {
|
||||
LOG.info("Contact added");
|
||||
MailboxUpdateSentToNewContactEvent
|
||||
m = (MailboxUpdateSentToNewContactEvent) e;
|
||||
onContactAdded(m.getContactId(), m.getMailboxUpdate());
|
||||
} else if (e instanceof ContactRemovedEvent) {
|
||||
LOG.info("Contact removed");
|
||||
ContactRemovedEvent c = (ContactRemovedEvent) e;
|
||||
onContactRemoved(c.getContactId());
|
||||
} else if (e instanceof RemoteMailboxUpdateEvent) {
|
||||
LOG.info("Remote mailbox update");
|
||||
RemoteMailboxUpdateEvent r = (RemoteMailboxUpdateEvent) e;
|
||||
onRemoteMailboxUpdate(r.getContact(), r.getMailboxUpdate());
|
||||
} else if (e instanceof OwnMailboxConnectionStatusEvent) {
|
||||
OwnMailboxConnectionStatusEvent o =
|
||||
(OwnMailboxConnectionStatusEvent) e;
|
||||
onOwnMailboxConnectionStatusChanged(o.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onTorActive() {
|
||||
// If we checked the plugin at startup concurrently with the plugin
|
||||
// becoming active then `online` may already be true when we receive
|
||||
// the first TransportActiveEvent, in which case ignore it
|
||||
if (online) return;
|
||||
LOG.info("Online");
|
||||
online = true;
|
||||
createClients();
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onTorInactive() {
|
||||
// If we checked the plugin at startup concurrently with the plugin
|
||||
// becoming inactive then `online` may already be false when we
|
||||
// receive the first TransportInactiveEvent, in which case ignore it
|
||||
if (!online) return;
|
||||
LOG.info("Offline");
|
||||
online = false;
|
||||
destroyClients();
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onMailboxPaired(MailboxProperties ownProps,
|
||||
Map<ContactId, MailboxUpdateWithMailbox> localUpdates) {
|
||||
for (Entry<ContactId, MailboxUpdateWithMailbox> e :
|
||||
localUpdates.entrySet()) {
|
||||
ContactId c = e.getKey();
|
||||
Updates u = contactUpdates.get(c);
|
||||
contactUpdates.put(c, new Updates(e.getValue(), u.remote));
|
||||
}
|
||||
ownProperties = ownProps;
|
||||
if (!online) return;
|
||||
if (!isOwnMailboxUsable(ownProperties)) {
|
||||
LOG.warning("We have a mailbox but we can't use it");
|
||||
return;
|
||||
}
|
||||
// Create and start a client for our mailbox
|
||||
createAndStartClientForOwnMailbox();
|
||||
// Assign contacts to our mailbox for upload/download
|
||||
for (Entry<ContactId, Updates> e : contactUpdates.entrySet()) {
|
||||
ContactId c = e.getKey();
|
||||
Updates u = e.getValue();
|
||||
if (!isOwnMailboxUsable(ownProperties, u.remote)) {
|
||||
// Our mailbox isn't usable for communicating with this
|
||||
// contact, so don't assign/reassign this contact
|
||||
continue;
|
||||
}
|
||||
if (isContactMailboxUsable(u.remote)) {
|
||||
// The contact has a usable mailbox, so the contact should
|
||||
// currently be assigned to the contact's mailbox for upload
|
||||
// and download. Reassign the contact to our mailbox for
|
||||
// download
|
||||
MailboxClient contactClient =
|
||||
requireNonNull(contactClients.get(c));
|
||||
contactClient.deassignContactForDownload(c);
|
||||
} else {
|
||||
// The contact doesn't have a usable mailbox, so assign the
|
||||
// contact to our mailbox for upload
|
||||
assignContactToOwnMailboxForUpload(c, u);
|
||||
}
|
||||
assignContactToOwnMailboxForDownload(c, u);
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onMailboxUnpaired(Map<ContactId, MailboxUpdate> localUpdates) {
|
||||
for (Entry<ContactId, MailboxUpdate> e : localUpdates.entrySet()) {
|
||||
ContactId c = e.getKey();
|
||||
Updates updates = contactUpdates.get(c);
|
||||
contactUpdates.put(c, new Updates(e.getValue(), updates.remote));
|
||||
}
|
||||
MailboxProperties oldOwnProperties = ownProperties;
|
||||
ownProperties = null;
|
||||
if (!online) return;
|
||||
// Destroy the client for our own mailbox, if any
|
||||
destroyOwnClient();
|
||||
// Reassign contacts to their own mailboxes for download where possible
|
||||
for (Entry<ContactId, Updates> e : contactUpdates.entrySet()) {
|
||||
ContactId c = e.getKey();
|
||||
Updates u = e.getValue();
|
||||
if (isContactMailboxUsable(u.remote) &&
|
||||
isOwnMailboxUsable(oldOwnProperties, u.remote)) {
|
||||
// The contact should currently be assigned to our mailbox
|
||||
// for download. Reassign the contact to the contact's
|
||||
// mailbox for download
|
||||
MailboxClient contactClient =
|
||||
requireNonNull(contactClients.get(c));
|
||||
assignContactToContactMailboxForDownload(c, contactClient, u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onContactAdded(ContactId c, MailboxUpdate u) {
|
||||
Updates old = contactUpdates.put(c, new Updates(u, null));
|
||||
if (old != null) throw new IllegalStateException();
|
||||
// We haven't yet received an update from the newly added contact,
|
||||
// so at this stage we don't need to assign the contact to any
|
||||
// mailboxes for upload or download
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onContactRemoved(ContactId c) {
|
||||
Updates updates = requireNonNull(contactUpdates.remove(c));
|
||||
if (!online) return;
|
||||
// Destroy the client for the contact's mailbox, if any
|
||||
MailboxClient client = contactClients.remove(c);
|
||||
if (client != null) client.destroy();
|
||||
// If we have a mailbox and the contact is assigned to it for upload
|
||||
// and/or download, deassign the contact
|
||||
if (ownProperties == null) return;
|
||||
if (isOwnMailboxUsable(ownProperties, updates.remote)) {
|
||||
// We have a usable mailbox, so the contact should currently be
|
||||
// assigned to our mailbox for download. Deassign the contact from
|
||||
// our mailbox for download
|
||||
requireNonNull(ownClient).deassignContactForDownload(c);
|
||||
if (!isContactMailboxUsable(updates.remote)) {
|
||||
// The contact doesn't have a usable mailbox, so the contact
|
||||
// should currently be assigned to our mailbox for upload.
|
||||
// Deassign the contact from our mailbox for upload
|
||||
requireNonNull(ownClient).deassignContactForUpload(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onRemoteMailboxUpdate(ContactId c, MailboxUpdate remote) {
|
||||
Updates old = contactUpdates.get(c);
|
||||
MailboxUpdate oldRemote = old.remote;
|
||||
Updates u = new Updates(old.local, remote);
|
||||
contactUpdates.put(c, u);
|
||||
if (!online) return;
|
||||
// What may have changed?
|
||||
// * Contact's mailbox may be usable now, was unusable before
|
||||
// * Contact's mailbox may be unusable now, was usable before
|
||||
// * Contact's mailbox may have been replaced
|
||||
// * Contact's mailbox may have changed its API versions
|
||||
// * Contact may be able to use our mailbox now, was unable before
|
||||
// * Contact may be unable to use our mailbox now, was able before
|
||||
boolean wasContactMailboxUsable = isContactMailboxUsable(oldRemote);
|
||||
boolean isContactMailboxUsable = isContactMailboxUsable(remote);
|
||||
boolean wasOwnMailboxUsable =
|
||||
isOwnMailboxUsable(ownProperties, oldRemote);
|
||||
boolean isOwnMailboxUsable = isOwnMailboxUsable(ownProperties, remote);
|
||||
|
||||
// Create/destroy/replace the client for the contact's mailbox if needed
|
||||
MailboxClient contactClient = null;
|
||||
boolean clientReplaced = false;
|
||||
if (isContactMailboxUsable) {
|
||||
if (wasContactMailboxUsable) {
|
||||
MailboxProperties oldProps = getMailboxProperties(oldRemote);
|
||||
MailboxProperties newProps = getMailboxProperties(remote);
|
||||
if (oldProps.equals(newProps)) {
|
||||
// The contact previously had a usable mailbox, now has
|
||||
// a usable mailbox, it's the same mailbox, and its API
|
||||
// versions haven't changed. Keep using the existing client
|
||||
contactClient = requireNonNull(contactClients.get(c));
|
||||
} else {
|
||||
// The contact previously had a usable mailbox and now has
|
||||
// a usable mailbox, but either it's a new mailbox or its
|
||||
// API versions have changed. Replace the client
|
||||
requireNonNull(contactClients.remove(c)).destroy();
|
||||
contactClient = createAndStartClient(c);
|
||||
clientReplaced = true;
|
||||
}
|
||||
} else {
|
||||
// The client didn't previously have a usable mailbox but now
|
||||
// has one. Create and start a client
|
||||
contactClient = createAndStartClient(c);
|
||||
}
|
||||
} else if (wasContactMailboxUsable) {
|
||||
// The client previously had a usable mailbox but no longer does.
|
||||
// Destroy the existing client
|
||||
requireNonNull(contactClients.remove(c)).destroy();
|
||||
}
|
||||
|
||||
boolean wasAssignedToOwnMailboxForUpload =
|
||||
wasOwnMailboxUsable && !wasContactMailboxUsable;
|
||||
boolean willBeAssignedToOwnMailboxForUpload =
|
||||
isOwnMailboxUsable && !isContactMailboxUsable;
|
||||
|
||||
boolean wasAssignedToContactMailboxForDownload =
|
||||
!wasOwnMailboxUsable && wasContactMailboxUsable;
|
||||
boolean willBeAssignedToContactMailboxForDownload =
|
||||
!isOwnMailboxUsable && isContactMailboxUsable;
|
||||
|
||||
// Deassign the contact for upload/download if needed
|
||||
if (wasAssignedToOwnMailboxForUpload &&
|
||||
!willBeAssignedToOwnMailboxForUpload) {
|
||||
requireNonNull(ownClient).deassignContactForUpload(c);
|
||||
}
|
||||
if (wasOwnMailboxUsable && !isOwnMailboxUsable) {
|
||||
requireNonNull(ownClient).deassignContactForDownload(c);
|
||||
}
|
||||
// If the client for the contact's mailbox was replaced or destroyed
|
||||
// above then we don't need to deassign the contact for download
|
||||
if (wasAssignedToContactMailboxForDownload &&
|
||||
!willBeAssignedToContactMailboxForDownload &&
|
||||
!clientReplaced && isContactMailboxUsable) {
|
||||
requireNonNull(contactClient).deassignContactForDownload(c);
|
||||
}
|
||||
// We never need to deassign the contact from the contact's mailbox for
|
||||
// upload: this would only be needed if the contact's mailbox were no
|
||||
// longer usable, in which case the client would already have been
|
||||
// destroyed above. Thanks to the linter for spotting this
|
||||
|
||||
// Assign the contact for upload/download if needed
|
||||
if (!wasAssignedToOwnMailboxForUpload &&
|
||||
willBeAssignedToOwnMailboxForUpload) {
|
||||
assignContactToOwnMailboxForUpload(c, u);
|
||||
}
|
||||
if (!wasOwnMailboxUsable && isOwnMailboxUsable) {
|
||||
assignContactToOwnMailboxForDownload(c, u);
|
||||
}
|
||||
if ((!wasContactMailboxUsable || clientReplaced) &&
|
||||
isContactMailboxUsable) {
|
||||
assignContactToContactMailboxForUpload(c, contactClient, u);
|
||||
}
|
||||
if ((!wasAssignedToContactMailboxForDownload || clientReplaced) &&
|
||||
willBeAssignedToContactMailboxForDownload) {
|
||||
assignContactToContactMailboxForDownload(c, contactClient, u);
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onOwnMailboxConnectionStatusChanged(MailboxStatus status) {
|
||||
if (!online || ownProperties == null) return;
|
||||
List<MailboxVersion> oldServerSupports =
|
||||
ownProperties.getServerSupports();
|
||||
List<MailboxVersion> newServerSupports = status.getServerSupports();
|
||||
if (!oldServerSupports.equals(newServerSupports)) {
|
||||
LOG.info("Our mailbox's supported API versions have changed");
|
||||
// This potentially affects every assignment of contacts to
|
||||
// mailboxes for upload and download, so just rebuild the clients
|
||||
// and assignments from scratch
|
||||
destroyClients();
|
||||
createClients();
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void createAndStartClientForOwnMailbox() {
|
||||
if (ownClient != null) throw new IllegalStateException();
|
||||
ownClient = mailboxClientFactory.createOwnMailboxClient(
|
||||
reachabilityMonitor, requireNonNull(ownProperties));
|
||||
ownClient.start();
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private MailboxClient createAndStartClient(ContactId c) {
|
||||
MailboxClient client = mailboxClientFactory
|
||||
.createContactMailboxClient(reachabilityMonitor);
|
||||
MailboxClient old = contactClients.put(c, client);
|
||||
if (old != null) throw new IllegalStateException();
|
||||
client.start();
|
||||
return client;
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void assignContactToOwnMailboxForDownload(ContactId c, Updates u) {
|
||||
MailboxProperties localProps = getMailboxProperties(u.local);
|
||||
requireNonNull(ownClient).assignContactForDownload(c,
|
||||
requireNonNull(ownProperties),
|
||||
requireNonNull(localProps.getOutboxId()));
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void assignContactToOwnMailboxForUpload(ContactId c, Updates u) {
|
||||
MailboxProperties localProps = getMailboxProperties(u.local);
|
||||
requireNonNull(ownClient).assignContactForUpload(c,
|
||||
requireNonNull(ownProperties),
|
||||
requireNonNull(localProps.getInboxId()));
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void assignContactToContactMailboxForDownload(ContactId c,
|
||||
MailboxClient contactClient, Updates u) {
|
||||
MailboxProperties remoteProps =
|
||||
getMailboxProperties(requireNonNull(u.remote));
|
||||
contactClient.assignContactForDownload(c, remoteProps,
|
||||
requireNonNull(remoteProps.getInboxId()));
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void assignContactToContactMailboxForUpload(ContactId c,
|
||||
MailboxClient contactClient, Updates u) {
|
||||
MailboxProperties remoteProps =
|
||||
getMailboxProperties(requireNonNull(u.remote));
|
||||
contactClient.assignContactForUpload(c, remoteProps,
|
||||
requireNonNull(remoteProps.getOutboxId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link MailboxProperties} included in the given update,
|
||||
* which must be a {@link MailboxUpdateWithMailbox}.
|
||||
*/
|
||||
private MailboxProperties getMailboxProperties(MailboxUpdate update) {
|
||||
if (!(update instanceof MailboxUpdateWithMailbox)) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
MailboxUpdateWithMailbox mailbox = (MailboxUpdateWithMailbox) update;
|
||||
return mailbox.getMailboxProperties();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we can use our own mailbox to communicate with the
|
||||
* contact that sent the given update.
|
||||
*/
|
||||
private boolean isOwnMailboxUsable(
|
||||
@Nullable MailboxProperties ownProperties,
|
||||
@Nullable MailboxUpdate remote) {
|
||||
if (ownProperties == null || remote == null) return false;
|
||||
return isMailboxUsable(remote.getClientSupports(),
|
||||
ownProperties.getServerSupports());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we can use the contact's mailbox to communicate with
|
||||
* the contact that sent the given update.
|
||||
*/
|
||||
private boolean isContactMailboxUsable(@Nullable MailboxUpdate remote) {
|
||||
if (remote instanceof MailboxUpdateWithMailbox) {
|
||||
MailboxUpdateWithMailbox remoteMailbox =
|
||||
(MailboxUpdateWithMailbox) remote;
|
||||
return isMailboxUsable(remoteMailbox.getClientSupports(),
|
||||
remoteMailbox.getMailboxProperties().getServerSupports());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we can communicate with a contact that has the given
|
||||
* client-supported API versions via a mailbox with the given
|
||||
* server-supported API versions.
|
||||
*/
|
||||
private boolean isMailboxUsable(List<MailboxVersion> contactClient,
|
||||
List<MailboxVersion> server) {
|
||||
return isClientCompatibleWithServer(contactClient, server)
|
||||
&& isClientCompatibleWithServer(CLIENT_SUPPORTS, server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if our client-supported API versions are compatible with
|
||||
* our own mailbox's server-supported API versions.
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
private boolean isOwnMailboxUsable(MailboxProperties ownProperties) {
|
||||
return isClientCompatibleWithServer(CLIENT_SUPPORTS,
|
||||
ownProperties.getServerSupports());
|
||||
}
|
||||
|
||||
/**
|
||||
* A container for the latest {@link MailboxUpdate updates} sent to and
|
||||
* received from a given contact.
|
||||
*/
|
||||
private static class Updates {
|
||||
|
||||
/**
|
||||
* The latest update sent to the contact.
|
||||
*/
|
||||
private final MailboxUpdate local;
|
||||
|
||||
/**
|
||||
* The latest update received from the contact, or null if no update
|
||||
* has been received.
|
||||
*/
|
||||
@Nullable
|
||||
private final MailboxUpdate remote;
|
||||
|
||||
private Updates(MailboxUpdate local, @Nullable MailboxUpdate remote) {
|
||||
this.local = local;
|
||||
this.remote = remote;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFileId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
import org.briarproject.bramble.mailbox.TorReachabilityMonitor.TorReachabilityObserver;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Queue;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
abstract class MailboxDownloadWorker implements MailboxWorker,
|
||||
ConnectivityObserver, TorReachabilityObserver {
|
||||
|
||||
/**
|
||||
* When the worker is started it waits for a connectivity check, then
|
||||
* starts its first download cycle: checking for files to download,
|
||||
* downloading and deleting the files, and checking again until all files
|
||||
* have been downloaded and deleted.
|
||||
* <p>
|
||||
* The worker then waits for our Tor hidden service to be reachable before
|
||||
* starting its second download cycle. This ensures that if a contact
|
||||
* tried and failed to connect to our hidden service before it was
|
||||
* reachable, and therefore uploaded a file to the mailbox instead, we'll
|
||||
* find the file in the second download cycle.
|
||||
*/
|
||||
protected enum State {
|
||||
CREATED,
|
||||
CONNECTIVITY_CHECK,
|
||||
DOWNLOAD_CYCLE_1,
|
||||
WAITING_FOR_TOR,
|
||||
DOWNLOAD_CYCLE_2,
|
||||
FINISHED,
|
||||
DESTROYED
|
||||
}
|
||||
|
||||
protected static final Logger LOG =
|
||||
getLogger(MailboxDownloadWorker.class.getName());
|
||||
|
||||
private final ConnectivityChecker connectivityChecker;
|
||||
private final TorReachabilityMonitor torReachabilityMonitor;
|
||||
protected final MailboxApiCaller mailboxApiCaller;
|
||||
protected final MailboxApi mailboxApi;
|
||||
private final MailboxFileManager mailboxFileManager;
|
||||
protected final MailboxProperties mailboxProperties;
|
||||
protected final Object lock = new Object();
|
||||
|
||||
@GuardedBy("lock")
|
||||
protected State state = State.CREATED;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
protected Cancellable apiCall = null;
|
||||
|
||||
/**
|
||||
* Creates the API call that starts the worker's download cycle.
|
||||
*/
|
||||
protected abstract ApiCall createApiCallForDownloadCycle();
|
||||
|
||||
MailboxDownloadWorker(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor torReachabilityMonitor,
|
||||
MailboxApiCaller mailboxApiCaller,
|
||||
MailboxApi mailboxApi,
|
||||
MailboxFileManager mailboxFileManager,
|
||||
MailboxProperties mailboxProperties) {
|
||||
this.connectivityChecker = connectivityChecker;
|
||||
this.torReachabilityMonitor = torReachabilityMonitor;
|
||||
this.mailboxApiCaller = mailboxApiCaller;
|
||||
this.mailboxApi = mailboxApi;
|
||||
this.mailboxFileManager = mailboxFileManager;
|
||||
this.mailboxProperties = mailboxProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
LOG.info("Started");
|
||||
synchronized (lock) {
|
||||
// Don't allow the worker to be reused
|
||||
if (state != State.CREATED) return;
|
||||
state = State.CONNECTIVITY_CHECK;
|
||||
}
|
||||
// Avoid leaking observer in case destroy() is called concurrently
|
||||
// before observer is added
|
||||
connectivityChecker.checkConnectivity(mailboxProperties, this);
|
||||
boolean destroyed;
|
||||
synchronized (lock) {
|
||||
destroyed = state == State.DESTROYED;
|
||||
}
|
||||
if (destroyed) connectivityChecker.removeObserver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
LOG.info("Destroyed");
|
||||
Cancellable apiCall;
|
||||
synchronized (lock) {
|
||||
state = State.DESTROYED;
|
||||
apiCall = this.apiCall;
|
||||
this.apiCall = null;
|
||||
}
|
||||
if (apiCall != null) apiCall.cancel();
|
||||
connectivityChecker.removeObserver(this);
|
||||
torReachabilityMonitor.removeObserver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectivityCheckSucceeded() {
|
||||
LOG.info("Connectivity check succeeded");
|
||||
synchronized (lock) {
|
||||
if (state != State.CONNECTIVITY_CHECK) return;
|
||||
state = State.DOWNLOAD_CYCLE_1;
|
||||
// Start first download cycle
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
createApiCallForDownloadCycle());
|
||||
}
|
||||
}
|
||||
|
||||
void onDownloadCycleFinished() {
|
||||
boolean addObserver = false;
|
||||
synchronized (lock) {
|
||||
if (state == State.DOWNLOAD_CYCLE_1) {
|
||||
LOG.info("First download cycle finished");
|
||||
state = State.WAITING_FOR_TOR;
|
||||
apiCall = null;
|
||||
addObserver = true;
|
||||
} else if (state == State.DOWNLOAD_CYCLE_2) {
|
||||
LOG.info("Second download cycle finished");
|
||||
state = State.FINISHED;
|
||||
apiCall = null;
|
||||
}
|
||||
}
|
||||
if (addObserver) {
|
||||
// Avoid leaking observer in case destroy() is called concurrently
|
||||
// before observer is added
|
||||
torReachabilityMonitor.addOneShotObserver(this);
|
||||
boolean destroyed;
|
||||
synchronized (lock) {
|
||||
destroyed = state == State.DESTROYED;
|
||||
}
|
||||
if (destroyed) torReachabilityMonitor.removeObserver(this);
|
||||
}
|
||||
}
|
||||
|
||||
void downloadNextFile(Queue<FolderFile> queue) {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
if (queue.isEmpty()) {
|
||||
// Check for files again, as new files may have arrived while
|
||||
// we were downloading
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
createApiCallForDownloadCycle());
|
||||
} else {
|
||||
FolderFile file = queue.remove();
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
new SimpleApiCall(() ->
|
||||
apiCallDownloadFile(file, queue)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void apiCallDownloadFile(FolderFile file, Queue<FolderFile> queue)
|
||||
throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
}
|
||||
LOG.info("Downloading file");
|
||||
File tempFile = mailboxFileManager.createTempFileForDownload();
|
||||
try {
|
||||
mailboxApi.getFile(mailboxProperties, file.folderId, file.fileId,
|
||||
tempFile);
|
||||
} catch (IOException | ApiException e) {
|
||||
if (!tempFile.delete()) {
|
||||
LOG.warning("Failed to delete temporary file");
|
||||
}
|
||||
throw e;
|
||||
} catch (TolerableFailureException e) {
|
||||
// File not found - continue to the next file
|
||||
LOG.warning("File does not exist");
|
||||
if (!tempFile.delete()) {
|
||||
LOG.warning("Failed to delete temporary file");
|
||||
}
|
||||
downloadNextFile(queue);
|
||||
return;
|
||||
}
|
||||
mailboxFileManager.handleDownloadedFile(tempFile);
|
||||
deleteFile(file, queue);
|
||||
}
|
||||
|
||||
private void deleteFile(FolderFile file, Queue<FolderFile> queue) {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
new SimpleApiCall(() -> apiCallDeleteFile(file, queue)));
|
||||
}
|
||||
}
|
||||
|
||||
private void apiCallDeleteFile(FolderFile file, Queue<FolderFile> queue)
|
||||
throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
}
|
||||
try {
|
||||
mailboxApi.deleteFile(mailboxProperties, file.folderId,
|
||||
file.fileId);
|
||||
} catch (TolerableFailureException e) {
|
||||
// File not found - continue to the next file
|
||||
LOG.warning("File does not exist");
|
||||
}
|
||||
downloadNextFile(queue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTorReachable() {
|
||||
LOG.info("Our Tor hidden service is reachable");
|
||||
synchronized (lock) {
|
||||
if (state != State.WAITING_FOR_TOR) return;
|
||||
state = State.DOWNLOAD_CYCLE_2;
|
||||
// Start second download cycle
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
createApiCallForDownloadCycle());
|
||||
}
|
||||
}
|
||||
|
||||
// Package access for testing
|
||||
static class FolderFile {
|
||||
|
||||
final MailboxFolderId folderId;
|
||||
final MailboxFileId fileId;
|
||||
|
||||
FolderFile(MailboxFolderId folderId, MailboxFileId fileId) {
|
||||
this.folderId = folderId;
|
||||
this.fileId = fileId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
interface MailboxFileManager {
|
||||
|
||||
/**
|
||||
* Creates an empty file for storing a download.
|
||||
*/
|
||||
File createTempFileForDownload() throws IOException;
|
||||
|
||||
/**
|
||||
* Creates a file to be uploaded to the given contact and writes any
|
||||
* waiting data to the file. The IDs of any messages sent or acked will
|
||||
* be added to the given {@link OutgoingSessionRecord}.
|
||||
*/
|
||||
File createAndWriteTempFileForUpload(ContactId contactId,
|
||||
OutgoingSessionRecord sessionRecord) throws IOException;
|
||||
|
||||
/**
|
||||
* Handles a file that has been downloaded. The file should be created
|
||||
* with {@link #createTempFileForDownload()}.
|
||||
*/
|
||||
void handleDownloadedFile(File f);
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.connection.ConnectionManager;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
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.TransportConnectionWriter;
|
||||
import org.briarproject.bramble.api.plugin.event.TransportActiveEvent;
|
||||
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
|
||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
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.IoUtils.delete;
|
||||
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";
|
||||
static final String UPLOAD_DIR_NAME = "uploads";
|
||||
|
||||
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 {
|
||||
return createTempFile(DOWNLOAD_DIR_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File createAndWriteTempFileForUpload(ContactId contactId,
|
||||
OutgoingSessionRecord sessionRecord) throws IOException {
|
||||
File f = createTempFile(UPLOAD_DIR_NAME);
|
||||
// We shouldn't reach this point until the plugin has been started
|
||||
SimplexPlugin plugin =
|
||||
(SimplexPlugin) requireNonNull(pluginManager.getPlugin(ID));
|
||||
TransportProperties p = new TransportProperties();
|
||||
p.put(PROP_PATH, f.getAbsolutePath());
|
||||
TransportConnectionWriter writer = plugin.createWriter(p);
|
||||
if (writer == null) {
|
||||
delete(f);
|
||||
throw new IOException();
|
||||
}
|
||||
MailboxFileWriter decorated = new MailboxFileWriter(writer);
|
||||
LOG.info("Writing file for upload");
|
||||
connectionManager.manageOutgoingConnection(contactId, ID, decorated,
|
||||
sessionRecord);
|
||||
if (decorated.awaitDisposal()) {
|
||||
// An exception was thrown during the session - delete the file
|
||||
delete(f);
|
||||
throw new IOException();
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
private File createTempFile(String dirName) throws IOException {
|
||||
// Wait for orphaned files to be handled before creating new files
|
||||
try {
|
||||
orphanLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
File dir = createDirectoryIfNeeded(dirName);
|
||||
return File.createTempFile("mailbox", ".tmp", dir);
|
||||
}
|
||||
|
||||
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) {
|
||||
// Wait for the transport to become active before handling orphaned
|
||||
// files so that we can get the plugin from the plugin manager
|
||||
if (e instanceof TransportActiveEvent) {
|
||||
TransportActiveEvent t = (TransportActiveEvent) e;
|
||||
if (t.getTransportId().equals(ID)) {
|
||||
ioExecutor.execute(this::handleOrphanedFiles);
|
||||
eventBus.removeListener(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called at startup, as soon as the plugin is started, to
|
||||
* delete any files that were left in the upload directory at the last
|
||||
* shutdown and handle any files that were left in the download directory.
|
||||
*/
|
||||
@IoExecutor
|
||||
private void handleOrphanedFiles() {
|
||||
try {
|
||||
File uploadDir = createDirectoryIfNeeded(UPLOAD_DIR_NAME);
|
||||
File[] orphanedUploads = uploadDir.listFiles();
|
||||
if (orphanedUploads != null) {
|
||||
for (File f : orphanedUploads) delete(f);
|
||||
}
|
||||
File downloadDir = createDirectoryIfNeeded(DOWNLOAD_DIR_NAME);
|
||||
File[] orphanedDownloads = downloadDir.listFiles();
|
||||
// Now that we've got the list of orphaned downloads, new files
|
||||
// can be created in the download directory
|
||||
orphanLatch.countDown();
|
||||
if (orphanedDownloads != null) {
|
||||
for (File f : orphanedDownloads) 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");
|
||||
delete(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class MailboxFileWriter
|
||||
implements TransportConnectionWriter {
|
||||
|
||||
private final TransportConnectionWriter delegate;
|
||||
private final BlockingQueue<Boolean> disposalResult =
|
||||
new ArrayBlockingQueue<>(1);
|
||||
|
||||
private MailboxFileWriter(TransportConnectionWriter delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaxLatency() {
|
||||
return delegate.getMaxLatency();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxIdleTime() {
|
||||
return delegate.getMaxIdleTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLossyAndCheap() {
|
||||
return delegate.isLossyAndCheap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
return delegate.getOutputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(boolean exception) throws IOException {
|
||||
delegate.dispose(exception);
|
||||
disposalResult.add(exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the delegate to be disposed and returns true if an
|
||||
* exception occurred.
|
||||
*/
|
||||
private boolean awaitDisposal() {
|
||||
try {
|
||||
return disposalResult.take();
|
||||
} catch (InterruptedException e) {
|
||||
LOG.info("Interrupted while waiting for disposal");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,12 @@ import org.briarproject.bramble.api.mailbox.MailboxPairingTask;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxStatus;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -98,37 +100,60 @@ class MailboxManagerImpl implements MailboxManager {
|
||||
|
||||
@Override
|
||||
public boolean checkConnection() {
|
||||
boolean success;
|
||||
List<MailboxVersion> versions = null;
|
||||
try {
|
||||
MailboxProperties props = db.transactionWithNullableResult(true,
|
||||
mailboxSettingsManager::getOwnMailboxProperties);
|
||||
success = api.checkStatus(props);
|
||||
if (props == null) throw new DbException();
|
||||
versions = api.getServerSupports(props);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
// we don't treat this is a failure to record
|
||||
return false;
|
||||
} catch (IOException | MailboxApi.ApiException e) {
|
||||
// we record this as a failure
|
||||
success = false;
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
try {
|
||||
recordCheckResult(success);
|
||||
recordCheckResult(versions);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
return success;
|
||||
return versions != null;
|
||||
}
|
||||
|
||||
private void recordCheckResult(boolean success) throws DbException {
|
||||
private void recordCheckResult(@Nullable List<MailboxVersion> versions)
|
||||
throws DbException {
|
||||
long now = clock.currentTimeMillis();
|
||||
db.transaction(false, txn -> {
|
||||
if (success) {
|
||||
mailboxSettingsManager.recordSuccessfulConnection(txn, now);
|
||||
if (versions != null) {
|
||||
mailboxSettingsManager
|
||||
.recordSuccessfulConnection(txn, now, versions);
|
||||
} else {
|
||||
mailboxSettingsManager.recordFailedConnectionAttempt(txn, now);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@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,34 +1,49 @@
|
||||
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.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventExecutor;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxPropertyManager;
|
||||
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.plugin.PluginManager;
|
||||
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 java.util.concurrent.Executor;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.CLIENT_ID;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.MAJOR_VERSION;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.MINOR_VERSION;
|
||||
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
|
||||
MailboxPropertyValidator mailboxPropertyValidator;
|
||||
MailboxUpdateValidator mailboxUpdateValidator;
|
||||
@Inject
|
||||
MailboxPropertyManager mailboxPropertyManager;
|
||||
MailboxUpdateManager mailboxUpdateManager;
|
||||
@Inject
|
||||
MailboxFileManager mailboxFileManager;
|
||||
@Inject
|
||||
MailboxClientManager mailboxClientManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@@ -44,43 +59,123 @@ public class MailboxModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MailboxSettingsManager provideMailboxSettingsManager(
|
||||
MailboxSettingsManagerImpl mailboxSettingsManager) {
|
||||
return mailboxSettingsManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
MailboxApi providesMailboxApi(MailboxApiImpl mailboxApi) {
|
||||
UrlConverter provideUrlConverter(UrlConverterImpl urlConverter) {
|
||||
return urlConverter;
|
||||
}
|
||||
|
||||
@Provides
|
||||
MailboxApi provideMailboxApi(MailboxApiImpl mailboxApi) {
|
||||
return mailboxApi;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MailboxPropertyValidator provideMailboxPropertyValidator(
|
||||
ValidationManager validationManager, ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock) {
|
||||
MailboxPropertyValidator validator = new MailboxPropertyValidator(
|
||||
MailboxUpdateValidator provideMailboxUpdateValidator(
|
||||
ValidationManager validationManager,
|
||||
ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder,
|
||||
Clock clock,
|
||||
FeatureFlags featureFlags) {
|
||||
MailboxUpdateValidator validator = new MailboxUpdateValidator(
|
||||
clientHelper, metadataEncoder, clock);
|
||||
validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION,
|
||||
validator);
|
||||
if (featureFlags.shouldEnableMailbox()) {
|
||||
validationManager.registerMessageValidator(CLIENT_ID,
|
||||
MAJOR_VERSION, validator);
|
||||
}
|
||||
return validator;
|
||||
}
|
||||
|
||||
@Provides
|
||||
List<MailboxVersion> provideClientSupports() {
|
||||
return CLIENT_SUPPORTS;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MailboxPropertyManager provideMailboxPropertyManager(
|
||||
MailboxUpdateManager provideMailboxUpdateManager(
|
||||
FeatureFlags featureFlags,
|
||||
LifecycleManager lifecycleManager,
|
||||
ValidationManager validationManager, ContactManager contactManager,
|
||||
ClientVersioningManager clientVersioningManager,
|
||||
MailboxSettingsManager mailboxSettingsManager,
|
||||
MailboxPropertyManagerImpl mailboxPropertyManager) {
|
||||
lifecycleManager.registerOpenDatabaseHook(mailboxPropertyManager);
|
||||
validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION,
|
||||
mailboxPropertyManager);
|
||||
contactManager.registerContactHook(mailboxPropertyManager);
|
||||
clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION,
|
||||
MINOR_VERSION, mailboxPropertyManager);
|
||||
mailboxSettingsManager.registerMailboxHook(mailboxPropertyManager);
|
||||
return mailboxPropertyManager;
|
||||
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;
|
||||
}
|
||||
|
||||
@Provides
|
||||
MailboxWorkerFactory provideMailboxWorkerFactory(
|
||||
MailboxWorkerFactoryImpl mailboxWorkerFactory) {
|
||||
return mailboxWorkerFactory;
|
||||
}
|
||||
|
||||
@Provides
|
||||
MailboxClientFactory provideMailboxClientFactory(
|
||||
MailboxClientFactoryImpl mailboxClientFactory) {
|
||||
return mailboxClientFactory;
|
||||
}
|
||||
|
||||
@Provides
|
||||
MailboxApiCaller provideMailboxApiCaller(
|
||||
MailboxApiCallerImpl mailboxApiCaller) {
|
||||
return mailboxApiCaller;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
TorReachabilityMonitor provideTorReachabilityMonitor(
|
||||
TorReachabilityMonitorImpl reachabilityMonitor) {
|
||||
return reachabilityMonitor;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MailboxClientManager provideMailboxClientManager(
|
||||
@EventExecutor Executor eventExecutor,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
TransactionManager db,
|
||||
ContactManager contactManager,
|
||||
PluginManager pluginManager,
|
||||
MailboxSettingsManager mailboxSettingsManager,
|
||||
MailboxUpdateManager mailboxUpdateManager,
|
||||
MailboxClientFactory mailboxClientFactory,
|
||||
TorReachabilityMonitor reachabilityMonitor,
|
||||
FeatureFlags featureFlags,
|
||||
LifecycleManager lifecycleManager,
|
||||
EventBus eventBus) {
|
||||
MailboxClientManager manager = new MailboxClientManager(eventExecutor,
|
||||
dbExecutor, db, contactManager, pluginManager,
|
||||
mailboxSettingsManager, mailboxUpdateManager,
|
||||
mailboxClientFactory, reachabilityMonitor);
|
||||
if (featureFlags.shouldEnableMailbox()) {
|
||||
lifecycleManager.registerService(manager);
|
||||
eventBus.addListener(manager);
|
||||
}
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
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.MailboxPropertyManager;
|
||||
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;
|
||||
|
||||
@@ -24,7 +24,7 @@ class MailboxPairingTaskFactoryImpl implements MailboxPairingTaskFactory {
|
||||
private final Clock clock;
|
||||
private final MailboxApi api;
|
||||
private final MailboxSettingsManager mailboxSettingsManager;
|
||||
private final MailboxPropertyManager mailboxPropertyManager;
|
||||
private final MailboxUpdateManager mailboxUpdateManager;
|
||||
|
||||
@Inject
|
||||
MailboxPairingTaskFactoryImpl(
|
||||
@@ -34,20 +34,20 @@ class MailboxPairingTaskFactoryImpl implements MailboxPairingTaskFactory {
|
||||
Clock clock,
|
||||
MailboxApi api,
|
||||
MailboxSettingsManager mailboxSettingsManager,
|
||||
MailboxPropertyManager mailboxPropertyManager) {
|
||||
MailboxUpdateManager mailboxUpdateManager) {
|
||||
this.eventExecutor = eventExecutor;
|
||||
this.db = db;
|
||||
this.crypto = crypto;
|
||||
this.clock = clock;
|
||||
this.api = api;
|
||||
this.mailboxSettingsManager = mailboxSettingsManager;
|
||||
this.mailboxPropertyManager = mailboxPropertyManager;
|
||||
this.mailboxUpdateManager = mailboxUpdateManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxPairingTask createPairingTask(String qrCodePayload) {
|
||||
return new MailboxPairingTaskImpl(qrCodePayload, eventExecutor, db,
|
||||
crypto, clock, api, mailboxSettingsManager,
|
||||
mailboxPropertyManager);
|
||||
mailboxUpdateManager);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ 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.MailboxPropertiesUpdate;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxPropertyManager;
|
||||
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;
|
||||
@@ -51,7 +51,7 @@ class MailboxPairingTaskImpl implements MailboxPairingTask {
|
||||
private final Clock clock;
|
||||
private final MailboxApi api;
|
||||
private final MailboxSettingsManager mailboxSettingsManager;
|
||||
private final MailboxPropertyManager mailboxPropertyManager;
|
||||
private final MailboxUpdateManager mailboxUpdateManager;
|
||||
|
||||
private final Object lock = new Object();
|
||||
@GuardedBy("lock")
|
||||
@@ -68,7 +68,7 @@ class MailboxPairingTaskImpl implements MailboxPairingTask {
|
||||
Clock clock,
|
||||
MailboxApi api,
|
||||
MailboxSettingsManager mailboxSettingsManager,
|
||||
MailboxPropertyManager mailboxPropertyManager) {
|
||||
MailboxUpdateManager mailboxUpdateManager) {
|
||||
this.payload = payload;
|
||||
this.eventExecutor = eventExecutor;
|
||||
this.db = db;
|
||||
@@ -76,7 +76,7 @@ class MailboxPairingTaskImpl implements MailboxPairingTask {
|
||||
this.clock = clock;
|
||||
this.api = api;
|
||||
this.mailboxSettingsManager = mailboxSettingsManager;
|
||||
this.mailboxPropertyManager = mailboxPropertyManager;
|
||||
this.mailboxUpdateManager = mailboxUpdateManager;
|
||||
state = new MailboxPairingState.QrCodeReceived();
|
||||
}
|
||||
|
||||
@@ -115,21 +115,20 @@ class MailboxPairingTaskImpl implements MailboxPairingTask {
|
||||
private void pairMailbox() throws IOException, ApiException, DbException {
|
||||
MailboxProperties mailboxProperties = decodeQrCodePayload(payload);
|
||||
setState(new MailboxPairingState.Pairing());
|
||||
MailboxAuthToken ownerToken = api.setup(mailboxProperties);
|
||||
MailboxProperties ownerProperties = new MailboxProperties(
|
||||
mailboxProperties.getBaseUrl(), ownerToken, true);
|
||||
MailboxProperties ownerProperties = api.setup(mailboxProperties);
|
||||
long time = clock.currentTimeMillis();
|
||||
db.transaction(false, txn -> {
|
||||
mailboxSettingsManager
|
||||
.setOwnMailboxProperties(txn, ownerProperties);
|
||||
mailboxSettingsManager.recordSuccessfulConnection(txn, time);
|
||||
mailboxSettingsManager.recordSuccessfulConnection(txn, time,
|
||||
ownerProperties.getServerSupports());
|
||||
// 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)) {
|
||||
MailboxPropertiesUpdate remoteProps = mailboxPropertyManager
|
||||
.getRemoteProperties(txn, c.getId());
|
||||
if (remoteProps == null) {
|
||||
MailboxUpdate update = mailboxUpdateManager.getRemoteUpdate(
|
||||
txn, c.getId());
|
||||
if (update == null || !update.hasMailbox()) {
|
||||
db.resetUnackedMessagesToSend(txn, c.getId());
|
||||
}
|
||||
}
|
||||
@@ -179,10 +178,9 @@ class MailboxPairingTaskImpl implements MailboxPairingTask {
|
||||
LOG.info("QR code is valid");
|
||||
byte[] onionPubKey = Arrays.copyOfRange(bytes, 1, 33);
|
||||
String onion = crypto.encodeOnion(onionPubKey);
|
||||
String baseUrl = "http://" + onion + ".onion";
|
||||
byte[] tokenBytes = Arrays.copyOfRange(bytes, 33, 65);
|
||||
MailboxAuthToken setupToken = new MailboxAuthToken(tokenBytes);
|
||||
return new MailboxProperties(baseUrl, setupToken, true);
|
||||
return new MailboxProperties(onion, setupToken, new ArrayList<>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
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.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.MailboxPropertiesUpdate;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxPropertyManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager.MailboxHook;
|
||||
import org.briarproject.bramble.api.mailbox.RemoteMailboxPropertiesUpdateEvent;
|
||||
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.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 MailboxPropertyManagerImpl implements MailboxPropertyManager,
|
||||
OpenDatabaseHook, ContactHook, ClientVersioningHook,
|
||||
IncomingMessageHook, MailboxHook {
|
||||
|
||||
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
|
||||
MailboxPropertyManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
|
||||
ClientVersioningManager clientVersioningManager,
|
||||
MetadataParser metadataParser,
|
||||
ContactGroupFactory contactGroupFactory, Clock clock,
|
||||
MailboxSettingsManager mailboxSettingsManager,
|
||||
CryptoComponent crypto) {
|
||||
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())) {
|
||||
return;
|
||||
}
|
||||
db.addGroup(txn, localGroup);
|
||||
// Set things up for any pre-existing contacts
|
||||
for (Contact c : db.getContacts(txn)) {
|
||||
addingContact(txn, c);
|
||||
}
|
||||
}
|
||||
|
||||
@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());
|
||||
// If we are paired, create and send props to the newly added contact
|
||||
MailboxProperties ownProps =
|
||||
mailboxSettingsManager.getOwnMailboxProperties(txn);
|
||||
if (ownProps != null) {
|
||||
createAndSendProperties(txn, c, ownProps.getOnion());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removingContact(Transaction txn, Contact c) throws DbException {
|
||||
db.removeGroup(txn, getContactGroup(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mailboxPaired(Transaction txn, String ownOnion)
|
||||
throws DbException {
|
||||
for (Contact c : db.getContacts(txn)) {
|
||||
createAndSendProperties(txn, c, ownOnion);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mailboxUnpaired(Transaction txn) throws DbException {
|
||||
for (Contact c : db.getContacts(txn)) {
|
||||
sendEmptyProperties(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());
|
||||
MailboxPropertiesUpdate p = parseProperties(body);
|
||||
txn.attach(new RemoteMailboxPropertiesUpdateEvent(c, p));
|
||||
// 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
|
||||
@Nullable
|
||||
public MailboxPropertiesUpdate getLocalProperties(Transaction txn,
|
||||
ContactId c) throws DbException {
|
||||
return getProperties(txn, db.getContact(txn, c), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public MailboxPropertiesUpdate getRemoteProperties(Transaction txn,
|
||||
ContactId c) throws DbException {
|
||||
return getProperties(txn, db.getContact(txn, c), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and sends an update message to the given contact. The message
|
||||
* holds our own mailbox's onion, and generated unique properties. All of
|
||||
* which the contact needs to communicate with our Mailbox.
|
||||
*/
|
||||
private void createAndSendProperties(Transaction txn,
|
||||
Contact c, String ownOnion) throws DbException {
|
||||
MailboxPropertiesUpdate p = new MailboxPropertiesUpdate(ownOnion,
|
||||
new MailboxAuthToken(crypto.generateUniqueId().getBytes()),
|
||||
new MailboxFolderId(crypto.generateUniqueId().getBytes()),
|
||||
new MailboxFolderId(crypto.generateUniqueId().getBytes()));
|
||||
Group g = getContactGroup(c);
|
||||
storeMessageReplaceLatest(txn, g.getId(), p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an empty update message to the given contact. The empty update
|
||||
* indicates for the receiving contact that we no longer have a Mailbox that
|
||||
* they can use.
|
||||
*/
|
||||
private void sendEmptyProperties(Transaction txn, Contact c)
|
||||
throws DbException {
|
||||
Group g = getContactGroup(c);
|
||||
storeMessageReplaceLatest(txn, g.getId(), null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MailboxPropertiesUpdate getProperties(Transaction txn,
|
||||
Contact c, boolean local) throws DbException {
|
||||
MailboxPropertiesUpdate p = null;
|
||||
Group g = getContactGroup(c);
|
||||
try {
|
||||
LatestUpdate latest = findLatest(txn, g.getId(), local);
|
||||
if (latest != null) {
|
||||
BdfList body =
|
||||
clientHelper.getMessageAsList(txn, latest.messageId);
|
||||
p = parseProperties(body);
|
||||
}
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
private void storeMessageReplaceLatest(Transaction txn, GroupId g,
|
||||
@Nullable MailboxPropertiesUpdate p) 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, p));
|
||||
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,
|
||||
@Nullable MailboxPropertiesUpdate p) {
|
||||
BdfDictionary dict = new BdfDictionary();
|
||||
if (p != null) {
|
||||
dict.put(PROP_KEY_ONION, p.getOnion());
|
||||
dict.put(PROP_KEY_AUTHTOKEN, p.getAuthToken().getBytes());
|
||||
dict.put(PROP_KEY_INBOXID, p.getInboxId().getBytes());
|
||||
dict.put(PROP_KEY_OUTBOXID, p.getOutboxId().getBytes());
|
||||
}
|
||||
return BdfList.of(version, dict);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MailboxPropertiesUpdate parseProperties(BdfList body)
|
||||
throws FormatException {
|
||||
BdfDictionary dict = body.getDictionary(1);
|
||||
return clientHelper.parseAndValidateMailboxPropertiesUpdate(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,28 +8,34 @@ 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.OwnMailboxConnectionStatusEvent;
|
||||
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.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
|
||||
@Immutable
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class MailboxSettingsManagerImpl implements MailboxSettingsManager {
|
||||
|
||||
// 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";
|
||||
@@ -55,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);
|
||||
}
|
||||
@@ -67,11 +74,28 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
|
||||
public void setOwnMailboxProperties(Transaction txn, MailboxProperties p)
|
||||
throws DbException {
|
||||
Settings s = new Settings();
|
||||
s.put(SETTINGS_KEY_ONION, p.getBaseUrl());
|
||||
s.put(SETTINGS_KEY_ONION, p.getOnion());
|
||||
s.put(SETTINGS_KEY_TOKEN, p.getAuthToken().toString());
|
||||
List<MailboxVersion> serverSupports = p.getServerSupports();
|
||||
encodeServerSupports(serverSupports, s);
|
||||
settingsManager.mergeSettings(txn, s, SETTINGS_NAMESPACE);
|
||||
for (MailboxHook hook : hooks) {
|
||||
hook.mailboxPaired(txn, p.getOnion());
|
||||
hook.mailboxPaired(txn, p);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeOwnMailboxProperties(Transaction txn) throws DbException {
|
||||
Settings s = new Settings();
|
||||
s.put(SETTINGS_KEY_ONION, "");
|
||||
s.put(SETTINGS_KEY_TOKEN, "");
|
||||
s.put(SETTINGS_KEY_ATTEMPTS, "");
|
||||
s.put(SETTINGS_KEY_LAST_ATTEMPT, "");
|
||||
s.put(SETTINGS_KEY_LAST_SUCCESS, "");
|
||||
s.put(SETTINGS_KEY_SERVER_SUPPORTS, "");
|
||||
settingsManager.mergeSettings(txn, s, SETTINGS_NAMESPACE);
|
||||
for (MailboxHook hook : hooks) {
|
||||
hook.mailboxUnpaired(txn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,34 +106,60 @@ 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 {
|
||||
public void recordSuccessfulConnection(Transaction txn, long now,
|
||||
List<MailboxVersion> versions) throws DbException {
|
||||
// if we no longer have a paired mailbox, return
|
||||
Settings oldSettings =
|
||||
settingsManager.getSettings(txn, SETTINGS_NAMESPACE);
|
||||
String onion = oldSettings.get(SETTINGS_KEY_ONION);
|
||||
String token = oldSettings.get(SETTINGS_KEY_TOKEN);
|
||||
if (isNullOrEmpty(onion) || isNullOrEmpty(token)) return;
|
||||
Settings s = new Settings();
|
||||
// record the successful connection
|
||||
s.putLong(SETTINGS_KEY_LAST_ATTEMPT, now);
|
||||
s.putLong(SETTINGS_KEY_LAST_SUCCESS, now);
|
||||
s.putInt(SETTINGS_KEY_ATTEMPTS, 0);
|
||||
encodeServerSupports(versions, s);
|
||||
settingsManager.mergeSettings(txn, s, SETTINGS_NAMESPACE);
|
||||
MailboxStatus status = new MailboxStatus(now, now, 0);
|
||||
for (MailboxHook hook : hooks) {
|
||||
hook.serverSupportedVersionsReceived(txn, versions);
|
||||
}
|
||||
// broadcast status event
|
||||
MailboxStatus status = new MailboxStatus(now, now, 0, versions);
|
||||
txn.attach(new OwnMailboxConnectionStatusEvent(status));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordFailedConnectionAttempt(Transaction txn, long now)
|
||||
throws DbException {
|
||||
// if we no longer have a paired mailbox, return
|
||||
Settings oldSettings =
|
||||
settingsManager.getSettings(txn, SETTINGS_NAMESPACE);
|
||||
String onion = oldSettings.get(SETTINGS_KEY_ONION);
|
||||
String token = oldSettings.get(SETTINGS_KEY_TOKEN);
|
||||
if (isNullOrEmpty(onion) || isNullOrEmpty(token)) return;
|
||||
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, newAttempts);
|
||||
settingsManager.mergeSettings(txn, newSettings, SETTINGS_NAMESPACE);
|
||||
MailboxStatus status = new MailboxStatus(now, lastSuccess, newAttempts);
|
||||
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
|
||||
@@ -131,4 +181,30 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
|
||||
if (isNullOrEmpty(filename)) return null;
|
||||
return filename;
|
||||
}
|
||||
|
||||
private void encodeServerSupports(List<MailboxVersion> serverSupports,
|
||||
Settings s) {
|
||||
int[] ints = new int[serverSupports.size() * 2];
|
||||
int i = 0;
|
||||
for (MailboxVersion v : serverSupports) {
|
||||
ints[i++] = v.getMajor();
|
||||
ints[i++] = v.getMinor();
|
||||
}
|
||||
s.putIntArray(SETTINGS_KEY_SERVER_SUPPORTS, ints);
|
||||
}
|
||||
|
||||
private List<MailboxVersion> parseServerSupports(Settings s)
|
||||
throws DbException {
|
||||
if (!s.containsKey(SETTINGS_KEY_SERVER_SUPPORTS)) return emptyList();
|
||||
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,487 @@
|
||||
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.MailboxPairedEvent;
|
||||
import org.briarproject.bramble.api.mailbox.event.MailboxUnpairedEvent;
|
||||
import org.briarproject.bramble.api.mailbox.event.MailboxUpdateSentToNewContactEvent;
|
||||
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.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE;
|
||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||
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, false);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
addingContact(txn, c, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param attachEvent True if a {@link MailboxUpdateSentToNewContactEvent}
|
||||
* should be attached to the transaction. We should only do this when
|
||||
* adding a new contact, not when setting up this client for an existing
|
||||
* contact.
|
||||
*/
|
||||
private void addingContact(Transaction txn, Contact c, boolean attachEvent)
|
||||
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);
|
||||
MailboxUpdate u;
|
||||
if (ownProps != null) {
|
||||
// We are paired, create and send props to the newly added contact
|
||||
u = createAndSendUpdateWithMailbox(txn, c,
|
||||
ownProps.getServerSupports(), ownProps.getOnion());
|
||||
} else {
|
||||
// Not paired, but we still want to get our clientSupports sent
|
||||
u = sendUpdateNoMailbox(txn, c);
|
||||
}
|
||||
if (attachEvent) {
|
||||
txn.attach(new MailboxUpdateSentToNewContactEvent(c.getId(), u));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removingContact(Transaction txn, Contact c) throws DbException {
|
||||
db.removeGroup(txn, getContactGroup(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mailboxPaired(Transaction txn, MailboxProperties p)
|
||||
throws DbException {
|
||||
Map<ContactId, MailboxUpdateWithMailbox> localUpdates = new HashMap<>();
|
||||
for (Contact c : db.getContacts(txn)) {
|
||||
MailboxUpdateWithMailbox u = createAndSendUpdateWithMailbox(txn, c,
|
||||
p.getServerSupports(), p.getOnion());
|
||||
localUpdates.put(c.getId(), u);
|
||||
}
|
||||
txn.attach(new MailboxPairedEvent(p, localUpdates));
|
||||
// Store the list of server-supported versions
|
||||
try {
|
||||
storeSentServerSupports(txn, p.getServerSupports());
|
||||
} catch (FormatException e) {
|
||||
throw new DbException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mailboxUnpaired(Transaction txn) throws DbException {
|
||||
Map<ContactId, MailboxUpdate> localUpdates = new HashMap<>();
|
||||
for (Contact c : db.getContacts(txn)) {
|
||||
MailboxUpdate u = sendUpdateNoMailbox(txn, c);
|
||||
localUpdates.put(c.getId(), u);
|
||||
}
|
||||
txn.attach(new MailboxUnpairedEvent(localUpdates));
|
||||
// Remove the list of server-supported versions
|
||||
try {
|
||||
BdfDictionary meta = BdfDictionary.of(new BdfEntry(
|
||||
GROUP_KEY_SENT_SERVER_SUPPORTS, NULL_VALUE));
|
||||
clientHelper.mergeGroupMetadata(txn, localGroup.getId(), meta);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serverSupportedVersionsReceived(Transaction txn,
|
||||
List<MailboxVersion> serverSupports) throws DbException {
|
||||
try {
|
||||
List<MailboxVersion> oldServerSupports =
|
||||
loadSentServerSupports(txn);
|
||||
if (serverSupports.equals(oldServerSupports)) return;
|
||||
storeSentServerSupports(txn, serverSupports);
|
||||
for (Contact c : db.getContacts(txn)) {
|
||||
Group contactGroup = getContactGroup(c);
|
||||
LatestUpdate latest =
|
||||
findLatest(txn, contactGroup.getId(), true);
|
||||
// This method should only be called when we have a mailbox,
|
||||
// in which case we should have sent a local update to every
|
||||
// contact
|
||||
if (latest == null) throw new DbException();
|
||||
BdfList body =
|
||||
clientHelper.getMessageAsList(txn, latest.messageId);
|
||||
MailboxUpdate oldUpdate = parseUpdate(body);
|
||||
if (!oldUpdate.hasMailbox()) throw new DbException();
|
||||
MailboxUpdateWithMailbox newUpdate = updateServerSupports(
|
||||
(MailboxUpdateWithMailbox) oldUpdate, serverSupports);
|
||||
storeMessageReplaceLatest(txn, contactGroup.getId(), newUpdate,
|
||||
latest);
|
||||
}
|
||||
} catch (FormatException e) {
|
||||
throw new DbException();
|
||||
}
|
||||
}
|
||||
|
||||
private void storeSentServerSupports(Transaction txn,
|
||||
List<MailboxVersion> serverSupports)
|
||||
throws DbException, FormatException {
|
||||
BdfDictionary meta = BdfDictionary.of(new BdfEntry(
|
||||
GROUP_KEY_SENT_SERVER_SUPPORTS,
|
||||
encodeSupportsList(serverSupports)));
|
||||
clientHelper.mergeGroupMetadata(txn, localGroup.getId(), meta);
|
||||
}
|
||||
|
||||
private List<MailboxVersion> loadSentServerSupports(Transaction txn)
|
||||
throws DbException, FormatException {
|
||||
BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn,
|
||||
localGroup.getId());
|
||||
BdfList serverSupports =
|
||||
meta.getOptionalList(GROUP_KEY_SENT_SERVER_SUPPORTS);
|
||||
if (serverSupports == null) return emptyList();
|
||||
return clientHelper.parseMailboxVersionList(serverSupports);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link MailboxUpdateWithMailbox} that updates the list
|
||||
* of server-supported API versions in the given
|
||||
* {@link MailboxUpdateWithMailbox}.
|
||||
*/
|
||||
private MailboxUpdateWithMailbox updateServerSupports(
|
||||
MailboxUpdateWithMailbox old, List<MailboxVersion> serverSupports) {
|
||||
MailboxProperties oldProps = old.getMailboxProperties();
|
||||
MailboxProperties newProps = new MailboxProperties(oldProps.getOnion(),
|
||||
oldProps.getAuthToken(), serverSupports,
|
||||
requireNonNull(oldProps.getInboxId()),
|
||||
requireNonNull(oldProps.getOutboxId()));
|
||||
return new MailboxUpdateWithMailbox(old.getClientSupports(), newProps);
|
||||
}
|
||||
|
||||
@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 MailboxUpdateWithMailbox createAndSendUpdateWithMailbox(
|
||||
Transaction txn, Contact c, List<MailboxVersion> serverSupports,
|
||||
String ownOnion) throws DbException {
|
||||
MailboxProperties properties = new MailboxProperties(ownOnion,
|
||||
new MailboxAuthToken(crypto.generateUniqueId().getBytes()),
|
||||
serverSupports,
|
||||
new MailboxFolderId(crypto.generateUniqueId().getBytes()),
|
||||
new MailboxFolderId(crypto.generateUniqueId().getBytes()));
|
||||
MailboxUpdateWithMailbox u =
|
||||
new MailboxUpdateWithMailbox(clientSupports, properties);
|
||||
Group g = getContactGroup(c);
|
||||
storeMessageReplaceLatest(txn, g.getId(), u);
|
||||
return 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 MailboxUpdate sendUpdateNoMailbox(Transaction txn, Contact c)
|
||||
throws DbException {
|
||||
Group g = getContactGroup(c);
|
||||
MailboxUpdate u = new MailboxUpdate(clientSupports);
|
||||
storeMessageReplaceLatest(txn, g.getId(), u);
|
||||
return 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);
|
||||
storeMessageReplaceLatest(txn, g, u, latest);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void storeMessageReplaceLatest(Transaction txn, GroupId g,
|
||||
MailboxUpdate u, @Nullable LatestUpdate latest)
|
||||
throws DbException, FormatException {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,15 @@ import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.MSG_KEY_LOCAL;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.MSG_KEY_VERSION;
|
||||
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 MailboxPropertyValidator extends BdfMessageValidator {
|
||||
class MailboxUpdateValidator extends BdfMessageValidator {
|
||||
|
||||
MailboxPropertyValidator(ClientHelper clientHelper,
|
||||
MailboxUpdateValidator(ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock) {
|
||||
super(clientHelper, metadataEncoder, clock);
|
||||
}
|
||||
@@ -31,14 +31,19 @@ class MailboxPropertyValidator extends BdfMessageValidator {
|
||||
@Override
|
||||
protected BdfMessageContext validateMessage(Message m, Group g,
|
||||
BdfList body) throws InvalidMessageException, FormatException {
|
||||
// Version, properties
|
||||
checkSize(body, 2);
|
||||
// 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(1);
|
||||
clientHelper.parseAndValidateMailboxPropertiesUpdate(dictionary);
|
||||
BdfDictionary dictionary = body.getDictionary(3);
|
||||
clientHelper.parseAndValidateMailboxUpdate(clientSupports,
|
||||
serverSupports, dictionary);
|
||||
// Return the metadata
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(MSG_KEY_VERSION, version);
|
||||
@@ -0,0 +1,464 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.connection.ConnectionRegistry;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventExecutor;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
|
||||
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
import org.briarproject.bramble.api.sync.event.GroupVisibilityUpdatedEvent;
|
||||
import org.briarproject.bramble.api.sync.event.MessageSharedEvent;
|
||||
import org.briarproject.bramble.api.sync.event.MessageToAckEvent;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.MAX_LATENCY;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||
import static org.briarproject.bramble.util.IoUtils.delete;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class MailboxUploadWorker implements MailboxWorker, ConnectivityObserver,
|
||||
EventListener {
|
||||
|
||||
/**
|
||||
* When the worker is started it checks for data to send. If data is ready
|
||||
* to send, the worker waits for a connectivity check, then writes and
|
||||
* uploads a file and checks again for data to send.
|
||||
* <p>
|
||||
* If data is due to be sent at some time in the future, the worker
|
||||
* schedules a wakeup for that time and also listens for events indicating
|
||||
* that new data may be ready to send.
|
||||
* <p>
|
||||
* If there's no data to send, the worker listens for events indicating
|
||||
* that new data may be ready to send.
|
||||
* <p>
|
||||
* Whenever we're directly connected to the contact, the worker doesn't
|
||||
* check for data to send or start connectivity checks until the contact
|
||||
* disconnects. However, if the worker has already started writing and
|
||||
* uploading a file when the contact connects, the worker will finish the
|
||||
* upload.
|
||||
*/
|
||||
private enum State {
|
||||
CREATED,
|
||||
CONNECTED_TO_CONTACT,
|
||||
CHECKING_FOR_DATA,
|
||||
WAITING_FOR_DATA,
|
||||
CONNECTIVITY_CHECK,
|
||||
WRITING_UPLOADING,
|
||||
DESTROYED
|
||||
}
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(MailboxUploadWorker.class.getName());
|
||||
|
||||
/**
|
||||
* When we're waiting for data to send and an event indicates that new data
|
||||
* may have become available, wait this long before checking the DB. This
|
||||
* should help to avoid creating lots of small files when several acks or
|
||||
* messages become available to send in a short period (eg when reading a
|
||||
* file downloaded from a mailbox).
|
||||
* <p>
|
||||
* Package access for testing.
|
||||
*/
|
||||
static final long CHECK_DELAY_MS = 5_000;
|
||||
|
||||
/**
|
||||
* How long to wait before retrying when an exception occurs while writing
|
||||
* a file.
|
||||
* <p>
|
||||
* Package access for testing.
|
||||
*/
|
||||
static final long RETRY_DELAY_MS = MINUTES.toMillis(1);
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final DatabaseComponent db;
|
||||
private final Clock clock;
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final EventBus eventBus;
|
||||
private final ConnectionRegistry connectionRegistry;
|
||||
private final ConnectivityChecker connectivityChecker;
|
||||
private final MailboxApiCaller mailboxApiCaller;
|
||||
private final MailboxApi mailboxApi;
|
||||
private final MailboxFileManager mailboxFileManager;
|
||||
private final MailboxProperties mailboxProperties;
|
||||
private final MailboxFolderId folderId;
|
||||
private final ContactId contactId;
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
@GuardedBy("lock")
|
||||
private State state = State.CREATED;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private Cancellable wakeupTask = null, checkTask = null, apiCall = null;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private File file = null;
|
||||
|
||||
MailboxUploadWorker(@IoExecutor Executor ioExecutor,
|
||||
DatabaseComponent db,
|
||||
Clock clock,
|
||||
TaskScheduler taskScheduler,
|
||||
EventBus eventBus,
|
||||
ConnectionRegistry connectionRegistry,
|
||||
ConnectivityChecker connectivityChecker,
|
||||
MailboxApiCaller mailboxApiCaller,
|
||||
MailboxApi mailboxApi,
|
||||
MailboxFileManager mailboxFileManager,
|
||||
MailboxProperties mailboxProperties,
|
||||
MailboxFolderId folderId,
|
||||
ContactId contactId) {
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.db = db;
|
||||
this.clock = clock;
|
||||
this.taskScheduler = taskScheduler;
|
||||
this.eventBus = eventBus;
|
||||
this.connectionRegistry = connectionRegistry;
|
||||
this.connectivityChecker = connectivityChecker;
|
||||
this.mailboxApiCaller = mailboxApiCaller;
|
||||
this.mailboxApi = mailboxApi;
|
||||
this.mailboxFileManager = mailboxFileManager;
|
||||
this.mailboxProperties = mailboxProperties;
|
||||
this.folderId = folderId;
|
||||
this.contactId = contactId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
LOG.info("Started");
|
||||
synchronized (lock) {
|
||||
// Don't allow the worker to be reused
|
||||
if (state != State.CREATED) return;
|
||||
state = State.CHECKING_FOR_DATA;
|
||||
}
|
||||
ioExecutor.execute(this::checkForDataToSend);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
LOG.info("Destroyed");
|
||||
Cancellable wakeupTask, checkTask, apiCall;
|
||||
File file;
|
||||
synchronized (lock) {
|
||||
state = State.DESTROYED;
|
||||
wakeupTask = this.wakeupTask;
|
||||
this.wakeupTask = null;
|
||||
checkTask = this.checkTask;
|
||||
this.checkTask = null;
|
||||
apiCall = this.apiCall;
|
||||
this.apiCall = null;
|
||||
file = this.file;
|
||||
this.file = null;
|
||||
}
|
||||
if (wakeupTask != null) wakeupTask.cancel();
|
||||
if (checkTask != null) checkTask.cancel();
|
||||
if (apiCall != null) apiCall.cancel();
|
||||
if (file != null) delete(file);
|
||||
connectivityChecker.removeObserver(this);
|
||||
eventBus.removeListener(this);
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void checkForDataToSend() {
|
||||
synchronized (lock) {
|
||||
checkTask = null;
|
||||
if (state != State.CHECKING_FOR_DATA) return;
|
||||
// Check whether we're directly connected to the contact. Calling
|
||||
// this while holding the lock isn't ideal, but it avoids races
|
||||
if (connectionRegistry.isConnected(contactId)) {
|
||||
state = State.CONNECTED_TO_CONTACT;
|
||||
return;
|
||||
}
|
||||
}
|
||||
LOG.info("Checking for data to send");
|
||||
try {
|
||||
db.transaction(true, txn -> {
|
||||
long nextSendTime;
|
||||
if (db.containsAcksToSend(txn, contactId)) {
|
||||
nextSendTime = 0L;
|
||||
} else {
|
||||
nextSendTime = db.getNextSendTime(txn, contactId,
|
||||
MAX_LATENCY);
|
||||
}
|
||||
// Handle the result on the event executor to avoid races with
|
||||
// incoming events
|
||||
txn.attach(() -> handleNextSendTime(nextSendTime));
|
||||
});
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void handleNextSendTime(long nextSendTime) {
|
||||
if (nextSendTime == Long.MAX_VALUE) {
|
||||
// Nothing is sendable now or due to be sent in the future. Wait
|
||||
// for an event indicating that new data may be ready to send
|
||||
waitForDataToSend();
|
||||
} else {
|
||||
// Work out the delay until data's ready to send (may be negative)
|
||||
long delay = nextSendTime - clock.currentTimeMillis();
|
||||
if (delay > 0) {
|
||||
// Schedule a wakeup when data will be ready to send. If an
|
||||
// event is received in the meantime indicating that new data
|
||||
// may be ready to send, we'll cancel the wakeup
|
||||
scheduleWakeup(delay);
|
||||
} else {
|
||||
// Data is ready to send now
|
||||
checkConnectivity();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void waitForDataToSend() {
|
||||
synchronized (lock) {
|
||||
if (state != State.CHECKING_FOR_DATA) return;
|
||||
state = State.WAITING_FOR_DATA;
|
||||
LOG.info("Waiting for data to send");
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void scheduleWakeup(long delay) {
|
||||
synchronized (lock) {
|
||||
if (state != State.CHECKING_FOR_DATA) return;
|
||||
state = State.WAITING_FOR_DATA;
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Scheduling wakeup in " + delay + " ms");
|
||||
}
|
||||
wakeupTask = taskScheduler.schedule(this::wakeUp, ioExecutor,
|
||||
delay, MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void wakeUp() {
|
||||
LOG.info("Woke up");
|
||||
synchronized (lock) {
|
||||
wakeupTask = null;
|
||||
if (state != State.WAITING_FOR_DATA) return;
|
||||
state = State.CHECKING_FOR_DATA;
|
||||
}
|
||||
checkForDataToSend();
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void checkConnectivity() {
|
||||
synchronized (lock) {
|
||||
if (state != State.CHECKING_FOR_DATA) return;
|
||||
state = State.CONNECTIVITY_CHECK;
|
||||
}
|
||||
LOG.info("Checking connectivity");
|
||||
// Avoid leaking observer in case destroy() is called concurrently
|
||||
// before observer is added
|
||||
connectivityChecker.checkConnectivity(mailboxProperties, this);
|
||||
boolean destroyed;
|
||||
synchronized (lock) {
|
||||
destroyed = state == State.DESTROYED;
|
||||
}
|
||||
if (destroyed) connectivityChecker.removeObserver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectivityCheckSucceeded() {
|
||||
LOG.info("Connectivity check succeeded");
|
||||
synchronized (lock) {
|
||||
if (state != State.CONNECTIVITY_CHECK) return;
|
||||
state = State.WRITING_UPLOADING;
|
||||
}
|
||||
ioExecutor.execute(this::writeAndUploadFile);
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void writeAndUploadFile() {
|
||||
synchronized (lock) {
|
||||
if (state != State.WRITING_UPLOADING) return;
|
||||
}
|
||||
OutgoingSessionRecord sessionRecord = new OutgoingSessionRecord();
|
||||
File file;
|
||||
try {
|
||||
file = mailboxFileManager.createAndWriteTempFileForUpload(
|
||||
contactId, sessionRecord);
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
// Try again after a delay
|
||||
synchronized (lock) {
|
||||
if (state != State.WRITING_UPLOADING) return;
|
||||
state = State.CHECKING_FOR_DATA;
|
||||
checkTask = taskScheduler.schedule(this::checkForDataToSend,
|
||||
ioExecutor, RETRY_DELAY_MS, MILLISECONDS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
boolean deleteFile = false;
|
||||
synchronized (lock) {
|
||||
if (state == State.WRITING_UPLOADING) {
|
||||
this.file = file;
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
new SimpleApiCall(() -> apiCallUploadFile(file,
|
||||
sessionRecord)));
|
||||
} else {
|
||||
deleteFile = true;
|
||||
}
|
||||
}
|
||||
if (deleteFile) delete(file);
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void apiCallUploadFile(File file,
|
||||
OutgoingSessionRecord sessionRecord)
|
||||
throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state != State.WRITING_UPLOADING) return;
|
||||
}
|
||||
LOG.info("Uploading file");
|
||||
mailboxApi.addFile(mailboxProperties, folderId, file);
|
||||
markMessagesSentOrAcked(sessionRecord);
|
||||
delete(file);
|
||||
synchronized (lock) {
|
||||
if (state != State.WRITING_UPLOADING) return;
|
||||
state = State.CHECKING_FOR_DATA;
|
||||
apiCall = null;
|
||||
this.file = null;
|
||||
}
|
||||
checkForDataToSend();
|
||||
}
|
||||
|
||||
private void markMessagesSentOrAcked(OutgoingSessionRecord sessionRecord) {
|
||||
Collection<MessageId> acked = sessionRecord.getAckedIds();
|
||||
Collection<MessageId> sent = sessionRecord.getSentIds();
|
||||
try {
|
||||
db.transaction(false, txn -> {
|
||||
if (!acked.isEmpty()) {
|
||||
db.setAckSent(txn, contactId, acked);
|
||||
}
|
||||
if (!sent.isEmpty()) {
|
||||
db.setMessagesSent(txn, contactId, sent, MAX_LATENCY);
|
||||
}
|
||||
});
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof MessageToAckEvent) {
|
||||
MessageToAckEvent m = (MessageToAckEvent) e;
|
||||
if (m.getContactId().equals(contactId)) {
|
||||
LOG.info("Message to ack");
|
||||
onDataToSend();
|
||||
}
|
||||
} else if (e instanceof MessageSharedEvent) {
|
||||
MessageSharedEvent m = (MessageSharedEvent) e;
|
||||
// If the contact is present in the map (ie the value is not null)
|
||||
// and the value is true, the message's group is shared with the
|
||||
// contact and therefore the message may now be sendable
|
||||
if (m.getGroupVisibility().get(contactId) == TRUE) {
|
||||
LOG.info("Message shared");
|
||||
onDataToSend();
|
||||
}
|
||||
} else if (e instanceof GroupVisibilityUpdatedEvent) {
|
||||
GroupVisibilityUpdatedEvent g = (GroupVisibilityUpdatedEvent) e;
|
||||
if (g.getVisibility() == SHARED &&
|
||||
g.getAffectedContacts().contains(contactId)) {
|
||||
LOG.info("Group shared");
|
||||
onDataToSend();
|
||||
}
|
||||
} else if (e instanceof ContactConnectedEvent) {
|
||||
ContactConnectedEvent c = (ContactConnectedEvent) e;
|
||||
if (c.getContactId().equals(contactId)) {
|
||||
LOG.info("Contact connected");
|
||||
onContactConnected();
|
||||
}
|
||||
} else if (e instanceof ContactDisconnectedEvent) {
|
||||
ContactDisconnectedEvent c = (ContactDisconnectedEvent) e;
|
||||
if (c.getContactId().equals(contactId)) {
|
||||
LOG.info("Contact disconnected");
|
||||
onContactDisconnected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onDataToSend() {
|
||||
Cancellable wakeupTask;
|
||||
synchronized (lock) {
|
||||
if (state != State.WAITING_FOR_DATA) return;
|
||||
state = State.CHECKING_FOR_DATA;
|
||||
wakeupTask = this.wakeupTask;
|
||||
this.wakeupTask = null;
|
||||
// Delay the check to avoid creating lots of small files
|
||||
checkTask = taskScheduler.schedule(this::checkForDataToSend,
|
||||
ioExecutor, CHECK_DELAY_MS, MILLISECONDS);
|
||||
}
|
||||
// If we had scheduled a wakeup when data was due to be sent, cancel it
|
||||
if (wakeupTask != null) wakeupTask.cancel();
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onContactConnected() {
|
||||
Cancellable wakeupTask = null, checkTask = null;
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
// If we're checking for data to send, waiting for data to send,
|
||||
// or checking connectivity then wait until we disconnect from
|
||||
// the contact before proceeding. If we're writing or uploading
|
||||
// a file then continue
|
||||
if (state == State.CHECKING_FOR_DATA ||
|
||||
state == State.WAITING_FOR_DATA ||
|
||||
state == State.CONNECTIVITY_CHECK) {
|
||||
state = State.CONNECTED_TO_CONTACT;
|
||||
wakeupTask = this.wakeupTask;
|
||||
this.wakeupTask = null;
|
||||
checkTask = this.checkTask;
|
||||
this.checkTask = null;
|
||||
}
|
||||
}
|
||||
if (wakeupTask != null) wakeupTask.cancel();
|
||||
if (checkTask != null) checkTask.cancel();
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onContactDisconnected() {
|
||||
synchronized (lock) {
|
||||
if (state != State.CONNECTED_TO_CONTACT) return;
|
||||
state = State.CHECKING_FOR_DATA;
|
||||
}
|
||||
ioExecutor.execute(this::checkForDataToSend);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* A worker that downloads files from a contact's mailbox.
|
||||
*/
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
interface MailboxWorker {
|
||||
|
||||
/**
|
||||
* Asynchronously starts the worker.
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Destroys the worker and cancels any pending tasks or retries.
|
||||
*/
|
||||
void destroy();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
interface MailboxWorkerFactory {
|
||||
|
||||
MailboxWorker createUploadWorker(ConnectivityChecker connectivityChecker,
|
||||
MailboxProperties properties, MailboxFolderId folderId,
|
||||
ContactId contactId);
|
||||
|
||||
MailboxWorker createDownloadWorkerForContactMailbox(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor reachabilityMonitor,
|
||||
MailboxProperties properties);
|
||||
|
||||
MailboxWorker createDownloadWorkerForOwnMailbox(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor reachabilityMonitor,
|
||||
MailboxProperties properties);
|
||||
|
||||
MailboxWorker createContactListWorkerForOwnMailbox(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
MailboxProperties properties);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.connection.ConnectionRegistry;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class MailboxWorkerFactoryImpl implements MailboxWorkerFactory {
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final DatabaseComponent db;
|
||||
private final Clock clock;
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final EventBus eventBus;
|
||||
private final ConnectionRegistry connectionRegistry;
|
||||
private final MailboxApiCaller mailboxApiCaller;
|
||||
private final MailboxApi mailboxApi;
|
||||
private final MailboxFileManager mailboxFileManager;
|
||||
private final MailboxUpdateManager mailboxUpdateManager;
|
||||
|
||||
@Inject
|
||||
MailboxWorkerFactoryImpl(@IoExecutor Executor ioExecutor,
|
||||
DatabaseComponent db,
|
||||
Clock clock,
|
||||
TaskScheduler taskScheduler,
|
||||
EventBus eventBus,
|
||||
ConnectionRegistry connectionRegistry,
|
||||
MailboxApiCaller mailboxApiCaller,
|
||||
MailboxApi mailboxApi,
|
||||
MailboxFileManager mailboxFileManager,
|
||||
MailboxUpdateManager mailboxUpdateManager) {
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.db = db;
|
||||
this.clock = clock;
|
||||
this.taskScheduler = taskScheduler;
|
||||
this.eventBus = eventBus;
|
||||
this.connectionRegistry = connectionRegistry;
|
||||
this.mailboxApiCaller = mailboxApiCaller;
|
||||
this.mailboxApi = mailboxApi;
|
||||
this.mailboxFileManager = mailboxFileManager;
|
||||
this.mailboxUpdateManager = mailboxUpdateManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxWorker createUploadWorker(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
MailboxProperties properties, MailboxFolderId folderId,
|
||||
ContactId contactId) {
|
||||
MailboxUploadWorker worker = new MailboxUploadWorker(ioExecutor, db,
|
||||
clock, taskScheduler, eventBus, connectionRegistry,
|
||||
connectivityChecker, mailboxApiCaller, mailboxApi,
|
||||
mailboxFileManager, properties, folderId, contactId);
|
||||
eventBus.addListener(worker);
|
||||
return worker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxWorker createDownloadWorkerForContactMailbox(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor reachabilityMonitor,
|
||||
MailboxProperties properties) {
|
||||
return new ContactMailboxDownloadWorker(connectivityChecker,
|
||||
reachabilityMonitor, mailboxApiCaller, mailboxApi,
|
||||
mailboxFileManager, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxWorker createDownloadWorkerForOwnMailbox(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor reachabilityMonitor,
|
||||
MailboxProperties properties) {
|
||||
return new OwnMailboxDownloadWorker(connectivityChecker,
|
||||
reachabilityMonitor, mailboxApiCaller, mailboxApi,
|
||||
mailboxFileManager, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MailboxWorker createContactListWorkerForOwnMailbox(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
MailboxProperties properties) {
|
||||
OwnMailboxContactListWorker worker = new OwnMailboxContactListWorker(
|
||||
ioExecutor, db, eventBus, connectivityChecker, mailboxApiCaller,
|
||||
mailboxApi, mailboxUpdateManager, properties);
|
||||
eventBus.addListener(worker);
|
||||
return worker;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.HOURS;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class OwnMailboxClient implements MailboxClient, ConnectivityObserver {
|
||||
|
||||
/**
|
||||
* How often to check our own mailbox's connectivity.
|
||||
* <p>
|
||||
* Package access for testing.
|
||||
*/
|
||||
static final long CONNECTIVITY_CHECK_INTERVAL_MS = HOURS.toMillis(1);
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(OwnMailboxClient.class.getName());
|
||||
|
||||
private final MailboxWorkerFactory workerFactory;
|
||||
private final ConnectivityChecker connectivityChecker;
|
||||
private final TorReachabilityMonitor reachabilityMonitor;
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final Executor ioExecutor;
|
||||
private final MailboxProperties properties;
|
||||
private final MailboxWorker contactListWorker;
|
||||
private final Object lock = new Object();
|
||||
|
||||
/**
|
||||
* Upload workers: one worker per contact assigned for upload.
|
||||
*/
|
||||
@GuardedBy("lock")
|
||||
private final Map<ContactId, MailboxWorker> uploadWorkers = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Download worker: shared between all contacts assigned for download.
|
||||
* Null if no contacts are assigned for download.
|
||||
*/
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private MailboxWorker downloadWorker = null;
|
||||
|
||||
/**
|
||||
* IDs of contacts assigned for download, so that we know when to
|
||||
* create/destroy the download worker.
|
||||
*/
|
||||
@GuardedBy("lock")
|
||||
private final Set<ContactId> assignedForDownload = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Scheduled task for periodically checking whether the mailbox is
|
||||
* reachable.
|
||||
*/
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private Cancellable connectivityTask = null;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private boolean destroyed = false;
|
||||
|
||||
OwnMailboxClient(MailboxWorkerFactory workerFactory,
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor reachabilityMonitor,
|
||||
TaskScheduler taskScheduler,
|
||||
@IoExecutor Executor ioExecutor,
|
||||
MailboxProperties properties) {
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
this.workerFactory = workerFactory;
|
||||
this.connectivityChecker = connectivityChecker;
|
||||
this.reachabilityMonitor = reachabilityMonitor;
|
||||
this.taskScheduler = taskScheduler;
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.properties = properties;
|
||||
contactListWorker = workerFactory.createContactListWorkerForOwnMailbox(
|
||||
connectivityChecker, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
LOG.info("Started");
|
||||
checkConnectivity();
|
||||
contactListWorker.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
LOG.info("Destroyed");
|
||||
List<MailboxWorker> uploadWorkers;
|
||||
MailboxWorker downloadWorker;
|
||||
Cancellable connectivityTask;
|
||||
synchronized (lock) {
|
||||
uploadWorkers = new ArrayList<>(this.uploadWorkers.values());
|
||||
this.uploadWorkers.clear();
|
||||
downloadWorker = this.downloadWorker;
|
||||
this.downloadWorker = null;
|
||||
connectivityTask = this.connectivityTask;
|
||||
this.connectivityTask = null;
|
||||
destroyed = true;
|
||||
}
|
||||
// Destroy the workers (with apologies to Mr Marx and Mr Engels)
|
||||
for (MailboxWorker worker : uploadWorkers) worker.destroy();
|
||||
if (downloadWorker != null) downloadWorker.destroy();
|
||||
// If a connectivity check is scheduled, cancel it
|
||||
if (connectivityTask != null) connectivityTask.cancel();
|
||||
contactListWorker.destroy();
|
||||
// The connectivity checker belongs to the client, so it should be
|
||||
// destroyed. The Tor reachability monitor is shared between clients,
|
||||
// so it should not be destroyed
|
||||
connectivityChecker.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assignContactForUpload(ContactId contactId,
|
||||
MailboxProperties properties, MailboxFolderId folderId) {
|
||||
LOG.info("Contact assigned for upload");
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
MailboxWorker uploadWorker = workerFactory.createUploadWorker(
|
||||
connectivityChecker, properties, folderId, contactId);
|
||||
synchronized (lock) {
|
||||
MailboxWorker old = uploadWorkers.put(contactId, uploadWorker);
|
||||
if (old != null) throw new IllegalStateException();
|
||||
}
|
||||
uploadWorker.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deassignContactForUpload(ContactId contactId) {
|
||||
LOG.info("Contact deassigned for upload");
|
||||
MailboxWorker uploadWorker;
|
||||
synchronized (lock) {
|
||||
uploadWorker = uploadWorkers.remove(contactId);
|
||||
}
|
||||
if (uploadWorker != null) uploadWorker.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assignContactForDownload(ContactId contactId,
|
||||
MailboxProperties properties, MailboxFolderId folderId) {
|
||||
LOG.info("Contact assigned for download");
|
||||
if (!properties.isOwner()) throw new IllegalArgumentException();
|
||||
// Create a download worker if we don't already have one. The worker
|
||||
// will use the API to discover which folders have files to download,
|
||||
// so it doesn't need to track the set of assigned contacts
|
||||
MailboxWorker toStart = null;
|
||||
synchronized (lock) {
|
||||
if (!assignedForDownload.add(contactId)) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (downloadWorker == null) {
|
||||
toStart = workerFactory.createDownloadWorkerForOwnMailbox(
|
||||
connectivityChecker, reachabilityMonitor, properties);
|
||||
downloadWorker = toStart;
|
||||
}
|
||||
}
|
||||
if (toStart != null) toStart.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deassignContactForDownload(ContactId contactId) {
|
||||
LOG.info("Contact deassigned for download");
|
||||
// If there are no more contacts assigned for download, destroy the
|
||||
// download worker
|
||||
MailboxWorker toDestroy = null;
|
||||
synchronized (lock) {
|
||||
if (!assignedForDownload.remove(contactId)) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (assignedForDownload.isEmpty()) {
|
||||
toDestroy = downloadWorker;
|
||||
downloadWorker = null;
|
||||
}
|
||||
}
|
||||
if (toDestroy != null) toDestroy.destroy();
|
||||
}
|
||||
|
||||
private void checkConnectivity() {
|
||||
synchronized (lock) {
|
||||
if (destroyed) return;
|
||||
connectivityTask = null;
|
||||
}
|
||||
connectivityChecker.checkConnectivity(properties, this);
|
||||
// Avoid leaking observer in case destroy() is called concurrently
|
||||
// before observer is added
|
||||
boolean removeObserver;
|
||||
synchronized (lock) {
|
||||
removeObserver = destroyed;
|
||||
}
|
||||
if (removeObserver) connectivityChecker.removeObserver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectivityCheckSucceeded() {
|
||||
synchronized (lock) {
|
||||
if (destroyed) return;
|
||||
connectivityTask = taskScheduler.schedule(this::checkConnectivity,
|
||||
ioExecutor, CONNECTIVITY_CHECK_INTERVAL_MS, MILLISECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
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.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
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;
|
||||
|
||||
@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;
|
||||
|
||||
@Inject
|
||||
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 {
|
||||
LOG.info("Checking whether own mailbox is reachable");
|
||||
List<MailboxVersion> serverSupports =
|
||||
mailboxApi.getServerSupports(properties);
|
||||
LOG.info("Own mailbox is reachable");
|
||||
long now = clock.currentTimeMillis();
|
||||
db.transaction(false, txn -> mailboxSettingsManager
|
||||
.recordSuccessfulConnection(txn, now, serverSupports));
|
||||
// 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,369 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
|
||||
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.NoSuchContactException;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventExecutor;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdate;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateWithMailbox;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.MailboxContact;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class OwnMailboxContactListWorker
|
||||
implements MailboxWorker, ConnectivityObserver, EventListener {
|
||||
|
||||
/**
|
||||
* When the worker is started it waits for a connectivity check, then
|
||||
* fetches the remote contact list and compares it to the local contact
|
||||
* list.
|
||||
* <p>
|
||||
* Any contacts that are missing from the remote list are added to the
|
||||
* mailbox's contact list, while any contacts that are missing from the
|
||||
* local list are removed from the mailbox's contact list.
|
||||
* <p>
|
||||
* Once the remote contact list has been brought up to date, the worker
|
||||
* waits for events indicating that contacts have been added or removed.
|
||||
* Each time an event is received, the worker updates the mailbox's
|
||||
* contact list and then goes back to waiting.
|
||||
*/
|
||||
private enum State {
|
||||
CREATED,
|
||||
CONNECTIVITY_CHECK,
|
||||
FETCHING_CONTACT_LIST,
|
||||
UPDATING_CONTACT_LIST,
|
||||
WAITING_FOR_CHANGES,
|
||||
DESTROYED
|
||||
}
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(OwnMailboxContactListWorker.class.getName());
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final DatabaseComponent db;
|
||||
private final EventBus eventBus;
|
||||
private final ConnectivityChecker connectivityChecker;
|
||||
private final MailboxApiCaller mailboxApiCaller;
|
||||
private final MailboxApi mailboxApi;
|
||||
private final MailboxUpdateManager mailboxUpdateManager;
|
||||
private final MailboxProperties mailboxProperties;
|
||||
private final Object lock = new Object();
|
||||
|
||||
@GuardedBy("lock")
|
||||
private State state = State.CREATED;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private Cancellable apiCall = null;
|
||||
|
||||
/**
|
||||
* A queue of updates waiting to be applied to the remote contact list.
|
||||
*/
|
||||
@GuardedBy("lock")
|
||||
private final Queue<Update> updates = new LinkedList<>();
|
||||
|
||||
OwnMailboxContactListWorker(@IoExecutor Executor ioExecutor,
|
||||
DatabaseComponent db,
|
||||
EventBus eventBus,
|
||||
ConnectivityChecker connectivityChecker,
|
||||
MailboxApiCaller mailboxApiCaller,
|
||||
MailboxApi mailboxApi,
|
||||
MailboxUpdateManager mailboxUpdateManager,
|
||||
MailboxProperties mailboxProperties) {
|
||||
if (!mailboxProperties.isOwner()) throw new IllegalArgumentException();
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.db = db;
|
||||
this.connectivityChecker = connectivityChecker;
|
||||
this.mailboxApiCaller = mailboxApiCaller;
|
||||
this.mailboxApi = mailboxApi;
|
||||
this.mailboxUpdateManager = mailboxUpdateManager;
|
||||
this.mailboxProperties = mailboxProperties;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
LOG.info("Started");
|
||||
synchronized (lock) {
|
||||
if (state != State.CREATED) return;
|
||||
state = State.CONNECTIVITY_CHECK;
|
||||
}
|
||||
// Avoid leaking observer in case destroy() is called concurrently
|
||||
// before observer is added
|
||||
connectivityChecker.checkConnectivity(mailboxProperties, this);
|
||||
boolean destroyed;
|
||||
synchronized (lock) {
|
||||
destroyed = state == State.DESTROYED;
|
||||
}
|
||||
if (destroyed) connectivityChecker.removeObserver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
LOG.info("Destroyed");
|
||||
Cancellable apiCall;
|
||||
synchronized (lock) {
|
||||
state = State.DESTROYED;
|
||||
apiCall = this.apiCall;
|
||||
this.apiCall = null;
|
||||
}
|
||||
if (apiCall != null) apiCall.cancel();
|
||||
connectivityChecker.removeObserver(this);
|
||||
eventBus.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectivityCheckSucceeded() {
|
||||
LOG.info("Connectivity check succeeded");
|
||||
synchronized (lock) {
|
||||
if (state != State.CONNECTIVITY_CHECK) return;
|
||||
state = State.FETCHING_CONTACT_LIST;
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(
|
||||
new SimpleApiCall(this::apiCallFetchContactList));
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void apiCallFetchContactList() throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state != State.FETCHING_CONTACT_LIST) return;
|
||||
}
|
||||
LOG.info("Fetching remote contact list");
|
||||
Collection<ContactId> remote =
|
||||
mailboxApi.getContacts(mailboxProperties);
|
||||
ioExecutor.execute(() -> loadLocalContactList(remote));
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void loadLocalContactList(Collection<ContactId> remote) {
|
||||
synchronized (lock) {
|
||||
if (state != State.FETCHING_CONTACT_LIST) return;
|
||||
apiCall = null;
|
||||
}
|
||||
LOG.info("Loading local contact list");
|
||||
try {
|
||||
db.transaction(true, txn -> {
|
||||
Collection<Contact> local = db.getContacts(txn);
|
||||
// Handle the result on the event executor to avoid races with
|
||||
// incoming events
|
||||
txn.attach(() -> reconcileContactLists(local, remote));
|
||||
});
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void reconcileContactLists(Collection<Contact> local,
|
||||
Collection<ContactId> remote) {
|
||||
Set<ContactId> localIds = new HashSet<>();
|
||||
for (Contact c : local) localIds.add(c.getId());
|
||||
remote = new HashSet<>(remote);
|
||||
synchronized (lock) {
|
||||
if (state != State.FETCHING_CONTACT_LIST) return;
|
||||
for (ContactId c : localIds) {
|
||||
if (!remote.contains(c)) updates.add(new Update(true, c));
|
||||
}
|
||||
for (ContactId c : remote) {
|
||||
if (!localIds.contains(c)) updates.add(new Update(false, c));
|
||||
}
|
||||
if (updates.isEmpty()) {
|
||||
LOG.info("Contact list is up to date");
|
||||
state = State.WAITING_FOR_CHANGES;
|
||||
} else {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info(updates.size() + " updates to apply");
|
||||
}
|
||||
state = State.UPDATING_CONTACT_LIST;
|
||||
ioExecutor.execute(this::updateContactList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void updateContactList() {
|
||||
Update update;
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST) return;
|
||||
update = updates.poll();
|
||||
if (update == null) {
|
||||
LOG.info("No more updates to process");
|
||||
state = State.WAITING_FOR_CHANGES;
|
||||
apiCall = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (update.add) loadMailboxProperties(update.contactId);
|
||||
else removeContact(update.contactId);
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void loadMailboxProperties(ContactId c) {
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST) return;
|
||||
}
|
||||
LOG.info("Loading mailbox properties for contact");
|
||||
try {
|
||||
MailboxUpdate mailboxUpdate = db.transactionWithResult(true, txn ->
|
||||
mailboxUpdateManager.getLocalUpdate(txn, c));
|
||||
if (mailboxUpdate instanceof MailboxUpdateWithMailbox) {
|
||||
addContact(c, (MailboxUpdateWithMailbox) mailboxUpdate);
|
||||
} else {
|
||||
// Our own mailbox was concurrently unpaired. This worker will
|
||||
// be destroyed soon, so we can stop here
|
||||
LOG.info("Own mailbox was unpaired");
|
||||
}
|
||||
} catch (NoSuchContactException e) {
|
||||
// Contact was removed concurrently. Move on to the next update.
|
||||
// Later we may process a removal update for this contact, which
|
||||
// was never added to the mailbox's contact list. The removal API
|
||||
// call should fail safely with a TolerableFailureException
|
||||
LOG.info("No such contact");
|
||||
updateContactList();
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void addContact(ContactId c, MailboxUpdateWithMailbox withMailbox) {
|
||||
MailboxProperties props = withMailbox.getMailboxProperties();
|
||||
MailboxContact contact = new MailboxContact(c, props.getAuthToken(),
|
||||
requireNonNull(props.getInboxId()),
|
||||
requireNonNull(props.getOutboxId()));
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST) return;
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(new SimpleApiCall(() ->
|
||||
apiCallAddContact(contact)));
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void apiCallAddContact(MailboxContact contact)
|
||||
throws IOException, ApiException, TolerableFailureException {
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST) return;
|
||||
}
|
||||
LOG.info("Adding contact to remote contact list");
|
||||
mailboxApi.addContact(mailboxProperties, contact);
|
||||
updateContactList();
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void removeContact(ContactId c) {
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST) return;
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(new SimpleApiCall(() ->
|
||||
apiCallRemoveContact(c)));
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void apiCallRemoveContact(ContactId c)
|
||||
throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST) return;
|
||||
}
|
||||
LOG.info("Removing contact from remote contact list");
|
||||
try {
|
||||
mailboxApi.deleteContact(mailboxProperties, c);
|
||||
} catch (TolerableFailureException e) {
|
||||
// Catch this so we can continue to the next update
|
||||
LOG.warning("Contact does not exist");
|
||||
}
|
||||
updateContactList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof ContactAddedEvent) {
|
||||
LOG.info("Contact added");
|
||||
onContactAdded(((ContactAddedEvent) e).getContactId());
|
||||
} else if (e instanceof ContactRemovedEvent) {
|
||||
LOG.info("Contact removed");
|
||||
onContactRemoved(((ContactRemovedEvent) e).getContactId());
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onContactAdded(ContactId c) {
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST &&
|
||||
state != State.WAITING_FOR_CHANGES) {
|
||||
return;
|
||||
}
|
||||
updates.add(new Update(true, c));
|
||||
if (state == State.WAITING_FOR_CHANGES) {
|
||||
state = State.UPDATING_CONTACT_LIST;
|
||||
ioExecutor.execute(this::updateContactList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventExecutor
|
||||
private void onContactRemoved(ContactId c) {
|
||||
synchronized (lock) {
|
||||
if (state != State.UPDATING_CONTACT_LIST &&
|
||||
state != State.WAITING_FOR_CHANGES) {
|
||||
return;
|
||||
}
|
||||
updates.add(new Update(false, c));
|
||||
if (state == State.WAITING_FOR_CHANGES) {
|
||||
state = State.UPDATING_CONTACT_LIST;
|
||||
ioExecutor.execute(this::updateContactList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An update that should be applied to the remote contact list.
|
||||
*/
|
||||
private static class Update {
|
||||
|
||||
/**
|
||||
* True if the contact should be added, false if the contact should be
|
||||
* removed.
|
||||
*/
|
||||
private final boolean add;
|
||||
private final ContactId contactId;
|
||||
|
||||
private Update(boolean add, ContactId contactId) {
|
||||
this.add = add;
|
||||
this.contactId = contactId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.Collections.shuffle;
|
||||
import static java.util.logging.Level.INFO;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class OwnMailboxDownloadWorker extends MailboxDownloadWorker {
|
||||
|
||||
/**
|
||||
* The maximum number of files that will be downloaded before checking
|
||||
* again for folders with available files. This ensures that if a file
|
||||
* arrives during a download cycle, its folder will be checked within a
|
||||
* reasonable amount of time even if some other folder has a very large
|
||||
* number of files.
|
||||
* <p>
|
||||
* Package access for testing.
|
||||
*/
|
||||
static final int MAX_ROUND_ROBIN_FILES = 1000;
|
||||
|
||||
OwnMailboxDownloadWorker(
|
||||
ConnectivityChecker connectivityChecker,
|
||||
TorReachabilityMonitor torReachabilityMonitor,
|
||||
MailboxApiCaller mailboxApiCaller,
|
||||
MailboxApi mailboxApi,
|
||||
MailboxFileManager mailboxFileManager,
|
||||
MailboxProperties mailboxProperties) {
|
||||
super(connectivityChecker, torReachabilityMonitor, mailboxApiCaller,
|
||||
mailboxApi, mailboxFileManager, mailboxProperties);
|
||||
if (!mailboxProperties.isOwner()) throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ApiCall createApiCallForDownloadCycle() {
|
||||
return new SimpleApiCall(this::apiCallListFolders);
|
||||
}
|
||||
|
||||
private void apiCallListFolders() throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
}
|
||||
LOG.info("Listing folders with available files");
|
||||
List<MailboxFolderId> folders =
|
||||
mailboxApi.getFolders(mailboxProperties);
|
||||
if (folders.isEmpty()) onDownloadCycleFinished();
|
||||
else listNextFolder(new LinkedList<>(folders), new HashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the next folder from `queue` and starts a task to list the
|
||||
* files in the folder and add them to `available`.
|
||||
*/
|
||||
private void listNextFolder(Queue<MailboxFolderId> queue,
|
||||
Map<MailboxFolderId, Queue<MailboxFile>> available) {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
MailboxFolderId folder = queue.remove();
|
||||
apiCall = mailboxApiCaller.retryWithBackoff(new SimpleApiCall(() ->
|
||||
apiCallListFolder(folder, queue, available)));
|
||||
}
|
||||
}
|
||||
|
||||
private void apiCallListFolder(MailboxFolderId folder,
|
||||
Queue<MailboxFolderId> queue,
|
||||
Map<MailboxFolderId, Queue<MailboxFile>> available)
|
||||
throws IOException, ApiException {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
}
|
||||
LOG.info("Listing folder");
|
||||
try {
|
||||
List<MailboxFile> files =
|
||||
mailboxApi.getFiles(mailboxProperties, folder);
|
||||
if (!files.isEmpty()) {
|
||||
available.put(folder, new LinkedList<>(files));
|
||||
}
|
||||
} catch (TolerableFailureException e) {
|
||||
LOG.warning("Folder does not exist");
|
||||
}
|
||||
if (queue.isEmpty()) {
|
||||
LOG.info("Finished listing folders");
|
||||
if (available.isEmpty()) onDownloadCycleFinished();
|
||||
else createDownloadQueue(available);
|
||||
} else {
|
||||
listNextFolder(queue, available);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits the given folders in round-robin order to create a queue of up to
|
||||
* {@link #MAX_ROUND_ROBIN_FILES} to download.
|
||||
*/
|
||||
private void createDownloadQueue(
|
||||
Map<MailboxFolderId, Queue<MailboxFile>> available) {
|
||||
synchronized (lock) {
|
||||
if (state == State.DESTROYED) return;
|
||||
}
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info(available.size() + " folders have available files");
|
||||
}
|
||||
Queue<FolderFile> queue = createRoundRobinQueue(available);
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Downloading " + queue.size() + " files");
|
||||
}
|
||||
downloadNextFile(queue);
|
||||
}
|
||||
|
||||
// Package access for testing
|
||||
Queue<FolderFile> createRoundRobinQueue(
|
||||
Map<MailboxFolderId, Queue<MailboxFile>> available) {
|
||||
List<MailboxFolderId> roundRobin = new ArrayList<>(available.keySet());
|
||||
// Shuffle the folders so we don't always favour the same folders
|
||||
shuffle(roundRobin);
|
||||
Queue<FolderFile> queue = new LinkedList<>();
|
||||
while (queue.size() < MAX_ROUND_ROBIN_FILES && !available.isEmpty()) {
|
||||
Iterator<MailboxFolderId> it = roundRobin.iterator();
|
||||
while (queue.size() < MAX_ROUND_ROBIN_FILES && it.hasNext()) {
|
||||
MailboxFolderId folder = it.next();
|
||||
Queue<MailboxFile> files = available.get(folder);
|
||||
MailboxFile file = files.remove();
|
||||
queue.add(new FolderFile(folder, file.name));
|
||||
if (files.isEmpty()) {
|
||||
available.remove(folder);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
return queue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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
|
||||
class SimpleApiCall implements ApiCall {
|
||||
|
||||
private static final Logger LOG = getLogger(SimpleApiCall.class.getName());
|
||||
|
||||
private final Attempt attempt;
|
||||
|
||||
SimpleApiCall(Attempt attempt) {
|
||||
this.attempt = attempt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean callApi() {
|
||||
try {
|
||||
attempt.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
|
||||
}
|
||||
}
|
||||
|
||||
interface Attempt {
|
||||
|
||||
/**
|
||||
* Makes a single attempt to call an API endpoint. If this method
|
||||
* throws an {@link IOException} or an {@link ApiException}, the call
|
||||
* will be retried. If it throws a {@link TolerableFailureException}
|
||||
* or returns without throwing an exception, the call will not be
|
||||
* retried.
|
||||
*/
|
||||
void tryToCallApi()
|
||||
throws IOException, ApiException, TolerableFailureException;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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);
|
||||
|
||||
/**
|
||||
* Removes an observer that was added via
|
||||
* {@link #addOneShotObserver(TorReachabilityObserver)}.
|
||||
*/
|
||||
void removeObserver(TorReachabilityObserver o);
|
||||
|
||||
interface TorReachabilityObserver {
|
||||
|
||||
void onTorReachable();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user