blob: 124fe4344f6c50ece85df1711facf442d7ccb9b1 [file] [log] [blame]
# Copyright (c) 2020 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import base64
import json
import logging
import os
from . import bluetooth_sdp_socket
from . import bluetooth_socket
from .bluetooth_virtual_device import BluetoothVirtualDevice
from chameleond.devices.chameleon_device import VirtualFlow
class TesterException(Exception):
"""A dummy exception class for Bluez class."""
pass
class BluetoothTester(BluetoothVirtualDevice, VirtualFlow):
"""Exposes Tester methods called remotely during Bluetooth autotests.
All instance methods of this object without a preceding '_' are exposed via
an XML-RPC server. This is not a stateless handler object, which means that
if you store state inside the delegate, that state will remain around for
future calls.
"""
BR_EDR_LE_PROFILE = (
bluetooth_socket.MGMT_SETTING_POWERED |
bluetooth_socket.MGMT_SETTING_CONNECTABLE |
bluetooth_socket.MGMT_SETTING_PAIRABLE |
bluetooth_socket.MGMT_SETTING_SSP |
bluetooth_socket.MGMT_SETTING_BREDR |
bluetooth_socket.MGMT_SETTING_LE)
LE_PROFILE = (
bluetooth_socket.MGMT_SETTING_POWERED |
bluetooth_socket.MGMT_SETTING_CONNECTABLE |
bluetooth_socket.MGMT_SETTING_PAIRABLE |
bluetooth_socket.MGMT_SETTING_LE)
PROFILE_SETTINGS = {
'computer': BR_EDR_LE_PROFILE,
'peripheral': LE_PROFILE
}
# Class of device/service. This can be generated using
# http://bluetooth-pentest.narod.ru/software/bluetooth_class_of_...
# device-service_generator.html
PROFILE_CLASS = {
'computer': 0x000104, # Desktop computer.
'peripheral': 0x000504 # Keyboard.
}
PROFILE_NAMES = {
'computer': ('ChromeOS Bluetooth Tester', 'Tester'),
'peripheral': ('ChromeOS Bluetooth Tester', 'Tester')
}
def __init__(self):
super(BluetoothTester, self).__init__('Tester')
# Open the Bluetooth Control socket to the kernel which provides us
# the needed raw management access to the Bluetooth Host Subsystem.
self._control = bluetooth_socket.BluetoothControlSocket()
# Open the Bluetooth SDP socket to the kernel which provides us the
# needed interface to use SDP commands.
self._sdp = bluetooth_sdp_socket.BluetoothSDPSocket()
# This is almost a constant, but it might not be forever.
self.index = 0
def AdapterPowerOff(self):
""" Turns the Bluetooth adapter off"""
_control = bluetooth_socket.BluetoothControlSocket()
return _control.set_powered(self.index, False)
def AdapterPowerOn(self):
""" Turns the Bluetooth adapter on"""
_control = bluetooth_socket.BluetoothControlSocket()
return _control.set_powered(self.index, True)
def ResetStack(self, next_device_type=None):
"""Restores BT stack to pristine state by restarting running services"""
# Restart bluetooth adapter
self.AdapterPowerOff()
self.AdapterPowerOn()
reset_cmds = ['service bluetooth restart',
'/etc/init.d/chameleond restart']
# Restart chameleon and bluetooth service
os.system(' && '.join(reset_cmds))
def setup(self, profile):
"""Set up the tester with the given profile.
@param profile: Profile to use for this test, valid values are:
computer - a standard computer profile
@return True on success, False otherwise.
"""
profile_settings = self.PROFILE_SETTINGS[profile]
profile_class = self.PROFILE_CLASS[profile]
(profile_name, profile_short_name) = self.PROFILE_NAMES[profile]
# b:144864466 In this class's functions, we create local sockets, as
# stale socket can result in memory allocation error
_control = bluetooth_socket.BluetoothControlSocket()
# Make sure the controller actually exists.
if self.index not in _control.read_index_list():
logging.warning('Bluetooth Controller missing on tester')
return False
# Make sure all of the settings are supported by the controller.
( address, bluetooth_version, manufacturer_id,
supported_settings, current_settings, class_of_device,
name, short_name ) = _control.read_info(self.index)
logging.info('Supported: {}; Requested: {}; Current: {}'.format(
supported_settings, profile_settings, current_settings))
if profile_settings & supported_settings != profile_settings:
logging.warning('Controller does not support requested settings')
return False
# The high 8 bits of class_of_device is part of Class of Service field
# which are not actually updated in kernel with
# _control.set_device_class() below due to a bug in kernel
# mgmt.c: set_dev_class(). Hence, we should keep these bits in
# profile_class to make the test setup correctly.
# Refer to this link about Class of Device/Service bits.
# https://www.bluetooth.com/specifications/assigned-numbers/baseband
# Since the class of device is used as an indication only and is not
# practically useful in autotest, the service class bits are just
# copied from previous _control.read_info() request.
# Refer to Bluetooth Spec. 4.2, "Vol 3, Part C, 3.2.4.4 Usage" about
# why it is not actually important.
# Refer to "Vol 2. Part E, 7.3.26 Write Class of Device Command" about
# the correct parameters to pass to set_device_class() which require
# 3 bytes instead of 2 bytes.
# Remove the following statement which modifies profile_class only
# when kernel is fixed and 3 bytes are passed in set_dev_class().
# However, this is of very low priority.
profile_class = ((class_of_device & 0xFF0000) |
(profile_class & 0x00FFFF))
# Class of Device/Service is set to 0 when only LE is enabled and it is
# set when BR_EDR is enabled . So the first byte of class_of_device will
# differ when adapter is changed from LE only to BR_EDR + LE.
# Ignore difference in first byte of class_of_device if that is the case
# Check if we are changing from LE to BREDR
le_to_bredr = bool((current_settings & bluetooth_socket.MGMT_SETTING_LE)
and (profile_settings
& bluetooth_socket.MGMT_SETTING_BREDR))
# Before beginning, force the adapter power off, even if it's already
# off; this is enough to persuade an AP-mode Intel chip to accept
# settings.
if not _control.set_powered(self.index, False):
logging.warning('Failed to power off adapter to accept settings')
return False
# Set the controller up as either BR/EDR only, LE only or Dual Mode.
# This is a bit tricky because it rejects commands outright unless
# it's in dual mode, so we actually have to figure out what changes
# we have to make, and we have to turn things on before we turn them
# off.
turn_on = (current_settings ^ profile_settings) & profile_settings
if turn_on & bluetooth_socket.MGMT_SETTING_BREDR:
if _control.set_bredr(self.index, True) is None:
logging.warning('Failed to enable BR/EDR')
return False
if turn_on & bluetooth_socket.MGMT_SETTING_LE:
if _control.set_le(self.index, True) is None:
logging.warning('Failed to enable LE')
return False
turn_off = (current_settings ^ profile_settings) & current_settings
if turn_off & bluetooth_socket.MGMT_SETTING_BREDR:
if _control.set_bredr(self.index, False) is None:
logging.warning('Failed to disable BR/EDR')
return False
if turn_off & bluetooth_socket.MGMT_SETTING_LE:
if _control.set_le(self.index, False) is None:
logging.warning('Failed to disable LE')
return False
if turn_off & bluetooth_socket.MGMT_SETTING_SECURE_CONNECTIONS:
if _control.set_secure_connections(self.index, False) is None:
logging.warning('Failed to disable secure connections')
return False
# Adjust settings that are BR/EDR specific that we need to set before
# powering on the adapter, and would be rejected otherwise.
if profile_settings & bluetooth_socket.MGMT_SETTING_BREDR:
if (_control.set_link_security(
self.index,
(profile_settings &
bluetooth_socket.MGMT_SETTING_LINK_SECURITY))
is None):
logging.warning('Failed to set link security setting')
return False
if (_control.set_ssp(
self.index,
profile_settings & bluetooth_socket.MGMT_SETTING_SSP)
is None):
logging.warning('Failed to set SSP setting')
return False
if (_control.set_hs(
self.index,
profile_settings & bluetooth_socket.MGMT_SETTING_HS)
is None):
logging.warning('Failed to set High Speed setting')
return False
# Split our the major and minor class; it's listed as a kernel bug
# that we supply these to the kernel without shifting the bits over
# to take out the CoD format field, so this might have to change
# one day.
major_class = (profile_class & 0x00ff00) >> 8
minor_class = profile_class & 0x0000ff
if (_control.set_device_class(
self.index, major_class, minor_class)
is None):
logging.warning('Failed to set device class')
return False
# Setup generic settings that apply to either BR/EDR, LE or dual-mode
# that still require the power to be off.
if (_control.set_connectable(
self.index,
profile_settings & bluetooth_socket.MGMT_SETTING_CONNECTABLE)
is None):
logging.warning('Failed to set connectable setting')
return False
if (_control.set_pairable(
self.index,
profile_settings & bluetooth_socket.MGMT_SETTING_PAIRABLE)
is None):
logging.warning('Failed to set pairable setting')
return False
if (_control.set_local_name(
self.index, profile_name, profile_short_name)
is None):
logging.warning('Failed to set local name')
return False
# Check and set discoverable property
if ((profile_settings ^ current_settings) &
bluetooth_socket.MGMT_SETTING_DISCOVERABLE):
logging.debug('Set discoverable to %x ',
profile_settings &
bluetooth_socket.MGMT_SETTING_DISCOVERABLE)
if _control.set_discoverable(
self.index,
profile_settings &
bluetooth_socket.MGMT_SETTING_DISCOVERABLE) is None:
logging.warning('Failed to set discoverable setting')
return False
# Now the settings have been set, power up the adapter.
if not _control.set_powered(
self.index,
profile_settings & bluetooth_socket.MGMT_SETTING_POWERED):
logging.warning('Failed to set powered setting')
return False
# Fast connectable can only be set once the controller is powered,
# and only when BR/EDR is enabled.
if profile_settings & bluetooth_socket.MGMT_SETTING_BREDR:
# Wait for the device class set event, this happens after the
# power up "command complete" event when we've pre-set the class
# even though it's a side-effect of doing that.
_control.wait_for_events(
self.index,
( bluetooth_socket.MGMT_EV_CLASS_OF_DEV_CHANGED, ))
if (_control.set_fast_connectable(
self.index,
profile_settings &
bluetooth_socket.MGMT_SETTING_FAST_CONNECTABLE)
is None):
logging.warning('Failed to set fast connectable setting')
return False
# Fetch the settings again and make sure they're all set correctly,
# including the BR/EDR flag.
( address, bluetooth_version, manufacturer_id,
supported_settings, current_settings, class_of_device,
name, short_name ) = _control.read_info(self.index)
# Check generic settings.
if profile_settings != current_settings:
logging.warning('Controller settings did not match those set: '
'%x != %x', current_settings, profile_settings)
return False
if name != profile_name:
logging.warning('Local name did not match that set: "%s" != "%s"',
name, profile_name)
return False
elif short_name != profile_short_name:
logging.warning('Short name did not match that set: "%s" != "%s"',
short_name, profile_short_name)
return False
# Check BR/EDR specific settings.
if profile_settings & bluetooth_socket.MGMT_SETTING_BREDR:
if class_of_device != profile_class:
if class_of_device & 0x00ffff == profile_class & 0x00ffff:
if not le_to_bredr:
logging.warning('Class of device matched that set, but '
'Service Class field did not: %x != %x '
'Reboot Tester? ',
class_of_device, profile_class)
else:
logging.debug('Service Class field differs but it is'
'expected since adapter changed from'
'LE only to BR/EDR')
return True
else:
logging.warning('Class of device did not match that set: '
'%x != %x', class_of_device, profile_class)
return False
return True
def set_discoverable(self, discoverable, timeout=0):
"""Set the discoverable state of the controller.
@param discoverable: Whether controller should be discoverable.
@param timeout: Timeout in seconds before disabling discovery again,
ignored when discoverable is False, must not be zero when
discoverable is True.
@return True on success, False otherwise.
"""
_control = bluetooth_socket.BluetoothControlSocket()
settings = _control.set_discoverable(self.index, discoverable, timeout)
return settings & bluetooth_socket.MGMT_SETTING_DISCOVERABLE
def read_info(self):
"""Read the adapter information from the Kernel.
@return the information as a JSON-encoded tuple of:
( address, bluetooth_version, manufacturer_id,
supported_settings, current_settings, class_of_device,
name, short_name )
"""
_control = bluetooth_socket.BluetoothControlSocket()
return json.dumps(_control.read_info(self.index))
def set_advertising(self, advertising):
"""Set the whether the controller is advertising via LE.
@param advertising: Whether controller should advertise via LE.
@return True on success, False otherwise.
"""
_control = bluetooth_socket.BluetoothControlSocket()
settings = _control.set_advertising(self.index, advertising)
return settings & bluetooth_socket.MGMT_SETTING_ADVERTISING
def discover_devices(self, br_edr=True, le_public=True, le_random=True):
"""Discover remote devices.
Activates device discovery and collects the set of devices found,
returning them as a list.
@param br_edr: Whether to detect BR/EDR devices.
@param le_public: Whether to detect LE Public Address devices.
@param le_random: Whether to detect LE Random Address devices.
@return List of devices found as JSON-encoded tuples with the format
(address, address_type, rssi, flags, base64-encoded eirdata),
or False if discovery could not be started.
"""
address_type = 0
if br_edr:
address_type |= 0x1
if le_public:
address_type |= 0x2
if le_random:
address_type |= 0x4
_control = bluetooth_socket.BluetoothControlSocket()
set_type = _control.start_discovery(self.index, address_type)
if set_type != address_type:
logging.warning('Discovery address type did not match that set: '
'%x != %x', set_type, address_type)
return False
devices = _control.get_discovered_devices(self.index)
return json.dumps([
(address, address_type, rssi, flags,
base64.encodestring(eirdata))
for address, address_type, rssi, flags, eirdata in devices
])
def connect(self, address):
"""Connect to device with the given address
@param address: Bluetooth address.
"""
self._sdp.connect(address)
return True
def service_search_request(self, uuids, max_rec_cnt, opts):
"""Send a Service Search Request
@param uuids: List of UUIDs (as integers) to look for.
@param max_rec_cnt: Maximum count of returned service records.
@param preferred_size: Preffered size of UUIDs in bits (16, 32, or 128).
@param forced_pdu_size: Use certain PDU size parameter instead of
calculating actual length of sequence.
@param invalid_request: Whether to send request with intentionally
invalid syntax for testing purposes (bool flag).
@return list of found services' service record handles or Error Code
"""
res = self._sdp.service_search_request(
uuids, max_rec_cnt,
opts.get('preferred_size', 32),
opts.get('forced_pdu_size'),
opts.get('invalid_request'))
return json.dumps(res)
def service_attribute_request(self, handle, max_attr_byte_count, attr_ids,
opts):
"""Send a Service Attribute Request
@param handle: service record from which attribute values are to be
retrieved.
@param max_attr_byte_count: maximum number of bytes of attribute data to
be returned in the response to this request.
@param attr_ids: a list, where each element is either an attribute ID
or a range of attribute IDs.
@param opts: dictionary of options, containing optional 'forced_pdu_size'
or 'invalid_request' arguments
@param forced_pdu_size: Use certain PDU size parameter instead of
calculating actual length of sequence.
@param invalid_request: Whether to send request with intentionally
invalid syntax for testing purposes (string with raw request).
@return list of found attributes IDs and their values or Error Code
"""
return json.dumps(
self._sdp.service_attribute_request(
handle, max_attr_byte_count, attr_ids,
opts.get('forced_pdu_size'),
opts.get('invalid_request'))
)
def service_search_attribute_request(self, uuids, max_attr_byte_count,
attr_ids, opts):
"""Send a Service Search Attribute Request
@param uuids: list of UUIDs (as integers) to look for.
@param max_attr_byte_count: maximum number of bytes of attribute data to
be returned in the response to this request.
@param attr_ids: a list, where each element is either an attribute ID
or a range of attribute IDs.
@param preferred_size: Preffered size of UUIDs in bits (16, 32, or 128).
@param forced_pdu_size: Use certain PDU size parameter instead of
calculating actual length of sequence.
@param invalid_request: Whether to send request with intentionally
invalid syntax for testing purposes (string to be prepended
to correct request).
@return list of found attributes IDs and their values or Error Code
"""
return json.dumps(
self._sdp.service_search_attribute_request(
uuids, max_attr_byte_count, attr_ids,
opts.get('preferred_size', 32),
opts.get('forced_pdu_size'),
opts.get('invalid_request'))
)