File: snoop.py

package info (click to toggle)
python-bumble 0.0.225-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 9,464 kB
  • sloc: python: 75,258; java: 3,782; javascript: 823; xml: 203; sh: 172; makefile: 8
file content (288 lines) | stat: -rw-r--r-- 10,043 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# Copyright 2021-2023 Google LLC
#
# 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
#
#      https://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 datetime
import logging
import os
import struct
from collections.abc import Generator

# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
from contextlib import contextmanager
from enum import IntEnum
from typing import BinaryIO

from bumble import core
from bumble.hci import HCI_COMMAND_PACKET, HCI_EVENT_PACKET

# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)


# -----------------------------------------------------------------------------
# Classes
# -----------------------------------------------------------------------------
class Snooper:
    """
    Base class for snooper implementations.

    A snooper is an object that will be provided with HCI packets as they are
    exchanged between a host and a controller.
    """

    class Direction(IntEnum):
        HOST_TO_CONTROLLER = 0
        CONTROLLER_TO_HOST = 1

    class DataLinkType(IntEnum):
        H1 = 1001
        H4 = 1002
        HCI_BSCP = 1003
        H5 = 1004

    def snoop(self, hci_packet: bytes, direction: Direction) -> None:
        """Snoop on an HCI packet."""


# -----------------------------------------------------------------------------
class BtSnooper(Snooper):
    """
    Snooper that saves HCI packets using the BTSnoop format, based on RFC 1761.
    """

    IDENTIFICATION_PATTERN = b'btsnoop\0'
    TIMESTAMP_ANCHOR = datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc)
    TIMESTAMP_DELTA = 0x00E03AB44A676000
    ONE_MS = datetime.timedelta(microseconds=1)

    def __init__(self, output: BinaryIO):
        self.output = output

        # Write the header
        self.output.write(
            self.IDENTIFICATION_PATTERN + struct.pack('>LL', 1, self.DataLinkType.H4)
        )

    def snoop(self, hci_packet: bytes, direction: Snooper.Direction) -> None:
        flags = int(direction)
        packet_type = hci_packet[0]
        if packet_type in (HCI_EVENT_PACKET, HCI_COMMAND_PACKET):
            flags |= 0x10

        # Compute the current timestamp
        timestamp = (
            int(
                (
                    datetime.datetime.now(tz=datetime.timezone.utc)
                    - self.TIMESTAMP_ANCHOR
                )
                / self.ONE_MS
            )
            + self.TIMESTAMP_DELTA
        )

        # Emit the record
        self.output.write(
            struct.pack(
                '>IIIIQ',
                len(hci_packet),  # Original Length
                len(hci_packet),  # Included Length
                flags,  # Packet Flags
                0,  # Cumulative Drops
                timestamp,  # Timestamp
            )
            + hci_packet
        )


# -----------------------------------------------------------------------------
class PcapSnooper(Snooper):
    """
    Snooper that saves or streames HCI packets using the PCAP format.
    """

    PCAP_MAGIC = 0xA1B2C3D4
    DLT_BLUETOOTH_HCI_H4_WITH_PHDR = 201

    def __init__(self, output: BinaryIO):
        self.output = output

        # Write the header
        self.output.write(
            struct.pack(
                "<IHHIIII",
                self.PCAP_MAGIC,
                2,  # Major PCAP Version
                4,  # Minor PCAP Version
                0,  # Reserved 1
                0,  # Reserved 2
                65535,  # SnapLen
                # FCS and f are set to 0 implicitly by the next line
                self.DLT_BLUETOOTH_HCI_H4_WITH_PHDR,  # The DLT in this PCAP
            )
        )

    def snoop(self, hci_packet: bytes, direction: Snooper.Direction):
        now = datetime.datetime.now(datetime.timezone.utc)
        sec = int(now.timestamp())
        usec = now.microsecond

        # Emit the record
        self.output.write(
            struct.pack(
                "<IIII",
                sec,  # Timestamp (Seconds)
                usec,  # Timestamp (Microseconds)
                len(hci_packet) + 4,
                len(hci_packet) + 4,  # +4 because of the addtional direction info...
            )
            + struct.pack(">I", int(direction))  # ...thats being added here
            + hci_packet
        )
        self.output.flush()  # flush after every packet for live logging


# -----------------------------------------------------------------------------
_SNOOPER_INSTANCE_COUNT = 0


