Compare commits

..

162 Commits

Author SHA1 Message Date
Sebastian Kürten
8a94a1708f Try fixing the localization issues 2021-02-12 16:57:14 +01:00
akwizgran
9d96ce6db0 Bump version numbers for 1.2.9 release. 2020-07-04 22:40:58 +01:00
akwizgran
45fb5bb445 Merge branch 'expiry-overflow-bug' into 'master'
Fix overflow in expiry warning code, bypass expiry code in release builds

See merge request briar/briar!1259
2020-07-04 21:38:22 +00:00
akwizgran
0756d92ca1 Fix overflow in expiry warning code, bypass code in release builds. 2020-07-04 22:23:06 +01:00
akwizgran
37f80c7682 Bump version numbers for 1.2.8 release. 2020-07-03 16:03:50 +01:00
akwizgran
b409215c57 Update translations. 2020-07-03 15:57:47 +01:00
Torsten Grote
4f0aaf03fd Merge branch '1744-check-socket-is-bound' into 'master'
Check server socket is bound

Closes #1744

See merge request briar/briar!1258
2020-07-03 14:51:05 +00:00
akwizgran
597fef6d50 Check server socket is bound. 2020-07-03 13:04:26 +01:00
akwizgran
1dd15567de Merge branch 'style-guide-colours' into 'master'
Use colours from style guide

See merge request briar/briar!1257
2020-06-29 17:09:41 +00:00
akwizgran
428f06abdd Use style guide colours in drawables. 2020-06-29 17:58:10 +01:00
Torsten Grote
e1d1c62708 Merge branch 'default-plugin-settings' into 'master'
Enable LAN plugin by default, move default settings to constants

See merge request briar/briar!1256
2020-06-29 16:52:51 +00:00
akwizgran
ae75090d23 Use colours from style guide. 2020-06-29 17:20:42 +01:00
Torsten Grote
443043ae09 Merge branch 'plugin-toggles' into 'master'
Merge plugin-toggles feature branch

See merge request briar/briar!1255
2020-06-29 13:07:52 +00:00
akwizgran
fb85730b8e Enable LAN by default, as on master.
Let's not break too many things at once.
2020-06-26 17:42:05 +01:00
akwizgran
48b1e77065 Use constants for default plugin settings. 2020-06-26 17:42:05 +01:00
akwizgran
a03953563f Show appropriate text for answered introduction requests. 2020-06-26 17:37:17 +01:00
akwizgran
033fd2d3b4 Update comments about migration periods, since this isn't merged yet. 2020-06-26 17:37:16 +01:00
akwizgran
011d8e1df7 Remove unused strings. 2020-06-26 17:37:16 +01:00
akwizgran
ef5e2dad72 Make inner class just a holder for resources. 2020-06-26 17:37:16 +01:00
akwizgran
f35e87c8ad Remove unused code, fix a null safety bug. 2020-06-26 17:37:16 +01:00
akwizgran
e4940a046a Move transport toggles back to the settings screen. 2020-06-26 17:37:16 +01:00
akwizgran
0a666df164 Correctly handle connectivity events on IPv6-only networks. 2020-06-26 17:37:16 +01:00
akwizgran
6fb4b95b18 Don't allow list elements to be null. 2020-06-26 17:37:16 +01:00
akwizgran
5567982fb4 Use IPv6 for key agreement if IPv4 isn't available. 2020-06-26 17:37:16 +01:00
akwizgran
25e50ceb10 Try to detect our address on an IPv6-only wifi network. 2020-06-26 17:37:16 +01:00
akwizgran
1495daf977 Remove redundant address checks.
Remote addresses are checked for acceptability before connecting.
2020-06-26 17:37:16 +01:00
akwizgran
badc6da649 Accept any link-local IPv6 address.
This allows IPv6 to be used when providing an access point.
2020-06-26 17:37:16 +01:00
akwizgran
e065d45d16 Recognise unusual AP addresses. 2020-06-26 17:37:16 +01:00
akwizgran
d0c53f1310 Only bind to the SLAAC address of the wifi interface.
This is just a precaution - on all devices I've tested, the wifi
interface is the only one with a SLAAC address.
2020-06-26 17:37:16 +01:00
akwizgran
e1084ffadd Support IPv6 SLAAC addresses. 2020-06-26 17:37:15 +01:00
akwizgran
2bd2f67693 Interrupt sync sessions when transport becomes inactive.
This ensures connections are closed when the user disables a transport.
2020-06-26 17:37:12 +01:00
akwizgran
c2b0a4b8d1 Remove unnecessary plugin lookup. 2020-06-26 17:34:38 +01:00
akwizgran
ee19d2f574 Use tinted icon for plugin settings dialog. 2020-06-26 17:34:38 +01:00
akwizgran
e9ec5734e2 Show dialog from controller. 2020-06-26 17:34:38 +01:00
akwizgran
7b1c6f3fdd Add icon, title to Change Settings dialog. 2020-06-26 17:34:38 +01:00
akwizgran
d689cf776c Change Tor settings after asking for confirmation. 2020-06-26 17:34:38 +01:00
akwizgran
f0fd1844dd Transition from one constraint set to another. 2020-06-26 17:34:38 +01:00
akwizgran
d16a301fc4 Make entire collapsed view clickable. 2020-06-26 17:34:38 +01:00
akwizgran
3ab88181eb Put the transport toggles in an expandable view (no animations). 2020-06-26 17:34:37 +01:00
akwizgran
802e599f09 Add STARTING_STOPPING state, use flags for reasons disabled. 2020-06-26 17:34:37 +01:00
akwizgran
a6bd59d3c9 Close small gap between setStarted() and setDisabledBySettings(). 2020-06-26 17:34:37 +01:00
akwizgran
b04b724028 Don't show Tor in the enabling state if it's disabled by settings. 2020-06-26 17:34:37 +01:00
akwizgran
71b0408fe6 Remove "don't connect" option from Tor network setting.
This has been replaced by the enable/disable setting and no longer
works.
2020-06-26 17:34:37 +01:00
Torsten Grote
2d38bd5734 [android] Scroll down when nav drawer chevron is pressed 2020-06-26 17:34:37 +01:00
Torsten Grote
ff5da8404a [android] remove unused strings 2020-06-26 17:34:37 +01:00
Torsten Grote
75615a4e7f [android] make transport plugin toggles functional 2020-06-26 17:34:37 +01:00
Torsten Grote
96e32ad64e [android] Add transport plugin toggles to NavDrawer 2020-06-26 17:34:37 +01:00
Torsten Grote
0fec5d7783 [bramble] Add method for enabling/disabling plugins to PluginManager 2020-06-26 17:34:37 +01:00
akwizgran
ee74b3774b Remove another redundant call to pluginStateChanged(). 2020-06-26 17:34:37 +01:00
akwizgran
c783a2f352 Enable LAN plugin before showing QR code. 2020-06-26 17:34:37 +01:00
akwizgran
77aa5401f3 Remove redundant call to pluginStateChanged(). 2020-06-26 17:34:37 +01:00
akwizgran
99686f5316 Use XML to specify dependencies between settings. 2020-06-26 17:34:37 +01:00
akwizgran
f5b4f6e071 Clean up logic for enabling/disabling settings. 2020-06-26 17:34:36 +01:00
akwizgran
a2de841e6a Don't remove old settings yet.
This avoids an unlikely race condition at startup, where the user opens
the settings screen before the Tor plugin has migrated the settings.
2020-06-26 17:34:36 +01:00
akwizgran
1f94c2d4e8 Enable LAN plugin in unit test. 2020-06-26 17:34:36 +01:00
akwizgran
413ce29c0c Enable BT plugin before showing QR code. 2020-06-26 17:34:36 +01:00
akwizgran
c67f758c90 Small code cleanups in key agreement UI. 2020-06-26 17:34:36 +01:00
akwizgran
339524500b Make REASON_USER into a generic reason code. 2020-06-26 17:34:36 +01:00
akwizgran
03811f78fa Add toggle setting for LAN plugin. 2020-06-26 17:34:36 +01:00
akwizgran
fc86c46456 Update semantics of Bluetooth setting.
The setting now enables/disables the plugin, not just contact
connections. The key agreement UI will need to be updated to change the
setting if the user agrees to use Bluetooth.
2020-06-26 17:34:36 +01:00
akwizgran
7ae86d70af Convert Bluetooth setting to a switch. 2020-06-26 17:34:36 +01:00
akwizgran
63e3c661a3 Add toggle setting for Tor plugin. 2020-06-26 17:34:36 +01:00
akwizgran
4f54bd90fb Remove redundant casts. 2020-06-26 17:34:36 +01:00
akwizgran
706c03aa8b Skip fetching RSS feeds if Tor is not active. 2020-06-26 17:34:36 +01:00
akwizgran
c42a987927 Use amber icon when enabling transports. 2020-06-26 17:34:36 +01:00
akwizgran
297dbe0b16 Only update bridge and padding settings if network is enabled. 2020-06-26 17:34:35 +01:00
akwizgran
4130662e1f Notify callback of state changes while holding lock. 2020-06-26 17:34:35 +01:00
akwizgran
c08bdf96cd Update javadocs for lock-safe methods. 2020-06-26 17:34:35 +01:00
akwizgran
8bb534564f Remove redundant logging. 2020-06-26 17:34:35 +01:00
akwizgran
5e60a717fc Remove debug logging. 2020-06-26 17:34:35 +01:00
akwizgran
dd1509350c Close server socket when BT is disabled. 2020-06-26 17:34:35 +01:00
akwizgran
465ba3d337 Remove unnecessary inner class, state checks. 2020-06-26 17:34:35 +01:00
akwizgran
7561c5039e Reset backoff before notifying of new state.
The new state may cause the poller to poll the
plugin. Let's avoid a race between updating and
querying the polling interval.
2020-06-26 17:34:35 +01:00
akwizgran
242d6f8a0e Move to enabling state earlier in Tor startup. 2020-06-26 17:34:35 +01:00
akwizgran
c554847b54 Add TransportStateEvent, rename existing events. 2020-06-26 17:34:35 +01:00
akwizgran
d30b250389 Ensure server socket is closed. 2020-06-26 17:34:35 +01:00
akwizgran
ecea2c587d Add method for getting reason why plugin is disabled. 2020-06-26 17:34:35 +01:00
akwizgran
43a91e2e57 Fix test expectations. 2020-06-26 17:34:35 +01:00
akwizgran
ea288b998b Rename available/unavailable states. 2020-06-26 17:34:34 +01:00
akwizgran
48dc598ca3 Update tests. 2020-06-26 17:34:34 +01:00
akwizgran
e2d63ac6a4 If adapter is disabled, forget that we enabled it. 2020-06-26 17:34:34 +01:00
akwizgran
afc85cdf52 Check that server sockets are closed as expected. 2020-06-26 17:34:34 +01:00
akwizgran
b2a1ea84f8 Provide more information about plugin states. 2020-06-26 17:34:32 +01:00
akwizgran
fcc26c093b Avoid NPE if there's no TelephonyManager. 2020-06-26 17:33:54 +01:00
Torsten Grote
5a741bf13b Merge branch '1712-bluetooth-connection-wake-lock' into 'master'
Hold a wake lock while Bluetooth connections are open

See merge request briar/briar!1251
2020-06-26 16:11:36 +00:00
akwizgran
5dc460851b Remove redundant logging. 2020-06-26 15:33:37 +01:00
akwizgran
b805514f70 Use renewable wake lock, try to guess a "safe" tag. 2020-06-26 14:58:06 +01:00
akwizgran
69d94c9f29 Hold a wake lock while Bluetooth connections are open. 2020-06-26 14:58:06 +01:00
Torsten Grote
53d4b7a0df Merge branch '1712-simple-connection-limiter' into 'master'
Simple connection limiter that closes connections cleanly

Closes #1712

See merge request briar/briar!1254
2020-06-26 11:36:21 +00:00
akwizgran
648f26542c Simple connection limiter that closes connections cleanly. 2020-06-26 10:57:08 +01:00
akwizgran
dcb5f95934 Merge branch '1712-prefer-lan-to-bluetooth' into 'master'
Close redundant connections

See merge request briar/briar!1249
2020-06-26 09:46:24 +00:00
akwizgran
730d553b0a Fix screenshot test (again). 2020-06-26 10:38:04 +01:00
akwizgran
7736a3b6fc Use separate methods for registering incoming and outgoing connections. 2020-06-26 09:59:03 +01:00
akwizgran
95f427863d Remove transport preferences for briar-headless. 2020-06-25 17:46:22 +01:00
Torsten Grote
ff8a422638 Merge branch '1712-connection-manager-refactoring' into 'master'
Connection manager refactoring

See merge request briar/briar!1248
2020-06-25 14:21:49 +00:00
akwizgran
78d7fc2106 Fix bug in reporting of connection state, add regression tests. 2020-06-02 12:00:06 +01:00
akwizgran
cc943be540 Update javadoc. 2020-06-01 15:30:30 +01:00
akwizgran
6eb77465f6 Don't try to reconnect if the connection was closed cleanly. 2020-06-01 14:49:55 +01:00
akwizgran
35d1b406f7 Refactor transport preferences. 2020-06-01 14:49:55 +01:00
akwizgran
2add63657e Inner class can be static. 2020-06-01 14:49:55 +01:00
akwizgran
d3751fbead Don't interrupt connections until priority is set.
This maintains compatibility with older peers that don't know about
priorities or transport preferences and will try to replace any
connections we close.
2020-06-01 14:49:55 +01:00
akwizgran
4aaa8c3b93 Don't poll if already connected via a better transport. 2020-05-25 17:47:33 +01:00
akwizgran
5b04527c54 Fix screenshot test. 2020-05-25 17:47:33 +01:00
akwizgran
7d6b65913a Combine connection chooser with connection registry. 2020-05-25 16:42:01 +01:00
akwizgran
36747acac1 Extract better and worse transports from preferences. 2020-05-25 14:47:34 +01:00
akwizgran
e8dbc00712 Refactor connection registry implementation. 2020-05-25 14:33:35 +01:00
akwizgran
d3d7212b08 Add registry method for deciding which contacts to poll. 2020-05-13 17:55:05 +01:00
akwizgran
2919657b4a Add unit tests for connection chooser. 2020-05-13 15:56:07 +01:00
akwizgran
0c338b362e Add InterruptibleConnection interface for easier testing. 2020-05-13 15:43:07 +01:00
akwizgran
8dd993dd9d Interrupt connections outside the lock. 2020-05-13 10:24:27 +01:00
akwizgran
1b2b50d91b Exchange priority records and close redundant connections. 2020-05-12 21:36:58 +01:00
akwizgran
ee9c771045 Add priority record for choosing between redundant connections. 2020-05-12 21:36:58 +01:00
akwizgran
9e6d67f13d Handle interrupts that occur before the outgoing session starts. 2020-05-12 21:36:57 +01:00
akwizgran
710b6d18ce Fix import in screenshot test. 2020-05-12 21:36:45 +01:00
akwizgran
dd4aa67643 Refactor connection creation back into manager. 2020-05-12 21:27:43 +01:00
akwizgran
79482d5e3a Move connection management to its own package. 2020-05-12 17:36:35 +01:00
akwizgran
ee0bf7218c Move some duplicated code into utility methods. 2020-05-12 17:25:55 +01:00
akwizgran
c1101c7fe1 Factor inner classes out of ConnectionManagerImpl. 2020-05-12 17:08:04 +01:00
Torsten Grote
708452713d Merge branch '1712-detect-dead-bluetooth-connections' into 'master'
Detect and close dead Bluetooth connections

See merge request briar/briar!1246
2020-05-11 15:55:07 +00:00
akwizgran
c80d3196af Use milliseconds for timing. 2020-05-11 15:42:23 +01:00
Torsten Grote
d1c2eb89a1 Merge branch '1712-fix-double-connection-counting' into 'master'
Don't count Bluetooth connections twice

See merge request briar/briar!1245
2020-05-11 14:06:30 +00:00
akwizgran
c4273d22ed Delegate all other methods to wrapped InputStream. 2020-05-08 16:22:46 +01:00
akwizgran
21f3a9f3c7 Add javadoc. 2020-05-08 16:22:46 +01:00
akwizgran
0281eec0da Add unit test for TimeoutInputStream. 2020-05-08 16:22:46 +01:00
akwizgran
d3fd309609 Only check timeouts when we have some streams to monitor. 2020-05-08 16:22:46 +01:00
akwizgran
f2f278c393 Add timeout monitor for Bluetooth connections. 2020-05-08 16:22:46 +01:00
akwizgran
e204d5a996 Don't count connections twice. 2020-05-08 15:17:27 +01:00
akwizgran
876efee1a8 Use keepalives to detect dead connections. 2020-05-08 14:21:41 +01:00
akwizgran
8fd9a40ffb Merge branch 'discover-bt-address-from-incoming-connection' into 'master'
Discover remote Bluetooth address from connection

See merge request briar/briar!1244
2020-04-29 15:31:30 +00:00
akwizgran
fb918457d4 Use constants for metadata keys. 2020-04-29 15:37:21 +01:00
akwizgran
b5fe55faf3 Validate remote address. 2020-04-29 15:28:27 +01:00
akwizgran
7320099494 Also store properties discovered from outgoing connections.
This is useful when adding a Bluetooth address is discovered while
adding a contact.
2020-04-28 17:56:01 +01:00
akwizgran
346bec94e8 Discover contacts' BT addresses from incoming connections. 2020-04-28 17:45:17 +01:00
akwizgran
856ec61759 Merge branch '1722-lastChatActivity' into 'master'
Include last private chat activity in list of contacts

Closes #1722

See merge request briar/briar!1242
2020-04-27 09:20:08 +00:00
Torsten Grote
f61e2b399e [headless] Fix unit tests by passing only timestamp into OutputContact 2020-04-20 09:44:51 -03:00
Nico Alt
6135f9152f Include last private chat activity in list of contacts
Fixes #1722.
2020-04-08 12:00:00 +00:00
Torsten Grote
84584d4d3c Merge branch 'tor-0.3.5.10' into 'master'
Upgrade Tor to version 0.3.5.10

Closes #1714

See merge request briar/briar!1241
2020-03-31 13:14:21 +00:00
akwizgran
17239810c8 Upgrade Tor to version 0.3.5.10. 2020-03-25 17:09:12 +00:00
Torsten Grote
9eee58657e Merge branch '1696-keystore-crash' into 'master'
Show a dialog instead of crashing if a hardware-backed key can't be loaded

Closes #1696

See merge request briar/briar!1233
2020-03-12 12:12:42 +00:00
Torsten Grote
76425455b8 Merge branch 'logging-for-account-bugs' into 'master'
Add logging to track down account bugs

See merge request briar/briar!1239
2020-03-11 14:18:50 +00:00
akwizgran
9ea7140a7f Add logging to track down account bugs. 2020-03-11 14:06:48 +00:00
akwizgran
bde9800c89 Add annotation for visibility. 2020-03-11 13:54:01 +00:00
Torsten Grote
4e5b6ed3e0 Merge branch '1367-db-race' into 'master'
Don't infer anything from existence of (possibly empty) DB directory

Closes #1528 and #1367

See merge request briar/briar!1238
2020-03-10 14:59:06 +00:00
akwizgran
77d037f061 Update javadocs. 2020-03-10 11:27:54 +00:00
Torsten Grote
676f5faef4 Merge branch 'fix-wifi-connectivity-misreporting' into 'master'
Fix misreporting of wifi status in LAN plugin

See merge request briar/briar!1237
2020-03-06 16:52:20 +00:00
akwizgran
8e21068465 Fix misreporting of wifi status in LAN plugin. 2020-03-06 13:35:06 +00:00
akwizgran
4a68e5347d Merge branch '1582-fix-climbing-snackbar' into 'master'
Fix climbing snackbar

Closes #1582

See merge request briar/briar!1223
2020-03-03 14:42:20 +00:00
Torsten Grote
27dd383496 Merge branch '1371-protect-code-cache-directory' into 'master'
Protect cache and code_cache directories when deleting account

Closes #1545 and #1371

See merge request briar/briar!1231
2020-02-26 14:03:39 +00:00
akwizgran
ed50582e27 Show a dialog if the DB key can't be decrypted due to a keystore error. 2020-02-25 15:00:49 +00:00
akwizgran
1546a05568 Catch exception if hardware-backed key can't be loaded. 2020-02-25 12:28:21 +00:00
akwizgran
4bdf966e67 Test that code_cache directory isn't deleted. 2020-02-25 11:23:07 +00:00
akwizgran
e1e67f3b2e Clear the cache directory but don't delete it. 2020-02-25 11:18:50 +00:00
akwizgran
1d63b16ff1 Don't delete the code_cache directory when deleting account.
This seems to avoid the disappearing account bug when installing a new
version.
2020-02-25 10:14:31 +00:00
akwizgran
618ab1f1ec Don't infer anything from existence of (possibly empty) DB directory. 2020-02-24 17:51:59 +00:00
Torsten Grote
421f0ebfa5 Merge branch 'network-prefix-length' into 'master'
Use network prefix length to determine which addresses are connectable

Closes #1178

See merge request briar/briar!1230
2020-02-19 13:11:24 +00:00
akwizgran
61db5d1b04 Make bit-twiddling code more readable. 2020-02-19 09:52:13 +00:00
akwizgran
b3d4012527 Use network prefix length to determine which addresses are connectable. 2020-02-18 11:22:29 +00:00
Torsten Grote
60172331ee Merge branch 'ipv4-link-local' into 'master'
Add support for IPv4 link-local addresses

See merge request briar/briar!1229
2020-02-17 12:42:01 +00:00
akwizgran
076debdc4b Merge branch '1328-reuse-port' into 'master'
Choose port in advance when providing wifi access point

Closes #1328

See merge request briar/briar!1228
2020-02-17 12:37:29 +00:00
akwizgran
ed13cbca6a Add support for IPv4 link-local addresses. 2020-02-17 11:42:13 +00:00
akwizgran
49cb1d0612 Choose port in advance when providing wifi access point. 2020-02-14 16:56:00 +00:00
Torsten Grote
1e4ad67ffc [android] Fix climbing snackbar
Use a fresh snackbar for pending contacts each time it needs to be
shown. Don't re-use the old instance and clear it in onStop().
2020-02-11 13:25:15 -03:00
248 changed files with 6827 additions and 3931 deletions

View File

@@ -11,8 +11,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode 10207
versionName "1.2.7"
versionCode 10209
versionName "1.2.9"
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -38,7 +38,7 @@ configurations {
dependencies {
implementation project(path: ':bramble-core', configuration: 'default')
tor 'org.briarproject:tor-android:0.3.5.9@zip'
tor 'org.briarproject:tor-android:0.3.5.10@zip'
tor 'org.briarproject:obfs4proxy-android:0.0.11-2@zip'
annotationProcessor 'com.google.dagger:dagger-compiler:2.24'

View File

@@ -12,6 +12,7 @@ import org.briarproject.bramble.api.identity.IdentityManager;
import java.io.File;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
@@ -20,6 +21,7 @@ import javax.annotation.concurrent.GuardedBy;
import javax.inject.Inject;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Arrays.asList;
import static java.util.logging.Level.INFO;
import static org.briarproject.bramble.util.IoUtils.deleteFileOrDir;
import static org.briarproject.bramble.util.LogUtils.logFileOrDir;
@@ -30,6 +32,12 @@ class AndroidAccountManager extends AccountManagerImpl
private static final Logger LOG =
Logger.getLogger(AndroidAccountManager.class.getName());
/**
* Directories that shouldn't be deleted when deleting the user's account.
*/
private static final List<String> PROTECTED_DIR_NAMES =
asList("cache", "code_cache", "lib", "shared_prefs");
protected final Context appContext;
private final SharedPreferences prefs;
@@ -81,7 +89,7 @@ class AndroidAccountManager extends AccountManagerImpl
if (!prefs.edit().clear().commit())
LOG.warning("Could not clear shared preferences");
}
// Delete files, except lib and shared_prefs directories
// Delete files, except protected directories
Set<File> files = new HashSet<>();
File dataDir = getDataDir();
@Nullable
@@ -90,14 +98,12 @@ class AndroidAccountManager extends AccountManagerImpl
LOG.warning("Could not list files in app data dir");
} else {
for (File file : fileArray) {
String name = file.getName();
if (!name.equals("lib") && !name.equals("shared_prefs")) {
if (!PROTECTED_DIR_NAMES.contains(file.getName())) {
files.add(file);
}
}
}
files.add(appContext.getFilesDir());
files.add(appContext.getCacheDir());
addIfNotNull(files, appContext.getExternalCacheDir());
if (SDK_INT >= 19) {
for (File file : appContext.getExternalCacheDirs()) {
@@ -109,12 +115,16 @@ class AndroidAccountManager extends AccountManagerImpl
addIfNotNull(files, file);
}
}
// Clear the cache directory but don't delete it
File cacheDir = appContext.getCacheDir();
File[] children = cacheDir.listFiles();
if (children != null) files.addAll(asList(children));
for (File file : files) {
if (LOG.isLoggable(INFO)) {
LOG.info("Deleting " + file.getAbsolutePath());
}
deleteFileOrDir(file);
}
// Recreate the cache dir as some OpenGL drivers expect it to exist
if (!new File(dataDir, "cache").mkdirs())
LOG.warning("Could not recreate cache dir");
}
private File getDataDir() {

View File

@@ -32,6 +32,7 @@ import static android.content.Intent.ACTION_SCREEN_OFF;
import static android.content.Intent.ACTION_SCREEN_ON;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.wifi.p2p.WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
import static java.util.concurrent.TimeUnit.MINUTES;
@@ -76,9 +77,9 @@ class AndroidNetworkManager implements NetworkManager, Service {
filter.addAction(ACTION_SCREEN_ON);
filter.addAction(ACTION_SCREEN_OFF);
filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
filter.addAction(WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
if (SDK_INT >= 23) filter.addAction(ACTION_DEVICE_IDLE_MODE_CHANGED);
appContext.registerReceiver(networkStateReceiver, filter);
}
@Override
@@ -136,7 +137,8 @@ class AndroidNetworkManager implements NetworkManager, Service {
}
private boolean isApEvent(@Nullable String action) {
return WIFI_AP_STATE_CHANGED_ACTION.equals(action);
return WIFI_AP_STATE_CHANGED_ACTION.equals(action) ||
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action);
}
}
}

View File

