class EnhancedPacket(BasePacketBlock): magic_number = 0x00000006 schema = [ ('interface_id', IntField(32, False)), ('timestamp_high', IntField(32, False)), ('timestamp_low', IntField(32, False)), ('packet_payload_info', PacketDataField()), ('options', OptionsField([ (2, 'epb_flags'), # todo: is this endianness dependent? (3, 'epb_hash'), # todo: process the hash value (4, 'epb_dropcount', 'u64'), ])) ] @property def captured_len(self): return self.packet_payload_info[0] @property def packet_len(self): return self.packet_payload_info[1] @property def packet_data(self): return self.packet_payload_info[2]
class InterfaceStatistics(SectionMemberBlock, BlockWithTimestampMixin, BlockWithInterfaceMixin): """ "The Interface Statistics Block (ISB) contains the capture statistics for a given interface [...]. The statistics are referred to the interface defined in the current Section identified by the Interface ID field." - pcapng spec, section 4.6. Other quoted citations are from this section unless otherwise noted. """ magic_number = 0x00000005 __slots__ = [] schema = [ ('interface_id', IntField(32, False), 0), ('timestamp_high', IntField(32, False), 0), ('timestamp_low', IntField(32, False), 0), ( 'options', OptionsField([ Option(2, 'isb_starttime', 'u64'), # todo: consider resolution Option(3, 'isb_endtime', 'u64'), Option(4, 'isb_ifrecv', 'u64'), Option(5, 'isb_ifdrop', 'u64'), Option(6, 'isb_filteraccept', 'u64'), Option(7, 'isb_osdrop', 'u64'), Option(8, 'isb_usrdeliv', 'u64'), ]), None), ]
class EnhancedPacket(BasePacketBlock, BlockWithTimestampMixin): """ "An Enhanced Packet Block (EPB) is the standard container for storing the packets coming from the network." - pcapng spec, section 4.3. Other quoted citations are from this section unless otherwise noted. """ magic_number = 0x00000006 __slots__ = [] schema = [ ('interface_id', IntField(32, False), 0), ('timestamp_high', IntField(32, False), 0), ('timestamp_low', IntField(32, False), 0), ('captured_len', IntField(32, False), 0), ('packet_len', IntField(32, False), 0), ('packet_data', PacketBytes('captured_len'), b''), ( 'options', OptionsField([ Option(2, 'epb_flags', 'epb_flags'), Option(3, 'epb_hash', 'type+bytes', multiple=True), # todo: process the hash value Option(4, 'epb_dropcount', 'u64'), ]), None) ]
class EnhancedPacket(BasePacketBlock, BlockWithTimestampMixin): """ "An Enhanced Packet Block (EPB) is the standard container for storing the packets coming from the network." - pcapng spec, section 4.3. Other quoted citations are from this section unless otherwise noted. """ magic_number = 0x00000006 __slots__ = [] schema = [ ("interface_id", IntField(32, False), 0), ("timestamp_high", IntField(32, False), 0), ("timestamp_low", IntField(32, False), 0), ("captured_len", IntField(32, False), 0), ("packet_len", IntField(32, False), 0), ("packet_data", PacketBytes("captured_len"), b""), ( "options", OptionsField( [ Option(2, "epb_flags", "epb_flags"), Option(3, "epb_hash", "type+bytes", multiple=True), # todo: process Option(4, "epb_dropcount", "u64"), ] ), None, ), ]
class Packet(BasePacketBlock): magic_number = 0x00000002 schema = [ ('interface_id', IntField(16, False)), ('drops_count', IntField(16, False)), ('timestamp_high', IntField(32, False)), ('timestamp_low', IntField(32, False)), ('packet_payload_info', PacketDataField()), ('options', OptionsField([ (2, 'epb_flags', 'u32'), # A flag! (3, 'epb_hash'), # Variable size! ])) ] @property def captured_len(self): return self.packet_payload_info[0] @property def packet_len(self): return self.packet_payload_info[1] @property def packet_data(self): return self.packet_payload_info[2]
class SectionHeader(Block): magic_number = 0x0a0d0d0a schema = [('version_major', IntField(16, False)), ('version_minor', IntField(16, False)), ('section_length', IntField(64, True)), ('options', OptionsField([ (2, 'shb_hardware', 'string'), (3, 'shb_os', 'string'), (4, 'shb_userappl', 'string'), ]))] def __init__(self, raw=None, endianness="<"): self._raw = raw self._decoded = None self.endianness = endianness self._interfaces_id = itertools.count(0) self.interfaces = {} self.interface_stats = {} def _decode(self): return struct_decode(self.schema, io.BytesIO(self._raw), endianness=self.endianness) def _encode(self, outstream): write_int(0x1A2B3C4D, outstream, 32) struct_encode(self.schema, self, outstream, endianness=self.endianness) def register_interface(self, interface): """Helper method to register an interface within this section""" assert isinstance(interface, InterfaceDescription) interface_id = next(self._interfaces_id) interface.interface_id = interface_id self.interfaces[interface_id] = interface def add_interface_stats(self, interface_stats): """Helper method to register interface stats within this section""" assert isinstance(interface_stats, InterfaceStatistics) self.interface_stats[interface_stats.interface_id] = interface_stats @property def version(self): return (self.version_major, self.version_minor) @property def length(self): return self.section_length def __repr__(self): return ('<{name} version={version} endianness={endianness} ' 'length={length} options={options}>').format( name=self.__class__.__name__, version='.'.join(str(x) for x in self.version), endianness=repr(self.endianness), length=self.length, options=repr(self.options))
class InterfaceDescription(SectionMemberBlock): magic_number = 0x00000001 schema = [ ('link_type', IntField(16, False)), # todo: enc/decode ('reserved', RawBytes(2)), ('snaplen', IntField(32, False)), ( 'options', OptionsField([ (2, 'if_name', 'string'), (3, 'if_description', 'string'), (4, 'if_IPv4addr', 'ipv4+mask'), (5, 'if_IPv6addr', 'ipv6+prefix'), (6, 'if_MACaddr', 'macaddr'), (7, 'if_EUIaddr', 'euiaddr'), (8, 'if_speed', 'u64'), (9, 'if_tsresol'), # Just keep the raw data (10, 'if_tzone', 'u32'), (11, 'if_filter', 'string'), (12, 'if_os', 'string'), (13, 'if_fcslen', 'u8'), (14, 'if_tsoffset', 'i64'), ])) ] @property # todo: cache this property def timestamp_resolution(self): # ------------------------------------------------------------ # Resolution of timestamps. If the Most Significant Bit is # equal to zero, the remaining bits indicates the resolution # of the timestamp as as a negative power of 10 (e.g. 6 means # microsecond resolution, timestamps are the number of # microseconds since 1/1/1970). If the Most Significant Bit is # equal to one, the remaining bits indicates the resolution as # as negative power of 2 (e.g. 10 means 1/1024 of second). If # this option is not present, a resolution of 10^-6 is assumed # (i.e. timestamps have the same resolution of the standard # 'libpcap' timestamps). # ------------------------------------------------------------ if 'if_tsresol' in self.options: return unpack_timestamp_resolution(self.options['if_tsresol']) return 1e-6 @property def statistics(self): # todo: ensure we always have an interface id -> how?? return self.section.interface_stats.get(self.interface_id) @property def link_type_description(self): try: return link_types.LINKTYPE_DESCRIPTIONS[self.link_type] except KeyError: return 'Unknown link type: 0x{0:04x}'.format(self.link_type)
class InterfaceStatistics(SectionMemberBlock, BlockWithTimestampMixin, BlockWithInterfaceMixin): magic_number = 0x00000005 schema = [ ('interface_id', IntField(32, False)), ('timestamp_high', IntField(32, False)), ('timestamp_low', IntField(32, False)), ('options', OptionsField([ (2, 'isb_starttime', 'u64'), # todo: consider resolution (3, 'isb_endtime', 'u64'), (4, 'isb_ifrecv', 'u64'), (5, 'isb_ifdrop', 'u64'), (6, 'isb_filteraccept', 'u64'), (7, 'isb_osdrop', 'u64'), (8, 'isb_usrdeliv', 'u64'), ])), ]
def _encode(self, outstream): fld_size = IntField(32, False) fld_data = RawBytes(0) if len(self.section.interfaces) > 1: # Spec is a bit ambiguous here. Section 4.4 says "it MUST # be assumed that all the Simple Packet Blocks have been captured # on the interface previously specified in the first Interface # Description Block." but later adds "A Simple Packet Block cannot # be present in a Section that has more than one interface because # of the impossibility to refer to the correct one (it does not # contain any Interface ID field)." Why would it say "the first" # IDB and not "the only" IDB if this was really forbidden? strictness.problem( "writing SimplePacket for section with multiple interfaces") if strictness.should_fix(): # Can't fix this. The IDBs have already been written. pass snap_len = self.interface.snaplen if snap_len > 0 and snap_len < self.captured_len: # This isn't a strictness issue, it *will* break other readers # if we write more bytes than the snaplen says to expect. fld_size.encode(self.packet_len, outstream, endianness=self.section.endianness) fld_data.encode(self.packet_data[:snap_len], outstream) else: fld_size.encode(self.packet_len, outstream, endianness=self.section.endianness) fld_data.encode(self.packet_data, outstream)
class ObsoletePacket(BasePacketBlock, BlockWithTimestampMixin): """ "The Packet Block is obsolete, and MUST NOT be used in new files. [...] A Packet Block was a container for storing packets coming from the network." - pcapng spec, Appendix A. Other quoted citations are from this appendix unless otherwise noted. """ magic_number = 0x00000002 __slots__ = [] schema = [ ("interface_id", IntField(16, False), 0), ("drops_count", IntField(16, False), 0), ("timestamp_high", IntField(32, False), 0), ("timestamp_low", IntField(32, False), 0), ("captured_len", IntField(32, False), 0), ("packet_len", IntField(32, False), 0), ("packet_data", PacketBytes("captured_len"), None), ( # The options have the same definitions as their epb_ equivalents "options", OptionsField( [ Option(2, "pack_flags", "epb_flags"), Option(3, "pack_hash", "type+bytes", multiple=True), ] ), None, ), ] def enhanced(self): """Return an EnhancedPacket with this block's attributes.""" opts_dict = dict(self.options) opts_dict["epb_dropcount"] = self.drops_count for a in ("flags", "hash"): try: opts_dict["epb_" + a] = opts_dict.pop("pack_" + a) except KeyError: pass return self.section.new_member( EnhancedPacket, interface_id=self.interface_id, timestamp_high=self.timestamp_high, timestamp_low=self.timestamp_low, packet_len=self.packet_len, packet_data=self.packet_data, options=opts_dict, ) # Do this check in _write() instead of _encode() to ensure the block gets written # with the correct magic number. def _write(self, outstream): strictness.problem("Packet Block is obsolete and must not be used") if strictness.should_fix(): self.enhanced()._write(outstream) else: super(ObsoletePacket, self)._write(outstream)
def test_decode_simple_struct(): schema = [ ('rawbytes', RawBytes(12)), ('int32s', IntField(32, True)), ('int32u', IntField(32, False)), ('int16s', IntField(16, True)), ('int16u', IntField(16, False)), ] stream = io.BytesIO() stream.write('Hello world!') stream.write(struct.pack('>i', -1234)) stream.write(struct.pack('>I', 1234)) stream.write(struct.pack('>h', -789)) stream.write(struct.pack('>H', 789)) stream.seek(0) decoded = struct_decode(schema, stream, '>') assert decoded['rawbytes'] == 'Hello world!' assert decoded['int32s'] == -1234 assert decoded['int32u'] == 1234 assert decoded['int16s'] == -789 assert decoded['int16u'] == 789
def test_decode_simple_struct(): schema = [ ("rawbytes", RawBytes(12), b""), ("int32s", IntField(32, True), 0), ("int32u", IntField(32, False), 0), ("int16s", IntField(16, True), 0), ("int16u", IntField(16, False), 0), ] stream = io.BytesIO() stream.write(b"Hello world!") stream.write(struct.pack(">i", -1234)) stream.write(struct.pack(">I", 1234)) stream.write(struct.pack(">h", -789)) stream.write(struct.pack(">H", 789)) stream.seek(0) decoded = struct_decode(schema, stream, ">") assert decoded["rawbytes"] == b"Hello world!" assert decoded["int32s"] == -1234 assert decoded["int32u"] == 1234 assert decoded["int16s"] == -789 assert decoded["int16u"] == 789
class ObsoletePacket(BasePacketBlock, BlockWithTimestampMixin): """ "The Packet Block is obsolete, and MUST NOT be used in new files. [...] A Packet Block was a container for storing packets coming from the network." - pcapng spec, Appendix A. Other quoted citations are from this appendix unless otherwise noted. """ magic_number = 0x00000002 __slots__ = [] schema = [ ('interface_id', IntField(16, False), 0), ('drops_count', IntField(16, False), 0), ('timestamp_high', IntField(32, False), 0), ('timestamp_low', IntField(32, False), 0), ('captured_len', IntField(32, False), 0), ('packet_len', IntField(32, False), 0), ('packet_data', PacketBytes('captured_len'), b''), ( 'options', OptionsField([ Option(2, 'pack_flags', 'epb_flags'), # Same definition as epb_flags Option(3, 'pack_hash', 'type+bytes', multiple=True), # Same definition as epb_hash ]), None) ] def enhanced(self): """Return an EnhancedPacket with this block's attributes.""" opts_dict = dict(self.options) opts_dict['epb_dropcount'] = self.drops_count for a in ('flags', 'hash'): try: opts_dict['epb_' + a] = opts_dict.pop('pack_' + a) except KeyError: pass return self.section.new_member(EnhancedPacket, interface_id=self.interface_id, timestamp_high=self.timestamp_high, timestamp_low=self.timestamp_low, packet_len=self.packet_len, packet_data=self.packet_data, options=opts_dict) def write(self, outstream): strictness.problem("Packet Block is obsolete and must not be used") if strictness.should_fix(): self.enhanced().write(outstream) else: super(ObsoletePacket, self).write(outstream)
def test_unpack_dummy_packet(): schema = [ ('a_string', RawBytes(8), ''), ('a_number', IntField(32, False), 0), ('options', OptionsField([]), None), ('pb_captured_len', IntField(32, False), 0), ('pb_orig_len', IntField(32, False), 0), ('packet_data', PacketBytes('pb_captured_len'), b''), ('spb_orig_len', IntField(32, False), 0), ('simple_packet_data', PacketBytes('spb_orig_len'), b''), ('name_res', ListField(NameResolutionRecordField()), []), ('another_number', IntField(32, False), 0), ] # Note: NULLs are for padding! data = io.BytesIO( b'\x01\x23\x45\x67\x89\xab\xcd\xef' b'\x00\x00\x01\x00' # Options b'\x00\x01\x00\x0cHello world!' b'\x00\x01\x00\x0fSpam eggs bacon\x00' b'\x00\x02\x00\x0fSome other text\x00' b'\x00\x00\x00\x00' # Enhanced Packet data b'\x00\x00\x00\x12' b'\x00\x01\x00\x00' b'These are 18 bytes\x00\x00' # Simple packet data b'\x00\x00\x00\x0d' b'Simple packet\x00\x00\x00' # List of name resolution items b'\x00\x01' # IPv4 b'\x00\x13' # Length: 19bytes b'\x0a\x22\x33\x44www.example.com\x00' # 19 bytes (10.34.51.68) b'\x00\x01' # IPv4 b'\x00\x13' # Length: 19bytes b'\xc0\xa8\x14\x01www.example.org\x00' # 19 bytes (192.168.20.1) b'\x00\x02' # IPv6 b'\x00\x1e' # 30 bytes b'\x00\x11\x22\x33\x44\x55\x66\x77' b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff' b'v6.example.net\x00\x00' b'\x00\x00\x00\x00' # End marker # Another number, to check end b'\xaa\xbb\xcc\xdd' ) unpacked = struct_decode(schema, data, endianness='>') assert unpacked['a_string'] == b'\x01\x23\x45\x67\x89\xab\xcd\xef' assert unpacked['a_number'] == 0x100 assert isinstance(unpacked['options'], Options) assert len(unpacked['options']) == 2 assert unpacked['options']['opt_comment'] == 'Hello world!' assert unpacked['options'][2] == b'Some other text' assert unpacked['pb_captured_len'] == 0x12 assert unpacked['pb_orig_len'] == 0x10000 assert unpacked['packet_data'] == b'These are 18 bytes' assert unpacked['spb_orig_len'] == 13 assert unpacked['simple_packet_data'] == b'Simple packet' assert unpacked['name_res'] == [ {'address': '10.34.51.68', 'names': ['www.example.com'], 'type': 1}, {'address': '192.168.20.1', 'names': ['www.example.org'], 'type': 1}, {'type': 2, 'address': '11:2233:4455:6677:8899:aabb:ccdd:eeff', 'names': ['v6.example.net']}]
class SimplePacket(BasePacketBlock): """ "The Simple Packet Block (SPB) is a lightweight container for storing the packets coming from the network." - pcapng spec, section 4.4. Other quoted citations are from this section unless otherwise noted. """ magic_number = 0x00000003 __slots__ = [] schema = [ ('packet_len', IntField(32, False), 0), # NOT the captured length ('packet_data', PacketBytes('captured_len'), b''), # we don't actually use this ] readonly_fields = set(('captured_len', 'interface_id')) def _decode(self): """Decodes the raw data of this block into its fields""" stream = six.BytesIO(self._raw) self._decoded = struct_decode(self.schema[:1], stream, self.section.endianness) # Now we can get our ``captured_len`` property which is required # to really know how much data we can load self._decoded['packet_data'] = RawBytes(self.captured_len).load( stream, self.section.endianness) del self._raw @property def interface_id(self): """ "The Simple Packet Block does not contain the Interface ID field. Therefore, it MUST be assumed that all the Simple Packet Blocks have been captured on the interface previously specified in the first Interface Description Block." """ return 0 @property def captured_len(self): """ "...the SnapLen value MUST be used to determine the size of the Packet Data field length." """ snap_len = self.interface.snaplen if snap_len == 0: # unlimited return self.packet_len else: return min(snap_len, self.packet_len) def _encode(self, outstream): fld_size = IntField(32, False) fld_data = RawBytes(0) if len(self.section.interfaces) > 1: # Spec is a bit ambiguous here. Section 4.4 says "it MUST # be assumed that all the Simple Packet Blocks have been captured # on the interface previously specified in the first Interface # Description Block." but later adds "A Simple Packet Block cannot # be present in a Section that has more than one interface because # of the impossibility to refer to the correct one (it does not # contain any Interface ID field)." Why would it say "the first" # IDB and not "the only" IDB if this was really forbidden? strictness.problem( "writing SimplePacket for section with multiple interfaces") if strictness.should_fix(): # Can't fix this. The IDBs have already been written. pass snap_len = self.interface.snaplen if snap_len > 0 and snap_len < self.captured_len: # This isn't a strictness issue, it *will* break other readers # if we write more bytes than the snaplen says to expect. fld_size.encode(self.packet_len, outstream, endianness=self.section.endianness) fld_data.encode(self.packet_data[:snap_len], outstream) else: fld_size.encode(self.packet_len, outstream, endianness=self.section.endianness) fld_data.encode(self.packet_data, outstream)
def test_unpack_dummy_packet(): schema = [ ("a_string", RawBytes(8), ""), ("a_number", IntField(32, False), 0), ("options", OptionsField([]), None), ("pb_captured_len", IntField(32, False), 0), ("pb_orig_len", IntField(32, False), 0), ("packet_data", PacketBytes("pb_captured_len"), b""), ("spb_orig_len", IntField(32, False), 0), ("simple_packet_data", PacketBytes("spb_orig_len"), b""), ("name_res", ListField(NameResolutionRecordField()), []), ("another_number", IntField(32, False), 0), ] # Note: NULLs are for padding! data = io.BytesIO( b"\x01\x23\x45\x67\x89\xab\xcd\xef" b"\x00\x00\x01\x00" # Options b"\x00\x01\x00\x0cHello world!" b"\x00\x01\x00\x0fSpam eggs bacon\x00" b"\x00\x02\x00\x0fSome other text\x00" b"\x00\x00\x00\x00" # Enhanced Packet data b"\x00\x00\x00\x12" b"\x00\x01\x00\x00" b"These are 18 bytes\x00\x00" # Simple packet data b"\x00\x00\x00\x0d" b"Simple packet\x00\x00\x00" # List of name resolution items b"\x00\x01" # IPv4 b"\x00\x13" # Length: 19bytes b"\x0a\x22\x33\x44www.example.com\x00" # 19 bytes (10.34.51.68) b"\x00\x01" # IPv4 b"\x00\x13" # Length: 19bytes b"\xc0\xa8\x14\x01www.example.org\x00" # 19 bytes (192.168.20.1) b"\x00\x02" # IPv6 b"\x00\x1e" # 30 bytes b"\x00\x11\x22\x33\x44\x55\x66\x77" b"\x88\x99\xaa\xbb\xcc\xdd\xee\xff" b"v6.example.net\x00\x00" b"\x00\x00\x00\x00" # End marker # Another number, to check end b"\xaa\xbb\xcc\xdd" ) unpacked = struct_decode(schema, data, endianness=">") assert unpacked["a_string"] == b"\x01\x23\x45\x67\x89\xab\xcd\xef" assert unpacked["a_number"] == 0x100 assert isinstance(unpacked["options"], Options) assert len(unpacked["options"]) == 2 assert unpacked["options"]["opt_comment"] == "Hello world!" assert unpacked["options"][2] == b"Some other text" assert unpacked["pb_captured_len"] == 0x12 assert unpacked["pb_orig_len"] == 0x10000 assert unpacked["packet_data"] == b"These are 18 bytes" assert unpacked["spb_orig_len"] == 13 assert unpacked["simple_packet_data"] == b"Simple packet" assert unpacked["name_res"] == [ {"address": "10.34.51.68", "names": ["www.example.com"], "type": 1}, {"address": "192.168.20.1", "names": ["www.example.org"], "type": 1}, { "type": 2, "address": "11:2233:4455:6677:8899:aabb:ccdd:eeff", "names": ["v6.example.net"], }, ]
class InterfaceDescription(SectionMemberBlock): """ "An Interface Description Block (IDB) is the container for information describing an interface on which packet data is captured." - pcapng spec, section 4.2. Other quoted citations are from this section unless otherwise noted. """ magic_number = 0x00000001 __slots__ = [] schema = [ ('link_type', IntField(16, False), 0), # todo: enc/decode ('reserved', IntField(16, False), 0), ('snaplen', IntField(32, False), 0), ( 'options', OptionsField([ Option(2, 'if_name', 'string'), Option(3, 'if_description', 'string'), Option(4, 'if_IPv4addr', 'ipv4+mask', multiple=True), Option(5, 'if_IPv6addr', 'ipv6+prefix', multiple=True), Option(6, 'if_MACaddr', 'macaddr'), Option(7, 'if_EUIaddr', 'euiaddr'), Option(8, 'if_speed', 'u64'), Option(9, 'if_tsresol', 'bytes'), # Just keep the raw data Option(10, 'if_tzone', 'u32'), Option(11, 'if_filter', 'type+bytes'), Option(12, 'if_os', 'string'), Option(13, 'if_fcslen', 'u8'), Option(14, 'if_tsoffset', 'i64'), Option(15, 'if_hardware', 'string'), ]), None) ] @property # todo: cache this property def timestamp_resolution(self): # ------------------------------------------------------------ # Resolution of timestamps. If the Most Significant Bit is # equal to zero, the remaining bits indicates the resolution # of the timestamp as as a negative power of 10 (e.g. 6 means # microsecond resolution, timestamps are the number of # microseconds since 1/1/1970). If the Most Significant Bit is # equal to one, the remaining bits indicates the resolution as # as negative power of 2 (e.g. 10 means 1/1024 of second). If # this option is not present, a resolution of 10^-6 is assumed # (i.e. timestamps have the same resolution of the standard # 'libpcap' timestamps). # ------------------------------------------------------------ if 'if_tsresol' in self.options: return unpack_timestamp_resolution(self.options['if_tsresol']) return 1e-6 @property def statistics(self): # todo: ensure we always have an interface id -> how?? return self.section.interface_stats.get(self.interface_id) @property def link_type_description(self): try: return link_types.LINKTYPE_DESCRIPTIONS[self.link_type] except KeyError: return 'Unknown link type: 0x{0:04x}'.format(self.link_type)
class SectionHeader(Block): """ "The Section Header Block (SHB) is mandatory. It identifies the beginning of a section of the capture file. The Section Header Block does not contain data but it rather identifies a list of blocks (interfaces, packets) that are logically correlated." - pcapng spec, section 4.1. Other quoted citations are from this section unless otherwise noted. """ magic_number = 0x0a0d0d0a __slots__ = [ 'endianness', '_interfaces_id', 'interfaces', 'interface_stats', ] schema = [('version_major', IntField(16, False), 1), ('version_minor', IntField(16, False), 0), ('section_length', IntField(64, True), -1), ('options', OptionsField([ Option(2, 'shb_hardware', 'string'), Option(3, 'shb_os', 'string'), Option(4, 'shb_userappl', 'string'), ]), None)] def __init__(self, endianness="<", **kwargs): super(SectionHeader, self).__init__(**kwargs) self.endianness = endianness self._interfaces_id = itertools.count(0) self.interfaces = {} self.interface_stats = {} def _encode(self, outstream): write_int(0x1A2B3C4D, outstream, 32, endianness=self.endianness) super(SectionHeader, self)._encode(outstream) def new_member(self, cls, **kwargs): """Helper method to create a block that's a member of this section""" assert issubclass(cls, SectionMemberBlock) blk = cls(section=self, **kwargs) # Some blocks (eg. SPB) don't have options if any([x[0] == 'options' for x in blk.schema]): blk.options.endianness = self.endianness if isinstance(blk, InterfaceDescription): self.register_interface(blk) elif isinstance(blk, InterfaceStatistics): self.add_interface_stats(blk) return blk def register_interface(self, interface): """Helper method to register an interface within this section""" assert isinstance(interface, InterfaceDescription) interface_id = next(self._interfaces_id) interface.interface_id = interface_id self.interfaces[interface_id] = interface def add_interface_stats(self, interface_stats): """Helper method to register interface stats within this section""" assert isinstance(interface_stats, InterfaceStatistics) self.interface_stats[interface_stats.interface_id] = interface_stats @property def version(self): return (self.version_major, self.version_minor) @property def length(self): return self.section_length # Block.decode() assumes all blocks have sections -- technically true... @property def section(self): return self def __repr__(self): return ('<{name} version={version} endianness={endianness} ' 'length={length} options={options}>').format( name=self.__class__.__name__, version='.'.join(str(x) for x in self.version), endianness=repr(self.endianness), length=self.length, options=repr(self.options))
def test_unpack_dummy_packet(): schema = [ ('a_string', RawBytes(8)), ('a_number', IntField(32, False)), ('options', OptionsField([])), ('packet_data', PacketDataField()), ('simple_packet_data', SimplePacketDataField()), ('name_res', ListField(NameResolutionRecordField())), ('another_number', IntField(32, False)), ] # Note: NULLs are for padding! data = io.BytesIO( '\x01\x23\x45\x67\x89\xab\xcd\xef' '\x00\x00\x01\x00' # Options '\x00\x01\x00\x0cHello world!' '\x00\x01\x00\x0fSpam eggs bacon\x00' '\x00\x02\x00\x0fSome other text\x00' '\x00\x00\x00\x00' # Enhanced Packet data '\x00\x00\x00\x12' '\x00\x01\x00\x00' 'These are 18 bytes\x00\x00' # Simple packet data '\x00\x00\x00\x0d' 'Simple packet\x00\x00\x00' # List of name resolution items '\x00\x01' # IPv4 '\x00\x13' # Length: 19bytes '\x0a\x22\x33\x44www.example.com\x00' # 19 bytes (10.34.51.68) '\x00\x01' # IPv4 '\x00\x13' # Length: 19bytes '\xc0\xa8\x14\x01www.example.org\x00' # 19 bytes (192.168.20.1) '\x00\x02' # IPv6 '\x00\x1e' # 30 bytes '\x00\x11\x22\x33\x44\x55\x66\x77' '\x88\x99\xaa\xbb\xcc\xdd\xee\xff' 'v6.example.net\x00\x00' '\x00\x00\x00\x00' # End marker # Another number, to check end '\xaa\xbb\xcc\xdd' ) unpacked = struct_decode(schema, data, endianness='>') assert unpacked['a_string'] == '\x01\x23\x45\x67\x89\xab\xcd\xef' assert unpacked['a_number'] == 0x100 assert isinstance(unpacked['options'], Options) assert len(unpacked['options']) == 2 assert unpacked['options']['opt_comment'] == 'Hello world!' assert unpacked['options'][2] == 'Some other text' assert unpacked['packet_data'] == (0x12, 0x10000, 'These are 18 bytes') assert unpacked['simple_packet_data'] == (13, 'Simple packet') assert unpacked['name_res'] == [ {'address': '\x0a\x22\x33\x44', 'name': 'www.example.com', 'type': 1}, {'address': '\xc0\xa8\x14\x01', 'name': 'www.example.org', 'type': 1}, {'type': 2, 'address': '\x00\x11\x22\x33\x44\x55\x66\x77' '\x88\x99\xaa\xbb\xcc\xdd\xee\xff', 'name': 'v6.example.net'}]