@contextmanager
def create_snooper(spec: str) -> Generator[Snooper, None, None]:
    """
    Create a snooper given a specification string.

    The general syntax for the specification string is:
      <snooper-type>:<type-specific-arguments>

    Supported snooper types are:

      btsnoop
        The syntax for the type-specific arguments for this type is:
        <io-type>:<io-type-specific-arguments>

        Supported I/O types are:

        file
          The type-specific arguments for this I/O type is a string that is converted
          to a file path using the python `str.format()` string formatting. The log
          records will be written to that file if it can be opened/created.
          The keyword args that may be referenced by the string pattern are:
            now: the value of `datetime.now()`
            utcnow: the value of `datetime.now(tz=datetime.timezone.utc)`
            pid: the current process ID.
            instance: the instance ID in the current process.

      pcapsnoop
        The syntax for the type-specific arguments for this type is:
        <io-type>:<io-type-specific-arguments>

        Supported I/O types are:

        file
          The type-specific arguments for this I/O type is a string that is converted
          to a file path using the python `str.format()` string formatting. The log
          records will be written to that file if it can be opened/created.
          The keyword args that may be referenced by the string pattern are:
            now: the value of `datetime.now()`
            utcnow: the value of `datetime.now(tz=datetime.timezone.utc)`
            pid: the current process ID.
            instance: the instance ID in the current process.

        pipe
          The type-specific arguments for this I/O type is a string that is converted
          to a path using the python `str.format()` string formatting. The log
          records will be written to the named pipe referenced by this path
          if it can be opened. The keyword args that may be referenced by the
          string pattern are:
            now: the value of `datetime.now()`
            utcnow: the value of `datetime.now(tz=datetime.timezone.utc)`
            pid: the current process ID.
            instance: the instance ID in the current process.

    Examples:
      btsnoop:file:my_btsnoop.log
      btsnoop:file:/tmp/bumble_{now:%Y-%m-%d-%H:%M:%S}_{pid}.log
      pcapsnoop:pipe:/tmp/bumble-extcap


    """
    if ':' not in spec:
        raise core.InvalidArgumentError('snooper type prefix missing')

    snooper_type, snooper_args = spec.split(':', maxsplit=1)

    global _SNOOPER_INSTANCE_COUNT

    if snooper_type == 'btsnoop':
        if ':' not in snooper_args:
            raise core.InvalidArgumentError('I/O type for btsnoop snooper type missing')

        io_type, io_name = snooper_args.split(':', maxsplit=1)
        if io_type == 'file':
            # Process the file name string pattern.
            file_path = io_name.format(
                now=datetime.datetime.now(),
                utcnow=datetime.datetime.now(tz=datetime.timezone.utc),
                pid=os.getpid(),
                instance=_SNOOPER_INSTANCE_COUNT,
            )

            # Open the file
            logger.debug(f'Snoop file: {file_path}')
            with open(file_path, 'wb') as snoop_file:
                _SNOOPER_INSTANCE_COUNT += 1
                yield BtSnooper(snoop_file)
                _SNOOPER_INSTANCE_COUNT -= 1
                return

    elif snooper_type == 'pcapsnoop':
        if ':' not in snooper_args:
            raise core.InvalidArgumentError(
                'I/O type for pcapsnoop snooper type missing'
            )

        io_type, io_name = snooper_args.split(':', maxsplit=1)
        if io_type in {'pipe', 'file'}:
            # Process the file name string pattern.
            file_path = io_name.format(
                now=datetime.datetime.now(),
                utcnow=datetime.datetime.now(tz=datetime.timezone.utc),
                pid=os.getpid(),
                instance=_SNOOPER_INSTANCE_COUNT,
            )

            # Open a file or pipe
            logger.debug(f'PCAP file: {file_path}')

            # Pipes we have to open with unbuffered binary I/O
            # so we pass ``buffering`` for pipes but not for files
            pcap_file: BinaryIO
            if io_type == 'pipe':
                pcap_file = open(file_path, 'wb', buffering=0)
            else:
                pcap_file = open(file_path, 'wb')

            with pcap_file:
                _SNOOPER_INSTANCE_COUNT += 1
                yield PcapSnooper(pcap_file)
                _SNOOPER_INSTANCE_COUNT -= 1
                return

        raise core.InvalidArgumentError(f'I/O type {io_type} not supported')

    raise core.InvalidArgumentError(f'snooper type {snooper_type} not found')