Compare commits

..

259 Commits

Author SHA1 Message Date
akwizgran
7536f16c61 Bump version numbers for 1.4.7 release. 2022-04-20 16:35:07 +01:00
akwizgran
ab628c1921 Update translations. 2022-04-20 16:14:58 +01:00
akwizgran
85e53479f2 Merge branch '2172-mailbox-status-ui' into 'master'
Improve MailboxStatusFragment and record check failures as well

Closes #2172

See merge request briar/briar!1632
2022-04-20 15:04:31 +00:00
akwizgran
116ee97056 Merge branch '1980-catch-security-exceptions-from-connectivity-manager' into 'master'
Catch SecurityExceptions from all ConnectivityManager calls

Closes #1980

See merge request briar/briar!1634
2022-04-20 14:51:57 +00:00
Torsten Grote
78938f1ac6 Merge branch '2280-check-lifecycle-before-recreating-removable-drive-tasks' into 'master'
Check lifecycle state before recreating removable drive tasks

Closes #2280

See merge request briar/briar!1629
2022-04-20 13:37:24 +00:00
akwizgran
afff66eaff Don't assume that non-null WifiInfo means we're connected to wifi. 2022-04-20 12:42:35 +01:00
akwizgran
a8624cd507 Guess connectivity when ConnectivityManager is broken. 2022-04-19 11:27:40 +01:00
akwizgran
e7fc37d81e Catch SecurityExceptions from all ConnectivityManager calls.
This issue occurs on Android 11 and no fix is expected. When the issue occurs, Tor connectivity and outgoing LAN connectivity will be broken until the app is restarted.
2022-04-19 11:03:08 +01:00
Torsten Grote
7bd220f18d Merge branch 'clear-glide-cache-under-more-circumstances' into 'master'
Clear the Glide cache in response to a wider range of warnings

See merge request briar/briar!1633
2022-04-18 16:46:31 +00:00
Torsten Grote
dea05c85a2 Improve MailboxStatusFragment and record check failures as well 2022-04-18 10:16:38 -03:00
Torsten Grote
174ca3cfb8 Merge branch '2214-catch-activity-not-found-exception-when-saving-image' into 'master'
Catch ActivityNotFoundException when saving image

Closes #2214

See merge request briar/briar!1627
2022-04-18 12:44:20 +00:00
akwizgran
961af66c8e Use new onSaveImageError() method for readability. 2022-04-18 13:33:09 +01:00
Torsten Grote
a86ea454d0 Merge branch '2143-rethrow-security-exceptions-when-opening-images' into 'master'
Rethrow SecurityExceptions when opening images

Closes #2143

See merge request briar/briar!1626
2022-04-18 12:12:20 +00:00
Torsten Grote
a7877bf7ee Merge branch '2273-rethrow-security-exceptions-for-removable-drives' into 'master'
Rethrow SecurityExceptions when opening files on removable drives

Closes #2273

See merge request briar/briar!1625
2022-04-18 12:11:42 +00:00
Torsten Grote
62ae0f745b Merge branch '2306-task-scheduler-zero-delay' into 'master'
Fixe race condition in AndroidTaskScheduler

Closes #2306

See merge request briar/briar!1624
2022-04-18 11:57:34 +00:00
Torsten Grote
f83abbe63d Merge branch '2305-increase-tor-connection-timeout' into 'master'
Increase Tor connection timeout to 2 minutes

Closes #2305

See merge request briar/briar!1623
2022-04-18 11:53:57 +00:00
Torsten Grote
e0b6b8435d Merge branch 'update-introduction-onboarding-text' into 'master'
Update introduction onboarding text

See merge request briar/briar!1631
2022-04-18 11:49:41 +00:00
akwizgran
d3c7832245 Update introduction onboarding text.
The old text caused some confusion in user testing because contacts can now add each other remotely.
2022-04-18 11:34:22 +01:00
akwizgran
a043e8b1cf Check lifecycle state before recreating removable drive tasks. 2022-04-17 12:28:26 +01:00
akwizgran
bc013296f6 Catch ActivityNotFoundException when saving image. 2022-04-17 11:59:00 +01:00
akwizgran
c1fabcd46b Rethrow SecurityExceptions when opening images. 2022-04-17 11:51:49 +01:00
akwizgran
3c08e86822 Rethrow SecurityExceptions when opening files on removable drives. 2022-04-17 11:36:16 +01:00
akwizgran
de2c9670d5 Clear the Glide cache in response to a wider range of warnings. 2022-04-17 10:50:59 +01:00
akwizgran
9632754274 Ensure task is added to queue before queue is checked. 2022-04-16 19:32:51 +01:00
akwizgran
b275a0ffff Increase Tor connection timeout to 2 minutes. 2022-04-16 16:07:03 +01:00
akwizgran
74a3f54d28 Merge branch '2172-mailbox-status-ui' into 'master'
Implement status UI for mailbox connection

Closes #2172

See merge request briar/briar!1617
2022-04-14 12:46:28 +00:00
Torsten Grote
edcb234b93 Show OfflineFragment when TorPlugin becomes inactive in mailbox flow 2022-04-12 10:10:09 -03:00
Torsten Grote
dae00c7e4e Show different mailbox status in UI
and show failure status after unsuccessful attempt
2022-04-12 10:01:43 -03:00
Torsten Grote
29b16c4d74 Re-use OfflineFragment when offline in mailbox status screen 2022-04-12 09:35:39 -03:00
Torsten Grote
40d58a9359 Prevent memory leak and crash when refreshing MailboxStatusFragment 2022-04-07 11:00:41 -03:00
Torsten Grote
60a1a4d2d1 Make MailboxManager#checkConnection() blocking and let the UI manage the executor 2022-04-07 10:44:24 -03:00
Torsten Grote
238aeb3abd Merge branch 'extend-timeout-for-pre-release-tests' into 'master'
Extend timeout for pre-release tests

See merge request briar/briar!1618
2022-04-04 11:13:50 +00:00
akwizgran
62c16fad09 Merge branch '2191-reset-retransmission-times-when-contacts-mailbox-props-change' into 'master'
Reset retransmission times when contact's mailbox props change

Closes #2191

See merge request briar/briar!1619
2022-04-04 10:19:02 +00:00
Daniel Lublin
68e57bda0d Reset retransmission times when contact's mailbox props change 2022-04-04 12:01:19 +02:00
akwizgran
0df73dbf0a Extend timeout for pre-release tests. 2022-04-02 08:16:34 +01:00
Torsten Grote
5b648cbd35 Add connection check button to Mailbox status UI
and update the last connection timestamp accordingly
2022-04-01 13:55:11 -03:00
Torsten Grote
5e7891d78a Add checkConnection() to MailboxManager 2022-04-01 13:55:11 -03:00
akwizgran
d5e17c8201 Bump version numbers for 1.4.6 release. 2022-04-01 17:05:12 +01:00
Torsten Grote
d572ae71e7 Merge branch 'more-non-default-bridges' into 'master'
Vanilla bridges

See merge request briar/briar!1611
2022-04-01 16:02:58 +00:00
akwizgran
2e9d9dac84 Update translations. 2022-04-01 16:45:59 +01:00
akwizgran
573817c4c9 Map el to el-GR for Play Store metadata. 2022-04-01 16:44:07 +01:00
Torsten Grote
4f00f39d3f Merge branch 'initialise-mailbox-eager-singletons' into 'master'
Initialise mailbox eager singletons at startup

See merge request briar/briar!1616
2022-04-01 15:30:47 +00:00
akwizgran
c7d3628ecb Update Play Store metadata. 2022-04-01 16:22:46 +01:00
akwizgran
b198bef5f8 Initialise mailbox eager singletons at startup. 2022-04-01 16:02:12 +01:00
Torsten Grote
cff94009a1 Merge branch 'tor-0.4.5' into 'master'
Upgrade to Tor 0.4.5.12-1 and obfs4proxy 0.0.12

See merge request briar/briar!1608
2022-04-01 14:00:50 +00:00
Torsten Grote
44f9f0bbc5 Merge branch 'more-tor-events' into 'master'
Log more Tor events and react to CIRCUIT_NOT_ESTABLISHED

See merge request briar/briar!1605
2022-04-01 13:22:09 +00:00
akwizgran
5fdb43ce9b Merge branch '2192-reset-retransmission-times-when-pairing-mailbox' into 'master'
Reset retransmission times when pairing (new) mailbox

Closes #2192

See merge request briar/briar!1615
2022-04-01 13:08:47 +00:00
Daniel Lublin
725d11d960 Extend test 2022-04-01 14:56:05 +02:00
Daniel Lublin
7cf2c2faa7 Reset retransmission times when pairing (new) mailbox 2022-04-01 14:56:05 +02:00
akwizgran
4b3c26feb6 Merge branch 'fix_openOutputStream' into 'master'
Fix usage of ContentResolver.openOutputStream()

See merge request briar/briar!1607
2022-04-01 12:36:50 +00:00
akwizgran
2fbeb29195 Merge branch 'simplify-and-get-rid-of-contactmanager' into 'master'
Simplify and get rid of injected contactmanager

See merge request briar/briar!1614
2022-04-01 11:18:43 +00:00
akwizgran
5892fba237 Merge branch '2267-broadcast-event-when-recording-own-mailbox-connection-status' into 'master'
Broadcast event when recording connection status of own mailbox

Closes #2267

See merge request briar/briar!1613
2022-04-01 11:17:47 +00:00
akwizgran
cc9f04980a Merge branch 'fix-exception-logging' into 'master'
Don't warn about background exceptions unless one was thrown

See merge request briar/briar!1610
2022-04-01 11:15:33 +00:00
Daniel Lublin
44fb2a5c59 Use db directly, get rid of injected ContactManager 2022-04-01 10:31:52 +02:00
Daniel Lublin
68e534348f Broadcast event when recording connection status of own mailbox 2022-04-01 09:14:50 +02:00
akwizgran
795a8f1e70 Upgrade Tor to 0.4.5.12-2. 2022-03-31 14:20:17 +01:00
akwizgran
bf968b227e Merge branch '2295-broadcast-event-on-contact-mailbox-props-update' into 'master'
Broadcast event when a contact's Mailbox properties are updated

Closes #2295

See merge request briar/briar!1612
2022-03-31 09:24:00 +00:00
Daniel Lublin
8b94dad01f Broadcast event when a contact's Mailbox properties are updated 2022-03-31 09:20:19 +02:00
akwizgran
fa0610fff1 Use vanilla bridges in parallel with obfs4 bridges. 2022-03-30 18:01:28 +01:00
Torsten Grote
1d94db8d60 Merge branch 'compact-db-at-shutdown' into 'master'
Always compact the DB at shutdown

See merge request briar/briar!1609
2022-03-30 16:29:29 +00:00
akwizgran
1d4f450960 Update translations. 2022-03-30 17:03:29 +01:00
akwizgran
7f6b31d36c Don't warn about background exceptions unless one was thrown. 2022-03-30 16:54:11 +01:00
akwizgran
05737d858d Increase number of unreachable bridges allowed. 2022-03-30 14:30:44 +01:00
akwizgran
2c8e2ab6b8 Remove two unreliable non-default bridges. 2022-03-30 14:29:55 +01:00
akwizgran
97f64fb31c Remove the max compaction interval.
We always compact on shutdown, or on startup if we've shut down uncleanly without compacting.
2022-03-30 13:32:45 +01:00
akwizgran
e66152e812 Compact the DB at shutdown, and at startup if not closed cleanly. 2022-03-30 13:32:39 +01:00
akwizgran
101ffa2f08 Merge branch '2181-implement-sync-client-for-mailbox-props' into 'master'
Implement sync client for mailbox properties

Closes #2181

See merge request briar/briar!1591
2022-03-30 12:29:15 +00:00
Daniel Lublin
13eebe393a Rename, clarifying this is not an address; it has no scheme, no .onion 2022-03-30 13:32:28 +02:00
Daniel Lublin
5bc5791ddb Implement the Mailbox property client 2022-03-30 13:32:28 +02:00
akwizgran
a35e9af1de Add method for generating a unique ID, remove equals() methods. 2022-03-30 13:23:59 +02:00
akwizgran
ade89c14c4 Update translations. 2022-03-30 11:28:58 +01:00
akwizgran
16cfb89310 Add more non-default bridges. 2022-03-29 16:47:33 +01:00
akwizgran
78f00863dd Upgrade to obfs4proxy 0.0.12. 2022-03-29 16:39:07 +01:00
akwizgran
bd50a109cd Keep empty directory for unpacking Tor binaries. 2022-03-29 16:19:47 +01:00
akwizgran
38c91aea32 Upgrade Tor to 0.4.5.12-1. 2022-03-29 15:49:40 +01:00
akwizgran
92517ae7c0 Merge branch '2265-replace-eta-with-maxlatency-retransmission-logic' into 'master'
Replace ETA with max latency in retransmission logic

Closes #2265

See merge request briar/briar!1606
2022-03-29 13:12:38 +00:00
Daniel Lublin
dd1c8c8301 Replace ETA with max latency in retransmission logic 2022-03-29 14:57:03 +02:00
Torsten Grote
edc1029e92 Merge branch '2270-huawei-power-management-crash' into 'master'
Catch exception in Huawei power management setup

Closes #2270

See merge request briar/briar!1602
2022-03-29 12:54:16 +00:00
akwizgran
27e9338a12 Merge branch 'exceptions-on-thread-in-bramble-testcases' into 'master'
Exceptions on thread in bramble testcases

See merge request briar/briar!1584
2022-03-29 10:59:25 +00:00
Sebastian Kürten
243df3096a Add logging message when BrambleTestCase detects background thread exception 2022-03-29 09:30:51 +02:00
cketti
50f9718037 Truncate existing file when writing to removable drive 2022-03-29 01:27:01 +02:00
cketti
88c8bd32a5 Truncate existing file when saving image 2022-03-29 01:22:46 +02:00
akwizgran
3e597ceff8 Use a constructor that Animal Sniffer knows about. 2022-03-28 15:09:26 +01:00
akwizgran
3d6972fd73 Fix race condition in IntroductionIntegrationTest. 2022-03-28 14:59:43 +01:00
akwizgran
288f3331ec Include background exception in test failure report. 2022-03-28 14:59:01 +01:00
akwizgran
a14ee55f12 Update translations. 2022-03-21 14:19:02 +00:00
akwizgran
2a85907565 Update translations. 2022-03-21 14:16:34 +00:00
Torsten Grote
1fe7b2f451 Merge branch 'transifex-migration' into 'master'
Migrate to Transifex Go client

See merge request briar/briar!1603
2022-03-21 12:23:01 +00:00
akwizgran
585ceb626b Merge branch 'update-bridge-config' into 'master'
Update circumvention config

See merge request briar/briar!1604
2022-03-15 12:52:22 +00:00
akwizgran
5da782cf18 Turkmenistan needs non-default bridges. 2022-03-15 12:07:59 +00:00
akwizgran
ec6b999d30 Belarus needs non-default bridges. 2022-03-15 12:06:34 +00:00
akwizgran
d8a925a94f Remove Syria and Turkey from list of countries blocking Tor.
https://explorer.ooni.org/chart/circumvention?since=2021-12-13&until=2022-03-13&probe_cc=BY%2CCN%2CEG%2CIR%2CRU%2CSY%2CTR%2CVE
2022-03-15 12:06:34 +00:00
akwizgran
3de4386e63 Log the scrubbed onion address when an HS descriptor is uploaded.
This allows us to distinguish between descriptors for our permanent hidden service and rendezvous hidden services.
2022-03-15 12:04:22 +00:00
akwizgran
8c60787866 Fix inverted logic. 2022-03-15 12:04:22 +00:00
akwizgran
fa8ca8e6cf Log more Tor events and react to CIRCUIT_NOT_ESTABLISHED.
In future we should also react to CLOCK_SKEW and maybe CLOCK_JUMPED.
2022-03-15 12:04:22 +00:00
akwizgran
07814d43de Restore comment to Transifex config. 2022-03-15 11:57:30 +00:00
akwizgran
d80ba0f556 Migrate to Transifex Go client. 2022-03-15 10:35:35 +00:00
akwizgran
d70e1ed32e Merge branch 'make-bridge-test-more-robust' into 'master'
Make bridge test more robust

See merge request briar/briar!1601
2022-03-10 17:15:29 +00:00
akwizgran
eec2c87797 Catch exception in Huawei power management setup. 2022-03-10 12:40:32 +00:00
akwizgran
a256027916 Tolerate up to 4 unreachable bridges per run. 2022-03-10 10:03:06 +00:00
akwizgran
bf0f99277a Remove four consistently failing bridges. 2022-03-10 10:02:00 +00:00
akwizgran
2d62deb2db Increase CI timeout for BridgeTest. 2022-03-09 12:57:20 +00:00
akwizgran
e3682bb331 Make more attempts per bridge to try to get stable results. 2022-03-09 12:54:34 +00:00
akwizgran
6805040ac4 Make BridgeTest more robust by trying each bridge three times. 2022-03-08 22:45:39 +00:00
akwizgran
4198e1f22a Add some default bridges from Tor Browser. 2022-03-08 22:44:47 +00:00
akwizgran
ee11d2a28d Merge branch '2269-use-full-camera-preview-when-scanning-qr-codes' into 'master'
Use whole preview image when decoding QR

Closes #2269

See merge request briar/briar!1600
2022-03-08 15:02:25 +00:00
Daniel Lublin
f3718e496c Use whole preview image when decoding QR
When scanning Mailbox QR, the whole preview is visible on screen, so we
should use it. We choose to never crop the preview, because it was
originally a speed optimization which no longer is needed.
2022-03-08 13:32:57 +01:00
Torsten Grote
414c296abd Merge branch '2272-get-network-interfaces-npe' into 'master'
Catch NPE thrown by NetworkInterface.getNetworkInterfaces()

Closes #2272

See merge request briar/briar!1599
2022-03-07 11:39:44 +00:00
akwizgran
79051439c5 Catch NPE thrown by NetworkInterface.getNetworkInterfaces(). 2022-03-05 13:54:48 +00:00
Sebastian Kürten
32b62d3e30 Allow BrambleTestCase to handle background thread exceptions gracefully during after() 2022-03-04 18:00:16 +01:00
Sebastian Kürten
e3f2a30120 Make BrambleTestCase fail if background thread throws an exception 2022-03-04 18:00:15 +01:00
Sebastian Kürten
58a122ee28 Add test that checks exception handling on background threads 2022-03-04 18:00:09 +01:00
akwizgran
f5f7b3eb51 Merge branch 'master' into 'master'
Changed from share icon to plus icon.

See merge request briar/briar!1596
2022-03-02 12:05:58 +00:00
FlyingP1g FlyingP1g
098128c8a8 Changed from share icon to plus icon. 2022-03-02 12:05:57 +00:00
Torsten Grote
27d566df7a Merge branch 'memory-stats' into 'master'
Collect some more memory stats for help with debugging OOM errors

See merge request briar/briar!1595
2022-02-28 14:58:07 +00:00
akwizgran
9469825f4f Collect some more memory stats for help with debugging OOM errors. 2022-02-28 13:58:15 +00:00
Torsten Grote
5ce90422c6 Merge branch 'update-play-store-metadata' into 'master'
Update Play Store metadata

See merge request briar/briar!1593
2022-02-28 12:39:17 +00:00
akwizgran
256662e094 Merge branch 'fix-screenshot-tests' into 'master'
Fix screenshot PromoVideoTest

See merge request briar/briar!1594
2022-02-25 18:09:37 +00:00
Torsten Grote
dc7f1e0c86 Fix screenshot PromoVideoTest
Needs also to upgrade androidTestImplementation dependencies, but this makes normal instrumentation tests fail due to method limit.
So those are not committed.
2022-02-25 14:37:28 -03:00
akwizgran
a54e1d424c Merge branch '2162-mailbox-pairing-ui-end' into 'master'
Implement final parts of UI for pairing Briar with mailbox

Closes #2162

See merge request briar/briar!1590
2022-02-25 14:27:34 +00:00
Torsten Grote
9fa3ee18a4 Capitalize more words and fix duplicate string 2022-02-25 11:17:49 -03:00
akwizgran
4df523aaf8 Bump version numbers for 1.4.5 release. 2022-02-24 16:06:34 +00:00
akwizgran
84be347695 Update Play Store metadata. 2022-02-24 16:00:37 +00:00
akwizgran
6783eae1b1 Update bridges. 2022-02-24 15:32:57 +00:00
akwizgran
fe58bd8f86 Update translations. 2022-02-24 11:11:17 +00:00
Torsten Grote
952ac2c922 Simplify fragment transitions for mailbox pairing UI
Now, trying again always starts before scanning, so the user needs to scan the code again.
2022-02-22 14:43:30 -03:00
Torsten Grote
4390c810d1 Address first round of review feedback for mailbox pairing UI 2022-02-21 14:32:28 -03:00
akwizgran
1a1b26d8f2 Merge branch 'ci-manual-fix' into 'master'
Fix required manual tests

See merge request briar/briar!1588
2022-02-21 12:33:43 +00:00
Torsten Grote
a567301e49 Add a minimal MailboxStatusFragment 2022-02-18 12:57:32 -03:00
Torsten Grote
5e8d5c96fc Implement UI for mailbox pairing error and final states 2022-02-18 12:57:32 -03:00
Torsten Grote
80d804d280 Use new MailboxManager in Android UI 2022-02-18 12:57:32 -03:00
Torsten Grote
7fad299cf0 Add network_security_config so we are allowed to connect to onion addresses
Otherwise trying to connect without TLS will throw an exception.
2022-02-18 12:57:32 -03:00
akwizgran
4e90641059 Merge branch 'onion-address-fix' into 'master'
Fix merge request race condition

See merge request briar/briar!1589
2022-02-18 15:33:00 +00:00
Torsten Grote
f7892050ea allow_failure of mailbox test to make it optional again 2022-02-18 12:18:37 -03:00
Torsten Grote
003ecdb81f Fix merge request race condition 2022-02-18 12:02:06 -03:00
Torsten Grote
9141a8bb3b Merge branch '2168-mailbox-pairing-backend' into 'master'
Implement backend for pairing mailbox

Closes #2168

See merge request briar/briar!1587
2022-02-18 14:56:44 +00:00
akwizgran
7ba2af077e Merge branch '2162-mailbox-pairing-ui' into 'master'
Implement UI for pairing Briar with mailbox

See merge request briar/briar!1585
2022-02-18 14:50:10 +00:00
Torsten Grote
ce7f44de01 Set our own mailbox status right after pairing 2022-02-18 11:01:04 -03:00
Torsten Grote
4a46b13e9d Address mailbox pairing backend review feedback 2022-02-18 09:50:38 -03:00
Torsten Grote
ae7ccdf34c Tweak wording of mailbox intro and download instructions 2022-02-18 09:28:50 -03:00
Torsten Grote
88c54ed3b0 Rename getOnionAddress() to getBaseUrl()
This can later include a version parameter as well.
2022-02-18 09:16:51 -03:00
Torsten Grote
653b744a02 Add getMailboxStatus method to MailboxManager 2022-02-18 09:16:51 -03:00
Torsten Grote
65e7bcb94e Add unit tests for MailboxPairingTask 2022-02-18 09:16:51 -03:00
Torsten Grote
d6bbe59d3a Implement backend for pairing mailbox 2022-02-18 09:16:50 -03:00
Torsten Grote
98dddf3572 Make hiding ActionBar up/back button in Final Fragment optional 2022-02-16 10:37:27 -03:00
akwizgran
6d22bab5ee Merge branch 'transactional-remove-pending-contact' into 'master'
Add transactional version of removePendingContact()

See merge request briar/briar!1586
2022-02-16 11:18:31 +00:00
Sebastian Kürten
7ae91a984f Add transactional version of removePendingContact() 2022-02-15 22:28:29 +01:00
Torsten Grote
fb50a5ba45 Remove custom back and action bar code from OfflineFragment
Also pop the offline fragment off the stack when trying again, so it doesn't show up again when we are not offline anymore.
2022-02-14 14:56:39 -03:00
Torsten Grote
80bc409225 Remove 'share mailbox download link' button 2022-02-14 13:47:04 -03:00
akwizgran
80cac277ac Merge branch '2257-mailbox-wipe' into 'master'
Add method for wiping the mailbox

Closes #2257

See merge request briar/briar!1582
2022-02-14 14:45:51 +00:00
Torsten Grote
888aea4b37 Scroll fragments down on small screens to make bottom buttons visible 2022-02-10 09:46:43 -03:00
Torsten Grote
e9d3f600fa Improve mailbox pairing strings 2022-02-10 09:43:28 -03:00
Torsten Grote
3055338ea8 Show offline fragment before scanning mailbox QR code 2022-02-10 09:41:03 -03:00
Torsten Grote
e4a7b1731a Do real check if mailbox is set up 2022-02-10 09:41:03 -03:00
Torsten Grote
2da8c19d3e Handle TorPlugin not being active during mailbox setup 2022-02-10 09:41:03 -03:00
Torsten Grote
237ac50b01 Handle scanning a wrong QR code
when pairing a mailbox.
2022-02-10 09:41:02 -03:00
Torsten Grote
73d9e05ada Scan Mailbox QR code for setup and show progress screen 2022-02-10 09:17:39 -03:00
Torsten Grote
e14773985d Show mailbox onboarding/download info
if the mailbox is not yet set up.
2022-02-10 08:51:58 -03:00
Torsten Grote
8b3dae6daf Add Mailbox entry into settings 2022-02-10 08:51:58 -03:00
Torsten Grote
065ceb8e98 Add FeatureFlag for mailbox 2022-02-10 08:51:58 -03:00
akwizgran
6d881892c7 Merge branch '2231-file-api' into 'master'
Add Mailbox File Mangement API

Closes #2233, #2232, and #2231

See merge request briar/briar!1581
2022-02-10 11:50:15 +00:00
Torsten Grote
16b503dd7b Introduce MailboxId sub-classes for even more type-safety 2022-02-07 15:58:54 -03:00
Torsten Grote
fc5533ec6e Add method for wiping the mailbox 2022-02-07 09:37:05 -03:00
Torsten Grote
5c153aeb6c Sort files returned by getFiles by time (oldest first). 2022-02-07 09:36:48 -03:00
akwizgran
36670a8bf6 Bump version numbers for 1.4.4 release. 2022-01-27 11:56:54 +00:00
akwizgran
32d62f9960 Update translations. 2022-01-27 11:56:10 +00:00
akwizgran
eafd6a1ca1 Merge branch '2143-security-exception-image-loading' into 'master'
Add FLAG_GRANT_READ_URI_PERMISSION when getting content

Closes #2143

See merge request briar/briar!1583
2022-01-27 11:47:03 +00:00
Torsten Grote
1614e72c43 Add FLAG_GRANT_READ_URI_PERMISSION when getting content 2022-01-27 07:57:51 -03:00
Torsten Grote
d3beb850ef Factor out getArray() for easier JSON parsing 2022-01-24 14:03:48 -03:00
Torsten Grote
f057f0859b Use MailboxId instead of String for type-safety 2022-01-24 13:50:58 -03:00
Torsten Grote
61ea7ff8de Make deleting a non-existent file is tolerable 2022-01-21 15:12:14 -03:00
Torsten Grote
0fba65a722 Add integration test for File Management API 2022-01-21 15:12:13 -03:00
Torsten Grote
3a191908c0 Add method for listing folders with files available
for download (owner only)
2022-01-21 15:12:13 -03:00
Torsten Grote
482258fc92 Add method for deleting a file from a mailbox 2022-01-21 15:11:47 -03:00
Torsten Grote
0cb2dcf6b7 Add method for downloading a file from a mailbox 2022-01-21 15:11:46 -03:00
Torsten Grote
76599a8d04 Add method for listing files from mailbox 2022-01-21 15:11:46 -03:00
Torsten Grote
173af62dec Add method for adding file to mailbox 2022-01-21 15:11:46 -03:00
akwizgran
a53a49e543 Merge branch '2250-refuse-to-start-if-android4-expired' into 'master'
Refuse to start app on Android 4 beyond expiry date

Closes #2250

See merge request briar/briar!1578
2022-01-18 15:03:25 +00:00
akwizgran
78b993bda4 Merge branch 'mailbox-integration-test' into 'master'
Add MailboxIntegrationTest against a real mailbox instance

See merge request briar/briar!1575
2022-01-18 14:46:38 +00:00
akwizgran
6b956611e7 Merge branch '2251-warn-briar-expires-on-android4' into 'master'
Show expiry warning when running on Android 4

Closes #2251

See merge request briar/briar!1577
2022-01-18 14:36:33 +00:00
Torsten Grote
d0c3c1f9f6 Move wait-for-mailbox.sh to bramble-core/src/test/bash 2022-01-18 11:33:08 -03:00
Torsten Grote
24d058cdcc Merge branch 'add-more-feature-flags-for-desktop' into 'master'
Add feature flags for private groups, forums and blogs

See merge request briar/briar!1572
2022-01-18 11:41:26 +00:00
Torsten Grote
a9ab7fd60f Do not kill mailbox at the end of integration test 2022-01-17 11:10:49 -03:00
Daniel Lublin
9e5201d571 Don't try to sign out; we know we aren't signed in 2022-01-17 13:09:40 +01:00
Daniel Lublin
39eebe4c02 Remove, BaseActivity does this 2022-01-17 11:21:49 +01:00
Daniel Lublin
171df265ab Let our activity actually show up, by avoiding to extend BriarActivity
If user needs to sign in or create an account, BriarActivity launches
StartupActivity. But we want to show up before that.

Implement our own signOut with BriarActivity as a template.
2022-01-17 11:04:50 +01:00
Daniel Lublin
9436757215 Reuse existing string (same kind of context) 2022-01-13 12:59:55 +01:00
Daniel Lublin
75370c8124 Refuse to start on Android 4 beyond the set expiry date 2022-01-13 12:59:55 +01:00
Daniel Lublin
10dceafde1 Show expiry warning when running on Android 4
After a set date a snackbar is shown, warning that Briar will stop
working at a later set date.
2022-01-13 12:59:03 +01:00
akwizgran
e3126f931e Merge branch 'move-proguard-rules' into 'master'
Move ProGuard rules for Briar to briar-android

See merge request briar/briar!1580
2022-01-13 11:44:38 +00:00
akwizgran
6ddedbba36 Move ProGuard rules for Briar to briar-android. 2022-01-13 10:37:10 +00:00
akwizgran
982637a0b0 Merge branch 'correct-proguard' into 'master'
Restore proguard rule to keep us alive

See merge request briar/briar!1579
2022-01-13 10:35:37 +00:00
Daniel Lublin
78ef8c8117 Restore proguard, letting us stay alive by keeping more classes
For one, fragment classes referenced *only* from settings.xml (in
app:fragment-attributes) where not pulled in. Accessing such settings
would cause a crash.
2022-01-12 13:51:44 +01:00
Torsten Grote
7319398c3b Merge branch 'dex-method-limit' into 'master'
Update ProGuard rules to stay within dex method limit

