mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
Merge 0eca9daae43ebd92b769c934eccfa504e7e16faf into c127eb83ab94c069c32d37530d2faecd381cd2a8
This commit is contained in:
commit
e25cf851d3
@ -101,6 +101,10 @@ class BluetoothConnectionManager(
|
||||
// Public property for address-peer mapping
|
||||
val addressPeerMap get() = connectionTracker.addressPeerMap
|
||||
|
||||
fun noteVerifiedAnnounceReceived(deviceAddress: String) {
|
||||
connectionTracker.noteAnnounceReceived(deviceAddress)
|
||||
}
|
||||
|
||||
fun observePeerIfCurrent(deviceAddress: String, linkID: String, peerID: String): Boolean =
|
||||
connectionTracker.observePeerIfCurrent(deviceAddress, linkID, peerID)
|
||||
|
||||
|
||||
@ -30,6 +30,9 @@ class BluetoothConnectionTracker(
|
||||
val addressPeerMap = ConcurrentHashMap<String, String>()
|
||||
// Track whether we have seen the first ANNOUNCE on a given device connection
|
||||
private val firstAnnounceSeen = ConcurrentHashMap<String, Boolean>()
|
||||
// CCCD writes that arrived before a verified ANNOUNCE. Grant the feed later
|
||||
// rather than rejecting the descriptor — Android clients do not retry it.
|
||||
private val pendingBroadcastSubscriptions = ConcurrentHashMap<String, BluetoothDevice>()
|
||||
// RSSI tracking from scan results (for devices we discover but may connect as servers)
|
||||
private val scanRSSI = ConcurrentHashMap<String, Int>()
|
||||
private val connectionStateLock = Any()
|
||||
@ -85,6 +88,7 @@ class BluetoothConnectionTracker(
|
||||
removePendingConnection(deviceAddress)
|
||||
// Mark as awaiting first ANNOUNCE on this connection
|
||||
firstAnnounceSeen[deviceAddress] = false
|
||||
pendingBroadcastSubscriptions.remove(deviceAddress)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -276,6 +280,7 @@ class BluetoothConnectionTracker(
|
||||
subscribedDevices.removeAll { it.address == deviceAddress }
|
||||
addressPeerMap.remove(deviceAddress)
|
||||
firstAnnounceSeen.remove(deviceAddress)
|
||||
pendingBroadcastSubscriptions.remove(deviceAddress)
|
||||
}
|
||||
Log.d(TAG, "Cleaned up device connection for $deviceAddress")
|
||||
}
|
||||
@ -292,6 +297,7 @@ class BluetoothConnectionTracker(
|
||||
subscribedDevices.removeAll { it.address == deviceAddress }
|
||||
addressPeerMap.remove(deviceAddress)
|
||||
firstAnnounceSeen.remove(deviceAddress)
|
||||
pendingBroadcastSubscriptions.remove(deviceAddress)
|
||||
Log.d(TAG, "Cleaned up device connection for $deviceAddress")
|
||||
true
|
||||
} else {
|
||||
@ -330,13 +336,47 @@ class BluetoothConnectionTracker(
|
||||
pendingConnections.clear()
|
||||
scanRSSI.clear()
|
||||
firstAnnounceSeen.clear()
|
||||
pendingBroadcastSubscriptions.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark that we have received the first ANNOUNCE over this device connection.
|
||||
* Record a CCCD enable. Grants the broadcast feed immediately when a verified
|
||||
* ANNOUNCE already arrived on this connection; otherwise defers until it does.
|
||||
*
|
||||
* @return true if the feed was granted now
|
||||
*/
|
||||
fun requestBroadcastSubscription(device: BluetoothDevice): Boolean {
|
||||
synchronized(connectionStateLock) {
|
||||
val action = GattNotificationEligibility.action(
|
||||
firstAnnounceSeen[device.address] == true
|
||||
)
|
||||
if (action == GattSubscriptionAction.GRANT) {
|
||||
pendingBroadcastSubscriptions.remove(device.address)
|
||||
addSubscribedDeviceIfAbsent(device)
|
||||
return true
|
||||
}
|
||||
pendingBroadcastSubscriptions[device.address] = device
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark that we have received the first ANNOUNCE over this device connection
|
||||
* and grant any CCCD write that was waiting on it.
|
||||
*/
|
||||
fun noteAnnounceReceived(deviceAddress: String) {
|
||||
firstAnnounceSeen[deviceAddress] = true
|
||||
synchronized(connectionStateLock) {
|
||||
firstAnnounceSeen[deviceAddress] = true
|
||||
pendingBroadcastSubscriptions.remove(deviceAddress)?.let { device ->
|
||||
addSubscribedDeviceIfAbsent(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSubscribedDeviceIfAbsent(device: BluetoothDevice) {
|
||||
if (subscribedDevices.none { it.address == device.address }) {
|
||||
subscribedDevices.add(device)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -283,7 +283,13 @@ class BluetoothGattServerManager(
|
||||
}
|
||||
|
||||
if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) {
|
||||
connectionTracker.addSubscribedDevice(device)
|
||||
val granted = connectionTracker.requestBroadcastSubscription(device)
|
||||
if (!granted) {
|
||||
Log.i(
|
||||
TAG,
|
||||
"Deferring broadcast feed for ${device.address} until verified ANNOUNCE"
|
||||
)
|
||||
}
|
||||
|
||||
connectionScope.launch {
|
||||
delay(100)
|
||||
|
||||
@ -579,6 +579,10 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
||||
val result = messageHandler.handleAnnounceWithResult(routed)
|
||||
if (result !is AnnounceHandlingResult.Accepted) return false
|
||||
|
||||
routed.relayAddress?.let { deviceAddress ->
|
||||
connectionManager.noteVerifiedAnnounceReceived(deviceAddress)
|
||||
}
|
||||
|
||||
DirectLinkAnnouncementPolicy.observationFor(routed, MAX_TTL)?.let { observation ->
|
||||
if (connectionManager.observePeerIfCurrent(
|
||||
observation.relayAddress,
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
/**
|
||||
* Whether a BLE central may receive the mesh broadcast notification feed.
|
||||
*
|
||||
* Granting the feed before the peer has sent a verified ANNOUNCE would let a
|
||||
* passive listener harvest it with no participation (#901). Android clients
|
||||
* write the CCCD during service discovery, before they send that ANNOUNCE, and
|
||||
* they do not retry the descriptor write — so a GATT reject here would leave a
|
||||
* legitimate peer unsubscribed even after they announce. Defer instead: accept
|
||||
* the CCCD write, withhold packets, then grant once the ANNOUNCE verifies.
|
||||
*/
|
||||
internal enum class GattSubscriptionAction {
|
||||
GRANT,
|
||||
DEFER,
|
||||
}
|
||||
|
||||
internal object GattNotificationEligibility {
|
||||
fun action(hasVerifiedAnnounce: Boolean): GattSubscriptionAction =
|
||||
if (hasVerifiedAnnounce) GattSubscriptionAction.GRANT else GattSubscriptionAction.DEFER
|
||||
}
|
||||
@ -79,6 +79,58 @@ class BluetoothConnectionTrackerLinkObservationTest {
|
||||
assertTrue(tracker.addressPeerMap.containsValue(PEER_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cccd before announce withholds the feed then grants it`() {
|
||||
val address = "AA:BB:CC:DD:EE:FF"
|
||||
val device = mock<BluetoothDevice>()
|
||||
whenever(device.address).thenReturn(address)
|
||||
tracker.addDeviceConnection(
|
||||
address,
|
||||
BluetoothConnectionTracker.DeviceConnection(device = device, linkID = "link-a")
|
||||
)
|
||||
|
||||
assertFalse(tracker.requestBroadcastSubscription(device))
|
||||
assertTrue(tracker.getSubscribedDevices().none { it.address == address })
|
||||
|
||||
tracker.noteAnnounceReceived(address)
|
||||
assertTrue(tracker.getSubscribedDevices().any { it.address == address })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `announce before cccd grants the feed on the descriptor write`() {
|
||||
val address = "AA:BB:CC:DD:EE:00"
|
||||
val device = mock<BluetoothDevice>()
|
||||
whenever(device.address).thenReturn(address)
|
||||
tracker.addDeviceConnection(
|
||||
address,
|
||||
BluetoothConnectionTracker.DeviceConnection(device = device, linkID = "link-b")
|
||||
)
|
||||
|
||||
tracker.noteAnnounceReceived(address)
|
||||
assertTrue(tracker.requestBroadcastSubscription(device))
|
||||
assertTrue(tracker.getSubscribedDevices().any { it.address == address })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disconnect drops a deferred broadcast subscription`() {
|
||||
val address = "AA:BB:CC:DD:EE:01"
|
||||
val device = mock<BluetoothDevice>()
|
||||
whenever(device.address).thenReturn(address)
|
||||
tracker.addDeviceConnection(
|
||||
address,
|
||||
BluetoothConnectionTracker.DeviceConnection(device = device, linkID = "link-c")
|
||||
)
|
||||
tracker.requestBroadcastSubscription(device)
|
||||
tracker.cleanupDeviceConnection(address)
|
||||
|
||||
tracker.addDeviceConnection(
|
||||
address,
|
||||
BluetoothConnectionTracker.DeviceConnection(device = device, linkID = "link-d")
|
||||
)
|
||||
tracker.noteAnnounceReceived(address)
|
||||
assertTrue(tracker.getSubscribedDevices().none { it.address == address })
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PEER_ID = "0011223344556677"
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class GattNotificationEligibilityTest {
|
||||
@Test
|
||||
fun `subscription is deferred before a verified announce`() {
|
||||
assertEquals(
|
||||
GattSubscriptionAction.DEFER,
|
||||
GattNotificationEligibility.action(hasVerifiedAnnounce = false)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `subscription is granted after a verified announce`() {
|
||||
assertEquals(
|
||||
GattSubscriptionAction.GRANT,
|
||||
GattNotificationEligibility.action(hasVerifiedAnnounce = true)
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user