diff options
-rw-r--r-- | framework/tests/bumble/src/android/bluetooth/RfcommTest.kt | 85 | ||||
-rw-r--r-- | pandora/server/bumble_experimental/rfcomm.py | 45 |
2 files changed, 122 insertions, 8 deletions
diff --git a/framework/tests/bumble/src/android/bluetooth/RfcommTest.kt b/framework/tests/bumble/src/android/bluetooth/RfcommTest.kt index 404847df68..9a55a02242 100644 --- a/framework/tests/bumble/src/android/bluetooth/RfcommTest.kt +++ b/framework/tests/bumble/src/android/bluetooth/RfcommTest.kt @@ -19,15 +19,22 @@ import android.Manifest import android.annotation.SuppressLint import android.bluetooth.test_utils.EnableBluetoothRule import android.content.Context +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.util.Log import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import com.android.bluetooth.flags.Flags import com.android.compatibility.common.util.AdoptShellPermissionsRule import com.google.common.truth.Truth import com.google.protobuf.ByteString +import java.io.IOException import java.time.Duration import java.util.UUID import java.util.concurrent.TimeUnit +import kotlin.concurrent.thread import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import org.junit.After @@ -42,7 +49,6 @@ import org.mockito.kotlin.timeout import org.mockito.kotlin.verify import pandora.RfcommProto import pandora.RfcommProto.ServerId -import pandora.RfcommProto.StartServerRequest @SuppressLint("MissingPermission") @RunWith(AndroidJUnit4::class) @@ -52,9 +58,13 @@ class RfcommTest { private val mManager = mContext.getSystemService(BluetoothManager::class.java) private val mAdapter = mManager!!.adapter - // Gives shell permissions during the test. @Rule(order = 0) @JvmField + val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + // Gives shell permissions during the test. + @Rule(order = 1) + @JvmField val mPermissionsRule = AdoptShellPermissionsRule( InstrumentationRegistry.getInstrumentation().getUiAutomation(), @@ -64,15 +74,21 @@ class RfcommTest { ) // Set up a Bumble Pandora device for the duration of the test. - @Rule(order = 1) @JvmField val mBumble = PandoraDevice() + @Rule(order = 2) @JvmField val mBumble = PandoraDevice() - @Rule(order = 2) @JvmField val enableBluetoothRule = EnableBluetoothRule(false, true) + @Rule(order = 3) @JvmField val enableBluetoothRule = EnableBluetoothRule(false, true) private lateinit var mRemoteDevice: BluetoothDevice private lateinit var host: Host private var mConnectionCounter = 1 private var mProfileServiceListener = mock<BluetoothProfile.ServiceListener>() + @OptIn(ExperimentalStdlibApi::class) + private val bdAddrFormat = HexFormat { bytes { byteSeparator = ":" } } + @OptIn(ExperimentalStdlibApi::class) + private val mLocalAddress: ByteString = + ByteString.copyFrom("DA:4C:10:DE:17:00".hexToByteArray(bdAddrFormat)) + /* Setup: 1. Initialize host and mRemoteDevice @@ -381,6 +397,37 @@ class RfcommTest { } } + /* + Test Steps: + 1. Create listening socket and connect + 2. Disconnect RFCOMM from remote device + */ + @RequiresFlagsEnabled(Flags.FLAG_TRIGGER_SEC_PROC_ON_INC_ACCESS_REQ) + @Test + fun serverSecureConnectThenRemoteDisconnect() { + // connect + val (serverSock, connection) = connectRemoteToListeningSocket() + val disconnectRequest = + RfcommProto.DisconnectionRequest.newBuilder().setConnection(connection).build() + // disconnect from remote + mBumble.rfcommBlocking().disconnect(disconnectRequest) + Truth.assertThat(serverSock.channel).isEqualTo(-1) // ensure disconnected at RFCOMM Layer + } + + /* + Test Steps: + 1. Create listening socket and connect + 2. Disconnect RFCOMM from local device + */ + @RequiresFlagsEnabled(Flags.FLAG_TRIGGER_SEC_PROC_ON_INC_ACCESS_REQ) + @Test + fun serverSecureConnectThenLocalDisconnect() { + // connect + val (serverSock, _) = connectRemoteToListeningSocket() + serverSock.close() + Truth.assertThat(serverSock.channel).isEqualTo(-1) // ensure disconnected at RFCOMM Layer + } + private fun createConnectAcceptSocket( isSecure: Boolean, server: ServerId, @@ -428,7 +475,8 @@ class RfcommTest { uuid: String = TEST_UUID, block: (ServerId) -> Unit, ) { - val request = StartServerRequest.newBuilder().setName(name).setUuid(uuid).build() + val request = + RfcommProto.StartServerRequest.newBuilder().setName(name).setUuid(uuid).build() val response = mBumble.rfcommBlocking().startServer(request) try { @@ -443,6 +491,33 @@ class RfcommTest { } } + private fun connectRemoteToListeningSocket( + name: String = TEST_SERVER_NAME, + uuid: String = TEST_UUID, + ): Pair<BluetoothServerSocket, RfcommProto.RfcommConnection> { + var connection: RfcommProto.RfcommConnection? = null + val connectRequest = + RfcommProto.ConnectionRequest.newBuilder() + .setAddress(mLocalAddress) + .setUuid(uuid) + .build() + val t = thread { + val connectResponse = mBumble.rfcommBlocking().connectToServer(connectRequest) + connection = connectResponse.connection + } + val socket = mAdapter.listenUsingRfcommWithServiceRecord(name, UUID.fromString(uuid)) + + try { + socket.accept(3000) // 3 second timeout + } catch (e: IOException) { + Log.e(TAG, "Unexpected IOException: $e") + } + t.join() + Truth.assertThat(connection).isNotNull() + + return Pair(socket, connection!!) + } + private fun getProfileProxy(context: Context, profile: Int): BluetoothProfile { mAdapter.getProfileProxy(context, mProfileServiceListener, profile) val proxyCaptor = argumentCaptor<BluetoothProfile>() diff --git a/pandora/server/bumble_experimental/rfcomm.py b/pandora/server/bumble_experimental/rfcomm.py index aa59566cf9..2f7c05ed4c 100644 --- a/pandora/server/bumble_experimental/rfcomm.py +++ b/pandora/server/bumble_experimental/rfcomm.py @@ -17,10 +17,13 @@ from typing import Dict, Optional from bumble import core from bumble.device import Device +from bumble.hci import Address from bumble.rfcomm import ( - Server, - make_service_sdp_records, + Client, DLC, + make_service_sdp_records, + find_rfcomm_channel_with_uuid, + Server, ) from bumble.pandora import utils import grpc @@ -30,6 +33,8 @@ from pandora_experimental.rfcomm_pb2 import ( AcceptConnectionResponse, ConnectionRequest, ConnectionResponse, + DisconnectionRequest, + DisconnectionResponse, RfcommConnection, RxRequest, RxResponse, @@ -59,9 +64,12 @@ class RFCOMMService(RFCOMMServicer): class Connection: - def __init__(self, dlc): + client: Optional[Client] + + def __init__(self, dlc, client=None): self.dlc = dlc self.data_queue = asyncio.Queue() + self.client = client class ServerPort: @@ -84,6 +92,27 @@ class RFCOMMService(RFCOMMServicer): self.saved_dlc = dlc @utils.rpc + async def ConnectToServer(self, request: ConnectionRequest, context: grpc.ServicerContext) -> ConnectionResponse: + logging.info(f"ConnectToServer") + address = Address(address=bytes(reversed(request.address)), address_type=Address.PUBLIC_DEVICE_ADDRESS) + acl_connection = self.device.find_connection_by_bd_addr(address, transport=0) # BR/EDR + if acl_connection is None: + acl_connection = await self.device.connect(address, transport=0) # BR/EDR transport + + channel = await find_rfcomm_channel_with_uuid(acl_connection, request.uuid) + + client = Client(acl_connection) + mux = await client.start() + assert mux is not None + + dlc = await mux.open_dlc(channel) + id = self.next_conn_id + self.next_conn_id += 1 + self.connections[id] = self.Connection(dlc=dlc, client=client) + self.connections[id].dlc.sink = self.connections[id].data_queue.put_nowait + return ConnectionResponse(connection=RfcommConnection(id=id)) + + @utils.rpc async def StartServer(self, request: StartServerRequest, context: grpc.ServicerContext) -> StartServerResponse: uuid = core.UUID(request.uuid) logging.info(f"StartServer {uuid}") @@ -119,6 +148,16 @@ class RFCOMMService(RFCOMMServicer): return AcceptConnectionResponse(connection=RfcommConnection(id=id)) @utils.rpc + async def Disconnect(self, request: DisconnectionRequest, context: grpc.ServicerContext) -> DisconnectionResponse: + logging.info(f"Disconnect") + rfcomm_connection = self.connections[request.connection.id] + assert rfcomm_connection is not None + if rfcomm_connection.client is not None: + await rfcomm_connection.client.shutdown() + del rfcomm_connection + return DisconnectionResponse() + + @utils.rpc async def StopServer(self, request: StopServerRequest, context: grpc.ServicerContext) -> StopServerResponse: logging.info(f"StopServer") assert self.server_ports[request.server.id] is not None |