@@ -9,6 +9,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
@@ -30,6 +31,7 @@ import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Logger;
import javax.annotation.Nullable;
@@ -65,6 +67,7 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
private static final int MAX_DISCOVERY_MS = 10_000;
private final ScheduledExecutorService scheduler;
private final AndroidExecutor androidExecutor;
private final Context appContext;
private final Clock clock;
@@ -76,11 +79,14 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
private volatile BluetoothAdapter adapter = null;
AndroidBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
Executor ioExecutor, AndroidExecutor androidExecutor,
Context appContext, SecureRandom secureRandom, Clock clock,
Backoff backoff, PluginCallback callback, int maxLatency) {
super(connectionLimiter, ioExecutor, secureRandom, backoff, callback,
maxLatency);
TimeoutMonitor timeoutMonitor, Executor ioExecutor,
SecureRandom secureRandom, ScheduledExecutorService scheduler,
AndroidExecutor androidExecutor, Context appContext, Clock clock,
Backoff backoff, PluginCallback callback, int maxLatency,
int maxIdleTime) {
super(connectionLimiter, timeoutMonitor, ioExecutor, secureRandom,
backoff, callback, maxLatency, maxIdleTime);
this.scheduler = scheduler;
this.androidExecutor = androidExecutor;
this.appContext = appContext;
this.clock = clock;
@@ -148,6 +154,12 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
wasEnabledByUs = true;
}
@Override
void onAdapterDisabled() {
super.onAdapterDisabled();
wasEnabledByUs = false;
}
@Override
@Nullable
String getBluetoothAddress() {
@@ -172,9 +184,10 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
return wrapSocket(ss.accept());
}
private DuplexTransportConnection wrapSocket(BluetoothSocket s) {
return new AndroidBluetoothTransportConnection(this,
connectionLimiter, s);
private DuplexTransportConnection wrapSocket(BluetoothSocket s)
throws IOException {
return new AndroidBluetoothTransportConnection(this, connectionLimiter,
timeoutMonitor, appContext, scheduler, s);
}
@Override

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.bluetooth;
import android.content.Context;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.BackoffFactory;
@@ -15,6 +16,7 @@ import org.briarproject.bramble.api.system.Clock;
import java.security.SecureRandom;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.concurrent.Immutable;
@@ -25,28 +27,34 @@ import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
public class AndroidBluetoothPluginFactory implements DuplexPluginFactory {
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;
private final ScheduledExecutorService scheduler;
private final AndroidExecutor androidExecutor;
private final Context appContext;
private final SecureRandom secureRandom;
private final EventBus eventBus;
private final Clock clock;
private final TimeoutMonitor timeoutMonitor;
private final BackoffFactory backoffFactory;
public AndroidBluetoothPluginFactory(Executor ioExecutor,
ScheduledExecutorService scheduler,
AndroidExecutor androidExecutor, Context appContext,
SecureRandom secureRandom, EventBus eventBus, Clock clock,
BackoffFactory backoffFactory) {
TimeoutMonitor timeoutMonitor, BackoffFactory backoffFactory) {
this.ioExecutor = ioExecutor;
this.scheduler = scheduler;
this.androidExecutor = androidExecutor;
this.appContext = appContext;
this.secureRandom = secureRandom;
this.eventBus = eventBus;
this.clock = clock;
this.timeoutMonitor = timeoutMonitor;
this.backoffFactory = backoffFactory;
}
@@ -63,12 +71,13 @@ public class AndroidBluetoothPluginFactory implements DuplexPluginFactory {
@Override
public DuplexPlugin createPlugin(PluginCallback callback) {
BluetoothConnectionLimiter connectionLimiter =
new BluetoothConnectionLimiterImpl();
new BluetoothConnectionLimiterImpl(eventBus);
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE);
AndroidBluetoothPlugin plugin = new AndroidBluetoothPlugin(
connectionLimiter, ioExecutor, androidExecutor, appContext,
secureRandom, clock, backoff, callback, MAX_LATENCY);
connectionLimiter, timeoutMonitor, ioExecutor, secureRandom,
scheduler, androidExecutor, appContext, clock, backoff,
callback, MAX_LATENCY, MAX_IDLE_TIME);
eventBus.addListener(plugin);
return plugin;
}

View File

@@ -1,33 +1,60 @@
package org.briarproject.bramble.plugin.bluetooth;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.PowerManager;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.duplex.AbstractDuplexTransportConnection;
import org.briarproject.bramble.util.RenewableWakeLock;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.ScheduledExecutorService;
import static android.content.Context.POWER_SERVICE;
import static android.os.PowerManager.PARTIAL_WAKE_LOCK;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
import static org.briarproject.bramble.util.AndroidUtils.getWakeLockTag;
import static org.briarproject.bramble.util.AndroidUtils.isValidBluetoothAddress;
@NotNullByDefault
class AndroidBluetoothTransportConnection
extends AbstractDuplexTransportConnection {
private final BluetoothConnectionLimiter connectionManager;
private final BluetoothConnectionLimiter connectionLimiter;
private final RenewableWakeLock wakeLock;
private final BluetoothSocket socket;
private final InputStream in;
AndroidBluetoothTransportConnection(Plugin plugin,
BluetoothConnectionLimiter connectionManager,
BluetoothSocket socket) {
BluetoothConnectionLimiter connectionLimiter,
TimeoutMonitor timeoutMonitor, Context appContext,
ScheduledExecutorService scheduler, BluetoothSocket socket)
throws IOException {
super(plugin);
this.connectionManager = connectionManager;
this.connectionLimiter = connectionLimiter;
this.socket = socket;
in = timeoutMonitor.createTimeoutInputStream(
socket.getInputStream(), plugin.getMaxIdleTime() * 2);
PowerManager powerManager = (PowerManager)
requireNonNull(appContext.getSystemService(POWER_SERVICE));
String tag = getWakeLockTag(appContext);
wakeLock = new RenewableWakeLock(powerManager, scheduler,
PARTIAL_WAKE_LOCK, tag, 1, MINUTES);
wakeLock.acquire();
String address = socket.getRemoteDevice().getAddress();
if (isValidBluetoothAddress(address)) remote.put(PROP_ADDRESS, address);
}
@Override
protected InputStream getInputStream() throws IOException {
return socket.getInputStream();
protected InputStream getInputStream() {
return in;
}
@Override
@@ -40,7 +67,8 @@ class AndroidBluetoothTransportConnection
try {
socket.close();
} finally {
connectionManager.connectionClosed(this);
wakeLock.release();
connectionLimiter.connectionClosed(this);
}
}
}

View File

@@ -1,25 +1,32 @@
package org.briarproject.bramble.plugin.tcp;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkInfo;
import android.net.NetworkCapabilities;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import org.briarproject.bramble.PoliteExecutor;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.network.event.NetworkStatusEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.settings.Settings;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
@@ -28,31 +35,25 @@ import javax.net.SocketFactory;
import static android.content.Context.CONNECTIVITY_SERVICE;
import static android.content.Context.WIFI_SERVICE;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Collections.emptyList;
import static java.util.Collections.list;
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.plugin.LanTcpConstants.DEFAULT_PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
class AndroidLanTcpPlugin extends LanTcpPlugin {
private static final Logger LOG =
getLogger(AndroidLanTcpPlugin.class.getName());
private static final byte[] WIFI_AP_ADDRESS_BYTES =
{(byte) 192, (byte) 168, 43, 1};
private static final InetAddress WIFI_AP_ADDRESS;
static {
try {
WIFI_AP_ADDRESS = InetAddress.getByAddress(WIFI_AP_ADDRESS_BYTES);
} catch (UnknownHostException e) {
// Should only be thrown if the address has an illegal length
throw new AssertionError(e);
}
}
private final Executor connectionStatusExecutor;
private final ConnectivityManager connectivityManager;
@Nullable
@@ -62,8 +63,9 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
AndroidLanTcpPlugin(Executor ioExecutor, Context appContext,
Backoff backoff, PluginCallback callback, int maxLatency,
int maxIdleTime) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime);
int maxIdleTime, int connectionTimeout) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime,
connectionTimeout);
// Don't execute more than one connection status check at a time
connectionStatusExecutor =
new PoliteExecutor("AndroidLanTcpPlugin", ioExecutor, 1);
@@ -79,34 +81,138 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
@Override
public void start() {
if (used.getAndSet(true)) throw new IllegalStateException();
running = true;
initialisePortProperty();
Settings settings = callback.getSettings();
state.setStarted(settings.getBoolean(PREF_PLUGIN_ENABLE,
DEFAULT_PREF_PLUGIN_ENABLE));
updateConnectionStatus();
}
@Override
public void stop() {
running = false;
tryToClose(socket);
}
@Override
protected Socket createSocket() throws IOException {
return socketFactory.createSocket();
}
@Override
protected Collection<InetAddress> getLocalIpAddresses() {
// If the device doesn't have wifi, don't open any sockets
if (wifiManager == null) return emptyList();
// If we're connected to a wifi network, use that network
protected List<InetAddress> getUsableLocalInetAddresses(boolean ipv4) {
InetAddress addr = getWifiAddress(ipv4);
return addr == null ? emptyList() : singletonList(addr);
}
@Nullable
private InetAddress getWifiAddress(boolean ipv4) {
Pair<InetAddress, Boolean> wifi = getWifiIpv4Address();
if (ipv4) return wifi == null ? null : wifi.getFirst();
// If there's no wifi IPv4 address, we might be a client on an
// IPv6-only wifi network. We can only detect this on API 21+
if (wifi == null) {
return SDK_INT >= 21 ? getWifiClientIpv6Address() : null;
}
// Use the wifi IPv4 address to determine which interface's IPv6
// address we should return (if the interface has a suitable address)
return getIpv6AddressForInterface(wifi.getFirst());
}
/**
* Returns a {@link Pair} where the first element is the IPv4 address of
* the wifi interface and the second element is true if this device is
* providing an access point, or false if this device is a client. Returns
* null if this device isn't connected to wifi as an access point or client.
*/
@Nullable
private Pair<InetAddress, Boolean> getWifiIpv4Address() {
if (wifiManager == null) return null;
// If we're connected to a wifi network, return its address
WifiInfo info = wifiManager.getConnectionInfo();
if (info != null && info.getIpAddress() != 0)
return singletonList(intToInetAddress(info.getIpAddress()));
// If we're running an access point, return its address
if (super.getLocalIpAddresses().contains(WIFI_AP_ADDRESS))
return singletonList(WIFI_AP_ADDRESS);
// No suitable addresses
return emptyList();
if (info != null && info.getIpAddress() != 0) {
return new Pair<>(intToInetAddress(info.getIpAddress()), false);
}
List<InterfaceAddress> ifAddrs = getLocalInterfaceAddresses();
// If we're providing a normal access point, return its address
for (InterfaceAddress ifAddr : ifAddrs) {
if (isAndroidWifiApAddress(ifAddr)) {
return new Pair<>(ifAddr.getAddress(), true);
}
}
// If we're providing a wifi direct access point, return its address
for (InterfaceAddress ifAddr : ifAddrs) {
if (isAndroidWifiDirectApAddress(ifAddr)) {
return new Pair<>(ifAddr.getAddress(), true);
}
}
// Not connected to wifi
return null;
}
/**
* Returns true if the given address belongs to a network provided by an
* Android access point (including the access point's own address).
* <p>
* The access point's address is usually 192.168.43.1, but at least one
* device (Honor 8A) may use other addresses in the range 192.168.43.0/24.
*/
private boolean isAndroidWifiApAddress(InterfaceAddress ifAddr) {
if (ifAddr.getNetworkPrefixLength() != 24) return false;
byte[] ip = ifAddr.getAddress().getAddress();
return ip.length == 4
&& ip[0] == (byte) 192
&& ip[1] == (byte) 168
&& ip[2] == (byte) 43;
}
/**
* Returns true if the given address belongs to a network provided by an
* Android wifi direct legacy mode access point (including the access
* point's own address).
*/
private boolean isAndroidWifiDirectApAddress(InterfaceAddress ifAddr) {
if (ifAddr.getNetworkPrefixLength() != 24) return false;
byte[] ip = ifAddr.getAddress().getAddress();
return ip.length == 4
&& ip[0] == (byte) 192
&& ip[1] == (byte) 168
&& ip[2] == (byte) 49;
}
/**
* Returns a link-local IPv6 address for the wifi client interface, or null
* if there's no such interface or it doesn't have a suitable address.
*/
@TargetApi(21)
@Nullable
private InetAddress getWifiClientIpv6Address() {
for (Network net : connectivityManager.getAllNetworks()) {
NetworkCapabilities caps =
connectivityManager.getNetworkCapabilities(net);
if (caps == null || !caps.hasTransport(TRANSPORT_WIFI)) continue;
LinkProperties props = connectivityManager.getLinkProperties(net);
if (props == null) continue;
for (LinkAddress linkAddress : props.getLinkAddresses()) {
InetAddress addr = linkAddress.getAddress();
if (isIpv6LinkLocalAddress(addr)) return addr;
}
}
return null;
}
/**
* Returns a link-local IPv6 address for the interface with the given IPv4
* address, or null if the interface doesn't have a suitable address.
*/
@Nullable
private InetAddress getIpv6AddressForInterface(InetAddress ipv4) {
try {
NetworkInterface iface = NetworkInterface.getByInetAddress(ipv4);
if (iface == null) return null;
for (InetAddress addr : list(iface.getInetAddresses())) {
if (isIpv6LinkLocalAddress(addr)) return addr;
}
// No suitable address
return null;
} catch (SocketException e) {
logException(LOG, WARNING, e);
return null;
}
}
private InetAddress intToInetAddress(int ip) {
@@ -128,9 +234,11 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
private SocketFactory getSocketFactory() {
if (SDK_INT < 21) return SocketFactory.getDefault();
for (Network net : connectivityManager.getAllNetworks()) {
NetworkInfo info = connectivityManager.getNetworkInfo(net);
if (info != null && info.getType() == TYPE_WIFI)
NetworkCapabilities caps =
connectivityManager.getNetworkCapabilities(net);
if (caps != null && caps.hasTransport(TRANSPORT_WIFI)) {
return net.getSocketFactory();
}
}
LOG.warning("Could not find suitable socket factory");
return SocketFactory.getDefault();
@@ -138,30 +246,59 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
@Override
public void eventOccurred(Event e) {
super.eventOccurred(e);
if (e instanceof NetworkStatusEvent) updateConnectionStatus();
}
private void updateConnectionStatus() {
connectionStatusExecutor.execute(() -> {
if (!running) return;
Collection<InetAddress> addrs = getLocalIpAddresses();
if (addrs.contains(WIFI_AP_ADDRESS)) {
State s = getState();
if (s != ACTIVE && s != INACTIVE) return;
Pair<InetAddress, Boolean> wifi = getPreferredWifiAddress();
if (wifi == null) {
LOG.info("Not connected to wifi");
socketFactory = SocketFactory.getDefault();
// Server sockets may not have been closed automatically when
// interface was taken down. If any sockets are open, closing
// them here will cause the sockets to be cleared and the state
// to be updated in acceptContactConnections()
if (s == ACTIVE) {
LOG.info("Closing server sockets");
tryToClose(state.getServerSocket(true), LOG, WARNING);
tryToClose(state.getServerSocket(false), LOG, WARNING);
}
} else if (wifi.getSecond()) {
LOG.info("Providing wifi hotspot");
// There's no corresponding Network object and thus no way
// to get a suitable socket factory, so we won't be able to
// make outgoing connections on API 21+ if another network
// has internet access
socketFactory = SocketFactory.getDefault();
if (socket == null || socket.isClosed()) bind();
} else if (addrs.isEmpty()) {
LOG.info("Not connected to wifi");
socketFactory = SocketFactory.getDefault();
tryToClose(socket);
if (s == INACTIVE) bind();
} else {
LOG.info("Connected to wifi");
socketFactory = getSocketFactory();
if (socket == null || socket.isClosed()) bind();
if (s == INACTIVE) bind();
}
});
}
/**
* Returns a {@link Pair} where the first element is an IP address (IPv4 if
* available, otherwise IPv6) of the wifi interface and the second element
* is true if this device is providing an access point, or false if this
* device is a client. Returns null if this device isn't connected to wifi
* as an access point or client.
*/
@Nullable
private Pair<InetAddress, Boolean> getPreferredWifiAddress() {
Pair<InetAddress, Boolean> wifi = getWifiIpv4Address();
// If there's no wifi IPv4 address, we might be a client on an
// IPv6-only wifi network. We can only detect this on API 21+
if (wifi == null && SDK_INT >= 21) {
InetAddress ipv6 = getWifiClientIpv6Address();
if (ipv6 != null) return new Pair<>(ipv6, false);
}
return wifi;
}
}

View File

@@ -21,10 +21,11 @@ import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID;
@NotNullByDefault
public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
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 int MAX_LATENCY = 30_000; // 30 seconds
private static final int MAX_IDLE_TIME = 30_000; // 30 seconds
private static final int CONNECTION_TIMEOUT = 3_000; // 3 seconds
private static final int MIN_POLLING_INTERVAL = 60_000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 600_000; // 10 mins
private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor;
@@ -55,7 +56,8 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE);
AndroidLanTcpPlugin plugin = new AndroidLanTcpPlugin(ioExecutor,
appContext, backoff, callback, MAX_LATENCY, MAX_IDLE_TIME);
appContext, backoff, callback, MAX_LATENCY, MAX_IDLE_TIME,
CONNECTION_TIMEOUT);
eventBus.addListener(plugin);
return plugin;
}

View File

