├── usbrply ├── __init__.py ├── serial │ ├── __init__.py │ ├── serial.py │ ├── printers.py │ ├── mpsse.py │ └── parsers.py ├── printers.py ├── parsers.py ├── filters.py ├── printer.py ├── setup_filter.py ├── commenter.py ├── usb.py ├── util.py ├── com_pcap.py ├── main.py ├── vidpid_filter.py ├── pcap_util.py ├── pyprinter.py ├── cprinter.py ├── lin_pcap.py └── win_pcap.py ├── .gitignore ├── requirements.txt ├── docs └── usbPcap.png ├── .gitmodules ├── format.sh ├── COPYING.txt ├── setup.py ├── pyscraper_example.py ├── json_ex.py ├── usbserial.py ├── README.md └── test └── test_misc.py /usbrply/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /usbrply/serial/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /usbrply/serial/serial.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | usbrply.egg-info/ 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-pcapng 2 | 3 | -------------------------------------------------------------------------------- /docs/usbPcap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnDMcMaster/usbrply/HEAD/docs/usbPcap.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "usbrply-test"] 2 | path = test/data 3 | url = https://github.com/JohnDMcMaster/usbrply-test.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /format.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # find . -name '*.py' -and -not -path './.git/*' -and -not -path './archive/*' | xargs -0 -P $(nproc) yapf -p -i 3 | find . -name '*.py' -and -not -path './.git/*' -and -not -path './archive/*' -exec echo {} \; -exec yapf -p -i {} \; 4 | 5 | -------------------------------------------------------------------------------- /usbrply/printers.py: -------------------------------------------------------------------------------- 1 | from .printer import JSONPrinter 2 | from .pyprinter import LibusbPyPrinter 3 | from .cprinter import LibusbCPrinter 4 | 5 | 6 | def run(ofmt, j, argsj={}): 7 | if not ofmt: 8 | ofmt = "libusb-py" 9 | cls = { 10 | "json": JSONPrinter, 11 | "libusb-c": LibusbCPrinter, 12 | "libusb-py": LibusbPyPrinter, 13 | # "linux": LinuxPrinter, 14 | }[ofmt] 15 | 16 | printer = cls(argsj) 17 | printer.run(j) 18 | -------------------------------------------------------------------------------- /COPYING.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019 John McMaster 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | 5 | def read(fname): 6 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 7 | 8 | 9 | setup(name="usbrply", 10 | version="2.1.1", 11 | author="John McMaster", 12 | author_email='JohnDMcMaster@gmail.com', 13 | description=("Replay captured USB packets from .pcap file."), 14 | license="BSD", 15 | keywords="libusb pcap", 16 | url='https://github.com/JohnDMcMaster/usbrply', 17 | packages=find_packages(), 18 | install_requires=[ 19 | "python-pcapng", 20 | ], 21 | long_description=read('README.md'), 22 | long_description_content_type='text/markdown', 23 | classifiers=[ 24 | "License :: OSI Approved :: BSD License", 25 | ], 26 | platforms='any', 27 | entry_points={'console_scripts': ['usbrply=usbrply.main:main']}) 28 | -------------------------------------------------------------------------------- /usbrply/parsers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Aggregates parser engines together 3 | """ 4 | 5 | from . import lin_pcap 6 | from . import win_pcap 7 | from . import pcap_util 8 | 9 | 10 | def pcap2json_prepare(fn, argsj={}): 11 | """ 12 | argsj: argument dict 13 | """ 14 | 15 | # TODO: add Windows engine back in 16 | 17 | parser = argsj.get("parser", "auto") 18 | if parser == "auto": 19 | parser = pcap_util.guess_parser(fn) 20 | # print("Guess parser: %s" % parser) 21 | argsj["parser"] = parser 22 | 23 | cls = { 24 | "lin-pcap": lin_pcap.Gen, 25 | "lin-pcapng": lin_pcap.Gen, 26 | "win-pcap": win_pcap.Gen, 27 | "win-pcapng": win_pcap.Gen, 28 | }[parser] 29 | gen = cls(fn, argsj) 30 | return gen 31 | 32 | def pcap2json(fn, argsj={}): 33 | gen = pcap2json_prepare(fn, argsj) 34 | # k,v generator 35 | return gen.run() 36 | 37 | 38 | def jgen2j(jgen): 39 | """ 40 | Convert generator into static JSON 41 | Converts generates to lists 42 | byetarray must already be converted to hex 43 | """ 44 | 45 | j = {} 46 | for k, v in jgen: 47 | # Convert nested generator to list 48 | if k == "data": 49 | v = list(v) 50 | j[k] = v 51 | return j 52 | -------------------------------------------------------------------------------- /usbrply/filters.py: -------------------------------------------------------------------------------- 1 | from .commenter import Commenter 2 | from .setup_filter import SetupFilter 3 | from .vidpid_filter import VidpidFilter 4 | # from .device_filter import DeviceFilter 5 | from .parsers import jgen2j 6 | 7 | filters = { 8 | "commenter": Commenter, 9 | "setup": SetupFilter, 10 | "vidpid": VidpidFilter, 11 | # "device": DeviceFilter, 12 | } 13 | 14 | 15 | def run(filts, jgen, argsj={}, verbose=False): 16 | """ 17 | Run series of given filters on input jgen 18 | First filter in array is applied first 19 | """ 20 | if len(filts) == 0: 21 | return jgen 22 | 23 | filt0 = filts[0] 24 | cls = filters[filt0] 25 | obj = cls(argsj, verbose=verbose) 26 | return run(filts[1:], 27 | obj.run(jgen), 28 | argsj=argsj, 29 | verbose=verbose) 30 | 31 | # include object references 32 | def runx(filts, jgen, argsj={}, verbose=False): 33 | if len(filts) == 0: 34 | return {}, jgen 35 | 36 | filt0 = filts[0] 37 | cls = filters[filt0] 38 | obj = cls(argsj, verbose=verbose) 39 | objr, retr = runx(filts[1:], 40 | obj.run(jgen), 41 | argsj=argsj, 42 | verbose=verbose) 43 | objr[filt0] = obj 44 | return objr, retr 45 | 46 | 47 | # FIXME: loaded data is not generated 48 | # This causes issues with filters that expect generated, not JSON data 49 | # ex: "for (k, v) in gen" vs "for (k, v) in gen.items()" conflicts 50 | def run_filter(j, cls, argsj={}, verbose=False): 51 | c = cls(argsj=argsj, verbose=verbose) 52 | ret = c.run(j) 53 | return jgen2j(ret) 54 | -------------------------------------------------------------------------------- /usbrply/printer.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import json 3 | import sys 4 | from . import parsers 5 | 6 | indent = "" 7 | 8 | print_file = sys.stdout 9 | 10 | 11 | def default_print_file(fname, cap_file): 12 | global print_file 13 | """ 14 | Given argument for explicit file name and a cap file, 15 | default to the file name, otherwise munge cap file into .py version 16 | """ 17 | if not fname: 18 | fname = cap_file.replace('.pcapng', 19 | '.py').replace('.pcap', 20 | '.py').replace('.cap', '.py') 21 | 22 | assert fname != cap_file, (fname, cap_file) 23 | print_file = open(fname, "w") 24 | 25 | 26 | def indent_reset(): 27 | global indent 28 | 29 | indent = "" 30 | 31 | 32 | def indent_inc(): 33 | global indent 34 | 35 | indent += " " 36 | 37 | 38 | def indent_dec(): 39 | global indent 40 | 41 | indent = indent[4:] 42 | 43 | 44 | def indented(s): 45 | print("%s%s" % (indent, s), file=print_file) 46 | 47 | 48 | def get_indent(): 49 | return indent 50 | 51 | 52 | class Printer(object): 53 | def __init__(self, argsj): 54 | self.argsj = argsj 55 | 56 | def run(self, j): 57 | raise Exception("Required") 58 | 59 | 60 | class JSONPrinter(Printer): 61 | def __init__(self, argsj): 62 | Printer.__init__(self, argsj) 63 | 64 | def run(self, jgen): 65 | j = parsers.jgen2j(jgen) 66 | 67 | json.dump(j, 68 | print_file, 69 | sort_keys=True, 70 | indent=4, 71 | separators=(',', ': ')) 72 | -------------------------------------------------------------------------------- /usbrply/setup_filter.py: -------------------------------------------------------------------------------- 1 | from .usb import req2s 2 | 3 | setup_reqs = [ 4 | # reply type 5 | "GET_STATUS", 6 | "CLEAR_FEATURE", 7 | "SET_FEATURE", 8 | "SET_ADDRESS", 9 | "GET_DESCRIPTOR", 10 | "SET_DESCRIPTOR", 11 | "GET_CONFIGURATION", 12 | "SET_CONFIGURATION", 13 | "SET_INTERFACE", 14 | "GET_INTERFACE", 15 | "CLEAR_FEATURE", 16 | "SYNCH_FRAME", 17 | ] 18 | 19 | 20 | class SetupFilter(object): 21 | def __init__(self, argsj, verbose=False): 22 | # self.setup = argsj.get("setup", False) 23 | self.verbose = verbose 24 | self.entries = 0 25 | self.drops = 0 26 | 27 | def should_filter(self, data): 28 | return req2s(data["bRequestType"], data["bRequest"]) in setup_reqs 29 | 30 | def gen_data(self, datas): 31 | for data in datas: 32 | self.entries += 1 33 | if data["type"] in ("controlWrite", 34 | "controlRead") and self.should_filter(data): 35 | if self.verbose: 36 | print("SetupFilter drop %s (%s %s %s)" % 37 | (data['type'], 38 | req2s(data["bRequestType"], data["bRequest"]), 39 | data["bRequestType"], data["bRequest"])) 40 | self.drops += 1 41 | continue 42 | yield data 43 | yield { 44 | "type": 45 | "comment", 46 | "v": 47 | "SetupFilter: dropped %s / %s entries" % (self.drops, self.entries) 48 | } 49 | 50 | def run(self, jgen): 51 | for k, v in jgen: 52 | if k == "data": 53 | yield k, self.gen_data(v) 54 | else: 55 | yield k, v 56 | -------------------------------------------------------------------------------- /pyscraper_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Example showing how to selectively translate parts of a USB packet capture into higher level functionality 4 | When something isn't recognized it prints using default usbrply decoding 5 | """ 6 | 7 | import json 8 | import subprocess 9 | 10 | from usbrply.pyprinter import LibusbPyPrinter 11 | from usbrply.printer import indented 12 | 13 | 14 | class Scraper: 15 | def __init__(self, verbose=False): 16 | self.pyprint = LibusbPyPrinter(argsj={"wrapper": True}, 17 | verbose=verbose) 18 | 19 | def parse_data(self, d): 20 | # Translate to higher level function 21 | if d["type"] == "controlRead" and d["bRequest"] == 0x02: 22 | indented("vendor_request1()") 23 | # Ignore bulk reads 24 | elif d["type"] == "bulkRead": 25 | pass 26 | # Default: print as normal python replay 27 | else: 28 | self.pyprint.parse_data(d) 29 | 30 | def run(self, j): 31 | self.pyprint.header() 32 | 33 | for d in j["data"]: 34 | self.parse_data(d) 35 | 36 | self.pyprint.footer() 37 | 38 | 39 | def load_json(fin, usbrply=""): 40 | if fin.find('.cap') >= 0 or fin.find('.pcapng') >= 0: 41 | json_fn = '/tmp/scrape.json' 42 | cmd = 'usbrply %s --json %s >%s' % (usbrply, fin, json_fn) 43 | subprocess.check_call(cmd, shell=True) 44 | else: 45 | json_fn = fin 46 | 47 | j = json.load(open(json_fn)) 48 | return j, json_fn 49 | 50 | 51 | if __name__ == "__main__": 52 | import argparse 53 | 54 | parser = argparse.ArgumentParser(description='') 55 | parser.add_argument('--usbrply', default='') 56 | parser.add_argument('fin') 57 | args = parser.parse_args() 58 | 59 | j, json_fn = load_json( 60 | args.fin, 61 | args.usbrply, 62 | ) 63 | 64 | scraper = Scraper() 65 | scraper.run(j) 66 | -------------------------------------------------------------------------------- /usbrply/commenter.py: -------------------------------------------------------------------------------- 1 | from .usb import req2s, feat_i2s 2 | import binascii 3 | 4 | 5 | class Commenter(object): 6 | def __init__(self, argsj, verbose=False): 7 | self.fx2 = argsj.get("fx2", False) 8 | self.verbose = verbose 9 | 10 | # FX2 regs: http://www.keil.com/dd/docs/datashts/cypress/fx2_trm.pdf 11 | # FX2LP regs: http://www.cypress.com/file/126446/download 12 | def try_comment(self, data): 13 | # Table 9-3. Standard Device Requests 14 | vendor = None 15 | if self.fx2: 16 | vendor = { 17 | 0xA0: "FX2_REG_W", 18 | } 19 | reqs = req2s(data["bRequestType"], data["bRequest"], vendor) 20 | if not reqs: 21 | return 22 | 23 | ret = '%s (0x%02X)' % (reqs, data["bRequest"]) 24 | if reqs == 'SET_ADDRESS': 25 | ret += ': 0x%02x/%d' % (data["wValue"], data["wValue"]) 26 | elif reqs == 'SET_FEATURE' or reqs == 'CLEAR_FEATURE': 27 | ret += ': 0x%02X (%s)' % (data["wValue"], 28 | feat_i2s.get(data["wValue"], 'unknown')) 29 | elif reqs == 'FX2_REG_W': 30 | addr = data["wValue"] 31 | reg2s = { 32 | 0xE600: 'CPUCS', 33 | } 34 | reg = reg2s.get(addr, None) 35 | 36 | # 5.4 FX2 Memory Maps 37 | # Appendix C 38 | # FX2 Register Summary 39 | ret += ': addr=0x%04X' % (data["wValue"]) 40 | if reg: 41 | ret += ' (%s)' % (reg, ) 42 | elif addr < 0x1000: 43 | ret += ' (FW load)' 44 | # FX2: 8K of on-chip RAM (the "Main RAM") at addresses 0x0000-0x1FFF 45 | # FX2LP: 16K 46 | elif 0x0000 <= addr <= 0x3FFF: 47 | ret += ' (main RAM addr=0x%04X)' % addr 48 | # 512 bytes of on-chip RAM (the "Scratch RAM") at addresses 0xE000-0xE1FFF 49 | elif 0xE000 <= addr <= 0xE1FF: 50 | ret += ' (scratch RAM)' 51 | # The CPU communicates with the SIE using a set of registers occupying on-chip RAM addresses 0xE600-0xE6FF" 52 | elif 0xE600 <= addr <= 0xE6FF: 53 | ret += ' (unknown reg)' 54 | # per memory map: 7.5KB of USB regs and 4K EP buffers 55 | elif 0xE200 <= addr <= 0xFFFF: 56 | ret += ' (unknown misc)' 57 | else: 58 | ret += ' (unknown)' 59 | 60 | bdat = binascii.unhexlify(data["data"]) 61 | if len(bdat) == 1: 62 | dat = ord(bdat) 63 | if reg == 'CPUCS': 64 | if dat & 1 == 1: 65 | ret += ', reset: hold' 66 | else: 67 | ret += ', reset: release' 68 | l = data.get("comments", []) 69 | l.append(ret) 70 | data["comments"] = l 71 | 72 | def gen_data(self, datas): 73 | for data in datas: 74 | if data["type"] in ("controlWrite", "controlRead"): 75 | self.try_comment(data) 76 | yield data 77 | 78 | def run(self, jgen): 79 | for k, v in jgen: 80 | if k == "data": 81 | yield k, self.gen_data(v) 82 | else: 83 | yield k, v 84 | -------------------------------------------------------------------------------- /json_ex.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | import ast 4 | import json 5 | import binascii 6 | 7 | prefix = ' ' * 8 8 | 9 | 10 | def str2hex(buff, prefix='', terse=True): 11 | if len(buff) == 0: 12 | return '""' 13 | buff = bytearray(buff) 14 | ret = '' 15 | if terse and len(buff) > 16: 16 | ret += '\n' 17 | for i in xrange(len(buff)): 18 | if i % 16 == 0: 19 | if i != 0: 20 | ret += '" \\\n' 21 | if len(buff) <= 16: 22 | ret += '"' 23 | if not terse or len(buff) > 16: 24 | ret += '%s"' % prefix 25 | 26 | ret += "\\x%02X" % (buff[i], ) 27 | return ret + '"' 28 | 29 | 30 | def fmt_terse(data): 31 | ret = str2hex(data, prefix=prefix) 32 | if len(data) > 16: 33 | ret += '\n%s' % prefix 34 | return ret 35 | 36 | 37 | def dump(fin): 38 | j = json.load(open(fin)) 39 | pi = 0 40 | ps = j['data'] 41 | while pi < len(ps): 42 | p = ps[pi] 43 | if p['type'] == 'comment': 44 | #print '# %s' % p['v'] 45 | pass 46 | elif p['type'] == 'controlRead': 47 | ''' 48 | # Generated from packet 6/7 49 | # None (0xB0) 50 | buff = controlRead(0xC0, 0xB0, 0x0000, 0x0000, 4096) 51 | # NOTE:: req max 4096 but got 3 52 | validate_read("\x00\x00\x00", buff, "packet 6/7") 53 | ''' 54 | print('buff = controlRead(0x%02X, 0x%02X, 0x%04X, 0x%04X, %d)' % 55 | (p['req'], p['reqt'], p['val'], p['ind'], p['len'])) 56 | data = binascii.unhexlify(p['data']) 57 | print('# Req: %d, got: %d' % (p['len'], len(data))) 58 | print('validate_read(%s, buff, "packet %d/%d")' % 59 | (fmt_terse(data), p['packn'][0], p['packn'][1])) 60 | elif p['type'] == 'controlWrite': 61 | ''' 62 | controlWrite(0x40, 0xB2, 0x0000, 0x0000, "") 63 | ''' 64 | data = binascii.unhexlify(p['data']) 65 | print('buff = controlWrite(0x%02X, 0x%02X, 0x%04X, 0x%04X, %s)' % 66 | (p['req'], p['reqt'], p['val'], p['ind'], 67 | str2hex(data, prefix=prefix))) 68 | elif p['type'] == 'bulkRead': 69 | ''' 70 | buff = bulkRead(0x86, 0x0200) 71 | # NOTE:: req max 512 but got 4 72 | validate_read("\x08\x16\x01\x00", buff, "packet 8/9") 73 | ''' 74 | print('buff = bulkRead(0x%02X, 0x%04X)' % (p['endp'], p['len'])) 75 | data = binascii.unhexlify(p['data']) 76 | print('# Req: %d, got: %d' % (p['len'], len(data))) 77 | print('validate_read(%s, buff, "packet %d/%d")' % 78 | (fmt_terse(data), p['packn'][0], p['packn'][1])) 79 | elif p['type'] == 'bulkWrite': 80 | ''' 81 | bulkWrite(0x02, "\x01") 82 | ''' 83 | data = binascii.unhexlify(p['data']) 84 | print('bulkWrite(0x%02X, %s)' % 85 | (p['endp'], str2hex(data, prefix=' ' * 8))) 86 | else: 87 | raise Exception("Unknown type: %s" % p['type']) 88 | pi += 1 89 | 90 | 91 | if __name__ == "__main__": 92 | import argparse 93 | 94 | parser = argparse.ArgumentParser(description='') 95 | parser.add_argument('fin') 96 | args = parser.parse_args() 97 | dump(args.fin) 98 | -------------------------------------------------------------------------------- /usbrply/usb.py: -------------------------------------------------------------------------------- 1 | ''' 2 | possible transfer mode 3 | ''' 4 | URB_TRANSFER_IN = 0x80 5 | URB_ISOCHRONOUS = 0x0 6 | URB_INTERRUPT = 0x1 7 | URB_CONTROL = 0x2 8 | URB_BULK = 0x3 9 | # Wireshark GUI's name 10 | USB_IRP_INFO = 0xFE 11 | ''' 12 | possible event type 13 | ''' 14 | URB_SUBMIT = ord('S') 15 | URB_COMPLETE = ord('C') 16 | URB_ERROR = ord('E') 17 | 18 | urb_type2str = { 19 | URB_SUBMIT: 'URB_SUBMIT', 20 | URB_COMPLETE: 'URB_COMPLETE', 21 | URB_ERROR: 'URB_ERROR', 22 | } 23 | 24 | USB_REQ_GET_STATUS = 0x00 25 | USB_REQ_CLEAR_FEATURE = 0x01 26 | # 0x02 is reserved 27 | USB_REQ_SET_FEATURE = 0x03 28 | # 0x04 is reserved 29 | USB_REQ_SET_ADDRESS = 0x05 30 | USB_REQ_GET_DESCRIPTOR = 0x06 31 | USB_REQ_SET_DESCRIPTOR = 0x07 32 | USB_REQ_GET_CONFIGURATION = 0x08 33 | USB_REQ_SET_CONFIGURATION = 0x09 34 | USB_REQ_GET_INTERFACE = 0x0A 35 | USB_REQ_SET_INTERFACE = 0x0B 36 | USB_REQ_SYNCH_FRAME = 0x0C 37 | 38 | USB_DIR_OUT = 0 # to device 39 | USB_DIR_IN = 0x80 # to host 40 | 41 | USB_TYPE_MASK = (0x03 << 5) # 0x60 42 | USB_TYPE_STANDARD = (0x00 << 5) # 0x00 43 | USB_TYPE_CLASS = (0x01 << 5) # 0x20 44 | USB_TYPE_VENDOR = (0x02 << 5) # 0x40 45 | USB_TYPE_RESERVED = (0x03 << 5) # 0x60 46 | 47 | USB_RECIP_MASK = 0x1f 48 | USB_RECIP_DEVICE = 0x00 49 | USB_RECIP_INTERFACE = 0x01 50 | USB_RECIP_ENDPOINT = 0x02 51 | USB_RECIP_OTHER = 0x03 52 | # From Wireless USB 1.0 53 | USB_RECIP_PORT = 0x04 54 | USB_RECIP_RPIPE = 0x05 55 | 56 | # Table 9-6. Standard Feature Selectors 57 | feat_i2s = { 58 | # Endpoint 59 | 0: 'ENDPOINT_HALT', 60 | # Device 61 | 1: 'DEVICE_REMOTE_WAKEUP', 62 | # Device 63 | 2: 'TEST_MODE', 64 | } 65 | 66 | 67 | def req2s(bRequestType, bRequest, vendor=None): 68 | m = { 69 | USB_TYPE_STANDARD: { 70 | USB_REQ_GET_STATUS: "GET_STATUS", 71 | USB_REQ_CLEAR_FEATURE: "CLEAR_FEATURE", 72 | USB_REQ_SET_FEATURE: "SET_FEATURE", 73 | USB_REQ_SET_ADDRESS: "SET_ADDRESS", 74 | USB_REQ_GET_DESCRIPTOR: "GET_DESCRIPTOR", 75 | USB_REQ_SET_DESCRIPTOR: "SET_DESCRIPTOR", 76 | USB_REQ_GET_CONFIGURATION: "GET_CONFIGURATION", 77 | USB_REQ_SET_CONFIGURATION: "SET_CONFIGURATION", 78 | USB_REQ_SET_INTERFACE: "SET_INTERFACE", 79 | }, 80 | USB_TYPE_CLASS: { 81 | USB_REQ_GET_STATUS: "GET_STATUS", 82 | USB_REQ_CLEAR_FEATURE: "CLEAR_FEATURE", 83 | USB_REQ_SET_FEATURE: "SET_FEATURE", 84 | USB_REQ_GET_INTERFACE: "GET_INTERFACE", 85 | }, 86 | # 2020-12-29: these are interfering with a device 87 | # Leave these out for now, try to figure out why they were added 88 | # and more concretely document 89 | # USB_TYPE_VENDOR: { 90 | # USB_REQ_GET_STATUS: "GET_STATUS", 91 | # USB_REQ_SET_FEATURE: "SET_FEATURE", 92 | # USB_REQ_CLEAR_FEATURE: "CLEAR_FEATURE", 93 | # USB_REQ_SYNCH_FRAME: "SYNCH_FRAME", 94 | #}, 95 | } 96 | if vendor: 97 | m[USB_TYPE_VENDOR].update(vendor) 98 | 99 | reqType = bRequestType & USB_TYPE_MASK 100 | n = m.get(reqType, None) 101 | if n is None or not bRequest in n: 102 | return None 103 | reqs = n[bRequest] 104 | return reqs 105 | 106 | 107 | def request_type2str(bRequestType): 108 | ret = "" 109 | 110 | if (bRequestType & USB_DIR_IN) == USB_DIR_IN: 111 | ret += "USB_DIR_IN" 112 | else: 113 | ret += "USB_DIR_OUT" 114 | 115 | m = { 116 | USB_TYPE_STANDARD: " | USB_TYPE_STANDARD", 117 | USB_TYPE_CLASS: " | USB_TYPE_CLASS", 118 | USB_TYPE_VENDOR: " | USB_TYPE_VENDOR", 119 | USB_TYPE_RESERVED: " | USB_TYPE_RESERVED", 120 | } 121 | ret += m[bRequestType & USB_TYPE_MASK] 122 | 123 | m = { 124 | USB_RECIP_DEVICE: " | USB_RECIP_DEVICE", 125 | USB_RECIP_INTERFACE: " | USB_RECIP_INTERFACE", 126 | USB_RECIP_ENDPOINT: " | USB_RECIP_ENDPOINT", 127 | USB_RECIP_OTHER: " | USB_RECIP_OTHER", 128 | USB_RECIP_PORT: " | USB_RECIP_PORT", 129 | USB_RECIP_RPIPE: " | USB_RECIP_RPIPE", 130 | } 131 | ret += m[bRequestType & USB_RECIP_MASK] 132 | 133 | return ret 134 | 135 | 136 | transfer2str = { 137 | URB_ISOCHRONOUS: "URB_ISOCHRONOUS", 138 | URB_INTERRUPT: "URB_INTERRUPT", 139 | URB_CONTROL: "URB_CONTROL", 140 | URB_BULK: "URB_BULK", 141 | } 142 | 143 | 144 | def transfer2str_safe(t): 145 | return transfer2str.get(t, "UNKNOWN_%02x" % t) 146 | -------------------------------------------------------------------------------- /usbrply/serial/printers.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from usbrply.printer import Printer, indented, indent_inc, indent_dec 3 | from usbrply.pyprinter import bytes2AnonArray 4 | import json 5 | from usbrply import util 6 | 7 | 8 | class TextFT2232CPrinter(object): 9 | def __init__(self, argsj=None): 10 | self.verbose = argsj.get("verbose", False) 11 | self.ascii = argsj.get("ascii", False) 12 | 13 | def next_json(self, j, prefix=None): 14 | if j['type'] == 'read': 15 | interface = j['interface'] 16 | data = j['data'] 17 | n = len(data) / 2 18 | prefix = j['prefix'] 19 | if self.ascii: 20 | indented("%u r %s: %s" % 21 | (interface, n, binascii.unhexlify(data))) 22 | else: 23 | indented("%u r %s: 0x%s" % (interface, n, data)) 24 | indented(" %s" % (binascii.hexlify(prefix, ))) 25 | elif j['type'] == 'write': 26 | interface = j['interface'] 27 | data = j['data'] 28 | n = len(data) / 2 29 | if self.ascii: 30 | indented("%u w %s: %s" % 31 | (interface, n, binascii.unhexlify(data))) 32 | else: 33 | indented("%u w %s: 0x%s" % (interface, n, data)) 34 | elif j['type'] == 'SET_DATA': 35 | indented( 36 | "i%s SET_DATA: parity %s, stop bits %s, break %s" % 37 | (j['interface'], j['parity'], j['stopbits'], j['breakon'])) 38 | else: 39 | indented("i%s %s: %s" % (j['interface'], j['type'], j)) 40 | 41 | def comment(self, s): 42 | indented('%s' % (s, )) 43 | 44 | def header(self): 45 | self.comment("Generated by usbrply-serial FT2232C") 46 | 47 | def footer(self): 48 | pass 49 | 50 | def run(self, j): 51 | if self.verbose: 52 | print("") 53 | print("") 54 | print("") 55 | 56 | self.header() 57 | 58 | for d in j["data"]: 59 | self.next_json(d) 60 | 61 | self.footer() 62 | 63 | 64 | class JSONSPrinter(object): 65 | def __init__(self, argsj=None): 66 | pass 67 | 68 | def run(self, j): 69 | j = util.hex_jdata(j) 70 | if not util.validate_json(j): 71 | raise ValueError("Require simple JSON") 72 | print(json.dumps(j, sort_keys=True, indent=4, separators=(',', ': '))) 73 | 74 | 75 | class PythonFT2232CPrinter(object): 76 | def __init__(self, argsj=None): 77 | self.verbose = argsj.get("verbose", False) 78 | self.ascii = argsj.get("ascii", False) 79 | self.wrapper = argsj.get("wrapper", False) 80 | 81 | def next_json(self, j, prefix=None): 82 | if j['type'] == 'read': 83 | interface = j['interface'] 84 | data = binascii.unhexlify(j["data"]) 85 | indented("buff = ser%u.read(%u)" % (interface, len(data))) 86 | indented("validate_read(%s, buff, \"%s\")" % 87 | (bytes2AnonArray(data), "packet")) 88 | elif j['type'] == 'write': 89 | data_str = bytes2AnonArray(binascii.unhexlify(j["data"])) 90 | interface = j['interface'] 91 | indented("ser%u.write(%s)" % (interface, data_str)) 92 | else: 93 | # assert 0 94 | print('# next_json: %s' % (j['type'], )) 95 | 96 | def comment(self, s): 97 | indented('# %s' % (s, )) 98 | 99 | def header(self): 100 | if not self.wrapper: 101 | return 102 | # self.comment("Generated by usbrply-serial FT2232C") 103 | print(''' 104 | #!/usr/bin/env python3 105 | import binascii 106 | import time 107 | import serial 108 | 109 | def validate_read(expected, actual, msg): 110 | if expected != actual: 111 | print('Failed %s' % msg) 112 | print(' Expected; %s' % binascii.hexlify(expected,)) 113 | print(' Actual: %s' % binascii.hexlify(actual,)) 114 | #raise Exception('failed validate: %s' % msg) 115 | 116 | ''') 117 | print('def replay(ser0):') 118 | indent_inc() 119 | 120 | def footer(self): 121 | indent_dec() 122 | if not self.wrapper: 123 | return 124 | print(''' 125 | if __name__ == "__main__": 126 | import argparse 127 | 128 | parser = argparse.ArgumentParser(description='Replay USB-serial commands') 129 | parser.add_argument('-p', 130 | "--port", 131 | default="/dev/ttyUSB0", 132 | help="") 133 | args = parser.parse_args() 134 | 135 | serial0 = serial.Serial( 136 | args.port, 137 | baudrate=115200, 138 | bytesize=serial.EIGHTBITS, 139 | parity=serial.PARITY_NONE, 140 | stopbits=serial.STOPBITS_ONE, 141 | rtscts=False, 142 | dsrdtr=False, 143 | xonxoff=False, 144 | timeout=0, 145 | writeTimeout=None) 146 | replay(serial0) 147 | 148 | ''') 149 | 150 | def run(self, j): 151 | self.header() 152 | 153 | for d in j["data"]: 154 | self.next_json(d) 155 | 156 | self.footer() 157 | -------------------------------------------------------------------------------- /usbrply/util.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | import json 4 | from collections import OrderedDict 5 | import binascii 6 | 7 | 8 | def clear_screen(): 9 | # 00000000 1b 5b 33 3b 4a 1b 5b 48 1b 5b 32 4a |.[3;J.[H.[2J| 10 | sys.stdout.write("\x1b\x5b\x33\x3b\x4a\x1b\x5b\x48\x1b\x5b\x32\x4a") 11 | sys.stdout.flush() 12 | 13 | 14 | def hexdump(data, label=None, indent='', address_width=8, f=sys.stdout): 15 | def isprint(c): 16 | return c >= ' ' and c <= '~' 17 | 18 | if label: 19 | print(label) 20 | 21 | bytes_per_half_row = 8 22 | bytes_per_row = 16 23 | data = bytearray(data) 24 | data_len = len(data) 25 | 26 | def hexdump_half_row(start): 27 | left = max(data_len - start, 0) 28 | 29 | real_data = min(bytes_per_half_row, left) 30 | 31 | f.write(''.join('%02X ' % c for c in data[start:start + real_data])) 32 | f.write(''.join(' ' * (bytes_per_half_row - real_data))) 33 | f.write(' ') 34 | 35 | return start + bytes_per_half_row 36 | 37 | pos = 0 38 | while pos < data_len: 39 | row_start = pos 40 | f.write(indent) 41 | if address_width: 42 | f.write(('%%0%dX ' % address_width) % pos) 43 | pos = hexdump_half_row(pos) 44 | pos = hexdump_half_row(pos) 45 | f.write("|") 46 | # Char view 47 | left = data_len - row_start 48 | real_data = min(bytes_per_row, left) 49 | 50 | f.write(''.join([ 51 | c if isprint(c) else '.' 52 | for c in tostr(data[row_start:row_start + real_data]) 53 | ])) 54 | f.write((" " * (bytes_per_row - real_data)) + "|\n") 55 | 56 | 57 | def add_bool_arg(parser, yes_arg, default=False, **kwargs): 58 | dashed = yes_arg.replace('--', '') 59 | dest = dashed.replace('-', '_') 60 | parser.add_argument(yes_arg, 61 | dest=dest, 62 | action='store_true', 63 | default=default, 64 | **kwargs) 65 | kwargs['help'] = 'Disable above' 66 | parser.add_argument('--no-' + dashed, 67 | dest=dest, 68 | action='store_false', 69 | **kwargs) 70 | 71 | 72 | # In Python2 bytes_data is a string, in Python3 it's bytes. 73 | # The element type is different (string vs int) and we have to deal 74 | # with that when printing this number as hex. 75 | if sys.version_info[0] == 2: 76 | myord = ord 77 | else: 78 | myord = lambda x: x 79 | 80 | 81 | def tobytes(buff): 82 | if type(buff) is str: 83 | #return bytearray(buff, 'ascii') 84 | return bytearray([myord(c) for c in buff]) 85 | elif type(buff) is bytearray or type(buff) is bytes: 86 | return buff 87 | else: 88 | assert 0, type(buff) 89 | 90 | 91 | def tostr(buff): 92 | if type(buff) is str: 93 | return buff 94 | elif type(buff) is bytearray or type(buff) is bytes: 95 | return ''.join([chr(b) for b in buff]) 96 | else: 97 | assert 0, type(buff) 98 | 99 | 100 | def isprint(c): 101 | return c >= ' ' and c <= '~' 102 | 103 | 104 | def to_pintable_str(buff): 105 | buff = tostr(buff) 106 | return "".join([c for c in buff if isprint(c)]) 107 | 108 | 109 | # Used by scraper scripts 110 | # Due to python2 vs python3 issue, its better to subprocess this 111 | def load_pcap_json(fin, usbrply_args=""): 112 | if fin.find('.cap') >= 0 or fin.find('.pcapng') >= 0: 113 | json_fn = '/tmp/scrape.json' 114 | cmd = 'usbrply %s --json %s >%s' % (usbrply_args, fin, json_fn) 115 | subprocess.check_call(cmd, shell=True) 116 | else: 117 | json_fn = fin 118 | 119 | j = json.load(open(json_fn)) 120 | return j, json_fn 121 | 122 | 123 | """ 124 | Common issues: 125 | -Bytes 126 | -Bytearray 127 | """ 128 | 129 | 130 | def validate_json(j, prefix="top"): 131 | ret = True 132 | if type(j) in (str, int, float): 133 | return True 134 | elif j is None: 135 | return True 136 | elif type(j) in (OrderedDict, dict): 137 | for k, v in j.items(): 138 | ok = validate_json(k, prefix=prefix + " key") 139 | ret = ok and ret 140 | if ok: 141 | ret = validate_json(v, prefix=prefix + "[m %s]" % k) and ret 142 | elif type(j) in (tuple, list): 143 | for vi, v in enumerate(j): 144 | ret = validate_json(v, prefix=prefix + "[l %u]" % vi) and ret 145 | return True 146 | else: 147 | print("json @ %s: unexpected type %s" % (prefix, type(j))) 148 | return False 149 | return ret 150 | 151 | 152 | def hex_jdata(jdata): 153 | """ 154 | Some packets get converted to bytes/bytearray when parsing 155 | Convert them back for storage 156 | """ 157 | if type(jdata) in (dict, OrderedDict): 158 | ret = {} 159 | for k, v in jdata.items(): 160 | ret[k] = hex_jdata(v) 161 | return ret 162 | elif type(jdata) in (list, tuple): 163 | for vi, v in enumerate(jdata): 164 | jdata[vi] = hex_jdata(v) 165 | return jdata 166 | elif type(jdata) in (bytes, bytearray): 167 | return tostr(binascii.hexlify(jdata)) 168 | else: 169 | return jdata 170 | -------------------------------------------------------------------------------- /usbrply/serial/mpsse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parses raw FTDI calls into higher level MPSSE info 3 | 4 | If a bad command is detected, the MPSSE returns the value 0xFA, followed by the byte that caused the 5 | bad command. 6 | 7 | Use of the bad command detection is the recommended method of determining whether the MPSSE is in 8 | sync with the application program. By sending a bad command on purpose and looking for 0xFA, the 9 | application can determine whether communication with the MPSSE is possible. 10 | """ 11 | 12 | import binascii 13 | import json 14 | 15 | mpsse_cmd_s2i = { 16 | # think values < 0x10 are bit bang? 17 | 18 | # 3.3 MSB FIRST 19 | "MSB_DOUT_BYTES_PVE": 0x10, 20 | "MSB_11": 0x11, 21 | "MSB_12": 0x12, 22 | "MSB_13": 0x13, 23 | "MSB_20": 0x20, 24 | "MSB_24": 0x24, 25 | "MSB_22": 0x22, 26 | "MSB_26": 0x26, 27 | "MSB_31": 0x31, 28 | "MSB_34": 0x34, 29 | "MSB_33": 0x33, 30 | "MSB_36": 0x36, 31 | 32 | # 3.4 LSB FIRST 33 | "LSB_18": 0x18, 34 | "LSB_19": 0x19, 35 | "LSB_1A": 0x1A, 36 | "LSB_1B": 0x1B, 37 | "LSB_28": 0x28, 38 | "LSB_2C": 0x2C, 39 | "LSB_2A": 0x2A, 40 | "LSB_2E": 0x2E, 41 | "LSB_39": 0x39, 42 | "LSB_3C": 0x3C, 43 | "LSB_3B": 0x3B, 44 | "LSB_3E": 0x3E, 45 | 46 | # 3.5 TMS Commands 47 | "TMS_4A": 0x4A, 48 | "TMS_4B": 0x4B, 49 | "TMS_6A": 0x6A, 50 | "TMS_6B": 0x6B, 51 | "TMS_6E": 0x6E, 52 | "TMS_6F": 0x6F, 53 | 54 | # 3.6 Set / Read Data Bits High / Low Bytes 55 | "SETRDBHLB_80": 0x80, 56 | "SETRDBHLB_82": 0x82, 57 | "SETRDBHLB_81": 0x81, 58 | "SETRDBHLB_83": 0x83, 59 | 60 | # 3.7 Loopback Commands 61 | "LOOPBACK_EN": 0x84, 62 | "LOOPBACK_DIS": 0x85, 63 | 64 | # 3.8 Clock Divisor 65 | # 3.8.1 Set TCK/SK Divisor (FT2232D) 66 | # 3.8.2 Set clk divisor (FT232H/FT2232H/FT4232H) 67 | "SET_TCKSK_DIV": 0x86, 68 | 69 | # 4 Instructions for CPU mode 70 | # 4.2 CPUMode Read Short Address 71 | "CPU_90": 0x90, 72 | # 4.3 CPUMode Read Extended Address 73 | "CPU_91": 0x91, 74 | # 4.4 CPUMode Write Short Address 75 | "CPU_92": 0x92, 76 | # 4.5 CPUMode Write Extended Address 77 | "CPU_93": 0x93, 78 | 79 | # 5 Instructions for use in both MPSSE and MCU Host Emulation Modes 80 | # 5.1 Send Immediate 81 | "SEND_IMMEDIATE": "0x87", 82 | # 5.2 Wait On I/O High 83 | "WAIT_IO_HI": "0x88", 84 | # 5.3 Wait On I/O Low 85 | "WAIT_IO_LO": "0x89", 86 | 87 | # 6 FT232H, FT2232H & FT4232H ONLY 88 | 89 | # Disables the clk divide by 5 to allow for a 60MHz master clock. 90 | "TCK_X5": 0x8A, 91 | # Enables the clk divide by 5 to allow for backward compatibility with FT2232D 92 | "TCK_D5": 0x8B, 93 | # Enables 3 phase data clocking. Used by I 2 C interfaces to allow data on both clock edges. 94 | "ENABLE_3_PHASE_CLOCK": 0x8C, 95 | # Disables 3 phase data clocking. 96 | "DISABLE_3_PHASE_CLOCK": 0x8D, 97 | "CLOCK_N_CYCLES": 0x8E, 98 | "CLOCK_N8_CYCLES": 0x8F, 99 | "PULSE_CLOCK_IO_HIGH": 0x94, 100 | "PULSE_CLOCK_IO_LOW": 0x95, 101 | "ENABLE_ADAPTIVE_CLOCK": 0x96, 102 | "DISABLE_ADAPTIVE_CLOCK": 0x97, 103 | "CLOCK_N8_CYCLES_IO_HIGH": 0x9C, 104 | "CLOCK_N8_CYCLES_IO_LOW": 0x9D, 105 | 106 | # 7 FT232H ONLY 107 | # 7.1 Set I/O to only drive on a '0' and tristate on a '1' 108 | "TRISTATE_IO": 0x9E, 109 | 110 | # basically a nop for testing interface 111 | "INVALID_COMMAND": 0xAB, 112 | } 113 | mpsse_cmd_i2s = dict([(v, k) for k, v in mpsse_cmd_s2i.items()]) 114 | 115 | BAD_COMMAND = 0xFA 116 | """ 117 | Ingests Serial JSON 118 | """ 119 | 120 | 121 | class MPSSEParser: 122 | def __init__(self, argsj=None): 123 | self.jo = [] 124 | 125 | def next_json(self, j, prefix=None): 126 | self.jo.append(j) 127 | 128 | def handleRead(self, d): 129 | print(d['data']) 130 | buff = bytearray(binascii.unhexlify(d['data'])) 131 | if buff[0] == BAD_COMMAND: 132 | which = mpsse_cmd_i2s.get(buff[1], None) 133 | print("read invalid: %s" % which) 134 | else: 135 | print("read 0x%02X" % (buff[0], )) 136 | 137 | def handleWrite(self, d): 138 | print(d['data']) 139 | buff = bytearray(binascii.unhexlify(d['data'])) 140 | cmd = mpsse_cmd_i2s.get(buff[0], None) 141 | print(cmd) 142 | 143 | def run(self, j): 144 | for di, d in enumerate(j["data"]): 145 | print("") 146 | print(d) 147 | if d['type'] == 'read': 148 | self.handleRead(d) 149 | elif d['type'] == 'write': 150 | self.handleWrite(d) 151 | else: 152 | print('fixme: %s' % d['type']) 153 | 154 | jret = { 155 | "data": self.jo, 156 | } 157 | return jret 158 | 159 | 160 | class MPSSETextPrinter: 161 | def __init__(self, argsj=None): 162 | pass 163 | 164 | def next_json(self, j, prefix=None): 165 | print(j) 166 | 167 | def run(self, j): 168 | #self.header() 169 | 170 | for d in j["data"]: 171 | self.next_json(d) 172 | 173 | #self.footer() 174 | 175 | 176 | class MPSSEJSONSPrinter: 177 | def __init__(self, argsj=None): 178 | pass 179 | 180 | def run(self, j): 181 | print(json.dumps(j, sort_keys=True, indent=4, separators=(',', ': '))) 182 | -------------------------------------------------------------------------------- /usbrply/com_pcap.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .pcap_util import PcapParser, load_pcap 3 | 4 | 5 | def default_arg(argsj, k, default): 6 | val = argsj.get(k) 7 | if val is None: 8 | return default 9 | else: 10 | return val 11 | 12 | 13 | class PcapGen(object): 14 | def __init__(self, argsj): 15 | self.verbose = default_arg(argsj, "verbose", False) 16 | # JSON data objects buffered for next yield 17 | self.jbuff = None 18 | self.pcomments = None 19 | self.min_packet = default_arg(argsj, "min_packet", 0) 20 | self.max_packet = default_arg(argsj, "max_packet", float('inf')) 21 | self.cur_packn = None 22 | 23 | # XXX: don't think this is actually used, verify 24 | self.arg_fx2 = default_arg(argsj, "fx2", False) 25 | self.arg_device = default_arg(argsj, "device", None) 26 | # Default to device-hi if no other filtering is specified 27 | self.arg_device_hi = default_arg( 28 | argsj, "device_hi", self.arg_device is None 29 | and not argsj.get("vid") and not argsj.get("pid")) 30 | self.dev_drops = 0 31 | self.arg_setup = default_arg(argsj, "setup", False) 32 | self.arg_halt = default_arg(argsj, "halt", True) 33 | self.arg_remoteio = default_arg(argsj, "remoteio", False) 34 | self.arg_rel_pkt = default_arg(argsj, "rel_pkt", False) 35 | self.arg_print_short = default_arg(argsj, "print_short", False) 36 | self.arg_comment = default_arg(argsj, "comment", False) 37 | self.arg_packet_numbers = default_arg(argsj, "packet_numbers", True) 38 | 39 | # Either auto selected or user selected by now 40 | self.use_pcapng = "pcapng" in argsj["parser"] 41 | 42 | def gcomment(self, s): 43 | """Add global comment, not attached to a packet""" 44 | self.jbuff.append({'type': 'comment', 'v': s}) 45 | 46 | def gwarning(self, s): 47 | self.gcomment("WARNING: " + str(s)) 48 | 49 | def pcomment(self, s): 50 | """Add packet comment. Will be attached to the next packet""" 51 | self.pcomments.append(s) 52 | 53 | def pwarning(self, s): 54 | self.pcomment("WARNING: " + str(s)) 55 | 56 | def printv(self, s): 57 | if self.verbose: 58 | print(s) 59 | 60 | def packnum(self): 61 | ''' 62 | Originally I didn't print anything but found that it was better to keep the line numbers the same 63 | so that I could diff and then easier back annotate with packet numbers 64 | ''' 65 | if self.arg_packet_numbers: 66 | postfix = "%s/%s" % (self.submit.packet_number, self.pktn_str()) 67 | else: 68 | postfix = "N/A" 69 | self.pcomment("Generated from packet %s" % postfix) 70 | 71 | def packnumt(self): 72 | if self.arg_packet_numbers: 73 | return (self.submit.packet_number, self.pktn_str()) 74 | else: 75 | return (None, None) 76 | 77 | def gen_data(self): 78 | parser = PcapParser(self.arg_fin, use_pcapng=self.use_pcapng) 79 | npackets = 0 80 | while True: 81 | npackets += 1 82 | if not parser.next(self.loop_cb): 83 | break 84 | 85 | if self.verbose: 86 | print("gen_data: %s new JSON entries" % (len(self.jbuff), )) 87 | 88 | # Pop packets 89 | for data in self.jbuff: 90 | if 0: 91 | import json 92 | print(data) 93 | json.dumps(data) 94 | yield data 95 | self.jbuff = [] 96 | 97 | if len(self.pending_complete) != 0: 98 | self.gwarning("%lu pending complete requests" % 99 | (len(self.pending_complete))) 100 | # if len(self.pending_submit) != 0: 101 | # self.gwarning("%lu pending submit requests" % (len(self.pending_submit))) 102 | 103 | self.gcomment("PcapGen: generated %u packets" % npackets) 104 | self.gcomment("PcapGen device filter: dropped %u / %u packets" % 105 | (self.dev_drops, npackets)) 106 | 107 | # Pop epilogue packets 108 | for p in self.jbuff: 109 | yield p 110 | self.jbuff = [] 111 | 112 | def platform(self): 113 | assert 0, "required" 114 | 115 | def run(self): 116 | yield 'parser', self.parser() 117 | yield 'platform', self.platform() 118 | yield "fn", self.arg_fin 119 | yield 'args', sys.argv 120 | yield 'packet_min', self.min_packet 121 | yield 'packet_max', self.max_packet 122 | 123 | self.jbuff = [] 124 | self.pcomments = [] 125 | self.gcomment("Generated by usbrply") 126 | self.comment_source() 127 | self.gcomment("cmd: %s" % (' '.join(sys.argv), )) 128 | 129 | if self.arg_device_hi: 130 | self.arg_device = -1 131 | load_pcap(self.arg_fin, 132 | self.loop_cb_devmax, 133 | use_pcapng=self.use_pcapng) 134 | self.gcomment('PCapGen device hi: selected device %u' % 135 | self.arg_device) 136 | self.cur_packn = 0 137 | yield 'device', self.arg_device 138 | 139 | self.printv("parsing from range %s to %s" % 140 | (self.min_packet, self.max_packet)) 141 | yield "data", self.gen_data() 142 | -------------------------------------------------------------------------------- /usbrply/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import usbrply.parsers 5 | import usbrply.printers 6 | import usbrply.filters 7 | from usbrply.util import add_bool_arg 8 | 9 | 10 | def munge_argsj(args): 11 | argsj = args.__dict__ 12 | 13 | if argsj["device"] is None: 14 | argsj["device-hi"] = True 15 | 16 | argsj['vid'] = int(args.vid, 0) 17 | argsj['pid'] = int(args.pid, 0) 18 | 19 | if args.range: 20 | (min_packet, max_packet) = args.range.split(':') 21 | if len(min_packet) == 0: 22 | min_packet = 0 23 | else: 24 | min_packet = int(min_packet, 0) 25 | if len(max_packet) == 0: 26 | max_packet = float('inf') 27 | else: 28 | max_packet = int(max_packet, 0) 29 | argsj['min_packet'] = min_packet 30 | argsj['max_packet'] = max_packet 31 | 32 | return argsj 33 | 34 | 35 | def main(): 36 | parser = argparse.ArgumentParser(description='Replay captured USB packets') 37 | parser.add_argument('--range', '-r', help='inclusive range like 123:456') 38 | """ 39 | parser.add_argument('-k', 40 | dest='ofmt', 41 | default='libusb-py', 42 | action='store_const', 43 | const='linux', 44 | help='output linux kernel') 45 | """ 46 | parser.add_argument('-l', 47 | dest='ofmt', 48 | action='store_const', 49 | const='libusb-c', 50 | help='output libusb C (WARNING: experimental)') 51 | parser.add_argument('-p', 52 | dest='ofmt', 53 | action='store_const', 54 | const='libusb-py', 55 | help='output libusb python') 56 | parser.add_argument("--json", 57 | '-j', 58 | dest='ofmt', 59 | action='store_const', 60 | const='json', 61 | help='output json') 62 | parser.add_argument('-s', help='allow short') 63 | parser.add_argument('-f', help='custom call') 64 | add_bool_arg(parser, 65 | '--packet-numbers', 66 | default=True, 67 | help='print packet numbers') 68 | parser.add_argument('--verbose', '-v', action='store_true', help='verbose') 69 | parser.add_argument( 70 | '--parser', 71 | default="auto", 72 | help='Which parser engine to use. Choices: auto, lin-pcap, win-pcap') 73 | add_bool_arg( 74 | parser, 75 | '--sleep', 76 | default=False, 77 | help='Insert sleep statements between packets to keep original timing') 78 | add_bool_arg(parser, '--comment', default=False, help='General comments') 79 | add_bool_arg(parser, '--fx2', default=False, help='FX2 comments') 80 | add_bool_arg(parser, 81 | '--define', 82 | default=False, 83 | help='Use defines instead of raw numbers') 84 | add_bool_arg(parser, '--halt', default=True, help='Halt on bad packets') 85 | parser.add_argument('--vid', 86 | type=str, 87 | default="0", 88 | help='Only keep packets for given VID') 89 | parser.add_argument('--pid', 90 | type=str, 91 | default="0", 92 | help='Only keep packets for given PID') 93 | parser.add_argument('--device', 94 | type=int, 95 | default=None, 96 | help='Only keep packets for given device') 97 | add_bool_arg(parser, 98 | '--device-hi', 99 | default=None, 100 | help='Auto detect to highest device number') 101 | add_bool_arg(parser, 102 | '--rel-pkt', 103 | default=False, 104 | help='Only count kept packets') 105 | # http://sourceforge.net/p/libusb/mailman/message/25635949/ 106 | add_bool_arg(parser, 107 | '--remoteio', 108 | default=False, 109 | help='Warn on -EREMOTEIO resubmit (default: ignore)') 110 | add_bool_arg( 111 | parser, 112 | '--print-short', 113 | default=False, 114 | help='Print warning when request returns less data than requested') 115 | add_bool_arg( 116 | parser, 117 | '--setup', 118 | default=False, 119 | help='Emit initialization packets like CLEAR_FEATURE, SET_FEATURE') 120 | add_bool_arg(parser, 121 | '--wrapper', 122 | default=False, 123 | help='Emit code to make it a full executable program') 124 | 125 | parser.add_argument('fin', help='File name in') 126 | args = parser.parse_args() 127 | argsj = munge_argsj(args) 128 | 129 | parsed = usbrply.parsers.pcap2json(args.fin, argsj) 130 | filters = [] 131 | filters.append("vidpid") 132 | if not args.setup: 133 | filters.append("setup") 134 | if args.comment or args.fx2: 135 | filters.append("commenter") 136 | filtered = usbrply.filters.run(filters, 137 | parsed, 138 | argsj, 139 | verbose=args.verbose) 140 | usbrply.printers.run(args.ofmt, filtered, argsj=argsj) 141 | 142 | 143 | if __name__ == "__main__": 144 | main() 145 | -------------------------------------------------------------------------------- /usbrply/vidpid_filter.py: -------------------------------------------------------------------------------- 1 | from .usb import req2s 2 | import binascii 3 | import struct 4 | from .util import hexdump 5 | 6 | 7 | def default_arg(argsj, k, default): 8 | val = argsj.get(k) 9 | if val is None: 10 | return default 11 | else: 12 | return val 13 | 14 | 15 | def format_vidpid(vid, pid): 16 | def fmt(x): 17 | if x is None: 18 | return "None" 19 | else: 20 | return "%04x" % x 21 | 22 | if vid is None and pid is None: 23 | return "None" 24 | else: 25 | return "%s:%s" % (fmt(vid), fmt(pid)) 26 | 27 | 28 | class VidpidFilter(object): 29 | def __init__(self, argsj, verbose=False): 30 | # self.setup = argsj.get("setup", False) 31 | self.verbose = verbose 32 | self.entries = 0 33 | self.drops = 0 34 | 35 | self.arg_vid = default_arg(argsj, "vid", None) 36 | if not self.arg_vid: 37 | self.arg_vid = None 38 | self.arg_pid = default_arg(argsj, "pid", None) 39 | if not self.arg_pid: 40 | self.arg_pid = None 41 | self.device2vidpid = {} 42 | self.keep_device = None 43 | 44 | def should_filter(self, data): 45 | comments = [] 46 | 47 | device = data.get('device') 48 | # Comment / metadata 49 | if device is None: 50 | return False, comments 51 | 52 | # a new vid/pid mapping? 53 | # if data["type"] == "controlRead" and req2s(data["bRequestType"], data["bRequest"]) == "GET_DESCRIPTOR" and data["bDescriptorType"] == 0x01: 54 | # FIXME: hack 55 | buff = binascii.unhexlify(data.get("data", "")) 56 | if data["type"] == "controlRead" and req2s( 57 | data["bRequestType"], 58 | data["bRequest"]) == "GET_DESCRIPTOR" and len(buff) == 0x12: 59 | # not actually decoded 60 | # TODO: parse this more genericly 61 | vid, pid = struct.unpack(" %04X:%04X" % 65 | (device, vid, pid)) 66 | 67 | if self.arg_vid is None and self.arg_pid is None: 68 | return False, comments 69 | 70 | if (vid == self.arg_vid 71 | or self.arg_vid is None) or (pid == self.arg_pid 72 | or self.arg_pid is None): 73 | # Note: may appear multiple times 74 | # First during device 0 enumeration, then once assigned on bus 75 | # Keep the second one 76 | if device: 77 | if self.keep_device is None: 78 | comments.append( 79 | self.comment( 80 | "VidpidFilter: match device %u w/ 0x%04X:0x%04X" 81 | % (device, vid, pid))) 82 | elif self.keep_device != device: 83 | comments.append( 84 | self.comment( 85 | "WARNING VidpidFilter: already had different device" 86 | )) 87 | self.keep_device = device 88 | return False, comments 89 | 90 | if self.arg_vid is None and self.arg_pid is None: 91 | return False, comments 92 | 93 | # Filter: 94 | # Devices not matching target 95 | # Anything before we've established mapping. Most of this traffic isn't important and simplifies parser 96 | # print(self.keep_device, device) 97 | return self.keep_device is None or device != self.keep_device, comments 98 | 99 | def comment(self, s): 100 | return { 101 | "type": "comment", 102 | "v": s, 103 | } 104 | 105 | def gen_data(self, datas): 106 | self.verbose and print("vidpid: want %s" % 107 | (format_vidpid(self.arg_vid, self.arg_pid))) 108 | for data in datas: 109 | self.entries += 1 110 | should_filter, yields = self.should_filter(data) 111 | for y in yields: 112 | yield y 113 | if should_filter: 114 | self.verbose and print( 115 | "VidpidFilter drop %s (%s %s %s)" % 116 | (data['type'], req2s(data["bRequestType"], 117 | data["bRequest"]), 118 | data["bRequestType"], data["bRequest"])) 119 | self.verbose and print( 120 | "VidpidFilter drop device %s" % data.get('device')) 121 | self.drops += 1 122 | continue 123 | else: 124 | self.verbose and print( 125 | "VidpidFilter keep device %s" % data.get('device')) 126 | yield data 127 | yield self.comment("VidpidFilter: dropped %s / %s entries, want %s" % 128 | (self.drops, self.entries, 129 | format_vidpid(self.arg_vid, self.arg_pid))) 130 | 131 | def run(self, jgen): 132 | for k, v in jgen: 133 | if k == "data": 134 | yield k, self.gen_data(v) 135 | else: 136 | yield k, v 137 | if self.verbose: 138 | print("vidpid: %u device mappings" % (len(self.device2vidpid))) 139 | for k, (vid, pid) in sorted(self.device2vidpid.items()): 140 | print(" %s: %04X:%04X" % (k, vid, pid)) 141 | 142 | yield "device2vidpid", self.device2vidpid 143 | -------------------------------------------------------------------------------- /usbrply/pcap_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from .usb import * 4 | 5 | # High performance C pcap librar 6 | # Python 2 only, no longer maintained 7 | try: 8 | import pcap 9 | except ImportError: 10 | pcap = None 11 | 12 | # Slow but reliable pure Python 13 | try: 14 | import pcapng 15 | except ImportError: 16 | pcapng = None 17 | 18 | import sys 19 | """ 20 | Quick hack to detect packet format 21 | I don't think the API i'm using 22 | """ 23 | 24 | 25 | def guess_linux(buff): 26 | """ 27 | linux heuristics 28 | 0x8: one of SCE 29 | 0x1C:0x1F (urb status): 0 => success, almost always 30 | windows: 31 | """ 32 | if len(buff) < 0x30: 33 | return False 34 | return sum(buff[0x1C:0x20]) == 0 35 | 36 | 37 | def guess_windows(buff): 38 | """ 39 | windows heuristics 40 | 0xA:0xD (error code): 0 => success, almost always 41 | linux: endpoint, device, bus id. Unlikely to be 0 42 | 0x10 (IRP information): either 0 or 1 43 | """ 44 | if len(buff) < 0x24: 45 | return False 46 | return sum(buff[0x0A:0x0E]) == 0 47 | 48 | 49 | class PcapParser(object): 50 | def __init__(self, fn, use_pcapng=None): 51 | self.fp = None 52 | self.fn = fn 53 | self.scanner = None 54 | self.pcap = None 55 | 56 | # Select library 57 | self.use_pcapng = use_pcapng 58 | if self.use_pcapng is None: 59 | # User higher performance library if available 60 | self.use_pcapng = False if pcap else True 61 | # self.pcapng = "pcapng" in argsj["parser"] 62 | if self.use_pcapng: 63 | assert pcapng, "pcapng library requested but no pcapng library" 64 | else: 65 | assert pcap, "pcap library requested but no pcap library" 66 | 67 | # Initialize library 68 | if self.use_pcapng: 69 | self.fp = open(fn, 'rb') 70 | self.scanner = pcapng.FileScanner(self.fp) 71 | self.scanner_iter = self.scanner.__iter__() 72 | else: 73 | self.pcap = pcap.pcapObject() 74 | self.pcap.open_offline(fn) 75 | 76 | def __del__(self): 77 | if self.scanner: 78 | del self.scanner 79 | if self.pcap: 80 | del self.pcap 81 | if self.fp: 82 | self.fp.close() 83 | 84 | def next(self, loop_cb): 85 | """return True if there was data and might be more, False if nothing was processed""" 86 | if self.use_pcapng: 87 | while True: 88 | try: 89 | block = next(self.scanner_iter) 90 | except StopIteration: 91 | return False 92 | 93 | if not isinstance(block, pcapng.blocks.EnhancedPacket): 94 | continue 95 | loop_cb(block.captured_len, block.packet_data, block.timestamp) 96 | return True 97 | else: 98 | got = [False] 99 | 100 | # return code isn't given to indicate end 101 | def my_loop_cb(*args, **kwargs): 102 | got[0] = True 103 | loop_cb(*args, **kwargs) 104 | 105 | self.pcap.loop(1, my_loop_cb) 106 | return got[0] 107 | 108 | 109 | def load_pcap(fn, loop_cb, lim=float('inf'), use_pcapng=None): 110 | parser = PcapParser(fn, use_pcapng=use_pcapng) 111 | i = 0 112 | while parser.next(loop_cb): 113 | i += 1 114 | if i >= lim: 115 | break 116 | 117 | 118 | def guess_parser_pcapng(fn): 119 | if not pcapng: 120 | return None 121 | 122 | with open(fn, "rb") as fp: 123 | scanner = pcapng.FileScanner(fp) 124 | blocks = iter(scanner) 125 | # Occurs if it can't parse the pcapng header 126 | try: 127 | block = next(blocks) 128 | except ValueError: 129 | return None 130 | 131 | assert type(block) is pcapng.blocks.SectionHeader 132 | 133 | os = block.options["shb_os"] 134 | if "Linux" in os: 135 | return "Linux" 136 | elif "Windows" in os: 137 | return "Windows" 138 | else: 139 | assert 0, "unexpected os %s" % (os, ) 140 | 141 | 142 | def guess_parser(fn): 143 | # pcapng detection is reliable and direct using file headers 144 | pcapng_guess = guess_parser_pcapng(fn) 145 | if pcapng_guess is not None: 146 | if pcapng_guess == "Windows": 147 | if pcap: 148 | return "win-pcap" 149 | else: 150 | return "win-pcapng" 151 | elif pcapng_guess == "Linux": 152 | if pcap: 153 | return "lin-pcap" 154 | else: 155 | return "lin-pcapng" 156 | else: 157 | assert 0 158 | else: 159 | windows = 0 160 | linux = 0 161 | 162 | def loop_cb_guess(caplen, packet, ts): 163 | nonlocal windows 164 | nonlocal linux 165 | 166 | packet = bytearray(packet) 167 | if guess_linux(packet): 168 | linux += 1 169 | if guess_windows(packet): 170 | windows += 1 171 | 172 | parser = PcapParser(fn) 173 | i = 0 174 | while parser.next(loop_cb_guess): 175 | i += 1 176 | if i >= 3: 177 | break 178 | 179 | if windows: 180 | assert linux == 0 181 | if parser.use_pcapng: 182 | return "win-pcapng" 183 | else: 184 | return "win-pcap" 185 | if linux: 186 | assert windows == 0 187 | if parser.use_pcapng: 188 | return "lin-pcapng" 189 | else: 190 | return "lin-pcap" 191 | assert 0, "failed to identify packet format" 192 | -------------------------------------------------------------------------------- /usbserial.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from usbrply.printer import Printer, indented, indent_inc, indent_dec 4 | from usbrply.util import add_bool_arg 5 | import usbrply.parsers 6 | import usbrply.serial.parsers as sparsers 7 | import usbrply.serial.printers as sprinters 8 | from usbrply.serial import mpsse 9 | import usbrply.main 10 | from usbrply import parsers 11 | 12 | import argparse 13 | 14 | 15 | def main(): 16 | parser = argparse.ArgumentParser( 17 | description='Decode USB-serial data (experimental / alpha quality)') 18 | parser.add_argument('--range', '-r', help='inclusive range like 123:456') 19 | """ 20 | parser.add_argument('-k', 21 | dest='ofmt', 22 | default='libusb-py', 23 | action='store_const', 24 | const='linux', 25 | help='output linux kernel') 26 | """ 27 | parser.add_argument('-l', 28 | dest='ofmt', 29 | action='store_const', 30 | const='libusb-c', 31 | help='output libusb C (WARNING: experimental)') 32 | parser.add_argument('-p', 33 | dest='ofmt', 34 | action='store_const', 35 | const='libusb-py', 36 | help='output libusb python') 37 | parser.add_argument("--json", 38 | '-j', 39 | dest='ofmt', 40 | action='store_const', 41 | const='json', 42 | help='output json') 43 | parser.add_argument("--text", 44 | '-t', 45 | dest='ofmt', 46 | action='store_const', 47 | const='text', 48 | help='output txt') 49 | parser.add_argument('-s', help='allow short') 50 | parser.add_argument('-f', help='custom call') 51 | add_bool_arg(parser, 52 | '--packet-numbers', 53 | default=True, 54 | help='print packet numbers') 55 | parser.add_argument('--verbose', '-v', action='store_true', help='verbose') 56 | parser.add_argument( 57 | '--parser', 58 | default="auto", 59 | help='Which parser engine to use. Choices: auto, lin-pcap, win-pcap') 60 | add_bool_arg( 61 | parser, 62 | '--sleep', 63 | default=False, 64 | help='Insert sleep statements between packets to keep original timing') 65 | add_bool_arg(parser, '--comment', default=False, help='General comments') 66 | add_bool_arg(parser, '--fx2', default=False, help='FX2 comments') 67 | add_bool_arg(parser, 68 | '--define', 69 | default=False, 70 | help='Use defines instead of raw numbers') 71 | add_bool_arg(parser, '--halt', default=True, help='Halt on bad packets') 72 | parser.add_argument('--vid', 73 | type=str, 74 | default="0", 75 | help='Only keep packets for given VID') 76 | parser.add_argument('--pid', 77 | type=str, 78 | default="0", 79 | help='Only keep packets for given PID') 80 | parser.add_argument('--device', 81 | type=int, 82 | default=None, 83 | help='Only keep packets for given device') 84 | add_bool_arg(parser, 85 | '--device-hi', 86 | default=None, 87 | help='Auto detect to highest device number') 88 | add_bool_arg(parser, 89 | '--rel-pkt', 90 | default=False, 91 | help='Only count kept packets') 92 | # http://sourceforge.net/p/libusb/mailman/message/25635949/ 93 | add_bool_arg(parser, 94 | '--remoteio', 95 | default=False, 96 | help='Warn on -EREMOTEIO resubmit (default: ignore)') 97 | add_bool_arg( 98 | parser, 99 | '--print-short', 100 | default=False, 101 | help='Print warning when request returns less data than requested') 102 | add_bool_arg( 103 | parser, 104 | '--setup', 105 | default=False, 106 | help='Emit initialization packets like CLEAR_FEATURE, SET_FEATURE') 107 | add_bool_arg(parser, 108 | '--wrapper', 109 | default=False, 110 | help='Emit code to make it a full executable program') 111 | 112 | add_bool_arg(parser, 113 | '--mpsee', 114 | default=False, 115 | help='Decode mpsee traffic (highly experimental)') 116 | add_bool_arg(parser, '--serial-emit-adata', default=True, help='') 117 | add_bool_arg(parser, '--serial-keep-raw-data', default=False, help='') 118 | add_bool_arg(parser, '--serial-keep-unused-data', default=False, help='') 119 | add_bool_arg(parser, '--serial-keep-empty-txrx', default=False, help='') 120 | 121 | parser.add_argument('fin', help='File name in') 122 | args = parser.parse_args() 123 | argsj = usbrply.main.munge_argsj(args) 124 | 125 | if args.verbose: 126 | print("") 127 | print("") 128 | print("") 129 | print("PASS: USB parse") 130 | gen = usbrply.parsers.pcap2json_prepare(args.fin, argsj) 131 | parsed = gen.run() 132 | # HACK: get from json output 133 | filters = [] 134 | filters.append("vidpid") 135 | if not args.setup: 136 | filters.append("setup") 137 | if args.comment or args.fx2: 138 | filters.append("commenter") 139 | fobjs, filtered = usbrply.filters.runx(filters, 140 | parsed, 141 | argsj, 142 | verbose=args.verbose) 143 | 144 | if args.verbose: 145 | print("") 146 | print("") 147 | print("") 148 | print("PASS: serial parse") 149 | j = parsers.jgen2j(filtered) 150 | fvidpid = fobjs["vidpid"] 151 | """ 152 | print(len(j)) 153 | import json 154 | print(json.dumps(j, 155 | sort_keys=True, 156 | indent=4, 157 | separators=(',', ': '))) 158 | # print(fvidpid.keep_device, gen.arg_device) 159 | """ 160 | # FIXME: device-hi is faster, so kind of messy 161 | device = gen.arg_device 162 | vid, pid = fvidpid.device2vidpid[device ] 163 | # print("%04X:%04X" % (vid, pid)) 164 | 165 | # FIXME: allow user to force parser 166 | if vid == 0x0403: 167 | fparser = sparsers.FT2232CParser 168 | else: 169 | raise Exception("Unknown device" % "%04X:%04X" % (vid, pid)) 170 | 171 | txtj = fparser(argsj=argsj).run(j) 172 | 173 | ofmt = args.ofmt 174 | if ofmt is None: 175 | ofmt = "libusb-py" 176 | printer = { 177 | 'text': sprinters.TextFT2232CPrinter, 178 | 'libusb-py': sprinters.PythonFT2232CPrinter, 179 | 'json': sprinters.JSONSPrinter, 180 | }[ofmt] 181 | 182 | if args.verbose: 183 | print("") 184 | print("") 185 | print("") 186 | print("PASS: serial print") 187 | printer(argsj=argsj).run(txtj) 188 | 189 | if args.mpsee: 190 | if args.verbose: 191 | print("") 192 | print("") 193 | print("") 194 | print("PASS: MPSSE parse") 195 | mpssej = mpsse.MPSSEParser().run(txtj) 196 | if args.verbose: 197 | print("") 198 | print("") 199 | print("") 200 | print("PASS: MPSSE print") 201 | mpsse.MPSSETextPrinter(args).run(mpssej) 202 | 203 | 204 | if __name__ == "__main__": 205 | main() 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # usbrply 2 | 3 | Convert a .pcap file (captured USB packets) to Python or C code that replays the captured USB commands. 4 | 5 | Supported packet sources are: 6 | * Linux Wireshark (via usbmon) 7 | * Windows Wireshark (via USBPcap) 8 | 9 | Supported output formats are: 10 | * libusb Python (primary) 11 | * (libusb C: fixme) 12 | * (Linux Kernel C: fixme) 13 | * JSON 14 | 15 | Example applications: 16 | * Rapidly reverse engineer and re-implement USB protocols 17 | * Record a proprietary Windows programming sequence and replay on an embedded Linux device 18 | * Snoop USB-serial packets 19 | 20 | Questions? Please reach out on github or join #usbrply on Freenode IRC 21 | 22 | # Linux installation 23 | 24 | ``` 25 | # Do one of these 26 | # Easier to setup, but slower 27 | sudo pip install python-pcapng 28 | # Much faster, but no longer maintained 29 | sudo apt-get install -y python-libpcap 30 | git clone https://github.com/JohnDMcMaster/usbrply.git 31 | cd usbrply 32 | sudo python setup.py install 33 | ``` 34 | 35 | # Windows installation 36 | 37 | There is probably an easier way to do this but this is what I got to work. Tested on Windows 7 x64 38 | 39 | Setup python and pip 40 | * Get the latest Python 3 release (https://www.python.org/downloads/) 41 | * I used Python 3.7.8 (Windows x86-64 executable installer) 42 | * Keep default setup options (in particular this will install pip) 43 | 44 | Install libusb1 45 | * pip install libusb1 46 | 47 | Install usb drivers 48 | * Some USB drivers won't replay. You need to switch their USB drivers to WinUSB. You can use Zadig to switch between USB libraries for a single device. 49 | * https://zadig.akeo.ie/ 50 | * Run Zadig 51 | * Click on **Options** then **List all devices** then select the USB device that doesn't replay properly and **Replace Driver** with WinUSB 52 | 53 | 54 | Install 55 | * Open a command prompt 56 | * Default should be your home dir (ex: C:\Users\mcmaster) 57 | * `python -m venv usbrply` 58 | * `usbrply\Scripts\activate.bat` 59 | * `pip install usbrply` 60 | 61 | Test 62 | * If not still in venv (prompt like "(usbrply)" ): `usbrply/Scripts/activate.bat` 63 | * `python usbrply\Scripts\usbrply -h` 64 | * You should get a help message 65 | * Download and place in your home dir: https://github.com/JohnDMcMaster/usbrply-test/raw/master/win1.pcapng 66 | * `python usbply\Scripts\usbrply win1.pcapng` 67 | * You should see python code that will reproduce the .pcap file commands 68 | 69 | # Sample workflows 70 | 71 | ## Capturing Windows traffic and replaying traffic in Python: 72 | * Install Wireshark. Make sure you install the USBPcap library 73 | * Start Wireshark 74 | * Connect USB device to computer 75 | * Select which USB device you want to capture by clicking on the tiny blue cogwheel and checking the box next to the USB device you want to capture ![usbPcap](docs/usbPcap.png) 76 | * Double click on the USBPcap to start the capture 77 | * Start your application, do your thing, etc to generate packets 78 | * Close application 79 | * Stop capture 80 | * Save capture. Save in pcap-ng format (either should work) 81 | * Close Wireshark 82 | * Run: `usbrply --wrapper --device-hi -p my.pcapng >replay.py` 83 | * Assuming your usb device is connected to the computer, go to "Device manager", find your device, right click on it, select "Properties", go to "Details" tab, select "Hardware IDs" from the drop-down, and you will find an entry of a form: HID\VID_046D&PID_C05A For this example the vid is 0x046D and the pid is 0xC05A 84 | * Scroll down to the bottom of replay.py and edit the following line: 85 | ``` 86 | if (vid, pid) == (**0x0000**, **0x0000**): 87 | ``` 88 | * Example edited line: 89 | ``` 90 | if (vid, pid) == (**0x046D**, **0xC05A**): 91 | ``` 92 | * Linux: run `python replay.py` 93 | * Verify expected device behavior. Did an LED blink? Did you get expected data back? 94 | 95 | ## Capturing Windows VM traffic from Linux host and replaying traffic in Python: 96 | Example: program a Xilinx dev board under Linux without knowing anything about the JTAG adapter USB protocol 97 | * On Linux host: 98 | * Install Wireshark 99 | * Enable usbmon so Wireshark can capture USB (e.g. `sudo modprobe usbmon`, see http://wiki.wireshark.org/CaptureSetup/USB) 100 | * Boot Windows VM (ie through VMWare) 101 | * Start Wireshark. Make sure you have USB permissions (ie you may need to sudo) 102 | * Connect USB device to computer 103 | * Use lsusb to determine which device bus is on. Try to choose a bus (port) with no other devices 104 | * Start catpure on bus from above 105 | * Attach USB device to Windows guest 106 | * On Windows guest: 107 | * Start your application, do your thing, etc to generate packets 108 | * Back on Linux host: 109 | * Stop capture in Wireshark. 110 | * Save capture. Save in pcap-ng format (either should work) 111 | * Run: `usbrply --device-hi -p my.pcapng >replay.py` 112 | * Detatch USB device from Windows guest 113 | * Run `python replay.py` 114 | * Verify expected device behavior. Did an LED blink? Did you get expected data back? 115 | 116 | ## Capturing from Linux Terminal 117 | 118 | * Install tshark 119 | * Enable usbmon so tshark can capture USB (e.g. `sudo modprobe usbmon`, see http://wiki.wireshark.org/CaptureSetup/USB) 120 | * Connect USB device to computer 121 | * Use `lsusb` to determine which device bus is on. Try to choose a bus (port) with no other devices 122 | * Start capturing using tshark like: `tshark -i usbmon1 -w capture.pcapng` 123 | * The usbmon device matches the bus number output from `lsusb` e.g. a device entry starting with `Bus 001 Device 002` would be captured from `usbmon1`. 124 | * Use the device as desired. 125 | * Stop capturing by hitting Ctrl-C. 126 | * Run: `usbrply --device-hi -p capture.pcapng > replay.py` 127 | 128 | # Command Line Options 129 | You may need to filter out USB devices. There are two ways to do this: 130 | * `--device-hi`: use the last device enumerated. This works well in most cases, including FX2 renumeration 131 | * `--device DEVICE`: manually specify the USB device used. Get this from lsusb output or Wireshark view 132 | 133 | Other useful switches: 134 | * `--rel-pkt`: intended to easier allow diffing two outputs. Ex: what changed in trace for LED on vs LED off? 135 | * `--no-packet-numbers`: alternative to above 136 | * `--fx2`: decode common FX2 commands (ex: CPU reset) 137 | * `--range RANGE`: only decode a specific packet range. Use along with Wireshark GUI or refine a previous decode 138 | * see `--help` for more 139 | 140 | # Version history 141 | 142 | v0.0.0 143 | * Crusty C++ program 144 | 145 | v0.0.1 146 | * Crusty python program 147 | 148 | v1.0.0 149 | * Separate parsing from printing 150 | * Windows data source officially supported 151 | 152 | v2.0.0 153 | * JSON: packn moved to new "submit" and "complete" entries 154 | * JSON now has raw urb structures (added to submit/complete) 155 | * python3 support 156 | * libpcapng support 157 | 158 | v2.0.1 159 | * Fix packaging issues 160 | 161 | v2.1.0 162 | * python2 support officially removed 163 | * VID/PID filter fixed 164 | * Windows pip install instructions 165 | * Linux: basic interrupt support 166 | * General interrupt cleanup / fixes 167 | * Better logging for dropped packets 168 | * --no-packet-numbers: line numbers line up vs --packet-numbers 169 | 170 | v2.1.1 171 | * Fix pip README 172 | 173 | # JSON output 174 | 175 | use -j switch to output a parsing intermediate representation that should resemble original USB requests 176 | along with associated metadata. 177 | This can be used in more advanced applications, such as if you need to decode a complicated protocol 178 | or convert USB output to higher level API calls. 179 | An example can be found here: https://github.com/ProgHQ/bpmicro/blob/master/scrape.py 180 | This example first aggregates USB packets into application specific packets, and then decodes these into API calls 181 | 182 | 183 | # USB serial decoder 184 | 185 | usbrply-serial supported adapters: 186 | * FT2232C: data rx/tx 187 | 188 | TODO: write doc 189 | 190 | -------------------------------------------------------------------------------- /usbrply/pyprinter.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from usbrply import printer 3 | from usbrply import parsers 4 | from .printer import Printer, indented, indent_inc, indent_dec, indent_reset 5 | import sys 6 | import binascii 7 | from . import usb 8 | from .util import myord 9 | 10 | 11 | def comment(s): 12 | indented("# %s" % (s, )) 13 | 14 | 15 | def bytes2AnonArray(bytes_data): 16 | # In Python2 bytes_data is a string, in Python3 it's bytes. 17 | # The element type is different (string vs int) and we have to deal 18 | # with that when printing this number as hex. 19 | 20 | byte_str = "b\"" 21 | 22 | for i in range(len(bytes_data)): 23 | if i and i % 16 == 0: 24 | byte_str += "\"\n b\"" 25 | byte_str += "\\x%02X" % (bytes_data[i], ) 26 | return byte_str + "\"" 27 | 28 | 29 | class LibusbPyPrinter(Printer): 30 | def __init__(self, argsj, verbose=None): 31 | Printer.__init__(self, argsj) 32 | self.prevd = None 33 | self.wrapper = argsj.get("wrapper", False) 34 | self.sleep = argsj.get("sleep", False) 35 | self.packet_numbers = argsj.get("packet_numbers", True) 36 | # FIXME 37 | self.vid = None 38 | self.pid = None 39 | if verbose is None: 40 | verbose = argsj.get("verbose", False) 41 | self.verbose = verbose 42 | self.argsj = argsj 43 | 44 | def print_imports(self): 45 | print('''\ 46 | import binascii 47 | import time 48 | import usb1 49 | ''', 50 | file=printer.print_file) 51 | 52 | def print_wrapper_header(self): 53 | print('''\ 54 | def validate_read(expected, actual, msg): 55 | if expected != actual: 56 | print('Failed %s' % msg) 57 | print(' Expected; %s' % binascii.hexlify(expected,)) 58 | print(' Actual: %s' % binascii.hexlify(actual,)) 59 | #raise Exception("failed validate: %s" % msg) 60 | 61 | ''', 62 | file=printer.print_file) 63 | print("def replay(dev):", file=printer.print_file) 64 | indent_inc() 65 | print('''\ 66 | def bulkRead(endpoint, length, timeout=None): 67 | return dev.bulkRead(endpoint, length, timeout=(1000 if timeout is None else timeout)) 68 | 69 | def bulkWrite(endpoint, data, timeout=None): 70 | dev.bulkWrite(endpoint, data, timeout=(1000 if timeout is None else timeout)) 71 | 72 | def controlRead(bRequestType, bRequest, wValue, wIndex, wLength, 73 | timeout=None): 74 | return dev.controlRead(bRequestType, bRequest, wValue, wIndex, wLength, 75 | timeout=(1000 if timeout is None else timeout)) 76 | 77 | def controlWrite(bRequestType, bRequest, wValue, wIndex, data, 78 | timeout=None): 79 | dev.controlWrite(bRequestType, bRequest, wValue, wIndex, data, 80 | timeout=(1000 if timeout is None else timeout)) 81 | 82 | def interruptRead(endpoint, size, timeout=None): 83 | return dev.interruptRead(endpoint, size, 84 | timeout=(1000 if timeout is None else timeout)) 85 | 86 | def interruptWrite(endpoint, data, timeout=None): 87 | dev.interruptWrite(endpoint, data, timeout=(1000 if timeout is None else timeout)) 88 | ''', 89 | file=printer.print_file) 90 | 91 | def header(self): 92 | indented("#!/usr/bin/env python3") 93 | comment("Generated by usbrply") 94 | comment("cmd: %s" % (" ".join(sys.argv), )) 95 | indented("") 96 | 97 | if self.wrapper: 98 | self.print_imports() 99 | print("", file=printer.print_file) 100 | self.print_wrapper_header() 101 | 102 | def footer(self): 103 | if not self.wrapper: 104 | return 105 | print(''' 106 | def open_dev(vid_want, pid_want, usbcontext=None): 107 | if usbcontext is None: 108 | usbcontext = usb1.USBContext() 109 | 110 | print("Scanning for devices...") 111 | for udev in usbcontext.getDeviceList(skip_on_error=True): 112 | vid = udev.getVendorID() 113 | pid = udev.getProductID() 114 | if (vid, pid) == (vid_want, pid_want): 115 | print("Found device") 116 | print("Bus %03i Device %03i: ID %04x:%04x" % ( 117 | udev.getBusNumber(), 118 | udev.getDeviceAddress(), 119 | vid, 120 | pid)) 121 | return udev.open() 122 | raise Exception("Failed to find a device") 123 | 124 | def main(): 125 | import argparse 126 | 127 | vid_want = ''' + self.vid_str() + ''' 128 | pid_want = ''' + self.pid_str() + ''' 129 | parser = argparse.ArgumentParser(description="Replay captured USB packets") 130 | args = parser.parse_args() 131 | 132 | usbcontext = usb1.USBContext() 133 | dev = open_dev(vid_want, pid_want, usbcontext) 134 | dev.claimInterface(0) 135 | dev.resetDevice() 136 | replay(dev) 137 | 138 | if __name__ == "__main__": 139 | main() 140 | ''', 141 | file=printer.print_file) 142 | 143 | def vid_str(self): 144 | if self.vid: 145 | return "0x%04X" % (self.vid, ) 146 | else: 147 | return "None" 148 | 149 | def pid_str(self): 150 | if self.pid: 151 | return "0x%04X" % (self.pid, ) 152 | else: 153 | return "None" 154 | 155 | def packet_number_str(self, d): 156 | if self.packet_numbers: 157 | return "packet %s/%s" % (d["submit"]["packn"], 158 | d["complete"]["packn"]) 159 | else: 160 | # TODO: consider counting instead of by captured index 161 | return "packet" 162 | 163 | def parse_data(self, d): 164 | # print(d) 165 | if self.sleep and self.prevd and d["type"] != "comment": 166 | try: 167 | # Fall back to t_urb for original pcap format on Linux? 168 | def gett(d): 169 | if "t" in d["submit"]: 170 | return d["submit"]["t"] 171 | elif "t_urb" in d["submit"]: 172 | return d["submit"]["t"] 173 | else: 174 | raise Exception( 175 | "Requested sleep but couldn't establish time reference" 176 | ) 177 | 178 | dt = gett(d) - gett(self.prevd) 179 | except KeyError: 180 | raise ValueError("Input JSON does not support timestamps") 181 | if dt >= 0.001: 182 | indented("time.sleep(%.3f)" % (dt, )) 183 | 184 | if d["type"] == "comment": 185 | comment(d["v"]) 186 | return 187 | 188 | packet_numbering = self.packet_number_str(d) 189 | 190 | if "comments" in d: 191 | for c in d["comments"]: 192 | comment(c) 193 | 194 | if d["type"] == "controlRead": 195 | # Is it legal to have a 0 length control in? 196 | indented("buff = controlRead(0x%02X, 0x%02X, 0x%04X, 0x%04X, %u)" % 197 | (d["bRequestType"], d["bRequest"], d["wValue"], 198 | d["wIndex"], d["wLength"])) 199 | indented("validate_read(%s, buff, \"%s\")" % (bytes2AnonArray( 200 | binascii.unhexlify(d["data"])), packet_numbering)) 201 | elif d["type"] == "controlWrite": 202 | data_str = bytes2AnonArray(binascii.unhexlify(d["data"])) 203 | indented("controlWrite(0x%02X, 0x%02X, 0x%04X, 0x%04X, %s)" % 204 | (d["bRequestType"], d["bRequest"], d["wValue"], 205 | d["wIndex"], data_str)) 206 | 207 | elif d["type"] == "bulkRead": 208 | indented("buff = bulkRead(0x%02X, 0x%04X)" % (d["endp"], d["len"])) 209 | indented("validate_read(%s, buff, \"%s\")" % (bytes2AnonArray( 210 | binascii.unhexlify(d["data"])), packet_numbering)) 211 | elif d["type"] == "bulkWrite": 212 | # Note that its the submit from earlier, not the ack that we care about 213 | data_str = bytes2AnonArray(binascii.unhexlify(d["data"])) 214 | # def bulkWrite(self, endpoint, data, timeout=0): 215 | indented("bulkWrite(0x%02X, %s)" % (d["endp"], data_str)) 216 | 217 | elif d["type"] == "interruptIn": 218 | indented("buff = interruptRead(0x%02X, 0x%04X)" % 219 | (d["endp"], d["len"])) 220 | indented("validate_read(%s, buff, \"%s\")" % (bytes2AnonArray( 221 | binascii.unhexlify(d["data"])), packet_numbering)) 222 | 223 | elif d["type"] == "interruptOut": 224 | data_str = bytes2AnonArray(binascii.unhexlify(d["data"])) 225 | indented("interruptWrite(0x%02X, %s)" % (d["endp"], data_str)) 226 | elif d["type"] == "irpInfo": 227 | comment("IRP_INFO(): func %s" % 228 | (d["submit"]["urb"]["usb_func_str"], )) 229 | elif d["type"] == "abortPipe": 230 | comment("ABORT_PIPE()") 231 | else: 232 | if self.verbose: 233 | print("LibusbPyPrinter WARNING: dropping %s" % (d["type"], )) 234 | 235 | # these aren't event added to JSON right now 236 | # print("%s# WARNING: omitting interrupt" % (indent,)) 237 | 238 | if d["type"] != "comment": 239 | self.prevd = d 240 | 241 | def run(self, jgen): 242 | indent_reset() 243 | self.header() 244 | 245 | # Last wire command (ie non-comment) 246 | # Used to optionally generate timing 247 | self.prevd = None 248 | 249 | # Convert generator into static JSON 250 | # caches vid/pid among other things 251 | j = parsers.jgen2j(jgen) 252 | 253 | for d in j["data"]: 254 | self.parse_data(d) 255 | 256 | if self.wrapper and (self.vid is None or self.pid is None): 257 | if len(j["device2vidpid"]) != 1: 258 | raise Exception( 259 | "Failed to guess vid/pid: found %u device entries" % 260 | len(j["device2vidpid"])) 261 | for (vid, pid) in j["device2vidpid"].values(): 262 | self.vid = vid 263 | self.pid = pid 264 | if self.wrapper: 265 | self.footer() 266 | -------------------------------------------------------------------------------- /usbrply/serial/parsers.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from _collections import OrderedDict 3 | from usbrply import util 4 | 5 | FTDI_DEVICE_OUT_REQTYPE = 64 6 | FTDI_DEVICE_IN_REQTYPE = 192 7 | 8 | req_s2i = { 9 | "RESET": 0, 10 | "SET_MODEM_CTRL": 1, 11 | "SET_FLOW_CTRL": 2, 12 | "SET_BAUDRATE": 3, 13 | "SET_DATA": 4, 14 | "POLL_MODEM_STATUS": 0x05, 15 | "SET_EVENT_CHAR": 0x06, 16 | "SET_ERROR_CHAR": 0x07, 17 | "SET_LATENCY_TIMER": 0x09, 18 | "GET_LATENCY_TIMER": 0x0A, 19 | "SET_BITMODE": 0x0B, 20 | "READ_PINS": 0x0C, 21 | "READ_EEPROM": 0x90, 22 | "WRITE_EEPROM": 0x91, 23 | "ERASE_EEPROM": 0x92, 24 | } 25 | req_i2s = dict([(v, k) for k, v in req_s2i.items()]) 26 | 27 | INTERFACE_ANY = 0 28 | INTERFACE_A = 1 29 | INTERFACE_B = 2 30 | INTERFACE_C = 3 31 | INTERFACE_D = 4 32 | 33 | 34 | def interface_i2str(i): 35 | if i == INTERFACE_A: 36 | return "A" 37 | elif i == INTERFACE_B: 38 | return "B" 39 | elif i == INTERFACE_C: 40 | return "C" 41 | elif i == INTERFACE_D: 42 | return "D" 43 | assert 0 44 | 45 | 46 | def flags2dict(s2i, vali): 47 | ret = {} 48 | for k, v in s2i.items(): 49 | ret[k] = bool(vali & v) 50 | return ret 51 | 52 | 53 | """ 54 | "is a packet json output format but a serial input format 55 | A options to consider: 56 | -Don't preserve source packets 57 | -Only output serial packets 58 | """ 59 | 60 | 61 | class FT2232CParser: 62 | def __init__(self, argsj=None): 63 | self.jo = [] 64 | # Lower level packet definitions generating last packet 65 | self.raw_jdata = [] 66 | self.verbose = argsj.get("verbose", False) 67 | 68 | # emit ASCII data decoding 69 | # unprintable characters are replaced with .s 70 | self.emit_adata = argsj.get("serial_emit_adata", True) 71 | 72 | # After parsing packet stash the source packet(s) 73 | self.keep_raw_data = argsj.get("serial_keep_raw_data", False) 74 | # Unknown packets are passed through as-is 75 | self.keep_unused_data = argsj.get("serial_keep_unused_data", False) 76 | # When false they are passed through (if passthrough is kept) 77 | self.keep_empty_txrx = argsj.get("serial_keep_empty_txrx", False) 78 | 79 | if argsj.get("serial_keep_everything", False): 80 | self.keep_raw_data = True 81 | self.keep_unused_data = True 82 | self.keep_empty_txrx = True 83 | 84 | def add_raw_jdata(self, data): 85 | self.raw_jdata.append(data) 86 | 87 | def passthrough_jdata(self, jdata): 88 | # Should have already bound if something was buffered 89 | assert not self.raw_jdata 90 | if self.keep_unused_data: 91 | self.jo.append(jdata) 92 | 93 | def next_jdata(self, jdata): 94 | if self.keep_raw_data and self.raw_jdata: 95 | jdata["raw_data"] = self.raw_jdata 96 | self.jo.append(jdata) 97 | self.raw_jdata = [] 98 | 99 | def header(self): 100 | # comment("Generated by usbrply-serial FT2232C") 101 | pass 102 | 103 | def footer(self): 104 | pass 105 | 106 | def handleControlRead(self, d): 107 | if d['bRequestType'] != FTDI_DEVICE_IN_REQTYPE: 108 | self.passthrough_jdata(d) 109 | return 110 | request = req_i2s[d['bRequest']] 111 | self.verbose and print("bRequest", request) 112 | if request == "POLL_MODEM_STATUS": 113 | assert d['wLength'] == 2 114 | buff = bytearray(binascii.unhexlify(d['data'])) 115 | assert buff[1] & 0x0F == 0 116 | j = { 117 | # buff1 118 | # Clear to send 119 | 'CTS': bool(buff[1] & 0x10), 120 | # Data set ready 121 | 'DTS': bool(buff[1] & 0x20), 122 | # Ring indicator 123 | 'RI': bool(buff[1] & 0x40), 124 | # Receive line signal detect 125 | 'RLSD': bool(buff[1] & 0x80), 126 | # buff0 127 | # Data ready 128 | 'DR': bool(buff[0] & 0x01), 129 | # Overrun error 130 | 'OE': bool(buff[0] & 0x02), 131 | # Parity error 132 | 'PE': bool(buff[0] & 0x04), 133 | # Framing error 134 | 'FE': bool(buff[0] & 0x08), 135 | # Break interrupt 136 | 'BI': bool(buff[0] & 0x10), 137 | # Transmitter holding register 138 | 'THRE': bool(buff[0] & 0x20), 139 | # Transmitter empty 140 | 'TEMT': bool(buff[0] & 0x40), 141 | # Error in RCVR FIFO 142 | 'ERR': bool(buff[0] & 0x80), 143 | } 144 | else: 145 | j = {} 146 | j["type"] = request 147 | self.verbose and print("%s: FIXME" % (request, )) 148 | 149 | j["type"] = request 150 | j['rw'] = 'r' 151 | j['interface'] = interface_i2str(d["wIndex"] & 0xFF) 152 | self.add_raw_jdata(d) 153 | self.next_jdata(j) 154 | 155 | def handleControlWrite(self, d): 156 | """ 157 | ('bRequest', 'SET_EVENT_CHAR') 158 | SET_EVENT_CHAR: FIXME 159 | {'wValue': 0, 'data': '', 'bRequest': 7, 'packn': (301, 302), 'type': 'controlWrite', 'bRequestType': 64, 'wIndex': 1} 160 | ('bRequest', 'SET_ERROR_CHAR') 161 | SET_ERROR_CHAR: FIXME 162 | {'wValue': 1, 'data': '', 'bRequest': 9, 'packn': (303, 304), 'type': 'controlWrite', 'bRequestType': 64, 'wIndex': 1} 163 | ('bRequest', 'SET_LATENCY_TIMER') 164 | SET_LATENCY_TIMER: FIXME 165 | {'wValue': 0, 'data': '', 'bRequest': 2, 'packn': (305, 306), 'type': 'controlWrite', 'bRequestType': 64, 'wIndex': 257} 166 | ('bRequest', 'SET_FLOW_CTRL') 167 | {'wValue': 0, 'data': '', 'bRequest': 11, 'packn': (309, 310), 'type': 'controlWrite', 'bRequestType': 64, 'wIndex': 1} 168 | ('bRequest', 'SET_BITMODE') 169 | SET_BITMODE: FIXME 170 | {'wValue': 512, 'data': '', 'bRequest': 11, 'packn': (311, 312), 'type': 'controlWrite', 'bRequestType': 64, 'wIndex': 1} 171 | ('bRequest', 'SET_BITMODE') 172 | SET_BITMODE: FIXME 173 | {'wValue': 2, 'data': '', 'bRequest': 9, 'packn': (621, 622), 'type': 'controlWrite', 'bRequestType': 64, 'wIndex': 1} 174 | ('bRequest', 'SET_LATENCY_TIMER') 175 | SET_LATENCY_TIMER: FIXME 176 | """ 177 | if d['bRequestType'] != FTDI_DEVICE_OUT_REQTYPE: 178 | self.passthrough_jdata(d) 179 | return 180 | request = req_i2s[d['bRequest']] 181 | self.verbose and print("bRequest", request) 182 | 183 | def SET_FLOW_CTRL(d): 184 | flag_s2i = { 185 | "DISABLE_FLOW_CTRL": 0x0, 186 | "RTS_CTS_HS": 0x01, 187 | "DTR_DSR_HS": 0x02, 188 | "XON_XOFF_HS": 0x04, 189 | } 190 | return flags2dict(flag_s2i, d["wIndex"] >> 8) 191 | 192 | def SET_DATA(d): 193 | parity = { 194 | 0: "NONE", 195 | 1: "ODD", 196 | 2: "EVEN", 197 | 3: "MARK", 198 | 4: "SPACE", 199 | }[(d['wValue'] >> 8) & 0x7] 200 | 201 | stopbits = { 202 | 0: "1", 203 | 1: "15", 204 | 2: "2", 205 | }[(d['wValue'] >> 11) & 0x3] 206 | 207 | breakon = { 208 | 0: "OFF", 209 | 1: "ON", 210 | }[(d['wValue'] >> 14) & 0x1] 211 | 212 | j = { 213 | 'parity': parity, 214 | 'stopbits': stopbits, 215 | 'breakon': breakon, 216 | } 217 | 218 | return j 219 | 220 | def SET_EVENT_CHAR(d): 221 | """Set the special event character""" 222 | j = { 223 | "char": d['wValue'] & 0xFF, 224 | "enable": bool(d['wValue'] & 0x100), 225 | } 226 | return j 227 | 228 | def SET_ERROR_CHAR(d): 229 | """Set error character""" 230 | j = { 231 | "char": d['wValue'] & 0xFF, 232 | "enable": bool(d['wValue'] & 0x100), 233 | } 234 | return j 235 | 236 | def SET_LATENCY_TIMER(d): 237 | """keeps data in the internal buffer if the buffer is not full yet""" 238 | latency = d['wValue'] 239 | assert 1 <= latency <= 255 240 | j = { 241 | 'latency': latency, 242 | } 243 | return j 244 | 245 | def SET_BITMODE(d): 246 | """ 247 | FT_SetBitMode - mode = 0, mask = 0 - Reset the MPSSE controller. Perform a general reset on 248 | the MPSSE, not the port itself 249 | 250 | FT_SetBitMode - mode = 2, mask = 0 - Enable the MPSSE controller. Pin directions are set later 251 | through the MPSSE commands. 252 | """ 253 | flag_s2i = { 254 | "RESET": 0x0, 255 | "BITBANG": 0x01, 256 | "MPSSE": 0x02, 257 | "SYNCBB": 0x04, 258 | "MCU": 0x08, 259 | "OPTO": 0x10, 260 | "CBUS": 0x20, 261 | "SYNCFF": 0x40, 262 | "FT1284": 0x80, 263 | } 264 | j = flags2dict(flag_s2i, d["wValue"] >> 8) 265 | j['bitmask'] = d["wValue"] & 0xFF 266 | return j 267 | 268 | def DEFAULT(d): 269 | j = {} 270 | self.verbose and print("%s: FIXME" % (request, )) 271 | return j 272 | 273 | j = { 274 | "SET_FLOW_CTRL": SET_FLOW_CTRL, 275 | "SET_DATA": SET_DATA, 276 | "SET_EVENT_CHAR": SET_EVENT_CHAR, 277 | "SET_ERROR_CHAR": SET_ERROR_CHAR, 278 | "SET_LATENCY_TIMER": SET_LATENCY_TIMER, 279 | "SET_BITMODE": SET_BITMODE, 280 | }.get(request, DEFAULT)(d) 281 | 282 | j["type"] = request 283 | j['rw'] = 'w' 284 | j['interface'] = interface_i2str(d["wIndex"] & 0xFF) 285 | self.add_raw_jdata(d) 286 | self.next_jdata(j) 287 | 288 | def handleBulkWrite(self, d): 289 | interface = { 290 | 0x02: 0, 291 | 0x04: 1, 292 | }[d["endp"]] 293 | 294 | self.add_raw_jdata(d) 295 | j = { 296 | "type": "write", 297 | "interface": interface, 298 | "data": d["data"], 299 | } 300 | if self.emit_adata: 301 | j["adata"] = util.to_pintable_str(binascii.unhexlify(d["data"])) 302 | self.next_jdata(j) 303 | 304 | def handleBulkRead(self, d): 305 | assert len(d["data"]) % 2 == 0 306 | # json encodes in hex 307 | # protocol itself encodes in hex 308 | data = binascii.unhexlify(d["data"]) 309 | 310 | interface = { 311 | 0x81: 0, 312 | 0x83: 1, 313 | }.get(d["endp"]) 314 | if interface is None: 315 | self.verbose and print( 316 | "WARNING: skipping unknown interface packet") 317 | self.passthrough_jdata(d) 318 | return 319 | 320 | prefix = data[0:2] 321 | data = data[2:] 322 | # meh lots of these and not sure what they mean 323 | # should look into these but just ignore for now 324 | # assert prefix == "\x42\x60" or prefix == "\x32\x60" or prefix == "\x32\x00", d 325 | 326 | if len(data) == 0 and not self.keep_empty_txrx: 327 | self.passthrough_jdata(d) 328 | else: 329 | self.add_raw_jdata(d) 330 | j = { 331 | "type": "read", 332 | "interface": interface, 333 | "data": binascii.hexlify(data), 334 | "prefix": prefix, 335 | } 336 | if self.emit_adata: 337 | j["adata"] = util.to_pintable_str(data) 338 | self.next_jdata(j) 339 | 340 | def run(self, j): 341 | self.header() 342 | 343 | for di, d in enumerate(j["data"]): 344 | if 0 and di > 500: 345 | print("debug break") 346 | break 347 | if d["type"] == "bulkWrite": 348 | self.handleBulkWrite(d) 349 | elif d["type"] == "bulkRead": 350 | self.handleBulkRead(d) 351 | elif d["type"] == "controlRead": 352 | self.handleControlRead(d) 353 | elif d["type"] == "controlWrite": 354 | self.handleControlWrite(d) 355 | else: 356 | self.passthrough_jdata(d) 357 | 358 | # Should be flushed now 359 | assert not self.raw_jdata 360 | 361 | self.footer() 362 | j = { 363 | "data": self.jo, 364 | } 365 | 366 | return j 367 | -------------------------------------------------------------------------------- /usbrply/cprinter.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from usbrply import printer 3 | from usbrply import parsers 4 | from .printer import Printer, indented, indent_inc, indent_dec, get_indent, indent_reset 5 | import sys 6 | import binascii 7 | from . import usb 8 | from .util import myord 9 | 10 | 11 | def comment(s): 12 | indented('//%s' % (s, )) 13 | 14 | 15 | def bytes2AnonArray(buf): 16 | # Sample 17 | # (char[]){0x05, 0x40, 0x07, 0x3A} 18 | 19 | ret = "(char[]){" 20 | 21 | chunk_size = 16 22 | for start in range(0, len(buf), chunk_size): 23 | # Break up long line 24 | if len(buf) > chunk_size: 25 | if start: 26 | ret += "," 27 | ret += "\n " + get_indent() 28 | nums = ["0x%02X" % x for x in buf[start:start + 16]] 29 | ret += ", ".join(nums) 30 | return ret + "}" 31 | 32 | 33 | # Fall back to t_urb for original pcap format on Linux? 34 | def gett(d): 35 | if "t" in d["submit"]: 36 | return d["submit"]["t"] 37 | elif "t_urb" in d["submit"]: 38 | return d["submit"]["t"] 39 | else: 40 | raise Exception( 41 | "Requested sleep but couldn't establish time reference") 42 | 43 | 44 | class LibusbCPrinter(Printer): 45 | def __init__(self, argsj, verbose=None): 46 | Printer.__init__(self, argsj) 47 | self.prevd = None 48 | self.wrapper = argsj.get("wrapper", False) 49 | self.sleep = argsj.get("sleep", False) 50 | self.packet_numbers = argsj.get("packet_numbers", True) 51 | # FIXME 52 | self.vid = None 53 | self.pid = None 54 | if verbose is None: 55 | verbose = argsj.get("verbose", False) 56 | self.verbose = verbose 57 | self.argsj = argsj 58 | 59 | def print_imports(self): 60 | print('''\ 61 | /* 62 | Ubuntu quick start: 63 | sudo apt-get install -y libusb-1.0-0-dev 64 | usbrply -l --wrapper my.pcapng ->main.c 65 | gcc -I/usr/include/libusb-1.0 main.c -lusb-1.0 66 | ./main 67 | */ 68 | #include 69 | #include 70 | #include 71 | ''', 72 | file=printer.print_file) 73 | 74 | def print_macros(self): 75 | print('''\ 76 | //Automatically calculate expected buffer size 77 | #define VALIDATE_READ(expected, actual, actual_size, msg) \ 78 | validate_read(expected, sizeof(expected), actual, actual_size, msg) 79 | #define RET_ON_ERR(_x) do { usbret = _x; if (usbret < 0) { return usbret; } } while(0) 80 | #define RET_ON_XFER_ERR(_x, _nexpect) do { \ 81 | usbret = _x; \ 82 | if (usbret < 0) { \ 83 | return usbret; \ 84 | } \ 85 | if (bytes_transferred != (_nexpect)) { \ 86 | return -1; \ 87 | } \ 88 | } while(0) 89 | ''', 90 | file=printer.print_file) 91 | 92 | def print_wrapper_header(self): 93 | print('''\ 94 | const char *hexlify(const void *buf, size_t size) { 95 | //FIXME 96 | return NULL; 97 | } 98 | 99 | int validate_read(const uint8_t *expected, size_t expected_size, const uint8_t *actual, size_t actual_size, const char *msg) { 100 | if (expected_size != actual_size || memcmp(expected, actual, expected_size)) { 101 | printf("Failed %s\\n", msg); 102 | //printf(" Expected; %s\\n", hexlify(expected, expected_size)); 103 | //printf(" Actual: %s\\n", hexlify(actual, actual_size)); 104 | return -1; 105 | } 106 | return 0; 107 | } 108 | 109 | ''', 110 | file=printer.print_file) 111 | print('int replay(libusb_device_handle *devh) {', 112 | file=printer.print_file) 113 | indent_inc() 114 | print('''\ 115 | //Error and/or size 116 | int usbret = 0; 117 | //XXX: size this automatically / better? Just be big for now 118 | uint8_t buff[16384]; 119 | (void)buff; 120 | int timeout = 0; 121 | unsigned bytes_transferred = 0; 122 | ''', 123 | file=printer.print_file) 124 | 125 | def header(self): 126 | comment("Generated by usbrply") 127 | comment("***WARNING: libusb-c is alpha level quality***") 128 | comment("cmd: %s" % (' '.join(sys.argv), )) 129 | indented("") 130 | 131 | if self.wrapper: 132 | self.print_imports() 133 | print("", file=printer.print_file) 134 | self.print_macros() 135 | print("", file=printer.print_file) 136 | self.print_wrapper_header() 137 | else: 138 | self.print_macros() 139 | print("", file=printer.print_file) 140 | 141 | def footer(self): 142 | if not self.wrapper: 143 | return 144 | print(''' 145 | } 146 | 147 | static struct libusb_device_handle *get_device(struct libusb_context *usb_ctx, uint16_t vid, uint16_t pid) { 148 | struct libusb_device **devices = NULL; 149 | ssize_t count = 0; 150 | 151 | count = libusb_get_device_list(usb_ctx, &devices); 152 | if (count < 0) { 153 | printf("Error getting device list: %ld (%s)\\n", count, libusb_error_name(count)); 154 | return NULL; 155 | } 156 | 157 | printf("Scanning for devices...\\n"); 158 | for (int devi = 0; devi < count; ++devi) { 159 | struct libusb_device *dev = devices[devi]; 160 | struct libusb_device_descriptor desc; 161 | struct libusb_device_handle *devh = NULL; 162 | int usbret; 163 | 164 | usbret = libusb_get_device_descriptor(dev, &desc); 165 | if (usbret != 0) { 166 | printf("Error reading device descriptor: %d (%s)\\n", usbret, libusb_error_name(usbret)); 167 | libusb_free_device_list(devices, 1); 168 | return NULL; 169 | } 170 | if (desc.idVendor == vid && desc.idProduct == pid) { 171 | printf("Found device\\n"); 172 | printf("Bus %03i Device %03i: ID %04x:%04x\\n", 173 | libusb_get_bus_number(dev), libusb_get_device_address(dev), 174 | desc.idVendor, desc.idProduct); 175 | usbret = libusb_open(dev, &devh); 176 | if (usbret != 0) { 177 | printf("Error opening device: %d (%s)\\n", usbret, libusb_error_name(usbret)); 178 | libusb_free_device_list(devices, 1); 179 | return NULL; 180 | } 181 | libusb_free_device_list(devices, 1); 182 | return devh; 183 | } 184 | } 185 | printf("Failed to find USB device\\n"); 186 | libusb_free_device_list(devices, 1); 187 | return NULL; 188 | } 189 | 190 | int main(int argc, char **argv) { 191 | uint16_t vid = ''' + "0x%04X" % self.vid + '''; 192 | uint16_t pid = ''' + "0x%04X" % self.pid + '''; 193 | struct libusb_context *usb_ctx = NULL; 194 | libusb_device_handle *devh= NULL; 195 | int err; 196 | int bytes_transferred; 197 | (void)bytes_transferred; 198 | 199 | int ret = libusb_init(&usb_ctx); 200 | if (ret < 0 || usb_ctx == NULL) { 201 | printf("Error initializing libusb: %s\\n", libusb_error_name(ret)); 202 | return 1; 203 | } 204 | devh = get_device(usb_ctx, vid, pid); 205 | 206 | /* 207 | err = libusb_set_configuration(devh, 1); 208 | if (err != 0) { 209 | printf("Error setting configuration: %d (%s)\\n", err, libusb_error_name(err)); 210 | libusb_close(devh); 211 | libusb_exit(usb_ctx); 212 | return 1; 213 | } 214 | */ 215 | err = libusb_claim_interface(devh, 0); 216 | if (err < 0) { 217 | printf("Error claiming interface: %d (%s)\\n", err, libusb_error_name(err)); 218 | libusb_close(devh); 219 | libusb_exit(usb_ctx); 220 | return 1; 221 | } 222 | //dev.resetDevice() 223 | replay(devh); 224 | } 225 | ''', 226 | file=printer.print_file) 227 | 228 | def packet_number_str(self, d): 229 | if self.packet_numbers: 230 | return "packet %s/%s" % (d["submit"]["packn"], 231 | d["complete"]["packn"]) 232 | else: 233 | # TODO: consider counting instead of by captured index 234 | return "packet" 235 | 236 | def parse_data(self, d): 237 | if self.sleep and self.prevd and d["type"] != "comment": 238 | try: 239 | dt = gett(d) - gett(self.prevd) 240 | except KeyError: 241 | raise ValueError("Input JSON does not support timestamps") 242 | if dt >= 0.001: 243 | indented('time.sleep(%.3f)' % (dt, )) 244 | 245 | if d["type"] == "comment": 246 | comment(d["v"]) 247 | return 248 | 249 | packet_numbering = self.packet_number_str(d) 250 | 251 | if "comments" in d: 252 | for c in d["comments"]: 253 | comment(c) 254 | 255 | if d["type"] == "controlRead": 256 | """ 257 | int LIBUSB_CALL libusb_control_transfer(libusb_device_handle *dev_handle, 258 | uint8_t request_type, uint8_t bRequest, uint16_t wValue, uint16_t wIndex, 259 | unsigned char *data, uint16_t wLength, unsigned int timeout); 260 | """ 261 | 262 | if d["wLength"]: 263 | data = "buff" 264 | else: 265 | data = "NULL" 266 | 267 | indented( 268 | "RET_ON_ERR(libusb_control_transfer(devh, 0x%02X, 0x%02X, 0x%04X, 0x%04X, %s, 0x%04X, timeout));" 269 | % (d["bRequestType"], d["bRequest"], d["wValue"], d["wIndex"], 270 | data, d["wLength"])) 271 | indented( 272 | "RET_ON_ERR(validate_read(%s, %u, buff, usbret, \"%s\"));" % ( 273 | bytes2AnonArray(binascii.unhexlify(d["data"])), 274 | len(d["data"]), 275 | packet_numbering, 276 | )) 277 | elif d["type"] == "controlWrite": 278 | data_str = bytes2AnonArray(binascii.unhexlify( 279 | d["data"])) if d["data"] else "NULL" 280 | indented( 281 | "RET_ON_ERR(libusb_control_transfer(devh, 0x%02X, 0x%02X, 0x%04X, 0x%04X, %s, 0x%04X, timeout));" 282 | % (d["bRequestType"], d["bRequest"], d["wValue"], d["wIndex"], 283 | data_str, len(d["data"]))) 284 | 285 | elif d["type"] == "bulkRead": 286 | indented( 287 | "RET_ON_ERR(libusb_bulk_transfer(devh, 0x%02X, buff, 0x%04X, &bytes_transferred, timeout));" 288 | % (d["endp"], len(d["data"]))) 289 | if len(d["data"]): 290 | indented( 291 | "RET_ON_ERR(validate_read(%s, %u, buff, bytes_transferred, \"%s\"));" 292 | % ( 293 | bytes2AnonArray(binascii.unhexlify(d["data"])), 294 | len(d["data"]), 295 | packet_numbering, 296 | )) 297 | elif d["type"] == "bulkWrite": 298 | data_str = bytes2AnonArray(binascii.unhexlify( 299 | d["data"])) if d["data"] else "NULL" 300 | indented( 301 | "RET_ON_XFER_ERR(libusb_bulk_transfer(devh, 0x%02X, %s, 0x%04X, &bytes_transferred, timeout), 0x%04X);" 302 | % (d["endp"], data_str, len(d["data"]), len(d["data"]))) 303 | elif d["type"] == "interruptIn": 304 | indented( 305 | "RET_ON_ERR(libusb_interrupt_transfer(devh, 0x%02X, buff, 0x%04X, &bytes_transferred, timeout));" 306 | % (d["endp"], len(d["data"]))) 307 | if len(d["data"]): 308 | indented( 309 | "RET_ON_ERR(validate_read(%s, %u, buff, bytes_transferred, \"%s\"));" 310 | % ( 311 | bytes2AnonArray(binascii.unhexlify(d["data"])), 312 | len(d["data"]), 313 | packet_numbering, 314 | )) 315 | elif d["type"] == "interruptOut": 316 | data_str = bytes2AnonArray(binascii.unhexlify( 317 | d["data"])) if d["data"] else "NULL" 318 | indented( 319 | "RET_ON_XFER_ERR(libusb_interrupt_transfer(devh, 0x%02X, %s, 0x%04X, &bytes_transferred, timeout), 0x%04X);" 320 | % (d["endp"], data_str, len(d["data"]), len(d["data"]))) 321 | elif d["type"] == "irpInfo": 322 | comment("IRP_INFO(): func %s" % 323 | (d["submit"]["urb"]["usb_func_str"], )) 324 | elif d["type"] == "abortPipe": 325 | comment("ABORT_PIPE()") 326 | else: 327 | if self.verbose: 328 | print("LibusbPyPrinter WARNING: dropping %s" % (d["type"], )) 329 | 330 | # these aren't event added to JSON right now 331 | # print('%s# WARNING: omitting interrupt' % (indent,)) 332 | 333 | if d["type"] != "comment": 334 | self.prevd = d 335 | 336 | def run(self, jgen): 337 | indent_reset() 338 | self.header() 339 | 340 | # Last wire command (ie non-comment) 341 | # Used to optionally generate timing 342 | self.prevd = None 343 | 344 | # Convert generator into static JSON 345 | # caches vid/pid among other things 346 | j = parsers.jgen2j(jgen) 347 | 348 | for d in j["data"]: 349 | self.parse_data(d) 350 | 351 | if self.wrapper and (self.vid is None or self.pid is None): 352 | if len(j["device2vidpid"]) != 1: 353 | raise Exception( 354 | "Failed to guess vid/pid: found %u device entries" % 355 | len(j["device2vidpid"])) 356 | for (vid, pid) in j["device2vidpid"].values(): 357 | self.vid = vid 358 | self.pid = pid 359 | if self.wrapper: 360 | self.footer() 361 | -------------------------------------------------------------------------------- /test/test_misc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import usbrply.parsers 4 | import usbrply.printers 5 | from usbrply import printer 6 | from usbrply import parsers 7 | from usbrply import filters 8 | from usbrply.serial.parsers import FT2232CParser 9 | from usbrply.serial.printers import JSONSPrinter 10 | from usbrply.serial.mpsse import MPSSEParser 11 | from usbrply import util 12 | import unittest 13 | import os 14 | import json 15 | import subprocess 16 | import shutil 17 | import binascii 18 | 19 | 20 | def printj(j): 21 | """For debugging""" 22 | print(json.dumps(j, sort_keys=True, indent=4, separators=(',', ': '))) 23 | 24 | 25 | def find_packets(j): 26 | """Return non-comment packets in json""" 27 | ret = [] 28 | for packet in j["data"]: 29 | if packet["type"] == "comment": 30 | continue 31 | ret.append(packet) 32 | return ret 33 | 34 | 35 | def find_packet(j): 36 | """Return the single packet in json""" 37 | packets = find_packets(j) 38 | assert len(packets) == 1, len(packets) 39 | return packets[0] 40 | 41 | 42 | def run_printers_json(fn, argsj): 43 | j = parsers.jgen2j(usbrply.parsers.pcap2json(fn, argsj=argsj)) 44 | usbrply.printers.run("libusb-py", 45 | usbrply.parsers.pcap2json(fn, argsj=argsj), 46 | argsj=argsj) 47 | usbrply.printers.run("libusb-c", 48 | usbrply.parsers.pcap2json(fn, argsj=argsj), 49 | argsj=argsj) 50 | return j 51 | 52 | 53 | class TestCommon(unittest.TestCase): 54 | def setUp(self): 55 | """Call before every test case.""" 56 | print("") 57 | print("") 58 | print("") 59 | print("Start " + self._testMethodName) 60 | self.verbose = os.getenv("VERBOSE", "N") == "Y" 61 | printer.print_file = open("/dev/null", "w") 62 | self.argsj = {"verbose": self.verbose} 63 | self.tmp_dir = "/tmp/usbrply" 64 | if os.path.exists(self.tmp_dir): 65 | shutil.rmtree(self.tmp_dir) 66 | os.mkdir(self.tmp_dir) 67 | 68 | def tearDown(self): 69 | """Call after every test case.""" 70 | if printer.print_file: 71 | printer.print_file.close() 72 | if os.path.exists(self.tmp_dir): 73 | shutil.rmtree(self.tmp_dir) 74 | 75 | 76 | class TestCore(TestCommon): 77 | def print_all(self, fn): 78 | j = parsers.jgen2j(usbrply.parsers.pcap2json(fn, argsj=self.argsj)) 79 | usbrply.printers.run("libusb-py", 80 | usbrply.parsers.pcap2json(fn, argsj=self.argsj), 81 | argsj=self.argsj) 82 | if 0: 83 | usbrply.printers.run("libusb-c", 84 | usbrply.parsers.pcap2json(fn, 85 | argsj=self.argsj), 86 | argsj=self.argsj) 87 | return j 88 | 89 | """ 90 | ************************************************************************* 91 | misc tests 92 | ************************************************************************* 93 | """ 94 | 95 | def test_print_json(self): 96 | usbrply.printers.run( 97 | "json", 98 | usbrply.parsers.pcap2json("test/data/lin_misc.pcapng", 99 | argsj=self.argsj), 100 | argsj=self.argsj) 101 | 102 | """ 103 | ************************************************************************* 104 | pyprinter tests 105 | ************************************************************************* 106 | """ 107 | 108 | def test_pyprinter_lin(self): 109 | usbrply.printers.run( 110 | "libusb-py", 111 | usbrply.parsers.pcap2json("test/data/lin_misc.pcapng", 112 | argsj=self.argsj), 113 | argsj=self.argsj) 114 | 115 | def test_pyprinter_lin_wrapped(self): 116 | fn = "/tmp/usbrply/tmp.py" 117 | printer.print_file.close() 118 | with open(fn, "w") as printer.print_file: 119 | self.argsj["wrapper"] = True 120 | parsed = usbrply.parsers.pcap2json("test/data/lin_misc.pcapng", 121 | argsj=self.argsj) 122 | # filters.append("setup") 123 | # filters.append("commenter") 124 | filtered = filters.run(["vidpid"], parsed, self.argsj) 125 | usbrply.printers.run("libusb-py", filtered, argsj=self.argsj) 126 | 127 | subprocess.check_call("python3 %s -h >/dev/null" % fn, shell=True) 128 | 129 | def test_pyprinter_win(self): 130 | usbrply.printers.run( 131 | "libusb-py", 132 | usbrply.parsers.pcap2json("test/data/win_misc.pcapng", 133 | argsj=self.argsj), 134 | argsj=self.argsj) 135 | 136 | def test_pyprinter_win_wrapped(self): 137 | fn = "/tmp/usbrply/tmp.py" 138 | printer.print_file.close() 139 | 140 | with open(fn, "w") as printer.print_file: 141 | self.argsj["wrapper"] = True 142 | parsed = usbrply.parsers.pcap2json("test/data/win_misc.pcapng", 143 | argsj=self.argsj) 144 | # filters.append("setup") 145 | # filters.append("commenter") 146 | filtered = filters.run(["vidpid"], parsed, self.argsj) 147 | usbrply.printers.run("libusb-py", filtered, argsj=self.argsj) 148 | 149 | subprocess.check_call("python3 %s -h >/dev/null" % fn, shell=True) 150 | 151 | """ 152 | ************************************************************************* 153 | cprinter tests 154 | ************************************************************************* 155 | """ 156 | 157 | # FIXME: very basic right now 158 | def test_cprinter_lin(self): 159 | usbrply.printers.run("libusb-c", 160 | usbrply.parsers.pcap2json( 161 | "test/data/lin_control-out.pcapng", 162 | argsj=self.argsj), 163 | argsj=self.argsj) 164 | 165 | def test_cprinter_lin_wrapped(self): 166 | fn = "/tmp/usbrply/tmp.c" 167 | printer.print_file.close() 168 | 169 | with open(fn, "w") as printer.print_file: 170 | self.argsj["wrapper"] = True 171 | parsed = usbrply.parsers.pcap2json("test/data/lin_misc.pcapng", 172 | argsj=self.argsj) 173 | # filters.append("setup") 174 | # filters.append("commenter") 175 | filtered = filters.run(["vidpid"], parsed, self.argsj) 176 | usbrply.printers.run("libusb-c", filtered, argsj=self.argsj) 177 | 178 | if os.getenv("USBRPLY_TEST_ALL") == "Y": 179 | subprocess.check_call( 180 | "gcc -I/usr/include/libusb-1.0 %s -lusb-1.0 -o %s.out >/dev/null" 181 | % (fn, fn), 182 | shell=True) 183 | 184 | def test_cprinter_win(self): 185 | usbrply.printers.run( 186 | "libusb-c", 187 | usbrply.parsers.pcap2json("test/data/win_misc.pcapng", 188 | argsj=self.argsj), 189 | argsj=self.argsj) 190 | 191 | def test_cprinter_win_wrapped(self): 192 | fn = "/tmp/usbrply/tmp.c" 193 | printer.print_file.close() 194 | 195 | with open(fn, "w") as printer.print_file: 196 | self.argsj["wrapper"] = True 197 | parsed = usbrply.parsers.pcap2json("test/data/win_misc.pcapng", 198 | argsj=self.argsj) 199 | # filters.append("setup") 200 | # filters.append("commenter") 201 | filtered = filters.run(["vidpid"], parsed, self.argsj) 202 | usbrply.printers.run("libusb-c", filtered, argsj=self.argsj) 203 | 204 | if os.getenv("USBRPLY_TEST_ALL") == "Y": 205 | subprocess.check_call( 206 | "gcc -I/usr/include/libusb-1.0 %s -lusb-1.0 -o %s.out >/dev/null" 207 | % (fn, fn), 208 | shell=True) 209 | 210 | """ 211 | ************************************************************************* 212 | Windows packet tests 213 | ************************************************************************* 214 | """ 215 | 216 | def test_win_packets(self): 217 | """Windows large .pcap parse test""" 218 | self.print_all("test/data/win_misc.pcapng") 219 | 220 | def test_win_pipes(self): 221 | """ 222 | Verify special PIPE setup packets are handled: 223 | -URB_FUNCTION_ABORT_PIPE 224 | -URB_FUNCTION_SYNC_RESET_PIPE_AND_CLEAR_STALL 225 | 226 | Not exactly sure what this is but its at the beginning of my test capture 227 | Normally there? 228 | """ 229 | self.print_all("test/data/win_setup_pipes.pcapng") 230 | self.print_all("test/data/win_abort-pipe.pcapng") 231 | self.print_all("test/data/win_pipe-stall.pcapng") 232 | 233 | def test_win_interrupts(self): 234 | assert len( 235 | find_packets( 236 | self.print_all("test/data/win_interrupts.pcapng"))) > 1 237 | 238 | def test_win_interrupt_in(self): 239 | find_packet(self.print_all("test/data/win_interrupt-in.pcapng")) 240 | 241 | def test_win_bulk_out(self): 242 | find_packet(self.print_all("test/data/win_bulk-out.pcapng")) 243 | 244 | def test_win_bulk_in(self): 245 | find_packet(self.print_all("test/data/win_bulk-in.pcapng")) 246 | 247 | def test_win_control_in(self): 248 | find_packet(self.print_all("test/data/win_control-in.pcapng")) 249 | 250 | def test_win_control_out(self): 251 | """ 252 | Verify control out parses on Windows 253 | """ 254 | packet = find_packet( 255 | self.print_all("test/data/win_control-out_len-0.pcapng")) 256 | assert len(packet["data"]) == 0 257 | 258 | packet = find_packet( 259 | self.print_all("test/data/win_control-out.pcapng")) 260 | assert packet["data"] 261 | 262 | def test_win_irp_status(self): 263 | """ 264 | Code was failing irp's that had non-0 irp_status 265 | While success is typically 0, it's not strictly required 266 | """ 267 | self.print_all("test/data/win_irp-status-120.pcapng") 268 | """ 269 | FML 270 | https://github.com/JohnDMcMaster/usbrply/issues/70 271 | """ 272 | self.print_all("test/data/win_irp-status-neg.pcapng") 273 | 274 | """ 275 | ************************************************************************* 276 | Linux packet tests 277 | ************************************************************************* 278 | """ 279 | 280 | def test_lin_packets(self): 281 | """Linux .pcap parse test""" 282 | self.print_all("test/data/lin_misc.pcapng") 283 | 284 | def test_parse_lin_setup(self): 285 | """Linux .pcap parse test""" 286 | self.print_all("test/data/lin_setup.pcapng") 287 | 288 | def test_lin_control_in(self): 289 | find_packet(self.print_all("test/data/lin_control-in.pcapng")) 290 | 291 | def test_lin_control_out(self): 292 | find_packet(self.print_all("test/data/lin_control-out.pcapng")) 293 | 294 | """ 295 | def test_lin_interrupt_in(self): 296 | # FIXME: this file is bad, get new 297 | # find_packet(self.print_all("test/data/lin_interrupt-in.pcapng")) 298 | """ 299 | 300 | def test_lin_interrupt_out(self): 301 | find_packet(self.print_all("test/data/lin_interrupt-out.pcapng")) 302 | 303 | 304 | def data_serj2usbj(serj_data): 305 | """ 306 | Convert USB serial JSON back to original USB JSON 307 | """ 308 | ret = [] 309 | for data in serj_data: 310 | if "raw_data" in data: 311 | ret += data["raw_data"] 312 | else: 313 | ret.append(data) 314 | return ret 315 | 316 | 317 | def tx_string_data(serj): 318 | ret = "" 319 | for data in serj["data"]: 320 | if data["type"] == "write": 321 | ret += util.tostr(binascii.unhexlify(data["data"])) 322 | return ret 323 | 324 | 325 | def tx_string_adata(serj): 326 | ret = "" 327 | for data in serj["data"]: 328 | if data["type"] == "write": 329 | ret += data["adata"] 330 | return ret 331 | 332 | 333 | class TestSerial(TestCommon): 334 | def test_tx(self): 335 | fn = "test/data/ftdi_tx-l.pcapng" 336 | usbj = parsers.jgen2j(usbrply.parsers.pcap2json(fn, argsj=self.argsj)) 337 | serj = FT2232CParser(argsj=self.argsj).run(usbj) 338 | assert "l" == tx_string_adata(serj) 339 | assert "l" == tx_string_data(serj) 340 | 341 | def test_tx_multi(self): 342 | fn = "test/ftdi/2022-04-26_01_ft232-goodfet_tx-hello.pcapng" 343 | usbj = parsers.jgen2j(usbrply.parsers.pcap2json(fn, argsj=self.argsj)) 344 | serj = FT2232CParser(argsj=self.argsj).run(usbj) 345 | assert "hello" == tx_string_adata(serj) 346 | assert "hello" == tx_string_data(serj) 347 | 348 | def test_packet_loopback(self): 349 | """ 350 | Convert pcap to serial format and then extract original pcap 351 | """ 352 | fn = "test/ftdi/2022-04-26_01_ft232-goodfet_tx-hello.pcapng" 353 | self.argsj["keep_everything"] = True 354 | 355 | usbj = parsers.jgen2j(usbrply.parsers.pcap2json(fn, argsj=self.argsj)) 356 | serj = FT2232CParser(argsj=self.argsj).run(usbj) 357 | data_back = data_serj2usbj(serj["data"]) 358 | assert usbj["data"] == data_back 359 | 360 | def test_mpsee(self): 361 | fn = "test/ftdi/2022-04-26_01_ft232-goodfet_tx-hello.pcapng" 362 | usbj = parsers.jgen2j(usbrply.parsers.pcap2json(fn, argsj=self.argsj)) 363 | serj = FT2232CParser(argsj=self.argsj).run(usbj) 364 | mpseej = MPSSEParser(argsj=self.argsj).run(serj) 365 | 366 | 367 | if __name__ == "__main__": 368 | unittest.main() # run all tests 369 | -------------------------------------------------------------------------------- /usbrply/lin_pcap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from .usb import * 4 | from .util import hexdump, tostr 5 | from .com_pcap import PcapGen 6 | 7 | import sys 8 | import binascii 9 | import struct 10 | from collections import namedtuple 11 | import os 12 | import errno 13 | 14 | 15 | # When we get an IN request we may process packets in between 16 | class PendingRX: 17 | def __init__(self): 18 | # Unprocessed packet bytes 19 | 20 | self.raw = None 21 | #usb_urb_t urb 22 | self.urb = None 23 | self.urbts = None 24 | #usb_ctrlrequest m_ctrl 25 | # Only applies to control requests 26 | self.m_ctrl = None 27 | self.packet_number = 0 28 | 29 | # uint8_t *m_data_out 30 | self.m_data_out = None 31 | 32 | 33 | class payload_bytes_type_t: 34 | def __init__(self): 35 | self.req_in = 0 36 | self.req_in_last = None 37 | self.in_ = 0 38 | self.in_last = None 39 | 40 | self.req_out = 0 41 | self.req_out_last = None 42 | self.out = 0 43 | self.out_last = None 44 | 45 | 46 | def update_delta(pb): 47 | pb.req_in_last = pb.req_in 48 | pb.in_last = pb.in_ 49 | 50 | pb.req_out_last = pb.req_out 51 | pb.out_last = pb.out 52 | 53 | 54 | ''' 55 | struct usb_ctrlrequest { 56 | __u8 bRequestType; 57 | __u8 bRequest; 58 | __le16 wValue; 59 | __le16 wIndex; 60 | __le16 wLength; 61 | } __attribute__ ((packed)); 62 | ''' 63 | usb_ctrlrequest_nt = namedtuple( 64 | 'usb_ctrlrequest', 65 | ( 66 | 'bRequestType', 67 | 'bRequest', 68 | 'wValue', 69 | 'wIndex', 70 | 'wLength', 71 | # FIXME: what exactly are these? 72 | 'res')) 73 | usb_ctrlrequest_fmt = ' self.max_packet: 219 | # print("# Skipping packet %d" % (self.cur_packn)) 220 | return 221 | 222 | if caplen != len(packet): 223 | print("packet %s: malformed, caplen %d != len %d", self.pktn_str(), 224 | caplen, len(packet)) 225 | return 226 | if self.verbose: 227 | print('loop_cb, len: %d' % len(packet)) 228 | 229 | self.printv("Length %u" % (len(packet), )) 230 | if len(packet) < usb_urb_sz: 231 | self.verbose and hexdump(packet) 232 | raise ValueError("Packet size %d is not min size %d" % 233 | (len(packet), usb_urb_sz)) 234 | 235 | # caplen is actual length, len is reported 236 | self.urb_raw = packet 237 | self.urb = usb_urb(packet[0:usb_urb_sz]) 238 | self.urbts = ts 239 | dat_cur = packet[usb_urb_sz:] 240 | 241 | self.arg_device = max(self.arg_device, self.urb.device) 242 | 243 | def process_submit(self, dat_cur): 244 | # Find the matching submit request 245 | if self.urb.transfer_type == URB_CONTROL: 246 | self.processControlSubmit(dat_cur) 247 | elif self.urb.transfer_type == URB_BULK: 248 | self.processBulkSubmit(dat_cur) 249 | elif self.urb.transfer_type == URB_INTERRUPT: 250 | self.processInterruptSubmit(dat_cur) 251 | else: 252 | self.gwarning("packet %s: unhandled type 0x%02X" % 253 | (self.pktn_str(), self.urb.type)) 254 | 255 | def loop_cb(self, caplen, packet, ts): 256 | packet = bytearray(packet) 257 | self.cur_packn += 1 258 | if self.cur_packn < self.min_packet or self.cur_packn > self.max_packet: 259 | # print("# Skipping packet %d" % (self.cur_packn)) 260 | return 261 | if self.verbose: 262 | print("") 263 | print("") 264 | print("") 265 | print('LIN PACKET %s' % (self.cur_packn, )) 266 | 267 | if caplen != len(packet): 268 | print("packet %s: malformed, caplen %d != len %d", self.pktn_str(), 269 | caplen, len(packet)) 270 | return 271 | if self.verbose: 272 | print('Len: %d' % len(packet)) 273 | 274 | self.printv("Length %u" % (len(packet), )) 275 | if len(packet) < usb_urb_sz: 276 | hexdump(packet) 277 | raise ValueError("Packet size %d is not min size %d" % 278 | (len(packet), usb_urb_sz)) 279 | 280 | # caplen is actual length, len is reported 281 | self.urb_raw = packet 282 | self.urb = usb_urb(packet[0:usb_urb_sz]) 283 | dat_cur = packet[usb_urb_sz:] 284 | 285 | # Main packet filtering 286 | # Drop if not specified device 287 | if self.arg_device is not None and self.urb.device != self.arg_device: 288 | if self.verbose: 289 | print('packet %s: want device %s, got %s' % 290 | (self.pktn_str(), self.arg_device, self.urb.device)) 291 | self.dev_drops += 1 292 | return 293 | self.rel_pkt += 1 294 | 295 | if self.verbose: 296 | print("Header size: %lu" % (usb_urb_sz, )) 297 | print_urb(self.urb) 298 | 299 | if self.urb.type == URB_ERROR: 300 | print("oh noes!") 301 | if self.arg_halt: 302 | sys.exit(1) 303 | 304 | if self.urb.type == URB_COMPLETE: 305 | if self.verbose: 306 | print('Pending completes (%d):' % 307 | (len(self.pending_complete), )) 308 | for k in self.pending_complete: 309 | print(' 0x%016lX' % (k, )) 310 | # for some reason usbmon will occasionally give packets out of order 311 | if not self.urb.id in self.pending_complete: 312 | # Interrupts can generate these 313 | if self.verbose: 314 | self.gwarning( 315 | "Packet %s missing submit. URB ID: 0x%016lX" % 316 | (self.pktn_str(), self.urb.id)) 317 | else: 318 | self.process_complete(self.pending_complete[self.urb.id], 319 | self.urb, dat_cur) 320 | 321 | elif self.urb.type == URB_SUBMIT: 322 | self.process_submit(dat_cur) 323 | 324 | # Should have either generated no comments or attached them 325 | assert len(self.pcomments) == 0, ("Packet comments but no packets", 326 | self.pcomments) 327 | self.submit = None 328 | self.urb = None 329 | 330 | def pktn_str(self): 331 | if self.arg_rel_pkt: 332 | return self.rel_pkt 333 | else: 334 | return self.cur_packn 335 | 336 | def process_complete(self, pending_rx, urb_complete, dat_cur): 337 | """ 338 | Warning: this may be called with current urb as either the submit or the complete 339 | Normalize here which may swap self.urb 340 | """ 341 | # assert type(pending_rx) is PendingRX, type(pending_rx) 342 | self.submit = pending_rx 343 | self.urb = urb_complete 344 | 345 | # Discarded? 346 | if self.submit is not None: 347 | self.packnum() 348 | 349 | # What was EREMOTEIO? 350 | EREMOTEIO = -121 351 | if self.urb.status != 0 and not (not self.arg_remoteio 352 | and self.urb.status == EREMOTEIO): 353 | # Interrupts can generate these 354 | if self.verbose: 355 | self.gwarning( 356 | 'Packet %s complete code %s (%s)' % 357 | (self.submit.packet_number, self.urb.status, 358 | errno.errorcode.get(-self.urb.status, "unknown"))) 359 | 360 | # Find the matching submit request 361 | if self.urb.transfer_type == URB_CONTROL: 362 | if self.submit.m_ctrl.bRequestType & URB_TRANSFER_IN: 363 | self.processControlCompleteIn(dat_cur) 364 | else: 365 | self.processControlCompleteOut(dat_cur) 366 | elif self.urb.transfer_type == URB_BULK: 367 | if self.urb.endpoint & USB_DIR_IN: 368 | self.processBulkCompleteIn(dat_cur) 369 | else: 370 | self.processBulkCompleteOut(dat_cur) 371 | elif self.urb.transfer_type == URB_INTERRUPT: 372 | if self.urb.endpoint & URB_TRANSFER_IN: 373 | self.processInterruptCompleteIn(dat_cur) 374 | else: 375 | self.processInterruptCompleteOut(dat_cur) 376 | else: 377 | self.pwarning("unknown transfer type %u" % 378 | self.urb.transfer_type) 379 | self.processUnknownComplete(dat_cur) 380 | 381 | if self.urb.id in self.pending_complete: 382 | del self.pending_complete[self.urb.id] 383 | 384 | def processControlSubmit(self, dat_cur): 385 | pending = PendingRX() 386 | pending.raw = self.urb_raw 387 | pending.urb = self.urb 388 | pending.urbts = self.urbts 389 | 390 | if self.verbose: 391 | print('Remaining data: %d' % (len(dat_cur))) 392 | print('ctrlrequest: %d' % (len(self.urb.ctrlrequest))) 393 | ctrl = usb_ctrlrequest(self.urb.ctrlrequest[0:usb_ctrlrequest_sz]) 394 | 395 | if self.verbose: 396 | print("Packet %s control submit (control info size %lu)" % 397 | (self.pktn_str(), 666)) 398 | print(" bRequestType: %s (0x%02X)" % 399 | (request_type2str(ctrl.bRequestType), ctrl.bRequestType)) 400 | #print(" bRequest: %s (0x%02X)" % (request2str(ctrl), ctrl.bRequest)) 401 | print(" wValue: 0x%04X" % (ctrl.wValue)) 402 | print(" wIndex: 0x%04X" % (ctrl.wIndex)) 403 | print(" wLength: 0x%04X" % (ctrl.wLength)) 404 | 405 | if (ctrl.bRequestType & URB_TRANSFER_IN) == URB_TRANSFER_IN: 406 | self.printv("%d: IN" % (self.cur_packn)) 407 | else: 408 | self.printv("%d: OUT" % (self.cur_packn)) 409 | if len(dat_cur) != self.urb.data_length: 410 | self.gwarning( 411 | "remaining bytes %d != expected payload out bytes %d" % 412 | (len(dat_cur), self.urb.data_length)) 413 | hexdump(dat_cur, " ") 414 | #raise Exception('See above') 415 | pending.m_data_out = bytes(dat_cur) 416 | 417 | pending.m_ctrl = ctrl 418 | pending.packet_number = self.pktn_str() 419 | if self.verbose: 420 | print('Added pending control URB %s' % self.urb.id) 421 | self.pending_complete[self.urb.id] = pending 422 | 423 | def processControlCompleteIn(self, dat_cur): 424 | packet_numbering = '' 425 | data_size = 0 426 | data_str = "None" 427 | max_payload_sz = self.submit.m_ctrl.wLength 428 | 429 | # Is it legal to have a 0 length control in? 430 | if self.submit.m_ctrl.wLength: 431 | data_str = "buff" 432 | data_size = self.submit.m_ctrl.wLength 433 | 434 | # Verify we actually have enough / expected 435 | # If exact match don't care 436 | if len(dat_cur) != max_payload_sz: 437 | if len(dat_cur) < max_payload_sz: 438 | if self.arg_print_short: 439 | self.pcomment("NOTE:: req max %u but got %u" % 440 | (max_payload_sz, len(dat_cur))) 441 | else: 442 | raise Exception('invalid response') 443 | 444 | self.output_packet({ 445 | 'type': 'controlRead', 446 | 'bRequestType': self.submit.m_ctrl.bRequestType, 447 | 'bRequest': self.submit.m_ctrl.bRequest, 448 | 'wValue': self.submit.m_ctrl.wValue, 449 | 'wIndex': self.submit.m_ctrl.wIndex, 450 | 'wLength': self.submit.m_ctrl.wLength, 451 | 'data': bytes2Hex(dat_cur) 452 | }) 453 | 454 | if self.submit.m_ctrl.wLength: 455 | if self.arg_packet_numbers: 456 | packet_numbering = "packet %s/%s" % (self.submit.packet_number, 457 | self.pktn_str()) 458 | else: 459 | # TODO: consider counting instead of by captured index 460 | packet_numbering = "packet" 461 | 462 | def processControlCompleteOut(self, dat_cur): 463 | data_size = 0 464 | data_str = "None" 465 | 466 | #print('Control out w/ len %d' % len(submit.m_data_out)) 467 | 468 | # print("Data out size: %u vs urb size %u" % (submit.m_data_out_size, submit.urb.data_length )) 469 | if len(self.submit.m_data_out): 470 | # Note that its the submit from earlier, not the ack that we care about 471 | data_str = bytes2Hex(self.submit.m_data_out) 472 | data_size = len(self.submit.m_data_out) 473 | 474 | self.output_packet({ 475 | 'type': 'controlWrite', 476 | 'bRequestType': self.submit.m_ctrl.bRequestType, 477 | 'bRequest': self.submit.m_ctrl.bRequest, 478 | 'wValue': self.submit.m_ctrl.wValue, 479 | 'wIndex': self.submit.m_ctrl.wIndex, 480 | 'data': bytes2Hex(self.submit.m_data_out) 481 | }) 482 | 483 | def output_packet(self, j): 484 | urbj_submit = urb2json(self.submit.urb) 485 | urbj_complete = urb2json(self.urb) 486 | nsub, ncomplete = self.packnumt() 487 | j["submit"] = { 488 | "packn": nsub, 489 | 'urb': urbj_submit, 490 | 't': self.submit.urbts, 491 | 't_urb': urbj_submit["t"], 492 | } 493 | j["device"] = j["submit"]["urb"]["device"] 494 | j["complete"] = { 495 | 'packn': ncomplete, 496 | 'urb': urbj_complete, 497 | 't': self.urbts, 498 | 't_urb': urbj_complete["t"], 499 | } 500 | if len(self.pcomments): 501 | j["comments"] = self.pcomments 502 | self.jbuff.append(j) 503 | self.pcomments = [] 504 | 505 | def processBulkSubmit(self, dat_cur): 506 | pending = PendingRX() 507 | pending.raw = self.urb_raw 508 | pending.urb = self.urb 509 | pending.urbts = self.urbts 510 | 511 | if self.verbose: 512 | print('Remaining data: %d' % (len(dat_cur))) 513 | 514 | #if self.verbose: 515 | # print("Packet %d bulk submit (control info size %lu)" % (self.pktn_str(), 666)) 516 | 517 | if self.urb.endpoint & URB_TRANSFER_IN: 518 | self.printv("%d: IN" % (self.cur_packn)) 519 | else: 520 | self.printv("%d: OUT" % (self.cur_packn)) 521 | if len(dat_cur) != self.urb.data_length: 522 | self.pwarning( 523 | "remaining bytes %d != expected payload out bytes %d" % 524 | (len(dat_cur), self.urb.data_length)) 525 | hexdump(dat_cur, " ") 526 | #raise Exception('See above') 527 | pending.m_data_out = bytes(dat_cur) 528 | 529 | pending.packet_number = self.pktn_str() 530 | self.pending_complete[self.urb.id] = pending 531 | if self.verbose: 532 | print('Added pending bulk URB 0x%016lX' % self.urb.id) 533 | 534 | def processBulkCompleteIn(self, dat_cur): 535 | # packet_numbering = '' 536 | data_size = 0 537 | data_str = "None" 538 | max_payload_sz = self.submit.urb.length 539 | 540 | # FIXME: this is a messy conversion artfact from the C code 541 | # Is it legal to have a 0 length bulk in? 542 | if max_payload_sz: 543 | data_str = "buff" 544 | data_size = max_payload_sz 545 | 546 | self.output_packet({ 547 | 'type': 'bulkRead', 548 | 'endp': self.submit.urb.endpoint, 549 | 'len': data_size, 550 | 'data': bytes2Hex(dat_cur) 551 | }) 552 | 553 | # Verify we actually have enough / expected 554 | # If exact match don't care 555 | if len(dat_cur) > max_payload_sz: 556 | self.pwarning('requested max %u bytes but got %u' % 557 | (max_payload_sz, len(dat_cur))) 558 | elif len(dat_cur) < max_payload_sz and self.arg_print_short: 559 | self.pcomment("NOTE:: req max %u but got %u" % 560 | (max_payload_sz, len(dat_cur))) 561 | """ 562 | if max_payload_sz: 563 | if args.packet_numbers: 564 | packet_numbering = "packet %s/%s" % (self.submit.packet_number, 565 | self.pktn_str()) 566 | else: 567 | # TODO: consider counting instead of by captured index 568 | packet_numbering = "packet" 569 | """ 570 | 571 | def processBulkCompleteOut(self, dat_cur): 572 | # output below 573 | self.output_packet({ 574 | 'type': 'bulkWrite', 575 | 'endp': self.submit.urb.endpoint, 576 | 'data': bytes2Hex(self.submit.m_data_out) 577 | }) 578 | 579 | def processInterruptSubmit(self, dat_cur): 580 | pending = PendingRX() 581 | pending.raw = self.urb_raw 582 | pending.urb = self.urb 583 | pending.urbts = self.urbts 584 | pending.packet_number = self.pktn_str() 585 | if self.verbose: 586 | print('Added pending interrupt URB 0x%016lX' % self.urb.id) 587 | if not (self.urb.endpoint & URB_TRANSFER_IN): 588 | pending.m_data_out = bytes(dat_cur) 589 | self.pending_complete[self.urb.id] = pending 590 | 591 | def processInterruptCompleteOut(self, dat_cur): 592 | # looks like maybe windows doesn't report the request size? 593 | # think this is always 0 594 | # assert self.submit.urb.length == 0 595 | 596 | # FIXME: this is a messy conversion artifact from the C code 597 | # Is it legal to have a 0 length bulk in? 598 | data_size = 0 599 | # instead, use the recieved buffer size as a best estimate 600 | max_payload_sz = len(dat_cur) 601 | if max_payload_sz: 602 | data_size = max_payload_sz 603 | 604 | self.output_packet({ 605 | 'type': 'interruptOut', 606 | 'endp': self.submit.urb.endpoint, 607 | 'len': data_size, 608 | 'data': bytes2Hex(self.submit.m_data_out) 609 | }) 610 | 611 | def processInterruptCompleteIn(self, dat_cur): 612 | # looks like maybe windows doesn't report the request size? 613 | # think this is always 0 614 | # assert self.submit.urb.length == 0 615 | 616 | # FIXME: this is a messy conversion artifact from the C code 617 | # Is it legal to have a 0 length bulk in? 618 | data_size = 0 619 | # instead, use the recieved buffer size as a best estimate 620 | max_payload_sz = len(dat_cur) 621 | if max_payload_sz: 622 | data_size = max_payload_sz 623 | 624 | # output below 625 | self.output_packet({ 626 | 'type': 'interruptIn', 627 | 'endp': self.submit.urb.endpoint, 628 | 'len': data_size, 629 | 'data': bytes2Hex(dat_cur) 630 | }) 631 | 632 | def processUnknownComplete(self, dat_cur): 633 | self.output_packet({ 634 | 'type': 'unknown', 635 | }) 636 | -------------------------------------------------------------------------------- /usbrply/win_pcap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Windows is quirky 4 | http://desowin.org/usbpcap/capture_limitations.html 5 | 6 | Control packets 7 | -What are the status packets used for? 8 | -Control write has the request on the response packet 9 | 10 | Bulk packets 11 | -Only one packet for both request and response 12 | ''' 13 | 14 | from .usb import * 15 | from .util import hexdump, tostr 16 | from .com_pcap import PcapGen 17 | 18 | import sys 19 | import binascii 20 | import struct 21 | from collections import namedtuple 22 | 23 | # Windows transfer stages 24 | # Usb function: URB_FUNCTION_VENDOR_DEVICE (0x17) 25 | XFER_SETUP = 0 26 | # Usb function: URB_FUNCTION_CONTROL_TRANSFER (0x08) 27 | XFER_DATA = 1 28 | # Usb function: URB_FUNCTION_CONTROL_TRANSFER (0x08) 29 | XFER_STATUS = 2 30 | 31 | # https://github.com/tpn/winsdk-10/blob/master/Include/10.0.14393.0/shared/usb.h 32 | USBD_STATUS_SUCCESS = 0 33 | # https://github.com/JohnDMcMaster/usbrply/issues/70 34 | # Not really sure what this is (not in sdk's usb.h), but packet flow looks ok 35 | # FTDI device's GET_DESCRIPTOR 36 | USBD_STATUS_120_OK = 120 37 | 38 | # https://msdn.microsoft.com/en-us/library/windows/hardware/ff540409(v=vs.85).aspx 39 | # https://github.com/wine-mirror/wine/blob/master/include/ddk/usb.h 40 | URB_FUNCTION_ABORT_PIPE = 0x02 41 | URB_FUNCTION_CONTROL_TRANSFER = 0x08 42 | URB_FUNCTION_VENDOR_DEVICE = 0x17 43 | 44 | func_i2s = { 45 | 0x0000: "SELECT_CONFIGURATION", 46 | 0x0001: "SELECT_INTERFACE", 47 | 0x0002: "ABORT_PIPE", 48 | 0x0003: "TAKE_FRAME_LENGTH_CONTROL", 49 | 0x0004: "RELEASE_FRAME_LENGTH_CONTROL", 50 | 0x0005: "GET_FRAME_LENGTH", 51 | 0x0006: "SET_FRAME_LENGTH", 52 | 0x0007: "GET_CURRENT_FRAME_NUMBER", 53 | 0x0008: "CONTROL_TRANSFER", 54 | 0x0009: "BULK_OR_INTERRUPT_TRANSFER", 55 | 0x000A: "ISOCH_TRANSFER", 56 | 0x000B: "GET_DESCRIPTOR_FROM_DEVICE", 57 | 0x000C: "SET_DESCRIPTOR_TO_DEVICE", 58 | 0x000D: "SET_FEATURE_TO_DEVICE", 59 | 0x000E: "SET_FEATURE_TO_INTERFACE", 60 | 0x000F: "SET_FEATURE_TO_ENDPOINT", 61 | 0x0010: "CLEAR_FEATURE_TO_DEVICE", 62 | 0x0011: "CLEAR_FEATURE_TO_INTERFACE", 63 | 0x0012: "CLEAR_FEATURE_TO_ENDPOINT", 64 | 0x0013: "GET_STATUS_FROM_DEVICE", 65 | 0x0014: "GET_STATUS_FROM_INTERFACE", 66 | 0x0015: "GET_STATUS_FROM_ENDPOINT", 67 | 0x0016: "RESERVED_0X0016", 68 | 0x0017: "VENDOR_DEVICE", 69 | 0x0018: "VENDOR_INTERFACE", 70 | 0x0019: "VENDOR_ENDPOINT", 71 | 0x001A: "CLASS_DEVICE", 72 | 0x001B: "CLASS_INTERFACE", 73 | 0x001C: "CLASS_ENDPOINT", 74 | 0x001D: "RESERVE_0X001D", 75 | 0x001E: "SYNC_RESET_PIPE_AND_CLEAR_STALL", 76 | 0x001F: "CLASS_OTHER", 77 | 0x0020: "VENDOR_OTHER", 78 | 0x0021: "GET_STATUS_FROM_OTHER", 79 | 0x0022: "CLEAR_FEATURE_TO_OTHER", 80 | 0x0023: "SET_FEATURE_TO_OTHER", 81 | 0x0024: "GET_DESCRIPTOR_FROM_ENDPOINT", 82 | 0x0025: "SET_DESCRIPTOR_TO_ENDPOINT", 83 | 0x0026: "GET_CONFIGURATION", 84 | 0x0027: "GET_INTERFACE", 85 | 0x0028: "GET_DESCRIPTOR_FROM_INTERFACE", 86 | 0x0029: "SET_DESCRIPTOR_TO_INTERFACE", 87 | 0x002A: "GET_MS_FEATURE_DESCRIPTOR", 88 | 0x002B: "RESERVE_0X002B", 89 | 0x002C: "RESERVE_0X002C", 90 | 0x002D: "RESERVE_0X002D", 91 | 0x002E: "RESERVE_0X002E", 92 | 0x002F: "RESERVE_0X002F", 93 | 0x0030: "SYNC_RESET_PIPE", 94 | 0x0031: "SYNC_CLEAR_STALL", 95 | } 96 | 97 | 98 | def func_str(func): 99 | return func_i2s.get(func, "0x%04X" % func) 100 | 101 | 102 | ''' 103 | struct usb_ctrlrequest { 104 | __u8 bRequestType; 105 | __u8 bRequest; 106 | __le16 wValue; 107 | __le16 wIndex; 108 | __le16 wLength; 109 | } __attribute__ ((packed)); 110 | ''' 111 | usb_ctrlrequest_nt = namedtuple('usb_ctrlrequest_win', ( 112 | 'bRequestType', 113 | 'bRequest', 114 | 'wValue', 115 | 'wIndex', 116 | 'wLength', 117 | )) 118 | usb_ctrlrequest_fmt = ' FDO 187 | 'irp_info', 188 | # USB port 189 | # Ex: 3 190 | 'bus_id', 191 | # USB device on that port 192 | # Ex: 16 193 | 'device', 194 | # Which endpoint on that bus 195 | # Ex: 0x80 (0 in) 196 | 'endpoint', 197 | # Ex: URB_CONTROL 198 | 'transfer_type', 199 | # Length of data beyond header 200 | 'data_length', 201 | )) 202 | usb_urb_win_fmt = ( 203 | '<' 204 | 'H' # pcap_hdr_len 205 | 'Q' # irp_id 206 | 'i' # irp_status 207 | 'H' # usb_func 208 | 'B' # irp_info 209 | 'H' # bus_id 210 | 'H' # device 211 | 'B' # endpoint 212 | 'B' # transfer_type 213 | 'I' # data_length 214 | ) 215 | 216 | usb_urb_sz = struct.calcsize(usb_urb_win_fmt) 217 | 218 | 219 | def usb_urb(s): 220 | return usb_urb_win_nt(*struct.unpack(usb_urb_win_fmt, bytes(s))) 221 | 222 | 223 | # When we get an IN request we may process packets in between 224 | class PendingRX: 225 | def __init__(self): 226 | # Unprocessed packet bytes 227 | self.raw = None 228 | #usb_urb_t urb 229 | self.urb = None 230 | self.urbts = None 231 | #usb_ctrlrequest m_ctrl 232 | # Only applies to control requests 233 | self.m_ctrl = None 234 | self.packet_number = 0 235 | 236 | # uint8_t *m_data_out 237 | self.m_data_out = None 238 | 239 | 240 | class payload_bytes_type_t: 241 | def __init__(self): 242 | self.req_in = 0 243 | self.req_in_last = None 244 | self.in_ = 0 245 | self.in_last = None 246 | 247 | self.req_out = 0 248 | self.req_out_last = None 249 | self.out = 0 250 | self.out_last = None 251 | 252 | 253 | class payload_bytes_t: 254 | def __init__(self): 255 | self.ctrl = payload_bytes_type_t() 256 | self.bulk = payload_bytes_type_t() 257 | 258 | 259 | g_payload_bytes = payload_bytes_t() 260 | 261 | 262 | def update_delta(pb): 263 | pb.req_in_last = pb.req_in 264 | pb.in_last = pb.in_ 265 | 266 | pb.req_out_last = pb.req_out 267 | pb.out_last = pb.out 268 | 269 | 270 | def bytes2Hex(bytes_data): 271 | return tostr(binascii.hexlify(bytes_data)) 272 | 273 | 274 | def deviceStr(): 275 | # return "dev.udev" 276 | return "udev" 277 | 278 | 279 | def print_urb(urb): 280 | # unique per transaction, not packet 281 | print("URB id: %s" % (urb_id_str(urb.id))) 282 | print(" pcap_hdr_len: %s" % (urb.pcap_hdr_len, )) 283 | print(" irp_status: %s" % (urb.irp_status, )) 284 | print(" usb_func: %s" % (func_str(urb.usb_func), )) 285 | print(" irp_info: %s" % (irp_info_str(urb.irp_info), )) 286 | print(" bus_id: %s" % (urb.bus_id, )) 287 | print(" device: %s" % (urb.device, )) 288 | print(" endpoint: 0x%02X" % (urb.endpoint, )) 289 | print(" transfer_type: %s" % (urb.transfer_type, )) 290 | print(" data_length: %s" % (urb.data_length, )) 291 | 292 | 293 | def urb_add_str(urb): 294 | ret = dict(urb) 295 | ret["id_str"] = urb_id_str(urb["id"]) 296 | ret["usb_func_str"] = func_str(urb["usb_func"]) 297 | ret["irp_info_str"] = irp_info_str(urb["irp_info"]) 298 | return ret 299 | 300 | 301 | def urb2json(urb): 302 | j = dict(urb._asdict()) 303 | #j["ctrlrequest"] = binascii.hexlify(j["ctrlrequest"]) 304 | # j["data"] = binascii.hexlify(j["data"]) 305 | #j["t"] = j['sec'] + j['usec'] / 1e6 306 | return j 307 | 308 | 309 | def urb_error(urb): 310 | # return urb.irp_status != USBD_STATUS_SUCCESS 311 | # https://github.com/tpn/winsdk-10/blob/master/Include/10.0.14393.0/shared/usb.h#L266 312 | # #define USBD_ERROR(Status) ((USBD_STATUS)(Status) < 0) 313 | return urb.irp_status < 0 314 | 315 | 316 | def is_urb_submit(urb): 317 | return urb.usb_func == URB_FUNCTION_VENDOR_DEVICE 318 | 319 | 320 | def is_urb_complete(urb): 321 | return urb.usb_func == URB_FUNCTION_CONTROL_TRANSFER 322 | 323 | 324 | def urb_id_str(urb_id): 325 | # return binascii.hexlify(urb_id) 326 | return '0x%X' % urb_id 327 | 328 | 329 | class Gen(PcapGen): 330 | def __init__(self, fn, argsj={}): 331 | PcapGen.__init__(self, argsj) 332 | 333 | self.arg_fin = fn 334 | self.cur_packn = 0 335 | self.rel_pkt = 0 336 | 337 | self.previous_urb_complete_kept = None 338 | self.pending_complete = {} 339 | self.errors = 0 340 | 341 | self.urb = None 342 | self.urbts = None 343 | 344 | def comment_source(self): 345 | self.gcomment('Source: Windows pcap (USBPcap)') 346 | 347 | def parser(self): 348 | return "win-pcap" 349 | 350 | def platform(self): 351 | return "windows" 352 | 353 | def bad_packet(self, packet, msg): 354 | msg = "Packet %s: %s" % (self.pktn_str(), msg) 355 | 356 | self.errors += 1 357 | if self.arg_halt: 358 | hexdump(packet) 359 | raise ValueError(msg) 360 | if self.verbose: 361 | print(msg) 362 | hexdump(packet) 363 | 364 | def process_submit(self, dat_cur): 365 | # Find the matching submit request 366 | if self.urb.transfer_type == URB_CONTROL: 367 | self.processControlSubmit(dat_cur) 368 | elif self.urb.transfer_type == URB_BULK: 369 | self.processBulkSubmit(dat_cur) 370 | elif self.urb.transfer_type == URB_INTERRUPT: 371 | self.processGenericSubmit(dat_cur) 372 | self.printv('Added pending interrupt URB %s' % self.urb.id) 373 | # Pipe stall magic 374 | # FDO2PDO w/ URB_TRANSFER_IN 375 | # https://github.com/JohnDMcMaster/usbrply/issues/71 376 | elif self.urb.transfer_type == USB_IRP_INFO: 377 | self.processGenericSubmit(dat_cur) 378 | self.printv('Added pending IRP info URB %s' % self.urb.id) 379 | elif func_str(self.urb.usb_func) == "ABORT_PIPE": 380 | self.processGenericSubmit(dat_cur) 381 | self.printv('Added pending IRP info URB %s' % self.urb.id) 382 | else: 383 | self.processGenericSubmit(dat_cur) 384 | self.gwarning("packet %s: unhandled transfer_type 0x%02X" % 385 | (self.pktn_str(), self.urb.transfer_type)) 386 | 387 | def loop_cb(self, caplen, packet, ts): 388 | """ 389 | 2020-12-22 390 | Looks like there are two different structures 391 | Example observed on controlWrite 392 | 393 | Type 1 394 | Packet1: out FDO -> PDO 395 | Packet2: out PDO -> FDO 396 | 397 | Type 2 398 | Packet1: out FDO -> PDO 399 | Packet2: out FDO -> PDO 400 | Packet3: status PDO -> FDO 401 | 402 | Not sure exactly what this is (different windows version?) 403 | But lets make sure to handle both types 404 | """ 405 | try: 406 | packet = bytearray(packet) 407 | self.cur_packn += 1 408 | 409 | if self.cur_packn < self.min_packet or self.cur_packn > self.max_packet: 410 | # print("# Skipping packet %d" % (self.cur_packn)) 411 | return 412 | if self.verbose: 413 | print("") 414 | print("") 415 | print("") 416 | print('WIN PACKET %s' % (self.cur_packn, )) 417 | 418 | if caplen != len(packet): 419 | self.gwarning("packet %s: malformed, caplen %d != len %d", 420 | self.pktn_str(), caplen, len(packet)) 421 | return 422 | if self.verbose: 423 | hexdump(packet) 424 | print('Pending (%d):' % (len(self.pending_complete), )) 425 | for k in self.pending_complete: 426 | print(' %s' % (urb_id_str(k), )) 427 | 428 | self.printv("Length %u" % (len(packet), )) 429 | if len(packet) < usb_urb_sz: 430 | self.bad_packet( 431 | packet, 432 | "size %d is not min size %d" % (len(packet), usb_urb_sz)) 433 | return 434 | 435 | # caplen is actual length, len is reported 436 | self.urb_raw = packet 437 | self.urb = usb_urb(packet[0:usb_urb_sz]) 438 | self.urbts = ts 439 | dat_cur = packet[usb_urb_sz:] 440 | 441 | self.printv('ID %s, %s post-urb bytes' % 442 | (urb_id_str(self.urb.id), len(dat_cur))) 443 | 444 | # Main packet filtering 445 | # Drop if not specified device 446 | #print(self.pktn_str(), self.urb.device, args.device) 447 | if self.arg_device is not None and self.urb.device != self.arg_device: 448 | self.printv("Drop: device mismatch") 449 | self.dev_drops += 1 450 | return 451 | ''' 452 | # Status package may contain complete 453 | # But it looks like any 454 | if self.urb.transfer_type == URB_CONTROL: 455 | # Control transfer stage 456 | # 1: data 457 | # 2: status 458 | # 'xfer_stage', 459 | xfer_stage = dat_cur[0] 460 | """ 461 | #print('xfer_stage: %d' % xfer_stage) 462 | if xfer_stage == XFER_STATUS: 463 | if not self.urb.id in self.pending_complete: 464 | if self.urb.transfer_type != URB_INTERRUPT: 465 | self.gwarning( 466 | "Packet %s missing submit. URB ID: 0x%016lX" % 467 | (self.pktn_str(), self.urb.id)) 468 | else: 469 | self.process_complete(dat_cur) 470 | """ 471 | ''' 472 | 473 | self.rel_pkt += 1 474 | 475 | if self.verbose: 476 | # print("Header size: %lu" % (usb_urb_sz,)) 477 | print_urb(self.urb) 478 | 479 | # Success is almost universally 0 480 | # However: https://github.com/JohnDMcMaster/usbrply/issues/70 481 | if self.urb.irp_status > 0: 482 | self.gwarning("packet %s: suspicious irp_status %d (0x%08X)" % 483 | (self.pktn_str(), self.urb.irp_status, 484 | self.urb.irp_status)) 485 | 486 | if urb_error(self.urb): 487 | # Need to treat this as a warning for now 488 | # https://github.com/JohnDMcMaster/usbrply/issues/70 489 | # self.bad_packet(packet, 490 | # "bad irp_status %d (0x%08X)" % (self.urb.irp_status, 0x100000000 + self.urb.irp_status)) 491 | # return 492 | self.gwarning("packet %s: bad irp_status %d (0x%08X)" % 493 | (self.pktn_str(), self.urb.irp_status, 494 | 0x100000000 + self.urb.irp_status)) 495 | 496 | # Complete? 497 | # May be "out" or "status" 498 | if self.urb.irp_info & 1 == INFO_PDO2FDO: 499 | if not self.urb.id in self.pending_complete: 500 | if self.urb.transfer_type != URB_INTERRUPT: 501 | self.gwarning( 502 | "Packet %s missing submit. URB ID: 0x%016lX" % 503 | (self.pktn_str(), self.urb.id)) 504 | else: 505 | self.process_complete(dat_cur) 506 | # Otherwise submit 507 | else: 508 | self.process_submit(dat_cur) 509 | 510 | assert len(self.pcomments) == 0, ("Packet comments but no packets", 511 | self.pcomments) 512 | self.submit = None 513 | self.urb = None 514 | except: 515 | print('ERROR: packet %s' % self.pktn_str()) 516 | raise 517 | 518 | def pktn_str(self): 519 | if self.arg_rel_pkt: 520 | return self.rel_pkt 521 | else: 522 | return self.cur_packn 523 | 524 | def process_complete(self, dat_cur): 525 | self.printv("process_complete") 526 | self.submit = self.pending_complete[self.urb.id] 527 | # Done with it, get rid of it 528 | del self.pending_complete[self.urb.id] 529 | self.printv("Matched submit packet %s" % self.submit.packet_number) 530 | 531 | # Discarded? 532 | if self.submit is None: 533 | return 534 | 535 | self.packnum() 536 | 537 | if self.previous_urb_complete_kept is not None: 538 | ''' 539 | For bulk packets this can get tricky 540 | The intention was mostly for control packets where timing might be more critical 541 | ''' 542 | 543 | self.previous_urb_complete_kept = self.urb 544 | 545 | # Find the matching submit request 546 | if self.urb.transfer_type == URB_CONTROL: 547 | if self.submit.m_ctrl.bRequestType & URB_TRANSFER_IN: 548 | self.processControlCompleteIn(dat_cur) 549 | else: 550 | self.processControlCompleteOut(dat_cur) 551 | elif self.urb.transfer_type == URB_BULK: 552 | if self.urb.endpoint & URB_TRANSFER_IN: 553 | g_payload_bytes.bulk.in_ += self.urb.data_length 554 | self.processBulkCompleteIn(dat_cur) 555 | else: 556 | g_payload_bytes.bulk.out += self.urb.data_length 557 | self.processBulkCompleteOut(dat_cur) 558 | elif self.urb.transfer_type == URB_INTERRUPT: 559 | if self.urb.endpoint & URB_TRANSFER_IN: 560 | self.processInterruptCompleteIn(dat_cur) 561 | else: 562 | self.processInterruptCompleteOut(dat_cur) 563 | elif self.urb.transfer_type == USB_IRP_INFO: 564 | self.processIrpInfoComplete(dat_cur) 565 | elif func_str(self.urb.usb_func) == "ABORT_PIPE": 566 | self.processAbortPipe(dat_cur) 567 | else: 568 | self.pwarning("unknown transfer type %u" % self.urb.transfer_type) 569 | self.processUnknownComplete(dat_cur) 570 | 571 | def processControlSubmit(self, dat_cur): 572 | pending = PendingRX() 573 | pending.raw = self.urb_raw 574 | pending.urb = self.urb 575 | pending.urbts = self.urbts 576 | 577 | self.printv('Remaining data: %d' % (len(dat_cur))) 578 | #self.printv('ctrlrequest: %d' % (len(self.urb.ctrlrequest))) 579 | # Skip xfer_stage 580 | dat_cur = dat_cur[1:] 581 | ctrl = usb_ctrlrequest(dat_cur[0:usb_ctrlrequest_sz]) 582 | dat_cur = dat_cur[usb_ctrlrequest_sz:] 583 | 584 | if self.verbose: 585 | print("Packet %s control submit (control info size %lu)" % 586 | (self.pktn_str(), 666)) 587 | print(" bRequestType: %s (0x%02X)" % 588 | (request_type2str(ctrl.bRequestType), ctrl.bRequestType)) 589 | #print(" bRequest: %s (0x%02X)" % (request2str(ctrl), ctrl.bRequest)) 590 | print(" wValue: 0x%04X" % (ctrl.wValue)) 591 | print(" wIndex: 0x%04X" % (ctrl.wIndex)) 592 | print(" wLength: 0x%04X" % (ctrl.wLength)) 593 | 594 | if (ctrl.bRequestType & URB_TRANSFER_IN) == URB_TRANSFER_IN: 595 | self.printv("%d: IN" % (self.cur_packn)) 596 | else: 597 | self.printv("%d: OUT" % (self.cur_packn)) 598 | pending.m_data_out = bytearray(dat_cur) 599 | 600 | pending.m_ctrl = ctrl 601 | pending.packet_number = self.pktn_str() 602 | self.pending_complete[self.urb.id] = pending 603 | self.printv('Added pending control URB %s, len %d' % 604 | (urb_id_str(self.urb.id), len(self.pending_complete))) 605 | 606 | def processControlCompleteIn(self, dat_cur): 607 | packet_numbering = '' 608 | max_payload_sz = self.submit.m_ctrl.wLength 609 | 610 | # Skip xfer_stage 611 | dat_cur = dat_cur[1:] 612 | 613 | # Verify we actually have enough / expected 614 | # If exact match don't care 615 | if len(dat_cur) != max_payload_sz: 616 | if len(dat_cur) < max_payload_sz: 617 | self.gcomment("NOTE:: req max %u but got %u" % 618 | (max_payload_sz, len(dat_cur))) 619 | else: 620 | raise Exception('invalid response') 621 | 622 | self.output_packet({ 623 | 'type': 'controlRead', 624 | 'bRequestType': self.submit.m_ctrl.bRequestType, 625 | 'bRequest': self.submit.m_ctrl.bRequest, 626 | 'wValue': self.submit.m_ctrl.wValue, 627 | 'wIndex': self.submit.m_ctrl.wIndex, 628 | 'wLength': self.submit.m_ctrl.wLength, 629 | 'data': bytes2Hex(dat_cur) 630 | }) 631 | 632 | def processControlCompleteOut(self, dat_cur): 633 | self.output_packet({ 634 | 'type': 'controlWrite', 635 | 'bRequestType': self.submit.m_ctrl.bRequestType, 636 | 'bRequest': self.submit.m_ctrl.bRequest, 637 | 'wValue': self.submit.m_ctrl.wValue, 638 | 'wIndex': self.submit.m_ctrl.wIndex, 639 | 'data': bytes2Hex(self.submit.m_data_out) 640 | }) 641 | 642 | def output_packet(self, j): 643 | urbj_submit = urb2json(self.submit.urb) 644 | urbj_complete = urb2json(self.urb) 645 | nsub, ncomplete = self.packnumt() 646 | assert self.submit.urbts is not None 647 | j["submit"] = { 648 | "packn": nsub, 649 | 'urb': urb_add_str(urbj_submit), 650 | 't': self.submit.urbts, 651 | } 652 | assert self.urbts is not None 653 | j["complete"] = { 654 | 'packn': ncomplete, 655 | 'urb': urb_add_str(urbj_complete), 656 | 't': self.urbts, 657 | } 658 | if len(self.pcomments): 659 | j["comments"] = self.pcomments 660 | self.verbose and print("output_packet", j["submit"]["t"], 661 | j["complete"]["t"]) 662 | self.jbuff.append(j) 663 | self.pcomments = [] 664 | j["device"] = self.urb.device 665 | 666 | def processBulkSubmit(self, dat_cur): 667 | if self.urb.endpoint & URB_TRANSFER_IN: 668 | g_payload_bytes.bulk.req_in += self.urb.data_length 669 | else: 670 | g_payload_bytes.bulk.req_out += self.urb.data_length 671 | 672 | pending = PendingRX() 673 | pending.raw = self.urb_raw 674 | pending.urb = self.urb 675 | pending.urbts = self.urbts 676 | 677 | self.printv('Remaining data: %d' % (len(dat_cur))) 678 | 679 | # self.printv("Packet %d bulk submit (control info size %lu)" % (self.pktn_str(), 666)) 680 | 681 | if self.urb.endpoint & URB_TRANSFER_IN: 682 | self.printv("%d: IN" % (self.cur_packn)) 683 | else: 684 | self.printv("%d: OUT" % (self.cur_packn)) 685 | if len(dat_cur) != self.urb.data_length: 686 | self.gcomment( 687 | "WARNING: remaining bytes %d != expected payload out bytes %d" 688 | % (len(dat_cur), self.urb.data_length)) 689 | hexdump(dat_cur, " ") 690 | raise Exception('See above') 691 | pending.m_data_out = bytearray(dat_cur) 692 | 693 | pending.packet_number = self.pktn_str() 694 | self.pending_complete[self.urb.id] = pending 695 | self.printv('Added pending bulk URB %s' % self.urb.id) 696 | 697 | def processBulkCompleteIn(self, dat_cur): 698 | # looks like maybe windows doesn't report the request size? 699 | # think this is always 0 700 | assert self.submit.urb.data_length == 0 701 | 702 | # FIXME: this is a messy conversion artifact from the C code 703 | # Is it legal to have a 0 length bulk in? 704 | data_size = 0 705 | # instead, use the recieved buffer size as a best estimated 706 | max_payload_sz = len(dat_cur) 707 | if max_payload_sz: 708 | data_size = max_payload_sz 709 | 710 | # output below 711 | self.output_packet({ 712 | 'type': 'bulkRead', 713 | 'endp': self.submit.urb.endpoint, 714 | 'len': data_size, 715 | 'data': bytes2Hex(dat_cur) 716 | }) 717 | 718 | def processGenericSubmit(self, dat_cur): 719 | pending = PendingRX() 720 | pending.raw = self.urb_raw 721 | pending.urb = self.urb 722 | pending.urbts = self.urbts 723 | pending.packet_number = self.pktn_str() 724 | self.pending_complete[self.urb.id] = pending 725 | 726 | def processIrpInfoComplete(self, dat_cur): 727 | # assume 0 size for now 728 | assert self.submit.urb.data_length == 0 729 | assert len(dat_cur) == 0 730 | 731 | self.output_packet({ 732 | 'type': 'irpInfo', 733 | }) 734 | 735 | def processAbortPipe(self, dat_cur): 736 | self.output_packet({ 737 | 'type': 'abortPipe', 738 | }) 739 | 740 | def processUnknownComplete(self, dat_cur): 741 | self.output_packet({ 742 | 'type': 'unknown', 743 | }) 744 | 745 | def processInterruptCompleteOut(self, dat_cur): 746 | # looks like maybe windows doesn't report the request size? 747 | # think this is always 0 748 | assert self.submit.urb.data_length == 0 749 | 750 | # FIXME: this is a messy conversion artifact from the C code 751 | # Is it legal to have a 0 length bulk in? 752 | data_size = 0 753 | # instead, use the recieved buffer size as a best estimate 754 | max_payload_sz = len(dat_cur) 755 | if max_payload_sz: 756 | data_size = max_payload_sz 757 | 758 | self.output_packet({ 759 | 'type': 'interruptOut', 760 | 'endp': self.submit.urb.endpoint, 761 | 'len': data_size, 762 | 'data': bytes2Hex(dat_cur) 763 | }) 764 | 765 | def processInterruptCompleteIn(self, dat_cur): 766 | # looks like maybe windows doesn't report the request size? 767 | # think this is always 0 768 | assert self.submit.urb.data_length == 0 769 | 770 | # FIXME: this is a messy conversion artifact from the C code 771 | # Is it legal to have a 0 length bulk in? 772 | data_size = 0 773 | # instead, use the recieved buffer size as a best estimate 774 | max_payload_sz = len(dat_cur) 775 | if max_payload_sz: 776 | data_size = max_payload_sz 777 | 778 | # output below 779 | self.output_packet({ 780 | 'type': 'interruptIn', 781 | 'endp': self.submit.urb.endpoint, 782 | 'len': data_size, 783 | 'data': bytes2Hex(dat_cur) 784 | }) 785 | 786 | def processBulkCompleteOut(self, dat_cur): 787 | self.output_packet({ 788 | 'type': 'bulkWrite', 789 | 'endp': self.submit.urb.endpoint, 790 | 'data': bytes2Hex(self.submit.m_data_out) 791 | }) 792 | 793 | def loop_cb_devmax(self, caplen, packet, ts): 794 | self.cur_packn += 1 795 | if self.cur_packn < self.min_packet or self.cur_packn > self.max_packet: 796 | # print("# Skipping packet %d" % (self.cur_packn)) 797 | return 798 | 799 | if caplen != len(packet): 800 | print("packet %s: malformed, caplen %d != len %d", self.pktn_str(), 801 | caplen, len(packet)) 802 | return 803 | #if self.verbose: 804 | # print('Len: %d' % len(packet)) 805 | 806 | # dbg("Length %u" % (len(packet),)) 807 | if len(packet) < usb_urb_sz: 808 | hexdump(packet) 809 | raise ValueError("Packet size %d is not min size %d" % 810 | (len(packet), usb_urb_sz)) 811 | 812 | # caplen is actual length, len is reported 813 | self.urb_raw = packet 814 | self.urb = usb_urb(packet[0:usb_urb_sz]) 815 | 816 | self.arg_device = max(self.arg_device, self.urb.device) 817 | --------------------------------------------------------------------------------