Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revise the descriptor API #126

Merged
merged 4 commits into from
Nov 22, 2024
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
46 changes: 18 additions & 28 deletions facedancer/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .request import USBRequestHandler

from .interface import USBInterface
from .descriptor import USBDescribable, USBClassDescriptor
from .descriptor import USBDescribable, USBDescriptor
from .endpoint import USBEndpoint


Expand Down Expand Up @@ -62,36 +62,19 @@ def from_binary_descriptor(cls, data):

# Unpack the main collection of data into the descriptor itself.
descriptor_type, total_length, num_interfaces, index, string_index, \
attributes, max_power = struct.unpack_from('<xBHBBBBB', data[0:length])
attributes, half_max_power = struct.unpack_from('<xBHBBBBB', data[0:length])

configuration = cls(
number=index,
configuration_string=string_index,
max_power=max_power,
max_power=half_max_power * 2,
self_powered=(attributes >> 6) & 1,
supports_remote_wakeup=(attributes >> 5) & 1,
)

# Extract the subordinate descriptors, and parse them.
interfaces = cls._parse_subordinate_descriptors(data[length:total_length])

for interface in interfaces:
configuration.add_interface(interface)

return configuration


@classmethod
def _parse_subordinate_descriptors(cls, data):
"""
Generates descriptor objects from the list of subordinate descriptors.

Args:
data: The raw bytes for the descriptor to be parsed.
"""

# TODO: handle recieving interfaces out of order?
interfaces = []
data = data[length:total_length]
last_interface = None
last_endpoint = None

# Continue parsing until we run out of descriptors.
while data:
Expand All @@ -102,16 +85,23 @@ def _parse_subordinate_descriptors(cls, data):

# If we have an interface descriptor, add it to our list of interfaces.
if isinstance(descriptor, USBInterface):
interfaces.append(descriptor)
configuration.add_interface(descriptor)
last_interface = descriptor
last_endpoint = None
elif isinstance(descriptor, USBEndpoint):
interfaces[-1].add_endpoint(descriptor)
elif isinstance(descriptor, USBClassDescriptor):
interfaces[-1].set_class(descriptor)
last_interface.add_endpoint(descriptor)
last_endpoint = descriptor
elif isinstance(descriptor, USBDescriptor):
descriptor.include_in_config = True
if len(last_interface.endpoints) == 0:
last_interface.add_descriptor(descriptor)
else:
last_endpoint.add_descriptor(descriptor)

# Move on to the next descriptor.
data = data[length:]

return interfaces
return configuration


def __post_init__(self):
Expand Down
21 changes: 18 additions & 3 deletions facedancer/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .magic import AutoInstantiable

from enum import IntEnum
from warnings import warn


class USBDescribable(object):
Expand Down Expand Up @@ -40,8 +41,7 @@ def from_binary_descriptor(cls, data):
if subclass.handles_binary_descriptor(data):
return subclass.from_binary_descriptor(data)

return None

return USBDescriptor.from_binary_descriptor(data)


@dataclass
Expand All @@ -54,18 +54,33 @@ class USBDescriptor(USBDescribable, AutoInstantiable):
type_number : int = None
parent : USBDescribable = None

# Whether this descriptor should be included in a GET_CONFIGURATION response.
include_in_config : bool = False

def __call__(self, index=0):
""" Converts the descriptor object into raw bytes. """
return self.raw

def get_identifier(self):
return self.number
return (self.type_number, self.number)

@classmethod
def from_binary_descriptor(cls, data):
return USBDescriptor(raw=data, type_number=data[1], number=None)

@dataclass
class USBClassDescriptor(USBDescriptor):
""" Class for arbitrary USB Class descriptors. """

include_in_config = True

def __init_subclass__(cls, **kwargs):
warn(
"The USBClassDescriptor class is deprecated. "
"Use USBDescriptor with include_in_config=True instead.",
UserWarning, 3)
super().__init_subclass__(**kwargs)


@dataclass
class USBStringDescriptor(USBDescriptor):
Expand Down
17 changes: 11 additions & 6 deletions facedancer/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ class USBBaseDevice(USBDescribable, USBRequestHandler):