@@ -27,6 +27,7 @@ import static android.content.Context.MODE_PRIVATE;
import static android.content.Context.POWER_SERVICE;
import static android.os.PowerManager.PARTIAL_WAKE_LOCK;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.briarproject.bramble.util.AndroidUtils.getWakeLockTag;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -53,7 +54,7 @@ class AndroidTorPlugin extends TorPlugin {
appContext.getSystemService(POWER_SERVICE);
if (pm == null) throw new AssertionError();
wakeLock = new RenewableWakeLock(pm, scheduler, PARTIAL_WAKE_LOCK,
getWakeLockTag(), 1, MINUTES);
getWakeLockTag(appContext), 1, MINUTES);
}
@Override
@@ -74,7 +75,6 @@ class AndroidTorPlugin extends TorPlugin {
@Override
protected void enableNetwork(boolean enable) throws IOException {
if (!running) return;
if (enable) wakeLock.acquire();
super.enableNetwork(enable);
if (!enable) wakeLock.release();
@@ -85,17 +85,4 @@ class AndroidTorPlugin extends TorPlugin {
super.stop();
wakeLock.release();
}
private String getWakeLockTag() {
PackageManager pm = appContext.getPackageManager();
for (PackageInfo info : pm.getInstalledPackages(0)) {
String name = info.packageName.toLowerCase();
if (name.startsWith("com.huawei.powergenie")) {
return "LocationManagerService";
} else if (name.startsWith("com.evenwell.powermonitor")) {
return "AudioIn";
}
}
return getClass().getSimpleName();
}
}

View File

@@ -61,12 +61,12 @@ class AndroidLocationUtils implements LocationUtils {
private String getCountryFromPhoneNetwork() {
Object o = appContext.getSystemService(TELEPHONY_SERVICE);
TelephonyManager tm = (TelephonyManager) o;
return tm.getNetworkCountryIso();
return tm == null ? "" : tm.getNetworkCountryIso();
}
private String getCountryFromSimCard() {
Object o = appContext.getSystemService(TELEPHONY_SERVICE);
TelephonyManager tm = (TelephonyManager) o;
return tm.getSimCountryIso();
return tm == null ? "" : tm.getSimCountryIso();
}
}

View File

@@ -3,6 +3,8 @@ package org.briarproject.bramble.util;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.provider.Settings;
@@ -71,7 +73,7 @@ public class AndroidUtils {
return new Pair<>("", "");
}
private static boolean isValidBluetoothAddress(@Nullable String address) {
public static boolean isValidBluetoothAddress(@Nullable String address) {
return !StringUtils.isNullOrEmpty(address)
&& BluetoothAdapter.checkBluetoothAddress(address)
&& !address.equals(FAKE_BLUETOOTH_ADDRESS);
@@ -117,4 +119,17 @@ public class AndroidUtils {
if (SDK_INT < 24) return new String[] {"image/jpeg", "image/png"};
else return new String[] {"image/jpeg", "image/png", "image/gif"};
}
public static String getWakeLockTag(Context ctx) {
PackageManager pm = ctx.getPackageManager();
for (PackageInfo info : pm.getInstalledPackages(0)) {
String name = info.packageName.toLowerCase();
if (name.startsWith("com.huawei.powergenie")) {
return "LocationManagerService";
} else if (name.startsWith("com.evenwell.powermonitor")) {
return "AudioIn";
}
}
return ctx.getPackageName();
}
}

View File

@@ -72,7 +72,9 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
@Test
public void testDeleteAccountClearsSharedPrefsAndDeletesFiles()
throws Exception {
// Directories 'lib' and 'shared_prefs' should be spared
// Directories 'code_cache', 'lib' and 'shared_prefs' should be spared
File codeCacheDir = new File(testDir, "code_cache");
File codeCacheFile = new File(codeCacheDir, "file");
File libDir = new File(testDir, "lib");
File libFile = new File(libDir, "file");
File sharedPrefsDir = new File(testDir, "shared_prefs");
@@ -111,6 +113,8 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
assertTrue(dbDir.mkdirs());
assertTrue(keyDir.mkdirs());
assertTrue(codeCacheDir.mkdirs());
assertTrue(codeCacheFile.createNewFile());
assertTrue(libDir.mkdirs());
assertTrue(libFile.createNewFile());
assertTrue(sharedPrefsDir.mkdirs());
@@ -126,6 +130,8 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
assertFalse(dbDir.exists());
assertFalse(keyDir.exists());
assertTrue(codeCacheDir.exists());
assertTrue(codeCacheFile.exists());
assertTrue(libDir.exists());
assertTrue(libFile.exists());
assertTrue(sharedPrefsDir.exists());

View File

@@ -70,7 +70,7 @@ dependencyVerification {
'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.11-2:obfs4proxy-android-0.0.11-2.zip:57e55cbe87aa2aac210fdbb6cd8cdeafe15f825406a08ebf77a8b787aa2c6a8a',
'org.briarproject:tor-android:0.3.5.9:tor-android-0.3.5.9.zip:853b0440feccd6904bd03e6b2de53a62ebcde1d58068beeadc447a7dff950bc8',
'org.briarproject:tor-android:0.3.5.10:tor-android-0.3.5.10.zip:edd83bf557fcff2105eaa0bdb3f607a6852ebe7360920929ae3039dd5f4774c5',
'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.codehaus.groovy:groovy-all:2.4.15:groovy-all-2.4.15.jar:51d6c4e71782e85674239189499854359d380fb75e1a703756e3aaa5b98a5af0',

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.api.account;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -13,7 +14,8 @@ public interface AccountManager {
* Returns true if the manager has the database key. This will be false
* before {@link #createAccount(String, String)} or {@link #signIn(String)}
* has been called, and true after {@link #createAccount(String, String)}
* or {@link #signIn(String)} has returned true, until the process exits.
* or {@link #signIn(String)} has returned true, until
* {@link #deleteAccount()} is called or the process exits.
*/
boolean hasDatabaseKey();
@@ -22,25 +24,22 @@ public interface AccountManager {
* before {@link #createAccount(String, String)} or {@link #signIn(String)}
* has been called, and non-null after
* {@link #createAccount(String, String)} or {@link #signIn(String)} has
* returned true, until the process exits.
* returned true, until {@link #deleteAccount()} is called or the process
* exits.
*/
@Nullable
SecretKey getDatabaseKey();
/**
* Returns true if the encrypted database key can be loaded from disk, and
* the database directory exists and is a directory.
* Returns true if the encrypted database key can be loaded from disk.
*/
boolean accountExists();
/**
* Creates an identity with the given name and registers it with the
* {@link IdentityManager}. Creates a database key, encrypts it with the
* given password and stores it on disk.
* <p/>
* This method does not create the database directory, so
* {@link #accountExists()} will continue to return false until the
* database directory is created.
* given password and stores it on disk. {@link #accountExists()} will
* return true after this method returns true.
*/
boolean createAccount(String name, String password);
@@ -54,17 +53,19 @@ public interface AccountManager {
* Loads the encrypted database key from disk and decrypts it with the
* given password.
*
* @return true if the database key was successfully loaded and decrypted.
* @throws DecryptionException If the database key could not be loaded and
* decrypted.
*/
boolean signIn(String password);
void signIn(String password) throws DecryptionException;
/**
* Loads the encrypted database key from disk, decrypts it with the old
* password, encrypts it with the new password, and stores it on disk,
* replacing the old key.
*
* @return true if the database key was successfully loaded, re-encrypted
* and stored.
* @throws DecryptionException If the database key could not be loaded and
* decrypted.
*/
boolean changePassword(String oldPassword, String newPassword);
void changePassword(String oldPassword, String newPassword)
throws DecryptionException;
}

View File

@@ -1,8 +1,11 @@
package org.briarproject.bramble.api.plugin;
package org.briarproject.bramble.api.connection;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
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;
@NotNullByDefault

View File

@@ -0,0 +1,130 @@
package org.briarproject.bramble.api.connection;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginConfig;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionClosedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionOpenedEvent;
import org.briarproject.bramble.api.sync.Priority;
import java.util.Collection;
/**
* Keeps track of which contacts are currently connected by which transports.
*/
@NotNullByDefault
public interface ConnectionRegistry {
/**
* Registers an incoming connection from the given contact over the given
* transport. The connection's {@link Priority priority} can be set later
* via {@link #setPriority(ContactId, TransportId, InterruptibleConnection,
* Priority)} if a priority record is received from the contact.
* <p>
* Broadcasts {@link ConnectionOpenedEvent}. Also broadcasts
* {@link ContactConnectedEvent} if this is the only connection with the
* contact.
*/
void registerIncomingConnection(ContactId c, TransportId t,
InterruptibleConnection conn);
/**
* Registers an outgoing connection to the given contact over the given
* transport.
* <p>
* Broadcasts {@link ConnectionOpenedEvent}. Also broadcasts
* {@link ContactConnectedEvent} if this is the only connection with the
* contact.
* <p>
* If the registry has any "better" connections with the given contact, the
* given connection will be interrupted. If the registry has any "worse"
* connections with the given contact, those connections will be
* interrupted.
* <p>
* Connection A is considered "better" than connection B if both
* connections have had their priorities set, and either A's transport is
* {@link PluginConfig#getTransportPreferences() preferred} to B's, or
* they use the same transport and A has higher {@link Priority priority}
* than B.
* <p>
* For backward compatibility, connections without priorities are not
* considered better or worse than other connections.
*/
void registerOutgoingConnection(ContactId c, TransportId t,
InterruptibleConnection conn, Priority priority);
/**
* Unregisters a connection with the given contact over the given transport.
* <p>
* Broadcasts {@link ConnectionClosedEvent}. Also broadcasts
* {@link ContactDisconnectedEvent} if this is the only connection with
* the contact.
*/
void unregisterConnection(ContactId c, TransportId t,
InterruptibleConnection conn, boolean incoming, boolean exception);
/**
* Sets the {@link Priority priority} of a connection that was previously
* registered via {@link #registerIncomingConnection(ContactId, TransportId,
* InterruptibleConnection)}.
* <p>
* If the registry has any "better" connections with the given contact, the
* given connection will be interrupted. If the registry has any "worse"
* connections with the given contact, those connections will be
* interrupted.
* <p>
* Connection A is considered "better" than connection B if both
* connections have had their priorities set, and either A's transport is
* {@link PluginConfig#getTransportPreferences() preferred} to B's, or
* they use the same transport and A has higher {@link Priority priority}
* than B.
* <p>
* For backward compatibility, connections without priorities are not
* considered better or worse than other connections.
*/
void setPriority(ContactId c, TransportId t, InterruptibleConnection conn,
Priority priority);
/**
* Returns any contacts that are connected via the given transport.
*/
Collection<ContactId> getConnectedContacts(TransportId t);
/**
* Returns any contacts that are connected via the given transport or any
* {@link PluginConfig#getTransportPreferences() better} transport.
*/
Collection<ContactId> getConnectedOrBetterContacts(TransportId t);
/**
* Returns true if the given contact is connected via the given transport.
*/
boolean isConnected(ContactId c, TransportId t);
/**
* Returns true if the given contact is connected via any transport.
*/
boolean isConnected(ContactId c);
/**
* Registers a connection with the given pending contact. Broadcasts
* {@link RendezvousConnectionOpenedEvent} if this is the only connection
* with the pending contact.
*
* @return True if this is the only connection with the pending contact,
* false if it is redundant and should be closed
*/
boolean registerConnection(PendingContactId p);
/**
* Unregisters a connection with the given pending contact. Broadcasts
* {@link RendezvousConnectionClosedEvent}.
*/
void unregisterConnection(PendingContactId p, boolean success);
}

View File

@@ -0,0 +1,19 @@
package org.briarproject.bramble.api.connection;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
/**
* A duplex sync connection that can be closed by interrupting its outgoing
* sync session.
*/
@NotNullByDefault
public interface InterruptibleConnection {
/**
* Interrupts the connection's outgoing sync session. If the underlying
* transport connection is alive and the remote peer is cooperative, this
* should result in both sync sessions ending and the connection being
* cleanly closed.
*/
void interruptOutgoingSession();
}

View File

@@ -142,16 +142,17 @@ public interface CryptoComponent {
/**
* Decrypts and authenticates the given ciphertext that has been read from
* storage. The encryption and authentication keys are derived from the
* given password. Returns null if the ciphertext cannot be decrypted and
* authenticated (for example, if the password is wrong).
* given password.
*
* @param keyStrengthener Used to strengthen the password-based key. If
* null, or if strengthening was not used when encrypting the ciphertext,
* the password-based key will not be strengthened
* @throws DecryptionException If the ciphertext cannot be decrypted and
* authenticated (for example, if the password is wrong).
*/
@Nullable
byte[] decryptWithPassword(byte[] ciphertext, String password,
@Nullable KeyStrengthener keyStrengthener);
@Nullable KeyStrengthener keyStrengthener)
throws DecryptionException;
/**
* Returns true if the given ciphertext was encrypted using a strengthened

View File

@@ -0,0 +1,17 @@
package org.briarproject.bramble.api.crypto;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public class DecryptionException extends Exception {
private final DecryptionResult result;
public DecryptionException(DecryptionResult result) {
this.result = result;
}
public DecryptionResult getDecryptionResult() {
return result;
}
}

View File

@@ -0,0 +1,29 @@
package org.briarproject.bramble.api.crypto;
/**
* The result of a password-based decryption operation.
*/
public enum DecryptionResult {
/**
* Decryption succeeded.
*/
SUCCESS,
/**
* Decryption failed because the format of the ciphertext was invalid.
*/
INVALID_CIPHERTEXT,
/**
* Decryption failed because the {@link KeyStrengthener} used for
* encryption was not available for decryption.
*/
KEY_STRENGTHENER_ERROR,
/**
* Decryption failed because the password used for decryption did not match
* the password used for encryption.
*/
INVALID_PASSWORD
}

View File

@@ -18,6 +18,8 @@ public interface EventBus {
/**
* Asynchronously notifies all listeners of an event. Listeners are
* notified on the {@link EventExecutor}.
* <p>
* This method can safely be called while holding a lock.
*/
void broadcast(Event e);
}

View File

@@ -0,0 +1,15 @@
package org.briarproject.bramble.api.io;
import java.io.InputStream;
public interface TimeoutMonitor {
/**
* Returns an {@link InputStream} that wraps the given stream and allows
* read timeouts to be detected.
*
* @param timeoutMs The read timeout in milliseconds. Timeouts will be
* detected eventually but are not guaranteed to be detected immediately.
*/
InputStream createTimeoutInputStream(InputStream in, long timeoutMs);
}

View File

@@ -6,8 +6,10 @@ public interface BluetoothConstants {
int UUID_BYTES = 16;
// Transport properties
String PROP_ADDRESS = "address";
String PROP_UUID = "uuid";
String PREF_BT_ENABLE = "enable";
// Default value for PREF_PLUGIN_ENABLE
boolean DEFAULT_PREF_PLUGIN_ENABLE = false;
}

View File

@@ -1,67 +0,0 @@
package org.briarproject.bramble.api.plugin;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionClosedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionOpenedEvent;
import java.util.Collection;
/**
* Keeps track of which contacts are currently connected by which transports.
*/
@NotNullByDefault
public interface ConnectionRegistry {
/**
* Registers a connection with the given contact over the given transport.
* Broadcasts {@link ConnectionOpenedEvent}. Also broadcasts
* {@link ContactConnectedEvent} if this is the only connection with the
* contact.
*/
void registerConnection(ContactId c, TransportId t, boolean incoming);
/**
* Unregisters a connection with the given contact over the given transport.
* Broadcasts {@link ConnectionClosedEvent}. Also broadcasts
* {@link ContactDisconnectedEvent} if this is the only connection with
* the contact.
*/
void unregisterConnection(ContactId c, TransportId t, boolean incoming);
/**
* Returns any contacts that are connected via the given transport.
*/
Collection<ContactId> getConnectedContacts(TransportId t);
/**
* Returns true if the given contact is connected via the given transport.
*/
boolean isConnected(ContactId c, TransportId t);
/**
* Returns true if the given contact is connected via any transport.
*/
boolean isConnected(ContactId c);
/**
* Registers a connection with the given pending contact. Broadcasts
* {@link RendezvousConnectionOpenedEvent} if this is the only connection
* with the pending contact.
*
* @return True if this is the only connection with the pending contact,
* false if it is redundant and should be closed
*/
boolean registerConnection(PendingContactId p);
/**
* Unregisters a connection with the given pending contact. Broadcasts
* {@link RendezvousConnectionClosedEvent}.
*/
void unregisterConnection(PendingContactId p, boolean success);
}

View File

@@ -4,10 +4,15 @@ public interface LanTcpConstants {
TransportId ID = new TransportId("org.briarproject.bramble.lan");
// a transport property (shared with contacts)
// Transport properties (shared with contacts)
String PROP_IP_PORTS = "ipPorts";
String PROP_PORT = "port";
String PROP_IPV6 = "ipv6";
// a local setting
// Local settings (not shared with contacts)
String PREF_LAN_IP_PORTS = "ipPorts";
String PREF_IPV6 = "ipv6";
// Default value for PREF_PLUGIN_ENABLE
boolean DEFAULT_PREF_PLUGIN_ENABLE = true;
}

View File

@@ -3,12 +3,55 @@ package org.briarproject.bramble.api.plugin;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.settings.SettingsManager;
import java.util.Collection;
@NotNullByDefault
public interface Plugin {
enum State {
/**
* The plugin has not finished starting or has been stopped.
*/
STARTING_STOPPING,
/**
* The plugin is disabled by settings. Use {@link #getReasonsDisabled()}
* to find out which settings are responsible.
*/
DISABLED,
/**
* The plugin is being enabled and can't yet make or receive
* connections.
*/
ENABLING,
/**
* The plugin is enabled and can make or receive connections.
*/
ACTIVE,
/**
* The plugin is enabled but can't make or receive connections
*/
INACTIVE
}
/**
* The string for the boolean preference
* to use with the {@link SettingsManager} to enable or disable the plugin.
*/
String PREF_PLUGIN_ENABLE = "enable";
/**
* Reason flag returned by {@link #getReasonsDisabled()} to indicate that
* the plugin has been disabled by the user.
*/
int REASON_USER = 1;
/**
* Returns the plugin's transport identifier.
*/
@@ -35,9 +78,18 @@ public interface Plugin {
void stop() throws PluginException;
/**
* Returns true if the plugin is running.
* Returns the current state of the plugin.
*/
boolean isRunning();
State getState();
/**
* Returns a set of flags indicating why the plugin is
* {@link State#DISABLED disabled}, or 0 if the plugin is not disabled.
* <p>
* The flags used are plugin-specific, except the generic flag
* {@link #REASON_USER}, which may be used by any plugin.
*/
int getReasonsDisabled();
/**
* Returns true if the plugin should be polled periodically to attempt to

View File

@@ -1,6 +1,10 @@
package org.briarproject.bramble.api.plugin;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin.State;
import org.briarproject.bramble.api.plugin.event.TransportActiveEvent;
import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent;
import org.briarproject.bramble.api.plugin.event.TransportStateEvent;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.settings.Settings;
@@ -32,12 +36,17 @@ public interface PluginCallback extends ConnectionHandler {
void mergeLocalProperties(TransportProperties p);
/**
* Signals that the transport is enabled.
* Informs the callback of the plugin's current state.
* <p>
* If the current state is different from the previous state, the callback
* will broadcast a {@link TransportStateEvent}. If the current state is
* {@link State#ACTIVE} and the previous state was not
* {@link State#ACTIVE}, the callback will broadcast a
* {@link TransportActiveEvent}. If the current state is not
* {@link State#ACTIVE} and the previous state was {@link State#ACTIVE},
* the callback will broadcast a {@link TransportInactiveEvent}.
* <p>
* This method can safely be called while holding a lock.
*/
void transportEnabled();
/**
* Signals that the transport is disabled.
*/
void transportDisabled();
void pluginStateChanged(State state);
}

View File

@@ -5,6 +5,8 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@NotNullByDefault
public interface PluginConfig {
@@ -14,4 +16,11 @@ public interface PluginConfig {
Collection<SimplexPluginFactory> getSimplexFactories();
boolean shouldPoll();
/**
* Returns a map representing transport preferences. For each entry in the
* map, connections via the transports identified by the value are
* preferred to connections via the transport identified by the key.
*/
Map<TransportId, List<TransportId>> getTransportPreferences();
}

View File

@@ -41,4 +41,17 @@ public interface PluginManager {
* Returns any duplex plugins that support rendezvous.
*/
Collection<DuplexPlugin> getRendezvousPlugins();
/**
* Enables or disables the plugin
* identified by the given {@link TransportId}.
* <p>
* Note that this applies the change asynchronously
* and there are no order guarantees.
* <p>
* If no plugin with the given {@link TransportId} is registered,
* this is a no-op.
*/
void setPluginEnabled(TransportId t, boolean enabled);
}

View File

@@ -4,6 +4,7 @@ public interface TorConstants {
TransportId ID = new TransportId("org.briarproject.bramble.tor");
// Transport properties
String PROP_ONION_V2 = "onion";
String PROP_ONION_V3 = "onion3";
@@ -13,14 +14,37 @@ public interface TorConstants {
int CONNECT_TO_PROXY_TIMEOUT = 5000; // Milliseconds
int EXTRA_SOCKET_TIMEOUT = 30000; // Milliseconds
// Local settings (not shared with contacts)
String PREF_TOR_NETWORK = "network2";
String PREF_TOR_PORT = "port";
String PREF_TOR_MOBILE = "useMobileData";
String PREF_TOR_ONLY_WHEN_CHARGING = "onlyWhenCharging";
// Values for PREF_TOR_NETWORK
int PREF_TOR_NETWORK_AUTOMATIC = 0;
int PREF_TOR_NETWORK_WITHOUT_BRIDGES = 1;
int PREF_TOR_NETWORK_WITH_BRIDGES = 2;
// TODO: Remove when settings migration code is removed
int PREF_TOR_NETWORK_NEVER = 3;
// Default values for local settings
boolean DEFAULT_PREF_PLUGIN_ENABLE = true;
int DEFAULT_PREF_TOR_NETWORK = PREF_TOR_NETWORK_AUTOMATIC;
boolean DEFAULT_PREF_TOR_MOBILE = true;
boolean DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING = false;
/**
* Reason flag returned by {@link Plugin#getReasonsDisabled()}.
*/
int REASON_BATTERY = 2;
/**
* Reason flag returned by {@link Plugin#getReasonsDisabled()}.
*/
int REASON_MOBILE_DATA = 4;
/**
* Reason flag returned by {@link Plugin#getReasonsDisabled()}.
*/
int REASON_COUNTRY_BLOCKED = 8;
}

View File

@@ -4,4 +4,7 @@ public interface WanTcpConstants {
TransportId ID = new TransportId("org.briarproject.bramble.wan");
// Default value for PREF_PLUGIN_ENABLE
boolean DEFAULT_PREF_PLUGIN_ENABLE = false;
}

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.io.IOException;
import java.io.InputStream;
@@ -14,6 +15,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
public abstract class AbstractDuplexTransportConnection
implements DuplexTransportConnection {
protected final TransportProperties remote = new TransportProperties();
private final Plugin plugin;
private final Reader reader;
private final Writer writer;
@@ -44,6 +47,11 @@ public abstract class AbstractDuplexTransportConnection
return writer;
}
@Override
public TransportProperties getRemoteProperties() {
return remote;
}
private class Reader implements TransportConnectionReader {
@Override

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.api.plugin.duplex;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.properties.TransportProperties;
/**
* An interface for reading and writing data over a duplex transport. The
@@ -23,4 +24,10 @@ public interface DuplexTransportConnection {
* for writing to the connection.
*/
TransportConnectionWriter getWriter();
/**
* Returns a possibly empty set of {@link TransportProperties} describing
* the remote peer.
*/
TransportProperties getRemoteProperties();
}

View File

@@ -13,13 +13,14 @@ public class ConnectionClosedEvent extends Event {
private final ContactId contactId;
private final TransportId transportId;
private final boolean incoming;
private final boolean incoming, exception;
public ConnectionClosedEvent(ContactId contactId, TransportId transportId,
boolean incoming) {
boolean incoming, boolean exception) {
this.contactId = contactId;
this.transportId = transportId;
this.incoming = incoming;
this.exception = exception;
}
public ContactId getContactId() {
@@ -33,4 +34,8 @@ public class ConnectionClosedEvent extends Event {
public boolean isIncoming() {
return incoming;
}
public boolean isException() {
return exception;
}
}

View File

@@ -2,20 +2,22 @@ package org.briarproject.bramble.api.plugin.event;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin.State;
import org.briarproject.bramble.api.plugin.TransportId;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when a transport is disabled.
* An event that is broadcast when a plugin enters the {@link State#ACTIVE}
* state.
*/
@Immutable
@NotNullByDefault
public class TransportDisabledEvent extends Event {
public class TransportActiveEvent extends Event {
private final TransportId transportId;
public TransportDisabledEvent(TransportId transportId) {
public TransportActiveEvent(TransportId transportId) {
this.transportId = transportId;
}

View File

@@ -2,20 +2,22 @@ package org.briarproject.bramble.api.plugin.event;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin.State;
import org.briarproject.bramble.api.plugin.TransportId;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when a transport is enabled.
* An event that is broadcast when a plugin leaves the {@link State#ACTIVE}
* state.
*/
@Immutable
@NotNullByDefault
public class TransportEnabledEvent extends Event {
public class TransportInactiveEvent extends Event {
private final TransportId transportId;
public TransportEnabledEvent(TransportId transportId) {
public TransportInactiveEvent(TransportId transportId) {
this.transportId = transportId;
}

View File

@@ -0,0 +1,32 @@
package org.briarproject.bramble.api.plugin.event;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin.State;
import org.briarproject.bramble.api.plugin.TransportId;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when the {@link State state} of a plugin changes.
*/
@Immutable
@NotNullByDefault
public class TransportStateEvent extends Event {
private final TransportId transportId;
private final State state;
public TransportStateEvent(TransportId transportId, State state) {
this.transportId = transportId;
this.state = state;
}
public TransportId getTransportId() {
return transportId;
}
public State getState() {
return state;
}
}

View File

@@ -11,4 +11,28 @@ public interface TransportPropertyConstants {
* The maximum length of a property's key or value in UTF-8 bytes.
*/
int MAX_PROPERTY_LENGTH = 100;
/**
* Message metadata key for the transport ID of a local or remote update,
* as a BDF string.
*/
String MSG_KEY_TRANSPORT_ID = "transportId";
/**
* 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";
/**
* Group metadata key for any discovered transport properties of the
* contact, as a BDF dictionary.
*/
String GROUP_KEY_DISCOVERED = "discovered";
}

View File

@@ -34,6 +34,14 @@ public interface TransportPropertyManager {
void addRemoteProperties(Transaction txn, ContactId c,
Map<TransportId, TransportProperties> props) throws DbException;
/**
* Stores the given properties discovered from an incoming transport
* connection. They will be overridden by any properties received while
* adding the contact or synced from the contact.
*/
void addRemotePropertiesFromConnection(ContactId c, TransportId t,
TransportProperties props) throws DbException;
/**
* Returns the local transport properties for all transports.
*/

View File

@@ -0,0 +1,23 @@
package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable;
/**
* A record containing a nonce for choosing between redundant sessions.
*/
@Immutable
@NotNullByDefault
public class Priority {
private final byte[] nonce;
public Priority(byte[] nonce) {
this.nonce = nonce;
}
public byte[] getNonce() {
return nonce;
}
}

View File

@@ -0,0 +1,13 @@
package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
/**
* An interface for handling a {@link Priority} record received by an
* incoming {@link SyncSession}.
*/
@NotNullByDefault
public interface PriorityHandler {
void handle(Priority p);
}

View File

@@ -10,4 +10,5 @@ public interface RecordTypes {
byte OFFER = 2;
byte REQUEST = 3;
byte VERSIONS = 4;
byte PRIORITY = 5;
}

View File

@@ -49,4 +49,10 @@ public interface SyncConstants {
* simultaneously.
*/
int MAX_SUPPORTED_VERSIONS = 10;
/**
* The length of the priority nonce used for choosing between redundant
* connections.
*/
int PRIORITY_NONCE_BYTES = 16;
}

View File

@@ -28,4 +28,8 @@ public interface SyncRecordReader {
boolean hasVersions() throws IOException;
Versions readVersions() throws IOException;
boolean hasPriority() throws IOException;
Priority readPriority() throws IOException;
}

View File

@@ -17,5 +17,7 @@ public interface SyncRecordWriter {
void writeVersions(Versions v) throws IOException;
void writePriority(Priority p) throws IOException;
void flush() throws IOException;
}

View File

@@ -2,18 +2,23 @@ package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.transport.StreamWriter;
import java.io.InputStream;
import javax.annotation.Nullable;
@NotNullByDefault
public interface SyncSessionFactory {
SyncSession createIncomingSession(ContactId c, InputStream in);
SyncSession createIncomingSession(ContactId c, InputStream in,
PriorityHandler handler);
SyncSession createSimplexOutgoingSession(ContactId c, int maxLatency,
StreamWriter streamWriter);
SyncSession createSimplexOutgoingSession(ContactId c, TransportId t,
int maxLatency, StreamWriter streamWriter);
SyncSession createDuplexOutgoingSession(ContactId c, int maxLatency,
int maxIdleTime, StreamWriter streamWriter);
SyncSession createDuplexOutgoingSession(ContactId c, TransportId t,
int maxLatency, int maxIdleTime, StreamWriter streamWriter,
@Nullable Priority priority);
}

View File

@@ -0,0 +1,26 @@
package org.briarproject.bramble.api.sync.event;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when all sync connections using a given
* transport should be closed.
*/
@Immutable
@NotNullByDefault
public class CloseSyncConnectionsEvent extends Event {
private final TransportId transportId;
public CloseSyncConnectionsEvent(TransportId transportId) {
this.transportId = transportId;
}
public TransportId getTransportId() {
return transportId;
}
}

View File

@@ -117,4 +117,10 @@ public class IoUtils {
throw new IOException(e);
}
}
public static boolean isNonEmptyDirectory(File f) {
if (!f.isDirectory()) return false;
File[] children = f.listFiles();
return children != null && children.length > 0;
}
}

View File

@@ -2,13 +2,17 @@ package org.briarproject.bramble.util;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.net.Inet6Address;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import javax.annotation.Nullable;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.bramble.util.StringUtils.isValidMac;
import static org.briarproject.bramble.util.StringUtils.toHexString;
@NotNullByDefault
public class PrivacyUtils {
@@ -19,7 +23,7 @@ public class PrivacyUtils {
@Nullable
public static String scrubMacAddress(@Nullable String address) {
if (address == null || address.length() == 0) return null;
if (isNullOrEmpty(address) || !isValidMac(address)) return address;
// this is a fake address we need to know about
if (address.equals("02:00:00:00:00:00")) return address;
// keep first and last octet of MAC address
@@ -27,39 +31,37 @@ public class PrivacyUtils {
+ address.substring(14, 17);
}
@Nullable
public static String scrubInetAddress(InetAddress address) {
// don't scrub link and site local addresses
if (address.isLinkLocalAddress() || address.isSiteLocalAddress())
return address.toString();
// completely scrub IPv6 addresses
if (address instanceof Inet6Address) return "[scrubbed]";
// keep first and last octet of IPv4 addresses
return scrubInetAddress(address.toString());
if (address instanceof Inet4Address) {
// Don't scrub local IPv4 addresses
if (address.isLoopbackAddress() || address.isLinkLocalAddress() ||
address.isSiteLocalAddress()) {
return address.getHostAddress();
}
// Keep first and last octet of non-local IPv4 addresses
return scrubIpv4Address(address.getAddress());
} else {
// Keep first and last octet of IPv6 addresses
return scrubIpv6Address(address.getAddress());
}
}
@Nullable
public static String scrubInetAddress(@Nullable String address) {
if (address == null) return null;
int firstDot = address.indexOf(".");
if (firstDot == -1) return "[scrubbed]";
String prefix = address.substring(0, firstDot + 1);
int lastDot = address.lastIndexOf(".");
String suffix = address.substring(lastDot, address.length());
return prefix + "[scrubbed]" + suffix;
private static String scrubIpv4Address(byte[] ipv4) {
return (ipv4[0] & 0xFF) + ".[scrubbed]." + (ipv4[3] & 0xFF);
}
private static String scrubIpv6Address(byte[] ipv6) {
String hex = toHexString(ipv6).toLowerCase();
return hex.substring(0, 2) + "[scrubbed]" + hex.substring(30);
}
@Nullable
public static String scrubSocketAddress(InetSocketAddress address) {
InetAddress inetAddress = address.getAddress();
return scrubInetAddress(inetAddress);
return scrubInetAddress(address.getAddress());
}
@Nullable
public static String scrubSocketAddress(SocketAddress address) {
if (address instanceof InetSocketAddress)
return scrubSocketAddress((InetSocketAddress) address);
return scrubInetAddress(address.toString());
return "[scrubbed]";
}
}

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble;
import org.briarproject.bramble.client.ClientModule;
import org.briarproject.bramble.connection.ConnectionModule;
import org.briarproject.bramble.contact.ContactModule;
import org.briarproject.bramble.crypto.CryptoExecutorModule;
import org.briarproject.bramble.crypto.CryptoModule;
@@ -9,6 +10,7 @@ import org.briarproject.bramble.db.DatabaseExecutorModule;
import org.briarproject.bramble.db.DatabaseModule;
import org.briarproject.bramble.event.EventModule;
import org.briarproject.bramble.identity.IdentityModule;
import org.briarproject.bramble.io.IoModule;
import org.briarproject.bramble.keyagreement.KeyAgreementModule;
import org.briarproject.bramble.lifecycle.LifecycleModule;
import org.briarproject.bramble.plugin.PluginModule;
@@ -27,6 +29,7 @@ import dagger.Module;
@Module(includes = {
ClientModule.class,
ConnectionModule.class,
ContactModule.class,
CryptoModule.class,
CryptoExecutorModule.class,
@@ -35,6 +38,7 @@ import dagger.Module;
DatabaseExecutorModule.class,
EventModule.class,
IdentityModule.class,
IoModule.class,
KeyAgreementModule.class,
LifecycleModule.class,
PluginModule.class,

View File

@@ -2,6 +2,7 @@ package org.briarproject.bramble.account;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.KeyStrengthener;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseConfig;
@@ -17,6 +18,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.logging.Logger;
import javax.annotation.Nullable;
@@ -24,6 +26,7 @@ import javax.annotation.concurrent.GuardedBy;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.fromHexString;
import static org.briarproject.bramble.util.StringUtils.toHexString;
@@ -95,7 +98,7 @@ class AccountManagerImpl implements AccountManager {
}
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(
new FileInputStream(f), "UTF-8"));
new FileInputStream(f), Charset.forName("UTF-8")));
String key = reader.readLine();
reader.close();
return key;
@@ -147,7 +150,7 @@ class AccountManagerImpl implements AccountManager {
@GuardedBy("stateChangeLock")
private void writeDbKeyToFile(String key, File f) throws IOException {
FileOutputStream out = new FileOutputStream(f);
out.write(key.getBytes("UTF-8"));
out.write(key.getBytes(Charset.forName("UTF-8")));
out.flush();
out.close();
}
@@ -155,8 +158,7 @@ class AccountManagerImpl implements AccountManager {
@Override
public boolean accountExists() {
synchronized (stateChangeLock) {
return loadEncryptedDatabaseKey() != null
&& databaseConfig.getDatabaseDirectory().isDirectory();
return loadEncryptedDatabaseKey() != null;
}
}
@@ -193,31 +195,24 @@ class AccountManagerImpl implements AccountManager {
}
@Override
public boolean signIn(String password) {
public void signIn(String password) throws DecryptionException {
synchronized (stateChangeLock) {
SecretKey key = loadAndDecryptDatabaseKey(password);
if (key == null) return false;
databaseKey = key;
return true;
databaseKey = loadAndDecryptDatabaseKey(password);
}
}
@GuardedBy("stateChangeLock")
@Nullable
private SecretKey loadAndDecryptDatabaseKey(String password) {
private SecretKey loadAndDecryptDatabaseKey(String password)
throws DecryptionException {
String hex = loadEncryptedDatabaseKey();
if (hex == null) {
LOG.warning("Failed to load encrypted database key");
return null;
throw new DecryptionException(INVALID_CIPHERTEXT);
}
byte[] ciphertext = fromHexString(hex);
KeyStrengthener keyStrengthener = databaseConfig.getKeyStrengthener();
byte[] plaintext = crypto.decryptWithPassword(ciphertext, password,
keyStrengthener);
if (plaintext == null) {
LOG.info("Failed to decrypt database key");
return null;
}
SecretKey key = new SecretKey(plaintext);
// If the DB key was encrypted with a weak key and a key strengthener
// is now available, re-encrypt the DB key with a strengthened key
@@ -230,10 +225,11 @@ class AccountManagerImpl implements AccountManager {
}
@Override
public boolean changePassword(String oldPassword, String newPassword) {
public void changePassword(String oldPassword, String newPassword)
throws DecryptionException {
synchronized (stateChangeLock) {
SecretKey key = loadAndDecryptDatabaseKey(oldPassword);
return key != null && encryptAndStoreDatabaseKey(key, newPassword);
encryptAndStoreDatabaseKey(key, newPassword);
}
}
}

View File

@@ -0,0 +1,79 @@
package org.briarproject.bramble.connection;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
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.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
import static org.briarproject.bramble.util.IoUtils.read;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
abstract class Connection {
protected static final Logger LOG = getLogger(Connection.class.getName());
final KeyManager keyManager;
final ConnectionRegistry connectionRegistry;
final StreamReaderFactory streamReaderFactory;
final StreamWriterFactory streamWriterFactory;
Connection(KeyManager keyManager, ConnectionRegistry connectionRegistry,
StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory) {
this.keyManager = keyManager;
this.connectionRegistry = connectionRegistry;
this.streamReaderFactory = streamReaderFactory;
this.streamWriterFactory = streamWriterFactory;
}
@Nullable
StreamContext recogniseTag(TransportConnectionReader reader,
TransportId transportId) {
StreamContext ctx;
try {
byte[] tag = readTag(reader.getInputStream());
return keyManager.getStreamContext(transportId, tag);
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
return null;
}
}
private byte[] readTag(InputStream in) throws IOException {
byte[] tag = new byte[TAG_LENGTH];
read(in, tag);
return tag;
}
void disposeOnError(TransportConnectionReader reader, boolean recognised) {
try {
reader.dispose(true, recognised);
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
void disposeOnError(TransportConnectionWriter writer) {
try {
writer.dispose(true);
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
}

View File

@@ -0,0 +1,114 @@
package org.briarproject.bramble.connection;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.contact.ContactExchangeManager;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.HandshakeManager;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
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.properties.TransportPropertyManager;
import org.briarproject.bramble.api.sync.SyncSessionFactory;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import java.security.SecureRandom;
import java.util.concurrent.Executor;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@Immutable
@NotNullByDefault
class ConnectionManagerImpl implements ConnectionManager {
private final Executor ioExecutor;
private final KeyManager keyManager;
private final StreamReaderFactory streamReaderFactory;
private final StreamWriterFactory streamWriterFactory;
private final SyncSessionFactory syncSessionFactory;
private final HandshakeManager handshakeManager;
private final ContactExchangeManager contactExchangeManager;
private final ConnectionRegistry connectionRegistry;
private final TransportPropertyManager transportPropertyManager;
private final SecureRandom secureRandom;
@Inject
ConnectionManagerImpl(@IoExecutor Executor ioExecutor,
KeyManager keyManager, StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory,
SyncSessionFactory syncSessionFactory,
HandshakeManager handshakeManager,
ContactExchangeManager contactExchangeManager,
ConnectionRegistry connectionRegistry,
TransportPropertyManager transportPropertyManager,
SecureRandom secureRandom) {
this.ioExecutor = ioExecutor;
this.keyManager = keyManager;
this.streamReaderFactory = streamReaderFactory;
this.streamWriterFactory = streamWriterFactory;
this.syncSessionFactory = syncSessionFactory;
this.handshakeManager = handshakeManager;
this.contactExchangeManager = contactExchangeManager;
this.connectionRegistry = connectionRegistry;
this.transportPropertyManager = transportPropertyManager;
this.secureRandom = secureRandom;
}
@Override
public void manageIncomingConnection(TransportId t,
TransportConnectionReader r) {
ioExecutor.execute(new IncomingSimplexSyncConnection(keyManager,
connectionRegistry, streamReaderFactory, streamWriterFactory,
syncSessionFactory, transportPropertyManager, t, r));
}
@Override
public void manageIncomingConnection(TransportId t,
DuplexTransportConnection d) {
ioExecutor.execute(new IncomingDuplexSyncConnection(keyManager,
connectionRegistry, streamReaderFactory, streamWriterFactory,
syncSessionFactory, transportPropertyManager, ioExecutor,
t, d));
}
@Override
public void manageIncomingConnection(PendingContactId p, TransportId t,
DuplexTransportConnection d) {
ioExecutor.execute(new IncomingHandshakeConnection(keyManager,
connectionRegistry, streamReaderFactory, streamWriterFactory,
handshakeManager, contactExchangeManager, this, p, t, d));
}
@Override
public void manageOutgoingConnection(ContactId c, TransportId t,
TransportConnectionWriter w) {
ioExecutor.execute(new OutgoingSimplexSyncConnection(keyManager,
connectionRegistry, streamReaderFactory, streamWriterFactory,
syncSessionFactory, transportPropertyManager, c, t, w));
}
@Override
public void manageOutgoingConnection(ContactId c, TransportId t,
DuplexTransportConnection d) {
ioExecutor.execute(new OutgoingDuplexSyncConnection(keyManager,
connectionRegistry, streamReaderFactory, streamWriterFactory,
syncSessionFactory, transportPropertyManager, ioExecutor,
secureRandom, c, t, d));
}
@Override
public void manageOutgoingConnection(PendingContactId p, TransportId t,
DuplexTransportConnection d) {
ioExecutor.execute(new OutgoingHandshakeConnection(keyManager,
connectionRegistry, streamReaderFactory, streamWriterFactory,
handshakeManager, contactExchangeManager, this, p, t, d));
}
}

View File

@@ -0,0 +1,26 @@
package org.briarproject.bramble.connection;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class ConnectionModule {
@Provides
ConnectionManager provideConnectionManager(
ConnectionManagerImpl connectionManager) {
return connectionManager;
}
@Provides
@Singleton
ConnectionRegistry provideConnectionRegistry(
ConnectionRegistryImpl connectionRegistry) {
return connectionRegistry;
}
}

View File

@@ -0,0 +1,283 @@
package org.briarproject.bramble.connection;
import org.briarproject.bramble.api.Bytes;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.connection.InterruptibleConnection;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginConfig;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionClosedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionOpenedEvent;
import org.briarproject.bramble.api.sync.Priority;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import static java.util.Collections.emptyList;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
@ThreadSafe
@NotNullByDefault
class ConnectionRegistryImpl implements ConnectionRegistry {
private static final Logger LOG =
getLogger(ConnectionRegistryImpl.class.getName());
private final EventBus eventBus;
private final Map<TransportId, List<TransportId>> transportPrefs;
private final Object lock = new Object();
@GuardedBy("lock")
private final Map<ContactId, List<ConnectionRecord>> contactConnections;
@GuardedBy("lock")
private final Set<PendingContactId> connectedPendingContacts;
@Inject
ConnectionRegistryImpl(EventBus eventBus, PluginConfig pluginConfig) {
this.eventBus = eventBus;
transportPrefs = pluginConfig.getTransportPreferences();
contactConnections = new HashMap<>();
connectedPendingContacts = new HashSet<>();
}
@Override
public void registerIncomingConnection(ContactId c, TransportId t,
InterruptibleConnection conn) {
registerConnection(c, t, conn, true);
}
@Override
public void registerOutgoingConnection(ContactId c, TransportId t,
InterruptibleConnection conn, Priority priority) {
registerConnection(c, t, conn, false);
setPriority(c, t, conn, priority);
}
private void registerConnection(ContactId c, TransportId t,
InterruptibleConnection conn, boolean incoming) {
if (LOG.isLoggable(INFO)) {
if (incoming) LOG.info("Incoming connection registered: " + t);
else LOG.info("Outgoing connection registered: " + t);
}
boolean firstConnection;
synchronized (lock) {
List<ConnectionRecord> recs = contactConnections.get(c);
if (recs == null) {
recs = new ArrayList<>();
contactConnections.put(c, recs);
}
firstConnection = recs.isEmpty();
recs.add(new ConnectionRecord(t, conn));
}
eventBus.broadcast(new ConnectionOpenedEvent(c, t, incoming));
if (firstConnection) {
LOG.info("Contact connected");
eventBus.broadcast(new ContactConnectedEvent(c));
}
}
@Override
public void setPriority(ContactId c, TransportId t,
InterruptibleConnection conn, Priority priority) {
if (LOG.isLoggable(INFO)) LOG.info("Setting connection priority: " + t);
List<InterruptibleConnection> toInterrupt;
boolean interruptNewConnection = false;
synchronized (lock) {
List<ConnectionRecord> recs = contactConnections.get(c);
if (recs == null) throw new IllegalArgumentException();
toInterrupt = new ArrayList<>(recs.size());
for (ConnectionRecord rec : recs) {
if (rec.conn == conn) {
// Store the priority of this connection
rec.priority = priority;
} else if (rec.priority != null) {
int compare = compareConnections(t, priority,
rec.transportId, rec.priority);
if (compare == -1) {
// The old connection is better than the new one
interruptNewConnection = true;
} else if (compare == 1 && !rec.interrupted) {
// The new connection is better than the old one
toInterrupt.add(rec.conn);
rec.interrupted = true;
}
}
}
}
if (interruptNewConnection) {
LOG.info("Interrupting new connection");
conn.interruptOutgoingSession();
}
for (InterruptibleConnection old : toInterrupt) {
LOG.info("Interrupting old connection");
old.interruptOutgoingSession();
}
}
private int compareConnections(TransportId tA, Priority pA, TransportId tB,
Priority pB) {
if (getBetterTransports(tA).contains(tB)) return -1;
if (getBetterTransports(tB).contains(tA)) return 1;
return tA.equals(tB) ? Bytes.compare(pA.getNonce(), pB.getNonce()) : 0;
}
private List<TransportId> getBetterTransports(TransportId t) {
List<TransportId> better = transportPrefs.get(t);
return better == null ? emptyList() : better;
}
@Override
public void unregisterConnection(ContactId c, TransportId t,
InterruptibleConnection conn, boolean incoming, boolean exception) {
if (LOG.isLoggable(INFO)) {
if (incoming) LOG.info("Incoming connection unregistered: " + t);
else LOG.info("Outgoing connection unregistered: " + t);
}
boolean lastConnection;
synchronized (lock) {
List<ConnectionRecord> recs = contactConnections.get(c);
if (recs == null || !recs.remove(new ConnectionRecord(t, conn)))
throw new IllegalArgumentException();
lastConnection = recs.isEmpty();
}
eventBus.broadcast(
new ConnectionClosedEvent(c, t, incoming, exception));
if (lastConnection) {
LOG.info("Contact disconnected");
eventBus.broadcast(new ContactDisconnectedEvent(c));
}
}
@Override
public Collection<ContactId> getConnectedContacts(TransportId t) {
synchronized (lock) {
List<ContactId> contactIds = new ArrayList<>();
for (Entry<ContactId, List<ConnectionRecord>> e :
contactConnections.entrySet()) {
for (ConnectionRecord rec : e.getValue()) {
if (rec.transportId.equals(t)) {
contactIds.add(e.getKey());
break;
}
}
}
if (LOG.isLoggable(INFO)) {
LOG.info(contactIds.size() + " contacts connected: " + t);
}
return contactIds;
}
}
@Override
public Collection<ContactId> getConnectedOrBetterContacts(TransportId t) {
synchronized (lock) {
List<TransportId> better = getBetterTransports(t);
List<ContactId> contactIds = new ArrayList<>();
for (Entry<ContactId, List<ConnectionRecord>> e :
contactConnections.entrySet()) {
for (ConnectionRecord rec : e.getValue()) {
if (rec.transportId.equals(t) ||
better.contains(rec.transportId)) {
contactIds.add(e.getKey());
break;
}
}
}
if (LOG.isLoggable(INFO)) {
LOG.info(contactIds.size()
+ " contacts connected or better: " + t);
}
return contactIds;
}
}
@Override
public boolean isConnected(ContactId c, TransportId t) {
synchronized (lock) {
List<ConnectionRecord> recs = contactConnections.get(c);
if (recs == null) return false;
for (ConnectionRecord rec : recs) {
if (rec.transportId.equals(t)) return true;
}
return false;
}
}
@Override
public boolean isConnected(ContactId c) {
synchronized (lock) {
List<ConnectionRecord> recs = contactConnections.get(c);
return recs != null && !recs.isEmpty();
}
}
@Override
public boolean registerConnection(PendingContactId p) {
boolean added;
synchronized (lock) {
added = connectedPendingContacts.add(p);
}
if (added) eventBus.broadcast(new RendezvousConnectionOpenedEvent(p));
return added;
}
@Override
public void unregisterConnection(PendingContactId p, boolean success) {
synchronized (lock) {
if (!connectedPendingContacts.remove(p))
throw new IllegalArgumentException();
}
eventBus.broadcast(new RendezvousConnectionClosedEvent(p, success));
}
private static class ConnectionRecord {
private final TransportId transportId;
private final InterruptibleConnection conn;
@GuardedBy("lock")
@Nullable
private Priority priority = null;
@GuardedBy("lock")
private boolean interrupted = false;
private ConnectionRecord(TransportId transportId,
InterruptibleConnection conn) {
this.transportId = transportId;
this.conn = conn;
}
@Override
public boolean equals(Object o) {
if (o instanceof ConnectionRecord) {
return conn == ((ConnectionRecord) o).conn;
} else {
return false;
}
}
@Override
public int hashCode() {
return conn.hashCode();
}
}
}

View File

@@ -0,0 +1,109 @@
package org.briarproject.bramble.connection;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.connection.InterruptibleConnection;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
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.properties.TransportProperties;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.sync.Priority;
import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.SyncSessionFactory;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import org.briarproject.bramble.api.transport.StreamWriter;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import java.io.IOException;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
@NotNullByDefault
abstract class DuplexSyncConnection extends SyncConnection
implements InterruptibleConnection {
final Executor ioExecutor;
final TransportId transportId;
final TransportConnectionReader reader;
final TransportConnectionWriter writer;
final TransportProperties remote;
private final Object interruptLock = new Object();
@GuardedBy("interruptLock")
@Nullable
private SyncSession outgoingSession = null;
@GuardedBy("interruptLock")
private boolean interruptWaiting = false;
@Override
public void interruptOutgoingSession() {
SyncSession out = null;
synchronized (interruptLock) {
if (outgoingSession == null) interruptWaiting = true;
else out = outgoingSession;
}
if (out != null) out.interrupt();
}
void setOutgoingSession(SyncSession outgoingSession) {
boolean interruptWasWaiting = false;
synchronized (interruptLock) {
this.outgoingSession = outgoingSession;
if (interruptWaiting) {
interruptWasWaiting = true;
interruptWaiting = false;
}
}
if (interruptWasWaiting) outgoingSession.interrupt();
}
DuplexSyncConnection(KeyManager keyManager,
ConnectionRegistry connectionRegistry,
StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory,
SyncSessionFactory syncSessionFactory,
TransportPropertyManager transportPropertyManager,
Executor ioExecutor, TransportId transportId,
DuplexTransportConnection connection) {
super(keyManager, connectionRegistry, streamReaderFactory,
streamWriterFactory, syncSessionFactory,
transportPropertyManager);
this.ioExecutor = ioExecutor;
this.transportId = transportId;
reader = connection.getReader();
writer = connection.getWriter();
remote = connection.getRemoteProperties();
}
void onReadError(boolean recognised) {
disposeOnError(reader, recognised);
disposeOnError(writer);
interruptOutgoingSession();
}
void onWriteError() {
disposeOnError(reader, true);
disposeOnError(writer);
}
SyncSession createDuplexOutgoingSession(StreamContext ctx,
TransportConnectionWriter w, @Nullable Priority priority)
throws IOException {
StreamWriter streamWriter = streamWriterFactory.createStreamWriter(
w.getOutputStream(), ctx);
ContactId c = requireNonNull(ctx.getContactId());
return syncSessionFactory.createDuplexOutgoingSession(c,
ctx.getTransportId(), w.getMaxLatency(), w.getMaxIdleTime(),
streamWriter, priority);
}
}

View File

@@ -0,0 +1,72 @@
package org.briarproject.bramble.connection;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.contact.ContactExchangeManager;
import org.briarproject.bramble.api.contact.HandshakeManager;
import org.briarproject.bramble.api.contact.PendingContactId;
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.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import javax.annotation.Nullable;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
abstract class HandshakeConnection extends Connection {
final HandshakeManager handshakeManager;
final ContactExchangeManager contactExchangeManager;
final ConnectionManager connectionManager;
final PendingContactId pendingContactId;
final TransportId transportId;
final DuplexTransportConnection connection;
final TransportConnectionReader reader;
final TransportConnectionWriter writer;
HandshakeConnection(KeyManager keyManager,
ConnectionRegistry connectionRegistry,
StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory,
HandshakeManager handshakeManager,
ContactExchangeManager contactExchangeManager,
ConnectionManager connectionManager,
PendingContactId pendingContactId,
TransportId transportId, DuplexTransportConnection connection) {
super(keyManager, connectionRegistry, streamReaderFactory,
streamWriterFactory);
this.handshakeManager = handshakeManager;
this.contactExchangeManager = contactExchangeManager;
this.connectionManager = connectionManager;
this.pendingContactId = pendingContactId;
this.transportId = transportId;
this.connection = connection;
reader = connection.getReader();
writer = connection.getWriter();
}
@Nullable
StreamContext allocateStreamContext(PendingContactId pendingContactId,
TransportId transportId) {
try {
return keyManager.getStreamContext(pendingContactId, transportId);
} catch (DbException e) {
logException(LOG, WARNING, e);
return null;
}
}
void onError(boolean recognised) {
disposeOnError(reader, recognised);
disposeOnError(writer);
}
}

View File

@@ -0,0 +1,107 @@
package org.briarproject.bramble.connection;
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.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.sync.PriorityHandler;
import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.SyncSessionFactory;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import java.io.IOException;
import java.util.concurrent.Executor;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class IncomingDuplexSyncConnection extends DuplexSyncConnection
implements Runnable {
IncomingDuplexSyncConnection(KeyManager keyManager,
ConnectionRegistry connectionRegistry,
StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory,
SyncSessionFactory syncSessionFactory,
TransportPropertyManager transportPropertyManager,
Executor ioExecutor, TransportId transportId,
DuplexTransportConnection connection) {
super(keyManager, connectionRegistry, streamReaderFactory,
streamWriterFactory, syncSessionFactory,
transportPropertyManager, ioExecutor, transportId, connection);
}
@Override
public void run() {
// Read and recognise the tag
StreamContext ctx = recogniseTag(reader, transportId);
if (ctx == null) {
LOG.info("Unrecognised tag");
onReadError(false);
return;
}
ContactId contactId = ctx.getContactId();
if (contactId == null) {
LOG.warning("Expected contact tag, got rendezvous tag");
onReadError(true);
return;
}
if (ctx.isHandshakeMode()) {
// TODO: Support handshake mode for contacts
LOG.warning("Received handshake tag, expected rotation mode");
onReadError(true);
return;
}
connectionRegistry.registerIncomingConnection(contactId, transportId,
this);
// Start the outgoing session on another thread
ioExecutor.execute(() -> runOutgoingSession(contactId));
try {
// Store any transport properties discovered from the connection
transportPropertyManager.addRemotePropertiesFromConnection(
contactId, transportId, remote);
// Update the connection registry when we receive our priority
PriorityHandler handler = p -> connectionRegistry.setPriority(
contactId, transportId, this, p);
// Create and run the incoming session
createIncomingSession(ctx, reader, handler).run();
reader.dispose(false, true);
interruptOutgoingSession();
connectionRegistry.unregisterConnection(contactId, transportId,
this, true, false);
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
onReadError(true);
connectionRegistry.unregisterConnection(contactId, transportId,
this, true, true);
}
}
private void runOutgoingSession(ContactId contactId) {
// Allocate a stream context
StreamContext ctx = allocateStreamContext(contactId, transportId);
if (ctx == null) {
LOG.warning("Could not allocate stream context");
onWriteError();
return;
}
try {
// Create and run the outgoing session
SyncSession out = createDuplexOutgoingSession(ctx, writer, null);
setOutgoingSession(out);
out.run();
writer.dispose(false);
} catch (IOException e) {
logException(LOG, WARNING, e);
onWriteError();
}
}
}

View File

@@ -0,0 +1,93 @@
package org.briarproject.bramble.connection;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.contact.ContactExchangeManager;
import org.briarproject.bramble.api.contact.HandshakeManager;
import org.briarproject.bramble.api.contact.HandshakeManager.HandshakeResult;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import org.briarproject.bramble.api.transport.StreamWriter;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import java.io.IOException;
import java.io.InputStream;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class IncomingHandshakeConnection extends HandshakeConnection
implements Runnable {
IncomingHandshakeConnection(KeyManager keyManager,
ConnectionRegistry connectionRegistry,
StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory,
HandshakeManager handshakeManager,
ContactExchangeManager contactExchangeManager,
ConnectionManager connectionManager,
PendingContactId pendingContactId,
TransportId transportId, DuplexTransportConnection connection) {
super(keyManager, connectionRegistry, streamReaderFactory,
streamWriterFactory, handshakeManager, contactExchangeManager,
connectionManager, pendingContactId, transportId, connection);
}
@Override
public void run() {
// Read and recognise the tag
StreamContext ctxIn = recogniseTag(reader, transportId);
if (ctxIn == null) {
LOG.info("Unrecognised tag");
onError(false);
return;
}
PendingContactId inPendingContactId = ctxIn.getPendingContactId();
if (inPendingContactId == null) {
LOG.warning("Expected rendezvous tag, got contact tag");
onError(true);
return;
}
// Allocate the outgoing stream context
StreamContext ctxOut =
allocateStreamContext(pendingContactId, transportId);
if (ctxOut == null) {
LOG.warning("Could not allocate stream context");
onError(true);
return;
}
// Close the connection if it's redundant
if (!connectionRegistry.registerConnection(pendingContactId)) {
LOG.info("Redundant rendezvous connection");
onError(true);
return;
}
// Handshake and exchange contacts
try {
InputStream in = streamReaderFactory.createStreamReader(
reader.getInputStream(), ctxIn);
// Flush the output stream to send the outgoing stream header
StreamWriter out = streamWriterFactory.createStreamWriter(
writer.getOutputStream(), ctxOut);
out.getOutputStream().flush();
HandshakeResult result =
handshakeManager.handshake(pendingContactId, in, out);
contactExchangeManager.exchangeContacts(pendingContactId,
connection, result.getMasterKey(), result.isAlice(), false);
connectionRegistry.unregisterConnection(pendingContactId, true);
// Reuse the connection as a transport connection
connectionManager.manageIncomingConnection(transportId, connection);
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onError(true);
connectionRegistry.unregisterConnection(pendingContactId, false);
}
}
}

View File

@@ -0,0 +1,79 @@
package org.briarproject.bramble.connection;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.sync.PriorityHandler;
import org.briarproject.bramble.api.sync.SyncSessionFactory;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import java.io.IOException;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class IncomingSimplexSyncConnection extends SyncConnection implements Runnable {
private final TransportId transportId;
private final TransportConnectionReader reader;
IncomingSimplexSyncConnection(KeyManager keyManager,
ConnectionRegistry connectionRegistry,
StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory,
SyncSessionFactory syncSessionFactory,
TransportPropertyManager transportPropertyManager,
TransportId transportId, TransportConnectionReader reader) {
super(keyManager, connectionRegistry, streamReaderFactory,
streamWriterFactory, syncSessionFactory,
transportPropertyManager);
this.transportId = transportId;
this.reader = reader;
}
@Override
public void run() {
// Read and recognise the tag
StreamContext ctx = recogniseTag(reader, transportId);
if (ctx == null) {
LOG.info("Unrecognised tag");
onError(false);
return;
}
ContactId contactId = ctx.getContactId();
if (contactId == null) {
LOG.warning("Received rendezvous stream, expected contact");
onError(true);
return;
}
if (ctx.isHandshakeMode()) {
// TODO: Support handshake mode for contacts
LOG.warning("Received handshake tag, expected rotation mode");
onError(true);
return;
}
try {
// We don't expect to receive a priority for this connection
PriorityHandler handler = p ->
LOG.info("Ignoring priority for simplex connection");
// Create and run the incoming session
createIncomingSession(ctx, reader, handler).run();
reader.dispose(false, true);
} catch (IOException e) {
logException(LOG, WARNING, e);
onError(true);
}
}
private void onError(boolean recognised) {
disposeOnError(reader, recognised);
}
}

View File

@@ -0,0 +1,140 @@
package org.briarproject.bramble.connection;
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.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.sync.Priority;
import org.briarproject.bramble.api.sync.PriorityHandler;
import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.SyncSessionFactory;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.concurrent.Executor;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.sync.SyncConstants.PRIORITY_NONCE_BYTES;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class OutgoingDuplexSyncConnection extends DuplexSyncConnection
implements Runnable {
private final SecureRandom secureRandom;
private final ContactId contactId;
OutgoingDuplexSyncConnection(KeyManager keyManager,
ConnectionRegistry connectionRegistry,
StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory,
SyncSessionFactory syncSessionFactory,
TransportPropertyManager transportPropertyManager,
Executor ioExecutor, SecureRandom secureRandom, ContactId contactId,
TransportId transportId, DuplexTransportConnection connection) {
super(keyManager, connectionRegistry, streamReaderFactory,
streamWriterFactory, syncSessionFactory,
transportPropertyManager, ioExecutor, transportId, connection);
this.secureRandom = secureRandom;
this.contactId = contactId;
}
@Override
public void run() {
// Allocate a stream context
StreamContext ctx = allocateStreamContext(contactId, transportId);
if (ctx == null) {
LOG.warning("Could not allocate stream context");
onWriteError();
return;
}
if (ctx.isHandshakeMode()) {
// TODO: Support handshake mode for contacts
LOG.warning("Cannot use handshake mode stream context");
onWriteError();
return;
}
// Start the incoming session on another thread
Priority priority = generatePriority();
ioExecutor.execute(() -> runIncomingSession(priority));
try {
// Create and run the outgoing session
SyncSession out =
createDuplexOutgoingSession(ctx, writer, priority);
setOutgoingSession(out);
out.run();
writer.dispose(false);
} catch (IOException e) {
logException(LOG, WARNING, e);
onWriteError();
}
}
private void runIncomingSession(Priority priority) {
// Read and recognise the tag
StreamContext ctx = recogniseTag(reader, transportId);
// Unrecognised tags are suspicious in this case
if (ctx == null) {
LOG.warning("Unrecognised tag for returning stream");
onReadError();
return;
}
// Check that the stream comes from the expected contact
ContactId inContactId = ctx.getContactId();
if (inContactId == null) {
LOG.warning("Expected contact tag, got rendezvous tag");
onReadError();
return;
}
if (!contactId.equals(inContactId)) {
LOG.warning("Wrong contact ID for returning stream");
onReadError();
return;
}
if (ctx.isHandshakeMode()) {
// TODO: Support handshake mode for contacts
LOG.warning("Received handshake tag, expected rotation mode");
onReadError();
return;
}
connectionRegistry.registerOutgoingConnection(contactId, transportId,
this, priority);
try {
// Store any transport properties discovered from the connection
transportPropertyManager.addRemotePropertiesFromConnection(
contactId, transportId, remote);
// We don't expect to receive a priority for this connection
PriorityHandler handler = p ->
LOG.info("Ignoring priority for outgoing connection");
// Create and run the incoming session
createIncomingSession(ctx, reader, handler).run();
reader.dispose(false, true);
interruptOutgoingSession();
connectionRegistry.unregisterConnection(contactId, transportId,
this, false, false);
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
onReadError();
connectionRegistry.unregisterConnection(contactId, transportId,
this, false, true);
}
}
private void onReadError() {
// 'Recognised' is always true for outgoing connections
onReadError(true);
}
private Priority generatePriority() {
byte[] nonce = new byte[PRIORITY_NONCE_BYTES];
secureRandom.nextBytes(nonce);
return new Priority(nonce);
}
}

View File

@@ -0,0 +1,115 @@
package org.briarproject.bramble.connection;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactExchangeManager;
import org.briarproject.bramble.api.contact.HandshakeManager;
import org.briarproject.bramble.api.contact.HandshakeManager.HandshakeResult;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import org.briarproject.bramble.api.transport.StreamWriter;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import java.io.IOException;
import java.io.InputStream;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class OutgoingHandshakeConnection extends HandshakeConnection
implements Runnable {
OutgoingHandshakeConnection(KeyManager keyManager,
ConnectionRegistry connectionRegistry,
StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory,
HandshakeManager handshakeManager,
ContactExchangeManager contactExchangeManager,
ConnectionManager connectionManager,
PendingContactId pendingContactId,
TransportId transportId, DuplexTransportConnection connection) {
super(keyManager, connectionRegistry, streamReaderFactory,
streamWriterFactory, handshakeManager, contactExchangeManager,
connectionManager, pendingContactId, transportId, connection);
}
@Override
public void run() {
// Allocate the outgoing stream context
StreamContext ctxOut =
allocateStreamContext(pendingContactId, transportId);
if (ctxOut == null) {
LOG.warning("Could not allocate stream context");
onError();
return;
}
// Flush the output stream to send the outgoing stream header
StreamWriter out;
try {
out = streamWriterFactory.createStreamWriter(
writer.getOutputStream(), ctxOut);
out.getOutputStream().flush();
} catch (IOException e) {
logException(LOG, WARNING, e);
onError();
return;
}
// Read and recognise the tag
StreamContext ctxIn = recogniseTag(reader, transportId);
// Unrecognised tags are suspicious in this case
if (ctxIn == null) {
LOG.warning("Unrecognised tag for returning stream");
onError();
return;
}
// Check that the stream comes from the expected pending contact
PendingContactId inPendingContactId = ctxIn.getPendingContactId();
if (inPendingContactId == null) {
LOG.warning("Expected rendezvous tag, got contact tag");
onError();
return;
}
if (!inPendingContactId.equals(pendingContactId)) {
LOG.warning("Wrong pending contact ID for returning stream");
onError();
return;
}
// Close the connection if it's redundant
if (!connectionRegistry.registerConnection(pendingContactId)) {
LOG.info("Redundant rendezvous connection");
onError();
return;
}
// Handshake and exchange contacts
try {
InputStream in = streamReaderFactory.createStreamReader(
reader.getInputStream(), ctxIn);
HandshakeResult result =
handshakeManager.handshake(pendingContactId, in, out);
Contact contact = contactExchangeManager.exchangeContacts(
pendingContactId, connection, result.getMasterKey(),
result.isAlice(), false);
connectionRegistry.unregisterConnection(pendingContactId, true);
// Reuse the connection as a transport connection
connectionManager.manageOutgoingConnection(contact.getId(),
transportId, connection);
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onError();
connectionRegistry.unregisterConnection(pendingContactId, false);
}
}
private void onError() {
// 'Recognised' is always true for outgoing connections
onError(true);
}
}

View File

@@ -0,0 +1,78 @@
package org.briarproject.bramble.connection;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.contact.ContactId;
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.SyncSession;
import org.briarproject.bramble.api.sync.SyncSessionFactory;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import org.briarproject.bramble.api.transport.StreamWriter;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import java.io.IOException;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class OutgoingSimplexSyncConnection extends SyncConnection implements Runnable {
private final ContactId contactId;
private final TransportId transportId;
private final TransportConnectionWriter writer;
OutgoingSimplexSyncConnection(KeyManager keyManager,
ConnectionRegistry connectionRegistry,
StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory,
SyncSessionFactory syncSessionFactory,
TransportPropertyManager transportPropertyManager,
ContactId contactId, TransportId transportId,
TransportConnectionWriter writer) {
super(keyManager, connectionRegistry, streamReaderFactory,
streamWriterFactory, syncSessionFactory,
transportPropertyManager);
this.contactId = contactId;
this.transportId = transportId;
this.writer = writer;
}
@Override
public void run() {
// Allocate a stream context
StreamContext ctx = allocateStreamContext(contactId, transportId);
if (ctx == null) {
LOG.warning("Could not allocate stream context");
onError();
return;
}
try {
// Create and run the outgoing session
createSimplexOutgoingSession(ctx, writer).run();
writer.dispose(false);
} catch (IOException e) {
logException(LOG, WARNING, e);
onError();
}
}
private void onError() {
disposeOnError(writer);
}
private SyncSession createSimplexOutgoingSession(StreamContext ctx,
TransportConnectionWriter w) throws IOException {
StreamWriter streamWriter = streamWriterFactory.createStreamWriter(
w.getOutputStream(), ctx);
ContactId c = requireNonNull(ctx.getContactId());
return syncSessionFactory.createSimplexOutgoingSession(c,
ctx.getTransportId(), w.getMaxLatency(), streamWriter);
}
}

View File

@@ -0,0 +1,64 @@
package org.briarproject.bramble.connection;
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;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.sync.PriorityHandler;
import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.SyncSessionFactory;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import java.io.IOException;
import java.io.InputStream;
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;
@NotNullByDefault
class SyncConnection extends Connection {
final SyncSessionFactory syncSessionFactory;
final TransportPropertyManager transportPropertyManager;
SyncConnection(KeyManager keyManager, ConnectionRegistry connectionRegistry,
StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory,
SyncSessionFactory syncSessionFactory,
TransportPropertyManager transportPropertyManager) {
super(keyManager, connectionRegistry, streamReaderFactory,
streamWriterFactory);
this.syncSessionFactory = syncSessionFactory;
this.transportPropertyManager = transportPropertyManager;
}
@Nullable
StreamContext allocateStreamContext(ContactId contactId,
TransportId transportId) {
try {
return keyManager.getStreamContext(contactId, transportId);
} catch (DbException e) {
logException(LOG, WARNING, e);
return null;
}
}
SyncSession createIncomingSession(StreamContext ctx,
TransportConnectionReader r, PriorityHandler handler)
throws IOException {
InputStream streamReader = streamReaderFactory.createStreamReader(
r.getInputStream(), ctx);
ContactId c = requireNonNull(ctx.getContactId());
return syncSessionFactory
.createIncomingSession(c, streamReader, handler);
}
}

View File

@@ -7,6 +7,7 @@ import net.i2p.crypto.eddsa.KeyPairGenerator;
import org.briarproject.bramble.api.crypto.AgreementPrivateKey;
import org.briarproject.bramble.api.crypto.AgreementPublicKey;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.crypto.KeyParser;
import org.briarproject.bramble.api.crypto.KeyStrengthener;
@@ -39,6 +40,9 @@ import static java.lang.System.arraycopy;
import static java.util.logging.Level.INFO;
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_AGREEMENT;
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_SIGNATURE;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD;
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
import static org.briarproject.bramble.util.ByteUtils.INT_32_BYTES;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.now;
@@ -359,16 +363,17 @@ class CryptoComponentImpl implements CryptoComponent {
}
@Override
@Nullable
public byte[] decryptWithPassword(byte[] input, String password,
@Nullable KeyStrengthener keyStrengthener) {
@Nullable KeyStrengthener keyStrengthener)
throws DecryptionException {
AuthenticatedCipher cipher = new XSalsa20Poly1305AuthenticatedCipher();
int macBytes = cipher.getMacBytes();
// The input contains the format version, salt, cost parameter, IV,
// ciphertext and MAC
if (input.length < 1 + PBKDF_SALT_BYTES + INT_32_BYTES
+ STORAGE_IV_BYTES + macBytes)
return null; // Invalid input
+ STORAGE_IV_BYTES + macBytes) {
throw new DecryptionException(INVALID_CIPHERTEXT);
}
int inputOff = 0;
// Format version
byte formatVersion = input[inputOff];
@@ -376,7 +381,7 @@ class CryptoComponentImpl implements CryptoComponent {
// Check whether we support this format version
if (formatVersion != PBKDF_FORMAT_SCRYPT &&
formatVersion != PBKDF_FORMAT_SCRYPT_STRENGTHENED) {
return null;
throw new DecryptionException(INVALID_CIPHERTEXT);
}
// Salt
byte[] salt = new byte[PBKDF_SALT_BYTES];
@@ -385,8 +390,9 @@ class CryptoComponentImpl implements CryptoComponent {
// Cost parameter
long cost = ByteUtils.readUint32(input, inputOff);
inputOff += INT_32_BYTES;
if (cost < 2 || cost > Integer.MAX_VALUE)
return null; // Invalid cost parameter
if (cost < 2 || cost > Integer.MAX_VALUE) {
throw new DecryptionException(INVALID_CIPHERTEXT);
}
// IV
byte[] iv = new byte[STORAGE_IV_BYTES];
arraycopy(input, inputOff, iv, 0, iv.length);
@@ -394,8 +400,10 @@ class CryptoComponentImpl implements CryptoComponent {
// Derive the decryption key from the password
SecretKey key = passwordBasedKdf.deriveKey(password, salt, (int) cost);
if (formatVersion == PBKDF_FORMAT_SCRYPT_STRENGTHENED) {
if (keyStrengthener == null || !keyStrengthener.isInitialised())
return null; // Can't derive the same strengthened key
if (keyStrengthener == null || !keyStrengthener.isInitialised()) {
// Can't derive the same strengthened key
throw new DecryptionException(KEY_STRENGTHENER_ERROR);
}
key = keyStrengthener.strengthenKey(key);
}
// Initialise the cipher
@@ -411,7 +419,7 @@ class CryptoComponentImpl implements CryptoComponent {
cipher.process(input, inputOff, inputLen, output, 0);
return output;
} catch (GeneralSecurityException e) {
return null; // Invalid ciphertext
throw new DecryptionException(INVALID_PASSWORD);
}
}

View File

@@ -25,6 +25,7 @@ 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.db.JdbcUtils.tryToClose;
import static org.briarproject.bramble.util.IoUtils.isNonEmptyDirectory;
import static org.briarproject.bramble.util.LogUtils.logFileOrDir;
/**
@@ -69,8 +70,9 @@ class H2Database extends JdbcDatabase {
LOG.info("Contents of account directory before opening DB:");
logFileOrDir(LOG, INFO, dir.getParentFile());
}
boolean reopen = !dir.mkdirs();
boolean reopen = isNonEmptyDirectory(dir);
if (LOG.isLoggable(INFO)) LOG.info("Reopening DB: " + reopen);
if (!reopen && dir.mkdirs()) LOG.info("Created database directory");
super.open("org.h2.Driver", reopen, key, listener);
if (LOG.isLoggable(INFO)) {
LOG.info("Contents of account directory after opening DB:");

View File

@@ -20,9 +20,11 @@ import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
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.db.JdbcUtils.tryToClose;
import static org.briarproject.bramble.util.IoUtils.isNonEmptyDirectory;
/**
* Contains all the HSQLDB-specific code for the database.
@@ -64,7 +66,10 @@ class HyperSqlDatabase extends JdbcDatabase {
public boolean open(SecretKey key, @Nullable MigrationListener listener)
throws DbException {
this.key = key;
boolean reopen = !config.getDatabaseDirectory().mkdirs();
File dir = config.getDatabaseDirectory();
boolean reopen = isNonEmptyDirectory(dir);
if (LOG.isLoggable(INFO)) LOG.info("Reopening DB: " + reopen);
if (!reopen && dir.mkdirs()) LOG.info("Created database directory");
super.open("org.hsqldb.jdbc.JDBCDriver", reopen, key, listener);
return reopen;
}

View File

@@ -0,0 +1,18 @@
package org.briarproject.bramble.io;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class IoModule {
@Provides
@Singleton
TimeoutMonitor provideTimeoutMonitor(TimeoutMonitorImpl timeoutMonitor) {
return timeoutMonitor;
}
}

View File

@@ -0,0 +1,104 @@
package org.briarproject.bramble.io;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.concurrent.GuardedBy;
@NotNullByDefault
class TimeoutInputStream extends InputStream {
private final Clock clock;
private final InputStream in;
private final long timeoutMs;
private final CloseListener listener;
private final Object lock = new Object();
@GuardedBy("lock")
private long readStartedMs = -1;
TimeoutInputStream(Clock clock, InputStream in, long timeoutMs,
CloseListener listener) {
this.clock = clock;
this.in = in;
this.timeoutMs = timeoutMs;
this.listener = listener;
}
@Override
public int read() throws IOException {
synchronized (lock) {
readStartedMs = clock.currentTimeMillis();
}
int input = in.read();
synchronized (lock) {
readStartedMs = -1;
}
return input;
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
synchronized (lock) {
readStartedMs = clock.currentTimeMillis();
}
int read = in.read(b, off, len);
synchronized (lock) {
readStartedMs = -1;
}
return read;
}
@Override
public void close() throws IOException {
try {
in.close();
} finally {
listener.onClose(this);
}
}
@Override
public int available() throws IOException {
return in.available();
}
@Override
public void mark(int readlimit) {
in.mark(readlimit);
}
@Override
public boolean markSupported() {
return in.markSupported();
}
@Override
public void reset() throws IOException {
in.reset();
}
@Override
public long skip(long n) throws IOException {
return in.skip(n);
}
boolean hasTimedOut() {
synchronized (lock) {
return readStartedMs != -1 &&
clock.currentTimeMillis() - readStartedMs > timeoutMs;
}
}
interface CloseListener {
void onClose(TimeoutInputStream closed);
}
}

View File

@@ -0,0 +1,96 @@
package org.briarproject.bramble.io;
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.Scheduler;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import javax.inject.Inject;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
class TimeoutMonitorImpl implements TimeoutMonitor {
private static final Logger LOG =
getLogger(TimeoutMonitorImpl.class.getName());
private static final long CHECK_INTERVAL_MS = SECONDS.toMillis(10);
private final ScheduledExecutorService scheduler;
private final Executor ioExecutor;
private final Clock clock;
private final Object lock = new Object();
@GuardedBy("lock")
private final List<TimeoutInputStream> streams = new ArrayList<>();
@GuardedBy("lock")
private Future<?> task = null;
@Inject
TimeoutMonitorImpl(@Scheduler ScheduledExecutorService scheduler,
@IoExecutor Executor ioExecutor, Clock clock) {
this.scheduler = scheduler;
this.ioExecutor = ioExecutor;
this.clock = clock;
}
@Override
public InputStream createTimeoutInputStream(InputStream in,
long timeoutMs) {
TimeoutInputStream stream = new TimeoutInputStream(clock, in,
timeoutMs, this::removeStream);
synchronized (lock) {
if (streams.isEmpty()) {
task = scheduler.scheduleWithFixedDelay(this::checkTimeouts,
CHECK_INTERVAL_MS, CHECK_INTERVAL_MS, MILLISECONDS);
}
streams.add(stream);
}
return stream;
}
private void removeStream(TimeoutInputStream stream) {
Future<?> toCancel = null;
synchronized (lock) {
if (streams.remove(stream) && streams.isEmpty()) {
toCancel = task;
task = null;
}
}
if (toCancel != null) toCancel.cancel(false);
}
@Scheduler
private void checkTimeouts() {
ioExecutor.execute(() -> {
List<TimeoutInputStream> snapshot;
synchronized (lock) {
snapshot = new ArrayList<>(streams);
}
for (TimeoutInputStream stream : snapshot) {
if (stream.hasTimedOut()) {
LOG.info("Input stream has timed out");
try {
stream.close();
} catch (IOException e) {
logException(LOG, INFO, e);
}
}
}
});
}
}

View File

@@ -1,694 +0,0 @@
package org.briarproject.bramble.plugin;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactExchangeManager;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.HandshakeManager;
import org.briarproject.bramble.api.contact.HandshakeManager.HandshakeResult;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionManager;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
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.SyncSession;
import org.briarproject.bramble.api.sync.SyncSessionFactory;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import org.briarproject.bramble.api.transport.StreamWriter;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
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.api.transport.TransportConstants.TAG_LENGTH;
import static org.briarproject.bramble.util.IoUtils.read;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class ConnectionManagerImpl implements ConnectionManager {
private static final Logger LOG =
getLogger(ConnectionManagerImpl.class.getName());
private final Executor ioExecutor;
private final KeyManager keyManager;
private final StreamReaderFactory streamReaderFactory;
private final StreamWriterFactory streamWriterFactory;
private final SyncSessionFactory syncSessionFactory;
private final HandshakeManager handshakeManager;
private final ContactExchangeManager contactExchangeManager;
private final ConnectionRegistry connectionRegistry;
@Inject
ConnectionManagerImpl(@IoExecutor Executor ioExecutor,
KeyManager keyManager, StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory,
SyncSessionFactory syncSessionFactory,
HandshakeManager handshakeManager,
ContactExchangeManager contactExchangeManager,
ConnectionRegistry connectionRegistry) {
this.ioExecutor = ioExecutor;
this.keyManager = keyManager;
this.streamReaderFactory = streamReaderFactory;
this.streamWriterFactory = streamWriterFactory;
this.syncSessionFactory = syncSessionFactory;
this.handshakeManager = handshakeManager;
this.contactExchangeManager = contactExchangeManager;
this.connectionRegistry = connectionRegistry;
}
@Override
public void manageIncomingConnection(TransportId t,
TransportConnectionReader r) {
ioExecutor.execute(new ManageIncomingSimplexConnection(t, r));
}
@Override
public void manageIncomingConnection(TransportId t,
DuplexTransportConnection d) {
ioExecutor.execute(new ManageIncomingDuplexConnection(t, d));
}
@Override
public void manageIncomingConnection(PendingContactId p, TransportId t,
DuplexTransportConnection d) {
ioExecutor.execute(new ManageIncomingHandshakeConnection(p, t, d));
}
@Override
public void manageOutgoingConnection(ContactId c, TransportId t,
TransportConnectionWriter w) {
ioExecutor.execute(new ManageOutgoingSimplexConnection(c, t, w));
}
@Override
public void manageOutgoingConnection(ContactId c, TransportId t,
DuplexTransportConnection d) {
ioExecutor.execute(new ManageOutgoingDuplexConnection(c, t, d));
}
@Override
public void manageOutgoingConnection(PendingContactId p, TransportId t,
DuplexTransportConnection d) {
ioExecutor.execute(new ManageOutgoingHandshakeConnection(p, t, d));
}
private byte[] readTag(InputStream in) throws IOException {
byte[] tag = new byte[TAG_LENGTH];
read(in, tag);
return tag;
}
private SyncSession createIncomingSession(StreamContext ctx,
TransportConnectionReader r) throws IOException {
InputStream streamReader = streamReaderFactory.createStreamReader(
r.getInputStream(), ctx);
ContactId c = requireNonNull(ctx.getContactId());
return syncSessionFactory.createIncomingSession(c, streamReader);
}
private SyncSession createSimplexOutgoingSession(StreamContext ctx,
TransportConnectionWriter w) throws IOException {
StreamWriter streamWriter = streamWriterFactory.createStreamWriter(
w.getOutputStream(), ctx);
ContactId c = requireNonNull(ctx.getContactId());
return syncSessionFactory.createSimplexOutgoingSession(c,
w.getMaxLatency(), streamWriter);
}
private SyncSession createDuplexOutgoingSession(StreamContext ctx,
TransportConnectionWriter w) throws IOException {
StreamWriter streamWriter = streamWriterFactory.createStreamWriter(
w.getOutputStream(), ctx);
ContactId c = requireNonNull(ctx.getContactId());
return syncSessionFactory.createDuplexOutgoingSession(c,
w.getMaxLatency(), w.getMaxIdleTime(), streamWriter);
}
private void disposeOnError(TransportConnectionReader reader,
boolean recognised) {
try {
reader.dispose(true, recognised);
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
private void disposeOnError(TransportConnectionWriter writer) {
try {
writer.dispose(true);
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
private class ManageIncomingSimplexConnection implements Runnable {
private final TransportId transportId;
private final TransportConnectionReader reader;
private ManageIncomingSimplexConnection(TransportId transportId,
TransportConnectionReader reader) {
this.transportId = transportId;
this.reader = reader;
}
@Override
public void run() {
// Read and recognise the tag
StreamContext ctx;
try {
byte[] tag = readTag(reader.getInputStream());
ctx = keyManager.getStreamContext(transportId, tag);
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onError(false);
return;
}
if (ctx == null) {
LOG.info("Unrecognised tag");
onError(false);
return;
}
ContactId contactId = ctx.getContactId();
if (contactId == null) {
LOG.warning("Received rendezvous stream, expected contact");
onError(true);
return;
}
if (ctx.isHandshakeMode()) {
// TODO: Support handshake mode for contacts
LOG.warning("Received handshake tag, expected rotation mode");
onError(true);
return;
}
connectionRegistry.registerConnection(contactId, transportId, true);
try {
// Create and run the incoming session
createIncomingSession(ctx, reader).run();
reader.dispose(false, true);
} catch (IOException e) {
logException(LOG, WARNING, e);
onError(true);
} finally {
connectionRegistry.unregisterConnection(contactId, transportId,
true);
}
}
private void onError(boolean recognised) {
disposeOnError(reader, recognised);
}
}
private class ManageOutgoingSimplexConnection implements Runnable {
private final ContactId contactId;
private final TransportId transportId;
private final TransportConnectionWriter writer;
private ManageOutgoingSimplexConnection(ContactId contactId,
TransportId transportId, TransportConnectionWriter writer) {
this.contactId = contactId;
this.transportId = transportId;
this.writer = writer;
}
@Override
public void run() {
// Allocate a stream context
StreamContext ctx;
try {
ctx = keyManager.getStreamContext(contactId, transportId);
} catch (DbException e) {
logException(LOG, WARNING, e);
onError();
return;
}
if (ctx == null) {
LOG.warning("Could not allocate stream context");
onError();
return;
}
connectionRegistry.registerConnection(contactId, transportId,
false);
try {
// Create and run the outgoing session
createSimplexOutgoingSession(ctx, writer).run();
writer.dispose(false);
} catch (IOException e) {
logException(LOG, WARNING, e);
onError();
} finally {
connectionRegistry.unregisterConnection(contactId, transportId,
false);
}
}
private void onError() {
disposeOnError(writer);
}
}
private class ManageIncomingDuplexConnection implements Runnable {
private final TransportId transportId;
private final TransportConnectionReader reader;
private final TransportConnectionWriter writer;
@Nullable
private volatile SyncSession outgoingSession = null;
private ManageIncomingDuplexConnection(TransportId transportId,
DuplexTransportConnection connection) {
this.transportId = transportId;
reader = connection.getReader();
writer = connection.getWriter();
}
@Override
public void run() {
// Read and recognise the tag
StreamContext ctx;
try {
byte[] tag = readTag(reader.getInputStream());
ctx = keyManager.getStreamContext(transportId, tag);
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onReadError(false);
return;
}
if (ctx == null) {
LOG.info("Unrecognised tag");
onReadError(false);
return;
}
ContactId contactId = ctx.getContactId();
if (contactId == null) {
LOG.warning("Expected contact tag, got rendezvous tag");
onReadError(true);
return;
}
if (ctx.isHandshakeMode()) {
// TODO: Support handshake mode for contacts
LOG.warning("Received handshake tag, expected rotation mode");
onReadError(true);
return;
}
connectionRegistry.registerConnection(contactId, transportId, true);
// Start the outgoing session on another thread
ioExecutor.execute(() -> runOutgoingSession(contactId));
try {
// Create and run the incoming session
createIncomingSession(ctx, reader).run();
reader.dispose(false, true);
// Interrupt the outgoing session so it finishes cleanly
SyncSession out = outgoingSession;
if (out != null) out.interrupt();
} catch (IOException e) {
logException(LOG, WARNING, e);
onReadError(true);
} finally {
connectionRegistry.unregisterConnection(contactId, transportId,
true);
}
}
private void runOutgoingSession(ContactId contactId) {
// Allocate a stream context
StreamContext ctx;
try {
ctx = keyManager.getStreamContext(contactId, transportId);
} catch (DbException e) {
logException(LOG, WARNING, e);
onWriteError();
return;
}
if (ctx == null) {
LOG.warning("Could not allocate stream context");
onWriteError();
return;
}
try {
// Create and run the outgoing session
SyncSession out = createDuplexOutgoingSession(ctx, writer);
outgoingSession = out;
out.run();
writer.dispose(false);
} catch (IOException e) {
logException(LOG, WARNING, e);
onWriteError();
}
}
private void onReadError(boolean recognised) {
disposeOnError(reader, recognised);
disposeOnError(writer);
// Interrupt the outgoing session so it finishes
SyncSession out = outgoingSession;
if (out != null) out.interrupt();
}
private void onWriteError() {
disposeOnError(reader, true);
disposeOnError(writer);
}
}
private class ManageOutgoingDuplexConnection implements Runnable {
private final ContactId contactId;
private final TransportId transportId;
private final TransportConnectionReader reader;
private final TransportConnectionWriter writer;
@Nullable
private volatile SyncSession outgoingSession = null;
private ManageOutgoingDuplexConnection(ContactId contactId,
TransportId transportId, DuplexTransportConnection connection) {
this.contactId = contactId;
this.transportId = transportId;
reader = connection.getReader();
writer = connection.getWriter();
}
@Override
public void run() {
// Allocate a stream context
StreamContext ctx;
try {
ctx = keyManager.getStreamContext(contactId, transportId);
} catch (DbException e) {
logException(LOG, WARNING, e);
onWriteError();
return;
}
if (ctx == null) {
LOG.warning("Could not allocate stream context");
onWriteError();
return;
}
if (ctx.isHandshakeMode()) {
// TODO: Support handshake mode for contacts
LOG.warning("Cannot use handshake mode stream context");
onWriteError();
return;
}
// Start the incoming session on another thread
ioExecutor.execute(this::runIncomingSession);
try {
// Create and run the outgoing session
SyncSession out = createDuplexOutgoingSession(ctx, writer);
outgoingSession = out;
out.run();
writer.dispose(false);
} catch (IOException e) {
logException(LOG, WARNING, e);
onWriteError();
}
}
private void runIncomingSession() {
// Read and recognise the tag
StreamContext ctx;
try {
byte[] tag = readTag(reader.getInputStream());
ctx = keyManager.getStreamContext(transportId, tag);
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onReadError();
return;
}
// Unrecognised tags are suspicious in this case
if (ctx == null) {
LOG.warning("Unrecognised tag for returning stream");
onReadError();
return;
}
// Check that the stream comes from the expected contact
ContactId inContactId = ctx.getContactId();
if (inContactId == null) {
LOG.warning("Expected contact tag, got rendezvous tag");
onReadError();
return;
}
if (!contactId.equals(inContactId)) {
LOG.warning("Wrong contact ID for returning stream");
onReadError();
return;
}
if (ctx.isHandshakeMode()) {
// TODO: Support handshake mode for contacts
LOG.warning("Received handshake tag, expected rotation mode");
onReadError();
return;
}
connectionRegistry.registerConnection(contactId, transportId,
false);
try {
// Create and run the incoming session
createIncomingSession(ctx, reader).run();
reader.dispose(false, true);
// Interrupt the outgoing session so it finishes cleanly
SyncSession out = outgoingSession;
if (out != null) out.interrupt();
} catch (IOException e) {
logException(LOG, WARNING, e);
onReadError();
} finally {
connectionRegistry.unregisterConnection(contactId, transportId,
false);
}
}
private void onReadError() {
// 'Recognised' is always true for outgoing connections
disposeOnError(reader, true);
disposeOnError(writer);
// Interrupt the outgoing session so it finishes
SyncSession out = outgoingSession;
if (out != null) out.interrupt();
}
private void onWriteError() {
disposeOnError(reader, true);
disposeOnError(writer);
}
}
private class ManageIncomingHandshakeConnection implements Runnable {
private final PendingContactId pendingContactId;
private final TransportId transportId;
private final DuplexTransportConnection connection;
private final TransportConnectionReader reader;
private final TransportConnectionWriter writer;
private ManageIncomingHandshakeConnection(
PendingContactId pendingContactId, TransportId transportId,
DuplexTransportConnection connection) {
this.pendingContactId = pendingContactId;
this.transportId = transportId;
this.connection = connection;
reader = connection.getReader();
writer = connection.getWriter();
}
@Override
public void run() {
// Read and recognise the tag
StreamContext ctxIn;
try {
byte[] tag = readTag(reader.getInputStream());
ctxIn = keyManager.getStreamContext(transportId, tag);
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onError(false);
return;
}
if (ctxIn == null) {
LOG.info("Unrecognised tag");
onError(false);
return;
}
PendingContactId inPendingContactId = ctxIn.getPendingContactId();
if (inPendingContactId == null) {
LOG.warning("Expected rendezvous tag, got contact tag");
onError(true);
return;
}
// Allocate the outgoing stream context
StreamContext ctxOut;
try {
ctxOut = keyManager.getStreamContext(pendingContactId,
transportId);
} catch (DbException e) {
logException(LOG, WARNING, e);
onError(true);
return;
}
if (ctxOut == null) {
LOG.warning("Could not allocate stream context");
onError(true);
return;
}
// Close the connection if it's redundant
if (!connectionRegistry.registerConnection(pendingContactId)) {
LOG.info("Redundant rendezvous connection");
onError(true);
return;
}
// Handshake and exchange contacts
try {
InputStream in = streamReaderFactory.createStreamReader(
reader.getInputStream(), ctxIn);
// Flush the output stream to send the outgoing stream header
StreamWriter out = streamWriterFactory.createStreamWriter(
writer.getOutputStream(), ctxOut);
out.getOutputStream().flush();
HandshakeResult result = handshakeManager.handshake(
pendingContactId, in, out);
Contact contact = contactExchangeManager.exchangeContacts(
pendingContactId, connection, result.getMasterKey(),
result.isAlice(), false);
connectionRegistry.unregisterConnection(pendingContactId, true);
// Reuse the connection as a transport connection
manageOutgoingConnection(contact.getId(), transportId,
connection);
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onError(true);
connectionRegistry.unregisterConnection(pendingContactId,
false);
}
}
private void onError(boolean recognised) {
disposeOnError(reader, recognised);
disposeOnError(writer);
}
}
private class ManageOutgoingHandshakeConnection implements Runnable {
private final PendingContactId pendingContactId;
private final TransportId transportId;
private final DuplexTransportConnection connection;
private final TransportConnectionReader reader;
private final TransportConnectionWriter writer;
private ManageOutgoingHandshakeConnection(
PendingContactId pendingContactId, TransportId transportId,
DuplexTransportConnection connection) {
this.pendingContactId = pendingContactId;
this.transportId = transportId;
this.connection = connection;
reader = connection.getReader();
writer = connection.getWriter();
}
@Override
public void run() {
// Allocate the outgoing stream context
StreamContext ctxOut;
try {
ctxOut = keyManager.getStreamContext(pendingContactId,
transportId);
} catch (DbException e) {
logException(LOG, WARNING, e);
onError();
return;
}
if (ctxOut == null) {
LOG.warning("Could not allocate stream context");
onError();
return;
}
// Flush the output stream to send the outgoing stream header
StreamWriter out;
try {
out = streamWriterFactory.createStreamWriter(
writer.getOutputStream(), ctxOut);
out.getOutputStream().flush();
} catch (IOException e) {
logException(LOG, WARNING, e);
onError();
return;
}
// Read and recognise the tag
StreamContext ctxIn;
try {
byte[] tag = readTag(reader.getInputStream());
ctxIn = keyManager.getStreamContext(transportId, tag);
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onError();
return;
}
// Unrecognised tags are suspicious in this case
if (ctxIn == null) {
LOG.warning("Unrecognised tag for returning stream");
onError();
return;
}
// Check that the stream comes from the expected pending contact
PendingContactId inPendingContactId = ctxIn.getPendingContactId();
if (inPendingContactId == null) {
LOG.warning("Expected rendezvous tag, got contact tag");
onError();
return;
}
if (!inPendingContactId.equals(pendingContactId)) {
LOG.warning("Wrong pending contact ID for returning stream");
onError();
return;
}
// Close the connection if it's redundant
if (!connectionRegistry.registerConnection(pendingContactId)) {
LOG.info("Redundant rendezvous connection");
onError();
return;
}
// Handshake and exchange contacts
try {
InputStream in = streamReaderFactory.createStreamReader(
reader.getInputStream(), ctxIn);
HandshakeResult result = handshakeManager.handshake(
pendingContactId, in, out);
Contact contact = contactExchangeManager.exchangeContacts(
pendingContactId, connection, result.getMasterKey(),
result.isAlice(), false);
connectionRegistry.unregisterConnection(pendingContactId, true);
// Reuse the connection as a transport connection
manageOutgoingConnection(contact.getId(), transportId,
connection);
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onError();
connectionRegistry.unregisterConnection(pendingContactId,
false);
}
}
private void onError() {
// 'Recognised' is always true for outgoing connections
disposeOnError(reader, true);
disposeOnError(writer);
}
}
}

View File

@@ -1,150 +0,0 @@
package org.briarproject.bramble.plugin;
import org.briarproject.bramble.api.Multiset;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionClosedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionOpenedEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
@ThreadSafe
@NotNullByDefault
class ConnectionRegistryImpl implements ConnectionRegistry {
private static final Logger LOG =
getLogger(ConnectionRegistryImpl.class.getName());
private final EventBus eventBus;
private final Object lock = new Object();
@GuardedBy("lock")
private final Map<TransportId, Multiset<ContactId>> contactConnections;
@GuardedBy("lock")
private final Multiset<ContactId> contactCounts;
@GuardedBy("lock")
private final Set<PendingContactId> connectedPendingContacts;
@Inject
ConnectionRegistryImpl(EventBus eventBus) {
this.eventBus = eventBus;
contactConnections = new HashMap<>();
contactCounts = new Multiset<>();
connectedPendingContacts = new HashSet<>();
}
@Override
public void registerConnection(ContactId c, TransportId t,
boolean incoming) {
if (LOG.isLoggable(INFO)) {
if (incoming) LOG.info("Incoming connection registered: " + t);
else LOG.info("Outgoing connection registered: " + t);
}
boolean firstConnection = false;
synchronized (lock) {
Multiset<ContactId> m = contactConnections.get(t);
if (m == null) {
m = new Multiset<>();
contactConnections.put(t, m);
}
m.add(c);
if (contactCounts.add(c) == 1) firstConnection = true;
}
eventBus.broadcast(new ConnectionOpenedEvent(c, t, incoming));
if (firstConnection) {
LOG.info("Contact connected");
eventBus.broadcast(new ContactConnectedEvent(c));
}
}
@Override
public void unregisterConnection(ContactId c, TransportId t,
boolean incoming) {
if (LOG.isLoggable(INFO)) {
if (incoming) LOG.info("Incoming connection unregistered: " + t);
else LOG.info("Outgoing connection unregistered: " + t);
}
boolean lastConnection = false;
synchronized (lock) {
Multiset<ContactId> m = contactConnections.get(t);
if (m == null || !m.contains(c))
throw new IllegalArgumentException();
m.remove(c);
if (contactCounts.remove(c) == 0) lastConnection = true;
}
eventBus.broadcast(new ConnectionClosedEvent(c, t, incoming));
if (lastConnection) {
LOG.info("Contact disconnected");
eventBus.broadcast(new ContactDisconnectedEvent(c));
}
}
@Override
public Collection<ContactId> getConnectedContacts(TransportId t) {
synchronized (lock) {
Multiset<ContactId> m = contactConnections.get(t);
if (m == null) return Collections.emptyList();
List<ContactId> ids = new ArrayList<>(m.keySet());
if (LOG.isLoggable(INFO))
LOG.info(ids.size() + " contacts connected: " + t);
return ids;
}
}
@Override
public boolean isConnected(ContactId c, TransportId t) {
synchronized (lock) {
Multiset<ContactId> m = contactConnections.get(t);
return m != null && m.contains(c);
}
}
@Override
public boolean isConnected(ContactId c) {
synchronized (lock) {
return contactCounts.contains(c);
}
}
@Override
public boolean registerConnection(PendingContactId p) {
boolean added;
synchronized (lock) {
added = connectedPendingContacts.add(p);
}
if (added) eventBus.broadcast(new RendezvousConnectionOpenedEvent(p));
return added;
}
@Override
public void unregisterConnection(PendingContactId p, boolean success) {
synchronized (lock) {
if (!connectedPendingContacts.remove(p))
throw new IllegalArgumentException();
}
eventBus.broadcast(new RendezvousConnectionClosedEvent(p, success));
}
}

View File

@@ -1,13 +1,14 @@
package org.briarproject.bramble.plugin;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.Service;
import org.briarproject.bramble.api.lifecycle.ServiceException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionManager;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.Plugin.State;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.plugin.PluginConfig;
import org.briarproject.bramble.api.plugin.PluginException;
@@ -18,8 +19,9 @@ 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.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent;
import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent;
import org.briarproject.bramble.api.plugin.event.TransportActiveEvent;
import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent;
import org.briarproject.bramble.api.plugin.event.TransportStateEvent;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.api.properties.TransportProperties;
@@ -36,6 +38,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;
import javax.annotation.concurrent.ThreadSafe;
@@ -45,6 +48,9 @@ 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.plugin.Plugin.PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@@ -177,6 +183,26 @@ class PluginManagerImpl implements PluginManager, Service {
return supported;
}
@Override
public void setPluginEnabled(TransportId t, boolean enabled) {
Plugin plugin = plugins.get(t);
if (plugin == null) return;
Settings s = new Settings();
s.putBoolean(PREF_PLUGIN_ENABLE, enabled);
ioExecutor.execute(() -> mergeSettings(s, t.getString()));
}
private void mergeSettings(Settings s, String namespace) {
try {
long start = now();
settingsManager.mergeSettings(s, namespace);
logDuration(LOG, "Merging settings", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
private class PluginStarter implements Runnable {
private final Plugin plugin;
@@ -250,7 +276,8 @@ class PluginManagerImpl implements PluginManager, Service {
private class Callback implements PluginCallback {
private final TransportId id;
private final AtomicBoolean enabled = new AtomicBoolean(false);
private final AtomicReference<State> state =
new AtomicReference<>(STARTING_STOPPING);
private Callback(TransportId id) {
this.id = id;
@@ -278,11 +305,7 @@ class PluginManagerImpl implements PluginManager, Service {
@Override
public void mergeSettings(Settings s) {
try {
settingsManager.mergeSettings(s, id.getString());
} catch (DbException e) {
logException(LOG, WARNING, e);
}
PluginManagerImpl.this.mergeSettings(s, id.getString());
}
@Override
@@ -295,15 +318,20 @@ class PluginManagerImpl implements PluginManager, Service {
}
@Override
public void transportEnabled() {
if (!enabled.getAndSet(true))
eventBus.broadcast(new TransportEnabledEvent(id));
}
@Override
public void transportDisabled() {
if (enabled.getAndSet(false))
eventBus.broadcast(new TransportDisabledEvent(id));
public void pluginStateChanged(State newState) {
State oldState = state.getAndSet(newState);
if (newState != oldState) {
if (LOG.isLoggable(INFO)) {
LOG.info(id + " changed from state " + oldState
+ " to " + newState);
}
eventBus.broadcast(new TransportStateEvent(id, newState));
if (newState == ACTIVE) {
eventBus.broadcast(new TransportActiveEvent(id));
} else if (oldState == ACTIVE) {
eventBus.broadcast(new TransportInactiveEvent(id));
}
}
}
@Override

View File

@@ -3,8 +3,6 @@ package org.briarproject.bramble.plugin;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.plugin.BackoffFactory;
import org.briarproject.bramble.api.plugin.ConnectionManager;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.PluginConfig;
import org.briarproject.bramble.api.plugin.PluginManager;
@@ -29,20 +27,6 @@ public class PluginModule {
return new BackoffFactoryImpl();
}
@Provides
@Singleton
ConnectionManager provideConnectionManager(
ConnectionManagerImpl connectionManager) {
return connectionManager;
}
@Provides
@Singleton
ConnectionRegistry provideConnectionRegistry(
ConnectionRegistryImpl connectionRegistry) {
return connectionRegistry;
}
@Provides
@Singleton
PluginManager providePluginManager(LifecycleManager lifecycleManager,

View File

@@ -1,6 +1,8 @@
package org.briarproject.bramble.plugin;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
import org.briarproject.bramble.api.db.DbException;
@@ -9,8 +11,6 @@ import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionHandler;
import org.briarproject.bramble.api.plugin.ConnectionManager;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
@@ -20,8 +20,8 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent;
import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent;
import org.briarproject.bramble.api.plugin.event.TransportActiveEvent;
import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
@@ -98,21 +98,21 @@ class PollerImpl implements Poller, EventListener {
ConnectionClosedEvent c = (ConnectionClosedEvent) e;
// Reschedule polling, the polling interval may have decreased
reschedule(c.getTransportId());
if (!c.isIncoming()) {
// Connect to the disconnected contact
// If an outgoing connection failed, try to reconnect
if (!c.isIncoming() && c.isException()) {
connectToContact(c.getContactId(), c.getTransportId());
}
} else if (e instanceof ConnectionOpenedEvent) {
ConnectionOpenedEvent c = (ConnectionOpenedEvent) e;
// Reschedule polling, the polling interval may have decreased
reschedule(c.getTransportId());
} else if (e instanceof TransportEnabledEvent) {
TransportEnabledEvent t = (TransportEnabledEvent) e;
// Poll the newly enabled transport
} else if (e instanceof TransportActiveEvent) {
TransportActiveEvent t = (TransportActiveEvent) e;
// Poll the newly activated transport
pollNow(t.getTransportId());
} else if (e instanceof TransportDisabledEvent) {
TransportDisabledEvent t = (TransportDisabledEvent) e;
// Cancel polling for the disabled transport
} else if (e instanceof TransportInactiveEvent) {
TransportInactiveEvent t = (TransportInactiveEvent) e;
// Cancel polling for the deactivated transport
cancel(t.getTransportId());
}
}
@@ -215,7 +215,7 @@ class PollerImpl implements Poller, EventListener {
Map<ContactId, TransportProperties> remote =
transportPropertyManager.getRemoteProperties(t);
Collection<ContactId> connected =
connectionRegistry.getConnectedContacts(t);
connectionRegistry.getConnectedOrBetterContacts(t);
Collection<Pair<TransportProperties, ConnectionHandler>>
properties = new ArrayList<>();
for (Entry<ContactId, TransportProperties> e : remote.entrySet()) {

View File

@@ -23,17 +23,9 @@ interface BluetoothConnectionLimiter {
boolean canOpenContactConnection();
/**
* Informs the limiter that a contact connection has been opened. The
* limiter may close the new connection if key agreement is in progress.
* <p/>
* Returns false if the limiter has closed the new connection.
* Informs the limiter that the given connection has been opened.
*/
boolean contactConnectionOpened(DuplexTransportConnection conn);
/**
* Informs the limiter that a key agreement connection has been opened.
*/
void keyAgreementConnectionOpened(DuplexTransportConnection conn);
void connectionOpened(DuplexTransportConnection conn);
/**
* Informs the limiter that the given connection has been closed.

View File

@@ -1,46 +1,48 @@
package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.sync.event.CloseSyncConnectionsEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;
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 org.briarproject.bramble.util.LogUtils.logException;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
@NotNullByDefault
@ThreadSafe
class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter {
private static final Logger LOG =
Logger.getLogger(BluetoothConnectionLimiterImpl.class.getName());
getLogger(BluetoothConnectionLimiterImpl.class.getName());
private final EventBus eventBus;
private final Object lock = new Object();
// The following are locking: lock
private final LinkedList<DuplexTransportConnection> connections =
@GuardedBy("lock")
private final List<DuplexTransportConnection> connections =
new LinkedList<>();
@GuardedBy("lock")
private boolean keyAgreementInProgress = false;
BluetoothConnectionLimiterImpl(EventBus eventBus) {
this.eventBus = eventBus;
}
@Override
public void keyAgreementStarted() {
List<DuplexTransportConnection> close;
synchronized (lock) {
keyAgreementInProgress = true;
close = new ArrayList<>(connections);
connections.clear();
}
if (LOG.isLoggable(INFO)) {
LOG.info("Key agreement started, closing " + close.size() +
" connections");
}
for (DuplexTransportConnection conn : close) tryToClose(conn);
LOG.info("Key agreement started");
eventBus.broadcast(new CloseSyncConnectionsEvent(ID));
}
@Override
@@ -65,35 +67,12 @@ class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter {
}
@Override
public boolean contactConnectionOpened(DuplexTransportConnection conn) {
boolean accept = true;
public void connectionOpened(DuplexTransportConnection conn) {
synchronized (lock) {
if (keyAgreementInProgress) {
LOG.info("Refusing contact connection during key agreement");
accept = false;
} else {
LOG.info("Accepting contact connection");
connections.add(conn);
}
}
if (!accept) tryToClose(conn);
return accept;
}
@Override
public void keyAgreementConnectionOpened(DuplexTransportConnection conn) {
synchronized (lock) {
LOG.info("Accepting key agreement connection");
connections.add(conn);
}
}
private void tryToClose(DuplexTransportConnection conn) {
try {
conn.getWriter().dispose(false);
conn.getReader().dispose(false, false);
} catch (IOException e) {
logException(LOG, WARNING, e);
if (LOG.isLoggable(INFO)) {
LOG.info("Connection opened, " + connections.size() + " open");
}
}
}
@@ -101,8 +80,9 @@ class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter {
public void connectionClosed(DuplexTransportConnection conn) {
synchronized (lock) {
connections.remove(conn);
if (LOG.isLoggable(INFO))
if (LOG.isLoggable(INFO)) {
LOG.info("Connection closed, " + connections.size() + " open");
}
}
}

View File

@@ -5,11 +5,14 @@ import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStoppedListeningEvent;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.ConnectionHandler;
@@ -36,16 +39,22 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_BT_ENABLE;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@@ -60,17 +69,18 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
getLogger(BluetoothPlugin.class.getName());
final BluetoothConnectionLimiter connectionLimiter;
final TimeoutMonitor timeoutMonitor;
private final Executor ioExecutor;
private final SecureRandom secureRandom;
private final Backoff backoff;
private final PluginCallback callback;
private final int maxLatency;
private final int maxLatency, maxIdleTime;
private final AtomicBoolean used = new AtomicBoolean(false);
private volatile boolean running = false, contactConnections = false;
protected final PluginState state = new PluginState();
private volatile String contactConnectionsUuid = null;
private volatile SS socket = null;
abstract void initialiseAdapter() throws IOException;
@@ -105,28 +115,35 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
abstract DuplexTransportConnection discoverAndConnect(String uuid);
BluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
Executor ioExecutor, SecureRandom secureRandom,
Backoff backoff, PluginCallback callback, int maxLatency) {
TimeoutMonitor timeoutMonitor, Executor ioExecutor,
SecureRandom secureRandom, Backoff backoff,
PluginCallback callback, int maxLatency, int maxIdleTime) {
this.connectionLimiter = connectionLimiter;
this.timeoutMonitor = timeoutMonitor;
this.ioExecutor = ioExecutor;
this.secureRandom = secureRandom;
this.backoff = backoff;
this.callback = callback;
this.maxLatency = maxLatency;
this.maxIdleTime = maxIdleTime;
}
void onAdapterEnabled() {
LOG.info("Bluetooth enabled");
// We may not have been able to get the local address before
ioExecutor.execute(this::updateProperties);
if (shouldAllowContactConnections()) bind();
if (getState() == INACTIVE) bind();
}
void onAdapterDisabled() {
LOG.info("Bluetooth disabled");
tryToClose(socket);
connectionLimiter.allConnectionsClosed();
callback.transportDisabled();
// The server socket may not have been closed automatically
SS ss = state.clearServerSocket();
if (ss != null) {
LOG.info("Closing server socket");
tryToClose(ss);
}
}
@Override
@@ -141,38 +158,31 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
@Override
public int getMaxIdleTime() {
// Bluetooth detects dead connections so we don't need keepalives
return Integer.MAX_VALUE;
return maxIdleTime;
}
@Override
public void start() throws PluginException {
if (used.getAndSet(true)) throw new IllegalStateException();
Settings settings = callback.getSettings();
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
DEFAULT_PREF_PLUGIN_ENABLE);
state.setStarted(enabledByUser);
try {
initialiseAdapter();
} catch (IOException e) {
throw new PluginException(e);
}
updateProperties();
running = true;
loadSettings(callback.getSettings());
if (shouldAllowContactConnections()) {
if (enabledByUser) {
if (isAdapterEnabled()) bind();
else enableAdapter();
}
}
private void loadSettings(Settings settings) {
contactConnections = settings.getBoolean(PREF_BT_ENABLE, false);
}
private boolean shouldAllowContactConnections() {
return contactConnections;
}
private void bind() {
ioExecutor.execute(() -> {
if (!isRunning() || !shouldAllowContactConnections()) return;
if (getState() != INACTIVE) return;
// Bind a server socket to accept connections from contacts
SS ss;
try {
@@ -181,14 +191,13 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
logException(LOG, WARNING, e);
return;
}
if (!isRunning() || !shouldAllowContactConnections()) {
if (!state.setServerSocket(ss)) {
LOG.info("Closing redundant server socket");
tryToClose(ss);
return;
}
socket = ss;
backoff.reset();
callback.transportEnabled();
acceptContactConnections();
acceptContactConnections(ss);
});
}
@@ -217,34 +226,39 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
if (changed) callback.mergeLocalProperties(p);
}
private void acceptContactConnections() {
private void acceptContactConnections(SS ss) {
while (true) {
DuplexTransportConnection conn;
try {
conn = acceptConnection(socket);
conn = acceptConnection(ss);
} catch (IOException e) {
// This is expected when the socket is closed
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
// This is expected when the server socket is closed
LOG.info("Server socket closed");
state.clearServerSocket();
return;
}
LOG.info("Connection received");
connectionLimiter.connectionOpened(conn);
backoff.reset();
if (connectionLimiter.contactConnectionOpened(conn))
callback.handleConnection(conn);
if (!running) return;
callback.handleConnection(conn);
}
}
@Override
public void stop() {
running = false;
tryToClose(socket);
callback.transportDisabled();
SS ss = state.setStopped();
tryToClose(ss);
disableAdapterIfEnabledByUs();
}
@Override
public boolean isRunning() {
return running && isAdapterEnabled();
public State getState() {
return state.getState();
}
@Override
public int getReasonsDisabled() {
return state.getReasonsDisabled();
}
@Override
@@ -260,7 +274,7 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
@Override
public void poll(Collection<Pair<TransportProperties, ConnectionHandler>>
properties) {
if (!isRunning() || !shouldAllowContactConnections()) return;
if (getState() != ACTIVE) return;
backoff.increment();
for (Pair<TransportProperties, ConnectionHandler> p : properties) {
connect(p.getFirst(), p.getSecond());
@@ -273,13 +287,10 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
String uuid = p.get(PROP_UUID);
if (isNullOrEmpty(uuid)) return;
ioExecutor.execute(() -> {
if (!isRunning() || !shouldAllowContactConnections()) return;
if (!connectionLimiter.canOpenContactConnection()) return;
DuplexTransportConnection d = createConnection(p);
if (d != null) {
backoff.reset();
if (connectionLimiter.contactConnectionOpened(d))
h.handleConnection(d);
h.handleConnection(d);
}
});
}
@@ -317,16 +328,15 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
@Override
public DuplexTransportConnection createConnection(TransportProperties p) {
if (!isRunning() || !shouldAllowContactConnections()) return null;
if (getState() != ACTIVE) return null;
if (!connectionLimiter.canOpenContactConnection()) return null;
String address = p.get(PROP_ADDRESS);
if (isNullOrEmpty(address)) return null;
String uuid = p.get(PROP_UUID);
if (isNullOrEmpty(uuid)) return null;
DuplexTransportConnection conn = connect(address, uuid);
if (conn == null) return null;
// TODO: Why don't we reset the backoff here?
return connectionLimiter.contactConnectionOpened(conn) ? conn : null;
if (conn != null) connectionLimiter.connectionOpened(conn);
return conn;
}
@Override
@@ -336,7 +346,7 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
@Override
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
if (!isRunning()) return null;
if (getState() != ACTIVE) return null;
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
@@ -348,7 +358,7 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
logException(LOG, WARNING, e);
return null;
}
if (!isRunning()) {
if (getState() != ACTIVE) {
tryToClose(ss);
return null;
}
@@ -362,7 +372,7 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
@Override
public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor) {
if (!isRunning()) return null;
if (getState() != ACTIVE) return null;
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
DuplexTransportConnection conn;
@@ -382,7 +392,7 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
LOG.info("Connecting to key agreement UUID " + uuid);
conn = connect(address, uuid);
}
if (conn != null) connectionLimiter.keyAgreementConnectionOpened(conn);
if (conn != null) connectionLimiter.connectionOpened(conn);
return conn;
}
@@ -422,17 +432,18 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
}
}
@IoExecutor
private void onSettingsUpdated(Settings settings) {
boolean wasAllowed = shouldAllowContactConnections();
loadSettings(settings);
boolean isAllowed = shouldAllowContactConnections();
if (wasAllowed && !isAllowed) {
LOG.info("Contact connections disabled");
tryToClose(socket);
callback.transportDisabled();
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
DEFAULT_PREF_PLUGIN_ENABLE);
SS ss = state.setEnabledByUser(enabledByUser);
State s = getState();
if (ss != null) {
LOG.info("Disabled by user, closing server socket");
tryToClose(ss);
disableAdapterIfEnabledByUs();
} else if (!wasAllowed && isAllowed) {
LOG.info("Contact connections enabled");
} else if (s == INACTIVE) {
LOG.info("Enabled by user, opening server socket");
if (isAdapterEnabled()) bind();
else enableAdapter();
}
@@ -451,7 +462,7 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
public KeyAgreementConnection accept() throws IOException {
DuplexTransportConnection conn = acceptConnection(ss);
if (LOG.isLoggable(INFO)) LOG.info(ID + ": Incoming connection");
connectionLimiter.keyAgreementConnectionOpened(conn);
connectionLimiter.connectionOpened(conn);
return new KeyAgreementConnection(conn, ID);
}
@@ -460,4 +471,70 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
tryToClose(ss);
}
}
@ThreadSafe
@NotNullByDefault
protected class PluginState {
@GuardedBy("this")
private boolean started = false,
stopped = false,
enabledByUser = false;
@GuardedBy("this")
@Nullable
private SS serverSocket = null;
synchronized void setStarted(boolean enabledByUser) {
started = true;
this.enabledByUser = enabledByUser;
callback.pluginStateChanged(getState());
}
@Nullable
synchronized SS setStopped() {
stopped = true;
SS ss = serverSocket;
serverSocket = null;
callback.pluginStateChanged(getState());
return ss;
}
@Nullable
synchronized SS setEnabledByUser(boolean enabledByUser) {
this.enabledByUser = enabledByUser;
SS ss = null;
if (!enabledByUser) {
ss = serverSocket;
serverSocket = null;
}
callback.pluginStateChanged(getState());
return ss;
}
synchronized boolean setServerSocket(SS ss) {
if (stopped || serverSocket != null) return false;
serverSocket = ss;
callback.pluginStateChanged(getState());
return true;
}
@Nullable
synchronized SS clearServerSocket() {
SS ss = serverSocket;
serverSocket = null;
callback.pluginStateChanged(getState());
return ss;
}
synchronized State getState() {
if (!started || stopped) return STARTING_STOPPING;
if (!enabledByUser) return DISABLED;
return serverSocket == null ? INACTIVE : ACTIVE;
}
synchronized int getReasonsDisabled() {
return getState() == DISABLED ? REASON_USER : 0;
}
}
}

View File

@@ -16,6 +16,7 @@ import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.FileConstants.PROP_PATH;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@@ -45,7 +46,7 @@ abstract class FilePlugin implements SimplexPlugin {
@Override
public TransportConnectionReader createReader(TransportProperties p) {
if (!isRunning()) return null;
if (getState() != ACTIVE) return null;
String path = p.get(PROP_PATH);
if (isNullOrEmpty(path)) return null;
try {
@@ -60,7 +61,7 @@ abstract class FilePlugin implements SimplexPlugin {
@Override
public TransportConnectionWriter createWriter(TransportProperties p) {
if (!isRunning()) return null;
if (getState() != ACTIVE) return null;
String path = p.get(PROP_PATH);
if (isNullOrEmpty(path)) return null;
try {

View File

@@ -11,22 +11,26 @@ import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.util.IoUtils;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.lang.Integer.parseInt;
import static java.util.Collections.addAll;
import static java.util.Collections.emptyList;
import static java.util.Collections.sort;
@@ -34,28 +38,60 @@ import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_LAN;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.DEFAULT_PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.PREF_IPV6;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.PREF_LAN_IP_PORTS;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.PROP_IPV6;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.PROP_IP_PORTS;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.PROP_PORT;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
import static org.briarproject.bramble.util.ByteUtils.MAX_16_BIT_UNSIGNED;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.PrivacyUtils.scrubSocketAddress;
import static org.briarproject.bramble.util.StringUtils.fromHexString;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.bramble.util.StringUtils.join;
import static org.briarproject.bramble.util.StringUtils.toHexString;
import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong;
@NotNullByDefault
class LanTcpPlugin extends TcpPlugin {
private static final Logger LOG = getLogger(LanTcpPlugin.class.getName());
private static final LanAddressComparator ADDRESS_COMPARATOR =
new LanAddressComparator();
private static final int MAX_ADDRESSES = 4;
private static final String SEPARATOR = ",";
/**
* The IP address of an Android device providing a wifi access point.
* <p>
* Most devices use this address, but at least one device (Honor 8A) may
* use other addresses in the range 192.168.43.0/24.
*/
private static final InetAddress WIFI_AP_ADDRESS;
/**
* The IP address of an Android device providing a wifi direct
* legacy mode access point.
*/
private static final InetAddress WIFI_DIRECT_AP_ADDRESS;
static {
try {
WIFI_AP_ADDRESS = InetAddress.getByAddress(
new byte[] {(byte) 192, (byte) 168, 43, 1});
WIFI_DIRECT_AP_ADDRESS = InetAddress.getByAddress(
new byte[] {(byte) 192, (byte) 168, 49, 1});
} catch (UnknownHostException e) {
// Should only be thrown if the address has an illegal length
throw new AssertionError(e);
}
}
LanTcpPlugin(Executor ioExecutor, Backoff backoff, PluginCallback callback,
int maxLatency, int maxIdleTime) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime);
int maxLatency, int maxIdleTime, int connectionTimeout) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime,
connectionTimeout);
}
@Override
@@ -64,133 +100,234 @@ class LanTcpPlugin extends TcpPlugin {
}
@Override
protected List<InetSocketAddress> getLocalSocketAddresses() {
// Use the same address and port as last time if available
public void start() {
if (used.getAndSet(true)) throw new IllegalStateException();
initialisePortProperty();
Settings settings = callback.getSettings();
state.setStarted(settings.getBoolean(PREF_PLUGIN_ENABLE,
DEFAULT_PREF_PLUGIN_ENABLE));
bind();
}
protected void initialisePortProperty() {
TransportProperties p = callback.getLocalProperties();
String oldIpPorts = p.get(PROP_IP_PORTS);
List<InetSocketAddress> olds = parseSocketAddresses(oldIpPorts);
List<InetSocketAddress> locals = new ArrayList<>();
for (InetAddress local : getLocalIpAddresses()) {
if (isAcceptableAddress(local)) {
// If this is the old address, try to use the same port
for (InetSocketAddress old : olds) {
if (old.getAddress().equals(local))
locals.add(new InetSocketAddress(local, old.getPort()));
}
locals.add(new InetSocketAddress(local, 0));
}
if (isNullOrEmpty(p.get(PROP_PORT))) {
int port = chooseEphemeralPort();
p.put(PROP_PORT, String.valueOf(port));
callback.mergeLocalProperties(p);
}
sort(locals, ADDRESS_COMPARATOR);
}
@Override
protected boolean isEnabledByDefault() {
return DEFAULT_PREF_PLUGIN_ENABLE;
}
@Override
protected List<InetSocketAddress> getLocalSocketAddresses(boolean ipv4) {
TransportProperties p = callback.getLocalProperties();
int preferredPort = parsePortProperty(p.get(PROP_PORT));
String oldIpPorts = p.get(PROP_IP_PORTS);
List<InetSocketAddress> olds = parseIpv4SocketAddresses(oldIpPorts);
List<InetSocketAddress> locals = new ArrayList<>();
List<InetSocketAddress> fallbacks = new ArrayList<>();
for (InetAddress local : getUsableLocalInetAddresses(ipv4)) {
// If we've used this address before, try to use the same port
int port = preferredPort;
for (InetSocketAddress old : olds) {
if (old.getAddress().equals(local)) {
port = old.getPort();
break;
}
}
locals.add(new InetSocketAddress(local, port));
// Fall back to any available port
fallbacks.add(new InetSocketAddress(local, 0));
}
locals.addAll(fallbacks);
return locals;
}
private List<InetSocketAddress> parseSocketAddresses(String ipPorts) {
if (isNullOrEmpty(ipPorts)) return emptyList();
String[] split = ipPorts.split(SEPARATOR);
private int parsePortProperty(@Nullable String portProperty) {
if (isNullOrEmpty(portProperty)) return 0;
try {
return parseInt(portProperty);
} catch (NumberFormatException e) {
return 0;
}
}
private List<InetSocketAddress> parseIpv4SocketAddresses(String ipPorts) {
List<InetSocketAddress> addresses = new ArrayList<>();
for (String ipPort : split) {
InetSocketAddress a = parseSocketAddress(ipPort);
if (isNullOrEmpty(ipPorts)) return addresses;
for (String ipPort : ipPorts.split(SEPARATOR)) {
InetSocketAddress a = parseIpv4SocketAddress(ipPort);
if (a != null) addresses.add(a);
}
return addresses;
}
protected List<InetAddress> getUsableLocalInetAddresses(boolean ipv4) {
List<InterfaceAddress> ifAddrs =
new ArrayList<>(getLocalInterfaceAddresses());
// Prefer longer network prefixes
sort(ifAddrs, (a, b) ->
b.getNetworkPrefixLength() - a.getNetworkPrefixLength());
List<InetAddress> addrs = new ArrayList<>();
for (InterfaceAddress ifAddr : ifAddrs) {
InetAddress addr = ifAddr.getAddress();
if (isAcceptableAddress(addr, ipv4)) addrs.add(addr);
}
return addrs;
}
@Override
protected void setLocalSocketAddress(InetSocketAddress a) {
protected void setLocalSocketAddress(InetSocketAddress a, boolean ipv4) {
if (ipv4) setLocalIpv4SocketAddress(a);
else setLocalIpv6SocketAddress(a);
}
private void setLocalIpv4SocketAddress(InetSocketAddress a) {
String ipPort = getIpPortString(a);
updateRecentAddresses(PREF_LAN_IP_PORTS, PROP_IP_PORTS, ipPort);
}
private void setLocalIpv6SocketAddress(InetSocketAddress a) {
String hex = toHexString(a.getAddress().getAddress());
updateRecentAddresses(PREF_IPV6, PROP_IPV6, hex);
}
private void updateRecentAddresses(String settingKey, String propertyKey,
String item) {
// Get the list of recently used addresses
String setting = callback.getSettings().get(PREF_LAN_IP_PORTS);
List<String> recent = new ArrayList<>();
if (!isNullOrEmpty(setting))
String setting = callback.getSettings().get(settingKey);
Deque<String> recent = new LinkedList<>();
if (!isNullOrEmpty(setting)) {
addAll(recent, setting.split(SEPARATOR));
// Is the address already in the list?
if (recent.remove(ipPort)) {
// Move the address to the start of the list
recent.add(0, ipPort);
}
if (recent.remove(item)) {
// Move the item to the start of the list
recent.addFirst(item);
setting = join(recent, SEPARATOR);
} else {
// Add the address to the start of the list
recent.add(0, ipPort);
// Drop the least recently used address if the list is full
if (recent.size() > MAX_ADDRESSES)
recent = recent.subList(0, MAX_ADDRESSES);
// Add the item to the start of the list
recent.addFirst(item);
// Drop items from the end of the list if it's too long to encode
setting = join(recent, SEPARATOR);
while (utf8IsTooLong(setting, MAX_PROPERTY_LENGTH)) {
recent.removeLast();
setting = join(recent, SEPARATOR);
}
// Update the list of addresses shared with contacts
List<String> shared = new ArrayList<>(recent);
sort(shared);
String property = join(shared, SEPARATOR);
TransportProperties properties = new TransportProperties();
properties.put(PROP_IP_PORTS, property);
properties.put(propertyKey, setting);
callback.mergeLocalProperties(properties);
}
// Save the setting
Settings settings = new Settings();
settings.put(PREF_LAN_IP_PORTS, setting);
settings.put(settingKey, setting);
callback.mergeSettings(settings);
}
protected boolean isIpv6LinkLocalAddress(InetAddress a) {
return a instanceof Inet6Address && a.isLinkLocalAddress();
}
@Override
protected List<InetSocketAddress> getRemoteSocketAddresses(
TransportProperties p) {
return parseSocketAddresses(p.get(PROP_IP_PORTS));
TransportProperties p, boolean ipv4) {
if (ipv4) return getRemoteIpv4SocketAddresses(p);
else return getRemoteIpv6SocketAddresses(p);
}
private boolean isAcceptableAddress(InetAddress a) {
// Accept link-local and site-local IPv4 addresses
boolean ipv4 = a instanceof Inet4Address;
boolean loop = a.isLoopbackAddress();
boolean link = a.isLinkLocalAddress();
boolean site = a.isSiteLocalAddress();
return ipv4 && !loop && (link || site);
private List<InetSocketAddress> getRemoteIpv4SocketAddresses(
TransportProperties p) {
String ipPorts = p.get(PROP_IP_PORTS);
List<InetSocketAddress> remotes = parseIpv4SocketAddresses(ipPorts);
int port = parsePortProperty(p.get(PROP_PORT));
// If the contact has a preferred port, we can guess their IP:port when
// they're providing a wifi access point
if (port != 0) {
InetSocketAddress wifiAp =
new InetSocketAddress(WIFI_AP_ADDRESS, port);
if (!remotes.contains(wifiAp)) remotes.add(wifiAp);
InetSocketAddress wifiDirectAp =
new InetSocketAddress(WIFI_DIRECT_AP_ADDRESS, port);
if (!remotes.contains(wifiDirectAp)) remotes.add(wifiDirectAp);
}
return remotes;
}
private List<InetSocketAddress> getRemoteIpv6SocketAddresses(
TransportProperties p) {
List<InetAddress> addrs = parseIpv6Addresses(p.get(PROP_IPV6));
int port = parsePortProperty(p.get(PROP_PORT));
if (addrs.isEmpty() || port == 0) return emptyList();
List<InetSocketAddress> remotes = new ArrayList<>();
for (InetAddress addr : addrs) {
remotes.add(new InetSocketAddress(addr, port));
}
return remotes;
}
private List<InetAddress> parseIpv6Addresses(String property) {
if (isNullOrEmpty(property)) return emptyList();
try {
List<InetAddress> addrs = new ArrayList<>();
for (String hex : property.split(SEPARATOR)) {
byte[] ip = fromHexString(hex);
if (ip.length == 16) addrs.add(InetAddress.getByAddress(ip));
}
return addrs;
} catch (IllegalArgumentException | UnknownHostException e) {
return emptyList();
}
}
private boolean isAcceptableAddress(InetAddress a, boolean ipv4) {
if (ipv4) {
// Accept link-local and site-local IPv4 addresses
boolean isIpv4 = a instanceof Inet4Address;
boolean link = a.isLinkLocalAddress();
boolean site = a.isSiteLocalAddress();
return isIpv4 && (link || site);
} else {
// Accept link-local IPv6 addresses
return isIpv6LinkLocalAddress(a);
}
}
@Override
protected boolean isConnectable(InetSocketAddress remote) {
protected boolean isConnectable(InterfaceAddress local,
InetSocketAddress remote) {
if (remote.getPort() == 0) return false;
if (!isAcceptableAddress(remote.getAddress())) return false;
InetAddress remoteAddress = remote.getAddress();
boolean ipv4 = local.getAddress() instanceof Inet4Address;
if (!isAcceptableAddress(remoteAddress, ipv4)) return false;
// Try to determine whether the address is on the same LAN as us
if (socket == null) return false;
byte[] localIp = socket.getInetAddress().getAddress();
byte[] localIp = local.getAddress().getAddress();
byte[] remoteIp = remote.getAddress().getAddress();
return addressesAreOnSameLan(localIp, remoteIp);
int prefixLength = local.getNetworkPrefixLength();
return areAddressesInSameNetwork(localIp, remoteIp, prefixLength);
}
// Package access for testing
boolean addressesAreOnSameLan(byte[] localIp, byte[] remoteIp) {
// 10.0.0.0/8
if (isPrefix10(localIp)) return isPrefix10(remoteIp);
// 172.16.0.0/12
if (isPrefix172(localIp)) return isPrefix172(remoteIp);
// 192.168.0.0/16
if (isPrefix192(localIp)) return isPrefix192(remoteIp);
// Unrecognised prefix - may be compatible
static boolean areAddressesInSameNetwork(byte[] localIp, byte[] remoteIp,
int prefixLength) {
if (localIp.length != remoteIp.length) return false;
// Compare the first prefixLength bits of the addresses
for (int i = 0; i < prefixLength; i++) {
int byteIndex = i >> 3;
int bitIndex = i & 7; // 0 to 7
int mask = 128 >> bitIndex; // Select the bit at bitIndex
if ((localIp[byteIndex] & mask) != (remoteIp[byteIndex] & mask)) {
return false; // Addresses differ at bit i
}
}
return true;
}
private static boolean isPrefix10(byte[] ipv4) {
return ipv4[0] == 10;
}
private static boolean isPrefix172(byte[] ipv4) {
return ipv4[0] == (byte) 172 && (ipv4[1] & 0xF0) == 16;
}
private static boolean isPrefix192(byte[] ipv4) {
return ipv4[0] == (byte) 192 && ipv4[1] == (byte) 168;
}
// Returns the prefix length for an RFC 1918 address, or 0 for any other
// address
private static int getRfc1918PrefixLength(InetAddress addr) {
if (!(addr instanceof Inet4Address)) return 0;
if (!addr.isSiteLocalAddress()) return 0;
byte[] ipv4 = addr.getAddress();
if (isPrefix10(ipv4)) return 8;
if (isPrefix172(ipv4)) return 12;
if (isPrefix192(ipv4)) return 16;
return 0;
}
@Override
public boolean supportsKeyAgreement() {
return true;
@@ -209,7 +346,7 @@ class LanTcpPlugin extends TcpPlugin {
} catch (IOException e) {
if (LOG.isLoggable(INFO))
LOG.info("Failed to bind " + scrubSocketAddress(addr));
tryToClose(ss);
tryToClose(ss, LOG, WARNING);
}
}
if (ss == null || !ss.isBound()) {
@@ -225,10 +362,23 @@ class LanTcpPlugin extends TcpPlugin {
return new LanKeyAgreementListener(descriptor, ss);
}
private List<InetSocketAddress> getLocalSocketAddresses() {
List<InetSocketAddress> addrs = new ArrayList<>();
addrs.addAll(getLocalSocketAddresses(true));
addrs.addAll(getLocalSocketAddresses(false));
return addrs;
}
@Override
public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor) {
if (!isRunning()) return null;
ServerSocket ss = state.getServerSocket(true);
if (ss == null) return null;
InterfaceAddress local = getLocalInterfaceAddress(ss.getInetAddress());
if (local == null) {
LOG.warning("No interface for key agreement server socket");
return null;
}
InetSocketAddress remote;
try {
remote = parseSocketAddress(descriptor);
@@ -236,12 +386,11 @@ class LanTcpPlugin extends TcpPlugin {
LOG.info("Invalid IP/port in key agreement descriptor");
return null;
}
if (!isConnectable(remote)) {
if (!isConnectable(local, remote)) {
if (LOG.isLoggable(INFO)) {
SocketAddress local = socket.getLocalSocketAddress();
LOG.info(scrubSocketAddress(remote) +
" is not connectable from " +
scrubSocketAddress(local));
scrubSocketAddress(ss.getLocalSocketAddress()));
}
return null;
}
@@ -249,8 +398,8 @@ class LanTcpPlugin extends TcpPlugin {
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubSocketAddress(remote));
Socket s = createSocket();
s.bind(new InetSocketAddress(socket.getInetAddress(), 0));
s.connect(remote);
s.bind(new InetSocketAddress(ss.getInetAddress(), 0));
s.connect(remote, connectionTimeout);
s.setSoTimeout(socketTimeout);
if (LOG.isLoggable(INFO))
LOG.info("Connected to " + scrubSocketAddress(remote));
@@ -296,22 +445,7 @@ class LanTcpPlugin extends TcpPlugin {
@Override
public void close() {
IoUtils.tryToClose(ss, LOG, WARNING);
}
}
static class LanAddressComparator implements Comparator<InetSocketAddress> {
@Override
public int compare(InetSocketAddress a, InetSocketAddress b) {
// Prefer addresses with non-zero ports
int aPort = a.getPort(), bPort = b.getPort();
if (aPort > 0 && bPort == 0) return -1;
if (aPort == 0 && bPort > 0) return 1;
// Prefer addresses with longer RFC 1918 prefixes
int aPrefix = getRfc1918PrefixLength(a.getAddress());
int bPrefix = getRfc1918PrefixLength(b.getAddress());
return bPrefix - aPrefix;
tryToClose(ss, LOG, WARNING);
}
}
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.plugin.tcp;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.BackoffFactory;
@@ -18,18 +19,21 @@ import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID;
@NotNullByDefault
public class LanTcpPluginFactory implements DuplexPluginFactory {
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 int MAX_LATENCY = 30_000; // 30 seconds
private static final int MAX_IDLE_TIME = 30_000; // 30 seconds
private static final int CONNECTION_TIMEOUT = 3_000; // 3 seconds
private static final int MIN_POLLING_INTERVAL = 60_000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 600_000; // 10 mins
private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor;
private final EventBus eventBus;
private final BackoffFactory backoffFactory;
public LanTcpPluginFactory(Executor ioExecutor,
public LanTcpPluginFactory(Executor ioExecutor, EventBus eventBus,
BackoffFactory backoffFactory) {
this.ioExecutor = ioExecutor;
this.eventBus = eventBus;
this.backoffFactory = backoffFactory;
}
@@ -47,7 +51,9 @@ public class LanTcpPluginFactory implements DuplexPluginFactory {
public DuplexPlugin createPlugin(PluginCallback callback) {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE);
return new LanTcpPlugin(ioExecutor, backoff, callback, MAX_LATENCY,
MAX_IDLE_TIME);
LanTcpPlugin plugin = new LanTcpPlugin(ioExecutor, backoff, callback, MAX_LATENCY,
MAX_IDLE_TIME, CONNECTION_TIMEOUT);
eventBus.addListener(plugin);
return plugin;
}
}

View File

@@ -54,11 +54,13 @@ class PortMapperImpl implements PortMapper {
shutdownManager.addShutdownHook(() -> deleteMapping(port));
}
String externalString = gateway.getExternalIPAddress();
if (LOG.isLoggable(INFO))
LOG.info(
"External address " + scrubInetAddress(externalString));
if (externalString != null)
if (externalString == null) {
LOG.info("External address not available");
} else {
external = InetAddress.getByName(externalString);
if (LOG.isLoggable(INFO))
LOG.info("External address " + scrubInetAddress(external));
}
} catch (IOException | SAXException e) {
logException(LOG, WARNING, e);
}

View File

@@ -3,8 +3,12 @@ package org.briarproject.bramble.plugin.tcp;
import org.briarproject.bramble.PoliteExecutor;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.ConnectionHandler;
@@ -14,15 +18,16 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint;
import org.briarproject.bramble.util.IoUtils;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
@@ -35,20 +40,26 @@ import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import static java.net.NetworkInterface.getNetworkInterfaces;
import static java.util.Collections.emptyList;
import static java.util.Collections.list;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.PrivacyUtils.scrubSocketAddress;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
abstract class TcpPlugin implements DuplexPlugin {
abstract class TcpPlugin implements DuplexPlugin, EventListener {
private static final Logger LOG = getLogger(TcpPlugin.class.getName());
@@ -58,43 +69,52 @@ abstract class TcpPlugin implements DuplexPlugin {
protected final Executor ioExecutor, bindExecutor;
protected final Backoff backoff;
protected final PluginCallback callback;
protected final int maxLatency, maxIdleTime, socketTimeout;
protected final int maxLatency, maxIdleTime;
protected final int connectionTimeout, socketTimeout;
protected final AtomicBoolean used = new AtomicBoolean(false);
protected volatile boolean running = false;
protected volatile ServerSocket socket = null;
protected final PluginState state = new PluginState();
/**
* Returns zero or more socket addresses on which the plugin should listen,
* in order of preference. At most one of the addresses will be bound.
*/
protected abstract List<InetSocketAddress> getLocalSocketAddresses();
protected abstract List<InetSocketAddress> getLocalSocketAddresses(
boolean ipv4);
/**
* Adds the address on which the plugin is listening to the transport
* properties.
*/
protected abstract void setLocalSocketAddress(InetSocketAddress a);
protected abstract void setLocalSocketAddress(InetSocketAddress a,
boolean ipv4);
/**
* Returns zero or more socket addresses for connecting to a contact with
* the given transport properties.
*/
protected abstract List<InetSocketAddress> getRemoteSocketAddresses(
TransportProperties p);
TransportProperties p, boolean ipv4);
/**
* Returns true if connections to the given address can be attempted.
*/
protected abstract boolean isConnectable(InetSocketAddress remote);
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
protected abstract boolean isConnectable(InterfaceAddress local,
InetSocketAddress remote);
/**
* Returns true if the plugin is enabled by default.
*/
protected abstract boolean isEnabledByDefault();
TcpPlugin(Executor ioExecutor, Backoff backoff, PluginCallback callback,
int maxLatency, int maxIdleTime) {
int maxLatency, int maxIdleTime, int connectionTimeout) {
this.ioExecutor = ioExecutor;
this.backoff = backoff;
this.callback = callback;
this.maxLatency = maxLatency;
this.maxIdleTime = maxIdleTime;
this.connectionTimeout = connectionTimeout;
if (maxIdleTime > Integer.MAX_VALUE / 2)
socketTimeout = Integer.MAX_VALUE;
else socketTimeout = maxIdleTime * 2;
@@ -115,49 +135,50 @@ abstract class TcpPlugin implements DuplexPlugin {
@Override
public void start() {
if (used.getAndSet(true)) throw new IllegalStateException();
running = true;
Settings settings = callback.getSettings();
state.setStarted(
settings.getBoolean(PREF_PLUGIN_ENABLE, isEnabledByDefault()));
bind();
}
protected void bind() {
bindExecutor.execute(() -> {
if (!running) return;
if (socket != null && !socket.isClosed()) return;
ServerSocket ss = null;
for (InetSocketAddress addr : getLocalSocketAddresses()) {
try {
ss = new ServerSocket();
ss.bind(addr);
break;
} catch (IOException e) {
if (LOG.isLoggable(INFO))
LOG.info("Failed to bind " + scrubSocketAddress(addr));
tryToClose(ss);
}
}
if (ss == null || !ss.isBound()) {
LOG.info("Could not bind server socket");
return;
}
if (!running) {
tryToClose(ss);
return;
}
socket = ss;
backoff.reset();
InetSocketAddress local =
(InetSocketAddress) ss.getLocalSocketAddress();
setLocalSocketAddress(local);
if (LOG.isLoggable(INFO))
LOG.info("Listening on " + scrubSocketAddress(local));
callback.transportEnabled();
acceptContactConnections();
if (getState() != INACTIVE) return;
bind(true);
bind(false);
});
}
protected void tryToClose(@Nullable ServerSocket ss) {
IoUtils.tryToClose(ss, LOG, WARNING);
callback.transportDisabled();
private void bind(boolean ipv4) {
ServerSocket ss = null;
for (InetSocketAddress addr : getLocalSocketAddresses(ipv4)) {
try {
ss = new ServerSocket();
ss.bind(addr);
break;
} catch (IOException e) {
if (LOG.isLoggable(INFO))
LOG.info("Failed to bind " + scrubSocketAddress(addr));
tryToClose(ss, LOG, WARNING);
}
}
if (ss == null || !ss.isBound()) {
LOG.info("Could not bind server socket");
return;
}
if (!state.setServerSocket(ss, ipv4)) {
LOG.info("Closing redundant server socket");
tryToClose(ss, LOG, WARNING);
return;
}
backoff.reset();
InetSocketAddress local =
(InetSocketAddress) ss.getLocalSocketAddress();
setLocalSocketAddress(local, ipv4);
if (LOG.isLoggable(INFO))
LOG.info("Listening on " + scrubSocketAddress(local));
ServerSocket finalSocket = ss;
ioExecutor.execute(() -> acceptContactConnections(finalSocket, ipv4));
}
String getIpPortString(InetSocketAddress a) {
@@ -167,20 +188,22 @@ abstract class TcpPlugin implements DuplexPlugin {
return addr + ":" + a.getPort();
}
private void acceptContactConnections() {
while (isRunning()) {
private void acceptContactConnections(ServerSocket ss, boolean ipv4) {
while (true) {
Socket s;
try {
s = socket.accept();
s = ss.accept();
s.setSoTimeout(socketTimeout);
} catch (IOException e) {
// This is expected when the socket is closed
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
// This is expected when the server socket is closed
LOG.info("Server socket closed");
state.clearServerSocket(ss, ipv4);
return;
}
if (LOG.isLoggable(INFO))
if (LOG.isLoggable(INFO)) {
LOG.info("Connection from " +
scrubSocketAddress(s.getRemoteSocketAddress()));
}
backoff.reset();
callback.handleConnection(new TcpTransportConnection(this, s));
}
@@ -188,13 +211,17 @@ abstract class TcpPlugin implements DuplexPlugin {
@Override
public void stop() {
running = false;
tryToClose(socket);
for (ServerSocket ss : state.setStopped()) tryToClose(ss, LOG, WARNING);
}
@Override
public boolean isRunning() {
return running && socket != null && !socket.isClosed();
public State getState() {
return state.getState();
}
@Override
public int getReasonsDisabled() {
return state.getReasonsDisabled();
}
@Override
@@ -210,7 +237,7 @@ abstract class TcpPlugin implements DuplexPlugin {
@Override
public void poll(Collection<Pair<TransportProperties, ConnectionHandler>>
properties) {
if (!isRunning()) return;
if (getState() != ACTIVE) return;
backoff.increment();
for (Pair<TransportProperties, ConnectionHandler> p : properties) {
connect(p.getFirst(), p.getSecond());
@@ -229,14 +256,32 @@ abstract class TcpPlugin implements DuplexPlugin {
@Override
public DuplexTransportConnection createConnection(TransportProperties p) {
if (!isRunning()) return null;
for (InetSocketAddress remote : getRemoteSocketAddresses(p)) {
if (!isConnectable(remote)) {
DuplexTransportConnection c = createConnection(p, true);
if (c != null) return c;
return createConnection(p, false);
}
@Nullable
private DuplexTransportConnection createConnection(TransportProperties p,
boolean ipv4) {
ServerSocket ss = state.getServerSocket(ipv4);
if (ss == null) return null;
InterfaceAddress local = getLocalInterfaceAddress(ss.getInetAddress());
if (local == null) {
LOG.warning("No interface for server socket");
return null;
}
for (InetSocketAddress remote : getRemoteSocketAddresses(p, ipv4)) {
// Don't try to connect to our own address
if (!canConnectToOwnAddress() &&
remote.getAddress().equals(ss.getInetAddress())) {
continue;
}
if (!isConnectable(local, remote)) {
if (LOG.isLoggable(INFO)) {
SocketAddress local = socket.getLocalSocketAddress();
LOG.info(scrubSocketAddress(remote) +
" is not connectable from " +
scrubSocketAddress(local));
scrubSocketAddress(ss.getLocalSocketAddress()));
}
continue;
}
@@ -244,27 +289,45 @@ abstract class TcpPlugin implements DuplexPlugin {
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubSocketAddress(remote));
Socket s = createSocket();
s.bind(new InetSocketAddress(socket.getInetAddress(), 0));
s.connect(remote);
s.bind(new InetSocketAddress(ss.getInetAddress(), 0));
s.connect(remote, connectionTimeout);
s.setSoTimeout(socketTimeout);
if (LOG.isLoggable(INFO))
LOG.info("Connected to " + scrubSocketAddress(remote));
return new TcpTransportConnection(this, s);
} catch (IOException e) {
if (LOG.isLoggable(INFO))
if (LOG.isLoggable(INFO)) {
LOG.info("Could not connect to " +
scrubSocketAddress(remote));
}
}
}
return null;
}
@Nullable
InterfaceAddress getLocalInterfaceAddress(InetAddress a) {
for (InterfaceAddress ifAddr : getLocalInterfaceAddresses()) {
if (ifAddr.getAddress().equals(a)) return ifAddr;
}
return null;
}
// Override for testing
protected boolean canConnectToOwnAddress() {
return false;
}
protected Socket createSocket() throws IOException {
return new Socket();
}
int chooseEphemeralPort() {
return 32768 + (int) (Math.random() * 32768);
}
@Nullable
InetSocketAddress parseSocketAddress(String ipPort) {
InetSocketAddress parseIpv4SocketAddress(String ipPort) {
if (isNullOrEmpty(ipPort)) return null;
String[] split = ipPort.split(":");
if (split.length != 2) return null;
@@ -275,14 +338,7 @@ abstract class TcpPlugin implements DuplexPlugin {
InetAddress a = InetAddress.getByName(addr);
int p = Integer.parseInt(port);
return new InetSocketAddress(a, p);
} catch (UnknownHostException e) {
if (LOG.isLoggable(WARNING))
// not scrubbing to enable us to find the problem
LOG.warning("Invalid address: " + addr);
return null;
} catch (NumberFormatException e) {
if (LOG.isLoggable(WARNING))
LOG.warning("Invalid port: " + port);
} catch (UnknownHostException | NumberFormatException e) {
return null;
}
}
@@ -314,17 +370,140 @@ abstract class TcpPlugin implements DuplexPlugin {
throw new UnsupportedOperationException();
}
Collection<InetAddress> getLocalIpAddresses() {
List<InterfaceAddress> getLocalInterfaceAddresses() {
List<InterfaceAddress> addrs = new ArrayList<>();
for (NetworkInterface iface : getNetworkInterfaces()) {
addrs.addAll(iface.getInterfaceAddresses());
}
return addrs;
}
List<InetAddress> getLocalInetAddresses() {
List<InetAddress> addrs = new ArrayList<>();
for (NetworkInterface iface : getNetworkInterfaces()) {
addrs.addAll(list(iface.getInetAddresses()));
}
return addrs;
}
private List<NetworkInterface> getNetworkInterfaces() {
try {
Enumeration<NetworkInterface> ifaces = getNetworkInterfaces();
if (ifaces == null) return emptyList();
List<InetAddress> addrs = new ArrayList<>();
for (NetworkInterface iface : list(ifaces))
addrs.addAll(list(iface.getInetAddresses()));
return addrs;
Enumeration<NetworkInterface> ifaces =
NetworkInterface.getNetworkInterfaces();
return ifaces == null ? emptyList() : list(ifaces);
} catch (SocketException e) {
logException(LOG, WARNING, e);
return emptyList();
}
}
@Override
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
if (s.getNamespace().equals(getId().getString()))
ioExecutor.execute(() -> onSettingsUpdated(s.getSettings()));
}
}
@IoExecutor
private void onSettingsUpdated(Settings settings) {
boolean enabledByUser =
settings.getBoolean(PREF_PLUGIN_ENABLE, isEnabledByDefault());
List<ServerSocket> toClose = state.setEnabledByUser(enabledByUser);
State s = getState();
if (!toClose.isEmpty()) {
LOG.info("Disabled by user, closing server sockets");
for (ServerSocket ss : toClose) tryToClose(ss, LOG, WARNING);
} else if (s == INACTIVE) {
LOG.info("Enabled by user, opening server sockets");
bind();
}
}
@ThreadSafe
@NotNullByDefault
protected class PluginState {
@GuardedBy("this")
private boolean started = false, stopped = false, enabledByUser = false;
@GuardedBy("this")
@Nullable
private ServerSocket serverSocketV4 = null, serverSocketV6 = null;
synchronized void setStarted(boolean enabledByUser) {
started = true;
this.enabledByUser = enabledByUser;
callback.pluginStateChanged(getState());
}
synchronized List<ServerSocket> setStopped() {
stopped = true;
List<ServerSocket> toClose = clearServerSockets();
callback.pluginStateChanged(getState());
return toClose;
}
@GuardedBy("this")
private List<ServerSocket> clearServerSockets() {
List<ServerSocket> toClose = new ArrayList<>(2);
if (serverSocketV4 != null) {
toClose.add(serverSocketV4);
serverSocketV4 = null;
}
if (serverSocketV6 != null) {
toClose.add(serverSocketV6);
serverSocketV6 = null;
}
return toClose;
}
synchronized List<ServerSocket> setEnabledByUser(
boolean enabledByUser) {
this.enabledByUser = enabledByUser;
List<ServerSocket> toClose = enabledByUser
? emptyList() : clearServerSockets();
callback.pluginStateChanged(getState());
return toClose;
}
@Nullable
synchronized ServerSocket getServerSocket(boolean ipv4) {
return ipv4 ? serverSocketV4 : serverSocketV6;
}
synchronized boolean setServerSocket(ServerSocket ss, boolean ipv4) {
if (stopped) return false;
if (ipv4) {
if (serverSocketV4 != null) return false;
serverSocketV4 = ss;
} else {
if (serverSocketV6 != null) return false;
serverSocketV6 = ss;
}
callback.pluginStateChanged(getState());
return true;
}
synchronized void clearServerSocket(ServerSocket ss, boolean ipv4) {
if (ipv4) {
if (serverSocketV4 == ss) serverSocketV4 = null;
} else {
if (serverSocketV6 == ss) serverSocketV6 = null;
}
callback.pluginStateChanged(getState());
}
synchronized State getState() {
if (!started || stopped) return STARTING_STOPPING;
if (!enabledByUser) return DISABLED;
if (serverSocketV4 != null || serverSocketV6 != null) return ACTIVE;
return INACTIVE;
}
synchronized int getReasonsDisabled() {
return getState() == DISABLED ? REASON_USER : 0;
}
}
}

View File

@@ -10,12 +10,14 @@ import org.briarproject.bramble.api.properties.TransportProperties;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.briarproject.bramble.api.plugin.WanTcpConstants.DEFAULT_PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.WanTcpConstants.ID;
@MethodsNotNullByDefault
@@ -29,8 +31,10 @@ class WanTcpPlugin extends TcpPlugin {
private volatile MappingResult mappingResult;
WanTcpPlugin(Executor ioExecutor, Backoff backoff, PortMapper portMapper,
PluginCallback callback, int maxLatency, int maxIdleTime) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime);
PluginCallback callback, int maxLatency, int maxIdleTime,
int connectionTimeout) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime,
connectionTimeout);
this.portMapper = portMapper;
}
@@ -40,12 +44,18 @@ class WanTcpPlugin extends TcpPlugin {
}
@Override
protected List<InetSocketAddress> getLocalSocketAddresses() {
protected boolean isEnabledByDefault() {
return DEFAULT_PREF_PLUGIN_ENABLE;
}
@Override
protected List<InetSocketAddress> getLocalSocketAddresses(boolean ipv4) {
if (!ipv4) return emptyList();
// Use the same address and port as last time if available
TransportProperties p = callback.getLocalProperties();
InetSocketAddress old = parseSocketAddress(p.get(PROP_IP_PORT));
InetSocketAddress old = parseIpv4SocketAddress(p.get(PROP_IP_PORT));
List<InetSocketAddress> addrs = new LinkedList<>();
for (InetAddress a : getLocalIpAddresses()) {
for (InetAddress a : getLocalInetAddresses()) {
if (isAcceptableAddress(a)) {
// If this is the old address, try to use the same port
if (old != null && old.getAddress().equals(a))
@@ -73,26 +83,25 @@ class WanTcpPlugin extends TcpPlugin {
return ipv4 && !loop && !link && !site;
}
private int chooseEphemeralPort() {
return 32768 + (int) (Math.random() * 32768);
}
@Override
protected List<InetSocketAddress> getRemoteSocketAddresses(
TransportProperties p) {
InetSocketAddress parsed = parseSocketAddress(p.get(PROP_IP_PORT));
TransportProperties p, boolean ipv4) {
if (!ipv4) return emptyList();
InetSocketAddress parsed = parseIpv4SocketAddress(p.get(PROP_IP_PORT));
if (parsed == null) return emptyList();
return singletonList(parsed);
}
@Override
protected boolean isConnectable(InetSocketAddress remote) {
protected boolean isConnectable(InterfaceAddress local,
InetSocketAddress remote) {
if (remote.getPort() == 0) return false;
return isAcceptableAddress(remote.getAddress());
}
@Override
protected void setLocalSocketAddress(InetSocketAddress a) {
protected void setLocalSocketAddress(InetSocketAddress a, boolean ipv4) {
if (!ipv4) throw new AssertionError();
if (mappingResult != null && mappingResult.isUsable()) {
// Advertise the external address to contacts
if (a.equals(mappingResult.getInternal())) {

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.plugin.tcp;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.ShutdownManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
@@ -19,19 +20,22 @@ import static org.briarproject.bramble.api.plugin.WanTcpConstants.ID;
@NotNullByDefault
public class WanTcpPluginFactory implements DuplexPluginFactory {
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 int MAX_LATENCY = 30_000; // 30 seconds
private static final int MAX_IDLE_TIME = 30_000; // 30 seconds
private static final int CONNECTION_TIMEOUT = 30_000; // 30 seconds
private static final int MIN_POLLING_INTERVAL = 60_000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 600_000; // 10 mins
private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor;
private final EventBus eventBus;
private final BackoffFactory backoffFactory;
private final ShutdownManager shutdownManager;
public WanTcpPluginFactory(Executor ioExecutor,
public WanTcpPluginFactory(Executor ioExecutor, EventBus eventBus,
BackoffFactory backoffFactory, ShutdownManager shutdownManager) {
this.ioExecutor = ioExecutor;
this.eventBus = eventBus;
this.backoffFactory = backoffFactory;
this.shutdownManager = shutdownManager;
}
@@ -50,8 +54,10 @@ public class WanTcpPluginFactory implements DuplexPluginFactory {
public DuplexPlugin createPlugin(PluginCallback callback) {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE);
return new WanTcpPlugin(ioExecutor, backoff,
WanTcpPlugin plugin = new WanTcpPlugin(ioExecutor, backoff,
new PortMapperImpl(shutdownManager), callback, MAX_LATENCY,
MAX_IDLE_TIME);
MAX_IDLE_TIME, CONNECTION_TIMEOUT);
eventBus.addListener(plugin);
return plugin;
}
}

View File

@@ -17,7 +17,7 @@ public interface CircumventionProvider {
String[] BLOCKED = {"CN", "IR", "EG", "BY", "TR", "SY", "VE"};
/**
* Countries where obfs4 bridge connection are likely to work.
* Countries where obfs4 or meek bridge connections are likely to work.
* Should be a subset of {@link #BLOCKED}.
*/
String[] BRIDGES = { "CN", "IR", "EG", "BY", "TR", "SY", "VE" };

View File

@@ -15,6 +15,7 @@ import org.briarproject.bramble.api.network.NetworkManager;
import org.briarproject.bramble.api.network.NetworkStatus;
import org.briarproject.bramble.api.network.event.NetworkStatusEvent;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.ConnectionHandler;
@@ -54,6 +55,9 @@ import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.zip.ZipInputStream;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import javax.net.SocketFactory;
import static java.util.Arrays.asList;
@@ -65,7 +69,16 @@ import static java.util.logging.Logger.getLogger;
import static net.freehaven.tor.control.TorControlCommands.HS_ADDRESS;
import static net.freehaven.tor.control.TorControlCommands.HS_PRIVKEY;
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.Plugin.State.DISABLED;
import static org.briarproject.bramble.api.plugin.Plugin.State.ENABLING;
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.bramble.api.plugin.TorConstants.CONTROL_PORT;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.bramble.api.plugin.TorConstants.ID;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
@@ -76,6 +89,9 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHE
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_PORT;
import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V2;
import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V3;
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_BATTERY;
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_COUNTRY_BLOCKED;
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_MOBILE_DATA;
import static org.briarproject.bramble.plugin.tor.TorRendezvousCrypto.SEED_BYTES;
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
@@ -113,16 +129,14 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private final int maxLatency, maxIdleTime, socketTimeout;
private final File torDirectory, torFile, geoIpFile, obfs4File, configFile;
private final File doneFile, cookieFile;
private final ConnectionStatus connectionStatus;
private final AtomicBoolean used = new AtomicBoolean(false);
private volatile ServerSocket socket = null;
protected final PluginState state = new PluginState();
private volatile Socket controlSocket = null;
private volatile TorControlConnection controlConnection = null;
private volatile Settings settings = null;
protected volatile boolean running = false;
protected abstract int getProcessId();
protected abstract long getLastUpdateTime();
@@ -159,7 +173,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
configFile = new File(torDirectory, "torrc");
doneFile = new File(torDirectory, "done");
cookieFile = new File(torDirectory, ".tor/control_auth_cookie");
connectionStatus = new ConnectionStatus();
// Don't execute more than one connection status check at a time
connectionStatusExecutor =
new PoliteExecutor("TorPlugin", ioExecutor, 1);
@@ -190,7 +203,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
}
// Load the settings
settings = callback.getSettings();
settings = migrateSettings(callback.getSettings());
// Install or update the assets if necessary
if (!assetsAreUpToDate()) installAssets();
if (cookieFile.exists() && !cookieFile.delete())
@@ -258,7 +271,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
// Tell Tor to exit when the control connection is closed
controlConnection.takeOwnership();
controlConnection.resetConf(singletonList(OWNER));
running = true;
// Register to receive events from the Tor process
controlConnection.setEventHandler(this);
controlConnection.setEvents(asList(EVENTS));
@@ -266,11 +278,12 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
String phase = controlConnection.getInfo("status/bootstrap-phase");
if (phase != null && phase.contains("PROGRESS=100")) {
LOG.info("Tor has already bootstrapped");
connectionStatus.setBootstrapped();
state.setBootstrapped();
}
} catch (IOException e) {
throw new PluginException(e);
}
state.setStarted();
// Check whether we're online
updateConnectionStatus(networkManager.getNetworkStatus(),
batteryManager.isCharging());
@@ -278,6 +291,18 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
bind();
}
// TODO: Remove after a reasonable migration period (added 2020-06-25)
private Settings migrateSettings(Settings settings) {
int network = settings.getInt(PREF_TOR_NETWORK,
DEFAULT_PREF_TOR_NETWORK);
if (network == PREF_TOR_NETWORK_NEVER) {
settings.putInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK);
settings.putBoolean(PREF_PLUGIN_ENABLE, false);
callback.mergeSettings(settings);
}
return settings;
}
private boolean assetsAreUpToDate() {
return doneFile.lastModified() > getLastUpdateTime();
}
@@ -393,11 +418,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
tryToClose(ss, LOG, WARNING);
return;
}
if (!running) {
if (!state.setServerSocket(ss)) {
LOG.info("Closing redundant server socket");
tryToClose(ss, LOG, WARNING);
return;
}
socket = ss;
// Store the port number
String localPort = String.valueOf(ss.getLocalPort());
Settings s = new Settings();
@@ -412,7 +437,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
private void publishHiddenService(String port) {
if (!running) return;
if (!state.isTorRunning()) return;
LOG.info("Creating hidden service");
String privKey = settings.get(HS_PRIVKEY);
Map<Integer, String> portLines = singletonMap(80, "127.0.0.1:" + port);
@@ -450,14 +475,15 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
private void acceptContactConnections(ServerSocket ss) {
while (running) {
while (true) {
Socket s;
try {
s = ss.accept();
s.setSoTimeout(socketTimeout);
} catch (IOException e) {
// This is expected when the socket is closed
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
// This is expected when the server socket is closed
LOG.info("Server socket closed");
state.clearServerSocket(ss);
return;
}
LOG.info("Connection received");
@@ -467,10 +493,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
protected void enableNetwork(boolean enable) throws IOException {
if (!running) return;
connectionStatus.enableNetwork(enable);
state.enableNetwork(enable);
controlConnection.setConf("DisableNetwork", enable ? "0" : "1");
if (!enable) callback.transportDisabled();
}
private void enableBridges(boolean enable, boolean needsMeek)
@@ -494,9 +518,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override
public void stop() {
running = false;
tryToClose(socket, LOG, WARNING);
callback.transportDisabled();
ServerSocket ss = state.setStopped();
tryToClose(ss, LOG, WARNING);
if (controlSocket != null && controlConnection != null) {
try {
LOG.info("Stopping Tor");
@@ -510,8 +533,13 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
@Override
public boolean isRunning() {
return running && connectionStatus.isConnected();
public State getState() {
return state.getState();
}
@Override
public int getReasonsDisabled() {
return state.getReasonsDisabled();
}
@Override
@@ -527,7 +555,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override
public void poll(Collection<Pair<TransportProperties, ConnectionHandler>>
properties) {
if (!isRunning()) return;
if (getState() != ACTIVE) return;
backoff.increment();
for (Pair<TransportProperties, ConnectionHandler> p : properties) {
connect(p.getFirst(), p.getSecond());
@@ -546,7 +574,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override
public DuplexTransportConnection createConnection(TransportProperties p) {
if (!isRunning()) return null;
if (getState() != ACTIVE) return null;
String bestOnion = null;
String onion2 = p.get(PROP_ONION_V2);
String onion3 = p.get(PROP_ONION_V3);
@@ -634,8 +662,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
new TorTransportConnection(this, s));
}
} catch (IOException e) {
// This is expected when the socket is closed
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
// This is expected when the server socket is closed
LOG.info("Rendezvous server socket closed");
}
});
Map<Integer, String> portLines =
@@ -663,10 +691,9 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override
public void circuitStatus(String status, String id, String path) {
if (status.equals("BUILT") &&
connectionStatus.getAndSetCircuitBuilt()) {
state.getAndSetCircuitBuilt()) {
LOG.info("First circuit built");
backoff.reset();
if (isRunning()) callback.transportEnabled();
}
}
@@ -697,9 +724,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
public void message(String severity, String msg) {
if (LOG.isLoggable(INFO)) LOG.info(severity + " " + msg);
if (severity.equals("NOTICE") && msg.startsWith("Bootstrapped 100%")) {
connectionStatus.setBootstrapped();
state.setBootstrapped();
backoff.reset();
if (isRunning()) callback.transportEnabled();
}
}
@@ -736,7 +762,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private void disableNetwork() {
connectionStatusExecutor.execute(() -> {
try {
enableNetwork(false);
if (state.isTorRunning()) enableNetwork(false);
} catch (IOException ex) {
logException(LOG, WARNING, ex);
}
@@ -746,63 +772,90 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private void updateConnectionStatus(NetworkStatus status,
boolean charging) {
connectionStatusExecutor.execute(() -> {
if (!running) return;
if (!state.isTorRunning()) return;
boolean online = status.isConnected();
boolean wifi = status.isWifi();
String country = locationUtils.getCurrentCountry();
boolean blocked =
circumventionProvider.isTorProbablyBlocked(country);
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
DEFAULT_PREF_PLUGIN_ENABLE);
int network = settings.getInt(PREF_TOR_NETWORK,
PREF_TOR_NETWORK_AUTOMATIC);
boolean useMobile = settings.getBoolean(PREF_TOR_MOBILE, true);
DEFAULT_PREF_TOR_NETWORK);
boolean useMobile = settings.getBoolean(PREF_TOR_MOBILE,
DEFAULT_PREF_TOR_MOBILE);
boolean onlyWhenCharging =
settings.getBoolean(PREF_TOR_ONLY_WHEN_CHARGING, false);
settings.getBoolean(PREF_TOR_ONLY_WHEN_CHARGING,
DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING);
boolean bridgesWork = circumventionProvider.doBridgesWork(country);
boolean automatic = network == PREF_TOR_NETWORK_AUTOMATIC;
if (LOG.isLoggable(INFO)) {
LOG.info("Online: " + online + ", wifi: " + wifi);
if ("".equals(country)) LOG.info("Country code unknown");
if (country.isEmpty()) LOG.info("Country code unknown");
else LOG.info("Country code: " + country);
LOG.info("Charging: " + charging);
}
try {
if (!online) {
LOG.info("Disabling network, device is offline");
enableNetwork(false);
} else if (!charging && onlyWhenCharging) {
LOG.info("Disabling network, device is on battery");
enableNetwork(false);
} else if (network == PREF_TOR_NETWORK_NEVER ||
(!useMobile && !wifi)) {
LOG.info("Disabling network, device is using mobile data");
enableNetwork(false);
} else if (automatic && blocked && !bridgesWork) {
LOG.info("Disabling network, country is blocked");
enableNetwork(false);
} else if (network == PREF_TOR_NETWORK_WITH_BRIDGES ||
(automatic && bridgesWork)) {
if (circumventionProvider.needsMeek(country)) {
LOG.info("Enabling network, using meek bridges");
enableBridges(true, true);
int reasonsDisabled = 0;
boolean enableNetwork = false, enableBridges = false;
boolean useMeek = false, enableConnectionPadding = false;
if (!online) {
LOG.info("Disabling network, device is offline");
} else {
if (!enabledByUser) {
LOG.info("User has disabled Tor");
reasonsDisabled |= REASON_USER;
}
if (!charging && onlyWhenCharging) {
LOG.info("Configured not to use battery");
reasonsDisabled |= REASON_BATTERY;
}
if (!useMobile && !wifi) {
LOG.info("Configured not to use mobile data");
reasonsDisabled |= REASON_MOBILE_DATA;
}
if (automatic && blocked && !bridgesWork) {
LOG.info("Country is blocked");
reasonsDisabled |= REASON_COUNTRY_BLOCKED;
}
if (reasonsDisabled != 0) {
LOG.info("Disabling network due to settings");
} else {
LOG.info("Enabling network");
enableNetwork = true;
if (network == PREF_TOR_NETWORK_WITH_BRIDGES ||
(automatic && bridgesWork)) {
if (circumventionProvider.needsMeek(country)) {
LOG.info("Using meek bridges");
enableBridges = true;
useMeek = true;
} else {
LOG.info("Using obfs4 bridges");
enableBridges = true;
}
} else {
LOG.info("Enabling network, using obfs4 bridges");
enableBridges(true, false);
LOG.info("Not using bridges");
}
if (wifi && charging) {
LOG.info("Enabling connection padding");
enableConnectionPadding = true;
} else {
LOG.info("Disabling connection padding");
}
enableNetwork(true);
} else {
LOG.info("Enabling network, not using bridges");
enableBridges(false, false);
enableNetwork(true);
}
if (online && wifi && charging) {
LOG.info("Enabling connection padding");
enableConnectionPadding(true);
} else {
LOG.info("Disabling connection padding");
enableConnectionPadding(false);
}
state.setReasonsDisabled(reasonsDisabled);
try {
if (enableNetwork) {
enableBridges(enableBridges, useMeek);
enableConnectionPadding(enableConnectionPadding);
}
enableNetwork(enableNetwork);
} catch (IOException e) {
logException(LOG, WARNING, e);
}
@@ -810,33 +863,96 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
private void enableConnectionPadding(boolean enable) throws IOException {
if (!running) return;
controlConnection.setConf("ConnectionPadding", enable ? "1" : "0");
}
private static class ConnectionStatus {
@ThreadSafe
@NotNullByDefault
protected class PluginState {
// All of the following are locking: this
private boolean networkEnabled = false;
private boolean bootstrapped = false, circuitBuilt = false;
@GuardedBy("this")
private boolean started = false,
stopped = false,
networkInitialised = false,
networkEnabled = false,
bootstrapped = false,
circuitBuilt = false,
settingsChecked = false;
private synchronized void setBootstrapped() {
bootstrapped = true;
@GuardedBy("this")
private int reasonsDisabled = 0;
@GuardedBy("this")
@Nullable
private ServerSocket serverSocket = null;
synchronized void setStarted() {
started = true;
callback.pluginStateChanged(getState());
}
private synchronized boolean getAndSetCircuitBuilt() {
synchronized boolean isTorRunning() {
return started && !stopped;
}
@Nullable
synchronized ServerSocket setStopped() {
stopped = true;
ServerSocket ss = serverSocket;
serverSocket = null;
callback.pluginStateChanged(getState());
return ss;
}
synchronized void setBootstrapped() {
bootstrapped = true;
callback.pluginStateChanged(getState());
}
synchronized boolean getAndSetCircuitBuilt() {
boolean firstCircuit = !circuitBuilt;
circuitBuilt = true;
callback.pluginStateChanged(getState());
return firstCircuit;
}
private synchronized void enableNetwork(boolean enable) {
synchronized void enableNetwork(boolean enable) {
networkInitialised = true;
networkEnabled = enable;
if (!enable) circuitBuilt = false;
callback.pluginStateChanged(getState());
}
private synchronized boolean isConnected() {
return networkEnabled && bootstrapped && circuitBuilt;
synchronized void setReasonsDisabled(int reasonsDisabled) {
settingsChecked = true;
this.reasonsDisabled = reasonsDisabled;
callback.pluginStateChanged(getState());
}
// Doesn't affect getState()
synchronized boolean setServerSocket(ServerSocket ss) {
if (stopped || serverSocket != null) return false;
serverSocket = ss;
return true;
}
// Doesn't affect getState()
synchronized void clearServerSocket(ServerSocket ss) {
if (serverSocket == ss) serverSocket = null;
}
synchronized State getState() {
if (!started || stopped || !settingsChecked) {
return STARTING_STOPPING;
}
if (reasonsDisabled != 0) return DISABLED;
if (!networkInitialised) return ENABLING;
if (!networkEnabled) return INACTIVE;
return bootstrapped && circuitBuilt ? ACTIVE : ENABLING;
}
synchronized int getReasonsDisabled() {
return getState() == DISABLED ? reasonsDisabled : 0;
}
}
}

View File

@@ -37,6 +37,11 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.GROUP_KEY_DISCOVERED;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_LOCAL;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_TRANSPORT_ID;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_VERSION;
@Immutable
@NotNullByDefault
class TransportPropertyManagerImpl implements TransportPropertyManager,
@@ -111,10 +116,10 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
try {
// Find the latest update for this transport, if any
BdfDictionary d = metadataParser.parse(meta);
TransportId t = new TransportId(d.getString("transportId"));
TransportId t = new TransportId(d.getString(MSG_KEY_TRANSPORT_ID));
LatestUpdate latest = findLatest(txn, m.getGroupId(), t, false);
if (latest != null) {
if (d.getLong("version") > latest.version) {
if (d.getLong(MSG_KEY_VERSION) > latest.version) {
// This update is newer - delete the previous update
db.deleteMessage(txn, latest.messageId);
db.deleteMessageMetadata(txn, latest.messageId);
@@ -140,6 +145,27 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
}
}
@Override
public void addRemotePropertiesFromConnection(ContactId c, TransportId t,
TransportProperties props) throws DbException {
if (props.isEmpty()) return;
try {
db.transaction(false, txn -> {
Group g = getContactGroup(db.getContact(txn, c));
BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(
txn, g.getId());
BdfDictionary discovered =
meta.getOptionalDictionary(GROUP_KEY_DISCOVERED);
if (discovered == null) discovered = new BdfDictionary();
discovered.putAll(props);
meta.put(GROUP_KEY_DISCOVERED, discovered);
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
});
} catch (FormatException e) {
throw new DbException(e);
}
}
@Override
public Map<TransportId, TransportProperties> getLocalProperties()
throws DbException {
@@ -203,12 +229,26 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
Group g = getContactGroup(c);
try {
// Find the latest remote update
TransportProperties remote;
LatestUpdate latest = findLatest(txn, g.getId(), t, false);
if (latest == null) return new TransportProperties();
// Retrieve and parse the latest remote properties
BdfList message =
clientHelper.getMessageAsList(txn, latest.messageId);
return parseProperties(message);
if (latest == null) {
remote = new TransportProperties();
} else {
// Retrieve and parse the latest remote properties
BdfList message =
clientHelper.getMessageAsList(txn, latest.messageId);
remote = parseProperties(message);
}
// Merge in any discovered properties
BdfDictionary meta =
clientHelper.getGroupMetadataAsDictionary(txn, g.getId());
BdfDictionary d = meta.getOptionalDictionary(GROUP_KEY_DISCOVERED);
if (d == null) return remote;
TransportProperties merged =
clientHelper.parseAndValidateTransportProperties(d);
// Received properties override discovered properties
merged.putAll(remote);
return merged;
} catch (FormatException e) {
throw new DbException(e);
}
@@ -281,9 +321,9 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
long now = clock.currentTimeMillis();
Message m = clientHelper.createMessage(g, now, body);
BdfDictionary meta = new BdfDictionary();
meta.put("transportId", t.getString());
meta.put("version", version);
meta.put("local", local);
meta.put(MSG_KEY_TRANSPORT_ID, t.getString());
meta.put(MSG_KEY_VERSION, version);
meta.put(MSG_KEY_LOCAL, local);
clientHelper.addLocalMessage(txn, m, meta, shared, false);
} catch (FormatException e) {
throw new RuntimeException(e);
@@ -302,8 +342,9 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
.getMessageMetadataAsDictionary(txn, localGroup.getId());
for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
BdfDictionary meta = e.getValue();
TransportId t = new TransportId(meta.getString("transportId"));
long version = meta.getLong("version");
TransportId t =
new TransportId(meta.getString(MSG_KEY_TRANSPORT_ID));
long version = meta.getLong(MSG_KEY_VERSION);
latestUpdates.put(t, new LatestUpdate(e.getKey(), version));
}
return latestUpdates;
@@ -316,9 +357,10 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
clientHelper.getMessageMetadataAsDictionary(txn, g);
for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
BdfDictionary meta = e.getValue();
if (meta.getString("transportId").equals(t.getString())
&& meta.getBoolean("local") == local) {
return new LatestUpdate(e.getKey(), meta.getLong("version"));
if (meta.getString(MSG_KEY_TRANSPORT_ID).equals(t.getString())
&& meta.getBoolean(MSG_KEY_LOCAL) == local) {
return new LatestUpdate(e.getKey(),
meta.getLong(MSG_KEY_VERSION));
}
}
return null;

View File

@@ -2,6 +2,7 @@ package org.briarproject.bramble.rendezvous;
import org.briarproject.bramble.PoliteExecutor;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.contact.PendingContactState;
@@ -23,7 +24,6 @@ import org.briarproject.bramble.api.lifecycle.Service;
import org.briarproject.bramble.api.lifecycle.ServiceException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionHandler;
import org.briarproject.bramble.api.plugin.ConnectionManager;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
@@ -31,8 +31,8 @@ import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent;
import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent;
import org.briarproject.bramble.api.plugin.event.TransportActiveEvent;
import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint;
@@ -269,11 +269,11 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
} else if (e instanceof PendingContactRemovedEvent) {
PendingContactRemovedEvent p = (PendingContactRemovedEvent) e;
removePendingContactAsync(p.getId());
} else if (e instanceof TransportEnabledEvent) {
TransportEnabledEvent t = (TransportEnabledEvent) e;
} else if (e instanceof TransportActiveEvent) {
TransportActiveEvent t = (TransportActiveEvent) e;
addTransportAsync(t.getTransportId());
} else if (e instanceof TransportDisabledEvent) {
TransportDisabledEvent t = (TransportDisabledEvent) e;
} else if (e instanceof TransportInactiveEvent) {
TransportInactiveEvent t = (TransportInactiveEvent) e;
removeTransportAsync(t.getTransportId());
} else if (e instanceof RendezvousConnectionOpenedEvent) {
RendezvousConnectionOpenedEvent r =

View File

@@ -6,7 +6,7 @@ import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent;
import org.briarproject.bramble.api.plugin.event.TransportActiveEvent;
import org.briarproject.bramble.api.reporting.DevConfig;
import org.briarproject.bramble.api.reporting.DevReporter;
import org.briarproject.bramble.util.IoUtils;
@@ -92,8 +92,8 @@ class DevReporterImpl implements DevReporter, EventListener {
@Override
public void eventOccurred(Event e) {
if (e instanceof TransportEnabledEvent) {
TransportEnabledEvent t = (TransportEnabledEvent) e;
if (e instanceof TransportActiveEvent) {
TransportActiveEvent t = (TransportActiveEvent) e;
if (t.getTransportId().equals(TorConstants.ID))
ioExecutor.execute(this::sendReports);
}

View File

@@ -11,13 +11,17 @@ import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent;
import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.Priority;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncRecordWriter;
import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.Versions;
import org.briarproject.bramble.api.sync.event.CloseSyncConnectionsEvent;
import org.briarproject.bramble.api.sync.event.GroupVisibilityUpdatedEvent;
import org.briarproject.bramble.api.sync.event.MessageRequestedEvent;
import org.briarproject.bramble.api.sync.event.MessageSharedEvent;
@@ -35,6 +39,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -71,9 +76,12 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
private final EventBus eventBus;
private final Clock clock;
private final ContactId contactId;
private final TransportId transportId;
private final int maxLatency, maxIdleTime;
private final StreamWriter streamWriter;
private final SyncRecordWriter recordWriter;
@Nullable
private final Priority priority;
private final BlockingQueue<ThrowingRunnable<IOException>> writerTasks;
private final AtomicBoolean generateAckQueued = new AtomicBoolean(false);
@@ -86,18 +94,21 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
private volatile boolean interrupted = false;
DuplexOutgoingSession(DatabaseComponent db, Executor dbExecutor,
EventBus eventBus, Clock clock, ContactId contactId, int maxLatency,
int maxIdleTime, StreamWriter streamWriter,
SyncRecordWriter recordWriter) {
EventBus eventBus, Clock clock, ContactId contactId,
TransportId transportId, int maxLatency, int maxIdleTime,
StreamWriter streamWriter, SyncRecordWriter recordWriter,
@Nullable Priority priority) {
this.db = db;
this.dbExecutor = dbExecutor;
this.eventBus = eventBus;
this.clock = clock;
this.contactId = contactId;
this.transportId = transportId;
this.maxLatency = maxLatency;
this.maxIdleTime = maxIdleTime;
this.streamWriter = streamWriter;
this.recordWriter = recordWriter;
this.priority = priority;
writerTasks = new LinkedBlockingQueue<>();
}
@@ -108,6 +119,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
try {
// Send our supported protocol versions
recordWriter.writeVersions(new Versions(SUPPORTED_VERSIONS));
// Send our connection priority, if this is an outgoing connection
if (priority != null) recordWriter.writePriority(priority);
// Start a query for each type of record
generateAck();
generateBatch();
@@ -223,6 +236,12 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
} else if (e instanceof LifecycleEvent) {
LifecycleEvent l = (LifecycleEvent) e;
if (l.getLifecycleState() == STOPPING) interrupt();
} else if (e instanceof CloseSyncConnectionsEvent) {
CloseSyncConnectionsEvent c = (CloseSyncConnectionsEvent) e;
if (c.getTransportId().equals(transportId)) interrupt();
} else if (e instanceof TransportInactiveEvent) {
TransportInactiveEvent t = (TransportInactiveEvent) e;
if (t.getTransportId().equals(transportId)) interrupt();
}
}

View File

@@ -15,6 +15,8 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.Priority;
import org.briarproject.bramble.api.sync.PriorityHandler;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncRecordReader;
import org.briarproject.bramble.api.sync.SyncSession;
@@ -47,17 +49,19 @@ class IncomingSession implements SyncSession, EventListener {
private final EventBus eventBus;
private final ContactId contactId;
private final SyncRecordReader recordReader;
private final PriorityHandler priorityHandler;
private volatile boolean interrupted = false;
IncomingSession(DatabaseComponent db, Executor dbExecutor,
EventBus eventBus, ContactId contactId,
SyncRecordReader recordReader) {
SyncRecordReader recordReader, PriorityHandler priorityHandler) {
this.db = db;
this.dbExecutor = dbExecutor;
this.eventBus = eventBus;
this.contactId = contactId;
this.recordReader = recordReader;
this.priorityHandler = priorityHandler;
}
@IoExecutor
@@ -86,6 +90,9 @@ class IncomingSession implements SyncSession, EventListener {
} else if (recordReader.hasVersions()) {
Versions v = recordReader.readVersions();
dbExecutor.execute(new ReceiveVersions(v));
} else if (recordReader.hasPriority()) {
Priority p = recordReader.readPriority();
priorityHandler.handle(p);
} else {
// unknown records are ignored in RecordReader#eof()
throw new FormatException();

View File

@@ -11,11 +11,14 @@ import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent;
import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.SyncRecordWriter;
import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.Versions;
import org.briarproject.bramble.api.sync.event.CloseSyncConnectionsEvent;
import org.briarproject.bramble.api.transport.StreamWriter;
import java.io.IOException;
@@ -56,6 +59,7 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
private final Executor dbExecutor;
private final EventBus eventBus;
private final ContactId contactId;
private final TransportId transportId;
private final int maxLatency;
private final StreamWriter streamWriter;
private final SyncRecordWriter recordWriter;
@@ -65,12 +69,14 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
private volatile boolean interrupted = false;
SimplexOutgoingSession(DatabaseComponent db, Executor dbExecutor,
EventBus eventBus, ContactId contactId, int maxLatency,
StreamWriter streamWriter, SyncRecordWriter recordWriter) {
EventBus eventBus, ContactId contactId, TransportId transportId,
int maxLatency, StreamWriter streamWriter,
SyncRecordWriter recordWriter) {
this.db = db;
this.dbExecutor = dbExecutor;
this.eventBus = eventBus;
this.contactId = contactId;
this.transportId = transportId;
this.maxLatency = maxLatency;
this.streamWriter = streamWriter;
this.recordWriter = recordWriter;
@@ -123,6 +129,12 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
} else if (e instanceof LifecycleEvent) {
LifecycleEvent l = (LifecycleEvent) e;
if (l.getLifecycleState() == STOPPING) interrupt();
} else if (e instanceof CloseSyncConnectionsEvent) {
CloseSyncConnectionsEvent c = (CloseSyncConnectionsEvent) e;
if (c.getTransportId().equals(transportId)) interrupt();
} else if (e instanceof TransportInactiveEvent) {
TransportInactiveEvent t = (TransportInactiveEvent) e;
if (t.getTransportId().equals(transportId)) interrupt();
}
}

View File

@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.Priority;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncRecordReader;
import org.briarproject.bramble.api.sync.Versions;
@@ -26,10 +27,12 @@ import javax.annotation.concurrent.NotThreadSafe;
import static org.briarproject.bramble.api.sync.RecordTypes.ACK;
import static org.briarproject.bramble.api.sync.RecordTypes.MESSAGE;
import static org.briarproject.bramble.api.sync.RecordTypes.OFFER;
import static org.briarproject.bramble.api.sync.RecordTypes.PRIORITY;
import static org.briarproject.bramble.api.sync.RecordTypes.REQUEST;
import static org.briarproject.bramble.api.sync.RecordTypes.VERSIONS;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_SUPPORTED_VERSIONS;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.PRIORITY_NONCE_BYTES;
import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
@NotThreadSafe
@@ -48,7 +51,7 @@ class SyncRecordReaderImpl implements SyncRecordReader {
private static boolean isKnownRecordType(byte type) {
return type == ACK || type == MESSAGE || type == OFFER ||
type == REQUEST || type == VERSIONS;
type == REQUEST || type == VERSIONS || type == PRIORITY;
}
private final MessageFactory messageFactory;
@@ -174,4 +177,23 @@ class SyncRecordReaderImpl implements SyncRecordReader {
nextRecord = null;
return supported;
}
@Override
public boolean hasPriority() throws IOException {
return !eof() && getNextRecordType() == PRIORITY;
}
@Override
public Priority readPriority() throws IOException {
if (!hasPriority()) throw new FormatException();
return new Priority(readNonce());
}
private byte[] readNonce() throws IOException {
if (nextRecord == null) throw new AssertionError();
byte[] payload = nextRecord.getPayload();
if (payload.length != PRIORITY_NONCE_BYTES) throw new FormatException();
nextRecord = null;
return payload;
}
}

View File

@@ -8,6 +8,7 @@ import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.Priority;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncRecordWriter;
import org.briarproject.bramble.api.sync.Versions;
@@ -20,6 +21,7 @@ import javax.annotation.concurrent.NotThreadSafe;
import static org.briarproject.bramble.api.sync.RecordTypes.ACK;
import static org.briarproject.bramble.api.sync.RecordTypes.MESSAGE;
import static org.briarproject.bramble.api.sync.RecordTypes.OFFER;
import static org.briarproject.bramble.api.sync.RecordTypes.PRIORITY;
import static org.briarproject.bramble.api.sync.RecordTypes.REQUEST;
import static org.briarproject.bramble.api.sync.RecordTypes.VERSIONS;
import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
@@ -73,6 +75,12 @@ class SyncRecordWriterImpl implements SyncRecordWriter {
writeRecord(VERSIONS);
}
@Override
public void writePriority(Priority p) throws IOException {
writer.writeRecord(
new Record(PROTOCOL_VERSION, PRIORITY, p.getNonce()));
}
@Override
public void flush() throws IOException {
writer.flush();

View File

@@ -5,6 +5,9 @@ import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.sync.Priority;
import org.briarproject.bramble.api.sync.PriorityHandler;
import org.briarproject.bramble.api.sync.SyncRecordReader;
import org.briarproject.bramble.api.sync.SyncRecordReaderFactory;
import org.briarproject.bramble.api.sync.SyncRecordWriter;
@@ -18,6 +21,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@@ -46,29 +50,32 @@ class SyncSessionFactoryImpl implements SyncSessionFactory {
}
@Override
public SyncSession createIncomingSession(ContactId c, InputStream in) {
public SyncSession createIncomingSession(ContactId c, InputStream in,
PriorityHandler handler) {
SyncRecordReader recordReader =
recordReaderFactory.createRecordReader(in);
return new IncomingSession(db, dbExecutor, eventBus, c, recordReader);
return new IncomingSession(db, dbExecutor, eventBus, c, recordReader,
handler);
}
@Override
public SyncSession createSimplexOutgoingSession(ContactId c,
public SyncSession createSimplexOutgoingSession(ContactId c, TransportId t,
int maxLatency, StreamWriter streamWriter) {
OutputStream out = streamWriter.getOutputStream();
SyncRecordWriter recordWriter =
recordWriterFactory.createRecordWriter(out);
return new SimplexOutgoingSession(db, dbExecutor, eventBus, c,
return new SimplexOutgoingSession(db, dbExecutor, eventBus, c, t,
maxLatency, streamWriter, recordWriter);
}
@Override
public SyncSession createDuplexOutgoingSession(ContactId c, int maxLatency,
int maxIdleTime, StreamWriter streamWriter) {
public SyncSession createDuplexOutgoingSession(ContactId c, TransportId t,
int maxLatency, int maxIdleTime, StreamWriter streamWriter,
@Nullable Priority priority) {
OutputStream out = streamWriter.getOutputStream();
SyncRecordWriter recordWriter =
recordWriterFactory.createRecordWriter(out);
return new DuplexOutgoingSession(db, dbExecutor, eventBus, clock, c,
maxLatency, maxIdleTime, streamWriter, recordWriter);
return new DuplexOutgoingSession(db, dbExecutor, eventBus, clock, c, t,
maxLatency, maxIdleTime, streamWriter, recordWriter, priority);
}
}

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.account;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.KeyStrengthener;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseConfig;
@@ -19,12 +20,15 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import javax.annotation.Nullable;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
import static org.briarproject.bramble.test.TestUtils.getIdentity;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
@@ -35,6 +39,7 @@ import static org.briarproject.bramble.util.StringUtils.toHexString;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
public class AccountManagerImplTest extends BrambleMockTestCase {
@@ -83,8 +88,13 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
}
@Test
public void testSignInReturnsFalseIfDbKeyCannotBeLoaded() {
assertFalse(accountManager.signIn(password));
public void testSignInThrowsExceptionIfDbKeyCannotBeLoaded() {
try {
accountManager.signIn(password);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_CIPHERTEXT, expected.getDecryptionResult());
}
assertFalse(accountManager.hasDatabaseKey());
assertFalse(keyFile.exists());
@@ -92,11 +102,11 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
}
@Test
public void testSignInReturnsFalseIfPasswordIsWrong() throws Exception {
public void testSignInThrowsExceptionIfPasswordIsWrong() throws Exception {
context.checking(new Expectations() {{
oneOf(crypto).decryptWithPassword(encryptedKey, password,
keyStrengthener);
will(returnValue(null));
will(throwException(new DecryptionException(INVALID_PASSWORD)));
}});
storeDatabaseKey(keyFile, encryptedKeyHex);
@@ -105,7 +115,12 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertFalse(accountManager.signIn(password));
try {
accountManager.signIn(password);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_PASSWORD, expected.getDecryptionResult());
}
assertFalse(accountManager.hasDatabaseKey());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
@@ -128,7 +143,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertTrue(accountManager.signIn(password));
accountManager.signIn(password);
assertTrue(accountManager.hasDatabaseKey());
SecretKey decrypted = accountManager.getDatabaseKey();
assertNotNull(decrypted);
@@ -157,7 +172,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertTrue(accountManager.signIn(password));
accountManager.signIn(password);
assertTrue(accountManager.hasDatabaseKey());
SecretKey decrypted = accountManager.getDatabaseKey();
assertNotNull(decrypted);
@@ -239,55 +254,6 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
assertFalse(keyBackupFile.exists());
}
@Test
public void testAccountExistsReturnsFalseIfDbDirectoryDoesNotExist()
throws Exception {
storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertFalse(dbDir.exists());
assertFalse(accountManager.accountExists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertFalse(dbDir.exists());
}
@Test
public void testAccountExistsReturnsFalseIfDbDirectoryIsNotDirectory()
throws Exception {
storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertTrue(dbDir.createNewFile());
assertFalse(dbDir.isDirectory());
assertFalse(accountManager.accountExists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertTrue(dbDir.exists());
assertFalse(dbDir.isDirectory());
}
@Test
public void testAccountExistsReturnsTrueIfDbDirectoryIsDirectory()
throws Exception {
storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertTrue(dbDir.mkdirs());
assertTrue(dbDir.isDirectory());
assertTrue(accountManager.accountExists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertTrue(dbDir.exists());
assertTrue(dbDir.isDirectory());
}
@Test
public void testCreateAccountStoresDbKey() throws Exception {
context.checking(new Expectations() {{
@@ -315,26 +281,36 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
}
@Test
public void testChangePasswordReturnsFalseIfDbKeyCannotBeLoaded() {
assertFalse(accountManager.changePassword(password, newPassword));
public void testChangePasswordThrowsExceptionIfDbKeyCannotBeLoaded() {
try {
accountManager.changePassword(password, newPassword);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_CIPHERTEXT, expected.getDecryptionResult());
}
assertFalse(keyFile.exists());
assertFalse(keyBackupFile.exists());
}
@Test
public void testChangePasswordReturnsFalseIfPasswordIsWrong()
public void testChangePasswordThrowsExceptionIfPasswordIsWrong()
throws Exception {
context.checking(new Expectations() {{
oneOf(crypto).decryptWithPassword(encryptedKey, password,
keyStrengthener);
will(returnValue(null));
will(throwException(new DecryptionException(INVALID_PASSWORD)));
}});
storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertFalse(accountManager.changePassword(password, newPassword));
try {
accountManager.changePassword(password, newPassword);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_PASSWORD, expected.getDecryptionResult());
}
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
@@ -357,7 +333,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertTrue(accountManager.changePassword(password, newPassword));
accountManager.changePassword(password, newPassword);
assertEquals(newEncryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(newEncryptedKeyHex, loadDatabaseKey(keyBackupFile));
@@ -366,7 +342,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
private void storeDatabaseKey(File f, String hex) throws IOException {
f.getParentFile().mkdirs();
FileOutputStream out = new FileOutputStream(f);
out.write(hex.getBytes("UTF-8"));
out.write(hex.getBytes(Charset.forName("UTF-8")));
out.flush();
out.close();
}
@@ -374,7 +350,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
@Nullable
private String loadDatabaseKey(File f) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(
new FileInputStream(f), "UTF-8"));
new FileInputStream(f), Charset.forName("UTF-8")));
String hex = reader.readLine();
reader.close();
return hex;

View File

@@ -0,0 +1,611 @@
package org.briarproject.bramble.connection;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.connection.InterruptibleConnection;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.plugin.PluginConfig;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionClosedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionOpenedEvent;
import org.briarproject.bramble.api.sync.Priority;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.jmock.Expectations;
import org.junit.Test;
import java.util.Collection;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.briarproject.bramble.test.TestUtils.getContactId;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
import static org.briarproject.bramble.util.StringUtils.fromHexString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class ConnectionRegistryImplTest extends BrambleMockTestCase {
private final EventBus eventBus = context.mock(EventBus.class);
private final PluginConfig pluginConfig = context.mock(PluginConfig.class);
private final InterruptibleConnection conn1 =
context.mock(InterruptibleConnection.class, "conn1");
private final InterruptibleConnection conn2 =
context.mock(InterruptibleConnection.class, "conn2");
private final InterruptibleConnection conn3 =
context.mock(InterruptibleConnection.class, "conn3");
private final ContactId contactId1 = getContactId();
private final ContactId contactId2 = getContactId();
private final TransportId transportId1 = getTransportId();
private final TransportId transportId2 = getTransportId();
private final TransportId transportId3 = getTransportId();
private final PendingContactId pendingContactId =
new PendingContactId(getRandomId());
private final Priority low =
new Priority(fromHexString("00000000000000000000000000000000"));
private final Priority high =
new Priority(fromHexString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"));
@Test
public void testRegisterMultipleConnections() {
context.checking(new Expectations() {{
allowing(pluginConfig).getTransportPreferences();
will(returnValue(emptyMap()));
}});
ConnectionRegistry c =
new ConnectionRegistryImpl(eventBus, pluginConfig);
// The registry should be empty
assertEquals(emptyList(), c.getConnectedContacts(transportId1));
assertEquals(emptyList(), c.getConnectedOrBetterContacts(transportId1));
assertEquals(emptyList(), c.getConnectedContacts(transportId2));
assertEquals(emptyList(), c.getConnectedOrBetterContacts(transportId2));
assertEquals(emptyList(), c.getConnectedContacts(transportId3));
assertEquals(emptyList(), c.getConnectedOrBetterContacts(transportId3));
assertFalse(c.isConnected(contactId1));
assertFalse(c.isConnected(contactId1, transportId1));
assertFalse(c.isConnected(contactId1, transportId2));
assertFalse(c.isConnected(contactId1, transportId3));
// Check that a registered connection shows up - this should
// broadcast a ConnectionOpenedEvent and a ContactConnectedEvent
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
oneOf(eventBus).broadcast(with(any(ContactConnectedEvent.class)));
}});
c.registerIncomingConnection(contactId1, transportId1, conn1);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
assertTrue(c.isConnected(contactId1));
assertTrue(c.isConnected(contactId1, transportId1));
// Register another connection with the same contact and transport -
// this should broadcast a ConnectionOpenedEvent and lookup should be
// unaffected
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
}});
c.registerIncomingConnection(contactId1, transportId1, conn2);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
assertTrue(c.isConnected(contactId1));
assertTrue(c.isConnected(contactId1, transportId1));
// Unregister one of the connections - this should broadcast a
// ConnectionClosedEvent and lookup should be unaffected
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionClosedEvent.class)));
}});
c.unregisterConnection(contactId1, transportId1, conn1, true, false);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
assertTrue(c.isConnected(contactId1));
assertTrue(c.isConnected(contactId1, transportId1));
// Unregister the other connection - this should broadcast a
// ConnectionClosedEvent and a ContactDisconnectedEvent
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionClosedEvent.class)));
oneOf(eventBus).broadcast(with(any(
ContactDisconnectedEvent.class)));
}});
c.unregisterConnection(contactId1, transportId1, conn2, true, false);
context.assertIsSatisfied();
assertEquals(emptyList(), c.getConnectedContacts(transportId1));
assertEquals(emptyList(), c.getConnectedOrBetterContacts(transportId1));
assertFalse(c.isConnected(contactId1));
assertFalse(c.isConnected(contactId1, transportId1));
// Try to unregister the connection again - exception should be thrown
try {
c.unregisterConnection(contactId1, transportId1, conn2,
true, false);
fail();
} catch (IllegalArgumentException expected) {
// Expected
}
}
@Test
public void testRegisterMultipleContacts() {
context.checking(new Expectations() {{
allowing(pluginConfig).getTransportPreferences();
will(returnValue(emptyMap()));
}});
ConnectionRegistry c =
new ConnectionRegistryImpl(eventBus, pluginConfig);
// Register two contacts with one transport, then one of the contacts
// with a second transport - this should broadcast three
// ConnectionOpenedEvents and two ContactConnectedEvents
context.checking(new Expectations() {{
exactly(3).of(eventBus).broadcast(with(any(
ConnectionOpenedEvent.class)));
exactly(2).of(eventBus).broadcast(with(any(
ContactConnectedEvent.class)));
}});
c.registerIncomingConnection(contactId1, transportId1, conn1);
c.registerIncomingConnection(contactId2, transportId1, conn2);
c.registerIncomingConnection(contactId2, transportId2, conn3);
context.assertIsSatisfied();
assertTrue(c.isConnected(contactId1));
assertTrue(c.isConnected(contactId2));
assertTrue(c.isConnected(contactId1, transportId1));
assertFalse(c.isConnected(contactId1, transportId2));
assertTrue(c.isConnected(contactId2, transportId1));
assertTrue(c.isConnected(contactId2, transportId2));
Collection<ContactId> connected = c.getConnectedContacts(transportId1);
assertEquals(2, connected.size());
assertTrue(connected.contains(contactId1));
assertTrue(connected.contains(contactId2));
connected = c.getConnectedOrBetterContacts(transportId1);
assertEquals(2, connected.size());
assertTrue(connected.contains(contactId1));
assertTrue(connected.contains(contactId2));
assertEquals(singletonList(contactId2),
c.getConnectedContacts(transportId2));
assertEquals(singletonList(contactId2),
c.getConnectedOrBetterContacts(transportId2));
}
@Test
public void testConnectionsAreNotInterruptedUnlessPriorityIsSet() {
// Prefer transport 2 to transport 1
context.checking(new Expectations() {{
allowing(pluginConfig).getTransportPreferences();
will(returnValue(
singletonMap(transportId1, singletonList(transportId2))));
}});
ConnectionRegistry c =
new ConnectionRegistryImpl(eventBus, pluginConfig);
// Connect via transport 1 (worse than 2) with no priority set
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
oneOf(eventBus).broadcast(with(any(ContactConnectedEvent.class)));
}});
c.registerIncomingConnection(contactId1, transportId1, conn1);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
assertEquals(emptyList(), c.getConnectedContacts(transportId2));
assertEquals(emptyList(), c.getConnectedOrBetterContacts(transportId2));
// Connect via transport 2 (better than 1) and set priority to high -
// the old connection should not be interrupted, despite using a worse
// transport, to remain compatible with old peers
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
}});
c.registerOutgoingConnection(contactId1, transportId2, conn2, high);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId2));
// Connect via transport 3 (no preference) and set priority to high -
// again, no interruptions are expected
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
}});
c.registerOutgoingConnection(contactId1, transportId3, conn3, high);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId3));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId3));
}
@Test
public void testNewConnectionIsInterruptedIfOldConnectionUsesBetterTransport() {
// Prefer transport 1 to transport 2
context.checking(new Expectations() {{
allowing(pluginConfig).getTransportPreferences();
will(returnValue(
singletonMap(transportId2, singletonList(transportId1))));
}});
ConnectionRegistry c =
new ConnectionRegistryImpl(eventBus, pluginConfig);
// Connect via transport 1 (better than 2) and set priority to low
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
oneOf(eventBus).broadcast(with(any(ContactConnectedEvent.class)));
}});
c.registerOutgoingConnection(contactId1, transportId1, conn1, low);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
// The contact is not connected via transport 2 but is connected via a
// better transport
assertEquals(emptyList(), c.getConnectedContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId2));
// Connect via transport 2 (worse than 1) and set priority to high -
// the new connection should be interrupted because it uses a worse
// transport
context.checking(new Expectations() {{
oneOf(conn2).interruptOutgoingSession();
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
}});
c.registerOutgoingConnection(contactId1, transportId2, conn2, high);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId2));
// Connect via transport 3 (no preference) and set priority to low -
// no further interruptions
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
}});
c.registerOutgoingConnection(contactId1, transportId3, conn3, low);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId3));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId3));
// Unregister the interrupted connection (transport 2)
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionClosedEvent.class)));
}});
c.unregisterConnection(contactId1, transportId2, conn2, true, false);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
// The contact is not connected via transport 2 but is connected via a
// better transport
assertEquals(emptyList(), c.getConnectedContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId3));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId3));
}
@Test
public void testOldConnectionIsInterruptedIfNewConnectionUsesBetterTransport() {
// Prefer transport 2 to transport 1
context.checking(new Expectations() {{
allowing(pluginConfig).getTransportPreferences();
will(returnValue(
singletonMap(transportId1, singletonList(transportId2))));
}});
ConnectionRegistry c =
new ConnectionRegistryImpl(eventBus, pluginConfig);
// Connect via transport 1 (worse than 2) and set priority to high
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
oneOf(eventBus).broadcast(with(any(ContactConnectedEvent.class)));
}});
c.registerOutgoingConnection(contactId1, transportId1, conn1, high);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
assertEquals(emptyList(), c.getConnectedContacts(transportId2));
assertEquals(emptyList(), c.getConnectedOrBetterContacts(transportId2));
// Connect via transport 2 (better than 1) and set priority to low -
// the old connection should be interrupted because it uses a worse
// transport
context.checking(new Expectations() {{
oneOf(conn1).interruptOutgoingSession();
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
}});
c.registerOutgoingConnection(contactId1, transportId2, conn2, low);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId2));
// Connect via transport 3 (no preference) and set priority to high -
// no further interruptions
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
}});
c.registerOutgoingConnection(contactId1, transportId3, conn3, high);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId3));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId3));
// Unregister the interrupted connection (transport 1)
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionClosedEvent.class)));
}});
c.unregisterConnection(contactId1, transportId1, conn1, true, false);
context.assertIsSatisfied();
// The contact is not connected via transport 1 but is connected via a
// better transport
assertEquals(emptyList(), c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId2));
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId3));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId3));
}
@Test
public void testNewConnectionIsInterruptedIfOldConnectionHasHigherPriority() {
context.checking(new Expectations() {{
allowing(pluginConfig).getTransportPreferences();
will(returnValue(emptyMap()));
}});
ConnectionRegistry c =
new ConnectionRegistryImpl(eventBus, pluginConfig);
// Register a connection with high priority
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
oneOf(eventBus).broadcast(with(any(ContactConnectedEvent.class)));
}});
c.registerOutgoingConnection(contactId1, transportId1, conn1, high);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
// Register another connection via the same transport (no priority yet)
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
}});
c.registerIncomingConnection(contactId1, transportId1, conn2);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
// Set the priority of the second connection to low - the second
// connection should be interrupted
context.checking(new Expectations() {{
oneOf(conn2).interruptOutgoingSession();
}});
c.setPriority(contactId1, transportId1, conn2, low);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
// Register a third connection with low priority - it should also be
// interrupted
context.checking(new Expectations() {{
oneOf(conn3).interruptOutgoingSession();
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
}});
c.registerOutgoingConnection(contactId1, transportId1, conn3, low);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
}
@Test
public void testOldConnectionIsInterruptedIfNewConnectionHasHigherPriority() {
context.checking(new Expectations() {{
allowing(pluginConfig).getTransportPreferences();
will(returnValue(emptyMap()));
}});
ConnectionRegistry c =
new ConnectionRegistryImpl(eventBus, pluginConfig);
// Register a connection with low priority
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
oneOf(eventBus).broadcast(with(any(ContactConnectedEvent.class)));
}});
c.registerOutgoingConnection(contactId1, transportId1, conn1, low);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
// Register another connection via the same transport (no priority yet)
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
}});
c.registerIncomingConnection(contactId1, transportId1, conn2);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
// Set the priority of the second connection to high - the first
// connection should be interrupted
context.checking(new Expectations() {{
oneOf(conn1).interruptOutgoingSession();
}});
c.setPriority(contactId1, transportId1, conn2, high);
context.assertIsSatisfied();
assertEquals(singletonList(contactId1),
c.getConnectedContacts(transportId1));
assertEquals(singletonList(contactId1),
c.getConnectedOrBetterContacts(transportId1));
}
@Test
public void testRegisterAndUnregisterPendingContacts() {
context.checking(new Expectations() {{
allowing(pluginConfig).getTransportPreferences();
will(returnValue(emptyMap()));
}});
ConnectionRegistry c =
new ConnectionRegistryImpl(eventBus, pluginConfig);
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(
RendezvousConnectionOpenedEvent.class)));
}});
assertTrue(c.registerConnection(pendingContactId));
assertFalse(c.registerConnection(pendingContactId)); // Redundant
context.assertIsSatisfied();
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(
RendezvousConnectionClosedEvent.class)));
}});
c.unregisterConnection(pendingContactId, true);
context.assertIsSatisfied();
try {
c.unregisterConnection(pendingContactId, true);
fail();
} catch (IllegalArgumentException expected) {
// Expected
}
}
}

