Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions bumble/gatt_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
# -----------------------------------------------------------------------------
# Constants
# -----------------------------------------------------------------------------
GATT_SERVER_MAX_PREPARED_WRITES = 64 # Max queued Prepare Write Requests per bearer
GATT_SERVER_DEFAULT_MAX_MTU = 517


Expand Down Expand Up @@ -1176,7 +1177,18 @@ def on_att_prepare_write_request(
return

# Queue the partial value, to be committed on Execute Write Request
self.prepared_writes.setdefault(bearer, []).append(
queue = self.prepared_writes.setdefault(bearer, [])
if len(queue) >= GATT_SERVER_MAX_PREPARED_WRITES:
self.send_response(
bearer,
att.ATT_Error_Response(
request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.attribute_handle,
error_code=att.ATT_PREPARE_QUEUE_FULL_ERROR,
),
)
return
queue.append(
(
request.attribute_handle,
request.value_offset,
Expand Down Expand Up @@ -1210,7 +1222,8 @@ async def on_att_execute_write_request(
return

try:
# Reassemble the queued parts; reject a gap with INVALID_OFFSET
# Reassemble the queued parts; reject a gap with INVALID_OFFSET and
# an oversized value with INVALID_ATTRIBUTE_LENGTH
values: dict[int, bytearray] = {}
for handle, offset, part in queue:
buffer = values.setdefault(handle, bytearray())
Expand All @@ -1219,6 +1232,11 @@ async def on_att_execute_write_request(
error_code=att.ATT_INVALID_OFFSET_ERROR, att_handle=handle
)
buffer[offset : offset + len(part)] = part
if len(buffer) > GATT_MAX_ATTRIBUTE_VALUE_SIZE:
raise att.ATT_Error(
error_code=att.ATT_INVALID_ATTRIBUTE_LENGTH_ERROR,
att_handle=handle,
)

# Commit the reassembled values through the normal write path
for handle, value in values.items():
Expand Down
70 changes: 69 additions & 1 deletion tests/gatt_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import pytest
from typing_extensions import Self

from bumble import att, gatt_client, l2cap
from bumble import att, gatt_client, gatt_server, l2cap
from bumble.att import (
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
ATT_PDU,
Expand All @@ -44,6 +44,7 @@
from bumble.gatt import (
GATT_BATTERY_LEVEL_CHARACTERISTIC,
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
GATT_MAX_ATTRIBUTE_VALUE_SIZE,
Characteristic,
CharacteristicValue,
Descriptor,
Expand Down Expand Up @@ -1840,6 +1841,73 @@ async def test_write_long_value_cancel():
assert characteristic.value is None


# -----------------------------------------------------------------------------
async def _prepare_write_long_value(client, handle, value):
part_size = client.mtu - 5
for offset in range(0, len(value), part_size):
response = await client.send_request(
att.ATT_Prepare_Write_Request(
attribute_handle=handle,
value_offset=offset,
part_attribute_value=value[offset : offset + part_size],
)
)
assert isinstance(response, att.ATT_Prepare_Write_Response)


# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_write_long_value_too_long_rejected():
peer, _, characteristic = await _connect_with_writeable_characteristic()
client = peer.gatt_client

# A value of exactly the maximum attribute value size is accepted
max_value = bytes(i % 256 for i in range(GATT_MAX_ATTRIBUTE_VALUE_SIZE))
await _prepare_write_long_value(client, characteristic.handle, max_value)
response = await client.send_request(att.ATT_Execute_Write_Request(flags=0x01))
assert isinstance(response, att.ATT_Execute_Write_Response)
await async_barrier()
assert characteristic.value == max_value

# One byte more is rejected with INVALID_ATTRIBUTE_LENGTH, and not written
await _prepare_write_long_value(client, characteristic.handle, max_value + b'\0')
response = await client.send_request(att.ATT_Execute_Write_Request(flags=0x01))
assert isinstance(response, att.ATT_Error_Response)
assert response.error_code == att.ATT_INVALID_ATTRIBUTE_LENGTH_ERROR
assert response.attribute_handle_in_error == characteristic.handle
await async_barrier()
assert characteristic.value == max_value


# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_write_long_value_queue_full():
peer, _, characteristic = await _connect_with_writeable_characteristic()
client = peer.gatt_client

request = att.ATT_Prepare_Write_Request(
attribute_handle=characteristic.handle,
value_offset=0,
part_attribute_value=bytes([1, 2, 3, 4]),
)

# Fill the queue
for _ in range(gatt_server.GATT_SERVER_MAX_PREPARED_WRITES):
response = await client.send_request(request)
assert isinstance(response, att.ATT_Prepare_Write_Response)

# One more is rejected with PREPARE_QUEUE_FULL
response = await client.send_request(request)
assert isinstance(response, att.ATT_Error_Response)
assert response.error_code == att.ATT_PREPARE_QUEUE_FULL_ERROR

# Cancelling empties the queue, which then accepts new parts
response = await client.send_request(att.ATT_Execute_Write_Request(flags=0x00))
assert isinstance(response, att.ATT_Execute_Write_Response)
response = await client.send_request(request)
assert isinstance(response, att.ATT_Prepare_Write_Response)


# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_write_long_value_gap_rejected():
Expand Down
Loading