device_speed : DeviceSpeed = None

descriptors : Dict[int, Union[bytes, callable]] = field(default_factory=dict)
# Descriptors that can be requested with the GET_DESCRIPTOR request.
requestable_descriptors : Dict[tuple[int, int], Union[bytes, callable]] = field(default_factory=dict)
configurations : Dict[int, USBConfiguration] = field(default_factory=dict)
backend : FacedancerUSBApp = None

Expand Down Expand Up @@ -134,10 +135,10 @@ def __post_init__(self):

# If we don't have a collection of descriptors, gather any attached to the class.
subordinate_descriptors = instantiate_subordinates(self, USBDescriptor)
self.descriptors.update(subordinate_descriptors)
self.requestable_descriptors.update(subordinate_descriptors)

# Add our basic descriptor handlers.
self.descriptors.update({
self.requestable_descriptors.update({
DescriptorTypes.DEVICE: lambda _ : self.get_descriptor(),
DescriptorTypes.CONFIGURATION: self.get_configuration_descriptor,
DescriptorTypes.STRING: self.get_string_descriptor
Expand Down Expand Up @@ -745,7 +746,7 @@ def get_string_descriptor(self, index:int) -> bytes:

@staticmethod
def handle_generic_get_descriptor_request(
descriptor_container:Union['USBDevice', USBInterface],
self: Union['USBDevice', USBInterface],
request: USBControlRequest):
""" Handle GET_DESCRIPTOR requests; per USB2 [9.4.3] """

Expand All @@ -754,9 +755,13 @@ def handle_generic_get_descriptor_request(
# Extract the core parameters from the request.
descriptor_type = request.value_high
descriptor_index = request.value_low
identifier = (descriptor_type, descriptor_index)

# Try to find the descriptor associate with the request.
response = descriptor_container.descriptors.get(descriptor_type, None)
# Try to find the specific descriptor for the request.
response = self.requestable_descriptors.get(identifier, None)
# If that fails, try to find a function covering this type.
if response is None:
response = self.requestable_descriptors.get(descriptor_type, None)

# If we have a callable, we need to evaluate it to figure
# out what the actual descriptor should be.
Expand Down
2 changes: 1 addition & 1 deletion facedancer/devices/keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class KeyEventEndpoint(USBEndpoint):
#


class USBClassDescriptor(USBClassDescriptor):
class HIDDescriptor(USBDescriptor):
number : int = USBDescriptorTypeNumber.HID
raw : bytes = b'\x09\x21\x10\x01\x00\x01\x22\x2b\x00'

Expand Down
36 changes: 29 additions & 7 deletions facedancer/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

import struct

from typing import Iterable
from dataclasses import dataclass
from typing import Iterable, List, Dict
from dataclasses import dataclass, field

from .magic import AutoInstantiable
from .descriptor import USBDescribable
from .descriptor import USBDescribable, USBDescriptor
from .request import USBRequestHandler, get_request_handler_methods
from .request import to_this_endpoint, standard_request_handler
from .types import USBDirection, USBTransferType, USBSynchronizationType
Expand Down Expand Up @@ -49,6 +49,15 @@ class USBEndpoint(USBDescribable, AutoInstantiable, USBRequestHandler):
max_packet_size : int = 64
interval : int = 0

# Extra bytes that extend the basic endpoint descriptor.
extra_bytes : bytes = b''

# Descriptors that will be included in a GET_CONFIGURATION response.
attached_descriptors : List[USBDescriptor] = field(default_factory=list)

# Descriptors that can be requested with the GET_DESCRIPTOR request.
requestable_descriptors : Dict[tuple[int, int], USBDescriptor] = field(default_factory=dict)

parent : USBDescribable = None


Expand All @@ -75,7 +84,8 @@ def from_binary_descriptor(cls, data):
synchronization_type=sync_type,
usage_type=usage_type,
max_packet_size=max_packet_size,
interval=interval
interval=interval,
extra_bytes=data[7:]
)


Expand Down Expand Up @@ -165,21 +175,33 @@ def attributes(self):
((self.usage_type & 0x03) << 4)


def add_descriptor(self, descriptor: USBDescriptor):
""" Adds the provided descriptor to the endpoint. """
if descriptor.include_in_config:
self.attached_descriptors.append(descriptor)
else:
identifier = descriptor.get_identifier()
self.requestable_descriptors[identifier] = descriptor
descriptor.parent = self


def get_descriptor(self) -> bytes:
""" Get a descriptor string for this endpoint. """
# FIXME: use construct

d = bytearray([
7, # length of descriptor in bytes
5, # descriptor type 5 == endpoint
# length of descriptor in bytes
7 + len(self.extra_bytes),
# descriptor type 5 == endpoint
5,
self.address,
self.attributes,
self.max_packet_size & 0xff,
(self.max_packet_size >> 8) & 0xff,
self.interval
])

return d
return d + self.extra_bytes


#
Expand Down
51 changes: 33 additions & 18 deletions facedancer/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import struct

from typing import Dict, Iterable
from typing import Dict, List, Iterable
from dataclasses import dataclass, field

from .magic import instantiate_subordinates, AutoInstantiable
Expand Down Expand Up @@ -45,8 +45,11 @@ class USBInterface(USBDescribable, AutoInstantiable, USBRequestHandler):

interface_string : str = None

descriptors : Dict[int, bytes] = field(default_factory=dict)
class_descriptor : bytes = None
# Descriptors that will be included in a GET_CONFIGURATION response.
attached_descriptors : List[USBDescriptor] = field(default_factory=list)

# Descriptors that can be requested with the GET_DESCRIPTOR request.
requestable_descriptors : Dict[tuple[int, int], USBDescriptor] = field(default_factory=dict)

endpoints : Dict[int, USBEndpoint] = field(default_factory=dict)
parent : USBDescribable = None
Expand Down Expand Up @@ -75,16 +78,13 @@ def __post_init__(self):

# Capture any descriptors/endpoints declared directly on the class.
self.endpoints.update(instantiate_subordinates(self, USBEndpoint))
self.descriptors.update(instantiate_subordinates(self, USBDescriptor))

# If we weren't provided with a class descriptor, try to find one
# provided on the class.
if self.class_descriptor is None:
class_descriptors = instantiate_subordinates(self, USBClassDescriptor)
self.class_descriptor = list(class_descriptors.values())[0] if class_descriptors else None

# Add in our general interface-descriptor handler.
self.descriptors[USBDescriptorTypeNumber.INTERFACE] = self.get_descriptor
descriptors = instantiate_subordinates(self, USBDescriptor).items()
for (identifier, descriptor) in descriptors:
if descriptor.include_in_config:
self.attached_descriptors.append(descriptor)
else:
self.requestable_descriptors[identifier] = descriptor
descriptor.parent = self

# Populate our request handlers.
self._request_handler_methods = get_request_handler_methods(self)
Expand Down Expand Up @@ -129,6 +129,16 @@ def has_endpoint(self, endpoint_number: int, direction: USBDirection) -> USBEndp
return (self.get_endpoint(endpoint_number, direction) is not None)


def add_descriptor(self, descriptor: USBDescriptor):
""" Adds the provided descriptor to the interface. """
if descriptor.include_in_config:
self.attached_descriptors.append(descriptor)
else:
identifier = descriptor.get_identifier()
self.requestable_descriptors[identifier] = descriptor
descriptor.parent = self


#
# Event handlers.
#
Expand Down Expand Up @@ -226,16 +236,21 @@ def get_descriptor(self) -> bytes:
string_manager.get_index(self.interface_string)
])

# If we have a class object, append its class descriptor...
if self.class_descriptor:
if callable(self.class_descriptor):
d += self.class_descriptor()
for descriptor in self.attached_descriptors:
if callable(descriptor):
d += descriptor()
else:
d += self.class_descriptor
d += descriptor

# ... append each endpoint's endpoint descriptor.
for e in self.endpoints.values():
d += e.get_descriptor()
for descriptor in e.attached_descriptors:
if callable(descriptor):
d += descriptor()
else:
d += descriptor


return d

Expand Down
Loading