View File

@@ -2,13 +2,13 @@ package org.briarproject.bramble.contact;
import org.briarproject.bramble.BrambleCoreEagerSingletons;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.contact.ContactExchangeManager;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.plugin.ConnectionManager;
import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
import java.util.concurrent.Executor;

View File

@@ -1,25 +1,35 @@
package org.briarproject.bramble.crypto;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.KeyStrengthener;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.system.SystemClock;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.TestSecureRandomProvider;
import org.briarproject.bramble.test.TestUtils;
import org.jmock.Expectations;
import org.junit.Test;
import java.util.Random;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD;
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class PasswordBasedEncryptionTest extends BrambleTestCase {
public class PasswordBasedEncryptionTest extends BrambleMockTestCase {
private final KeyStrengthener keyStrengthener =
context.mock(KeyStrengthener.class);
private final CryptoComponentImpl crypto =
new CryptoComponentImpl(new TestSecureRandomProvider(),
new ScryptKdf(new SystemClock()));
@Test
public void testEncryptionAndDecryption() {
byte[] input = TestUtils.getRandomBytes(1234);
public void testEncryptionAndDecryption() throws Exception {
byte[] input = getRandomBytes(1234);
String password = "password";
byte[] ciphertext = crypto.encryptWithPassword(input, password, null);
byte[] output = crypto.decryptWithPassword(ciphertext, password, null);
@@ -27,14 +37,80 @@ public class PasswordBasedEncryptionTest extends BrambleTestCase {
}
@Test
public void testInvalidCiphertextReturnsNull() {
byte[] input = TestUtils.getRandomBytes(1234);
public void testInvalidFormatVersionThrowsException() {
byte[] input = getRandomBytes(1234);
String password = "password";
byte[] ciphertext = crypto.encryptWithPassword(input, password, null);
// Modify the ciphertext
int position = new Random().nextInt(ciphertext.length);
ciphertext[position] = (byte) (ciphertext[position] ^ 0xFF);
byte[] output = crypto.decryptWithPassword(ciphertext, password, null);
assertNull(output);
// Modify the format version
ciphertext[0] ^= (byte) 0xFF;
try {
crypto.decryptWithPassword(ciphertext, password, null);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_CIPHERTEXT, expected.getDecryptionResult());
}
}
@Test
public void testInvalidPasswordThrowsException() {
byte[] input = getRandomBytes(1234);
byte[] ciphertext = crypto.encryptWithPassword(input, "password", null);
// Try to decrypt with the wrong password
try {
crypto.decryptWithPassword(ciphertext, "wrong", null);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_PASSWORD, expected.getDecryptionResult());
}
}
@Test
public void testMissingKeyStrengthenerThrowsException() {
SecretKey strengthened = getSecretKey();
context.checking(new Expectations() {{
oneOf(keyStrengthener).strengthenKey(with(any(SecretKey.class)));
will(returnValue(strengthened));
}});
// Use the key strengthener during encryption
byte[] input = getRandomBytes(1234);
String password = "password";
byte[] ciphertext =
crypto.encryptWithPassword(input, password, keyStrengthener);
// The key strengthener is missing during decryption
try {
crypto.decryptWithPassword(ciphertext, password, null);
fail();
} catch (DecryptionException expected) {
assertEquals(KEY_STRENGTHENER_ERROR, expected.getDecryptionResult());
}
}
@Test
public void testKeyStrengthenerFailureThrowsException() {
SecretKey strengthened = getSecretKey();
context.checking(new Expectations() {{
oneOf(keyStrengthener).strengthenKey(with(any(SecretKey.class)));
will(returnValue(strengthened));
oneOf(keyStrengthener).isInitialised();
will(returnValue(false));
}});
// Use the key strengthener during encryption
byte[] input = getRandomBytes(1234);
String password = "password";
byte[] ciphertext =
crypto.encryptWithPassword(input, password, keyStrengthener);
// The key strengthener fails during decryption
try {
crypto.decryptWithPassword(ciphertext, password, keyStrengthener);
fail();
} catch (DecryptionException expected) {
assertEquals(KEY_STRENGTHENER_ERROR, expected.getDecryptionResult());
}
}
}

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