bumble 0.0.169__py3-none-any.whl → 0.0.172__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bumble/_version.py +6 -2
- bumble/apps/console.py +1 -1
- bumble/apps/controller_info.py +2 -1
- bumble/apps/pandora_server.py +6 -4
- bumble/apps/speaker/speaker.py +2 -2
- bumble/att.py +77 -51
- bumble/device.py +5 -3
- bumble/gatt.py +22 -20
- bumble/gatt_client.py +67 -32
- bumble/gatt_server.py +75 -31
- bumble/l2cap.py +92 -125
- bumble/pandora/security.py +49 -17
- bumble/smp.py +20 -7
- bumble/transport/android_emulator.py +7 -4
- bumble/transport/android_netsim.py +84 -50
- bumble/transport/common.py +3 -8
- bumble/transport/ws_client.py +9 -7
- bumble/utils.py +109 -1
- {bumble-0.0.169.dist-info → bumble-0.0.172.dist-info}/METADATA +2 -1
- {bumble-0.0.169.dist-info → bumble-0.0.172.dist-info}/RECORD +24 -24
- {bumble-0.0.169.dist-info → bumble-0.0.172.dist-info}/LICENSE +0 -0
- {bumble-0.0.169.dist-info → bumble-0.0.172.dist-info}/WHEEL +0 -0
- {bumble-0.0.169.dist-info → bumble-0.0.172.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.169.dist-info → bumble-0.0.172.dist-info}/top_level.txt +0 -0
bumble/l2cap.py
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
import asyncio
|
|
20
|
+
import enum
|
|
20
21
|
import logging
|
|
21
22
|
import struct
|
|
22
23
|
|
|
@@ -676,56 +677,35 @@ class L2CAP_LE_Flow_Control_Credit(L2CAP_Control_Frame):
|
|
|
676
677
|
|
|
677
678
|
# -----------------------------------------------------------------------------
|
|
678
679
|
class Channel(EventEmitter):
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
STATE_NAMES = {
|
|
703
|
-
CLOSED: 'CLOSED',
|
|
704
|
-
WAIT_CONNECT: 'WAIT_CONNECT',
|
|
705
|
-
WAIT_CONNECT_RSP: 'WAIT_CONNECT_RSP',
|
|
706
|
-
OPEN: 'OPEN',
|
|
707
|
-
WAIT_DISCONNECT: 'WAIT_DISCONNECT',
|
|
708
|
-
WAIT_CREATE: 'WAIT_CREATE',
|
|
709
|
-
WAIT_CREATE_RSP: 'WAIT_CREATE_RSP',
|
|
710
|
-
WAIT_MOVE: 'WAIT_MOVE',
|
|
711
|
-
WAIT_MOVE_RSP: 'WAIT_MOVE_RSP',
|
|
712
|
-
WAIT_MOVE_CONFIRM: 'WAIT_MOVE_CONFIRM',
|
|
713
|
-
WAIT_CONFIRM_RSP: 'WAIT_CONFIRM_RSP',
|
|
714
|
-
WAIT_CONFIG: 'WAIT_CONFIG',
|
|
715
|
-
WAIT_SEND_CONFIG: 'WAIT_SEND_CONFIG',
|
|
716
|
-
WAIT_CONFIG_REQ_RSP: 'WAIT_CONFIG_REQ_RSP',
|
|
717
|
-
WAIT_CONFIG_RSP: 'WAIT_CONFIG_RSP',
|
|
718
|
-
WAIT_CONFIG_REQ: 'WAIT_CONFIG_REQ',
|
|
719
|
-
WAIT_IND_FINAL_RSP: 'WAIT_IND_FINAL_RSP',
|
|
720
|
-
WAIT_FINAL_RSP: 'WAIT_FINAL_RSP',
|
|
721
|
-
WAIT_CONTROL_IND: 'WAIT_CONTROL_IND',
|
|
722
|
-
}
|
|
680
|
+
class State(enum.IntEnum):
|
|
681
|
+
# States
|
|
682
|
+
CLOSED = 0x00
|
|
683
|
+
WAIT_CONNECT = 0x01
|
|
684
|
+
WAIT_CONNECT_RSP = 0x02
|
|
685
|
+
OPEN = 0x03
|
|
686
|
+
WAIT_DISCONNECT = 0x04
|
|
687
|
+
WAIT_CREATE = 0x05
|
|
688
|
+
WAIT_CREATE_RSP = 0x06
|
|
689
|
+
WAIT_MOVE = 0x07
|
|
690
|
+
WAIT_MOVE_RSP = 0x08
|
|
691
|
+
WAIT_MOVE_CONFIRM = 0x09
|
|
692
|
+
WAIT_CONFIRM_RSP = 0x0A
|
|
693
|
+
|
|
694
|
+
# CONFIG substates
|
|
695
|
+
WAIT_CONFIG = 0x10
|
|
696
|
+
WAIT_SEND_CONFIG = 0x11
|
|
697
|
+
WAIT_CONFIG_REQ_RSP = 0x12
|
|
698
|
+
WAIT_CONFIG_RSP = 0x13
|
|
699
|
+
WAIT_CONFIG_REQ = 0x14
|
|
700
|
+
WAIT_IND_FINAL_RSP = 0x15
|
|
701
|
+
WAIT_FINAL_RSP = 0x16
|
|
702
|
+
WAIT_CONTROL_IND = 0x17
|
|
723
703
|
|
|
724
704
|
connection_result: Optional[asyncio.Future[None]]
|
|
725
705
|
disconnection_result: Optional[asyncio.Future[None]]
|
|
726
706
|
response: Optional[asyncio.Future[bytes]]
|
|
727
707
|
sink: Optional[Callable[[bytes], Any]]
|
|
728
|
-
state:
|
|
708
|
+
state: State
|
|
729
709
|
connection: Connection
|
|
730
710
|
|
|
731
711
|
def __init__(
|
|
@@ -741,7 +721,7 @@ class Channel(EventEmitter):
|
|
|
741
721
|
self.manager = manager
|
|
742
722
|
self.connection = connection
|
|
743
723
|
self.signaling_cid = signaling_cid
|
|
744
|
-
self.state =
|
|
724
|
+
self.state = self.State.CLOSED
|
|
745
725
|
self.mtu = mtu
|
|
746
726
|
self.psm = psm
|
|
747
727
|
self.source_cid = source_cid
|
|
@@ -751,10 +731,8 @@ class Channel(EventEmitter):
|
|
|
751
731
|
self.disconnection_result = None
|
|
752
732
|
self.sink = None
|
|
753
733
|
|
|
754
|
-
def
|
|
755
|
-
logger.debug(
|
|
756
|
-
f'{self} state change -> {color(Channel.STATE_NAMES[new_state], "cyan")}'
|
|
757
|
-
)
|
|
734
|
+
def _change_state(self, new_state: State) -> None:
|
|
735
|
+
logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
|
|
758
736
|
self.state = new_state
|
|
759
737
|
|
|
760
738
|
def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
|
|
@@ -767,7 +745,7 @@ class Channel(EventEmitter):
|
|
|
767
745
|
# Check that there isn't already a request pending
|
|
768
746
|
if self.response:
|
|
769
747
|
raise InvalidStateError('request already pending')
|
|
770
|
-
if self.state !=
|
|
748
|
+
if self.state != self.State.OPEN:
|
|
771
749
|
raise InvalidStateError('channel not open')
|
|
772
750
|
|
|
773
751
|
self.response = asyncio.get_running_loop().create_future()
|
|
@@ -787,14 +765,14 @@ class Channel(EventEmitter):
|
|
|
787
765
|
)
|
|
788
766
|
|
|
789
767
|
async def connect(self) -> None:
|
|
790
|
-
if self.state !=
|
|
768
|
+
if self.state != self.State.CLOSED:
|
|
791
769
|
raise InvalidStateError('invalid state')
|
|
792
770
|
|
|
793
771
|
# Check that we can start a new connection
|
|
794
772
|
if self.connection_result:
|
|
795
773
|
raise RuntimeError('connection already pending')
|
|
796
774
|
|
|
797
|
-
self.
|
|
775
|
+
self._change_state(self.State.WAIT_CONNECT_RSP)
|
|
798
776
|
self.send_control_frame(
|
|
799
777
|
L2CAP_Connection_Request(
|
|
800
778
|
identifier=self.manager.next_identifier(self.connection),
|
|
@@ -814,10 +792,10 @@ class Channel(EventEmitter):
|
|
|
814
792
|
self.connection_result = None
|
|
815
793
|
|
|
816
794
|
async def disconnect(self) -> None:
|
|
817
|
-
if self.state !=
|
|
795
|
+
if self.state != self.State.OPEN:
|
|
818
796
|
raise InvalidStateError('invalid state')
|
|
819
797
|
|
|
820
|
-
self.
|
|
798
|
+
self._change_state(self.State.WAIT_DISCONNECT)
|
|
821
799
|
self.send_control_frame(
|
|
822
800
|
L2CAP_Disconnection_Request(
|
|
823
801
|
identifier=self.manager.next_identifier(self.connection),
|
|
@@ -832,8 +810,8 @@ class Channel(EventEmitter):
|
|
|
832
810
|
return await self.disconnection_result
|
|
833
811
|
|
|
834
812
|
def abort(self) -> None:
|
|
835
|
-
if self.state == self.OPEN:
|
|
836
|
-
self.
|
|
813
|
+
if self.state == self.State.OPEN:
|
|
814
|
+
self._change_state(self.State.CLOSED)
|
|
837
815
|
self.emit('close')
|
|
838
816
|
|
|
839
817
|
def send_configure_request(self) -> None:
|
|
@@ -856,7 +834,7 @@ class Channel(EventEmitter):
|
|
|
856
834
|
|
|
857
835
|
def on_connection_request(self, request) -> None:
|
|
858
836
|
self.destination_cid = request.source_cid
|
|
859
|
-
self.
|
|
837
|
+
self._change_state(self.State.WAIT_CONNECT)
|
|
860
838
|
self.send_control_frame(
|
|
861
839
|
L2CAP_Connection_Response(
|
|
862
840
|
identifier=request.identifier,
|
|
@@ -866,24 +844,24 @@ class Channel(EventEmitter):
|
|
|
866
844
|
status=0x0000,
|
|
867
845
|
)
|
|
868
846
|
)
|
|
869
|
-
self.
|
|
847
|
+
self._change_state(self.State.WAIT_CONFIG)
|
|
870
848
|
self.send_configure_request()
|
|
871
|
-
self.
|
|
849
|
+
self._change_state(self.State.WAIT_CONFIG_REQ_RSP)
|
|
872
850
|
|
|
873
851
|
def on_connection_response(self, response):
|
|
874
|
-
if self.state !=
|
|
852
|
+
if self.state != self.State.WAIT_CONNECT_RSP:
|
|
875
853
|
logger.warning(color('invalid state', 'red'))
|
|
876
854
|
return
|
|
877
855
|
|
|
878
856
|
if response.result == L2CAP_Connection_Response.CONNECTION_SUCCESSFUL:
|
|
879
857
|
self.destination_cid = response.destination_cid
|
|
880
|
-
self.
|
|
858
|
+
self._change_state(self.State.WAIT_CONFIG)
|
|
881
859
|
self.send_configure_request()
|
|
882
|
-
self.
|
|
860
|
+
self._change_state(self.State.WAIT_CONFIG_REQ_RSP)
|
|
883
861
|
elif response.result == L2CAP_Connection_Response.CONNECTION_PENDING:
|
|
884
862
|
pass
|
|
885
863
|
else:
|
|
886
|
-
self.
|
|
864
|
+
self._change_state(self.State.CLOSED)
|
|
887
865
|
self.connection_result.set_exception(
|
|
888
866
|
ProtocolError(
|
|
889
867
|
response.result,
|
|
@@ -895,9 +873,9 @@ class Channel(EventEmitter):
|
|
|
895
873
|
|
|
896
874
|
def on_configure_request(self, request) -> None:
|
|
897
875
|
if self.state not in (
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
876
|
+
self.State.WAIT_CONFIG,
|
|
877
|
+
self.State.WAIT_CONFIG_REQ,
|
|
878
|
+
self.State.WAIT_CONFIG_REQ_RSP,
|
|
901
879
|
):
|
|
902
880
|
logger.warning(color('invalid state', 'red'))
|
|
903
881
|
return
|
|
@@ -918,25 +896,28 @@ class Channel(EventEmitter):
|
|
|
918
896
|
options=request.options, # TODO: don't accept everything blindly
|
|
919
897
|
)
|
|
920
898
|
)
|
|
921
|
-
if self.state ==
|
|
922
|
-
self.
|
|
899
|
+
if self.state == self.State.WAIT_CONFIG:
|
|
900
|
+
self._change_state(self.State.WAIT_SEND_CONFIG)
|
|
923
901
|
self.send_configure_request()
|
|
924
|
-
self.
|
|
925
|
-
elif self.state ==
|
|
926
|
-
self.
|
|
902
|
+
self._change_state(self.State.WAIT_CONFIG_RSP)
|
|
903
|
+
elif self.state == self.State.WAIT_CONFIG_REQ:
|
|
904
|
+
self._change_state(self.State.OPEN)
|
|
927
905
|
if self.connection_result:
|
|
928
906
|
self.connection_result.set_result(None)
|
|
929
907
|
self.connection_result = None
|
|
930
908
|
self.emit('open')
|
|
931
|
-
elif self.state ==
|
|
932
|
-
self.
|
|
909
|
+
elif self.state == self.State.WAIT_CONFIG_REQ_RSP:
|
|
910
|
+
self._change_state(self.State.WAIT_CONFIG_RSP)
|
|
933
911
|
|
|
934
912
|
def on_configure_response(self, response) -> None:
|
|
935
913
|
if response.result == L2CAP_Configure_Response.SUCCESS:
|
|
936
|
-
if self.state ==
|
|
937
|
-
self.
|
|
938
|
-
elif self.state in (
|
|
939
|
-
self.
|
|
914
|
+
if self.state == self.State.WAIT_CONFIG_REQ_RSP:
|
|
915
|
+
self._change_state(self.State.WAIT_CONFIG_REQ)
|
|
916
|
+
elif self.state in (
|
|
917
|
+
self.State.WAIT_CONFIG_RSP,
|
|
918
|
+
self.State.WAIT_CONTROL_IND,
|
|
919
|
+
):
|
|
920
|
+
self._change_state(self.State.OPEN)
|
|
940
921
|
if self.connection_result:
|
|
941
922
|
self.connection_result.set_result(None)
|
|
942
923
|
self.connection_result = None
|
|
@@ -966,7 +947,7 @@ class Channel(EventEmitter):
|
|
|
966
947
|
# TODO: decide how to fail gracefully
|
|
967
948
|
|
|
968
949
|
def on_disconnection_request(self, request) -> None:
|
|
969
|
-
if self.state in (
|
|
950
|
+
if self.state in (self.State.OPEN, self.State.WAIT_DISCONNECT):
|
|
970
951
|
self.send_control_frame(
|
|
971
952
|
L2CAP_Disconnection_Response(
|
|
972
953
|
identifier=request.identifier,
|
|
@@ -974,14 +955,14 @@ class Channel(EventEmitter):
|
|
|
974
955
|
source_cid=request.source_cid,
|
|
975
956
|
)
|
|
976
957
|
)
|
|
977
|
-
self.
|
|
958
|
+
self._change_state(self.State.CLOSED)
|
|
978
959
|
self.emit('close')
|
|
979
960
|
self.manager.on_channel_closed(self)
|
|
980
961
|
else:
|
|
981
962
|
logger.warning(color('invalid state', 'red'))
|
|
982
963
|
|
|
983
964
|
def on_disconnection_response(self, response) -> None:
|
|
984
|
-
if self.state !=
|
|
965
|
+
if self.state != self.State.WAIT_DISCONNECT:
|
|
985
966
|
logger.warning(color('invalid state', 'red'))
|
|
986
967
|
return
|
|
987
968
|
|
|
@@ -992,7 +973,7 @@ class Channel(EventEmitter):
|
|
|
992
973
|
logger.warning('unexpected source or destination CID')
|
|
993
974
|
return
|
|
994
975
|
|
|
995
|
-
self.
|
|
976
|
+
self._change_state(self.State.CLOSED)
|
|
996
977
|
if self.disconnection_result:
|
|
997
978
|
self.disconnection_result.set_result(None)
|
|
998
979
|
self.disconnection_result = None
|
|
@@ -1004,7 +985,7 @@ class Channel(EventEmitter):
|
|
|
1004
985
|
f'Channel({self.source_cid}->{self.destination_cid}, '
|
|
1005
986
|
f'PSM={self.psm}, '
|
|
1006
987
|
f'MTU={self.mtu}, '
|
|
1007
|
-
f'state={
|
|
988
|
+
f'state={self.state.name})'
|
|
1008
989
|
)
|
|
1009
990
|
|
|
1010
991
|
|
|
@@ -1014,33 +995,21 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|
|
1014
995
|
LE Credit-based Connection Oriented Channel
|
|
1015
996
|
"""
|
|
1016
997
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
STATE_NAMES = {
|
|
1025
|
-
INIT: 'INIT',
|
|
1026
|
-
CONNECTED: 'CONNECTED',
|
|
1027
|
-
CONNECTING: 'CONNECTING',
|
|
1028
|
-
DISCONNECTING: 'DISCONNECTING',
|
|
1029
|
-
DISCONNECTED: 'DISCONNECTED',
|
|
1030
|
-
CONNECTION_ERROR: 'CONNECTION_ERROR',
|
|
1031
|
-
}
|
|
998
|
+
class State(enum.IntEnum):
|
|
999
|
+
INIT = 0
|
|
1000
|
+
CONNECTED = 1
|
|
1001
|
+
CONNECTING = 2
|
|
1002
|
+
DISCONNECTING = 3
|
|
1003
|
+
DISCONNECTED = 4
|
|
1004
|
+
CONNECTION_ERROR = 5
|
|
1032
1005
|
|
|
1033
1006
|
out_queue: Deque[bytes]
|
|
1034
1007
|
connection_result: Optional[asyncio.Future[LeConnectionOrientedChannel]]
|
|
1035
1008
|
disconnection_result: Optional[asyncio.Future[None]]
|
|
1036
1009
|
out_sdu: Optional[bytes]
|
|
1037
|
-
state:
|
|
1010
|
+
state: State
|
|
1038
1011
|
connection: Connection
|
|
1039
1012
|
|
|
1040
|
-
@staticmethod
|
|
1041
|
-
def state_name(state: int) -> str:
|
|
1042
|
-
return name_or_number(LeConnectionOrientedChannel.STATE_NAMES, state)
|
|
1043
|
-
|
|
1044
1013
|
def __init__(
|
|
1045
1014
|
self,
|
|
1046
1015
|
manager: ChannelManager,
|
|
@@ -1083,19 +1052,17 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|
|
1083
1052
|
self.drained.set()
|
|
1084
1053
|
|
|
1085
1054
|
if connected:
|
|
1086
|
-
self.state =
|
|
1055
|
+
self.state = self.State.CONNECTED
|
|
1087
1056
|
else:
|
|
1088
|
-
self.state =
|
|
1057
|
+
self.state = self.State.INIT
|
|
1089
1058
|
|
|
1090
|
-
def
|
|
1091
|
-
logger.debug(
|
|
1092
|
-
f'{self} state change -> {color(self.state_name(new_state), "cyan")}'
|
|
1093
|
-
)
|
|
1059
|
+
def _change_state(self, new_state: State) -> None:
|
|
1060
|
+
logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
|
|
1094
1061
|
self.state = new_state
|
|
1095
1062
|
|
|
1096
|
-
if new_state == self.CONNECTED:
|
|
1063
|
+
if new_state == self.State.CONNECTED:
|
|
1097
1064
|
self.emit('open')
|
|
1098
|
-
elif new_state == self.DISCONNECTED:
|
|
1065
|
+
elif new_state == self.State.DISCONNECTED:
|
|
1099
1066
|
self.emit('close')
|
|
1100
1067
|
|
|
1101
1068
|
def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
|
|
@@ -1106,7 +1073,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|
|
1106
1073
|
|
|
1107
1074
|
async def connect(self) -> LeConnectionOrientedChannel:
|
|
1108
1075
|
# Check that we're in the right state
|
|
1109
|
-
if self.state != self.INIT:
|
|
1076
|
+
if self.state != self.State.INIT:
|
|
1110
1077
|
raise InvalidStateError('not in a connectable state')
|
|
1111
1078
|
|
|
1112
1079
|
# Check that we can start a new connection
|
|
@@ -1114,7 +1081,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|
|
1114
1081
|
if identifier in self.manager.le_coc_requests:
|
|
1115
1082
|
raise RuntimeError('too many concurrent connection requests')
|
|
1116
1083
|
|
|
1117
|
-
self.
|
|
1084
|
+
self._change_state(self.State.CONNECTING)
|
|
1118
1085
|
request = L2CAP_LE_Credit_Based_Connection_Request(
|
|
1119
1086
|
identifier=identifier,
|
|
1120
1087
|
le_psm=self.le_psm,
|
|
@@ -1134,10 +1101,10 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|
|
1134
1101
|
|
|
1135
1102
|
async def disconnect(self) -> None:
|
|
1136
1103
|
# Check that we're connected
|
|
1137
|
-
if self.state != self.CONNECTED:
|
|
1104
|
+
if self.state != self.State.CONNECTED:
|
|
1138
1105
|
raise InvalidStateError('not connected')
|
|
1139
1106
|
|
|
1140
|
-
self.
|
|
1107
|
+
self._change_state(self.State.DISCONNECTING)
|
|
1141
1108
|
self.flush_output()
|
|
1142
1109
|
self.send_control_frame(
|
|
1143
1110
|
L2CAP_Disconnection_Request(
|
|
@@ -1153,15 +1120,15 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|
|
1153
1120
|
return await self.disconnection_result
|
|
1154
1121
|
|
|
1155
1122
|
def abort(self) -> None:
|
|
1156
|
-
if self.state == self.CONNECTED:
|
|
1157
|
-
self.
|
|
1123
|
+
if self.state == self.State.CONNECTED:
|
|
1124
|
+
self._change_state(self.State.DISCONNECTED)
|
|
1158
1125
|
|
|
1159
1126
|
def on_pdu(self, pdu: bytes) -> None:
|
|
1160
1127
|
if self.sink is None:
|
|
1161
1128
|
logger.warning('received pdu without a sink')
|
|
1162
1129
|
return
|
|
1163
1130
|
|
|
1164
|
-
if self.state != self.CONNECTED:
|
|
1131
|
+
if self.state != self.State.CONNECTED:
|
|
1165
1132
|
logger.warning('received PDU while not connected, dropping')
|
|
1166
1133
|
|
|
1167
1134
|
# Manage the peer credits
|
|
@@ -1240,7 +1207,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|
|
1240
1207
|
self.credits = response.initial_credits
|
|
1241
1208
|
self.connected = True
|
|
1242
1209
|
self.connection_result.set_result(self)
|
|
1243
|
-
self.
|
|
1210
|
+
self._change_state(self.State.CONNECTED)
|
|
1244
1211
|
else:
|
|
1245
1212
|
self.connection_result.set_exception(
|
|
1246
1213
|
ProtocolError(
|
|
@@ -1251,7 +1218,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|
|
1251
1218
|
),
|
|
1252
1219
|
)
|
|
1253
1220
|
)
|
|
1254
|
-
self.
|
|
1221
|
+
self._change_state(self.State.CONNECTION_ERROR)
|
|
1255
1222
|
|
|
1256
1223
|
# Cleanup
|
|
1257
1224
|
self.connection_result = None
|
|
@@ -1271,11 +1238,11 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|
|
1271
1238
|
source_cid=request.source_cid,
|
|
1272
1239
|
)
|
|
1273
1240
|
)
|
|
1274
|
-
self.
|
|
1241
|
+
self._change_state(self.State.DISCONNECTED)
|
|
1275
1242
|
self.flush_output()
|
|
1276
1243
|
|
|
1277
1244
|
def on_disconnection_response(self, response) -> None:
|
|
1278
|
-
if self.state != self.DISCONNECTING:
|
|
1245
|
+
if self.state != self.State.DISCONNECTING:
|
|
1279
1246
|
logger.warning(color('invalid state', 'red'))
|
|
1280
1247
|
return
|
|
1281
1248
|
|
|
@@ -1286,7 +1253,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|
|
1286
1253
|
logger.warning('unexpected source or destination CID')
|
|
1287
1254
|
return
|
|
1288
1255
|
|
|
1289
|
-
self.
|
|
1256
|
+
self._change_state(self.State.DISCONNECTED)
|
|
1290
1257
|
if self.disconnection_result:
|
|
1291
1258
|
self.disconnection_result.set_result(None)
|
|
1292
1259
|
self.disconnection_result = None
|
|
@@ -1339,7 +1306,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|
|
1339
1306
|
return
|
|
1340
1307
|
|
|
1341
1308
|
def write(self, data: bytes) -> None:
|
|
1342
|
-
if self.state != self.CONNECTED:
|
|
1309
|
+
if self.state != self.State.CONNECTED:
|
|
1343
1310
|
logger.warning('not connected, dropping data')
|
|
1344
1311
|
return
|
|
1345
1312
|
|
|
@@ -1367,7 +1334,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|
|
1367
1334
|
def __str__(self) -> str:
|
|
1368
1335
|
return (
|
|
1369
1336
|
f'CoC({self.source_cid}->{self.destination_cid}, '
|
|
1370
|
-
f'State={self.
|
|
1337
|
+
f'State={self.state.name}, '
|
|
1371
1338
|
f'PSM={self.le_psm}, '
|
|
1372
1339
|
f'MTU={self.mtu}/{self.peer_mtu}, '
|
|
1373
1340
|
f'MPS={self.mps}/{self.peer_mps}, '
|
bumble/pandora/security.py
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
import asyncio
|
|
16
|
+
import contextlib
|
|
16
17
|
import grpc
|
|
17
18
|
import logging
|
|
18
19
|
|
|
@@ -27,8 +28,8 @@ from bumble.core import (
|
|
|
27
28
|
)
|
|
28
29
|
from bumble.device import Connection as BumbleConnection, Device
|
|
29
30
|
from bumble.hci import HCI_Error
|
|
31
|
+
from bumble.utils import EventWatcher
|
|
30
32
|
from bumble.pairing import PairingConfig, PairingDelegate as BasePairingDelegate
|
|
31
|
-
from contextlib import suppress
|
|
32
33
|
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
|
33
34
|
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
|
34
35
|
from google.protobuf import wrappers_pb2 # pytype: disable=pyi-error
|
|
@@ -232,7 +233,11 @@ class SecurityService(SecurityServicer):
|
|
|
232
233
|
sc=config.pairing_sc_enable,
|
|
233
234
|
mitm=config.pairing_mitm_enable,
|
|
234
235
|
bonding=config.pairing_bonding_enable,
|
|
235
|
-
identity_address_type=
|
|
236
|
+
identity_address_type=(
|
|
237
|
+
PairingConfig.AddressType.PUBLIC
|
|
238
|
+
if connection.self_address.is_public
|
|
239
|
+
else config.identity_address_type
|
|
240
|
+
),
|
|
236
241
|
delegate=PairingDelegate(
|
|
237
242
|
connection,
|
|
238
243
|
self,
|
|
@@ -294,23 +299,35 @@ class SecurityService(SecurityServicer):
|
|
|
294
299
|
try:
|
|
295
300
|
self.log.debug('Pair...')
|
|
296
301
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
connection.on("pairing", lambda *_: wait_for_security.set_result(True)) # type: ignore
|
|
305
|
-
connection.on("pairing_failure", wait_for_security.set_exception)
|
|
302
|
+
security_result = asyncio.get_running_loop().create_future()
|
|
303
|
+
|
|
304
|
+
with contextlib.closing(EventWatcher()) as watcher:
|
|
305
|
+
|
|
306
|
+
@watcher.on(connection, 'pairing')
|
|
307
|
+
def on_pairing(*_: Any) -> None:
|
|
308
|
+
security_result.set_result('success')
|
|
306
309
|
|
|
307
|
-
|
|
310
|
+
@watcher.on(connection, 'pairing_failure')
|
|
311
|
+
def on_pairing_failure(*_: Any) -> None:
|
|
312
|
+
security_result.set_result('pairing_failure')
|
|
308
313
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
314
|
+
@watcher.on(connection, 'disconnection')
|
|
315
|
+
def on_disconnection(*_: Any) -> None:
|
|
316
|
+
security_result.set_result('connection_died')
|
|
312
317
|
|
|
313
|
-
|
|
318
|
+
if (
|
|
319
|
+
connection.transport == BT_LE_TRANSPORT
|
|
320
|
+
and connection.role == BT_PERIPHERAL_ROLE
|
|
321
|
+
):
|
|
322
|
+
connection.request_pairing()
|
|
323
|
+
else:
|
|
324
|
+
await connection.pair()
|
|
325
|
+
|
|
326
|
+
result = await security_result
|
|
327
|
+
|
|
328
|
+
self.log.debug(f'Pairing session complete, status={result}')
|
|
329
|
+
if result != 'success':
|
|
330
|
+
return SecureResponse(**{result: empty_pb2.Empty()})
|
|
314
331
|
except asyncio.CancelledError:
|
|
315
332
|
self.log.warning("Connection died during encryption")
|
|
316
333
|
return SecureResponse(connection_died=empty_pb2.Empty())
|
|
@@ -369,6 +386,7 @@ class SecurityService(SecurityServicer):
|
|
|
369
386
|
str
|
|
370
387
|
] = asyncio.get_running_loop().create_future()
|
|
371
388
|
authenticate_task: Optional[asyncio.Future[None]] = None
|
|
389
|
+
pair_task: Optional[asyncio.Future[None]] = None
|
|
372
390
|
|
|
373
391
|
async def authenticate() -> None:
|
|
374
392
|
assert connection
|
|
@@ -415,6 +433,10 @@ class SecurityService(SecurityServicer):
|
|
|
415
433
|
if authenticate_task is None:
|
|
416
434
|
authenticate_task = asyncio.create_task(authenticate())
|
|
417
435
|
|
|
436
|
+
def pair(*_: Any) -> None:
|
|
437
|
+
if self.need_pairing(connection, level):
|
|
438
|
+
pair_task = asyncio.create_task(connection.pair())
|
|
439
|
+
|
|
418
440
|
listeners: Dict[str, Callable[..., None]] = {
|
|
419
441
|
'disconnection': set_failure('connection_died'),
|
|
420
442
|
'pairing_failure': set_failure('pairing_failure'),
|
|
@@ -425,6 +447,7 @@ class SecurityService(SecurityServicer):
|
|
|
425
447
|
'connection_encryption_change': on_encryption_change,
|
|
426
448
|
'classic_pairing': try_set_success,
|
|
427
449
|
'classic_pairing_failure': set_failure('pairing_failure'),
|
|
450
|
+
'security_request': pair,
|
|
428
451
|
}
|
|
429
452
|
|
|
430
453
|
# register event handlers
|
|
@@ -452,6 +475,15 @@ class SecurityService(SecurityServicer):
|
|
|
452
475
|
pass
|
|
453
476
|
self.log.debug('Authenticated')
|
|
454
477
|
|
|
478
|
+
# wait for `pair` to finish if any
|
|
479
|
+
if pair_task is not None:
|
|
480
|
+
self.log.debug('Wait for authentication...')
|
|
481
|
+
try:
|
|
482
|
+
await pair_task # type: ignore
|
|
483
|
+
except:
|
|
484
|
+
pass
|
|
485
|
+
self.log.debug('paired')
|
|
486
|
+
|
|
455
487
|
return WaitSecurityResponse(**kwargs)
|
|
456
488
|
|
|
457
489
|
def reached_security_level(
|
|
@@ -523,7 +555,7 @@ class SecurityStorageService(SecurityStorageServicer):
|
|
|
523
555
|
self.log.debug(f"DeleteBond: {address}")
|
|
524
556
|
|
|
525
557
|
if self.device.keystore is not None:
|
|
526
|
-
with suppress(KeyError):
|
|
558
|
+
with contextlib.suppress(KeyError):
|
|
527
559
|
await self.device.keystore.delete(str(address))
|
|
528
560
|
|
|
529
561
|
return empty_pb2.Empty()
|
bumble/smp.py
CHANGED
|
@@ -37,6 +37,7 @@ from typing import (
|
|
|
37
37
|
Optional,
|
|
38
38
|
Tuple,
|
|
39
39
|
Type,
|
|
40
|
+
cast,
|
|
40
41
|
)
|
|
41
42
|
|
|
42
43
|
from pyee import EventEmitter
|
|
@@ -1771,7 +1772,26 @@ class Manager(EventEmitter):
|
|
|
1771
1772
|
cid = SMP_BR_CID if connection.transport == BT_BR_EDR_TRANSPORT else SMP_CID
|
|
1772
1773
|
connection.send_l2cap_pdu(cid, command.to_bytes())
|
|
1773
1774
|
|
|
1775
|
+
def on_smp_security_request_command(
|
|
1776
|
+
self, connection: Connection, request: SMP_Security_Request_Command
|
|
1777
|
+
) -> None:
|
|
1778
|
+
connection.emit('security_request', request.auth_req)
|
|
1779
|
+
|
|
1774
1780
|
def on_smp_pdu(self, connection: Connection, pdu: bytes) -> None:
|
|
1781
|
+
# Parse the L2CAP payload into an SMP Command object
|
|
1782
|
+
command = SMP_Command.from_bytes(pdu)
|
|
1783
|
+
logger.debug(
|
|
1784
|
+
f'<<< Received SMP Command on connection [0x{connection.handle:04X}] '
|
|
1785
|
+
f'{connection.peer_address}: {command}'
|
|
1786
|
+
)
|
|
1787
|
+
|
|
1788
|
+
# Security request is more than just pairing, so let applications handle them
|
|
1789
|
+
if command.code == SMP_SECURITY_REQUEST_COMMAND:
|
|
1790
|
+
self.on_smp_security_request_command(
|
|
1791
|
+
connection, cast(SMP_Security_Request_Command, command)
|
|
1792
|
+
)
|
|
1793
|
+
return
|
|
1794
|
+
|
|
1775
1795
|
# Look for a session with this connection, and create one if none exists
|
|
1776
1796
|
if not (session := self.sessions.get(connection.handle)):
|
|
1777
1797
|
if connection.role == BT_CENTRAL_ROLE:
|
|
@@ -1782,13 +1802,6 @@ class Manager(EventEmitter):
|
|
|
1782
1802
|
)
|
|
1783
1803
|
self.sessions[connection.handle] = session
|
|
1784
1804
|
|
|
1785
|
-
# Parse the L2CAP payload into an SMP Command object
|
|
1786
|
-
command = SMP_Command.from_bytes(pdu)
|
|
1787
|
-
logger.debug(
|
|
1788
|
-
f'<<< Received SMP Command on connection [0x{connection.handle:04X}] '
|
|
1789
|
-
f'{connection.peer_address}: {command}'
|
|
1790
|
-
)
|
|
1791
|
-
|
|
1792
1805
|
# Delegate the handling of the command to the session
|
|
1793
1806
|
session.on_smp_command(command)
|
|
1794
1807
|
|
|
@@ -97,10 +97,13 @@ async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
|
|
|
97
97
|
raise ValueError('invalid mode')
|
|
98
98
|
|
|
99
99
|
# Create the transport object
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
class EmulatorTransport(PumpedTransport):
|
|
101
|
+
async def close(self):
|
|
102
|
+
await super().close()
|
|
103
|
+
await channel.close()
|
|
104
|
+
|
|
105
|
+
transport = EmulatorTransport(
|
|
106
|
+
PumpedPacketSource(hci_device.read), PumpedPacketSink(hci_device.write)
|
|
104
107
|
)
|
|
105
108
|
transport.start()
|
|
106
109
|
|