See merge request briar/briar!1576
2022-01-11 14:45:22 +00:00
akwizgran
841b8133d1 Update ProGuard rules to stay within dex method limit. 2022-01-11 10:50:49 +00:00
akwizgran
b334e8da27 Suppress ProGuard warning about Jackson's Java7SupportImpl. 2022-01-11 10:48:56 +00:00
akwizgran
0ac26883c6 Build APK for CI pipelines. 2022-01-11 10:48:03 +00:00
Torsten Grote
519837e829 Add MailboxIntegrationTest against a real mailbox instance 2022-01-07 15:34:19 -03:00
Torsten Grote
9fa54bf15c Actually throw TolerableFailureException when *deleting* a contact
Before, this was accidentally added to *listing* contacts.
2022-01-07 14:29:14 -03:00
akwizgran
af3389e0e1 Merge branch '2187-delete-contact-from-mailbox' into 'master'
Add method for deleting a contact and retrieving contact list from own mailbox

Closes #2182 and #2187

See merge request briar/briar!1574
2022-01-07 15:13:19 +00:00
Torsten Grote
f5cdad9100 Throw TolerableFailureException when deleting a contact returns 404 2022-01-07 12:03:21 -03:00
Torsten Grote
df4e6aa207 Add method for retrieving contact list from own mailbox 2022-01-07 11:33:57 -03:00
Torsten Grote
82443d9708 Add method for deleting a contact from own mailbox 2022-01-07 10:46:43 -03:00
akwizgran
27058ba0ca Merge branch '2183-mailbox-add-contact' into 'master'
Add method for adding a contact to own mailbox

Closes #2183

See merge request briar/briar!1573
2022-01-07 13:37:40 +00:00
Torsten Grote
f400cf5aa0 Throw ApiException when adding contact is not successful 2022-01-07 10:22:22 -03:00
Torsten Grote
e52c5ddc8e Rename PermanentFailureException to ApiException 2022-01-07 10:13:31 -03:00
Torsten Grote
835e9f6994 Add mailbox API endpoint for adding a contact 2022-01-07 10:13:31 -03:00
akwizgran
4193179eb8 Merge branch '2243-okhttp-mailbox-calls' into 'master'
Add /status mailbox API call and a test for it

Closes #2207 and #2243

See merge request briar/briar!1564
2022-01-07 13:10:49 +00:00
Torsten Grote
421b00517f Address review comments for MailboxApi 2022-01-07 09:51:29 -03:00
Sebastian Kürten
707802c459 Add feature flags for private groups, forums and blogs 2022-01-06 15:35:37 +01:00
Torsten Grote
9f1757ccaf Remove concept of fatal permanent exceptions
All exceptions will just cause the request to be tried again with some backoff.
2022-01-03 14:12:21 -03:00
Torsten Grote
d665fc17ec Add /status and /setup mailbox API call with tests 2022-01-03 14:10:44 -03:00
akwizgran
65be2d2b26 Merge branch 'transactional-attachment-reader' into 'master'
Transactional version of AttachmentReader#getAttachment()

See merge request briar/briar!1570
2021-12-23 17:05:32 +00:00
Sebastian Kürten
d2a39da3e0 Transactional version of AttachmentReader#getAttachment() 2021-12-20 13:55:08 +01:00
akwizgran
d13e4c976e Merge branch 'fix-string-substitutions' into 'master'
Fix multiple substitutions specified in non-positional format

See merge request briar/briar!1568
2021-12-17 11:23:03 +00:00
akwizgran
20b52804bf Merge branch 'add-pending-contact-transactional' into 'master'
Transactional versions of some more API calls

See merge request briar/briar!1561
2021-12-16 16:38:15 +00:00
ialokim
5b27eb354c transactional versions of addPendingContact, getPendingContacts, getConversationId and respondToIntroduction 2021-12-16 17:21:57 +01:00
Torsten Grote
c340071469 Merge branch 'jmock-java-11' into 'master'
Replace ClassImposteriser with ByteBuddyClassImposteriser

See merge request briar/briar!1569
2021-12-15 16:34:04 +00:00
akwizgran
506e274dff Merge branch '2242-migrate-okhttp' into 'master'
Migrate OkHttp to bramble-core

Closes #2242

See merge request briar/briar!1562
2021-12-15 16:04:33 +00:00
Torsten Grote
423356fdda Add missing bouncycastle dependency to witness 2021-12-15 10:48:55 -03:00
Torsten Grote
043a173828 Migrate OkHttp to bramble-core 2021-12-15 10:46:48 -03:00
akwizgran
f0501bbfab Merge branch '1483-peer-session-crash' into 'master'
Do not create PeerSession for groups we created

Closes #1483

See merge request briar/briar!1344
2021-12-15 12:37:46 +00:00
akwizgran
5cafde7b14 Merge branch 'test-deps' into 'master'
Upgrade briar-android unit test dependencies

See merge request briar/briar!1567
2021-12-15 12:34:54 +00:00
akwizgran
5117dbad7e Merge branch 'bouncycastle-java8' into 'master'
Switch to bouncycastle dependency for Java 1.8 to prevent Java 15 class warnings of multi-jar

See merge request briar/briar!1566
2021-12-15 10:39:38 +00:00
Torsten Grote
3a22388495 Fix multiple substitutions specified in non-positional format
The build warnings might persist until translations have been updated
2021-12-14 16:51:52 -03:00
Torsten Grote
1d4de46dfd Upgrade briar-android unit test dependencies 2021-12-14 16:22:11 -03:00
Torsten Grote
d805069dfe Switch to bouncycastle dependency for Java 1.8 to prevent Java 15 class warnings of multi-jar 2021-12-14 15:42:10 -03:00
akwizgran
74cb2a6ce5 Merge branch 'readme-reproducible' into 'master'
Add a section about reproducible builds to the readme

See merge request briar/briar!1565
2021-12-14 13:23:46 +00:00
Torsten Grote
2880a4adac Add a section about reproducible builds to the readme 2021-12-14 10:00:20 -03:00
akwizgran
e032e0ccd5 Bump version numbers for 1.4.3 release. 2021-12-13 16:58:13 +00:00
Torsten Grote
38a07e1d54 Merge branch 'fix-headless-jar-build-on-java-11' into 'master'
Fix headless jar build on Java 11, add to CI pipeline

See merge request briar/briar!1563
2021-12-13 16:22:43 +00:00
akwizgran
07b35db4d4 Fix headless jar build on Java 11, add to CI pipeline. 2021-12-13 15:38:17 +00:00
akwizgran
3b03db9f43 Bump version numbers for 1.4.2 release. 2021-12-13 15:17:34 +00:00
akwizgran
de3a74eedf Update translations. 2021-12-13 15:16:17 +00:00
akwizgran
5a39f9730f Merge branch 'non-default-bridges' into 'master'
Use non-default obfs4 bridges in Russia

See merge request briar/briar!1556
2021-12-13 13:34:19 +00:00
akwizgran
bdf02bbc6c Increase test timeout because meek bridge is super slow. 2021-12-13 12:53:42 +00:00
akwizgran
d5b2ebdb23 Include meek bridge in test and mark it as essential. 2021-12-13 11:56:52 +00:00
Torsten Grote
98bb8d4af1 Merge branch 'fix-database-locking' into 'master'
Use ReentrantLock as a lock (not a monitor) when marking DB as open.

See merge request briar/briar!1549
2021-12-13 11:48:28 +00:00
akwizgran
75cc19e578 Remove failing non-default bridge. 2021-12-13 11:47:03 +00:00
akwizgran
aad87e6e98 Update default bridges from upstream. 2021-12-13 11:46:43 +00:00
akwizgran
dad895c30d Merge branch '2225-error-handling-for-mailbox-downloads' into 'master'
Add methods to get StreamContext from tag, and mark it as recognised

Closes #2225

See merge request briar/briar!1560
2021-12-10 14:45:34 +00:00
Daniel Lublin
f8b3d79813 Add methods to get StreamContext from tag, and mark it as recognised
Separate methods are needed to be able to restart reading from a stream
in the case of errors. Tag should be marked as recognised only after
successfully reading the stream.

Closes #2225
2021-12-10 15:23:30 +01:00
akwizgran
0a98566298 Merge branch '2190-mailbox-reset-retransmission-times' into 'master'
Resolve "Method for resetting retransmission times"

Closes #2190

See merge request briar/briar!1559
2021-12-09 12:11:54 +00:00
Daniel Lublin
93a03d7e15 Reset using a single db query 2021-12-09 12:55:01 +01:00
Daniel Lublin
3eb3dbde09 Add database method to reset retransmission times
Will be used to ensure messages are not stranded on a Mailbox, when such
is added, removed, or otherwise changed.

Closes #2190.
2021-12-09 11:10:50 +01:00
akwizgran
fd56176450 Merge branch 'desktop-98-transactional' into 'master'
Add transactional versions of some API calls for Briar Desktop

See merge request briar/briar!1557
2021-12-08 11:08:47 +00:00
ialokim
d29812f055 add transactional versions of some API calls 2021-12-07 20:39:55 +01:00
akwizgran
403601b9f2 Merge branch '2088-fix-error-with-null-group' into 'master'
Offline hotspot: don't crash if group is null

Closes #2088

See merge request briar/briar!1552
2021-12-07 15:20:35 +00:00
akwizgran
992215b78a Merge branch '2230-tracking-uploads' into 'master'
Add MailboxSettingsManager methods for tracking pending uploads

Closes #2230

See merge request briar/briar!1558
2021-12-07 15:12:24 +00:00
Torsten Grote
658ca8de21 Add MailboxSettingsManager methods for tracking pending uploads 2021-12-07 10:07:54 -03:00
akwizgran
e0e2c0cc89 Add unit test. 2021-12-07 10:33:16 +00:00
akwizgran
114d80ad43 Use non-default obfs4 bridges in Russia. 2021-12-06 18:11:16 +00:00
Torsten Grote
8d5803098b Merge branch 'network-usage-metrics' into 'master'
Include network usage metrics in crash reports and feedback

See merge request briar/briar!1555
2021-11-30 18:08:07 +00:00
akwizgran
a9ed9da822 Include network usage metrics in crash reports and feedback. 2021-11-30 14:12:45 +00:00
Sebastian Kürten
2295db4361 Offline hotspot: don't crash if group is null 2021-11-22 17:26:01 +01:00
akwizgran
7aa1073bf5 Replace ClassImposteriser with ByteBuddyClassImposteriser.
This may avoid problems with ClassImposteriser when using Java 11. See
https://github.com/jmock-developers/jmock-library/releases/tag/2.10.0
2021-11-18 13:28:08 +00:00
akwizgran
6c702bad0a Use ReentrantLock as a lock (not a monitor) when marking DB as open. 2021-11-17 15:03:58 +00:00
Torsten Grote
d3dbcfd62d Recreate plausible private group sharing sessions when re-adding contact from group 2021-04-15 14:46:22 -03:00
Torsten Grote
c4c70f5ac2 Do not create PeerSession for groups we created
This needs a CreatorSession which gets created on-demand.
2021-04-15 14:44:58 -03:00
305 changed files with 10838 additions and 2065 deletions

View File

@@ -32,8 +32,9 @@ test:
extends: .base-test
stage: test
script:
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom animalSnifferMain animalSnifferTest
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom compileOfficialDebugAndroidTestSources compileScreenshotDebugAndroidTestSources check
- ./gradlew -Djava.security.egd=file:/dev/urandom animalSnifferMain animalSnifferTest
- ./gradlew -Djava.security.egd=file:/dev/urandom assembleOfficialDebug :briar-headless:linuxJars
- ./gradlew -Djava.security.egd=file:/dev/urandom compileOfficialDebugAndroidTestSources compileScreenshotDebugAndroidTestSources check
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: always
@@ -61,7 +62,7 @@ android test:
when: on_failure
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
when: on_success
when: manual
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- briar-android/**/*
@@ -84,35 +85,44 @@ test_reproducible:
.optional_tests:
stage: optional_tests
before_script:
- set -e
- export GRADLE_USER_HOME=$PWD/.gradle
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .gradle/wrapper
- .gradle/caches
script:
- OPTIONAL_TESTS=org.briarproject.bramble.plugin.tor.BridgeTest ./gradlew --info bramble-java:test --tests BridgeTest
after_script:
# these file change every time but should not be cached
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
extends: .base-test
bridge test:
extends: .optional_tests
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
when: on_success
allow_failure: true
allow_failure: false
- if: '$CI_COMMIT_TAG == null'
when: manual
allow_failure: true
script:
- OPTIONAL_TESTS=org.briarproject.bramble.plugin.tor.BridgeTest ./gradlew --info bramble-java:test --tests BridgeTest
timeout: 3h
mailbox integration test:
extends: .optional_tests
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
when: on_success
- if: '$CI_COMMIT_TAG == null'
when: manual
allow_failure: true # TODO figure out how not to allow failure while leaving this optional
script:
# start mailbox
- cd /opt && git clone --depth 1 https://code.briarproject.org/briar/briar-mailbox.git briar-mailbox
- cd briar-mailbox
- mkdir -p /root/.local/share # create directory that mailbox (currently) expects to exist
- ./gradlew run --args="--debug --setup-token 54686973206973206120736574757020746f6b656e20666f722042726961722e" &
# run mailbox integration test once mailbox has started
- cd "$CI_PROJECT_DIR"
- bramble-core/src/test/bash/wait-for-mailbox.sh
- OPTIONAL_TESTS=org.briarproject.bramble.mailbox.MailboxIntegrationTest ./gradlew --info bramble-core:test --tests MailboxIntegrationTest
pre_release_tests:
extends: .optional_tests
script:
- OPTIONAL_TESTS=org.briarproject.bramble.plugin.tor.BridgeTest ./gradlew --info bramble-java:test --tests BridgeTest
timeout: 3h
only:
- tags

View File

