You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
385 lines
17 KiB
385 lines
17 KiB
#!/usr/bin/env python3
|
|
#
|
|
# Copyright 2020 - The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import logging
|
|
|
|
from cert.behavior import IHasBehaviors, SingleArgumentBehavior, ReplyStage
|
|
from cert.closable import Closable
|
|
from cert.closable import safeClose
|
|
from cert.py_acl_manager import PyAclManager
|
|
from cert.truth import assertThat
|
|
import bluetooth_packets_python3 as bt_packets
|
|
from bluetooth_packets_python3 import l2cap_packets
|
|
from bluetooth_packets_python3 import RawBuilder
|
|
from bluetooth_packets_python3.l2cap_packets import CommandCode
|
|
from bluetooth_packets_python3.l2cap_packets import Final
|
|
from bluetooth_packets_python3.l2cap_packets import SegmentationAndReassembly
|
|
from bluetooth_packets_python3.l2cap_packets import SupervisoryFunction
|
|
from bluetooth_packets_python3.l2cap_packets import Poll
|
|
from bluetooth_packets_python3.l2cap_packets import InformationRequestInfoType
|
|
from bluetooth_packets_python3.l2cap_packets import ConfigurationResponseResult
|
|
from cert.event_stream import FilteringEventStream
|
|
from cert.event_stream import IEventStream
|
|
from cert.matchers import L2capMatchers
|
|
from cert.captures import L2capCaptures
|
|
|
|
|
|
class CertL2capChannel(IEventStream):
|
|
|
|
def __init__(self, device, scid, dcid, acl_stream, acl, control_channel, fcs=None):
|
|
self._device = device
|
|
self._scid = scid
|
|
self._dcid = dcid
|
|
self._acl_stream = acl_stream
|
|
self._acl = acl
|
|
self._control_channel = control_channel
|
|
self._config_rsp_received = False
|
|
self._config_rsp_sent = False
|
|
if fcs == l2cap_packets.FcsType.DEFAULT:
|
|
self._our_acl_view = FilteringEventStream(acl_stream, L2capMatchers.ExtractBasicFrameWithFcs(scid))
|
|
else:
|
|
self._our_acl_view = FilteringEventStream(acl_stream, L2capMatchers.ExtractBasicFrame(scid))
|
|
|
|
def get_event_queue(self):
|
|
return self._our_acl_view.get_event_queue()
|
|
|
|
def is_configured(self):
|
|
return self._config_rsp_received and self._config_rsp_sent
|
|
|
|
def send(self, packet):
|
|
frame = l2cap_packets.BasicFrameBuilder(self._dcid, packet)
|
|
self._acl.send(frame.Serialize())
|
|
|
|
def send_i_frame(self,
|
|
tx_seq,
|
|
req_seq,
|
|
f=Final.NOT_SET,
|
|
sar=SegmentationAndReassembly.UNSEGMENTED,
|
|
payload=None,
|
|
fcs=False):
|
|
if fcs == l2cap_packets.FcsType.DEFAULT:
|
|
frame = l2cap_packets.EnhancedInformationFrameWithFcsBuilder(self._dcid, tx_seq, f, req_seq, sar, payload)
|
|
else:
|
|
frame = l2cap_packets.EnhancedInformationFrameBuilder(self._dcid, tx_seq, f, req_seq, sar, payload)
|
|
self._acl.send(frame.Serialize())
|
|
|
|
def send_s_frame(self, req_seq, s=SupervisoryFunction.RECEIVER_READY, p=Poll.NOT_SET, f=Final.NOT_SET):
|
|
frame = l2cap_packets.EnhancedSupervisoryFrameBuilder(self._dcid, s, p, f, req_seq)
|
|
self._acl.send(frame.Serialize())
|
|
|
|
def config_request_for_me(self):
|
|
return L2capMatchers.ConfigurationRequest(self._scid)
|
|
|
|
def send_configure_request(self, options, sid=2, continuation=l2cap_packets.Continuation.END):
|
|
assertThat(self._scid).isNotEqualTo(1)
|
|
request = l2cap_packets.ConfigurationRequestBuilder(sid, self._dcid, continuation, options)
|
|
self._control_channel.send(request)
|
|
|
|
def _send_information_request(self, type):
|
|
assertThat(self._scid).isEqualTo(1)
|
|
signal_id = 3
|
|
information_request = l2cap_packets.InformationRequestBuilder(signal_id, type)
|
|
self.send(information_request)
|
|
|
|
def send_extended_features_request(self):
|
|
self._send_information_request(InformationRequestInfoType.EXTENDED_FEATURES_SUPPORTED)
|
|
|
|
def verify_configuration_request_and_respond(self, result=ConfigurationResponseResult.SUCCESS, options=None):
|
|
request_capture = L2capCaptures.ConfigurationRequest(self._scid)
|
|
assertThat(self._control_channel).emits(request_capture)
|
|
request = request_capture.get()
|
|
sid = request.GetIdentifier()
|
|
if options is None:
|
|
options = []
|
|
config_response = l2cap_packets.ConfigurationResponseBuilder(sid, self._dcid, l2cap_packets.Continuation.END,
|
|
result, options)
|
|
self._control_channel.send(config_response)
|
|
|
|
def send_configuration_response(self, request, result=ConfigurationResponseResult.SUCCESS, options=None):
|
|
sid = request.GetIdentifier()
|
|
if options is None:
|
|
options = []
|
|
config_response = l2cap_packets.ConfigurationResponseBuilder(sid, self._dcid, l2cap_packets.Continuation.END,
|
|
result, options)
|
|
self._control_channel.send(config_response)
|
|
self._config_rsp_sent = True
|
|
|
|
def verify_configuration_response(self, result=ConfigurationResponseResult.SUCCESS):
|
|
assertThat(self._control_channel).emits(L2capMatchers.ConfigurationResponse(result))
|
|
|
|
def disconnect_and_verify(self):
|
|
assertThat(self._scid).isNotEqualTo(1)
|
|
self._control_channel.send(l2cap_packets.DisconnectionRequestBuilder(1, self._dcid, self._scid))
|
|
|
|
assertThat(self._control_channel).emits(L2capMatchers.DisconnectionResponse(self._scid, self._dcid))
|
|
|
|
def verify_disconnect_request(self):
|
|
assertThat(self._control_channel).emits(L2capMatchers.DisconnectionRequest(self._dcid, self._scid))
|
|
|
|
|
|
class CertL2capControlChannelBehaviors(object):
|
|
|
|
def __init__(self, parent):
|
|
self.on_config_req_behavior = SingleArgumentBehavior(
|
|
lambda: CertL2capControlChannelBehaviors.CertReplyStage(parent))
|
|
|
|
def on_config_req(self, matcher):
|
|
return self.on_config_req_behavior.begin(matcher)
|
|
|
|
class CertReplyStage(ReplyStage):
|
|
|
|
def __init__(self, parent):
|
|
self.parent = parent
|
|
|
|
def send_configuration_response(self, result=ConfigurationResponseResult.SUCCESS, options=None):
|
|
self._commit(lambda request: self._send_configuration_response(request, result, options))
|
|
return self
|
|
|
|
def _send_configuration_response(self, request, result=ConfigurationResponseResult.SUCCESS, options=None):
|
|
dcid = request.GetDestinationCid()
|
|
if dcid not in self.parent.scid_to_channel:
|
|
logging.warning("Received config request with unknown dcid")
|
|
return
|
|
self.parent.scid_to_channel[dcid].send_configuration_response(request, result, options)
|
|
|
|
|
|
class CertL2cap(Closable, IHasBehaviors):
|
|
|
|
def __init__(self, device):
|
|
self._device = device
|
|
self._acl_manager = PyAclManager(device)
|
|
self._acl = None
|
|
|
|
self.control_table = {
|
|
CommandCode.CONNECTION_RESPONSE: self._on_connection_response_default,
|
|
CommandCode.CONFIGURATION_REQUEST: self._on_configuration_request_default,
|
|
CommandCode.CONFIGURATION_RESPONSE: self._on_configuration_response_default,
|
|
CommandCode.DISCONNECTION_REQUEST: self._on_disconnection_request_default,
|
|
CommandCode.DISCONNECTION_RESPONSE: self._on_disconnection_response_default,
|
|
CommandCode.INFORMATION_REQUEST: self._on_information_request_default,
|
|
CommandCode.INFORMATION_RESPONSE: self._on_information_response_default
|
|
}
|
|
|
|
self.scid_to_channel = {}
|
|
|
|
self.support_ertm = True
|
|
self.support_fcs = True
|
|
|
|
self._control_behaviors = CertL2capControlChannelBehaviors(self)
|
|
self._control_behaviors.on_config_req_behavior.set_default(self._send_configuration_response_default)
|
|
|
|
def close(self):
|
|
self._acl_manager.close()
|
|
safeClose(self._acl)
|
|
|
|
def get_behaviors(self):
|
|
return self._control_behaviors
|
|
|
|
def connect_acl(self, remote_addr):
|
|
self._acl_manager.initiate_connection(remote_addr)
|
|
self._acl = self._acl_manager.complete_outgoing_connection()
|
|
self.control_channel = CertL2capChannel(
|
|
self._device, 1, 1, self._acl.acl_stream, self._acl, control_channel=None)
|
|
self._acl.acl_stream.register_callback(self._handle_control_packet)
|
|
|
|
def accept_incoming_connection(self):
|
|
self._acl_manager.listen_for_an_incoming_connection()
|
|
self._acl = self._acl_manager.complete_incoming_connection()
|
|
|
|
def open_channel(self, signal_id, psm, scid, fcs=None):
|
|
self.control_channel.send(l2cap_packets.ConnectionRequestBuilder(signal_id, psm, scid))
|
|
|
|
response = L2capCaptures.ConnectionResponse(scid)
|
|
assertThat(self.control_channel).emits(response)
|
|
channel = CertL2capChannel(self._device, scid,
|
|
response.get().GetDestinationCid(), self._acl.acl_stream, self._acl,
|
|
self.control_channel, fcs)
|
|
self.scid_to_channel[scid] = channel
|
|
|
|
return channel
|
|
|
|
def verify_and_respond_open_channel_from_remote(self, psm=0x33, scid=None, fcs=None):
|
|
|
|
request = L2capCaptures.ConnectionRequest(psm)
|
|
assertThat(self.control_channel).emits(request)
|
|
|
|
sid = request.get().GetIdentifier()
|
|
dcid = request.get().GetSourceCid()
|
|
if scid is None or scid in self.scid_to_channel:
|
|
scid = dcid
|
|
channel = CertL2capChannel(self._device, scid, dcid, self._acl.acl_stream, self._acl, self.control_channel, fcs)
|
|
self.scid_to_channel[scid] = channel
|
|
|
|
connection_response = l2cap_packets.ConnectionResponseBuilder(
|
|
sid, scid, dcid, l2cap_packets.ConnectionResponseResult.SUCCESS,
|
|
l2cap_packets.ConnectionResponseStatus.NO_FURTHER_INFORMATION_AVAILABLE)
|
|
self.control_channel.send(connection_response)
|
|
|
|
return channel
|
|
|
|
def verify_and_respond_open_channel_from_remote_and_send_config_req(self, psm=0x33):
|
|
"""
|
|
Verify a connection request, and send a combo packet of connection response and configuration request
|
|
"""
|
|
request = L2capCaptures.ConnectionRequest(psm)
|
|
assertThat(self.control_channel).emits(request)
|
|
|
|
sid = request.get().GetIdentifier()
|
|
dcid = request.get().GetSourceCid()
|
|
scid = dcid
|
|
channel = CertL2capChannel(self._device, scid, dcid, self._acl.acl_stream, self._acl, self.control_channel)
|
|
self.scid_to_channel[scid] = channel
|
|
|
|
# Connection response and config request combo packet
|
|
conn_rsp_and_config_req = RawBuilder([
|
|
0x03, sid, 0x08, 0x00, dcid, 0x00, dcid, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, sid + 1, 0x04, 0x00, dcid,
|
|
0x00, 0x00, 0x00
|
|
])
|
|
self.control_channel.send(conn_rsp_and_config_req)
|
|
|
|
return channel
|
|
|
|
# prefer to use channel abstraction instead, if at all possible
|
|
def send_acl(self, packet):
|
|
self._acl.send(packet.Serialize())
|
|
|
|
def get_control_channel(self):
|
|
return self.control_channel
|
|
|
|
# Disable ERTM when exchange extened feature
|
|
def claim_ertm_unsupported(self):
|
|
self.support_ertm = False
|
|
|
|
def _on_connection_response_default(self, l2cap_control_view):
|
|
pass
|
|
|
|
def _on_configuration_request_default(self, l2cap_control_view):
|
|
request = l2cap_packets.ConfigurationRequestView(l2cap_control_view)
|
|
dcid = request.GetDestinationCid()
|
|
if dcid not in self.scid_to_channel:
|
|
logging.warning("Received config request with unknown dcid")
|
|
return
|
|
self._control_behaviors.on_config_req_behavior.run(request)
|
|
|
|
def _send_configuration_response_default(self, captured_request_view):
|
|
dcid = captured_request_view.GetDestinationCid()
|
|
if dcid not in self.scid_to_channel:
|
|
return
|
|
self.scid_to_channel[dcid].send_configuration_response(captured_request_view)
|
|
|
|
@staticmethod
|
|
def config_option_basic_explicit(mtu=642):
|
|
mtu_opt = l2cap_packets.MtuConfigurationOption()
|
|
mtu_opt.mtu = mtu
|
|
rfc_opt = l2cap_packets.RetransmissionAndFlowControlConfigurationOption()
|
|
rfc_opt.mode = l2cap_packets.RetransmissionAndFlowControlModeOption.L2CAP_BASIC
|
|
return [mtu_opt, rfc_opt]
|
|
|
|
@staticmethod
|
|
def config_option_mtu_explicit(mtu=642):
|
|
mtu_opt = l2cap_packets.MtuConfigurationOption()
|
|
mtu_opt.mtu = mtu
|
|
return [mtu_opt]
|
|
|
|
@staticmethod
|
|
def config_option_ertm(mtu=642,
|
|
fcs=None,
|
|
max_transmit=10,
|
|
mps=1010,
|
|
tx_window_size=10,
|
|
monitor_time_out=2000,
|
|
retransmission_time_out=1000):
|
|
result = []
|
|
mtu_opt = l2cap_packets.MtuConfigurationOption()
|
|
mtu_opt.mtu = mtu
|
|
result.append(mtu_opt)
|
|
if fcs is not None:
|
|
fcs_opt = l2cap_packets.FrameCheckSequenceOption()
|
|
fcs_opt.fcs_type = fcs
|
|
result.append(fcs_opt)
|
|
rfc_opt = l2cap_packets.RetransmissionAndFlowControlConfigurationOption()
|
|
rfc_opt.mode = l2cap_packets.RetransmissionAndFlowControlModeOption.ENHANCED_RETRANSMISSION
|
|
rfc_opt.tx_window_size = tx_window_size
|
|
rfc_opt.max_transmit = max_transmit
|
|
rfc_opt.retransmission_time_out = retransmission_time_out
|
|
rfc_opt.monitor_time_out = monitor_time_out
|
|
rfc_opt.maximum_pdu_size = mps
|
|
result.append(rfc_opt)
|
|
return result
|
|
|
|
@staticmethod
|
|
def config_option_ertm_with_max_transmit_one():
|
|
return CertL2cap.config_option_ertm(max_transmit=1)
|
|
|
|
@staticmethod
|
|
def config_option_ertm_with_mps(mps=1010):
|
|
return CertL2cap.config_option_ertm(mps=mps)
|
|
|
|
def _on_configuration_response_default(self, l2cap_control_view):
|
|
response = l2cap_packets.ConfigurationResponseView(l2cap_control_view)
|
|
scid = response.GetSourceCid()
|
|
if scid not in self.scid_to_channel:
|
|
logging.warning("Received config request with unknown dcid")
|
|
return
|
|
result = response.GetResult()
|
|
if result == ConfigurationResponseResult.SUCCESS:
|
|
self.scid_to_channel[scid]._config_rsp_received = True
|
|
|
|
def _on_disconnection_request_default(self, l2cap_control_view):
|
|
disconnection_request = l2cap_packets.DisconnectionRequestView(l2cap_control_view)
|
|
sid = disconnection_request.GetIdentifier()
|
|
scid = disconnection_request.GetSourceCid()
|
|
dcid = disconnection_request.GetDestinationCid()
|
|
disconnection_response = l2cap_packets.DisconnectionResponseBuilder(sid, dcid, scid)
|
|
self.control_channel.send(disconnection_response)
|
|
|
|
def _on_disconnection_response_default(self, l2cap_control_view):
|
|
pass
|
|
|
|
def _on_information_request_default(self, l2cap_control_view):
|
|
information_request = l2cap_packets.InformationRequestView(l2cap_control_view)
|
|
sid = information_request.GetIdentifier()
|
|
information_type = information_request.GetInfoType()
|
|
if information_type == l2cap_packets.InformationRequestInfoType.CONNECTIONLESS_MTU:
|
|
response = l2cap_packets.InformationResponseConnectionlessMtuBuilder(
|
|
sid, l2cap_packets.InformationRequestResult.SUCCESS, 100)
|
|
self.control_channel.send(response)
|
|
return
|
|
if information_type == l2cap_packets.InformationRequestInfoType.EXTENDED_FEATURES_SUPPORTED:
|
|
response = l2cap_packets.InformationResponseExtendedFeaturesBuilder(
|
|
sid, l2cap_packets.InformationRequestResult.SUCCESS, 0, 0, 0, self.support_ertm, 0, self.support_fcs, 0,
|
|
0, 0, 0, 0)
|
|
self.control_channel.send(response)
|
|
return
|
|
if information_type == l2cap_packets.InformationRequestInfoType.FIXED_CHANNELS_SUPPORTED:
|
|
response = l2cap_packets.InformationResponseFixedChannelsBuilder(
|
|
sid, l2cap_packets.InformationRequestResult.SUCCESS, 2)
|
|
self.control_channel.send(response)
|
|
return
|
|
|
|
def _on_information_response_default(self, l2cap_control_view):
|
|
pass
|
|
|
|
def _handle_control_packet(self, l2cap_packet):
|
|
packet_bytes = l2cap_packet.payload
|
|
l2cap_view = l2cap_packets.BasicFrameView(bt_packets.PacketViewLittleEndian(list(packet_bytes)))
|
|
if l2cap_view.GetChannelId() != 1:
|
|
return
|
|
l2cap_control_view = l2cap_packets.ControlView(l2cap_view.GetPayload())
|
|
fn = self.control_table.get(l2cap_control_view.GetCode())
|
|
if fn is not None:
|
|
fn(l2cap_control_view)
|