diff --git a/src/pyshark/consts.py b/src/pyshark/consts.py new file mode 100644 index 00000000..a83d76c0 --- /dev/null +++ b/src/pyshark/consts.py @@ -0,0 +1,7 @@ +# These are some helpful consts in case you don't know what the protocol name is in Wireshark. + +ETH = ETHERNET = "ETH" +IP = "IP" +UDP = "UDP" +TCP = "TCP" +DHCP = BOOTP = "BOOTP" diff --git a/src/pyshark/extensions/__init__.py b/src/pyshark/extensions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/pyshark/extensions/base.py b/src/pyshark/extensions/base.py new file mode 100644 index 00000000..bd239fbd --- /dev/null +++ b/src/pyshark/extensions/base.py @@ -0,0 +1,21 @@ +class LayerExtension(object): + """Extensions which can supply extra methods for layers. + + To implement, override one of the given methods or create a new one that receives the layer. + """ + # Wrapped protocol + PROTOCOL = None + FOR_JSON = False + + @classmethod + def fits_layer(cls, layer): + from pyshark.packet.layer import JsonLayer, Layer + kls = JsonLayer if cls.FOR_JSON else Layer + if cls.PROTOCOL.lower() == layer.layer_name.lower() and layer.__class__ == kls: + return True + return False + + @classmethod + def get_repr(cls, layer): + """Extra info which will appear in the packet repr if the layer is the highest layer""" + return "" diff --git a/src/pyshark/extensions/dns.py b/src/pyshark/extensions/dns.py new file mode 100644 index 00000000..24339336 --- /dev/null +++ b/src/pyshark/extensions/dns.py @@ -0,0 +1,20 @@ +from pyshark.extensions.base import LayerExtension + + +class DNSExtension(LayerExtension): + PROTOCOL = "DNS" + FOR_JSON = True + + @classmethod + def get_queries(cls, dns_layer): + from pyshark.packet.layer import JsonLayer + queries = dns_layer.get_field("Queries", as_dict=True) + + # The key is currently the description + for query_desc in queries: + queries[query_desc]["description"] = query_desc + return [JsonLayer("QUERY", query, full_name="dns") for query in queries.values()] + + +class MDNSExtension(DNSExtension): + PROTOCOL = "mDNS" diff --git a/src/pyshark/extensions/http.py b/src/pyshark/extensions/http.py new file mode 100644 index 00000000..380bd24b --- /dev/null +++ b/src/pyshark/extensions/http.py @@ -0,0 +1,19 @@ +from pyshark.extensions.base import LayerExtension + + +class HTTPExtension(LayerExtension): + PROTOCOL = "HTTP" + FOR_JSON = True + + @classmethod + def get_request_info(cls, layer): + from pyshark.packet.layer import JsonLayer + + request_field = [field for field in layer.field_names + if field.endswith("\\r\\n") and field != "\\r\\n"][0] + return JsonLayer("REQUEST", layer.get_field(request_field, as_dict=True), full_name="http") + + @classmethod + def get_repr(cls, layer): + is_request = layer.has_field("request") + return "REQUEST" if is_request else "RESPONSE" diff --git a/src/pyshark/extensions/registry.py b/src/pyshark/extensions/registry.py new file mode 100644 index 00000000..e675aa32 --- /dev/null +++ b/src/pyshark/extensions/registry.py @@ -0,0 +1,3 @@ +from pyshark.extensions import dns, http, ssl + +EXTENSIONS = [dns.DNSExtension, http.HTTPExtension, ssl.SSLExtension, dns.MDNSExtension] diff --git a/src/pyshark/extensions/ssl.py b/src/pyshark/extensions/ssl.py new file mode 100644 index 00000000..f85b0b21 --- /dev/null +++ b/src/pyshark/extensions/ssl.py @@ -0,0 +1,18 @@ +from pyshark.extensions.base import LayerExtension + + +class SSLExtension(LayerExtension): + PROTOCOL = "SSL" + FOR_JSON = True + + @classmethod + def get_extensions(cls, ssl_layer): + extensions = {} + if not ssl_layer.record.has_field("handshake"): + return {} + + for field_name in ssl_layer.record.handshake.field_names: + if field_name.startswith("Extension: "): + extensions[ + field_name.split(": ", 1)[1]] = ssl_layer.record.handshake.get(field_name) + return extensions diff --git a/src/pyshark/packet/layer.py b/src/pyshark/packet/layer.py index 066ed889..7fd4cf38 100755 --- a/src/pyshark/packet/layer.py +++ b/src/pyshark/packet/layer.py @@ -1,7 +1,9 @@ import os +import functools import py +from pyshark.extensions import registry from pyshark.packet.common import Pickleable from pyshark.packet.fields import LayerField, LayerFieldsContainer @@ -17,6 +19,8 @@ def __init__(self, xml_obj=None, raw_mode=False): self._layer_name = xml_obj.attrib['name'] self._all_fields = {} + self._extension = None + self._initialized_extension = False # We copy over all the fields from the XML object # Note: we don't read lazily from the XML because the lxml objects are very memory-inefficient @@ -31,13 +35,29 @@ def __init__(self, xml_obj=None, raw_mode=False): self._all_fields[attributes['name']] = LayerFieldsContainer(field_obj) def __getattr__(self, item): - val = self.get_field(item) - if val is None: - raise AttributeError() + try: + val = self.get_field(item) + except AttributeError: + if hasattr(self.get_extension(), item): + # Pass the packet along to the get_extension and return the function + return functools.partial(getattr(self.get_extension(), item), self) + # If not in get_extension. + raise + if self.raw_mode: return val.raw_value return val + def get_extension(self): + if not self._initialized_extension: + # We don't simply check if extension is None because it might continue to be so. + self._initialized_extension = True + for ext in registry.EXTENSIONS: + if ext.fits_layer(self): + self._extension = ext + break + return self._extension + def get(self, item, default=None): """ Works the same way as getattr, but returns the given default if not the field was not found @@ -48,7 +68,11 @@ def get(self, item, default=None): return default def __dir__(self): - return dir(type(self)) + list(self.__dict__.keys()) + self.field_names + extension_funcs = [] + if self.get_extension(): + extension_funcs = dir(self.get_extension()) + accessible_fields = [fn for fn in self.field_names if " " not in fn and "-" not in fn] + return dir(type(self)) + list(self.__dict__.keys()) + accessible_fields + extension_funcs def get_field(self, name): """ @@ -197,6 +221,9 @@ class JsonLayer(Layer): def __init__(self, layer_name, layer_dict, full_name=None, is_intermediate=False): """Creates a JsonLayer. All sublayers and fields are created lazily later.""" self._layer_name = layer_name + self._extension = None + self._initialized_extension = False + if not full_name: self._full_name = self._layer_name else: @@ -215,17 +242,21 @@ def _sanitize_field_name(self, field_name): @property def field_names(self): - return list(set([self._sanitize_field_name(name) for name in self._all_fields - if name.startswith(self._full_name)] + - [name.rsplit('.', 1)[1] for name in self._all_fields if '.' in name])) + #if name.startswith(self._full_name) + return list(set([self._sanitize_field_name(name) for name in self._all_fields] + + [name.rsplit('.', 1)[1] for name in self._all_fields if '.' in name + and not ' ' in name])) def _get_all_fields_with_alternates(self): return [self.get_field(name) for name in self.field_names] - def get_field(self, name): + def get_field(self, name, as_dict=False): """Gets a field by its full or partial name.""" - # We only make the wrappers here (lazily) to avoid creating a ton of objects needlessly. - field = self._wrapped_fields.get(name) + field = None + if not as_dict: + # We only make the wrappers here (lazily) to avoid creating a ton of objects needlessly. + field = self._wrapped_fields.get(name) + if field is None: is_fake = False field = self._get_internal_field_by_name(name) @@ -234,6 +265,8 @@ def get_field(self, name): is_fake = self._is_fake_field(name) if not is_fake: raise AttributeError("No such field %s" % name) + if as_dict: + return field field = self._make_wrapped_field(name, field, is_fake=is_fake) self._wrapped_fields[name] = field return field @@ -279,6 +312,9 @@ def _make_wrapped_field(self, name, field, is_fake=False, full_name=None): # Populate with all fields that are supposed to be inside of it field = {key: value for key, value in self._all_fields.items() if key.startswith(full_name)} + elif not name.startswith(self._full_name): + # Text field + pass if isinstance(field, dict): if name.endswith('_tree'): name = name.replace('_tree', '') diff --git a/src/pyshark/packet/packet.py b/src/pyshark/packet/packet.py index 1d03089b..6fe0487c 100644 --- a/src/pyshark/packet/packet.py +++ b/src/pyshark/packet/packet.py @@ -16,7 +16,7 @@ class Packet(Pickleable): """ def __init__(self, layers=None, frame_info=None, number=None, - length=None, captured_length=None, sniff_time=None, interface_captured=None): + length=None, captured_length=None, interface_captured=None): """ Creates a Packet object with the given layers and info. @@ -36,7 +36,6 @@ def __init__(self, layers=None, frame_info=None, number=None, self.interface_captured = interface_captured self.captured_length = captured_length self.length = length - self.sniff_timestamp = sniff_time def __getitem__(self, item): """ @@ -75,20 +74,28 @@ def get_raw_packet(self): @property def sniff_time(self): + return datetime.datetime.fromtimestamp(self.sniff_timestamp) + + @property + def sniff_timestamp(self): try: - timestamp = float(self.sniff_timestamp) + return float(self.frame_info.time_epoch) except ValueError: # If the value after the decimal point is negative, discard it # Google: wireshark fractional second - timestamp = float(self.sniff_timestamp.split(".")[0]) - return datetime.datetime.fromtimestamp(timestamp) + return float(self.frame_info.time_epoch.split(".")[0]) def __repr__(self): transport_protocol = '' if self.transport_layer != self.highest_layer and self.transport_layer is not None: transport_protocol = self.transport_layer + '/' - return '<%s%s Packet>' % (transport_protocol, self.highest_layer) + extra_repr = "" + if self[self.highest_layer].get_extension(): + extra_repr = self[self.highest_layer].get_repr() + if extra_repr: + extra_repr = " (%s)" % extra_repr + return '<%s%s%s Packet>' % (transport_protocol, self.highest_layer, extra_repr) def __str__(self): s = self._packet_string diff --git a/src/pyshark/tshark/tshark_json.py b/src/pyshark/tshark/tshark_json.py index 29f9b28d..09c73420 100644 --- a/src/pyshark/tshark/tshark_json.py +++ b/src/pyshark/tshark/tshark_json.py @@ -38,5 +38,4 @@ def packet_from_json_packet(json_pkt): return Packet(layers=layers, frame_info=JsonLayer('frame', frame_dict), number=int(frame_dict.get('frame.number', 0)), length=int(frame_dict['frame.len']), - sniff_time=frame_dict['frame.time'], interface_captured=frame_dict.get('frame.interface_id')) diff --git a/src/pyshark/tshark/tshark_xml.py b/src/pyshark/tshark/tshark_xml.py index fd22290e..eb45a957 100644 --- a/src/pyshark/tshark/tshark_xml.py +++ b/src/pyshark/tshark/tshark_xml.py @@ -38,6 +38,6 @@ def _packet_from_pdml_packet(pdml_packet): layers = [Layer(proto) for proto in pdml_packet.proto] geninfo, frame, layers = layers[0], layers[1], layers[2:] return Packet(layers=layers, frame_info=frame, number=geninfo.get_field_value('num'), - length=geninfo.get_field_value('len'), sniff_time=geninfo.get_field_value('timestamp', raw=True), + length=geninfo.get_field_value('len'), captured_length=geninfo.get_field_value('caplen'), - interface_captured=frame.get_field_value('interface_id', raw=True)) \ No newline at end of file + interface_captured=frame.get_field_value('interface_id', raw=True))