@@ -22,6 +22,15 @@ our site.
[Wiki](https://code.briarproject.org/briar/briar/-/wikis/home)
## Reproducible builds
We provide [docker images](https://code.briarproject.org/briar/briar-reproducer#briar-reproducer)
to ease the task of verifying that the published APK binaries
include nothing but our publicly available source code.
You can either use those images or use them as a blueprint to build your own environment
for reproduction.
## Donate
[![Donate using Liberapay](https://briarproject.org/img/liberapay.svg)](https://liberapay.com/Briar/donate) [![Flattr this](https://briarproject.org/img/flattr-badge-large.png "Flattr this")](https://flattr.com/t/592836/)

View File

@@ -15,8 +15,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 30
versionCode 10401
versionName "1.4.1"
versionCode 10407
versionName "1.4.7"
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -43,7 +43,7 @@ configurations {
dependencies {
implementation project(path: ':bramble-core', configuration: 'default')
tor "org.briarproject:tor-android:$tor_version"
tor "org.briarproject:obfs4proxy-android:$obfs4proxy_version@zip"
tor "org.briarproject:obfs4proxy-android:$obfs4proxy_version"
annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
@@ -53,7 +53,7 @@ dependencies {
testImplementation "junit:junit:$junit_version"
testImplementation "org.jmock:jmock:$jmock_version"
testImplementation "org.jmock:jmock-junit4:$jmock_version"
testImplementation "org.jmock:jmock-legacy:$jmock_version"
testImplementation "org.jmock:jmock-imposters:$jmock_version"
}
def torBinariesDir = 'src/main/res/raw'
@@ -70,11 +70,6 @@ clean.dependsOn cleanTorBinaries
task unpackTorBinaries {
doLast {
copy {
from configurations.tor.collect { zipTree(it) }
into torBinariesDir
include 'geoip.zip'
}
configurations.tor.each { outer ->
zipTree(outer).each { inner ->
if (inner.name.endsWith('_arm_pie.zip')) {

View File

@@ -1,6 +1,8 @@
-keep,includedescriptorclasses class org.briarproject.** { *; }
-keep class org.h2.** { *; }
# Keep the H2 classes that are loaded via reflection
-keep class org.h2.Driver { *; }
-keep class org.h2.engine.Engine { *; }
-keep class org.h2.store.fs.** { *; }
# Don't warn about unused dependencies of H2 classes
-dontwarn org.h2.**
-dontnote org.h2.**
@@ -15,5 +17,4 @@
-dontwarn sun.misc.Unsafe
-dontnote com.google.common.**
# UPnP library isn't used
-dontwarn org.bitlet.weupnp.**
-dontwarn com.fasterxml.jackson.databind.ext.Java7SupportImpl

View File

@@ -5,15 +5,12 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.logging.PersistentLogManager;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -26,18 +23,14 @@ 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 java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.AndroidUtils.getPersistentLogDir;
import static org.briarproject.bramble.util.IoUtils.deleteFileOrDir;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.logFileOrDir;
class AndroidAccountManager extends AccountManagerImpl
implements AccountManager {
private static final Logger LOG =
getLogger(AndroidAccountManager.class.getName());
Logger.getLogger(AndroidAccountManager.class.getName());
/**
* Directories that shouldn't be deleted when deleting the user's account.
@@ -47,22 +40,13 @@ class AndroidAccountManager extends AccountManagerImpl
protected final Context appContext;
private final SharedPreferences prefs;
private final PersistentLogManager logManager;
private final FeatureFlags featureFlags;
@Inject
AndroidAccountManager(
DatabaseConfig databaseConfig,
CryptoComponent crypto,
IdentityManager identityManager,
SharedPreferences prefs,
PersistentLogManager logManager,
FeatureFlags featureFlags,
Application app) {
AndroidAccountManager(DatabaseConfig databaseConfig,
CryptoComponent crypto, IdentityManager identityManager,
SharedPreferences prefs, Application app) {
super(databaseConfig, crypto, identityManager);
this.prefs = prefs;
this.logManager = logManager;
this.featureFlags = featureFlags;
appContext = app.getApplicationContext();
}
@@ -90,9 +74,6 @@ class AndroidAccountManager extends AccountManagerImpl
LOG.info("Contents of account directory after deleting:");
logFileOrDir(LOG, INFO, getDataDir());
}
if (featureFlags.shouldEnablePersistentLogs()) {
replacePersistentLogger();
}
}
}
@@ -153,13 +134,4 @@ class AndroidAccountManager extends AccountManagerImpl
private void addIfNotNull(Set<File> files, @Nullable File file) {
if (file != null) files.add(file);
}
private void replacePersistentLogger() {
File logDir = getPersistentLogDir(appContext);
try {
logManager.addLogHandler(logDir, getLogger(""));
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
}

View File

@@ -11,6 +11,8 @@ import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventExecutor;
@@ -38,6 +40,7 @@ import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.content.Context.CONNECTIVITY_SERVICE;
import static android.content.Context.WIFI_SERVICE;
import static android.content.Intent.ACTION_SCREEN_OFF;
import static android.content.Intent.ACTION_SCREEN_ON;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
@@ -111,15 +114,37 @@ class AndroidNetworkManager implements NetworkManager, Service {
@Override
public NetworkStatus getNetworkStatus() {
NetworkInfo net = connectivityManager.getActiveNetworkInfo();
boolean connected = net != null && net.isConnected();
boolean wifi = false, ipv6Only = false;
if (connected) {
wifi = net.getType() == TYPE_WIFI;
if (SDK_INT >= 23) ipv6Only = isActiveNetworkIpv6Only();
else ipv6Only = areAllAvailableNetworksIpv6Only();
// https://issuetracker.google.com/issues/175055271
try {
NetworkInfo net = connectivityManager.getActiveNetworkInfo();
boolean connected = net != null && net.isConnected();
boolean wifi = false, ipv6Only = false;
if (connected) {
wifi = net.getType() == TYPE_WIFI;
if (SDK_INT >= 23) ipv6Only = isActiveNetworkIpv6Only();
else ipv6Only = areAllAvailableNetworksIpv6Only();
}
return new NetworkStatus(connected, wifi, ipv6Only);
} catch (SecurityException e) {
logException(LOG, WARNING, e);
// Without the ConnectivityManager we can't detect whether we have
// internet access. Assume we do, which is probably less harmful
// than assuming we don't. Likewise, assume the connection is
// IPv6-only. Fall back to the WifiManager to detect whether we
// have a wifi connection.
LOG.info("ConnectivityManager is broken, guessing connectivity");
boolean connected = true, wifi = false, ipv6Only = true;
WifiManager wm = (WifiManager) app.getSystemService(WIFI_SERVICE);
if (wm != null) {
WifiInfo info = wm.getConnectionInfo();
if (info != null && info.getIpAddress() != 0) {
LOG.info("Connected to wifi");
wifi = true;
ipv6Only = false;
}
}
return new NetworkStatus(connected, wifi, ipv6Only);
}
return new NetworkStatus(connected, wifi, ipv6Only);
}
/**
@@ -130,23 +155,29 @@ class AndroidNetworkManager implements NetworkManager, Service {
*/
@TargetApi(23)
private boolean isActiveNetworkIpv6Only() {
Network net = connectivityManager.getActiveNetwork();
if (net == null) {
LOG.info("No active network");
// https://issuetracker.google.com/issues/175055271
try {
Network net = connectivityManager.getActiveNetwork();
if (net == null) {
LOG.info("No active network");
return false;
}
LinkProperties props = connectivityManager.getLinkProperties(net);
if (props == null) {
LOG.info("No link properties for active network");
return false;
}
boolean hasIpv6Unicast = false;
for (LinkAddress linkAddress : props.getLinkAddresses()) {
InetAddress addr = linkAddress.getAddress();
if (addr instanceof Inet4Address) return false;
if (!addr.isMulticastAddress()) hasIpv6Unicast = true;
}
return hasIpv6Unicast;
} catch (SecurityException e) {
logException(LOG, WARNING, e);
return false;
}
LinkProperties props = connectivityManager.getLinkProperties(net);
if (props == null) {
LOG.info("No link properties for active network");
return false;
}
boolean hasIpv6Unicast = false;
for (LinkAddress linkAddress : props.getLinkAddresses()) {
InetAddress addr = linkAddress.getAddress();
if (addr instanceof Inet4Address) return false;
if (!addr.isMulticastAddress()) hasIpv6Unicast = true;
}
return hasIpv6Unicast;
}
/**

View File

@@ -32,13 +32,22 @@ class AndroidRemovableDrivePlugin extends RemovableDrivePlugin {
InputStream openInputStream(TransportProperties p) throws IOException {
String uri = p.get(PROP_URI);
if (isNullOrEmpty(uri)) throw new IllegalArgumentException();
return app.getContentResolver().openInputStream(Uri.parse(uri));
try {
return app.getContentResolver().openInputStream(Uri.parse(uri));
} catch (SecurityException e) {
throw new IOException(e);
}
}
@Override
OutputStream openOutputStream(TransportProperties p) throws IOException {
String uri = p.get(PROP_URI);
if (isNullOrEmpty(uri)) throw new IllegalArgumentException();
return app.getContentResolver().openOutputStream(Uri.parse(uri));
try {
return app.getContentResolver()
.openOutputStream(Uri.parse(uri), "wt");
} catch (SecurityException e) {
throw new IOException(e);
}
}
}

View File

@@ -175,16 +175,24 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
@TargetApi(21)
@Nullable
private InetAddress getWifiClientIpv6Address() {
for (Network net : connectivityManager.getAllNetworks()) {
NetworkCapabilities caps =
connectivityManager.getNetworkCapabilities(net);
if (caps == null || !caps.hasTransport(TRANSPORT_WIFI)) continue;
LinkProperties props = connectivityManager.getLinkProperties(net);
if (props == null) continue;
for (LinkAddress linkAddress : props.getLinkAddresses()) {
InetAddress addr = linkAddress.getAddress();
if (isIpv6LinkLocalAddress(addr)) return addr;
// https://issuetracker.google.com/issues/175055271
try {
for (Network net : connectivityManager.getAllNetworks()) {
NetworkCapabilities caps =
connectivityManager.getNetworkCapabilities(net);
if (caps == null || !caps.hasTransport(TRANSPORT_WIFI)) {
continue;
}
LinkProperties props =
connectivityManager.getLinkProperties(net);
if (props == null) continue;
for (LinkAddress linkAddress : props.getLinkAddresses()) {
InetAddress addr = linkAddress.getAddress();
if (isIpv6LinkLocalAddress(addr)) return addr;
}
}
} catch (SecurityException e) {
logException(LOG, WARNING, e);
}
return null;
}
@@ -227,12 +235,17 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
// network's socket factory may try to connect via another network
private SocketFactory getSocketFactory() {
if (SDK_INT < 21) return SocketFactory.getDefault();
for (Network net : connectivityManager.getAllNetworks()) {
NetworkCapabilities caps =
connectivityManager.getNetworkCapabilities(net);
if (caps != null && caps.hasTransport(TRANSPORT_WIFI)) {
return net.getSocketFactory();
// https://issuetracker.google.com/issues/175055271
try {
for (Network net : connectivityManager.getAllNetworks()) {
NetworkCapabilities caps =
connectivityManager.getNetworkCapabilities(net);
if (caps != null && caps.hasTransport(TRANSPORT_WIFI)) {
return net.getSocketFactory();
}
}
} catch (SecurityException e) {
logException(LOG, WARNING, e);
}
LOG.warning("Could not find suitable socket factory");
return SocketFactory.getDefault();

View File

@@ -116,10 +116,12 @@ class AndroidTaskScheduler implements TaskScheduler, Service, AlarmListener {
long dueMillis = now + MILLISECONDS.convert(delay, unit);
Runnable wakeful = () ->
wakeLockManager.executeWakefully(task, executor, "TaskHandoff");
Future<?> check = scheduleCheckForDueTasks(delay, unit);
ScheduledTask s = new ScheduledTask(wakeful, dueMillis, check,
cancelled);
// Acquire the lock before scheduling the check to ensure the check
// doesn't access the task queue before the task has been added
ScheduledTask s;
synchronized (lock) {
Future<?> check = scheduleCheckForDueTasks(delay, unit);
s = new ScheduledTask(wakeful, dueMillis, check, cancelled);
tasks.add(s);
}
return s;
@@ -136,6 +138,7 @@ class AndroidTaskScheduler implements TaskScheduler, Service, AlarmListener {
return schedule(wrapped, executor, delay, unit, cancelled);
}
@GuardedBy("lock")
private Future<?> scheduleCheckForDueTasks(long delay, TimeUnit unit) {
Runnable wakeful = () -> wakeLockManager.runWakefully(
this::runDueTasks, "TaskScheduler");
@@ -206,7 +209,7 @@ class AndroidTaskScheduler implements TaskScheduler, Service, AlarmListener {
private final Future<?> check;
private final AtomicBoolean cancelled;
public ScheduledTask(Runnable task, long dueMillis,
private ScheduledTask(Runnable task, long dueMillis,
Future<?> check, AtomicBoolean cancelled) {
this.task = task;
this.dueMillis = dueMillis;

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.os.Build;
import android.os.Looper;
import android.provider.Settings;
import org.briarproject.bramble.api.Pair;
@@ -111,14 +112,10 @@ public class AndroidUtils {
return ctx.getDir(STORED_REPORTS, MODE_PRIVATE);
}
public static File getTemporaryLogFile(Context ctx) {
public static File getLogcatFile(Context ctx) {
return new File(ctx.getFilesDir(), STORED_LOGCAT);
}
public static File getPersistentLogDir(Context ctx) {
return ctx.getDir("log", MODE_PRIVATE);
}
/**
* Returns an array of supported content types for image attachments.
*/
@@ -138,4 +135,8 @@ public class AndroidUtils {
return null;
}
}
public static boolean isUiThread() {
return Looper.myLooper() == Looper.getMainLooper();
}
}

View File

@@ -4,22 +4,18 @@ import android.app.Application;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.logging.PersistentLogManager;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.jmock.Expectations;
import org.jmock.lib.legacy.ClassImposteriser;
import org.jmock.imposters.ByteBuddyClassImposteriser;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.util.logging.Logger;
import static android.content.Context.MODE_PRIVATE;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
@@ -31,10 +27,6 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
context.mock(SharedPreferences.class, "prefs");
private final SharedPreferences defaultPrefs =
context.mock(SharedPreferences.class, "defaultPrefs");
private final PersistentLogManager logManager =
context.mock(PersistentLogManager.class);
private final FeatureFlags featureFlags =
context.mock(FeatureFlags.class);
private final DatabaseConfig databaseConfig =
context.mock(DatabaseConfig.class);
private final CryptoComponent crypto = context.mock(CryptoComponent.class);
@@ -48,12 +40,11 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
private final File testDir = getTestDirectory();
private final File keyDir = new File(testDir, "key");
private final File dbDir = new File(testDir, "db");
private final File logDir = new File(testDir, "log");
private AndroidAccountManager accountManager;
public AndroidAccountManagerTest() {
context.setImposteriser(ClassImposteriser.INSTANCE);
context.setImposteriser(ByteBuddyClassImposteriser.INSTANCE);
app = context.mock(Application.class);
applicationInfo = new ApplicationInfo();
applicationInfo.dataDir = testDir.getAbsolutePath();
@@ -70,7 +61,7 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
will(returnValue(app));
}});
accountManager = new AndroidAccountManager(databaseConfig, crypto,
identityManager, prefs, logManager, featureFlags, app) {
identityManager, prefs, app) {
@Override
SharedPreferences getDefaultSharedPreferences() {
return defaultPrefs;
@@ -118,17 +109,10 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
will(returnValue(cacheDir));
oneOf(app).getExternalCacheDir();
will(returnValue(externalCacheDir));
oneOf(featureFlags).shouldEnablePersistentLogs();
will(returnValue(true));
oneOf(app).getDir("log", MODE_PRIVATE);
will(returnValue(logDir));
oneOf(logManager).addLogHandler(with(logDir),
with(any(Logger.class)));
}});
assertTrue(dbDir.mkdirs());
assertTrue(keyDir.mkdirs());
assertTrue(logDir.mkdirs());
assertTrue(codeCacheDir.mkdirs());
assertTrue(codeCacheFile.createNewFile());
assertTrue(libDir.mkdirs());
@@ -146,7 +130,6 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
assertFalse(dbDir.exists());
assertFalse(keyDir.exists());
assertFalse(logDir.exists());
assertTrue(codeCacheDir.exists());
assertTrue(codeCacheFile.exists());
assertTrue(libDir.exists());

View File

@@ -87,8 +87,8 @@ dependencyVerification {
'org.apache.httpcomponents:httpmime:4.5.6:httpmime-4.5.6.jar:0b2b1102c18d3c7e05a77214b9b7501a6f6056174ae5604e0e256776eda7553e',
'org.bouncycastle:bcpkix-jdk15on:1.56:bcpkix-jdk15on-1.56.jar:7043dee4e9e7175e93e0b36f45b1ec1ecb893c5f755667e8b916eb8dd201c6ca',
'org.bouncycastle:bcprov-jdk15on:1.56:bcprov-jdk15on-1.56.jar:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349',
'org.briarproject:obfs4proxy-android:0.0.12-dev-40245c4a:obfs4proxy-android-0.0.12-dev-40245c4a.zip:8ab05a8f8391be2cb5ab2b665c281a06d9e3a756bd0f95a40a36ca927866ea82',
'org.briarproject:tor-android:0.3.5.17:tor-android-0.3.5.17.jar:1888afc10a26b93d00a010ea27bf0b1b162a6d524688b08b98d70d14dc363b54',
'org.briarproject:obfs4proxy-android:0.0.12:obfs4proxy-android-0.0.12.jar:84159d2a4668abc40e3fccaa1f6fa0c04892863f9eb80a866ac8928d9f9a7e89',
'org.briarproject:tor-android:0.4.5.12-2:tor-android-0.4.5.12-2.jar:8545dbcef2bb6aa89c32bb6f8ac51f7a64bce3ae85845b3578ffdeb9b206feb9',
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
'org.checkerframework:checker-qual:3.5.0:checker-qual-3.5.0.jar:729990b3f18a95606fc2573836b6958bcdb44cb52bfbd1b7aa9c339cff35a5a4',

View File

@@ -9,11 +9,11 @@ apply from: 'witness.gradle'
dependencies {
implementation "com.google.dagger:dagger:$dagger_version"
implementation 'com.google.code.findbugs:jsr305:3.0.2'
implementation "com.fasterxml.jackson.core:jackson-annotations:$jackson_version"
testImplementation "junit:junit:$junit_version"
testImplementation "org.jmock:jmock:$jmock_version"
testImplementation "org.jmock:jmock-junit4:$jmock_version"
testImplementation "org.jmock:jmock-legacy:$jmock_version"
signature 'org.codehaus.mojo.signature:java16:1.1@signature'
}

View File

@@ -11,5 +11,11 @@ public interface FeatureFlags {
boolean shouldEnableDisappearingMessages();
boolean shouldEnablePersistentLogs();
boolean shouldEnableMailbox();
boolean shouldEnablePrivateGroupsInCore();
boolean shouldEnableForumsInCore();
boolean shouldEnableBlogsInCore();
}

View File

@@ -1,16 +1,8 @@
package org.briarproject.bramble.api;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Hashtable;
import java.util.Map;
import javax.annotation.Nullable;
import static org.briarproject.bramble.util.StringUtils.fromHexString;
import static org.briarproject.bramble.util.StringUtils.toHexString;
@NotNullByDefault
public abstract class StringMap extends Hashtable<String, String> {
protected StringMap(Map<String, String> m) {
@@ -60,19 +52,4 @@ public abstract class StringMap extends Hashtable<String, String> {
public void putLong(String key, long value) {
put(key, String.valueOf(value));
}
@Nullable
public byte[] getBytes(String key) {
String s = get(key);
if (s == null) return null;
try {
return fromHexString(s);
} catch (IllegalArgumentException e) {
return null;
}
}
public void putBytes(String key, byte[] value) {
put(key, toHexString(value));
}
}

View File

@@ -6,14 +6,14 @@ import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
@NotNullByDefault
public abstract class UniqueId extends Bytes {
public class UniqueId extends Bytes {
/**
* The length of a unique identifier in bytes.
*/
public static final int LENGTH = 32;
protected UniqueId(byte[] id) {
public UniqueId(byte[] id) {
super(id);
if (id.length != LENGTH) throw new IllegalArgumentException();
}

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.feed;
package org.briarproject.bramble.api;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -13,7 +13,7 @@ import javax.inject.Provider;
* collected.
*/
@NotNullByDefault
abstract class WeakSingletonProvider<T> implements Provider<T> {
public abstract class WeakSingletonProvider<T> implements Provider<T> {
private final Object lock = new Object();
@GuardedBy("lock")
@@ -31,5 +31,5 @@ abstract class WeakSingletonProvider<T> implements Provider<T> {
}
}
abstract T createInstance();
public abstract T createInstance();
}

View File

@@ -9,6 +9,7 @@ import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.mailbox.MailboxPropertiesUpdate;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.properties.TransportProperties;
@@ -20,6 +21,8 @@ import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.Map;
import javax.annotation.Nullable;
@NotNullByDefault
public interface ClientHelper {
@@ -123,6 +126,18 @@ public interface ClientHelper {
Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap(
BdfDictionary properties) throws FormatException;
/**
* Parse and validate the property dictionary of a Mailbox property update
* message.
*
* @return the properties for using the Mailbox, or null if there is no
* Mailbox available
* @throws FormatException if the properties are not valid
*/
@Nullable
MailboxPropertiesUpdate parseAndValidateMailboxPropertiesUpdate(
BdfDictionary properties) throws FormatException;
/**
* Retrieves the contact ID from the group metadata of the given contact
* group.

View File

@@ -107,6 +107,32 @@ public interface ContactManager {
*/
String getHandshakeLink() throws DbException;
/**
* Returns the handshake link that needs to be sent to a contact we want
* to add.
*/
String getHandshakeLink(Transaction txn) throws DbException;
/**
* Creates a {@link PendingContact} from the given handshake link and
* alias, adds it to the database and returns it.
*
* @param link The handshake link received from the pending contact
* @param alias The alias the user has given this pending contact
* @throws UnsupportedVersionException If the link uses a format version
* that is not supported
* @throws FormatException If the link is invalid
* @throws GeneralSecurityException If the pending contact's handshake
* public key is invalid
* @throws ContactExistsException If a contact with the same handshake
* public key already exists
* @throws PendingContactExistsException If a pending contact with the same
* handshake public key already exists
*/
PendingContact addPendingContact(Transaction txn, String link, String alias)
throws DbException, FormatException, GeneralSecurityException,
ContactExistsException, PendingContactExistsException;
/**
* Creates a {@link PendingContact} from the given handshake link and
* alias, adds it to the database and returns it.
@@ -140,11 +166,24 @@ public interface ContactManager {
Collection<Pair<PendingContact, PendingContactState>> getPendingContacts()
throws DbException;
/**
* Returns a list of {@link PendingContact PendingContacts} and their
* {@link PendingContactState states}.
*/
Collection<Pair<PendingContact, PendingContactState>> getPendingContacts(Transaction txn)
throws DbException;
/**
* Removes a {@link PendingContact}.
*/
void removePendingContact(PendingContactId p) throws DbException;
/**
* Removes a {@link PendingContact}.
*/
void removePendingContact(Transaction txn, PendingContactId p)
throws DbException;
/**
* Returns the contact with the given ID.
*/

View File

@@ -3,7 +3,6 @@ package org.briarproject.bramble.api.contact;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
/**
@@ -17,9 +16,4 @@ public class PendingContactId extends UniqueId {
public PendingContactId(byte[] id) {
super(id);
}
@Override
public boolean equals(@Nullable Object o) {
return o instanceof PendingContactId && super.equals(o);
}
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.api.crypto;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.security.GeneralSecurityException;
@@ -10,6 +11,8 @@ import javax.annotation.Nullable;
@NotNullByDefault
public interface CryptoComponent {
UniqueId generateUniqueId();
SecretKey generateSecretKey();
SecureRandom getSecureRandom();
@@ -172,9 +175,11 @@ public interface CryptoComponent {
String asciiArmour(byte[] b, int lineLength);
/**
* Encode the onion/hidden service address given its public key. As
* specified here: https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt?id=29245fd5#n2135
* Encode the Onion given its public key. Specified here:
* https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt?id=29245fd5#n2135
*
* @return the encoded onion, base32 chars
*/
String encodeOnionAddress(byte[] publicKey);
String encodeOnion(byte[] publicKey);
}

View File

@@ -471,6 +471,14 @@ public interface DatabaseComponent extends TransactionManager {
Map<MessageId, Integer> getUnackedMessagesToSend(Transaction txn,
ContactId c) throws DbException;
/**
* Resets the transmission count, expiry time and max latency of all messages
* that are eligible to be sent to the given contact. This includes messages
* that have already been sent and are not yet due for retransmission.
*/
void resetUnackedMessagesToSend(Transaction txn, ContactId c)
throws DbException;
/**
* Returns the total length, including headers, of all messages that are
* eligible to be sent to the given contact. This may include messages

View File

@@ -21,9 +21,4 @@ public class AuthorId extends UniqueId {
public AuthorId(byte[] id) {
super(id);
}
@Override
public boolean equals(Object o) {
return o instanceof AuthorId && super.equals(o);
}
}

View File

@@ -1,46 +0,0 @@
package org.briarproject.bramble.api.logging;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import java.io.File;
import java.io.IOException;
import java.util.Scanner;
import java.util.logging.Handler;
import java.util.logging.Logger;
@NotNullByDefault
public interface PersistentLogManager {
/**
* The namespace of the (@link Settings) where the log key is stored.
*/
String LOG_SETTINGS_NAMESPACE = "log";
/**
* The {@link Settings} key under which the log key is stored.
*/
String LOG_KEY_KEY = "logKey";
/**
* Creates and returns a persistent log handler that stores its logs in
* the given directory.
*/
Handler createLogHandler(File dir) throws IOException;
/**
* Creates a persistent log handler that stores its logs in the given
* directory and adds the handler to the given logger, replacing any
* existing persistent log handler.
*/
void addLogHandler(File dir, Logger logger) throws IOException;
/**
* Returns a {@link Scanner} for reading the persistent log entries stored
* in the given directory.
*
* @param old True if the previous session's log should be loaded, or false
* if the current session's log should be loaded
*/
Scanner getPersistentLog(File dir, boolean old) throws IOException;
}

View File

@@ -0,0 +1,8 @@
package org.briarproject.bramble.api.mailbox;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public class InvalidMailboxIdException extends Exception {
}

View File

@@ -0,0 +1,24 @@
package org.briarproject.bramble.api.mailbox;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
@NotNullByDefault
public class MailboxAuthToken extends MailboxId {
public MailboxAuthToken(byte[] id) {
super(id);
}
/**
* Creates a {@link MailboxAuthToken} from the given string.
*
* @throws InvalidMailboxIdException if token is not valid.
*/
public static MailboxAuthToken fromString(@Nullable String token)
throws InvalidMailboxIdException {
return new MailboxAuthToken(bytesFromString(token));
}
}

View File

@@ -0,0 +1,24 @@
package org.briarproject.bramble.api.mailbox;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
@NotNullByDefault
public class MailboxFileId extends MailboxId {
public MailboxFileId(byte[] id) {
super(id);
}
/**
* Creates a {@link MailboxFileId} from the given string.
*
* @throws IllegalArgumentException if token is not valid.
*/
public static MailboxFileId fromString(@Nullable String token)
throws InvalidMailboxIdException {
return new MailboxFileId(bytesFromString(token));
}
}

View File

@@ -0,0 +1,24 @@
package org.briarproject.bramble.api.mailbox;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
@NotNullByDefault
public class MailboxFolderId extends MailboxId {
public MailboxFolderId(byte[] id) {
super(id);
}
/**
* Creates a {@link MailboxFolderId} from the given string.
*
* @throws IllegalArgumentException if token is not valid.
*/
public static MailboxFolderId fromString(@Nullable String token)
throws InvalidMailboxIdException {
return new MailboxFolderId(bytesFromString(token));
}
}

View File

@@ -0,0 +1,49 @@
package org.briarproject.bramble.api.mailbox;
import com.fasterxml.jackson.annotation.JsonValue;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Locale;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import static org.briarproject.bramble.util.StringUtils.fromHexString;
import static org.briarproject.bramble.util.StringUtils.toHexString;
@ThreadSafe
@NotNullByDefault
public abstract class MailboxId extends UniqueId {
MailboxId(byte[] id) {
super(id);
}
/**
* Returns valid {@link MailboxId} bytes from the given string.
*
* @throws InvalidMailboxIdException if token is not valid.
*/
static byte[] bytesFromString(@Nullable String token)
throws InvalidMailboxIdException {
if (token == null || token.length() != 64) {
throw new InvalidMailboxIdException();
}
try {
return fromHexString(token);
} catch (IllegalArgumentException e) {
throw new InvalidMailboxIdException();
}
}
/**
* Returns the string representation expected by the mailbox API.
* Also used for serialization.
*/
@Override
@JsonValue
public String toString() {
return toHexString(getBytes()).toLowerCase(Locale.US);
}
}

View File

@@ -0,0 +1,44 @@
package org.briarproject.bramble.api.mailbox;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import javax.annotation.Nullable;
public interface MailboxManager {
/**
* @return true if a mailbox is already paired.
*/
boolean isPaired(Transaction txn) throws DbException;
/**
* @return the current status of the mailbox.
*/
MailboxStatus getMailboxStatus(Transaction txn) throws DbException;
/**
* Returns the currently running pairing task,
* or null if no pairing task is running.
*/
@Nullable
MailboxPairingTask getCurrentPairingTask();
/**
* Starts and returns a pairing task. If a pairing task is already running,
* it will be returned and the argument will be ignored.
*
* @param qrCodePayload The ISO-8859-1 encoded bytes of the mailbox QR code.
*/
MailboxPairingTask startPairingTask(String qrCodePayload);
/**
* Can be used by the UI to test the mailbox connection.
*
* @return true (success) or false (error).
* A {@link OwnMailboxConnectionStatusEvent} might be broadcast with a new
* {@link MailboxStatus}.
*/
boolean checkConnection();
}

View File

@@ -0,0 +1,25 @@
package org.briarproject.bramble.api.mailbox;
public abstract class MailboxPairingState {
public static class QrCodeReceived extends MailboxPairingState {
}
public static class Pairing extends MailboxPairingState {
}
public static class Paired extends MailboxPairingState {
}
public static class InvalidQrCode extends MailboxPairingState {
}
public static class MailboxAlreadyPaired extends MailboxPairingState {
}
public static class ConnectionError extends MailboxPairingState {
}
public static class UnexpectedError extends MailboxPairingState {
}
}

View File

@@ -0,0 +1,21 @@
package org.briarproject.bramble.api.mailbox;
import org.briarproject.bramble.api.Consumer;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public interface MailboxPairingTask extends Runnable {
/**
* Adds an observer to the task. The observer will be notified on the
* event thread of the current state of the task and any subsequent state
* changes.
*/
void addObserver(Consumer<MailboxPairingState> observer);
/**
* Removes an observer from the task.
*/
void removeObserver(Consumer<MailboxPairingState> observer);
}

View File

@@ -8,21 +8,27 @@ import javax.annotation.concurrent.Immutable;
@NotNullByDefault
public class MailboxProperties {
private final String onionAddress, authToken;
private final String baseUrl;
private final MailboxAuthToken authToken;
private final boolean owner;
public MailboxProperties(String onionAddress, String authToken,
public MailboxProperties(String baseUrl, MailboxAuthToken authToken,
boolean owner) {
this.onionAddress = onionAddress;
this.baseUrl = baseUrl;
this.authToken = authToken;
this.owner = owner;
}
public String getOnionAddress() {
return onionAddress;
public String getBaseUrl() {
return baseUrl;
}
public String getAuthToken() {
public String getOnion() {
return baseUrl.replaceFirst("^http://", "")
.replaceFirst("\\.onion$", "");
}
public MailboxAuthToken getAuthToken() {
return authToken;
}

View File

@@ -0,0 +1,41 @@
package org.briarproject.bramble.api.mailbox;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class MailboxPropertiesUpdate {
private final String onion;
private final MailboxAuthToken authToken;
private final MailboxFolderId inboxId;
private final MailboxFolderId outboxId;
public MailboxPropertiesUpdate(String onion,
MailboxAuthToken authToken, MailboxFolderId inboxId,
MailboxFolderId outboxId) {
this.onion = onion;
this.authToken = authToken;
this.inboxId = inboxId;
this.outboxId = outboxId;
}
public String getOnion() {
return onion;
}
public MailboxAuthToken getAuthToken() {
return authToken;
}
public MailboxFolderId getInboxId() {
return inboxId;
}
public MailboxFolderId getOutboxId() {
return outboxId;
}
}

View File

@@ -0,0 +1,67 @@
package org.briarproject.bramble.api.mailbox;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.ClientId;
import javax.annotation.Nullable;
@NotNullByDefault
public interface MailboxPropertyManager {
/**
* The unique ID of the mailbox property client.
*/
ClientId CLIENT_ID =
new ClientId("org.briarproject.bramble.mailbox.properties");
/**
* The current major version of the mailbox property client.
*/
int MAJOR_VERSION = 0;
/**
* The current minor version of the mailbox property client.
*/
int MINOR_VERSION = 0;
/**
* The number of properties required for a (non-empty) update message.
*/
int PROP_COUNT = 4;
/**
* The required properties of a non-empty update message.
*/
String PROP_KEY_ONION = "onion";
String PROP_KEY_AUTHTOKEN = "authToken";
String PROP_KEY_INBOXID = "inboxId";
String PROP_KEY_OUTBOXID = "outboxId";
/**
* Length of the Onion property.
*/
int PROP_ONION_LENGTH = 56;
/**
* Message metadata key for the version number of a local or remote update,
* as a BDF long.
*/
String MSG_KEY_VERSION = "version";
/**
* Message metadata key for whether an update is local or remote, as a BDF
* boolean.
*/
String MSG_KEY_LOCAL = "local";
@Nullable
MailboxPropertiesUpdate getLocalProperties(Transaction txn, ContactId c)
throws DbException;
@Nullable
MailboxPropertiesUpdate getRemoteProperties(Transaction txn, ContactId c)
throws DbException;
}

View File

@@ -1,7 +1,10 @@
package org.briarproject.bramble.api.mailbox;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
@@ -9,6 +12,13 @@ import javax.annotation.Nullable;
@NotNullByDefault
public interface MailboxSettingsManager {
/**
* Registers a hook to be called when a mailbox has been paired or unpaired.
* This method should be called before
* {@link LifecycleManager#startServices(SecretKey)}.
*/
void registerMailboxHook(MailboxHook hook);
@Nullable
MailboxProperties getOwnMailboxProperties(Transaction txn)
throws DbException;
@@ -23,4 +33,28 @@ public interface MailboxSettingsManager {
void recordFailedConnectionAttempt(Transaction txn, long now)
throws DbException;
void setPendingUpload(Transaction txn, ContactId id,
@Nullable String filename) throws DbException;
@Nullable
String getPendingUpload(Transaction txn, ContactId id) throws DbException;
interface MailboxHook {
/**
* Called when Briar is paired with a mailbox
*
* @param txn A read-write transaction
* @param ownOnion Our new mailbox's onion (56 base32 chars)
*/
void mailboxPaired(Transaction txn, String ownOnion)
throws DbException;
/**
* Called when the mailbox is unpaired
*
* @param txn A read-write transaction
*/
void mailboxUnpaired(Transaction txn) throws DbException;
}
}

View File

@@ -0,0 +1,25 @@
package org.briarproject.bramble.api.mailbox;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast by {@link MailboxSettingsManager} when
* recording the connection status of own Mailbox.
*/
@Immutable
@NotNullByDefault
public class OwnMailboxConnectionStatusEvent extends Event {
private final MailboxStatus status;
public OwnMailboxConnectionStatusEvent(MailboxStatus status) {
this.status = status;
}
public MailboxStatus getStatus() {
return status;
}
}

View File

@@ -0,0 +1,36 @@
package org.briarproject.bramble.api.mailbox;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when {@link MailboxPropertiesUpdate} are received
* from a contact.
*/
@Immutable
@NotNullByDefault
public class RemoteMailboxPropertiesUpdateEvent extends Event {
private final ContactId contactId;
@Nullable
private final MailboxPropertiesUpdate mailboxPropertiesUpdate;
public RemoteMailboxPropertiesUpdateEvent(ContactId contactId,
@Nullable MailboxPropertiesUpdate mailboxPropertiesUpdate) {
this.contactId = contactId;
this.mailboxPropertiesUpdate = mailboxPropertiesUpdate;
}
public ContactId getContact() {
return contactId;
}
@Nullable
public MailboxPropertiesUpdate getMailboxPropertiesUpdate() {
return mailboxPropertiesUpdate;
}
}

View File

@@ -1,5 +1,7 @@
package org.briarproject.bramble.api.plugin;
import static java.util.concurrent.TimeUnit.SECONDS;
public interface TorConstants {
TransportId ID = new TransportId("org.briarproject.bramble.tor");
@@ -10,8 +12,9 @@ public interface TorConstants {
int DEFAULT_SOCKS_PORT = 59050;
int DEFAULT_CONTROL_PORT = 59051;
int CONNECT_TO_PROXY_TIMEOUT = 5000; // Milliseconds
int EXTRA_SOCKET_TIMEOUT = 30000; // Milliseconds
int CONNECT_TO_PROXY_TIMEOUT = (int) SECONDS.toMillis(5);
int EXTRA_CONNECT_TIMEOUT = (int) SECONDS.toMillis(120);
int EXTRA_SOCKET_TIMEOUT = (int) SECONDS.toMillis(30);
// Local settings (not shared with contacts)
String PREF_TOR_NETWORK = "network2";

View File

@@ -8,24 +8,11 @@ import java.io.File;
@NotNullByDefault
public interface DevConfig {
/**
* Returns the public key for encrypting feedback and crash reports.
*/
PublicKey getDevPublicKey();
/**
* Returns the onion address for submitting feedback and crash reports.
*/
String getDevOnionAddress();
/**
* Returns the directory for storing unsent feedback and crash reports.
*/
File getReportDir();
/**
* Returns the temporary file for passing the encrypted app log from the
* main process to the crash reporter process.
*/
File getTemporaryLogFile();
File getLogcatFile();
}

View File

@@ -20,9 +20,4 @@ public class GroupId extends UniqueId {
public GroupId(byte[] id) {
super(id);
}
@Override
public boolean equals(Object o) {
return o instanceof GroupId && super.equals(o);
}
}

View File

@@ -27,9 +27,4 @@ public class MessageId extends UniqueId {
public MessageId(byte[] id) {
super(id);
}
@Override
public boolean equals(Object o) {
return o instanceof MessageId && super.equals(o);
}
}

View File

@@ -113,9 +113,25 @@ public interface KeyManager {
/**
* Looks up the given tag and returns a {@link StreamContext} for reading
* from the corresponding stream, or null if an error occurs or the tag was
* unexpected.
* unexpected. Marks the tag as recognised and updates the reordering
* window.
*/
@Nullable
StreamContext getStreamContext(TransportId t, byte[] tag)
throws DbException;
/**
* Looks up the given tag and returns a {@link StreamContext} for reading
* from the corresponding stream, or null if an error occurs or the tag was
* unexpected. Only returns the StreamContext; does not mark the tag as
* recognised.
*/
@Nullable
StreamContext getStreamContextOnly(TransportId t, byte[] tag)
throws DbException;
/**
* Marks the tag as recognised and updates the reordering window.
*/
void markTagAsRecognised(TransportId t, byte[] tag) throws DbException;
}

View File

@@ -1,10 +1,7 @@
package org.briarproject.bramble.util;
import java.io.File;
import java.util.Collection;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import static java.util.logging.Level.FINE;
@@ -60,13 +57,4 @@ public class LogUtils {
String type) {
logger.log(level, type + " " + f.getAbsolutePath() + " " + f.length());
}
public static String formatLog(Formatter formatter,
Collection<LogRecord> logRecords) {
StringBuilder sb = new StringBuilder();
for (LogRecord record : logRecords) {
sb.append(formatter.format(record)).append('\n');
}
return sb.toString();
}
}

View File

@@ -26,7 +26,7 @@ public class NetworkUtils {
// Despite what the docs say, the return value can be null
//noinspection ConstantConditions
return ifaces == null ? emptyList() : list(ifaces);
} catch (SocketException e) {
} catch (SocketException | NullPointerException e) {
logException(LOG, WARNING, e);
return emptyList();
}

View File

@@ -1,17 +1,46 @@
package org.briarproject.bramble.test;
import java.lang.Thread.UncaughtExceptionHandler;
import org.junit.After;
import org.junit.Before;
import static org.junit.Assert.fail;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
public abstract class BrambleTestCase {
private static final Logger LOG =
getLogger(BrambleTestCase.class.getName());
@Nullable
protected volatile Throwable exceptionInBackgroundThread = null;
public BrambleTestCase() {
// Ensure exceptions thrown on worker threads cause tests to fail
UncaughtExceptionHandler fail = (thread, throwable) -> {
throwable.printStackTrace();
fail();
LOG.log(WARNING, "Caught unhandled exception", throwable);
exceptionInBackgroundThread = throwable;
};
Thread.setDefaultUncaughtExceptionHandler(fail);
}
@Before
public void beforeBrambleTestCase() {
exceptionInBackgroundThread = null;
}
@After
public void afterBrambleTestCase() {
Throwable thrown = exceptionInBackgroundThread;
if (thrown != null) {
LOG.log(WARNING,
"Background thread has thrown an exception unexpectedly",
thrown);
throw new AssertionError(thrown);
}
}
}

View File

@@ -12,10 +12,15 @@ import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.crypto.SignaturePrivateKey;
import org.briarproject.bramble.api.crypto.SignaturePublicKey;
import org.briarproject.bramble.api.db.CommitAction;
import org.briarproject.bramble.api.db.EventAction;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.identity.Identity;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.mailbox.MailboxPropertiesUpdate;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.sync.ClientId;
@@ -25,7 +30,11 @@ import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.util.IoUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -35,6 +44,8 @@ import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import static java.util.Arrays.asList;
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES;
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_PUBLIC_KEY_BYTES;
@@ -45,6 +56,7 @@ import static org.briarproject.bramble.api.properties.TransportPropertyConstants
import static org.briarproject.bramble.api.sync.ClientId.MAX_CLIENT_ID_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
public class TestUtils {
@@ -209,6 +221,24 @@ public class TestUtils {
getAgreementPublicKey(), verified);
}
public static void writeBytes(File file, byte[] bytes)
throws IOException {
FileOutputStream outputStream = new FileOutputStream(file);
//noinspection TryFinallyCanBeTryWithResources
try {
outputStream.write(bytes);
} finally {
outputStream.close();
}
}
public static byte[] readBytes(File file) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
FileInputStream inputStream = new FileInputStream(file);
copyAndClose(inputStream, outputStream);
return outputStream.toByteArray();
}
public static double getMedian(Collection<? extends Number> samples) {
int size = samples.size();
if (size == 0) throw new IllegalArgumentException();
@@ -248,4 +278,27 @@ public class TestUtils {
return optionalTests != null &&
asList(optionalTests.split(",")).contains(testClass.getName());
}
public static boolean mailboxPropertiesUpdateEqual(
@Nullable MailboxPropertiesUpdate a,
@Nullable MailboxPropertiesUpdate b) {
if (a == null || b == null) {
return a == b;
}
return a.getOnion().equals(b.getOnion()) &&
a.getAuthToken().equals(b.getAuthToken()) &&
a.getInboxId().equals(b.getInboxId()) &&
a.getOutboxId().equals(b.getOutboxId());
}
public static boolean hasEvent(Transaction txn,
Class<? extends Event> eventClass) {
for (CommitAction action : txn.getActions()) {
if (action instanceof EventAction) {
Event event = ((EventAction) action).getEvent();
if (eventClass.isInstance(event)) return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,36 @@
package org.briarproject.bramble.test;
import org.junit.Test;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
public class ThreadExceptionTest extends BrambleTestCase {
@Test(expected = AssertionError.class)
public void testAssertionErrorMakesTestCaseFail() {
// This is what BrambleTestCase does, too:
fail();
}
@Test
public void testExceptionInThreadMakesTestCaseFail() {
Thread t = new Thread(() -> {
System.out.println("thread before exception");
throw new RuntimeException("boom");
});
t.start();
try {
t.join();
System.out.println("joined thread");
} catch (InterruptedException e) {
System.out.println("interrupted while joining thread");
fail();
}
assertNotNull(exceptionInBackgroundThread);
exceptionInBackgroundThread = null;
}
}

View File

@@ -1,6 +1,7 @@
dependencyVerification {
verify = [
'cglib:cglib:3.2.8:cglib-3.2.8.jar:3f64de999ecc5595dc84ca8ff0879d8a34c8623f9ef3c517a53ed59023fcb9db',
'com.fasterxml.jackson.core:jackson-annotations:2.13.0:jackson-annotations-2.13.0.jar:81f9724d8843e8b08f8f6c0609e7a2b030d00c34861c4ac7e2099a7235047d6f',
'com.google.code.findbugs:annotations:3.0.1:annotations-3.0.1.jar:6b47ff0a6de0ce17cbedc3abb0828ca5bce3009d53ea47b3723ff023c4742f79',
'com.google.code.findbugs:jsr305:3.0.2:jsr305-3.0.2.jar:766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7',
'com.google.dagger:dagger:2.33:dagger-2.33.jar:d8798c5b8cf6b125234e33af5c6293bb9f2208ce29b57924c35b8c0be7b6bdcb',

View File

@@ -10,13 +10,18 @@ apply from: '../dagger.gradle'
dependencies {
implementation project(path: ':bramble-api', configuration: 'default')
implementation 'org.bouncycastle:bcprov-jdk15on:1.69'
implementation 'org.bouncycastle:bcprov-jdk15to18:1.70'
//noinspection GradleDependency
implementation 'com.h2database:h2:1.4.192' // The last version that supports Java 1.6
implementation 'org.bitlet:weupnp:0.1.4'
implementation 'net.i2p.crypto:eddsa:0.2.0'
implementation 'org.whispersystems:curve25519-java:0.5.0'
implementation 'org.briarproject:jtorctl:0.3'
//noinspection GradleDependency
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
testImplementation project(path: ':bramble-api', configuration: 'testOutput')
@@ -25,7 +30,8 @@ dependencies {
testImplementation "junit:junit:$junit_version"
testImplementation "org.jmock:jmock:$jmock_version"
testImplementation "org.jmock:jmock-junit4:$jmock_version"
testImplementation "org.jmock:jmock-legacy:$jmock_version"
testImplementation "org.jmock:jmock-imposters:$jmock_version"
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.3"
testAnnotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"

View File

@@ -6,6 +6,7 @@ import org.briarproject.bramble.crypto.CryptoExecutorModule;
import org.briarproject.bramble.db.DatabaseExecutorModule;
import org.briarproject.bramble.identity.IdentityModule;
import org.briarproject.bramble.lifecycle.LifecycleModule;
import org.briarproject.bramble.mailbox.MailboxModule;
import org.briarproject.bramble.plugin.PluginModule;
import org.briarproject.bramble.properties.PropertiesModule;
import org.briarproject.bramble.rendezvous.RendezvousModule;
@@ -28,6 +29,8 @@ public interface BrambleCoreEagerSingletons {
void inject(LifecycleModule.EagerSingletons init);
void inject(MailboxModule.EagerSingletons init);
void inject(PluginModule.EagerSingletons init);
void inject(PropertiesModule.EagerSingletons init);
@@ -51,6 +54,7 @@ public interface BrambleCoreEagerSingletons {
c.inject(new DatabaseExecutorModule.EagerSingletons());
c.inject(new IdentityModule.EagerSingletons());
c.inject(new LifecycleModule.EagerSingletons());
c.inject(new MailboxModule.EagerSingletons());
c.inject(new RendezvousModule.EagerSingletons());
c.inject(new PluginModule.EagerSingletons());
c.inject(new PropertiesModule.EagerSingletons());

View File

@@ -14,7 +14,6 @@ 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.logging.LoggingModule;
import org.briarproject.bramble.mailbox.MailboxModule;
import org.briarproject.bramble.plugin.PluginModule;
import org.briarproject.bramble.properties.PropertiesModule;
@@ -45,7 +44,6 @@ import dagger.Module;
IoModule.class,
KeyAgreementModule.class,
LifecycleModule.class,
LoggingModule.class,
MailboxModule.class,
PluginModule.class,
PropertiesModule.class,

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.client;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.CryptoComponent;
@@ -22,6 +23,9 @@ import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorFactory;
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxPropertiesUpdate;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.properties.TransportProperties;
@@ -29,6 +33,7 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.util.Base32;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -39,6 +44,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@@ -46,6 +52,12 @@ import static org.briarproject.bramble.api.client.ContactGroupConstants.GROUP_KE
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_COUNT;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_KEY_AUTHTOKEN;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_KEY_INBOXID;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_KEY_ONION;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_KEY_OUTBOXID;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_ONION_LENGTH;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
import static org.briarproject.bramble.util.ValidationUtils.checkLength;
@@ -399,6 +411,35 @@ class ClientHelperImpl implements ClientHelper {
return tpMap;
}
@Override
@Nullable
public MailboxPropertiesUpdate parseAndValidateMailboxPropertiesUpdate(
BdfDictionary properties) throws FormatException {
if (properties.isEmpty()) {
return null;
}
// Accepting more props than we need, for forward compatibility
if (properties.size() < PROP_COUNT) {
throw new FormatException();
}
String onion = properties.getString(PROP_KEY_ONION);
checkLength(onion, PROP_ONION_LENGTH);
try {
Base32.decode(onion, true);
} catch (IllegalArgumentException e) {
throw new FormatException();
}
byte[] authToken = properties.getRaw(PROP_KEY_AUTHTOKEN);
checkLength(authToken, UniqueId.LENGTH);
byte[] inboxId = properties.getRaw(PROP_KEY_INBOXID);
checkLength(inboxId, UniqueId.LENGTH);
byte[] outboxId = properties.getRaw(PROP_KEY_OUTBOXID);
checkLength(outboxId, UniqueId.LENGTH);
return new MailboxPropertiesUpdate(onion,
new MailboxAuthToken(authToken), new MailboxFolderId(inboxId),
new MailboxFolderId(outboxId));
}
@Override
public ContactId getContactId(Transaction txn, GroupId contactGroupId)
throws DbException {

View File

@@ -121,28 +121,40 @@ class ContactManagerImpl implements ContactManager, EventListener {
@Override
public String getHandshakeLink() throws DbException {
KeyPair keyPair = db.transactionWithResult(true,
identityManager::getHandshakeKeys);
return db.transactionWithResult(true, this::getHandshakeLink);
}
@Override
public String getHandshakeLink(Transaction txn) throws DbException {
KeyPair keyPair = identityManager.getHandshakeKeys(txn);
return pendingContactFactory.createHandshakeLink(keyPair.getPublic());
}
@Override
public PendingContact addPendingContact(Transaction txn, String link,
String alias)
throws DbException, FormatException, GeneralSecurityException {
PendingContact p =
pendingContactFactory.createPendingContact(link, alias);
AuthorId local = identityManager.getLocalAuthor(txn).getId();
db.addPendingContact(txn, p, local);
KeyPair ourKeyPair = identityManager.getHandshakeKeys(txn);
keyManager.addPendingContact(txn, p.getId(), p.getPublicKey(),
ourKeyPair);
return p;
}
@Override
public PendingContact addPendingContact(String link, String alias)
throws DbException, FormatException, GeneralSecurityException {
PendingContact p =
pendingContactFactory.createPendingContact(link, alias);
Transaction txn = db.startTransaction(false);
try {
AuthorId local = identityManager.getLocalAuthor(txn).getId();
db.addPendingContact(txn, p, local);
KeyPair ourKeyPair = identityManager.getHandshakeKeys(txn);
keyManager.addPendingContact(txn, p.getId(), p.getPublicKey(),
ourKeyPair);
PendingContact p = addPendingContact(txn, link, alias);
db.commitTransaction(txn);
return p;
} finally {
db.endTransaction(txn);
}
return p;
}
@Override
@@ -154,8 +166,14 @@ class ContactManagerImpl implements ContactManager, EventListener {
@Override
public Collection<Pair<PendingContact, PendingContactState>> getPendingContacts()
throws DbException {
Collection<PendingContact> pendingContacts =
db.transactionWithResult(true, db::getPendingContacts);
return db.transactionWithResult(true, this::getPendingContacts);
}
@Override
public Collection<Pair<PendingContact, PendingContactState>> getPendingContacts(
Transaction txn)
throws DbException {
Collection<PendingContact> pendingContacts = db.getPendingContacts(txn);
List<Pair<PendingContact, PendingContactState>> pairs =
new ArrayList<>(pendingContacts.size());
for (PendingContact p : pendingContacts) {
@@ -168,7 +186,13 @@ class ContactManagerImpl implements ContactManager, EventListener {
@Override
public void removePendingContact(PendingContactId p) throws DbException {
db.transaction(false, txn -> db.removePendingContact(txn, p));
db.transaction(false, txn -> removePendingContact(txn, p));
}
@Override
public void removePendingContact(Transaction txn, PendingContactId p)
throws DbException {
db.removePendingContact(txn, p);
states.remove(p);
}

View File

@@ -8,6 +8,7 @@ import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.Blake2bDigest;
import org.bouncycastle.crypto.digests.SHA3Digest;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.crypto.AgreementPrivateKey;
import org.briarproject.bramble.api.crypto.AgreementPublicKey;
import org.briarproject.bramble.api.crypto.CryptoComponent;
@@ -41,6 +42,7 @@ import javax.inject.Inject;
import static java.lang.System.arraycopy;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_AGREEMENT;
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_SIGNATURE;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
@@ -54,7 +56,7 @@ import static org.briarproject.bramble.util.LogUtils.now;
class CryptoComponentImpl implements CryptoComponent {
private static final Logger LOG =
Logger.getLogger(CryptoComponentImpl.class.getName());
getLogger(CryptoComponentImpl.class.getName());
private static final int SIGNATURE_KEY_PAIR_BITS = 256;
private static final int STORAGE_IV_BYTES = 24; // 196 bits
@@ -128,6 +130,13 @@ class CryptoComponentImpl implements CryptoComponent {
}
}
@Override
public UniqueId generateUniqueId() {
byte[] b = new byte[UniqueId.LENGTH];
secureRandom.nextBytes(b);
return new UniqueId(b);
}
@Override
public SecretKey generateSecretKey() {
byte[] b = new byte[SecretKey.LENGTH];
@@ -449,7 +458,7 @@ class CryptoComponentImpl implements CryptoComponent {
}
@Override
public String encodeOnionAddress(byte[] publicKey) {
public String encodeOnion(byte[] publicKey) {
Digest digest = new SHA3Digest(256);
byte[] label = ".onion checksum".getBytes(Charset.forName("US-ASCII"));
digest.update(label, 0, label.length);

View File

@@ -757,6 +757,14 @@ interface Database<T> {
*/
void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException;
/**
* Resets the transmission count, expiry time and max latency of all
* messages that are eligible to be sent to the given contact. This includes
* messages that have already been sent and are not yet due for
* retransmission.
*/
void resetUnackedMessagesToSend(T txn, ContactId c) throws DbException;
/**
* Sets the cleanup timer duration for the given message. This does not
* start the message's cleanup timer.
@@ -841,12 +849,14 @@ interface Database<T> {
void stopCleanupTimer(T txn, MessageId m) throws DbException;
/**
* Updates the transmission count, expiry time and estimated time of arrival
* of the given message with respect to the given contact, using the latency
* of the transport over which it was sent.
* Updates the transmission count, expiry time and max latency of the given
* message with respect to the given contact.
*
* @param maxLatency latency of the transport over which the message was
* sent.
*/
void updateExpiryTimeAndEta(T txn, ContactId c, MessageId m, long maxLatency)
throws DbException;
void updateRetransmissionData(T txn, ContactId c, MessageId m,
long maxLatency) throws DbException;
/**
* Stores the given transport keys, deleting any keys they have replaced.

View File

@@ -437,7 +437,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
Message message = db.getMessage(txn, m);
totalLength += message.getRawLength();
messages.add(message);
db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
db.updateRetransmissionData(txn, c, m, maxLatency);
}
if (ids.isEmpty()) return null;
db.lowerRequestedFlag(txn, c, ids);
@@ -462,7 +462,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
totalLength += message.getRawLength();
messages.add(message);
sentIds.add(m);
db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
db.updateRetransmissionData(txn, c, m, maxLatency);
}
}
if (messages.isEmpty()) return messages;
@@ -483,7 +483,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.getMessagesToOffer(txn, c, maxMessages, maxLatency);
if (ids.isEmpty()) return null;
for (MessageId m : ids)
db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
db.updateRetransmissionData(txn, c, m, maxLatency);
return new Offer(ids);
}
@@ -518,7 +518,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
Message message = db.getMessage(txn, m);
totalLength += message.getRawLength();
messages.add(message);
db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
db.updateRetransmissionData(txn, c, m, maxLatency);
}
if (ids.isEmpty()) return null;
db.lowerRequestedFlag(txn, c, ids);
@@ -750,6 +750,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getUnackedMessagesToSend(txn, c);
}
@Override
public void resetUnackedMessagesToSend(Transaction transaction, ContactId c)
throws DbException {
T txn = unbox(transaction);
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
db.resetUnackedMessagesToSend(txn, c);
}
@Override
public long getUnackedMessageBytesToSend(Transaction transaction,
ContactId c) throws DbException {

View File

@@ -2,8 +2,6 @@ package org.briarproject.bramble.db;
import org.briarproject.bramble.api.settings.Settings;
import static java.util.concurrent.TimeUnit.DAYS;
interface DatabaseConstants {
/**
@@ -25,19 +23,6 @@ interface DatabaseConstants {
*/
String SCHEMA_VERSION_KEY = "schemaVersion";
/**
* The {@link Settings} key under which the time of the last database
* compaction is stored.
*/
String LAST_COMPACTED_KEY = "lastCompacted";
/**
* The maximum time between database compactions in milliseconds. When the
* database is opened it will be compacted if more than this amount of time
* has passed since the last compaction.
*/
long MAX_COMPACTION_INTERVAL_MS = DAYS.toMillis(30);
/**
* The {@link Settings} key under which the flag is stored indicating
* whether the database is marked as dirty.

View File

@@ -85,12 +85,17 @@ class H2Database extends JdbcDatabase {
public void close() throws DbException {
// H2 will close the database when the last connection closes
Connection c = null;
Statement s = null;
try {
c = createConnection();
super.closeAllConnections();
closeAllConnections();
setDirty(c, false);
s = c.createStatement();
s.execute("SHUTDOWN COMPACT");
s.close();
c.close();
} catch (SQLException e) {
tryToClose(s, LOG, WARNING);
tryToClose(c, LOG, WARNING);
throw new DbException(e);
}

View File

@@ -79,11 +79,11 @@ class HyperSqlDatabase extends JdbcDatabase {
Connection c = null;
Statement s = null;
try {
super.closeAllConnections();
closeAllConnections();
c = createConnection();
setDirty(c, false);
s = c.createStatement();
s.executeQuery("SHUTDOWN");
s.executeQuery("SHUTDOWN COMPACT");
s.close();
c.close();
} catch (SQLException e) {
@@ -106,7 +106,7 @@ class HyperSqlDatabase extends JdbcDatabase {
Connection c = null;
Statement s = null;
try {
super.closeAllConnections();
closeAllConnections();
c = createConnection();
s = c.createStatement();
s.executeQuery("SHUTDOWN COMPACT");

View File

@@ -85,8 +85,6 @@ import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING;
import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN;
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
import static org.briarproject.bramble.db.DatabaseConstants.DIRTY_KEY;
import static org.briarproject.bramble.db.DatabaseConstants.LAST_COMPACTED_KEY;
import static org.briarproject.bramble.db.DatabaseConstants.MAX_COMPACTION_INTERVAL_MS;
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
@@ -102,7 +100,7 @@ import static org.briarproject.bramble.util.LogUtils.now;
abstract class JdbcDatabase implements Database<Connection> {
// Package access for testing
static final int CODE_SCHEMA_VERSION = 49;
static final int CODE_SCHEMA_VERSION = 50;
// Time period offsets for incoming transport keys
private static final int OFFSET_PREV = -1;
@@ -252,7 +250,7 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " requested BOOLEAN NOT NULL,"
+ " expiry BIGINT NOT NULL,"
+ " txCount INT NOT NULL,"
+ " eta BIGINT NOT NULL,"
+ " maxLatency BIGINT," // Null if latency was reset
+ " PRIMARY KEY (messageId, contactId),"
+ " FOREIGN KEY (messageId)"
+ " REFERENCES messages (messageId)"
@@ -378,8 +376,7 @@ abstract class JdbcDatabase implements Database<Connection> {
throws DbException, SQLException;
// Used exclusively during open to compact the database after schema
// migrations or after DatabaseConstants#MAX_COMPACTION_INTERVAL_MS has
// elapsed
// migrations or if the database was not shut down cleanly
protected abstract void compactAndClose() throws DbException;
JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory,
@@ -405,7 +402,8 @@ abstract class JdbcDatabase implements Database<Connection> {
if (reopen) {
Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
wasDirtyOnInitialisation = isDirty(s);
compact = migrateSchema(txn, s, listener) || isCompactionDue(s);
boolean migrated = migrateSchema(txn, s, listener);
compact = wasDirtyOnInitialisation || migrated;
} else {
wasDirtyOnInitialisation = false;
createTables(txn);
@@ -429,16 +427,11 @@ abstract class JdbcDatabase implements Database<Connection> {
compactAndClose();
logDuration(LOG, "Compacting database", start);
// Allow the next transaction to reopen the DB
synchronized (connectionsLock) {
closed = false;
}
txn = startTransaction();
connectionsLock.lock();
try {
storeLastCompacted(txn);
commitTransaction(txn);
} catch (DbException e) {
abortTransaction(txn);
throw e;
closed = false;
} finally {
connectionsLock.unlock();
}
}
}
@@ -499,18 +492,11 @@ abstract class JdbcDatabase implements Database<Connection> {
new Migration45_46(),
new Migration46_47(dbTypes),
new Migration47_48(),
new Migration48_49()
new Migration48_49(),
new Migration49_50()
);
}
private boolean isCompactionDue(Settings s) {
long lastCompacted = s.getLong(LAST_COMPACTED_KEY, 0);
long elapsed = clock.currentTimeMillis() - lastCompacted;
if (LOG.isLoggable(INFO))
LOG.info(elapsed + " ms since last compaction");
return elapsed > MAX_COMPACTION_INTERVAL_MS;
}
private void storeSchemaVersion(Connection txn, int version)
throws DbException {
Settings s = new Settings();
@@ -518,12 +504,6 @@ abstract class JdbcDatabase implements Database<Connection> {
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
}
private void storeLastCompacted(Connection txn) throws DbException {
Settings s = new Settings();
s.putLong(LAST_COMPACTED_KEY, clock.currentTimeMillis());
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
}
private boolean isDirty(Settings s) {
return s.getBoolean(DIRTY_KEY, false);
}
@@ -537,7 +517,6 @@ abstract class JdbcDatabase implements Database<Connection> {
private void initialiseSettings(Connection txn) throws DbException {
Settings s = new Settings();
s.putInt(SCHEMA_VERSION_KEY, CODE_SCHEMA_VERSION);
s.putLong(LAST_COMPACTED_KEY, clock.currentTimeMillis());
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
}
@@ -917,9 +896,10 @@ abstract class JdbcDatabase implements Database<Connection> {
try {
String sql = "INSERT INTO statuses (messageId, contactId, groupId,"
+ " timestamp, length, state, groupShared, messageShared,"
+ " deleted, ack, seen, requested, expiry, txCount, eta)"
+ " deleted, ack, seen, requested, expiry, txCount,"
+ " maxLatency)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE, 0, 0,"
+ " 0)";
+ " NULL)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setInt(2, c.getInt());
@@ -1153,17 +1133,17 @@ abstract class JdbcDatabase implements Database<Connection> {
ps.setInt(2, DELIVERED.getValue());
} else {
long now = clock.currentTimeMillis();
long eta = now + maxLatency;
sql = "SELECT NULL FROM statuses"
+ " WHERE contactId = ? AND state = ?"
+ " AND groupShared = TRUE AND messageShared = TRUE"
+ " AND deleted = FALSE AND seen = FALSE"
+ " AND (expiry <= ? OR eta > ?)";
+ " AND (expiry <= ? OR maxLatency IS NULL"
+ " OR ? < maxLatency)";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setInt(2, DELIVERED.getValue());
ps.setLong(3, now);
ps.setLong(4, eta);
ps.setLong(4, maxLatency);
}
rs = ps.executeQuery();
boolean messagesToSend = rs.next();
@@ -2191,7 +2171,6 @@ abstract class JdbcDatabase implements Database<Connection> {
public Collection<MessageId> getMessagesToOffer(Connection txn,
ContactId c, int maxMessages, long maxLatency) throws DbException {
long now = clock.currentTimeMillis();
long eta = now + maxLatency;
PreparedStatement ps = null;
ResultSet rs = null;
try {
@@ -2200,13 +2179,14 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " AND groupShared = TRUE AND messageShared = TRUE"
+ " AND deleted = FALSE"
+ " AND seen = FALSE AND requested = FALSE"
+ " AND (expiry <= ? OR eta > ?)"
+ " AND (expiry <= ? OR maxLatency IS NULL"
+ " OR ? < maxLatency)"
+ " ORDER BY timestamp LIMIT ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setInt(2, DELIVERED.getValue());
ps.setLong(3, now);
ps.setLong(4, eta);
ps.setLong(4, maxLatency);
ps.setInt(5, maxMessages);
rs = ps.executeQuery();
List<MessageId> ids = new ArrayList<>();
@@ -2250,7 +2230,6 @@ abstract class JdbcDatabase implements Database<Connection> {
public Collection<MessageId> getMessagesToSend(Connection txn, ContactId c,
int maxLength, long maxLatency) throws DbException {
long now = clock.currentTimeMillis();
long eta = now + maxLatency;
PreparedStatement ps = null;
ResultSet rs = null;
try {
@@ -2259,13 +2238,14 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " AND groupShared = TRUE AND messageShared = TRUE"
+ " AND deleted = FALSE"
+ " AND seen = FALSE"
+ " AND (expiry <= ? OR eta > ?)"
+ " AND (expiry <= ? OR maxLatency IS NULL"
+ " OR ? < maxLatency)"
+ " ORDER BY timestamp";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setInt(2, DELIVERED.getValue());
ps.setLong(3, now);
ps.setLong(4, eta);
ps.setLong(4, maxLatency);
rs = ps.executeQuery();
List<MessageId> ids = new ArrayList<>();
int total = 0;
@@ -2549,7 +2529,6 @@ abstract class JdbcDatabase implements Database<Connection> {
public Collection<MessageId> getRequestedMessagesToSend(Connection txn,
ContactId c, int maxLength, long maxLatency) throws DbException {
long now = clock.currentTimeMillis();
long eta = now + maxLatency;
PreparedStatement ps = null;
ResultSet rs = null;
try {
@@ -2558,13 +2537,14 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " AND groupShared = TRUE AND messageShared = TRUE"
+ " AND deleted = FALSE"
+ " AND seen = FALSE AND requested = TRUE"
+ " AND (expiry <= ? OR eta > ?)"
+ " AND (expiry <= ? OR maxLatency IS NULL"
+ " OR ? < maxLatency)"
+ " ORDER BY timestamp";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setInt(2, DELIVERED.getValue());
ps.setLong(3, now);
ps.setLong(4, eta);
ps.setLong(4, maxLatency);
rs = ps.executeQuery();
List<MessageId> ids = new ArrayList<>();
int total = 0;
@@ -3290,6 +3270,30 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public void resetUnackedMessagesToSend(Connection txn, ContactId c)
throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE statuses SET expiry = 0, txCount = 0,"
+ " maxLatency = NULL"
+ " WHERE contactId = ? AND state = ?"
+ " AND groupShared = TRUE AND messageShared = TRUE"
+ " AND deleted = FALSE AND seen = FALSE";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setInt(2, DELIVERED.getValue());
int affected = ps.executeUpdate();
if (affected < 0) {
throw new DbStateException();
}
ps.close();
} catch (SQLException e) {
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public void setCleanupTimerDuration(Connection txn, MessageId m,
long duration) throws DbException {
@@ -3617,8 +3621,8 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public void updateExpiryTimeAndEta(Connection txn, ContactId c, MessageId m,
long maxLatency) throws DbException {
public void updateRetransmissionData(Connection txn, ContactId c,
MessageId m, long maxLatency) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
@@ -3634,13 +3638,12 @@ abstract class JdbcDatabase implements Database<Connection> {
rs.close();
ps.close();
sql = "UPDATE statuses"
+ " SET expiry = ?, txCount = txCount + 1, eta = ?"
+ " SET expiry = ?, txCount = txCount + 1, maxLatency = ?"
+ " WHERE messageId = ? AND contactId = ?";
ps = txn.prepareStatement(sql);
long now = clock.currentTimeMillis();
long eta = now + maxLatency;
ps.setLong(1, calculateExpiry(now, maxLatency, txCount));
ps.setLong(2, eta);
ps.setLong(2, maxLatency);
ps.setBytes(3, m.getBytes());
ps.setInt(4, c.getInt());
int affected = ps.executeUpdate();

View File

@@ -0,0 +1,45 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DbException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
class Migration49_50 implements Migration<Connection> {
private static final Logger LOG = getLogger(Migration49_50.class.getName());
@Override
public int getStartVersion() {
return 49;
}
@Override
public int getEndVersion() {
return 50;
}
@Override
public void migrate(Connection txn) throws DbException {
Statement s = null;
try {
s = txn.createStatement();
s.execute("ALTER TABLE statuses"
+ " ALTER COLUMN eta"
+ " RENAME TO maxLatency");
s.execute("ALTER TABLE statuses"
+ " ALTER COLUMN maxLatency"
+ " SET NULL");
s.execute("UPDATE statuses SET maxLatency = NULL");
} catch (SQLException e) {
tryToClose(s, LOG, WARNING);
throw new DbException(e);
}
}
}

View File

@@ -1,18 +1,49 @@
package org.briarproject.bramble.io;
import org.briarproject.bramble.api.WeakSingletonProvider;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import javax.annotation.Nonnull;
import javax.inject.Singleton;
import javax.net.SocketFactory;
import dagger.Module;
import dagger.Provides;
import okhttp3.Dns;
import okhttp3.OkHttpClient;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@Module
public class IoModule {
private static final int CONNECT_TIMEOUT = 60_000; // Milliseconds
@Provides
@Singleton
TimeoutMonitor provideTimeoutMonitor(TimeoutMonitorImpl timeoutMonitor) {
return timeoutMonitor;
}
// Share an HTTP client instance between requests where possible, while
// allowing the client to be garbage-collected between requests. The
// provider keeps a weak reference to the last client instance and reuses
// the instance until it gets garbage-collected. See
// https://medium.com/@leandromazzuquini/if-you-are-using-okhttp-you-should-know-this-61d68e065a2b
@Provides
@Singleton
WeakSingletonProvider<OkHttpClient> provideOkHttpClientProvider(
SocketFactory torSocketFactory, Dns noDnsLookups) {
return new WeakSingletonProvider<OkHttpClient>() {
@Override
@Nonnull
public OkHttpClient createInstance() {
return new OkHttpClient.Builder()
.socketFactory(torSocketFactory)
.dns(noDnsLookups) // Don't make local DNS lookups
.connectTimeout(CONNECT_TIMEOUT, MILLISECONDS)
.build();
}
};
}
}

View File

@@ -1,44 +0,0 @@
package org.briarproject.bramble.logging;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.system.TaskScheduler;
import java.io.OutputStream;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Formatter;
import java.util.logging.LogRecord;
import java.util.logging.StreamHandler;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
class FlushingStreamHandler extends StreamHandler {
private static final int FLUSH_DELAY_MS = 5_000;
private final TaskScheduler scheduler;
private final Executor ioExecutor;
private final AtomicBoolean flushScheduled = new AtomicBoolean(false);
FlushingStreamHandler(TaskScheduler scheduler,
Executor ioExecutor, OutputStream out, Formatter formatter) {
super(out, formatter);
this.scheduler = scheduler;
this.ioExecutor = ioExecutor;
}
@Override
public void publish(LogRecord record) {
super.publish(record);
if (!flushScheduled.getAndSet(true)) {
scheduler.schedule(this::scheduledFlush, ioExecutor,
FLUSH_DELAY_MS, MILLISECONDS);
}
}
@IoExecutor
private void scheduledFlush() {
flushScheduled.set(false);
flush();
}
}

View File

@@ -1,29 +0,0 @@
package org.briarproject.bramble.logging;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.logging.PersistentLogManager;
import java.util.logging.Formatter;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class LoggingModule {
@Provides
Formatter provideFormatter() {
return new BriefLogFormatter();
}
@Provides
@Singleton
PersistentLogManager providePersistentLogManager(
LifecycleManager lifecycleManager,
PersistentLogManagerImpl persistentLogManager) {
lifecycleManager.registerOpenDatabaseHook(persistentLogManager);
return persistentLogManager;
}
}

View File

@@ -1,176 +0,0 @@
package org.briarproject.bramble.logging;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook;
import org.briarproject.bramble.api.lifecycle.ShutdownManager;
import org.briarproject.bramble.api.logging.PersistentLogManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.system.TaskScheduler;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import org.briarproject.bramble.api.transport.StreamWriter;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Logger;
import java.util.logging.StreamHandler;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@ThreadSafe
@NotNullByDefault
class PersistentLogManagerImpl implements PersistentLogManager,
OpenDatabaseHook {
private static final Logger LOG =
getLogger(PersistentLogManagerImpl.class.getName());
private static final String LOG_FILE = "briar.log";
private static final String OLD_LOG_FILE = "briar.log.old";
private final TaskScheduler scheduler;
private final Executor ioExecutor;
private final ShutdownManager shutdownManager;
private final DatabaseComponent db;
private final StreamReaderFactory streamReaderFactory;
private final StreamWriterFactory streamWriterFactory;
private final Formatter formatter;
private final SecretKey logKey;
private final AtomicReference<Integer> shutdownHookHandle =
new AtomicReference<>();
@Nullable
private volatile SecretKey oldLogKey = null;
@Inject
PersistentLogManagerImpl(
TaskScheduler scheduler,
@IoExecutor Executor ioExecutor,
ShutdownManager shutdownManager,
DatabaseComponent db,
StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory,
Formatter formatter,
CryptoComponent crypto) {
this.scheduler = scheduler;
this.ioExecutor = ioExecutor;
this.shutdownManager = shutdownManager;
this.db = db;
this.streamReaderFactory = streamReaderFactory;
this.streamWriterFactory = streamWriterFactory;
this.formatter = formatter;
logKey = crypto.generateSecretKey();
}
@Override
public void onDatabaseOpened(Transaction txn) throws DbException {
Settings s = db.getSettings(txn, LOG_SETTINGS_NAMESPACE);
// Load the old log key, if any
byte[] oldKeyBytes = s.getBytes(LOG_KEY_KEY);
if (oldKeyBytes != null && oldKeyBytes.length == SecretKey.LENGTH) {
LOG.info("Loaded old log key");
oldLogKey = new SecretKey(oldKeyBytes);
}
// Store the current log key
s.putBytes(LOG_KEY_KEY, logKey.getBytes());
db.mergeSettings(txn, s, LOG_SETTINGS_NAMESPACE);
}
@Override
public Handler createLogHandler(File dir) throws IOException {
File logFile = new File(dir, LOG_FILE);
File oldLogFile = new File(dir, OLD_LOG_FILE);
if (oldLogFile.exists() && !oldLogFile.delete())
LOG.warning("Failed to delete old log file");
if (logFile.exists() && !logFile.renameTo(oldLogFile))
LOG.warning("Failed to rename log file");
try {
OutputStream out = new FileOutputStream(logFile);
StreamWriter writer =
streamWriterFactory.createLogStreamWriter(out, logKey);
StreamHandler handler = new FlushingStreamHandler(scheduler,
ioExecutor, writer.getOutputStream(), formatter);
// Flush the log and terminate the stream at shutdown
Runnable shutdownHook = () -> {
LOG.info("Shutting down");
handler.flush();
try {
writer.sendEndOfStream();
} catch (IOException e) {
logException(LOG, WARNING, e);
}
};
int handle = shutdownManager.addShutdownHook(shutdownHook);
// If a previous handler registered a shutdown hook, remove it
Integer oldHandle = shutdownHookHandle.getAndSet(handle);
if (oldHandle != null) {
shutdownManager.removeShutdownHook(oldHandle);
}
return handler;
} catch (SecurityException e) {
throw new IOException(e);
}
}
@Override
public void addLogHandler(File dir, Logger logger) throws IOException {
for (Handler h : logger.getHandlers()) {
if (h instanceof FlushingStreamHandler) logger.removeHandler(h);
}
logger.addHandler(createLogHandler(dir));
}
@Override
public Scanner getPersistentLog(File dir, boolean old)
throws IOException {
if (old) {
SecretKey oldLogKey = this.oldLogKey;
if (oldLogKey == null) {
LOG.info("Old log key has not been loaded");
return emptyScanner();
}
return getPersistentLog(new File(dir, OLD_LOG_FILE), oldLogKey);
} else {
return getPersistentLog(new File(dir, LOG_FILE), logKey);
}
}
private Scanner getPersistentLog(File logFile, SecretKey key)
throws IOException {
if (logFile.exists()) {
LOG.info("Reading log file");
InputStream in = new FileInputStream(logFile);
return new Scanner(streamReaderFactory.createLogStreamReader(in,
key));
} else {
LOG.info("Log file does not exist");
return emptyScanner();
}
}
private Scanner emptyScanner() {
return new Scanner(new ByteArrayInputStream(new byte[0]));
}
}

View File

@@ -0,0 +1,177 @@
package org.briarproject.bramble.mailbox;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
import org.briarproject.bramble.api.mailbox.MailboxFileId;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
interface MailboxApi {
/**
* Sets up the mailbox with the setup token.
*
* @param properties MailboxProperties with the setup token
* @return the owner token
* @throws ApiException for 401 response.
*/
MailboxAuthToken setup(MailboxProperties properties)
throws IOException, ApiException;
/**
* Checks the status of the mailbox.
*
* @return true if the status is OK, false otherwise.
* @throws ApiException for 401 response.
*/
boolean checkStatus(MailboxProperties properties)
throws IOException, ApiException;
/**
* Unpairs Briar and the mailbox (owner only).
* Resets mailbox state to that after first install
* (e.g. removes all stored files as well).
*/
void wipeMailbox(MailboxProperties properties)
throws IOException, ApiException;
/**
* Adds a new contact to the mailbox.
*
* @throws TolerableFailureException if response code is 409
* (contact was already added).
*/
void addContact(MailboxProperties properties, MailboxContact contact)
throws IOException, ApiException, TolerableFailureException;
/**
* Deletes a contact from the mailbox.
* This should get called after a contact was removed from Briar.
*
* @throws TolerableFailureException if response code is 404
* (contact probably was already deleted).
*/
void deleteContact(MailboxProperties properties, ContactId contactId)
throws IOException, ApiException, TolerableFailureException;
/**
* Gets a list of {@link ContactId}s from the mailbox.
* These are the contacts that the mailbox already knows about.
*/
Collection<ContactId> getContacts(MailboxProperties properties)
throws IOException, ApiException;
/**
* Used by contacts to send files to the owner
* and by the owner to send files to contacts.
* <p>
* The owner can add files to the contacts' inboxes
* and the contacts can add files to their own outbox.
*/
void addFile(MailboxProperties properties, MailboxFolderId folderId,
File file) throws IOException, ApiException;
/**
* Used by owner and contacts to list their files to retrieve.
* <p>
* Returns 200 OK with the list of files in JSON.
*/
List<MailboxFile> getFiles(MailboxProperties properties,
MailboxFolderId folderId) throws IOException, ApiException;
/**
* Used by owner and contacts to retrieve a file.
* <p>
* Returns 200 OK if successful with the files' raw bytes
* in the response body.
*
* @param file the empty file the response bytes will be written into.
*/
void getFile(MailboxProperties properties, MailboxFolderId folderId,
MailboxFileId fileId, File file) throws IOException, ApiException;
/**
* Used by owner and contacts to delete files.
* <p>
* Returns 200 OK (no exception) if deletion was successful.
*
* @throws TolerableFailureException on 404 response,
* because file was most likely deleted already.
*/
void deleteFile(MailboxProperties properties, MailboxFolderId folderId,
MailboxFileId fileId)
throws IOException, ApiException, TolerableFailureException;
/**
* Lists all contact outboxes that have files available
* for the owner to download.
*
* @return a list of folder names
* to be used with {@link #getFiles(MailboxProperties, MailboxFolderId)}.
* @throws IllegalArgumentException if used by non-owner.
*/
List<MailboxFolderId> getFolders(MailboxProperties properties)
throws IOException, ApiException;
@Immutable
@JsonSerialize
class MailboxContact {
public final int contactId;
public final MailboxAuthToken token;
public final MailboxFolderId inboxId, outboxId;
MailboxContact(ContactId contactId,
MailboxAuthToken token,
MailboxFolderId inboxId,
MailboxFolderId outboxId) {
this.contactId = contactId.getInt();
this.token = token;
this.inboxId = inboxId;
this.outboxId = outboxId;
}
}
@JsonSerialize
class MailboxFile implements Comparable<MailboxFile> {
public final MailboxFileId name;
public final long time;
public MailboxFile(MailboxFileId name, long time) {
this.name = name;
this.time = time;
}
@Override
public int compareTo(@Nonnull MailboxApi.MailboxFile mailboxFile) {
//noinspection UseCompareMethod
return time < mailboxFile.time ? -1 :
(time == mailboxFile.time ? 0 : 1);
}
}
@Immutable
class ApiException extends Exception {
}
@Immutable
class MailboxAlreadyPairedException extends ApiException {
}
/**
* A failure that does not need to be retried,
* e.g. when adding a contact that already exists.
*/
@Immutable
class TolerableFailureException extends Exception {
}
}

View File

@@ -0,0 +1,301 @@
package org.briarproject.bramble.mailbox;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.briarproject.bramble.api.WeakSingletonProvider;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.mailbox.InvalidMailboxIdException;
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
import org.briarproject.bramble.api.mailbox.MailboxFileId;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import static com.fasterxml.jackson.databind.MapperFeature.BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES;
import static java.util.Objects.requireNonNull;
import static okhttp3.internal.Util.EMPTY_REQUEST;
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
@NotNullByDefault
class MailboxApiImpl implements MailboxApi {
private final WeakSingletonProvider<OkHttpClient> httpClientProvider;
private final JsonMapper mapper = JsonMapper.builder()
.enable(BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES)
.build();
private static final MediaType JSON =
requireNonNull(MediaType.parse("application/json; charset=utf-8"));
private static final MediaType FILE =
requireNonNull(MediaType.parse("application/octet-stream"));
@Inject
MailboxApiImpl(WeakSingletonProvider<OkHttpClient> httpClientProvider) {
this.httpClientProvider = httpClientProvider;
}
@Override
public MailboxAuthToken setup(MailboxProperties properties)
throws IOException, ApiException {
if (!properties.isOwner()) throw new IllegalArgumentException();
Request request = getRequestBuilder(properties.getAuthToken())
.url(properties.getBaseUrl() + "/setup")
.put(EMPTY_REQUEST)
.build();
OkHttpClient client = httpClientProvider.get();
Response response = client.newCall(request).execute();
if (response.code() == 401) throw new MailboxAlreadyPairedException();
if (!response.isSuccessful()) throw new ApiException();
ResponseBody body = response.body();
if (body == null) throw new ApiException();
try {
JsonNode node = mapper.readTree(body.string());
JsonNode tokenNode = node.get("token");
if (tokenNode == null) {
throw new ApiException();
}
String ownerToken = tokenNode.textValue();
return MailboxAuthToken.fromString(ownerToken);
} catch (JacksonException | InvalidMailboxIdException e) {
throw new ApiException();
}
}
@Override
public boolean checkStatus(MailboxProperties properties)
throws IOException, ApiException {
if (!properties.isOwner()) throw new IllegalArgumentException();
Response response = sendGetRequest(properties, "/status");
if (response.code() == 401) throw new ApiException();
return response.isSuccessful();
}
@Override
public void wipeMailbox(MailboxProperties properties)
throws IOException, ApiException {
if (!properties.isOwner()) throw new IllegalArgumentException();
Request request = getRequestBuilder(properties.getAuthToken())
.url(properties.getBaseUrl() + "/")
.delete()
.build();
OkHttpClient client = httpClientProvider.get();
Response response = client.newCall(request).execute();
if (response.code() != 204) throw new ApiException();
}
/* Contact Management API (owner only) */
@Override
public void addContact(MailboxProperties properties, MailboxContact contact)
throws IOException, ApiException, TolerableFailureException {
if (!properties.isOwner()) throw new IllegalArgumentException();
byte[] bodyBytes = mapper.writeValueAsBytes(contact);
RequestBody body = RequestBody.create(JSON, bodyBytes);
Response response = sendPostRequest(properties, "/contacts", body);
if (response.code() == 409) throw new TolerableFailureException();
if (!response.isSuccessful()) throw new ApiException();
}
@Override
public void deleteContact(MailboxProperties properties, ContactId contactId)
throws IOException, ApiException, TolerableFailureException {
if (!properties.isOwner()) throw new IllegalArgumentException();
String url = properties.getBaseUrl() + "/contacts/" +
contactId.getInt();
Request request = getRequestBuilder(properties.getAuthToken())
.delete()
.url(url)
.build();
OkHttpClient client = httpClientProvider.get();
Response response = client.newCall(request).execute();
if (response.code() == 404) throw new TolerableFailureException();
if (response.code() != 200) throw new ApiException();
}
@Override
public Collection<ContactId> getContacts(MailboxProperties properties)
throws IOException, ApiException {
if (!properties.isOwner()) throw new IllegalArgumentException();
Response response = sendGetRequest(properties, "/contacts");
if (response.code() != 200) throw new ApiException();
ResponseBody body = response.body();
if (body == null) throw new ApiException();
try {
JsonNode node = mapper.readTree(body.string());
ArrayNode contactsNode = getArray(node, "contacts");
List<ContactId> list = new ArrayList<>();
for (JsonNode contactNode : contactsNode) {
if (!contactNode.isNumber()) throw new ApiException();
int id = contactNode.intValue();
if (id < 1) throw new ApiException();
list.add(new ContactId(id));
}
return list;
} catch (JacksonException e) {
throw new ApiException();
}
}
/* File Management (owner and contacts) */
@Override
public void addFile(MailboxProperties properties, MailboxFolderId folderId,
File file) throws IOException, ApiException {
String path = "/files/" + folderId;
RequestBody body = RequestBody.create(FILE, file);
Response response = sendPostRequest(properties, path, body);
if (response.code() != 200) throw new ApiException();
}
@Override
public List<MailboxFile> getFiles(MailboxProperties properties,
MailboxFolderId folderId) throws IOException, ApiException {
String path = "/files/" + folderId;
Response response = sendGetRequest(properties, path);
if (response.code() != 200) throw new ApiException();
ResponseBody body = response.body();
if (body == null) throw new ApiException();
try {
JsonNode node = mapper.readTree(body.string());
ArrayNode filesNode = getArray(node, "files");
List<MailboxFile> list = new ArrayList<>();
for (JsonNode fileNode : filesNode) {
if (!fileNode.isObject()) throw new ApiException();
ObjectNode objectNode = (ObjectNode) fileNode;
JsonNode nameNode = objectNode.get("name");
JsonNode timeNode = objectNode.get("time");
if (nameNode == null || !nameNode.isTextual()) {
throw new ApiException();
}
if (timeNode == null || !timeNode.isNumber()) {
throw new ApiException();
}
String name = nameNode.asText();
long time = timeNode.asLong();
if (time < 1) throw new ApiException();
list.add(new MailboxFile(MailboxFileId.fromString(name), time));
}
Collections.sort(list);
return list;
} catch (JacksonException | InvalidMailboxIdException e) {
throw new ApiException();
}
}
@Override
public void getFile(MailboxProperties properties, MailboxFolderId folderId,
MailboxFileId fileId, File file) throws IOException, ApiException {
String path = "/files/" + folderId + "/" + fileId;
Response response = sendGetRequest(properties, path);
if (response.code() != 200) throw new ApiException();
ResponseBody body = response.body();
if (body == null) throw new ApiException();
FileOutputStream outputStream = new FileOutputStream(file);
copyAndClose(body.byteStream(), outputStream);
}
@Override
public void deleteFile(MailboxProperties properties,
MailboxFolderId folderId, MailboxFileId fileId)
throws IOException, ApiException, TolerableFailureException {
String path = "/files/" + folderId + "/" + fileId;
Request request = getRequestBuilder(properties.getAuthToken())
.delete()
.url(properties.getBaseUrl() + path)
.build();
OkHttpClient client = httpClientProvider.get();
Response response = client.newCall(request).execute();
if (response.code() == 404) throw new TolerableFailureException();
if (response.code() != 200) throw new ApiException();
}
@Override
public List<MailboxFolderId> getFolders(MailboxProperties properties)
throws IOException, ApiException {
if (!properties.isOwner()) throw new IllegalArgumentException();
Response response = sendGetRequest(properties, "/folders");
if (response.code() != 200) throw new ApiException();
ResponseBody body = response.body();
if (body == null) throw new ApiException();
try {
JsonNode node = mapper.readTree(body.string());
ArrayNode filesNode = getArray(node, "folders");
List<MailboxFolderId> list = new ArrayList<>();
for (JsonNode fileNode : filesNode) {
if (!fileNode.isObject()) throw new ApiException();
ObjectNode objectNode = (ObjectNode) fileNode;
JsonNode idNode = objectNode.get("id");
if (idNode == null || !idNode.isTextual()) {
throw new ApiException();
}
String id = idNode.asText();
list.add(MailboxFolderId.fromString(id));
}
return list;
} catch (JacksonException | InvalidMailboxIdException e) {
throw new ApiException();
}
}
/* Helper Functions */
private Response sendGetRequest(MailboxProperties properties, String path)
throws IOException {
Request request = getRequestBuilder(properties.getAuthToken())
.url(properties.getBaseUrl() + path)
.build();
OkHttpClient client = httpClientProvider.get();
return client.newCall(request).execute();
}
private Response sendPostRequest(MailboxProperties properties, String path,
RequestBody body) throws IOException {
Request request = getRequestBuilder(properties.getAuthToken())
.url(properties.getBaseUrl() + path)
.post(body)
.build();
OkHttpClient client = httpClientProvider.get();
return client.newCall(request).execute();
}
private Request.Builder getRequestBuilder(MailboxId token) {
return new Request.Builder()
.addHeader("Authorization", "Bearer " + token);
}
/* JSON helpers */
private ArrayNode getArray(JsonNode node, String name) throws ApiException {
JsonNode arrayNode = node.get(name);
if (arrayNode == null || !arrayNode.isArray()) {
throw new ApiException();
}
return (ArrayNode) arrayNode;
}
}

View File

@@ -0,0 +1,134 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.mailbox.MailboxManager;
import org.briarproject.bramble.api.mailbox.MailboxPairingTask;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
import org.briarproject.bramble.api.mailbox.MailboxStatus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable
@NotNullByDefault
class MailboxManagerImpl implements MailboxManager {
private static final String TAG = MailboxManagerImpl.class.getName();
private final static Logger LOG = getLogger(TAG);
private final Executor ioExecutor;
private final MailboxApi api;
private final TransactionManager db;
private final MailboxSettingsManager mailboxSettingsManager;
private final MailboxPairingTaskFactory pairingTaskFactory;
private final Clock clock;
private final Object lock = new Object();
@Nullable
@GuardedBy("lock")
private MailboxPairingTask pairingTask = null;
@Inject
MailboxManagerImpl(
@IoExecutor Executor ioExecutor,
MailboxApi api,
TransactionManager db,
MailboxSettingsManager mailboxSettingsManager,
MailboxPairingTaskFactory pairingTaskFactory,
Clock clock) {
this.ioExecutor = ioExecutor;
this.api = api;
this.db = db;
this.mailboxSettingsManager = mailboxSettingsManager;
this.pairingTaskFactory = pairingTaskFactory;
this.clock = clock;
}
@Override
public boolean isPaired(Transaction txn) throws DbException {
return mailboxSettingsManager.getOwnMailboxProperties(txn) != null;
}
@Override
public MailboxStatus getMailboxStatus(Transaction txn) throws DbException {
return mailboxSettingsManager.getOwnMailboxStatus(txn);
}
@Nullable
@Override
public MailboxPairingTask getCurrentPairingTask() {
synchronized (lock) {
return pairingTask;
}
}
@Override
public MailboxPairingTask startPairingTask(String payload) {
MailboxPairingTask created;
synchronized (lock) {
if (pairingTask != null) return pairingTask;
created = pairingTaskFactory.createPairingTask(payload);
pairingTask = created;
}
ioExecutor.execute(() -> {
created.run();
synchronized (lock) {
// remove task after it finished
pairingTask = null;
}
});
return created;
}
@Override
public boolean checkConnection() {
boolean success;
try {
MailboxProperties props = db.transactionWithNullableResult(true,
mailboxSettingsManager::getOwnMailboxProperties);
success = api.checkStatus(props);
} catch (DbException e) {
logException(LOG, WARNING, e);
// we don't treat this is a failure to record
return false;
} catch (IOException | MailboxApi.ApiException e) {
// we record this as a failure
success = false;
logException(LOG, WARNING, e);
}
try {
recordCheckResult(success);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
return success;
}
private void recordCheckResult(boolean success) throws DbException {
long now = clock.currentTimeMillis();
db.transaction(false, txn -> {
if (success) {
mailboxSettingsManager.recordSuccessfulConnection(txn, now);
} else {
mailboxSettingsManager.recordFailedConnectionAttempt(txn, now);
}
});
}
}

View File

@@ -1,16 +1,86 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.data.MetadataEncoder;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.mailbox.MailboxManager;
import org.briarproject.bramble.api.mailbox.MailboxPropertyManager;
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
import org.briarproject.bramble.api.sync.validation.ValidationManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.versioning.ClientVersioningManager;
import javax.inject.Inject;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.CLIENT_ID;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.MAJOR_VERSION;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.MINOR_VERSION;
@Module
public class MailboxModule {
public static class EagerSingletons {
@Inject
MailboxPropertyValidator mailboxPropertyValidator;
@Inject
MailboxPropertyManager mailboxPropertyManager;
}
@Provides
@Singleton
MailboxManager providesMailboxManager(MailboxManagerImpl mailboxManager) {
return mailboxManager;
}
@Provides
MailboxPairingTaskFactory provideMailboxPairingTaskFactory(
MailboxPairingTaskFactoryImpl mailboxPairingTaskFactory) {
return mailboxPairingTaskFactory;
}
@Provides
MailboxSettingsManager provideMailboxSettingsManager(
MailboxSettingsManagerImpl mailboxSettingsManager) {
return mailboxSettingsManager;
}
@Provides
MailboxApi providesMailboxApi(MailboxApiImpl mailboxApi) {
return mailboxApi;
}
@Provides
@Singleton
MailboxPropertyValidator provideMailboxPropertyValidator(
ValidationManager validationManager, ClientHelper clientHelper,
MetadataEncoder metadataEncoder, Clock clock) {
MailboxPropertyValidator validator = new MailboxPropertyValidator(
clientHelper, metadataEncoder, clock);
validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION,
validator);
return validator;
}
@Provides
@Singleton
MailboxPropertyManager provideMailboxPropertyManager(
LifecycleManager lifecycleManager,
ValidationManager validationManager, ContactManager contactManager,
ClientVersioningManager clientVersioningManager,
MailboxSettingsManager mailboxSettingsManager,
MailboxPropertyManagerImpl mailboxPropertyManager) {
lifecycleManager.registerOpenDatabaseHook(mailboxPropertyManager);
validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION,
mailboxPropertyManager);
contactManager.registerContactHook(mailboxPropertyManager);
clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION,
MINOR_VERSION, mailboxPropertyManager);
mailboxSettingsManager.registerMailboxHook(mailboxPropertyManager);
return mailboxPropertyManager;
}
}

View File

@@ -0,0 +1,12 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.mailbox.MailboxPairingTask;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
interface MailboxPairingTaskFactory {
MailboxPairingTask createPairingTask(String qrCodePayload);
}

View File

@@ -0,0 +1,53 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.event.EventExecutor;
import org.briarproject.bramble.api.mailbox.MailboxPairingTask;
import org.briarproject.bramble.api.mailbox.MailboxPropertyManager;
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import java.util.concurrent.Executor;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@Immutable
@NotNullByDefault
class MailboxPairingTaskFactoryImpl implements MailboxPairingTaskFactory {
private final Executor eventExecutor;
private final DatabaseComponent db;
private final CryptoComponent crypto;
private final Clock clock;
private final MailboxApi api;
private final MailboxSettingsManager mailboxSettingsManager;
private final MailboxPropertyManager mailboxPropertyManager;
@Inject
MailboxPairingTaskFactoryImpl(
@EventExecutor Executor eventExecutor,
DatabaseComponent db,
CryptoComponent crypto,
Clock clock,
MailboxApi api,
MailboxSettingsManager mailboxSettingsManager,
MailboxPropertyManager mailboxPropertyManager) {
this.eventExecutor = eventExecutor;
this.db = db;
this.crypto = crypto;
this.clock = clock;
this.api = api;
this.mailboxSettingsManager = mailboxSettingsManager;
this.mailboxPropertyManager = mailboxPropertyManager;
}
@Override
public MailboxPairingTask createPairingTask(String qrCodePayload) {
return new MailboxPairingTaskImpl(qrCodePayload, eventExecutor, db,
crypto, clock, api, mailboxSettingsManager,
mailboxPropertyManager);
}
}

View File

@@ -0,0 +1,188 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.Consumer;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.EventExecutor;
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
import org.briarproject.bramble.api.mailbox.MailboxPairingState;
import org.briarproject.bramble.api.mailbox.MailboxPairingTask;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.mailbox.MailboxPropertiesUpdate;
import org.briarproject.bramble.api.mailbox.MailboxPropertyManager;
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
import org.briarproject.bramble.mailbox.MailboxApi.MailboxAlreadyPairedException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@ThreadSafe
@NotNullByDefault
class MailboxPairingTaskImpl implements MailboxPairingTask {
private final static Logger LOG =
getLogger(MailboxPairingTaskImpl.class.getName());
@SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19
private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
private static final int VERSION_REQUIRED = 32;
private final String payload;
private final Executor eventExecutor;
private final DatabaseComponent db;
private final CryptoComponent crypto;
private final Clock clock;
private final MailboxApi api;
private final MailboxSettingsManager mailboxSettingsManager;
private final MailboxPropertyManager mailboxPropertyManager;
private final Object lock = new Object();
@GuardedBy("lock")
private final List<Consumer<MailboxPairingState>> observers =
new ArrayList<>();
@GuardedBy("lock")
private MailboxPairingState state;
MailboxPairingTaskImpl(
String payload,
@EventExecutor Executor eventExecutor,
DatabaseComponent db,
CryptoComponent crypto,
Clock clock,
MailboxApi api,
MailboxSettingsManager mailboxSettingsManager,
MailboxPropertyManager mailboxPropertyManager) {
this.payload = payload;
this.eventExecutor = eventExecutor;
this.db = db;
this.crypto = crypto;
this.clock = clock;
this.api = api;
this.mailboxSettingsManager = mailboxSettingsManager;
this.mailboxPropertyManager = mailboxPropertyManager;
state = new MailboxPairingState.QrCodeReceived();
}
@Override
public void addObserver(Consumer<MailboxPairingState> o) {
MailboxPairingState state;
synchronized (lock) {
observers.add(o);
state = this.state;
eventExecutor.execute(() -> o.accept(state));
}
}
@Override
public void removeObserver(Consumer<MailboxPairingState> o) {
synchronized (lock) {
observers.remove(o);
}
}
@Override
public void run() {
try {
pairMailbox();
} catch (FormatException e) {
onMailboxError(e, new MailboxPairingState.InvalidQrCode());
} catch (MailboxAlreadyPairedException e) {
onMailboxError(e, new MailboxPairingState.MailboxAlreadyPaired());
} catch (IOException e) {
onMailboxError(e, new MailboxPairingState.ConnectionError());
} catch (ApiException | DbException e) {
onMailboxError(e, new MailboxPairingState.UnexpectedError());
}
}
private void pairMailbox() throws IOException, ApiException, DbException {
MailboxProperties mailboxProperties = decodeQrCodePayload(payload);
setState(new MailboxPairingState.Pairing());
MailboxAuthToken ownerToken = api.setup(mailboxProperties);
MailboxProperties ownerProperties = new MailboxProperties(
mailboxProperties.getBaseUrl(), ownerToken, true);
long time = clock.currentTimeMillis();
db.transaction(false, txn -> {
mailboxSettingsManager
.setOwnMailboxProperties(txn, ownerProperties);
mailboxSettingsManager.recordSuccessfulConnection(txn, time);
// A (possibly new) mailbox is paired. Reset message retransmission
// timers for contacts who doesn't have their own mailbox. This way,
// data stranded on our old mailbox will be re-uploaded to our new.
for (Contact c : db.getContacts(txn)) {
MailboxPropertiesUpdate remoteProps = mailboxPropertyManager
.getRemoteProperties(txn, c.getId());
if (remoteProps == null) {
db.resetUnackedMessagesToSend(txn, c.getId());
}
}
});
setState(new MailboxPairingState.Paired());
}
private void onMailboxError(Exception e, MailboxPairingState state) {
logException(LOG, WARNING, e);
setState(state);
}
private void setState(MailboxPairingState state) {
synchronized (lock) {
this.state = state;
notifyObservers();
}
}
@GuardedBy("lock")
private void notifyObservers() {
List<Consumer<MailboxPairingState>> observers =
new ArrayList<>(this.observers);
MailboxPairingState state = this.state;
eventExecutor.execute(() -> {
for (Consumer<MailboxPairingState> o : observers) o.accept(state);
});
}
private MailboxProperties decodeQrCodePayload(String payload)
throws FormatException {
byte[] bytes = payload.getBytes(ISO_8859_1);
if (bytes.length != 65) {
if (LOG.isLoggable(WARNING)) {
LOG.warning("QR code length is not 65: " + bytes.length);
}
throw new FormatException();
}
int version = bytes[0] & 0xFF;
if (version != VERSION_REQUIRED) {
if (LOG.isLoggable(WARNING)) {
LOG.warning("QR code has not version " + VERSION_REQUIRED +
": " + version);
}
throw new FormatException();
}
LOG.info("QR code is valid");
byte[] onionPubKey = Arrays.copyOfRange(bytes, 1, 33);
String onion = crypto.encodeOnion(onionPubKey);
String baseUrl = "http://" + onion + ".onion";
byte[] tokenBytes = Arrays.copyOfRange(bytes, 33, 65);
MailboxAuthToken setupToken = new MailboxAuthToken(tokenBytes);
return new MailboxProperties(baseUrl, setupToken, true);
}
}

View File

@@ -0,0 +1,303 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.client.ContactGroupFactory;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager.ContactHook;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.data.MetadataParser;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook;
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.mailbox.MailboxPropertiesUpdate;
import org.briarproject.bramble.api.mailbox.MailboxPropertyManager;
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager.MailboxHook;
import org.briarproject.bramble.api.mailbox.RemoteMailboxPropertiesUpdateEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.Group.Visibility;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.InvalidMessageException;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.validation.IncomingMessageHook;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.versioning.ClientVersioningManager;
import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE;
@NotNullByDefault
class MailboxPropertyManagerImpl implements MailboxPropertyManager,
OpenDatabaseHook, ContactHook, ClientVersioningHook,
IncomingMessageHook, MailboxHook {
private final DatabaseComponent db;
private final ClientHelper clientHelper;
private final ClientVersioningManager clientVersioningManager;
private final MetadataParser metadataParser;
private final ContactGroupFactory contactGroupFactory;
private final Clock clock;
private final MailboxSettingsManager mailboxSettingsManager;
private final CryptoComponent crypto;
private final Group localGroup;
@Inject
MailboxPropertyManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
ClientVersioningManager clientVersioningManager,
MetadataParser metadataParser,
ContactGroupFactory contactGroupFactory, Clock clock,
MailboxSettingsManager mailboxSettingsManager,
CryptoComponent crypto) {
this.db = db;
this.clientHelper = clientHelper;
this.clientVersioningManager = clientVersioningManager;
this.metadataParser = metadataParser;
this.contactGroupFactory = contactGroupFactory;
this.clock = clock;
this.mailboxSettingsManager = mailboxSettingsManager;
this.crypto = crypto;
localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID,
MAJOR_VERSION);
}
@Override
public void onDatabaseOpened(Transaction txn) throws DbException {
if (db.containsGroup(txn, localGroup.getId())) {
return;
}
db.addGroup(txn, localGroup);
// Set things up for any pre-existing contacts
for (Contact c : db.getContacts(txn)) {
addingContact(txn, c);
}
}
@Override
public void addingContact(Transaction txn, Contact c) throws DbException {
// Create a group to share with the contact
Group g = getContactGroup(c);
db.addGroup(txn, g);
// Apply the client's visibility to the contact group
Visibility client = clientVersioningManager
.getClientVisibility(txn, c.getId(), CLIENT_ID, MAJOR_VERSION);
db.setGroupVisibility(txn, c.getId(), g.getId(), client);
// Attach the contact ID to the group
clientHelper.setContactId(txn, g.getId(), c.getId());
// If we are paired, create and send props to the newly added contact
MailboxProperties ownProps =
mailboxSettingsManager.getOwnMailboxProperties(txn);
if (ownProps != null) {
createAndSendProperties(txn, c, ownProps.getOnion());
}
}
@Override
public void removingContact(Transaction txn, Contact c) throws DbException {
db.removeGroup(txn, getContactGroup(c));
}
@Override
public void mailboxPaired(Transaction txn, String ownOnion)
throws DbException {
for (Contact c : db.getContacts(txn)) {
createAndSendProperties(txn, c, ownOnion);
}
}
@Override
public void mailboxUnpaired(Transaction txn) throws DbException {
for (Contact c : db.getContacts(txn)) {
sendEmptyProperties(txn, c);
}
}
@Override
public void onClientVisibilityChanging(Transaction txn, Contact c,
Visibility v) throws DbException {
// Apply the client's visibility to the contact group
Group g = getContactGroup(c);
db.setGroupVisibility(txn, c.getId(), g.getId(), v);
}
@Override
public DeliveryAction incomingMessage(Transaction txn, Message m,
Metadata meta) throws DbException, InvalidMessageException {
try {
BdfDictionary d = metadataParser.parse(meta);
// Get latest non-local update in the same group (from same contact)
LatestUpdate latest = findLatest(txn, m.getGroupId(), false);
if (latest != null) {
if (d.getLong(MSG_KEY_VERSION) > latest.version) {
db.deleteMessage(txn, latest.messageId);
db.deleteMessageMetadata(txn, latest.messageId);
} else {
// Delete this update, we already have a newer one
db.deleteMessage(txn, m.getId());
db.deleteMessageMetadata(txn, m.getId());
return ACCEPT_DO_NOT_SHARE;
}
}
ContactId c = clientHelper.getContactId(txn, m.getGroupId());
BdfList body = clientHelper.getMessageAsList(txn, m.getId());
MailboxPropertiesUpdate p = parseProperties(body);
txn.attach(new RemoteMailboxPropertiesUpdateEvent(c, p));
// Reset message retransmission timers for the contact. Avoiding
// messages getting stranded:
// - on our mailbox, if they now have a mailbox but didn't before
// - on the contact's old mailbox, if they removed their mailbox
// - on the contact's old mailbox, if they replaced their mailbox
db.resetUnackedMessagesToSend(txn, c);
} catch (FormatException e) {
throw new InvalidMessageException(e);
}
return ACCEPT_DO_NOT_SHARE;
}
@Override
@Nullable
public MailboxPropertiesUpdate getLocalProperties(Transaction txn,
ContactId c) throws DbException {
return getProperties(txn, db.getContact(txn, c), true);
}
@Override
@Nullable
public MailboxPropertiesUpdate getRemoteProperties(Transaction txn,
ContactId c) throws DbException {
return getProperties(txn, db.getContact(txn, c), false);
}
/**
* Creates and sends an update message to the given contact. The message
* holds our own mailbox's onion, and generated unique properties. All of
* which the contact needs to communicate with our Mailbox.
*/
private void createAndSendProperties(Transaction txn,
Contact c, String ownOnion) throws DbException {
MailboxPropertiesUpdate p = new MailboxPropertiesUpdate(ownOnion,
new MailboxAuthToken(crypto.generateUniqueId().getBytes()),
new MailboxFolderId(crypto.generateUniqueId().getBytes()),
new MailboxFolderId(crypto.generateUniqueId().getBytes()));
Group g = getContactGroup(c);
storeMessageReplaceLatest(txn, g.getId(), p);
}
/**
* Sends an empty update message to the given contact. The empty update
* indicates for the receiving contact that we no longer have a Mailbox that
* they can use.
*/
private void sendEmptyProperties(Transaction txn, Contact c)
throws DbException {
Group g = getContactGroup(c);
storeMessageReplaceLatest(txn, g.getId(), null);
}
@Nullable
private MailboxPropertiesUpdate getProperties(Transaction txn,
Contact c, boolean local) throws DbException {
MailboxPropertiesUpdate p = null;
Group g = getContactGroup(c);
try {
LatestUpdate latest = findLatest(txn, g.getId(), local);
if (latest != null) {
BdfList body =
clientHelper.getMessageAsList(txn, latest.messageId);
p = parseProperties(body);
}
} catch (FormatException e) {
throw new DbException(e);
}
return p;
}
private void storeMessageReplaceLatest(Transaction txn, GroupId g,
@Nullable MailboxPropertiesUpdate p) throws DbException {
try {
LatestUpdate latest = findLatest(txn, g, true);
long version = latest == null ? 1 : latest.version + 1;
Message m = clientHelper.createMessage(g, clock.currentTimeMillis(),
encodeProperties(version, p));
BdfDictionary meta = new BdfDictionary();
meta.put(MSG_KEY_VERSION, version);
meta.put(MSG_KEY_LOCAL, true);
clientHelper.addLocalMessage(txn, m, meta, true, false);
if (latest != null) {
db.removeMessage(txn, latest.messageId);
}
} catch (FormatException e) {
throw new DbException(e);
}
}
@Nullable
private LatestUpdate findLatest(Transaction txn, GroupId g, boolean local)
throws DbException, FormatException {
Map<MessageId, BdfDictionary> metadata =
clientHelper.getMessageMetadataAsDictionary(txn, g);
// We should have at most 1 local and 1 remote
if (metadata.size() > 2) {
throw new IllegalStateException();
}
for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
BdfDictionary meta = e.getValue();
if (meta.getBoolean(MSG_KEY_LOCAL) == local) {
return new LatestUpdate(e.getKey(),
meta.getLong(MSG_KEY_VERSION));
}
}
return null;
}
private BdfList encodeProperties(long version,
@Nullable MailboxPropertiesUpdate p) {
BdfDictionary dict = new BdfDictionary();
if (p != null) {
dict.put(PROP_KEY_ONION, p.getOnion());
dict.put(PROP_KEY_AUTHTOKEN, p.getAuthToken().getBytes());
dict.put(PROP_KEY_INBOXID, p.getInboxId().getBytes());
dict.put(PROP_KEY_OUTBOXID, p.getOutboxId().getBytes());
}
return BdfList.of(version, dict);
}
@Nullable
private MailboxPropertiesUpdate parseProperties(BdfList body)
throws FormatException {
BdfDictionary dict = body.getDictionary(1);
return clientHelper.parseAndValidateMailboxPropertiesUpdate(dict);
}
private Group getContactGroup(Contact c) {
return contactGroupFactory.createContactGroup(CLIENT_ID, MAJOR_VERSION,
c);
}
private static class LatestUpdate {
private final MessageId messageId;
private final long version;
private LatestUpdate(MessageId messageId, long version) {
this.messageId = messageId;
this.version = version;
}
}
}

View File

@@ -0,0 +1,49 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.client.BdfMessageContext;
import org.briarproject.bramble.api.client.BdfMessageValidator;
import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.data.MetadataEncoder;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.InvalidMessageException;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.system.Clock;
import javax.annotation.concurrent.Immutable;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.MSG_KEY_LOCAL;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.MSG_KEY_VERSION;
import static org.briarproject.bramble.util.ValidationUtils.checkSize;
@Immutable
@NotNullByDefault
class MailboxPropertyValidator extends BdfMessageValidator {
MailboxPropertyValidator(ClientHelper clientHelper,
MetadataEncoder metadataEncoder, Clock clock) {
super(clientHelper, metadataEncoder, clock);
}
@Override
protected BdfMessageContext validateMessage(Message m, Group g,
BdfList body) throws InvalidMessageException, FormatException {
// Version, properties
checkSize(body, 2);
// Version
long version = body.getLong(0);
if (version < 0) throw new FormatException();
// Properties
BdfDictionary dictionary = body.getDictionary(1);
clientHelper.parseAndValidateMailboxPropertiesUpdate(dictionary);
// Return the metadata
BdfDictionary meta = new BdfDictionary();
meta.put(MSG_KEY_VERSION, version);
meta.put(MSG_KEY_LOCAL, false);
return new BdfMessageContext(meta);
}
}

View File

@@ -1,14 +1,22 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.mailbox.InvalidMailboxIdException;
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
import org.briarproject.bramble.api.mailbox.MailboxStatus;
import org.briarproject.bramble.api.mailbox.OwnMailboxConnectionStatusEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@@ -25,14 +33,21 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
static final String SETTINGS_KEY_LAST_ATTEMPT = "lastAttempt";
static final String SETTINGS_KEY_LAST_SUCCESS = "lastSuccess";
static final String SETTINGS_KEY_ATTEMPTS = "attempts";
static final String SETTINGS_UPLOADS_NAMESPACE = "mailbox-uploads";
private final SettingsManager settingsManager;
private final List<MailboxHook> hooks = new CopyOnWriteArrayList<>();
@Inject
MailboxSettingsManagerImpl(SettingsManager settingsManager) {
this.settingsManager = settingsManager;
}
@Override
public void registerMailboxHook(MailboxHook hook) {
hooks.add(hook);
}
@Override
public MailboxProperties getOwnMailboxProperties(Transaction txn)
throws DbException {
@@ -40,16 +55,24 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
String onion = s.get(SETTINGS_KEY_ONION);
String token = s.get(SETTINGS_KEY_TOKEN);
if (isNullOrEmpty(onion) || isNullOrEmpty(token)) return null;
return new MailboxProperties(onion, token, true);
try {
MailboxAuthToken tokenId = MailboxAuthToken.fromString(token);
return new MailboxProperties(onion, tokenId, true);
} catch (InvalidMailboxIdException e) {
throw new DbException(e);
}
}
@Override
public void setOwnMailboxProperties(Transaction txn, MailboxProperties p)
throws DbException {
Settings s = new Settings();
s.put(SETTINGS_KEY_ONION, p.getOnionAddress());
s.put(SETTINGS_KEY_TOKEN, p.getAuthToken());
s.put(SETTINGS_KEY_ONION, p.getBaseUrl());
s.put(SETTINGS_KEY_TOKEN, p.getAuthToken().toString());
settingsManager.mergeSettings(txn, s, SETTINGS_NAMESPACE);
for (MailboxHook hook : hooks) {
hook.mailboxPaired(txn, p.getOnion());
}
}
@Override
@@ -70,6 +93,8 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
s.putLong(SETTINGS_KEY_LAST_SUCCESS, now);
s.putInt(SETTINGS_KEY_ATTEMPTS, 0);
settingsManager.mergeSettings(txn, s, SETTINGS_NAMESPACE);
MailboxStatus status = new MailboxStatus(now, now, 0);
txn.attach(new OwnMailboxConnectionStatusEvent(status));
}
@Override
@@ -77,10 +102,33 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager {
throws DbException {
Settings oldSettings =
settingsManager.getSettings(txn, SETTINGS_NAMESPACE);
int attempts = oldSettings.getInt(SETTINGS_KEY_ATTEMPTS, 0);
int newAttempts = 1 + oldSettings.getInt(SETTINGS_KEY_ATTEMPTS, 0);
long lastSuccess = oldSettings.getLong(SETTINGS_KEY_LAST_SUCCESS, 0);
Settings newSettings = new Settings();
newSettings.putLong(SETTINGS_KEY_LAST_ATTEMPT, now);
newSettings.putInt(SETTINGS_KEY_ATTEMPTS, attempts + 1);
newSettings.putInt(SETTINGS_KEY_ATTEMPTS, newAttempts);
settingsManager.mergeSettings(txn, newSettings, SETTINGS_NAMESPACE);
MailboxStatus status = new MailboxStatus(now, lastSuccess, newAttempts);
txn.attach(new OwnMailboxConnectionStatusEvent(status));
}
@Override
public void setPendingUpload(Transaction txn, ContactId id,
@Nullable String filename) throws DbException {
Settings s = new Settings();
String value = filename == null ? "" : filename;
s.put(String.valueOf(id.getInt()), value);
settingsManager.mergeSettings(txn, s, SETTINGS_UPLOADS_NAMESPACE);
}
@Nullable
@Override
public String getPendingUpload(Transaction txn, ContactId id)
throws DbException {
Settings s =
settingsManager.getSettings(txn, SETTINGS_UPLOADS_NAMESPACE);
String filename = s.get(String.valueOf(id.getInt()));
if (isNullOrEmpty(filename)) return null;
return filename;
}
}

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask;
@Deprecated // We can simply remove tasks when they finish
@NotNullByDefault
interface RemovableDriveTaskRegistry {

View File

@@ -1,40 +1,73 @@
package org.briarproject.bramble.plugin.tor;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.List;
// TODO: Create a module for this so it doesn't have to be public
@NotNullByDefault
public interface CircumventionProvider {
enum BridgeType {
DEFAULT_OBFS4,
NON_DEFAULT_OBFS4,
VANILLA,
MEEK
}
/**
* Countries where Tor is blocked, i.e. vanilla Tor connection won't work.
*
* <p>
* See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
* and https://trac.torproject.org/projects/tor/wiki/doc/OONI/censorshipwiki
*/
String[] BLOCKED = {"CN", "IR", "EG", "BY", "TR", "SY", "VE"};
String[] BLOCKED = {"BY", "CN", "EG", "IR", "RU", "TM", "VE"};
/**
* Countries where obfs4 or meek bridge connections are likely to work.
* Should be a subset of {@link #BLOCKED}.
* Countries where bridge connections are likely to work.
* Should be a subset of {@link #BLOCKED} and the union of
* {@link #DEFAULT_BRIDGES}, {@link #NON_DEFAULT_BRIDGES} and
* {@link #MEEK_BRIDGES}.
*/
String[] BRIDGES = { "CN", "IR", "EG", "BY", "TR", "SY", "VE" };
String[] BRIDGES = {"BY", "CN", "EG", "IR", "RU", "TM", "VE"};
/**
* Countries where obfs4 bridges won't work and meek is needed.
* Countries where default obfs4 or vanilla bridges are likely to work.
* Should be a subset of {@link #BRIDGES}.
*/
String[] NEEDS_MEEK = {"CN", "IR"};
String[] DEFAULT_BRIDGES = {"EG", "VE"};
/**
* Countries where non-default obfs4 or vanilla bridges are likely to work.
* Should be a subset of {@link #BRIDGES}.
*/
String[] NON_DEFAULT_BRIDGES = {"BY", "RU", "TM"};
/**
* Countries where obfs4 and vanilla bridges won't work and meek is needed.
* Should be a subset of {@link #BRIDGES}.
*/
String[] MEEK_BRIDGES = {"CN", "IR"};
/**
* Returns true if vanilla Tor connections are blocked in the given country.
*/
boolean isTorProbablyBlocked(String countryCode);
/**
* Returns true if bridge connections of some type work in the given
* country.
*/
boolean doBridgesWork(String countryCode);
boolean needsMeek(String countryCode);
/**
* Returns the types of bridge connection that are suitable for the given
* country, or {@link #DEFAULT_BRIDGES} if no bridge type is known
* to work.
*/
List<BridgeType> getSuitableBridgeTypes(String countryCode);
@IoExecutor
List<String> getBridges(boolean meek);
List<String> getBridges(BridgeType type);
}

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.plugin.tor;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.InputStream;
import java.util.ArrayList;
@@ -9,24 +10,33 @@ import java.util.List;
import java.util.Scanner;
import java.util.Set;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.DEFAULT_OBFS4;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.VANILLA;
@Immutable
@NotNullByDefault
class CircumventionProviderImpl implements CircumventionProvider {
private final static String BRIDGE_FILE_NAME = "bridges";
private static final Set<String> BLOCKED_IN_COUNTRIES =
new HashSet<>(asList(BLOCKED));
private static final Set<String> BRIDGES_WORK_IN_COUNTRIES =
private static final Set<String> BRIDGE_COUNTRIES =
new HashSet<>(asList(BRIDGES));
private static final Set<String> BRIDGES_NEED_MEEK =
new HashSet<>(asList(NEEDS_MEEK));
@Nullable
private volatile List<String> bridges = null;
private static final Set<String> DEFAULT_OBFS4_BRIDGE_COUNTRIES =
new HashSet<>(asList(DEFAULT_BRIDGES));
private static final Set<String> NON_DEFAULT_BRIDGE_COUNTRIES =
new HashSet<>(asList(NON_DEFAULT_BRIDGES));
private static final Set<String> MEEK_COUNTRIES =
new HashSet<>(asList(MEEK_BRIDGES));
@Inject
CircumventionProviderImpl() {
@@ -39,33 +49,40 @@ class CircumventionProviderImpl implements CircumventionProvider {
@Override
public boolean doBridgesWork(String countryCode) {
return BRIDGES_WORK_IN_COUNTRIES.contains(countryCode);
return BRIDGE_COUNTRIES.contains(countryCode);
}
@Override
public boolean needsMeek(String countryCode) {
return BRIDGES_NEED_MEEK.contains(countryCode);
public List<BridgeType> getSuitableBridgeTypes(String countryCode) {
if (DEFAULT_OBFS4_BRIDGE_COUNTRIES.contains(countryCode)) {
return asList(DEFAULT_OBFS4, VANILLA);
} else if (NON_DEFAULT_BRIDGE_COUNTRIES.contains(countryCode)) {
return asList(NON_DEFAULT_OBFS4, VANILLA);
} else if (MEEK_COUNTRIES.contains(countryCode)) {
return singletonList(MEEK);
} else {
return asList(DEFAULT_OBFS4, VANILLA);
}
}
@Override
@IoExecutor
public List<String> getBridges(boolean useMeek) {
List<String> bridges = this.bridges;
if (bridges != null) return new ArrayList<>(bridges);
InputStream is = getClass().getClassLoader()
.getResourceAsStream(BRIDGE_FILE_NAME);
public List<String> getBridges(BridgeType type) {
InputStream is = requireNonNull(getClass().getClassLoader()
.getResourceAsStream(BRIDGE_FILE_NAME));
Scanner scanner = new Scanner(is);
bridges = new ArrayList<>();
List<String> bridges = new ArrayList<>();
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
boolean isMeekBridge = line.startsWith("Bridge meek");
if (useMeek && !isMeekBridge || !useMeek && isMeekBridge) continue;
if (!line.startsWith("#")) bridges.add(line);
if ((type == DEFAULT_OBFS4 && line.startsWith("d ")) ||
(type == NON_DEFAULT_OBFS4 && line.startsWith("n ")) ||
(type == VANILLA && line.startsWith("v ")) ||
(type == MEEK && line.startsWith("m "))) {
bridges.add(line.substring(2));
}
}
scanner.close();
this.bridges = bridges;
return bridges;
}

View File

@@ -33,6 +33,7 @@ import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.ResourceProvider;
import org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
@@ -84,7 +85,6 @@ import static org.briarproject.bramble.api.plugin.TorConstants.ID;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_AUTOMATIC;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_NEVER;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_WITH_BRIDGES;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_PORT;
@@ -92,6 +92,9 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V3;
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_BATTERY;
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_COUNTRY_BLOCKED;
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_MOBILE_DATA;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.DEFAULT_OBFS4;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4;
import static org.briarproject.bramble.plugin.tor.TorRendezvousCrypto.SEED_BYTES;
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
@@ -106,7 +109,14 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private static final Logger LOG = getLogger(TorPlugin.class.getName());
private static final String[] EVENTS = {
"CIRC", "ORCONN", "HS_DESC", "NOTICE", "WARN", "ERR"
"CIRC",
"ORCONN",
"STATUS_GENERAL",
"STATUS_CLIENT",
"HS_DESC",
"NOTICE",
"WARN",
"ERR"
};
private static final String OWNER = "__OwningControllerProcess";
private static final int COOKIE_TIMEOUT_MS = 3000;
@@ -226,7 +236,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
}
// Load the settings
settings = migrateSettings(callback.getSettings());
settings = callback.getSettings();
// Install or update the assets if necessary
if (!assetsAreUpToDate()) installAssets();
if (cookieFile.exists() && !cookieFile.delete())
@@ -300,11 +310,17 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
controlConnection.setEventHandler(this);
controlConnection.setEvents(asList(EVENTS));
// Check whether Tor has already bootstrapped
String phase = controlConnection.getInfo("status/bootstrap-phase");
if (phase != null && phase.contains("PROGRESS=100")) {
String info = controlConnection.getInfo("status/bootstrap-phase");
if (info != null && info.contains("PROGRESS=100")) {
LOG.info("Tor has already bootstrapped");
state.setBootstrapped();
}
// Check whether Tor has already built a circuit
info = controlConnection.getInfo("status/circuit-established");
if ("1".equals(info)) {
LOG.info("Tor has already built a circuit");
state.getAndSetCircuitBuilt(true);
}
} catch (IOException e) {
throw new PluginException(e);
}
@@ -316,18 +332,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
bind();
}
// TODO: Remove after a reasonable migration period (added 2020-06-25)
private Settings migrateSettings(Settings settings) {
int network = settings.getInt(PREF_TOR_NETWORK,
DEFAULT_PREF_TOR_NETWORK);
if (network == PREF_TOR_NETWORK_NEVER) {
settings.putInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK);
settings.putBoolean(PREF_PLUGIN_ENABLE, false);
callback.mergeSettings(settings);
}
return settings;
}
private boolean assetsAreUpToDate() {
return doneFile.lastModified() > getLastUpdateTime();
}
@@ -337,9 +341,14 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
// The done file may already exist from a previous installation
//noinspection ResultOfMethodCallIgnored
doneFile.delete();
// The GeoIP file may exist from a previous installation - we can
// save some space by deleting it.
// TODO: Remove after a reasonable migration period
// (added 2022-03-29)
//noinspection ResultOfMethodCallIgnored
geoIpFile.delete();
installTorExecutable();
installObfs4Executable();
extract(getGeoIpInputStream(), geoIpFile);
extract(getConfigInputStream(), configFile);
if (!doneFile.createNewFile())
LOG.warning("Failed to create done file");
@@ -377,14 +386,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
return zin;
}
private InputStream getGeoIpInputStream() throws IOException {
InputStream in = resourceProvider.getResourceInputStream("geoip",
".zip");
ZipInputStream zin = new ZipInputStream(in);
if (zin.getNextEntry() == null) throw new IOException();
return zin;
}
private InputStream getObfs4InputStream() throws IOException {
InputStream in = resourceProvider
.getResourceInputStream("obfs4proxy_" + architecture, ".zip");
@@ -542,20 +543,24 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
controlConnection.setConf("DisableNetwork", enable ? "0" : "1");
}
private void enableBridges(boolean enable, boolean needsMeek)
private void enableBridges(boolean enable, List<BridgeType> bridgeTypes)
throws IOException {
if (enable) {
Collection<String> conf = new ArrayList<>();
conf.add("UseBridges 1");
File obfs4File = getObfs4ExecutableFile();
if (needsMeek) {
if (bridgeTypes.contains(MEEK)) {
conf.add("ClientTransportPlugin meek_lite exec " +
obfs4File.getAbsolutePath());
} else {
}
if (bridgeTypes.contains(DEFAULT_OBFS4) ||
bridgeTypes.contains(NON_DEFAULT_OBFS4)) {
conf.add("ClientTransportPlugin obfs4 exec " +
obfs4File.getAbsolutePath());
}
conf.addAll(circumventionProvider.getBridges(needsMeek));
for (BridgeType bridgeType : bridgeTypes) {
conf.addAll(circumventionProvider.getBridges(bridgeType));
}
controlConnection.setConf(conf);
} else {
controlConnection.setConf("UseBridges", "0");
@@ -644,7 +649,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
} catch (IOException e) {
if (LOG.isLoggable(INFO)) {
LOG.info("Could not connect to v3 "
+ scrubOnion(onion3) + ": " + e.toString());
+ scrubOnion(onion3) + ": " + e);
}
tryToClose(s, LOG, WARNING);
return null;
@@ -680,8 +685,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
byte[] localSeed = alice ? aliceSeed : bobSeed;
byte[] remoteSeed = alice ? bobSeed : aliceSeed;
String blob = torRendezvousCrypto.getPrivateKeyBlob(localSeed);
String localOnion = torRendezvousCrypto.getOnionAddress(localSeed);
String remoteOnion = torRendezvousCrypto.getOnionAddress(remoteSeed);
String localOnion = torRendezvousCrypto.getOnion(localSeed);
String remoteOnion = torRendezvousCrypto.getOnion(remoteSeed);
TransportProperties remoteProperties = new TransportProperties();
remoteProperties.put(PROP_ONION_V3, remoteOnion);
try {
@@ -725,9 +730,10 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override
public void circuitStatus(String status, String id, String path) {
if (status.equals("BUILT") &&
state.getAndSetCircuitBuilt()) {
LOG.info("First circuit built");
// In case of races between receiving CIRCUIT_ESTABLISHED and setting
// DisableNetwork, set our circuitBuilt flag if not already set
if (status.equals("BUILT") && !state.getAndSetCircuitBuilt(true)) {
LOG.info("Circuit built");
backoff.reset();
}
}
@@ -738,9 +744,16 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override
public void orConnStatus(String status, String orName) {
if (LOG.isLoggable(INFO))
LOG.info("OR connection " + status + " " + orName);
if (status.equals("CLOSED") || status.equals("FAILED")) {
if (LOG.isLoggable(INFO)) LOG.info("OR connection " + status);
//noinspection IfCanBeSwitch
if (status.equals("LAUNCHED")) state.onOrConnectionLaunched();
else if (status.equals("FAILED")) state.onOrConnectionFailed();
else if (status.equals("CONNECTED")) state.onOrConnectionConnected();
else if (status.equals("CLOSED")) state.onOrConnectionClosed();
if ((status.equals("FAILED") || status.equals("CLOSED")) &&
state.getNumOrConnections() == 0) {
// Check whether we've lost connectivity
updateConnectionStatus(networkManager.getNetworkStatus(),
batteryManager.isCharging());
@@ -758,24 +771,81 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override
public void message(String severity, String msg) {
if (LOG.isLoggable(INFO)) LOG.info(severity + " " + msg);
if (severity.equals("NOTICE") && msg.startsWith("Bootstrapped 100%")) {
state.setBootstrapped();
backoff.reset();
if (msg.startsWith("Switching to guard context")) {
state.onSwitchingGuardContext();
}
}
@Override
public void unrecognized(String type, String msg) {
if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) {
if (LOG.isLoggable(INFO)) {
String[] words = msg.split(" ");
if (words.length > 1 && ONION_V3.matcher(words[1]).matches()) {
LOG.info("V3 descriptor uploaded");
} else {
LOG.info("V2 descriptor uploaded");
if (type.equals("STATUS_CLIENT")) {
handleClientStatus(removeSeverity(msg));
} else if (type.equals("STATUS_GENERAL")) {
handleGeneralStatus(removeSeverity(msg));
} else if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) {
String[] parts = msg.split(" ");
if (parts.length < 2) {
LOG.warning("Failed to parse HS_DESC UPLOADED event");
} else if (LOG.isLoggable(INFO)) {
LOG.info("V3 descriptor uploaded for " + scrubOnion(parts[1]));
}
}
}
private String removeSeverity(String msg) {
return msg.replaceFirst("[^ ]+ ", "");
}
private void handleClientStatus(String msg) {
if (msg.startsWith("BOOTSTRAP PROGRESS=100")) {
LOG.info("Bootstrapped");
state.setBootstrapped();
backoff.reset();
} else if (msg.startsWith("CIRCUIT_ESTABLISHED")) {
if (!state.getAndSetCircuitBuilt(true)) {
LOG.info("Circuit built");
backoff.reset();
}
} else if (msg.startsWith("CIRCUIT_NOT_ESTABLISHED")) {
if (state.getAndSetCircuitBuilt(false)) {
LOG.info("Circuit not built");
// TODO: Disable and re-enable network to prompt Tor to rebuild
// its guard/bridge connections? This will also close any
// established circuits, which might still be functioning
}
}
}
private void handleGeneralStatus(String msg) {
if (msg.startsWith("CLOCK_JUMPED")) {
Long time = parseLongArgument(msg, "TIME");
if (time != null && LOG.isLoggable(WARNING)) {
LOG.warning("Clock jumped " + time + " seconds");
}
} else if (msg.startsWith("CLOCK_SKEW")) {
Long skew = parseLongArgument(msg, "SKEW");
if (skew != null && LOG.isLoggable(WARNING)) {
LOG.warning("Clock is skewed by " + skew + " seconds");
}
}
}
@Nullable
private Long parseLongArgument(String msg, String argName) {
String[] args = msg.split(" ");
for (String arg : args) {
if (arg.startsWith(argName + "=")) {
try {
return Long.parseLong(arg.substring(argName.length() + 1));
} catch (NumberFormatException e) {
break;
}
}
}
if (LOG.isLoggable(WARNING)) {
LOG.warning("Failed to parse " + argName + " from '" + msg + "'");
}
return null;
}
@Override
@@ -844,7 +914,9 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
int reasonsDisabled = 0;
boolean enableNetwork = false, enableBridges = false;
boolean useMeek = false, enableConnectionPadding = false;
boolean enableConnectionPadding = false;
List<BridgeType> bridgeTypes =
circumventionProvider.getSuitableBridgeTypes(country);
if (!online) {
LOG.info("Disabling network, device is offline");
@@ -873,14 +945,10 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
enableNetwork = true;
if (network == PREF_TOR_NETWORK_WITH_BRIDGES ||
(automatic && bridgesWork)) {
if (ipv6Only ||
circumventionProvider.needsMeek(country)) {
LOG.info("Using meek bridges");
enableBridges = true;
useMeek = true;
} else {
LOG.info("Using obfs4 bridges");
enableBridges = true;
if (ipv6Only) bridgeTypes = singletonList(MEEK);
enableBridges = true;
if (LOG.isLoggable(INFO)) {
LOG.info("Using bridge types " + bridgeTypes);
}
} else {
LOG.info("Not using bridges");
@@ -898,7 +966,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
try {
if (enableNetwork) {
enableBridges(enableBridges, useMeek);
enableBridges(enableBridges, bridgeTypes);
enableConnectionPadding(enableConnectionPadding);
useIpv6(ipv6Only);
}
@@ -938,6 +1006,9 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Nullable
private ServerSocket serverSocket = null;
@GuardedBy("this")
private int orConnectionsPending = 0, orConnectionsConnected = 0;
private synchronized void setStarted() {
started = true;
callback.pluginStateChanged(getState());
@@ -961,11 +1032,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
callback.pluginStateChanged(getState());
}
private synchronized boolean getAndSetCircuitBuilt() {
boolean firstCircuit = !circuitBuilt;
circuitBuilt = true;
callback.pluginStateChanged(getState());
return firstCircuit;
private synchronized boolean getAndSetCircuitBuilt(boolean built) {
boolean old = circuitBuilt;
circuitBuilt = built;
if (built != old) callback.pluginStateChanged(getState());
return old;
}
private synchronized void enableNetwork(boolean enable) {
@@ -1006,5 +1077,58 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private synchronized int getReasonsDisabled() {
return getState() == DISABLED ? reasonsDisabled : 0;
}
private synchronized void onOrConnectionLaunched() {
orConnectionsPending++;
logOrConnections();
}
private synchronized void onOrConnectionFailed() {
orConnectionsPending--;
if (orConnectionsPending < 0) {
LOG.warning("Count was zero before connection failed");
orConnectionsPending = 0;
}
logOrConnections();
}
private synchronized void onOrConnectionConnected() {
orConnectionsPending--;
if (orConnectionsPending < 0) {
LOG.warning("Count was zero before connection connected");
orConnectionsPending = 0;
}
orConnectionsConnected++;
logOrConnections();
}
private synchronized void onOrConnectionClosed() {
orConnectionsConnected--;
if (orConnectionsConnected < 0) {
LOG.warning("Count was zero before connection closed");
orConnectionsConnected = 0;
}
logOrConnections();
}
private synchronized void onSwitchingGuardContext() {
// Tor doesn't seem to report events for connections belonging to
// the old guard context, so we have to reset the counters
orConnectionsPending = 0;
orConnectionsConnected = 0;
logOrConnections();
}
private synchronized int getNumOrConnections() {
return orConnectionsPending + orConnectionsConnected;
}
@GuardedBy("this")
private void logOrConnections() {
if (LOG.isLoggable(INFO)) {
LOG.info("OR connections: " + orConnectionsPending
+ " pending, " + orConnectionsConnected + " connected");
}
}
}
}

View File

@@ -4,7 +4,7 @@ interface TorRendezvousCrypto {
static final int SEED_BYTES = 32;
String getOnionAddress(byte[] seed);
String getOnion(byte[] seed);
String getPrivateKeyBlob(byte[] seed);
}

View File

@@ -21,9 +21,9 @@ class TorRendezvousCryptoImpl implements TorRendezvousCrypto {
}
@Override
public String getOnionAddress(byte[] seed) {
public String getOnion(byte[] seed) {
EdDSAPrivateKeySpec spec = new EdDSAPrivateKeySpec(seed, CURVE_SPEC);
return crypto.encodeOnionAddress(spec.getA().toByteArray());
return crypto.encodeOnion(spec.getA().toByteArray());
}
@Override

View File

@@ -10,6 +10,7 @@ import dagger.Module;
import dagger.Provides;
import static org.briarproject.bramble.api.plugin.TorConstants.CONNECT_TO_PROXY_TIMEOUT;
import static org.briarproject.bramble.api.plugin.TorConstants.EXTRA_CONNECT_TIMEOUT;
import static org.briarproject.bramble.api.plugin.TorConstants.EXTRA_SOCKET_TIMEOUT;
@Module
@@ -20,6 +21,6 @@ public class SocksModule {
InetSocketAddress proxy = new InetSocketAddress("127.0.0.1",
torSocksPort);
return new SocksSocketFactory(proxy, CONNECT_TO_PROXY_TIMEOUT,
EXTRA_SOCKET_TIMEOUT);
EXTRA_CONNECT_TIMEOUT, EXTRA_SOCKET_TIMEOUT);
}
}

View File

@@ -26,15 +26,18 @@ class SocksSocket extends Socket {
"Address type not supported"
};
@SuppressWarnings("MismatchedReadAndWriteOfArray")
private static final byte[] UNSPECIFIED_ADDRESS = new byte[4];
private final SocketAddress proxy;
private final int connectToProxyTimeout, extraSocketTimeout;
private final int connectToProxyTimeout;
private final int extraConnectTimeout, extraSocketTimeout;
SocksSocket(SocketAddress proxy, int connectToProxyTimeout,
int extraSocketTimeout) {
int extraConnectTimeout, int extraSocketTimeout) {
this.proxy = proxy;
this.connectToProxyTimeout = connectToProxyTimeout;
this.extraConnectTimeout = extraConnectTimeout;
this.extraSocketTimeout = extraSocketTimeout;
}
@@ -66,7 +69,7 @@ class SocksSocket extends Socket {
// Use the supplied timeout temporarily, plus any configured extra
int oldTimeout = getSoTimeout();
setSoTimeout(timeout + extraSocketTimeout);
setSoTimeout(timeout + extraConnectTimeout);
// Connect to the endpoint via the proxy
sendConnectRequest(out, host, port);

View File

@@ -11,18 +11,21 @@ import javax.net.SocketFactory;
class SocksSocketFactory extends SocketFactory {
private final SocketAddress proxy;
private final int connectToProxyTimeout, extraSocketTimeout;
private final int connectToProxyTimeout;
private final int extraConnectTimeout, extraSocketTimeout;
SocksSocketFactory(SocketAddress proxy, int connectToProxyTimeout,
int extraSocketTimeout) {
int extraConnectTimeout, int extraSocketTimeout) {
this.proxy = proxy;
this.connectToProxyTimeout = connectToProxyTimeout;
this.extraConnectTimeout = extraConnectTimeout;
this.extraSocketTimeout = extraSocketTimeout;
}
@Override
public Socket createSocket() {
return new SocksSocket(proxy, connectToProxyTimeout, extraSocketTimeout);
return new SocksSocket(proxy, connectToProxyTimeout,
extraConnectTimeout, extraSocketTimeout);
}
@Override

View File

@@ -7,14 +7,13 @@ import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.Map.Entry;
import java.util.Properties;
import javax.annotation.concurrent.Immutable;
import static java.net.NetworkInterface.getNetworkInterfaces;
import static java.util.Collections.list;
import static org.briarproject.bramble.util.NetworkUtils.getNetworkInterfaces;
@Immutable
@NotNullByDefault
@@ -25,14 +24,11 @@ abstract class AbstractSecureRandomProvider implements SecureRandomProvider {
out.writeLong(System.currentTimeMillis());
out.writeLong(System.nanoTime());
out.writeLong(Runtime.getRuntime().freeMemory());
Enumeration<NetworkInterface> ifaces = getNetworkInterfaces();
if (ifaces != null) {
for (NetworkInterface i : list(ifaces)) {
for (InetAddress a : list(i.getInetAddresses()))
out.write(a.getAddress());
byte[] hardware = i.getHardwareAddress();
if (hardware != null) out.write(hardware);
}
for (NetworkInterface i : getNetworkInterfaces()) {
for (InetAddress a : list(i.getInetAddresses()))
out.write(a.getAddress());
byte[] hardware = i.getHardwareAddress();
if (hardware != null) out.write(hardware);
}
for (Entry<String, String> e : System.getenv().entrySet()) {
out.writeUTF(e.getKey());

View File

@@ -215,6 +215,23 @@ class KeyManagerImpl implements KeyManager, Service, EventListener {
m.getStreamContext(txn, tag)));
}
@Override
public StreamContext getStreamContextOnly(TransportId t, byte[] tag)
throws DbException {
return withManager(t, m ->
db.transactionWithNullableResult(false, txn ->
m.getStreamContextOnly(txn, tag)));
}
@Override
public void markTagAsRecognised(TransportId t, byte[] tag)
throws DbException {
withManager(t, m -> {
db.transaction(false, txn -> m.markTagAsRecognised(txn, tag));
return null;
});
}
@Override
public void eventOccurred(Event e) {
if (e instanceof ContactRemovedEvent) {

View File

@@ -48,4 +48,9 @@ interface TransportKeyManager {
StreamContext getStreamContext(Transaction txn, byte[] tag)
throws DbException;
@Nullable
StreamContext getStreamContextOnly(Transaction txn, byte[] tag);
void markTagAsRecognised(Transaction txn, byte[] tag) throws DbException;
}

View File

@@ -393,56 +393,82 @@ class TransportKeyManagerImpl implements TransportKeyManager {
throws DbException {
lock.lock();
try {
// Look up the incoming keys for the tag
TagContext tagCtx = inContexts.remove(new Bytes(tag));
if (tagCtx == null) return null;
MutableIncomingKeys inKeys = tagCtx.inKeys;
// Create a stream context
StreamContext ctx = new StreamContext(tagCtx.contactId,
tagCtx.pendingContactId, transportId,
inKeys.getTagKey(), inKeys.getHeaderKey(),
tagCtx.streamNumber, tagCtx.handshakeMode);
// Update the reordering window
ReorderingWindow window = inKeys.getWindow();
Change change = window.setSeen(tagCtx.streamNumber);
// Add tags for any stream numbers added to the window
for (long streamNumber : change.getAdded()) {
byte[] addTag = new byte[TAG_LENGTH];
transportCrypto.encodeTag(addTag, inKeys.getTagKey(),
PROTOCOL_VERSION, streamNumber);
TagContext tagCtx1 = new TagContext(tagCtx.keySetId,
tagCtx.contactId, tagCtx.pendingContactId, inKeys,
streamNumber, tagCtx.handshakeMode);
inContexts.put(new Bytes(addTag), tagCtx1);
}
// Remove tags for any stream numbers removed from the window
for (long streamNumber : change.getRemoved()) {
if (streamNumber == tagCtx.streamNumber) continue;
byte[] removeTag = new byte[TAG_LENGTH];
transportCrypto.encodeTag(removeTag, inKeys.getTagKey(),
PROTOCOL_VERSION, streamNumber);
inContexts.remove(new Bytes(removeTag));
}
// Write the window back to the DB
db.setReorderingWindow(txn, tagCtx.keySetId, transportId,
inKeys.getTimePeriod(), window.getBase(),
window.getBitmap());
// If the outgoing keys are inactive, activate them
MutableTransportKeySet ks = keys.get(tagCtx.keySetId);
MutableOutgoingKeys outKeys =
ks.getKeys().getCurrentOutgoingKeys();
if (!outKeys.isActive()) {
LOG.info("Activating outgoing keys");
outKeys.activate();
considerReplacingOutgoingKeys(ks);
db.setTransportKeysActive(txn, transportId, tagCtx.keySetId);
}
StreamContext ctx = streamContextFromTag(tag);
if (ctx == null) return null;
markTagAsRecognised(txn, tag);
return ctx;
} finally {
lock.unlock();
}
}
@Override
public StreamContext getStreamContextOnly(Transaction txn, byte[] tag) {
lock.lock();
try {
return streamContextFromTag(tag);
} finally {
lock.unlock();
}
}
@GuardedBy("lock")
@Nullable
private StreamContext streamContextFromTag(byte[] tag) {
// Look up the incoming keys for the tag
TagContext tagCtx = inContexts.get(new Bytes(tag));
if (tagCtx == null) return null;
MutableIncomingKeys inKeys = tagCtx.inKeys;
// Create a stream context
return new StreamContext(tagCtx.contactId,
tagCtx.pendingContactId, transportId,
inKeys.getTagKey(), inKeys.getHeaderKey(),
tagCtx.streamNumber, tagCtx.handshakeMode);
}
@Override
public void markTagAsRecognised(Transaction txn, byte[] tag)
throws DbException {
TagContext tagCtx = inContexts.remove(new Bytes(tag));
if (tagCtx == null) return;
MutableIncomingKeys inKeys = tagCtx.inKeys;
// Update the reordering window
ReorderingWindow window = inKeys.getWindow();
Change change = window.setSeen(tagCtx.streamNumber);
// Add tags for any stream numbers added to the window
for (long streamNumber : change.getAdded()) {
byte[] addTag = new byte[TAG_LENGTH];
transportCrypto.encodeTag(addTag, inKeys.getTagKey(),
PROTOCOL_VERSION, streamNumber);
TagContext tagCtx1 = new TagContext(tagCtx.keySetId,
tagCtx.contactId, tagCtx.pendingContactId, inKeys,
streamNumber, tagCtx.handshakeMode);
inContexts.put(new Bytes(addTag), tagCtx1);
}
// Remove tags for any stream numbers removed from the window
for (long streamNumber : change.getRemoved()) {
if (streamNumber == tagCtx.streamNumber) continue;
byte[] removeTag = new byte[TAG_LENGTH];
transportCrypto.encodeTag(removeTag, inKeys.getTagKey(),
PROTOCOL_VERSION, streamNumber);
inContexts.remove(new Bytes(removeTag));
}
// Write the window back to the DB
db.setReorderingWindow(txn, tagCtx.keySetId, transportId,
inKeys.getTimePeriod(), window.getBase(),
window.getBitmap());
// If the outgoing keys are inactive, activate them
MutableTransportKeySet ks = keys.get(tagCtx.keySetId);
MutableOutgoingKeys outKeys =
ks.getKeys().getCurrentOutgoingKeys();
if (!outKeys.isActive()) {
LOG.info("Activating outgoing keys");
outKeys.activate();
considerReplacingOutgoingKeys(ks);
db.setTransportKeysActive(txn, transportId, tagCtx.keySetId);
}
}
@DatabaseExecutor
@Wakeful
private void updateKeys(Transaction txn) throws DbException {

View File

@@ -1,10 +1,26 @@
Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0
Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0
Bridge obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0
Bridge obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0
Bridge obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0
Bridge obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0
Bridge obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0
Bridge obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0
Bridge obfs4 78.46.188.239:37356 5A2D2F4158D0453E00C7C176978D3F41D69C45DB cert=3c0SwxpOisbohNxEc4tb875RVW8eOu1opRTVXJhafaKA/PNNtI7ElQIVOVZg1AdL5bxGCw iat-mode=0
Bridge meek_lite 192.0.2.2:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com
d Bridge obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1
d Bridge obfs4 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1
d Bridge obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1OpbAH0wNqOT6H6BmRQ iat-mode=1
d Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0
d Bridge obfs4 85.31.186.98:443 011F2599C0E9B27EE74B353155E244813763C3E5 cert=ayq0XzCwhpdysn5o0EyDUbmSOx3X/oTEbzDMvczHOdBJKlvIdHHLJGkZARtT4dcBFArPPg iat-mode=0
d Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0
d Bridge obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0
d Bridge obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0
d Bridge obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0
d Bridge obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0
d Bridge obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0
n Bridge obfs4 46.226.107.197:10300 A38FD6BDFD902882F5F5B9B7CCC95602A20B0BC4 cert=t8tA9q2AeGlmp/dO6oW9bkY5RqqmvqjArCEM9wjJoDnk6XtnaejkF0JTA7VamdyOzcvuBg iat-mode=0
n Bridge obfs4 185.181.11.86:443 A961609729E7FDF520B4E81F1F1B8FA1045285C3 cert=e5faG9Zk4Ni+e7z2YgGfevyKPQlMvkVGi4ublSsHYjaBovKeNXpOhbeFxzbZZoAzxAoGUQ iat-mode=0
n Bridge obfs4 85.242.211.221:8042 A36A938DD7FDB8BACC846BA326EE0BA0D89A9252 cert=1AN6Pt1eFca3Y/WYD2TGAU3Al9cO4eouXE9SX63s66Z/ks3tVmgQ5GeXi1B5DOvx6Il7Zw iat-mode=0
n Bridge obfs4 172.105.22.69:80 CBD17B33192A879433AB37C9E142541BD3459ABD cert=rk5YmpKypLsjlS4tjkYaZNBweYMa5tWQRhZ8Q2WRleNOgrhSceKo59BA8kp6kVfaMPXnSw iat-mode=0
n Bridge obfs4 46.128.93.192:7346 5D28B8E1D117B8720D56A8513CF32509DCA1D84F cert=ED6tZP50eF0vno09F5gFvoWTMdcWFEX2FtwXOUYRevjzKg30/y701f61Vycnh6HO9gkaMw iat-mode=0
n Bridge obfs4 74.104.165.202:9002 EF432018A6AA5D970B2F84E39CD30A147030141C cert=PhppfUusY85dHGvWtGTybZ1fED4DtbHmALkNMIOIYrAz1B4xN7/2a5gyiZe1epju1BOHVg iat-mode=0
n Bridge obfs4 70.34.242.31:443 7F026956402CDFF4BCBA8E11EE9C50E3FE0A2B72 cert=hP/KU7JATSfWH3HwS5Er/YLT0J+bRO3+s2fWx2yirrgf37EyrWvm/BQshoNje8WfUm6CBw iat-mode=0
n Bridge obfs4 192.3.163.88:57145 DEB62DE9643E5956CA4FA78035B48C9BBABE7F29 cert=RMz2z9uVVrioUhx0A8xUmiftRe2RpcXiqIuDfisdIomcHDf82nzfn83X/ixGUiA4rLCAdw iat-mode=0
n Bridge obfs4 93.95.226.151:41185 460B0CFFC0CF1D965F3DE064E08BA1915E7C916A cert=inluPzp5Jp5OzZar1eQb4dcQ/YlAj/v0kHAUCoCr3rmLt03+pVuVTjoH4mRy4+acXpn+Gw iat-mode=0
n Bridge obfs4 120.29.217.52:5223 40FE3DB9800272F9CDC76422F8ED7883280EE96D cert=/71PS4l8c/XJ4DIItlH9xMqNvPFg2RUTrHvPlQWh48u5et8h/yyyjCcYphUadDsfBWpaGQ iat-mode=0
v Bridge 5.45.96.40:9001 8723B591712AAA03FB92000370BD356AB4997FA7
v Bridge 135.181.113.164:54444 74AF4CCA614C454B7D3E81FF8BACD78CEBC7D7DE
v Bridge 152.44.197.85:10507 FF07DF6B4720DA4C50F1A025662D50916D6223F6
v Bridge 209.216.78.21:443 C870D381E7264CDB83BAEEBF074804808CCCDB8D
m Bridge meek_lite 192.0.2.2:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com

View File

@@ -0,0 +1,21 @@
#!/bin/bash
set -e
URL="http://127.0.0.1:8000/status"
attempt_counter=0
max_attempts=200 # 10min - CI for mailbox currently takes ~5min
echo "Waiting for mailbox to come online at $URL"
until [[ "$(curl -s -o /dev/null -w '%{http_code}' $URL)" == "401" ]]; do
if [ ${attempt_counter} -eq ${max_attempts} ]; then
echo "Timed out waiting for mailbox"
exit 1
fi
printf '.'
attempt_counter=$((attempt_counter + 1))
sleep 3
done
echo "Mailbox started"

View File

@@ -13,7 +13,7 @@ import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageContext;
import org.briarproject.bramble.test.ValidatorTestCase;
import org.jmock.Expectations;
import org.jmock.lib.legacy.ClassImposteriser;
import org.jmock.imposters.ByteBuddyClassImposteriser;
import org.junit.Test;
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
@@ -38,7 +38,7 @@ public class BdfMessageValidatorTest extends ValidatorTestCase {
private final Metadata meta = new Metadata();
public BdfMessageValidatorTest() {
context.setImposteriser(ClassImposteriser.INSTANCE);
context.setImposteriser(ByteBuddyClassImposteriser.INSTANCE);
}
@Test(expected = InvalidMessageException.class)

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.client;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.KeyParser;
@@ -20,13 +21,15 @@ import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorFactory;
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxPropertiesUpdate;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.DbExpectations;
import org.briarproject.bramble.util.StringUtils;
import org.jmock.Expectations;
import org.junit.Test;
@@ -41,15 +44,22 @@ import java.util.Random;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_KEY_AUTHTOKEN;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_KEY_INBOXID;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_KEY_ONION;
import static org.briarproject.bramble.api.mailbox.MailboxPropertyManager.PROP_KEY_OUTBOXID;
import static org.briarproject.bramble.test.TestUtils.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getMessage;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.getSignaturePrivateKey;
import static org.briarproject.bramble.test.TestUtils.getSignaturePublicKey;
import static org.briarproject.bramble.test.TestUtils.mailboxPropertiesUpdateEqual;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class ClientHelperImplTest extends BrambleMockTestCase {
@@ -78,13 +88,35 @@ public class ClientHelperImplTest extends BrambleMockTestCase {
private final long timestamp = message.getTimestamp();
private final Metadata metadata = new Metadata();
private final BdfList list = BdfList.of("Sign this!", getRandomBytes(42));
private final String label = StringUtils.getRandomString(5);
private final String label = getRandomString(5);
private final Author author = getAuthor();
private final ClientHelper clientHelper = new ClientHelperImpl(db,
messageFactory, bdfReaderFactory, bdfWriterFactory, metadataParser,
metadataEncoder, cryptoComponent, authorFactory);
private final MailboxPropertiesUpdate validMailboxPropsUpdate;
public ClientHelperImplTest() {
validMailboxPropsUpdate = new MailboxPropertiesUpdate(
"pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd",
new MailboxAuthToken(getRandomId()),
new MailboxFolderId(getRandomId()),
new MailboxFolderId(getRandomId()));
}
private BdfDictionary getValidMailboxPropsUpdateDict() {
BdfDictionary dict = new BdfDictionary();
dict.put(PROP_KEY_ONION, validMailboxPropsUpdate.getOnion());
dict.put(PROP_KEY_AUTHTOKEN, validMailboxPropsUpdate.getAuthToken()
.getBytes());
dict.put(PROP_KEY_INBOXID, validMailboxPropsUpdate.getInboxId()
.getBytes());
dict.put(PROP_KEY_OUTBOXID, validMailboxPropsUpdate.getOutboxId()
.getBytes());
return dict;
}
@Test
public void testAddLocalMessage() throws Exception {
boolean shared = new Random().nextBoolean();
@@ -513,4 +545,95 @@ public class ClientHelperImplTest extends BrambleMockTestCase {
will(returnValue(eof));
}});
}
@Test
public void testParseEmptyMailboxPropsUpdate() throws Exception {
BdfDictionary emptyPropsDict = new BdfDictionary();
MailboxPropertiesUpdate parsedProps = clientHelper
.parseAndValidateMailboxPropertiesUpdate(emptyPropsDict);
assertNull(parsedProps);
}
@Test
public void testParseValidMailboxPropsUpdate() throws Exception {
MailboxPropertiesUpdate parsedProps = clientHelper
.parseAndValidateMailboxPropertiesUpdate(
getValidMailboxPropsUpdateDict());
assertTrue(mailboxPropertiesUpdateEqual(validMailboxPropsUpdate,
parsedProps));
}
@Test(expected = FormatException.class)
public void testRejectsMailboxPropsUpdateOnionNotDecodable()
throws Exception {
BdfDictionary propsDict = getValidMailboxPropsUpdateDict();
String badOnion = "!" + propsDict.getString(PROP_KEY_ONION)
.substring(1);
propsDict.put(PROP_KEY_ONION, badOnion);
clientHelper.parseAndValidateMailboxPropertiesUpdate(propsDict);
}
@Test(expected = FormatException.class)
public void testRejectsMailboxPropsUpdateOnionWrongLength()
throws Exception {
BdfDictionary propsDict = getValidMailboxPropsUpdateDict();
String tooLongOnion = propsDict.getString(PROP_KEY_ONION) + "!";
propsDict.put(PROP_KEY_ONION, tooLongOnion);
clientHelper.parseAndValidateMailboxPropertiesUpdate(propsDict);
}
@Test(expected = FormatException.class)
public void testRejectsMailboxPropsUpdateInboxIdWrongLength()
throws Exception {
BdfDictionary propsDict = getValidMailboxPropsUpdateDict();
propsDict.put(PROP_KEY_INBOXID, getRandomBytes(UniqueId.LENGTH + 1));
clientHelper.parseAndValidateMailboxPropertiesUpdate(propsDict);
}
@Test(expected = FormatException.class)
public void testRejectsMailboxPropsUpdateOutboxIdWrongLength()
throws Exception {
BdfDictionary propsDict = getValidMailboxPropsUpdateDict();
propsDict.put(PROP_KEY_OUTBOXID, getRandomBytes(UniqueId.LENGTH + 1));
clientHelper.parseAndValidateMailboxPropertiesUpdate(propsDict);
}
@Test(expected = FormatException.class)
public void testRejectsMailboxPropsUpdateAuthTokenWrongLength()
throws Exception {
BdfDictionary propsDict = getValidMailboxPropsUpdateDict();
propsDict.put(PROP_KEY_AUTHTOKEN, getRandomBytes(UniqueId.LENGTH + 1));
clientHelper.parseAndValidateMailboxPropertiesUpdate(propsDict);
}
@Test(expected = FormatException.class)
public void testRejectsMailboxPropsUpdateMissingOnion() throws Exception {
BdfDictionary propsDict = getValidMailboxPropsUpdateDict();
propsDict.remove(PROP_KEY_ONION);
clientHelper.parseAndValidateMailboxPropertiesUpdate(propsDict);
}
@Test(expected = FormatException.class)
public void testRejectsMailboxPropsUpdateMissingAuthToken()
throws Exception {
BdfDictionary propsDict = getValidMailboxPropsUpdateDict();
propsDict.remove(PROP_KEY_AUTHTOKEN);
clientHelper.parseAndValidateMailboxPropertiesUpdate(propsDict);
}
@Test(expected = FormatException.class)
public void testRejectsMailboxPropsUpdateMissingInboxId() throws Exception {
BdfDictionary propsDict = getValidMailboxPropsUpdateDict();
propsDict.remove(PROP_KEY_INBOXID);
clientHelper.parseAndValidateMailboxPropertiesUpdate(propsDict);
}
@Test(expected = FormatException.class)
public void testRejectsMailboxPropsUpdateMissingOutboxId()
throws Exception {
BdfDictionary propsDict = getValidMailboxPropsUpdateDict();
propsDict.remove(PROP_KEY_OUTBOXID);
clientHelper.parseAndValidateMailboxPropertiesUpdate(propsDict);
}
}

View File

@@ -922,11 +922,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(ids));
oneOf(database).getMessage(txn, messageId);
will(returnValue(message));
oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId,
oneOf(database).updateRetransmissionData(txn, contactId, messageId,
maxLatency);
oneOf(database).getMessage(txn, messageId1);
will(returnValue(message1));
oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId1,
oneOf(database).updateRetransmissionData(txn, contactId, messageId1,
maxLatency);
oneOf(database).lowerRequestedFlag(txn, contactId, ids);
oneOf(database).commitTransaction(txn);
@@ -951,9 +951,9 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(true));
oneOf(database).getMessagesToOffer(txn, contactId, 123, maxLatency);
will(returnValue(ids));
oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId,
oneOf(database).updateRetransmissionData(txn, contactId, messageId,
maxLatency);
oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId1,
oneOf(database).updateRetransmissionData(txn, contactId, messageId1,
maxLatency);
oneOf(database).commitTransaction(txn);
}});
@@ -1005,12 +1005,12 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(ids));
oneOf(database).getMessage(txn, messageId);
will(returnValue(message));
oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId,
maxLatency);
oneOf(database).updateRetransmissionData(txn, contactId,
messageId, maxLatency);
oneOf(database).getMessage(txn, messageId1);
will(returnValue(message1));
oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId1,
maxLatency);
oneOf(database).updateRetransmissionData(txn, contactId,
messageId1, maxLatency);
oneOf(database).lowerRequestedFlag(txn, contactId, ids);
oneOf(database).commitTransaction(txn);
oneOf(eventBus).broadcast(with(any(MessagesSentEvent.class)));

View File

@@ -72,9 +72,6 @@ import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERE
import static org.briarproject.bramble.api.sync.validation.MessageState.INVALID;
import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING;
import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN;
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
import static org.briarproject.bramble.db.DatabaseConstants.LAST_COMPACTED_KEY;
import static org.briarproject.bramble.db.DatabaseConstants.MAX_COMPACTION_INTERVAL_MS;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
import static org.briarproject.bramble.test.TestUtils.getAgreementPrivateKey;
import static org.briarproject.bramble.test.TestUtils.getAgreementPublicKey;
@@ -444,7 +441,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertOneMessageToSendEagerly(db, txn);
// Mark the message as sent
db.updateExpiryTimeAndEta(txn, contactId, messageId, MAX_LATENCY);
db.updateRetransmissionData(txn, contactId, messageId, MAX_LATENCY);
// The message should no longer be sendable via lazy retransmission,
// but it should still be sendable via eager retransmission
@@ -1811,7 +1808,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertFalse(status.isSeen());
// Pretend the message was sent to the contact
db.updateExpiryTimeAndEta(txn, contactId, messageId, Integer.MAX_VALUE);
db.updateRetransmissionData(txn, contactId, messageId,
Integer.MAX_VALUE);
// The message should be sent but not seen
status = db.getMessageStatus(txn, contactId, messageId);
@@ -2052,12 +2050,12 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Update the message's expiry time as though we sent it - now the
// message should be sendable after one round-trip
db.updateExpiryTimeAndEta(txn, contactId, messageId, 1000);
db.updateRetransmissionData(txn, contactId, messageId, 1000);
assertEquals(now + 2000, db.getNextSendTime(txn, contactId));
// Update the message's expiry time again - now it should be sendable
// after two round-trips
db.updateExpiryTimeAndEta(txn, contactId, messageId, 1000);
db.updateRetransmissionData(txn, contactId, messageId, 1000);
assertEquals(now + 4000, db.getNextSendTime(txn, contactId));
// Delete the message - there should be no messages to send
@@ -2124,7 +2122,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Time: now
// Mark the message as sent
db.updateExpiryTimeAndEta(txn, contactId, messageId, MAX_LATENCY);
db.updateRetransmissionData(txn, contactId, messageId, MAX_LATENCY);
// The message should expire after 2 * MAX_LATENCY
assertEquals(now + MAX_LATENCY * 2, db.getNextSendTime(txn, contactId));
@@ -2161,6 +2159,51 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true, false, null);
// Retrieve the message from the database
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
ONE_MEGABYTE, MAX_LATENCY);
assertEquals(singletonList(messageId), ids);
// Mark the message as sent
db.updateRetransmissionData(txn, contactId, messageId, MAX_LATENCY);
// The message should expire after 2 * MAX_LATENCY
assertEquals(now + MAX_LATENCY * 2, db.getNextSendTime(txn, contactId));
// The message should not be sendable via the same transport
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
assertTrue(ids.isEmpty());
// The message should be sendable via a transport with a lower latency
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE,
MAX_LATENCY - 1);
assertEquals(singletonList(messageId), ids);
// The message should not be sendable via a slower transport
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE,
MAX_LATENCY + 1);
assertTrue(ids.isEmpty());
db.commitTransaction(txn);
db.close();
}
@Test
public void testResetRetransmissionTimes() throws Exception {
long now = System.currentTimeMillis();
AtomicLong time = new AtomicLong(now);
Database<Connection> db =
open(false, new TestMessageFactory(), new SettableClock(time));
Connection txn = db.startTransaction();
// Add a contact, a shared group and a shared message
db.addIdentity(txn, identity);
assertEquals(contactId,
db.addContact(txn, author, localAuthor.getId(), null, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true, false, null);
// Time: now
// Retrieve the message from the database
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -2169,70 +2212,27 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Time: now
// Mark the message as sent
db.updateExpiryTimeAndEta(txn, contactId, messageId, MAX_LATENCY);
db.updateRetransmissionData(txn, contactId, messageId, MAX_LATENCY);
// The message should expire after 2 * MAX_LATENCY
assertEquals(now + MAX_LATENCY * 2, db.getNextSendTime(txn, contactId));
// Time: now
// The message should not be sendable via the same transport
// Time: now + MAX_LATENCY * 2 - 1
// The message should not yet be sendable
time.set(now + MAX_LATENCY * 2 - 1);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
assertTrue(ids.isEmpty());
// Time: now
// The message should be sendable via a transport with a faster ETA
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE,
MAX_LATENCY - 1);
assertEquals(singletonList(messageId), ids);
// Reset the retransmission times
db.resetUnackedMessagesToSend(txn, contactId);
// Time: now + 1
// The message should no longer be sendable via the faster transport,
// as the ETA is now equal
time.set(now + 1);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE,
MAX_LATENCY - 1);
assertTrue(ids.isEmpty());
// The message should have infinitely short expiry
assertEquals(0, db.getNextSendTime(txn, contactId));
db.commitTransaction(txn);
db.close();
}
// The message should be sendable
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
assertFalse(ids.isEmpty());
@Test
public void testCompactionTime() throws Exception {
MessageFactory messageFactory = new TestMessageFactory();
long now = System.currentTimeMillis();
AtomicLong time = new AtomicLong(now);
Clock clock = new SettableClock(time);
// Time: now
// The last compaction time should be initialised to the current time
Database<Connection> db = open(false, messageFactory, clock);
Connection txn = db.startTransaction();
Settings s = db.getSettings(txn, DB_SETTINGS_NAMESPACE);
assertEquals(now, s.getLong(LAST_COMPACTED_KEY, 0));
db.commitTransaction(txn);
db.close();
// Time: now + MAX_COMPACTION_INTERVAL_MS
// The DB should not be compacted, so the last compaction time should
// not be updated
time.set(now + MAX_COMPACTION_INTERVAL_MS);
db = open(true, messageFactory, clock);
txn = db.startTransaction();
s = db.getSettings(txn, DB_SETTINGS_NAMESPACE);
assertEquals(now, s.getLong(LAST_COMPACTED_KEY, 0));
db.commitTransaction(txn);
db.close();
// Time: now + MAX_COMPACTION_INTERVAL_MS + 1
// The DB should be compacted, so the last compaction time should be
// updated
time.set(now + MAX_COMPACTION_INTERVAL_MS + 1);
db = open(true, messageFactory, clock);
txn = db.startTransaction();
s = db.getSettings(txn, DB_SETTINGS_NAMESPACE);
assertEquals(now + MAX_COMPACTION_INTERVAL_MS + 1,
s.getLong(LAST_COMPACTED_KEY, 0));
db.commitTransaction(txn);
db.close();
}

View File

@@ -11,9 +11,9 @@ import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
import org.briarproject.bramble.test.BrambleTestCase;
import org.jmock.Expectations;
import org.jmock.auto.Mock;
import org.jmock.imposters.ByteBuddyClassImposteriser;
import org.jmock.integration.junit4.JUnitRuleMockery;
import org.jmock.lib.concurrent.Synchroniser;
import org.jmock.lib.legacy.ClassImposteriser;
import org.junit.Rule;
import org.junit.Test;
@@ -35,7 +35,7 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
@Rule
public JUnitRuleMockery context = new JUnitRuleMockery() {{
// So we can mock concrete classes like KeyAgreementTransport
setImposteriser(ClassImposteriser.INSTANCE);
setImposteriser(ByteBuddyClassImposteriser.INSTANCE);
setThreadingPolicy(new Synchroniser());
}};

View File

@@ -14,7 +14,7 @@ import org.briarproject.bramble.api.record.RecordWriterFactory;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.CaptureArgumentAction;
import org.jmock.Expectations;
import org.jmock.lib.legacy.ClassImposteriser;
import org.jmock.imposters.ByteBuddyClassImposteriser;
import org.junit.Test;
import java.io.InputStream;
@@ -58,7 +58,7 @@ public class KeyAgreementTransportTest extends BrambleMockTestCase {
private KeyAgreementTransport kat;
public KeyAgreementTransportTest() {
context.setImposteriser(ClassImposteriser.INSTANCE);
context.setImposteriser(ByteBuddyClassImposteriser.INSTANCE);
inputStream = context.mock(InputStream.class);
outputStream = context.mock(OutputStream.class);
}

View File

@@ -0,0 +1,802 @@
package org.briarproject.bramble.mailbox;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.briarproject.bramble.api.WeakSingletonProvider;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
import org.briarproject.bramble.api.mailbox.MailboxFileId;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
import org.briarproject.bramble.mailbox.MailboxApi.MailboxContact;
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nonnull;
import javax.net.SocketFactory;
import okhttp3.OkHttpClient;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okio.Buffer;
import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.test.TestUtils.getContactId;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.readBytes;
import static org.briarproject.bramble.test.TestUtils.writeBytes;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class MailboxApiTest extends BrambleTestCase {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
private final OkHttpClient client = new OkHttpClient.Builder()
.socketFactory(SocketFactory.getDefault())
.connectTimeout(60_000, MILLISECONDS)
.build();
private final WeakSingletonProvider<OkHttpClient> httpClientProvider =
new WeakSingletonProvider<OkHttpClient>() {
@Override
@Nonnull
public OkHttpClient createInstance() {
return client;
}
};
private final MailboxApiImpl api = new MailboxApiImpl(httpClientProvider);
private final MailboxAuthToken token = new MailboxAuthToken(getRandomId());
private final MailboxAuthToken token2 = new MailboxAuthToken(getRandomId());
private final ContactId contactId = getContactId();
private final MailboxAuthToken contactToken =
new MailboxAuthToken(getRandomId());
private final MailboxFolderId contactInboxId =
new MailboxFolderId(getRandomId());
private final MailboxFolderId contactOutboxId =
new MailboxFolderId(getRandomId());
private final MailboxContact mailboxContact = new MailboxContact(
contactId, contactToken, contactInboxId, contactOutboxId);
@Test
public void testSetup() throws Exception {
String validResponse = "{\"token\":\"" + token2 + "\"}";
String invalidResponse = "{\"foo\":\"bar\"}";
String invalidTokenResponse = "{\"token\":{\"foo\":\"bar\"}}";
String invalidTokenResponse2 =
"{\"token\":\"" + getRandomString(64) + "\"}";
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody(validResponse));
server.enqueue(new MockResponse());
server.enqueue(new MockResponse().setBody(invalidResponse));
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse().setResponseCode(500));
server.enqueue(new MockResponse().setBody(invalidTokenResponse));
server.enqueue(new MockResponse().setBody(invalidTokenResponse2));
server.start();
String baseUrl = getBaseUrl(server);
MailboxProperties properties =
new MailboxProperties(baseUrl, token, true);
MailboxProperties properties2 =
new MailboxProperties(baseUrl, token2, true);
// valid response with valid token
assertEquals(token2, api.setup(properties));
RecordedRequest request1 = server.takeRequest();
assertEquals("/setup", request1.getPath());
assertEquals("PUT", request1.getMethod());
assertToken(request1, token);
// empty body
assertThrows(ApiException.class, () -> api.setup(properties));
RecordedRequest request2 = server.takeRequest();
assertEquals("/setup", request2.getPath());
assertEquals("PUT", request2.getMethod());
assertToken(request2, token);
// invalid response
assertThrows(ApiException.class, () -> api.setup(properties));
RecordedRequest request3 = server.takeRequest();
assertEquals("/setup", request3.getPath());
assertEquals("PUT", request3.getMethod());
assertToken(request3, token);
// 401 response
assertThrows(ApiException.class, () -> api.setup(properties2));
RecordedRequest request4 = server.takeRequest();
assertEquals("/setup", request4.getPath());
assertEquals("PUT", request4.getMethod());
assertToken(request4, token2);
// 500 response
assertThrows(ApiException.class, () -> api.setup(properties));
RecordedRequest request5 = server.takeRequest();
assertEquals("/setup", request5.getPath());
assertEquals("PUT", request5.getMethod());
assertToken(request5, token);
// invalid json dict token response
assertThrows(ApiException.class, () -> api.setup(properties));
RecordedRequest request6 = server.takeRequest();
assertEquals("/setup", request6.getPath());
assertEquals("PUT", request6.getMethod());
assertToken(request6, token);
// invalid non-hex string token response
assertThrows(ApiException.class, () -> api.setup(properties));
RecordedRequest request7 = server.takeRequest();
assertEquals("/setup", request7.getPath());
assertEquals("PUT", request7.getMethod());
assertToken(request7, token);
}
@Test
public void testSetupOnlyForOwner() {
MailboxProperties properties =
new MailboxProperties("", token, false);
assertThrows(
IllegalArgumentException.class,
() -> api.setup(properties)
);
}
@Test
public void testStatus() throws Exception {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse());
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse().setResponseCode(500));
server.start();
String baseUrl = getBaseUrl(server);
MailboxProperties properties =
new MailboxProperties(baseUrl, token, true);
MailboxProperties properties2 =
new MailboxProperties(baseUrl, token2, true);
assertTrue(api.checkStatus(properties));
RecordedRequest request1 = server.takeRequest();
assertEquals("/status", request1.getPath());
assertToken(request1, token);
assertThrows(ApiException.class, () -> api.checkStatus(properties2));
RecordedRequest request2 = server.takeRequest();
assertEquals("/status", request2.getPath());
assertToken(request2, token2);
assertFalse(api.checkStatus(properties));
RecordedRequest request3 = server.takeRequest();
assertEquals("/status", request3.getPath());
assertToken(request3, token);
}
@Test
public void testStatusOnlyForOwner() {
MailboxProperties properties =
new MailboxProperties("", token, false);
assertThrows(
IllegalArgumentException.class,
() -> api.checkStatus(properties)
);
}
@Test
public void testWipe() throws Exception {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(204));
server.enqueue(new MockResponse().setResponseCode(200));
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse().setResponseCode(500));
server.start();
String baseUrl = getBaseUrl(server);
MailboxProperties properties =
new MailboxProperties(baseUrl, token, true);
MailboxProperties properties2 =
new MailboxProperties(baseUrl, token2, true);
api.wipeMailbox(properties);
RecordedRequest request1 = server.takeRequest();
assertEquals("/", request1.getPath());
assertEquals("DELETE", request1.getMethod());
assertToken(request1, token);
assertThrows(ApiException.class, () -> api.wipeMailbox(properties2));
RecordedRequest request2 = server.takeRequest();
assertEquals("/", request2.getPath());
assertEquals("DELETE", request2.getMethod());
assertToken(request2, token2);
assertThrows(ApiException.class, () -> api.wipeMailbox(properties));
RecordedRequest request3 = server.takeRequest();
assertEquals("/", request3.getPath());
assertEquals("DELETE", request3.getMethod());
assertToken(request3, token);
assertThrows(ApiException.class, () -> api.wipeMailbox(properties));
RecordedRequest request4 = server.takeRequest();
assertEquals("/", request4.getPath());
assertEquals("DELETE", request4.getMethod());
assertToken(request4, token);
}
@Test
public void testWipeOnlyForOwner() {
MailboxProperties properties =
new MailboxProperties("", token, false);
assertThrows(IllegalArgumentException.class, () ->
api.wipeMailbox(properties));
}
@Test
public void testAddContact() throws Exception {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse());
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse().setResponseCode(409));
server.start();
String baseUrl = getBaseUrl(server);
MailboxProperties properties =
new MailboxProperties(baseUrl, token, true);
// contact gets added as expected
api.addContact(properties, mailboxContact);
RecordedRequest request1 = server.takeRequest();
assertEquals("/contacts", request1.getPath());
assertToken(request1, token);
String expected = "{\"contactId\":" + contactId.getInt() +
",\"token\":\"" + contactToken +
"\",\"inboxId\":\"" + contactInboxId +
"\",\"outboxId\":\"" + contactOutboxId +
"\"}";
assertEquals(expected, request1.getBody().readUtf8());
// request is not successful
assertThrows(ApiException.class, () ->
api.addContact(properties, mailboxContact));
RecordedRequest request2 = server.takeRequest();
assertEquals("/contacts", request2.getPath());
assertToken(request2, token);
// contact already exists
assertThrows(TolerableFailureException.class, () ->
api.addContact(properties, mailboxContact));
RecordedRequest request3 = server.takeRequest();
assertEquals("/contacts", request3.getPath());
assertToken(request3, token);
}
@Test
public void testAddContactOnlyForOwner() {
MailboxProperties properties =
new MailboxProperties("", token, false);
assertThrows(IllegalArgumentException.class, () ->
api.addContact(properties, mailboxContact));
}
@Test
public void testDeleteContact() throws Exception {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse());
server.enqueue(new MockResponse().setResponseCode(205));
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse().setResponseCode(404));
server.start();
String baseUrl = getBaseUrl(server);
MailboxProperties properties =
new MailboxProperties(baseUrl, token, true);
// contact gets deleted as expected
api.deleteContact(properties, contactId);
RecordedRequest request1 = server.takeRequest();
assertEquals("DELETE", request1.getMethod());
assertEquals("/contacts/" + contactId.getInt(), request1.getPath());
assertToken(request1, token);
// request is not returning 200
assertThrows(ApiException.class, () ->
api.deleteContact(properties, contactId));
RecordedRequest request2 = server.takeRequest();
assertEquals("DELETE", request2.getMethod());
assertEquals("/contacts/" + contactId.getInt(), request2.getPath());
assertToken(request2, token);
// request is not authorized
assertThrows(ApiException.class, () ->
api.deleteContact(properties, contactId));
RecordedRequest request3 = server.takeRequest();
assertEquals("DELETE", request3.getMethod());
assertEquals("/contacts/" + contactId.getInt(), request3.getPath());
assertToken(request3, token);
// tolerable 404 not found error
assertThrows(TolerableFailureException.class,
() -> api.deleteContact(properties, contactId));
RecordedRequest request4 = server.takeRequest();
assertEquals("/contacts/" + contactId.getInt(), request4.getPath());
assertEquals("DELETE", request4.getMethod());
assertToken(request4, token);
}
@Test
public void testDeleteContactOnlyForOwner() {
MailboxProperties properties =
new MailboxProperties("", token, false);
assertThrows(IllegalArgumentException.class, () ->
api.deleteContact(properties, contactId));
}
@Test
public void testGetContacts() throws Exception {
ContactId contactId2 = getContactId();
String validResponse1 = "{\"contacts\": [" + contactId.getInt() + "] }";
String validResponse2 = "{\"contacts\": [" + contactId.getInt() + ", " +
contactId2.getInt() + "] }";
String invalidResponse1 = "{\"foo\":\"bar\"}";
String invalidResponse2 = "{\"contacts\":{\"foo\":\"bar\"}}";
String invalidResponse3 = "{\"contacts\": [1, 2, \"foo\"] }";
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody(validResponse1));
server.enqueue(new MockResponse().setBody(validResponse2));
server.enqueue(new MockResponse());
server.enqueue(new MockResponse().setBody(invalidResponse1));
server.enqueue(new MockResponse().setBody(invalidResponse2));
server.enqueue(new MockResponse().setBody(invalidResponse3));
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse().setResponseCode(500));
server.start();
String baseUrl = getBaseUrl(server);
MailboxProperties properties =
new MailboxProperties(baseUrl, token, true);
// valid response with two contacts
assertEquals(singletonList(contactId), api.getContacts(properties));
RecordedRequest request1 = server.takeRequest();
assertEquals("/contacts", request1.getPath());
assertEquals("GET", request1.getMethod());
assertToken(request1, token);
// valid response with two contacts
List<ContactId> contacts = new ArrayList<>();
contacts.add(contactId);
contacts.add(contactId2);
assertEquals(contacts, api.getContacts(properties));
RecordedRequest request2 = server.takeRequest();
assertEquals("/contacts", request2.getPath());
assertEquals("GET", request2.getMethod());
assertToken(request2, token);
// empty body
assertThrows(ApiException.class, () -> api.getContacts(properties));
RecordedRequest request3 = server.takeRequest();
assertEquals("/contacts", request3.getPath());
assertEquals("GET", request3.getMethod());
assertToken(request3, token);
// invalid response: no contacts key
assertThrows(ApiException.class, () -> api.getContacts(properties));
RecordedRequest request4 = server.takeRequest();
assertEquals("/contacts", request4.getPath());
assertEquals("GET", request4.getMethod());
assertToken(request4, token);
// invalid response: no list in contacts
assertThrows(ApiException.class, () -> api.getContacts(properties));
RecordedRequest request5 = server.takeRequest();
assertEquals("/contacts", request5.getPath());
assertEquals("GET", request5.getMethod());
assertToken(request5, token);
// invalid response: list with non-numbers
assertThrows(ApiException.class, () -> api.getContacts(properties));
RecordedRequest request6 = server.takeRequest();
assertEquals("/contacts", request6.getPath());
assertEquals("GET", request6.getMethod());
assertToken(request6, token);
// 401 not authorized
assertThrows(ApiException.class, () -> api.getContacts(properties));
RecordedRequest request7 = server.takeRequest();
assertEquals("/contacts", request7.getPath());
assertEquals("GET", request7.getMethod());
assertToken(request7, token);
// 500 internal server error
assertThrows(ApiException.class, () -> api.getContacts(properties));
RecordedRequest request8 = server.takeRequest();
assertEquals("/contacts", request8.getPath());
assertEquals("GET", request8.getMethod());
assertToken(request8, token);
}
@Test
public void testGetContactsOnlyForOwner() {
MailboxProperties properties =
new MailboxProperties("", token, false);
assertThrows(
IllegalArgumentException.class,
() -> api.getContacts(properties)
);
}
@Test
public void testAddFile() throws Exception {
File file = folder.newFile();
byte[] bytes = getRandomBytes(1337);
writeBytes(file, bytes);
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse());
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse().setResponseCode(500));
server.start();
String baseUrl = getBaseUrl(server);
MailboxProperties properties =
new MailboxProperties(baseUrl, token, true);
// file gets uploaded as expected
api.addFile(properties, contactInboxId, file);
RecordedRequest request1 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request1.getPath());
assertEquals("POST", request1.getMethod());
assertToken(request1, token);
assertArrayEquals(bytes, request1.getBody().readByteArray());
// request is not successful
assertThrows(ApiException.class, () ->
api.addFile(properties, contactInboxId, file));
RecordedRequest request2 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request2.getPath());
assertEquals("POST", request1.getMethod());
assertToken(request2, token);
// server error
assertThrows(ApiException.class, () ->
api.addFile(properties, contactInboxId, file));
RecordedRequest request3 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request3.getPath());
assertEquals("POST", request1.getMethod());
assertToken(request3, token);
}
@Test
public void testGetFiles() throws Exception {
MailboxFile mailboxFile1 =
new MailboxFile(new MailboxFileId(getRandomId()), 1337);
MailboxFile mailboxFile2 =
new MailboxFile(new MailboxFileId(getRandomId()),
System.currentTimeMillis());
String fileResponse1 =
new ObjectMapper().writeValueAsString(mailboxFile1);
String fileResponse2 =
new ObjectMapper().writeValueAsString(mailboxFile2);
String validResponse1 = "{\"files\": [" + fileResponse1 + "] }";
String validResponse2 = "{\"files\": [" + fileResponse1 + ", " +
fileResponse2 + "] }";
String invalidResponse1 = "{\"files\":\"bar\"}";
String invalidResponse2 = "{\"files\":{\"foo\":\"bar\"}}";
String invalidResponse3 = "{\"files\": [" + fileResponse1 + ", 1] }";
String invalidResponse4 = "{\"contacts\": [ 1, 2 ] }";
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody(validResponse1));
server.enqueue(new MockResponse().setBody(validResponse2));
server.enqueue(new MockResponse());
server.enqueue(new MockResponse().setBody(invalidResponse1));
server.enqueue(new MockResponse().setBody(invalidResponse2));
server.enqueue(new MockResponse().setBody(invalidResponse3));
server.enqueue(new MockResponse().setBody(invalidResponse4));
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse().setResponseCode(500));
server.start();
String baseUrl = getBaseUrl(server);
MailboxProperties properties =
new MailboxProperties(baseUrl, token, true);
// valid response with one file
List<MailboxFile> received1 = api.getFiles(properties, contactInboxId);
assertEquals(1, received1.size());
assertEquals(mailboxFile1.name, received1.get(0).name);
assertEquals(mailboxFile1.time, received1.get(0).time);
RecordedRequest request1 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request1.getPath());
assertEquals("GET", request1.getMethod());
assertToken(request1, token);
// valid response with two files
List<MailboxFile> received2 = api.getFiles(properties, contactInboxId);
assertEquals(2, received2.size());
assertEquals(mailboxFile1.name, received2.get(0).name);
assertEquals(mailboxFile1.time, received2.get(0).time);
assertEquals(mailboxFile2.name, received2.get(1).name);
assertEquals(mailboxFile2.time, received2.get(1).time);
RecordedRequest request2 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request1.getPath());
assertEquals("GET", request2.getMethod());
assertToken(request2, token);
// empty body
assertThrows(ApiException.class, () ->
api.getFiles(properties, contactInboxId));
RecordedRequest request3 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request3.getPath());
assertEquals("GET", request3.getMethod());
assertToken(request3, token);
// invalid response: string instead of list
assertThrows(ApiException.class, () ->
api.getFiles(properties, contactInboxId));
RecordedRequest request4 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request4.getPath());
assertEquals("GET", request4.getMethod());
assertToken(request4, token);
// invalid response: object instead of list
assertThrows(ApiException.class, () ->
api.getFiles(properties, contactInboxId));
RecordedRequest request5 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request5.getPath());
assertEquals("GET", request5.getMethod());
assertToken(request5, token);
// invalid response: list with non-objects
assertThrows(ApiException.class, () ->
api.getFiles(properties, contactInboxId));
RecordedRequest request6 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request6.getPath());
assertEquals("GET", request6.getMethod());
assertToken(request6, token);
// no files key in root object
assertThrows(ApiException.class, () ->
api.getFiles(properties, contactInboxId));
RecordedRequest request7 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request7.getPath());
assertEquals("GET", request7.getMethod());
assertToken(request7, token);
// 401 not authorized
assertThrows(ApiException.class, () ->
api.getFiles(properties, contactInboxId));
RecordedRequest request8 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request8.getPath());
assertEquals("GET", request8.getMethod());
assertToken(request8, token);
// 500 internal server error
assertThrows(ApiException.class,
() -> api.getFiles(properties, contactInboxId));
RecordedRequest request9 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request9.getPath());
assertEquals("GET", request9.getMethod());
assertToken(request9, token);
}
@Test
public void testGetFile() throws Exception {
MailboxFileId name = new MailboxFileId(getRandomId());
File file1 = folder.newFile();
File file2 = folder.newFile();
File file3 = folder.newFile();
byte[] bytes = getRandomBytes(1337);
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody(new Buffer().write(bytes)));
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse().setResponseCode(500));
server.start();
String baseUrl = getBaseUrl(server);
MailboxProperties properties =
new MailboxProperties(baseUrl, token, true);
// file gets downloaded as expected
api.getFile(properties, contactOutboxId, name, file1);
RecordedRequest request1 = server.takeRequest();
assertEquals("/files/" + contactOutboxId + "/" + name,
request1.getPath());
assertEquals("GET", request1.getMethod());
assertToken(request1, token);
assertArrayEquals(bytes, readBytes(file1));
// request is not successful
assertThrows(ApiException.class, () ->
api.getFile(properties, contactOutboxId, name, file2));
RecordedRequest request2 = server.takeRequest();
assertEquals("/files/" + contactOutboxId + "/" + name,
request2.getPath());
assertEquals("GET", request1.getMethod());
assertToken(request2, token);
assertEquals(0, readBytes(file2).length);
// server error
assertThrows(ApiException.class, () ->
api.getFile(properties, contactOutboxId, name, file3));
RecordedRequest request3 = server.takeRequest();
assertEquals("/files/" + contactOutboxId + "/" + name,
request3.getPath());
assertEquals("GET", request1.getMethod());
assertToken(request3, token);
assertEquals(0, readBytes(file3).length);
}
@Test
public void testDeleteFile() throws Exception {
MailboxFileId name = new MailboxFileId(getRandomId());
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse());
server.enqueue(new MockResponse().setResponseCode(205));
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse().setResponseCode(404));
server.start();
String baseUrl = getBaseUrl(server);
MailboxProperties properties =
new MailboxProperties(baseUrl, token, true);
// file gets deleted as expected
api.deleteFile(properties, contactInboxId, name);
RecordedRequest request1 = server.takeRequest();
assertEquals("DELETE", request1.getMethod());
assertEquals("/files/" + contactInboxId + "/" + name,
request1.getPath());
assertToken(request1, token);
// request is not returning 200
assertThrows(ApiException.class, () ->
api.deleteFile(properties, contactInboxId, name));
RecordedRequest request2 = server.takeRequest();
assertEquals("DELETE", request2.getMethod());
assertEquals("/files/" + contactInboxId + "/" + name,
request2.getPath());
assertToken(request2, token);
// request is not authorized
assertThrows(ApiException.class, () ->
api.deleteFile(properties, contactInboxId, name));
RecordedRequest request3 = server.takeRequest();
assertEquals("DELETE", request3.getMethod());
assertEquals("/files/" + contactInboxId + "/" + name,
request3.getPath());
assertToken(request3, token);
// file not found is tolerable
assertThrows(TolerableFailureException.class, () ->
api.deleteFile(properties, contactInboxId, name));
RecordedRequest request4 = server.takeRequest();
assertEquals("DELETE", request4.getMethod());
assertEquals("/files/" + contactInboxId + "/" + name,
request4.getPath());
assertToken(request4, token);
}
@Test
public void testGetFolders() throws Exception {
MailboxFolderId id1 = new MailboxFolderId(getRandomId());
MailboxFolderId id2 = new MailboxFolderId(getRandomId());
String validResponse1 = "{\"folders\": [ {\"id\": \"" + id1 + "\"} ] }";
String validResponse2 = "{\"folders\": [ {\"id\": \"" + id1 + "\"}, " +
"{ \"id\": \"" + id2 + "\"} ] }";
String invalidResponse1 = "{\"folders\":\"bar\"}";
String invalidResponse2 = "{\"folders\":{\"foo\":\"bar\"}}";
String invalidResponse3 =
"{\"folders\": [ {\"id\": \"" + id1 + "\", 1] }";
String invalidResponse4 = "{\"files\": [ 1, 2 ] }";
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody(validResponse1));
server.enqueue(new MockResponse().setBody(validResponse2));
server.enqueue(new MockResponse());
server.enqueue(new MockResponse().setBody(invalidResponse1));
server.enqueue(new MockResponse().setBody(invalidResponse2));
server.enqueue(new MockResponse().setBody(invalidResponse3));
server.enqueue(new MockResponse().setBody(invalidResponse4));
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse().setResponseCode(500));
server.start();
String baseUrl = getBaseUrl(server);
MailboxProperties properties =
new MailboxProperties(baseUrl, token, true);
// valid response with one folders
assertEquals(singletonList(id1), api.getFolders(properties));
RecordedRequest request1 = server.takeRequest();
assertEquals("/folders", request1.getPath());
assertEquals("GET", request1.getMethod());
assertToken(request1, token);
// valid response with two folders
assertEquals(Arrays.asList(id1, id2), api.getFolders(properties));
RecordedRequest request2 = server.takeRequest();
assertEquals("/folders", request1.getPath());
assertEquals("GET", request2.getMethod());
assertToken(request2, token);
// empty body
assertThrows(ApiException.class, () -> api.getFolders(properties));
RecordedRequest request3 = server.takeRequest();
assertEquals("/folders", request3.getPath());
assertEquals("GET", request3.getMethod());
assertToken(request3, token);
// invalid response: string instead of list
assertThrows(ApiException.class, () -> api.getFolders(properties));
RecordedRequest request4 = server.takeRequest();
assertEquals("/folders", request4.getPath());
assertEquals("GET", request4.getMethod());
assertToken(request4, token);
// invalid response: object instead of list
assertThrows(ApiException.class, () -> api.getFolders(properties));
RecordedRequest request5 = server.takeRequest();
assertEquals("/folders", request5.getPath());
assertEquals("GET", request5.getMethod());
assertToken(request5, token);
// invalid response: list with non-objects
assertThrows(ApiException.class, () -> api.getFolders(properties));
RecordedRequest request6 = server.takeRequest();
assertEquals("/folders", request6.getPath());
assertEquals("GET", request6.getMethod());
assertToken(request6, token);
// no folders key in root object
assertThrows(ApiException.class, () -> api.getFolders(properties));
RecordedRequest request7 = server.takeRequest();
assertEquals("/folders", request7.getPath());
assertEquals("GET", request7.getMethod());
assertToken(request7, token);
// 401 not authorized
assertThrows(ApiException.class, () -> api.getFolders(properties));
RecordedRequest request8 = server.takeRequest();
assertEquals("/folders", request8.getPath());
assertEquals("GET", request8.getMethod());
assertToken(request8, token);
// 500 internal server error
assertThrows(ApiException.class, () -> api.getFolders(properties));
RecordedRequest request9 = server.takeRequest();
assertEquals("/folders", request9.getPath());
assertEquals("GET", request9.getMethod());
assertToken(request9, token);
}
@Test
public void testGetFoldersOnlyForOwner() {
MailboxProperties properties =
new MailboxProperties("", token, false);
assertThrows(IllegalArgumentException.class, () ->
api.getFolders(properties));
}
private String getBaseUrl(MockWebServer server) {
String baseUrl = server.url("").toString();
return baseUrl.substring(0, baseUrl.length() - 1);
}
private void assertToken(RecordedRequest request, MailboxId token) {
assertNotNull(request.getHeader("Authorization"));
assertEquals("Bearer " + token, request.getHeader("Authorization"));
}
}

View File

@@ -0,0 +1,272 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.WeakSingletonProvider;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.mailbox.InvalidMailboxIdException;
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
import org.briarproject.bramble.api.mailbox.MailboxFileId;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
import org.briarproject.bramble.mailbox.MailboxApi.MailboxContact;
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nonnull;
import javax.net.SocketFactory;
import okhttp3.OkHttpClient;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
import static org.briarproject.bramble.test.TestUtils.readBytes;
import static org.briarproject.bramble.test.TestUtils.writeBytes;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
public class MailboxIntegrationTest extends BrambleTestCase {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
private final static String URL_BASE = "http://127.0.0.1:8000";
private final static MailboxAuthToken SETUP_TOKEN;
static {
try {
SETUP_TOKEN = MailboxAuthToken.fromString(
"54686973206973206120736574757020746f6b656e20666f722042726961722e");
} catch (InvalidMailboxIdException e) {
throw new IllegalStateException();
}
}
private static final OkHttpClient client = new OkHttpClient.Builder()
.socketFactory(SocketFactory.getDefault())
.connectTimeout(60_000, MILLISECONDS)
.build();
private static final WeakSingletonProvider<OkHttpClient>
httpClientProvider =
new WeakSingletonProvider<OkHttpClient>() {
@Override
@Nonnull
public OkHttpClient createInstance() {
return client;
}
};
private final static MailboxApiImpl api =
new MailboxApiImpl(httpClientProvider);
// needs to be static to keep values across different tests
private static MailboxProperties ownerProperties;
/**
* Called before each test to make sure the mailbox is setup once
* before starting with individual tests.
* {@link BeforeClass} needs to be static, so we can't use the API class.
*/
@Before
public void ensureSetup() throws IOException, ApiException {
// Skip this test unless it's explicitly enabled in the environment
assumeTrue(isOptionalTestEnabled(MailboxIntegrationTest.class));
if (ownerProperties != null) return;
MailboxProperties setupProperties =
new MailboxProperties(URL_BASE, SETUP_TOKEN, true);
MailboxAuthToken ownerToken = api.setup(setupProperties);
ownerProperties = new MailboxProperties(URL_BASE, ownerToken, true);
}
@AfterClass
// we can't test wiping as a regular test as it stops the mailbox
public static void wipe() throws IOException, ApiException {
if (!isOptionalTestEnabled(MailboxIntegrationTest.class)) return;
api.wipeMailbox(ownerProperties);
// check doesn't work anymore
assertThrows(ApiException.class, () ->
api.checkStatus(ownerProperties));
// new setup doesn't work as mailbox is stopping
MailboxProperties setupProperties =
new MailboxProperties(URL_BASE, SETUP_TOKEN, true);
assertThrows(ApiException.class, () -> api.setup(setupProperties));
}
@Test
public void testStatus() throws Exception {
assertTrue(api.checkStatus(ownerProperties));
}
@Test
public void testContactApi() throws Exception {
ContactId contactId1 = new ContactId(1);
ContactId contactId2 = new ContactId(2);
MailboxContact mailboxContact1 = getMailboxContact(contactId1);
MailboxContact mailboxContact2 = getMailboxContact(contactId2);
// no contacts initially
assertEquals(emptyList(), api.getContacts(ownerProperties));
// added contact gets returned
api.addContact(ownerProperties, mailboxContact1);
assertEquals(singletonList(contactId1),
api.getContacts(ownerProperties));
// second contact also gets returned
api.addContact(ownerProperties, mailboxContact2);
assertEquals(Arrays.asList(contactId1, contactId2),
api.getContacts(ownerProperties));
// after both contacts get deleted, the list is empty again
api.deleteContact(ownerProperties, contactId1);
api.deleteContact(ownerProperties, contactId2);
assertEquals(emptyList(), api.getContacts(ownerProperties));
// deleting again is tolerable
assertThrows(TolerableFailureException.class,
() -> api.deleteContact(ownerProperties, contactId2));
}
@Test
public void testFileManagementApi() throws Exception {
// add contact, so we can leave each other files
ContactId contactId = new ContactId(1);
MailboxContact contact = getMailboxContact(contactId);
MailboxProperties contactProperties = new MailboxProperties(
ownerProperties.getBaseUrl(), contact.token, false);
api.addContact(ownerProperties, contact);
// upload a file for our contact
File file1 = folder.newFile();
byte[] bytes1 = getRandomBytes(2048);
writeBytes(file1, bytes1);
api.addFile(ownerProperties, contact.inboxId, file1);
// contact checks files
List<MailboxFile> files1 =
api.getFiles(contactProperties, contact.inboxId);
assertEquals(1, files1.size());
MailboxFileId fileName1 = files1.get(0).name;
// owner can't check files
assertThrows(ApiException.class, () ->
api.getFiles(ownerProperties, contact.inboxId));
// contact downloads file
File file1downloaded = folder.newFile();
api.getFile(contactProperties, contact.inboxId, fileName1,
file1downloaded);
assertArrayEquals(bytes1, readBytes(file1downloaded));
// owner can't download file, even if knowing name
File file1forbidden = folder.newFile();
assertThrows(ApiException.class, () -> api.getFile(ownerProperties,
contact.inboxId, fileName1, file1forbidden));
assertEquals(0, file1forbidden.length());
// owner can't delete file
assertThrows(ApiException.class, () ->
api.deleteFile(ownerProperties, contact.inboxId, fileName1));
// contact deletes file
api.deleteFile(contactProperties, contact.inboxId, fileName1);
assertEquals(0,
api.getFiles(contactProperties, contact.inboxId).size());
// contact uploads two files for the owner
File file2 = folder.newFile();
File file3 = folder.newFile();
byte[] bytes2 = getRandomBytes(2048);
byte[] bytes3 = getRandomBytes(1024);
writeBytes(file2, bytes2);
writeBytes(file3, bytes3);
api.addFile(contactProperties, contact.outboxId, file2);
api.addFile(contactProperties, contact.outboxId, file3);
// owner checks folders with available files
List<MailboxFolderId> folders = api.getFolders(ownerProperties);
assertEquals(singletonList(contact.outboxId), folders);
// owner lists files in contact's outbox
List<MailboxFile> files2 =
api.getFiles(ownerProperties, contact.outboxId);
assertEquals(2, files2.size());
MailboxFileId file2name = files2.get(0).name;
MailboxFileId file3name = files2.get(1).name;
// contact can't list files in contact's outbox
assertThrows(ApiException.class, () ->
api.getFiles(contactProperties, contact.outboxId));
// owner downloads both files from contact's outbox
File file2downloaded = folder.newFile();
File file3downloaded = folder.newFile();
api.getFile(ownerProperties, contact.outboxId, file2name,
file2downloaded);
api.getFile(ownerProperties, contact.outboxId, file3name,
file3downloaded);
byte[] downloadedBytes2 = readBytes(file2downloaded);
byte[] downloadedBytes3 = readBytes(file3downloaded);
// file order is preserved (sorted by time),
// so we know what file is which
assertArrayEquals(bytes2, downloadedBytes2);
assertArrayEquals(bytes3, downloadedBytes3);
// contact can't download files again, even if knowing name
File file2forbidden = folder.newFile();
File file3forbidden = folder.newFile();
assertThrows(ApiException.class, () -> api.getFile(contactProperties,
contact.outboxId, file2name, file2forbidden));
assertThrows(ApiException.class, () -> api.getFile(contactProperties,
contact.outboxId, file3name, file3forbidden));
assertEquals(0, file1forbidden.length());
assertEquals(0, file2forbidden.length());
// contact can't delete files in outbox
assertThrows(ApiException.class, () ->
api.deleteFile(contactProperties, contact.outboxId, file2name));
assertThrows(ApiException.class, () ->
api.deleteFile(contactProperties, contact.outboxId, file3name));
// owner deletes files
api.deleteFile(ownerProperties, contact.outboxId, file2name);
api.deleteFile(ownerProperties, contact.outboxId, file3name);
assertEquals(emptyList(),
api.getFiles(ownerProperties, contact.outboxId));
assertEquals(emptyList(), api.getFolders(ownerProperties));
// deleting a non-existent file is tolerable
assertThrows(TolerableFailureException.class, () ->
api.deleteFile(ownerProperties, contact.outboxId, file3name));
// owner deletes contact again to leave clean state for other tests
api.deleteContact(ownerProperties, contactId);
assertEquals(emptyList(), api.getContacts(ownerProperties));
}
private MailboxContact getMailboxContact(ContactId contactId) {
MailboxAuthToken authToken = new MailboxAuthToken(getRandomId());
MailboxFolderId inboxId = new MailboxFolderId(getRandomId());
MailboxFolderId outboxId = new MailboxFolderId(getRandomId());
return new MailboxContact(contactId, authToken, inboxId, outboxId);
}
}

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