Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added extensions + text fields to JSON #260

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/pyshark/consts.py
Original file line number Diff line number Diff line change
@@ -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"
Empty file.
21 changes: 21 additions & 0 deletions src/pyshark/extensions/base.py
Original file line number Diff line number Diff line change
@@ -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 ""
20 changes: 20 additions & 0 deletions src/pyshark/extensions/dns.py
Original file line number Diff line number Diff line change
@@ -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"
19 changes: 19 additions & 0 deletions src/pyshark/extensions/http.py
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 3 additions & 0 deletions src/pyshark/extensions/registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from pyshark.extensions import dns, http, ssl

EXTENSIONS = [dns.DNSExtension, http.HTTPExtension, ssl.SSLExtension, dns.MDNSExtension]
18 changes: 18 additions & 0 deletions src/pyshark/extensions/ssl.py
Original file line number Diff line number Diff line change
@@ -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
56 changes: 46 additions & 10 deletions src/pyshark/packet/layer.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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', '')
Expand Down
19 changes: 13 additions & 6 deletions src/pyshark/packet/packet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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):
"""
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/pyshark/tshark/tshark_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
4 changes: 2 additions & 2 deletions src/pyshark/tshark/tshark_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
interface_captured=frame.get_field_value('interface_id', raw=True))