From 280d693711c88eef5634858bd9a650a661853183 Mon Sep 17 00:00:00 2001 From: Dieter Dobbelaere Date: Fri, 23 Oct 2020 14:52:56 +0200 Subject: [PATCH] Add systemd Journal Export Block. --- examples/generate_pcapng.py | 4 +++ pcapng/blocks.py | 24 ++++++++++++++++- pcapng/structs.py | 53 ++++++++++++++++++++++++++++--------- tests/test_write_support.py | 10 +++++++ 4 files changed, 78 insertions(+), 13 deletions(-) diff --git a/examples/generate_pcapng.py b/examples/generate_pcapng.py index 43be066..29d59aa 100755 --- a/examples/generate_pcapng.py +++ b/examples/generate_pcapng.py @@ -50,3 +50,7 @@ spb = shb.new_member(blocks.SimplePacket) spb.packet_data = bytes(test_pl) writer.write_block(spb) + +jeb = shb.new_member(blocks.SystemdJournalExport) +spb.journal_entry = bytes("__REALTIME_TIMESTAMP=0\nMESSAGE=Hello!\n", "utf-8") +writer.write_block(jeb) diff --git a/pcapng/blocks.py b/pcapng/blocks.py index faa77b3..5421a3a 100644 --- a/pcapng/blocks.py +++ b/pcapng/blocks.py @@ -17,6 +17,7 @@ from pcapng.constants import link_types from pcapng.structs import ( IntField, + JournalEntryField, ListField, NameResolutionRecordField, Option, @@ -47,7 +48,10 @@ class Block(object): def __init__(self, **kwargs): if "raw" in kwargs: self._decoded = struct_decode( - self.schema, io.BytesIO(kwargs["raw"]), kwargs["endianness"] + self.schema, + io.BytesIO(kwargs["raw"]), + kwargs["endianness"], + len(kwargs["raw"]), ) else: self._decoded = {} @@ -650,6 +654,24 @@ class InterfaceStatistics( ] +@register_block +class SystemdJournalExport(SectionMemberBlock): + """ + "The systemd Journal Export Block is a lightweight container for systemd + Journal Export Format entry data. [...] Although the primary use of this + block is intended for importing data from systemd, it could potentially + be used to include arbitrary key-value data in a capture file." + - pcapng spec, section 4.7. Other quoted citations are from this section + unless otherwise noted. + """ + + magic_number = 0x00000009 + __slots__ = [] + schema = [ + ("journal_entry", JournalEntryField(), b""), + ] + + class UnknownBlock(Block): """ Class used to represent an unknown block. diff --git a/pcapng/structs.py b/pcapng/structs.py index ec7b887..ce67c30 100644 --- a/pcapng/structs.py +++ b/pcapng/structs.py @@ -292,7 +292,7 @@ class StructField(object): __slots__ = [] @abc.abstractmethod - def load(self, stream, endianness, seen=None): + def load(self, stream, endianness, max_size=None, seen=None): pass def __repr__(self): @@ -317,7 +317,7 @@ class RawBytes(StructField): def __init__(self, size): self.size = size # in bytes! - def load(self, stream, endianness=None, seen=None): + def load(self, stream, endianness=None, max_size=None, seen=None): return read_bytes_padded(stream, self.size) def encode(self, value, stream, endianness=None): @@ -343,7 +343,7 @@ def __init__(self, size, signed=False): self.size = size # in bits! self.signed = signed - def load(self, stream, endianness, seen=None): + def load(self, stream, endianness, max_size=None, seen=None): number = read_int(stream, self.size, signed=self.signed, endianness=endianness) return number @@ -372,7 +372,7 @@ class OptionsField(StructField): def __init__(self, options_schema): self.options_schema = options_schema - def load(self, stream, endianness, seen=None): + def load(self, stream, endianness, max_size=None, seen=None): options = read_options(stream, endianness) return Options(schema=self.options_schema, data=options, endianness=endianness) @@ -400,7 +400,7 @@ class PacketBytes(StructField): def __init__(self, len_field): self.dependency = len_field - def load(self, stream, endianness, seen=[]): + def load(self, stream, endianness, max_size=None, seen=[]): try: length = seen[self.dependency] except TypeError: @@ -445,7 +445,7 @@ class ListField(StructField): def __init__(self, subfield): self.subfield = subfield - def load(self, stream, endianness, seen=None): + def load(self, stream, endianness, max_size=None, seen=None): return list(self._iter_load(stream, endianness)) def _iter_load(self, stream, endianness): @@ -487,7 +487,7 @@ class NameResolutionRecordField(StructField): __slots__ = [] - def load(self, stream, endianness, seen=None): + def load(self, stream, endianness, max_size=None, seen=None): record_type = read_int(stream, 16, False, endianness) record_length = read_int(stream, 16, False, endianness) @@ -538,6 +538,26 @@ def encode_finish(self, stream, endianness): write_int(0, stream, 16, endianness=endianness) +class JournalEntryField(StructField): + """ + Field containing a "journal entry", used in the SystemdJournalExport block. + """ + + def load(self, stream, endianness, max_size, seen=None): + # Slurp all remaining bytes. + data = read_bytes_padded(stream, max_size) + + # Drop all trailing padding bytes. + data = data.rstrip(b"\x00") + + return data + + def encode(self, data, stream, endianness=None): + if not data: + raise ValueError("Journal entry invalid") + write_bytes_padded(stream, data) + + def read_options(stream, endianness): """ Read "options" from an "options block" in a stream, until a @@ -1005,7 +1025,7 @@ def _encode_value(self, value, ftype): raise ValueError("Unsupported field type: {0}".format(ftype)) -def struct_decode(schema, stream, endianness="="): +def struct_decode(schema, stream, endianness="=", max_size=None): """ Decode structured data from a stream, following a schema. @@ -1024,18 +1044,27 @@ def struct_decode(schema, stream, endianness="="): endianness specifier, as accepted by Python struct module (one of ``<>!=``, defaults to ``=``). + :param max_size: + maximum number of bytes to read, None for infinity. + :return: a dictionary mapping the field names to decoded data """ decoded = {} + prev_stream_pos = stream.tell() for name, field, default in schema: - decoded[name] = field.load(stream, endianness=endianness, seen=decoded) - return decoded + decoded[name] = field.load( + stream, endianness=endianness, max_size=max_size, seen=decoded + ) + if max_size is not None: + # Update max remaining number of bytes. + current_stream_pos = stream.tell() + max_size -= current_stream_pos - prev_stream_pos + prev_stream_pos = current_stream_pos -def block_decode(block, stream): - return struct_decode(block.schema, stream, block.section.endianness) + return decoded def struct_encode(schema, obj, outstream, endianness="="): diff --git a/tests/test_write_support.py b/tests/test_write_support.py index b193f2c..2e631c2 100644 --- a/tests/test_write_support.py +++ b/tests/test_write_support.py @@ -121,6 +121,16 @@ def test_write_read_all_blocks(): writer.write_block(blk) out_blocks.append(blk) + # systemd Journal Export Block. + blk = o_shb.new_member( + blocks.SystemdJournalExport, + journal_entry=bytes( + "__REALTIME_TIMESTAMP=0\nMESSAGE=Hello!\nPRIORITY=6\n", "utf-8" + ), + ) + writer.write_block(blk) + out_blocks.append(blk) + # Done writing blocks. # Now get back what we wrote and see if things line up. fake_file.seek(0)