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

Packet viewer #2603

Closed
wants to merge 1 commit into from
Closed
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
3 changes: 3 additions & 0 deletions .config/mypy/mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ ignore_missing_imports = True

[mypy-prompt_toolkit.*]
ignore_missing_imports = True

[mypy-urwid]
ignore_missing_imports = True
11 changes: 11 additions & 0 deletions .config/mypy/mypy_enabled.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,14 @@ scapy/fields.py
scapy/packet.py
scapy/plist.py
scapy/contrib/roce.py
scapy/modules/packet_viewer/button_bar.py
scapy/modules/packet_viewer/column_configuration.py
scapy/modules/packet_viewer/details_view.py
scapy/modules/packet_viewer/edit_view.py
scapy/modules/packet_viewer/extended_edit.py
scapy/modules/packet_viewer/extended_listbox.py
scapy/modules/packet_viewer/main_window.py
scapy/modules/packet_viewer/packet_list_view.py
scapy/modules/packet_viewer/pop_ups.py
scapy/modules/packet_viewer/row_formatter.py
scapy/modules/packet_viewer/viewer.py
31 changes: 31 additions & 0 deletions doc/scapy/development.rst
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,34 @@ following commands can then be used::
python3 setup.py sdist
twine check dist/scapy-2.4.3.tar.gz
twine upload dist/scapy-2.4.3.tar.gz



Packet Viewer
=============

UI Overview
-----------

.. image:: graphics/packet_viewer_ui_overview.*

Architecture Overview
---------------------

.. image:: graphics/packet_viewer_architecture_overview.*

* ``init``: Creates object

* ``msg_to_main_thread(<the msg>)``: With this signal other threads can send messages to the main thread, which is thereby awakened and processes this message. The DetailsView is just offering an interface for specialized views. Thus it depends on the implementation, which messages are sent. That's why an Asterik (*) is used there.

* ``add_packet``: Adds a packet to the PacketListView to display.

* ``update_selected_packet``: The MainWindow notifies the PacketListView that the currently selected packet has been modified. Thus the text representing this packet has to be updated.

* ``modified``: That's a predefined signal from urwid. It's emitted by a ListView when the selection changes or when a new item has been added.

* ``update_packets``: The MainWindow notifies a DetailsView that either the selected packet has changed or that a new packet has been received. A DetailsView can react or ignore it.

* ``notification``: Through this signal the DetailsView can display a notification to the user with an info or question popup.

* ``packet_modified``: The DetailsView shall emit this signal if it modified the selected packet. Related to update_selected_packet.
208 changes: 208 additions & 0 deletions doc/scapy/graphics/animations/animation-scapy-packet-viewer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/scapy/graphics/packet_viewer_ui_overview.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
115 changes: 115 additions & 0 deletions doc/scapy/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,121 @@ To allow Scapy to reach target destination additional options must be used::



Viewing packets like in Wireshark
---------------------------------

.. index::
single: viewer()

Problem
^^^^^^^
You want to see packets as in Wireshark with great flexibility.

Solution
^^^^^^^^
That's what :py:func:`viewer` is for!

.. image:: graphics/animations/animation-scapy-packet-viewer.svg


.. py:function:: viewer(source, ...)

It allows you to inspect, edit and filter lists of packets or shows
all captured packets on an interface. All packets can be modified in the terminal
interface. Any packet can be re-send on the current interface and new packets
can be crafted.

Columns
^^^^^^^

There are three groups of columns.
The viewer will always show the default columns.
If the user provides a custom configuration these columns will be shown.
If the user provides a ``basecls``, the packet viewer will try to get a
``basecls`` specific configuration from ``conf.contribs["packet_viewer_columns"]``. If no configuration
is present, the packet viewer will automatically create columns from the
``field_desc`` of the ``basecls``.

+---------------------+----------------------------+---------------------------------------------+
| Default | Additional columns | basecls |
+=====================+============================+=============================================+
| NO, TIME | Defined by the user | The fields of the basecls. |
| | with | Example: UDP --> SPORT, DPORT, LEN, CHKSUM |
| | ``viewer(s, cols, ...)`` | |
| | or defined in the config | |
+---------------------+----------------------------+---------------------------------------------+


`Example: Default columns`

``viewer(s)``
will have this columns:

``NO, TIME, REPR``

The viewer will add the ``REPR`` column if no basecls is specified.
This allows the user to see the most important data.


`Example: Custom configuration`

``viewer(s, [("MyLengthColumn", 10, len)], UDP)``
will have these columns:

``NO, TIME, MyLengthColumn, PAYLOAD``


`Example: Auto-generated columns from basecls`

``viewer(s, UDP)``
will have these columns:

``NO, TIME, SPORT, DPORT, LEN, CHKSUM, PAYLOAD``


`Example: Columns from configuration for basecls`

``conf.contribs["packet_viewer_columns"]["UDP"] = [("MyLengthColumn", 10, len)]``
``viewer(s, UDP)``
will have these columns:

``NO, TIME, MyLengthColumn, PAYLOAD``

Example script
^^^^^^^^^^^^^^

The following script displays all Ethernet packets received by the specified `L2Socket`.
All selected packets will be sent on the same socket after quitting the viewer.
Note that this script might require root privileges.


.. code:: python

from scapy.arch import L2Socket
from scapy.layers.l2 import Ether
from scapy.modules.packet_viewer.viewer import viewer

socket = L2Socket("eth0")
selected, _all = viewer(socket, basecls=Ether, globals_dict=globals())
[socket.send(p) for p in selected]

socket.close()

Views
^^^^^

:py:func:`viewer` takes a `views` argument. Views can offer additional information and features.

The views are independent from the Packet Viewer. They communicate only over interfaces.
So a plugin structure is used.

In `DetailsView` is the interface defined. It also offers the necessary structure to create a new view.
`ShowView` is an example of an already implemented view.

The `views` argument takes the views which should be accessible in the viewer.
If none specified, it only adds the `ShowView`.


Viewing packets with Wireshark
------------------------------

Expand Down
14 changes: 14 additions & 0 deletions scapy/modules/packet_viewer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# This file is part of Scapy
# See http://www.secdev.org/projects/scapy for more information
# Copyright (C) Andreas Korb <andreas.d.korb@gmail.com>
# Copyright (C) Nils Weiss <nils@we155.de>
# This program is published under a GPLv2 license


try:
import urwid # noqa: F401
from scapy.modules.packet_viewer.viewer import viewer # noqa: F401
except ImportError:
raise ImportError("urwid is not installed! "
"You may install urwid in order to use the "
"packet_viewer, via `pip install urwid`")
144 changes: 144 additions & 0 deletions scapy/modules/packet_viewer/button_bar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# This file is part of Scapy
# See http://www.secdev.org/projects/scapy for more information
# Copyright (C) Andreas Korb <andreas.d.korb@gmail.com>
# Copyright (C) Nils Weiss <nils@we155.de>
# This program is published under a GPLv2 license

from collections import OrderedDict
from typing import Tuple, Callable, List
from urwid import AttrMap, Button, Columns, Text


class ButtonBar(Columns):
def __init__(self, commands):
# type: (OrderedDict[str, Action]) -> None
"""
The commandline interface renders a set of buttons implemented
through Action objects. The key for each button is defined by the
key in the commands dict. The Action object delivers the text to
display and the function to execute on a key press.
:param commands: A dictionary to describe the supported keys. The key
of the dict maps to the key press, when the Action
is executed.
"""
self._actions = commands
self._key_button_map = OrderedDict((cmd[0], self._create_button(cmd))
for cmd in commands.items())

widgets = [(len(btn.get_label()) + 2, btn)
for btn in self._key_button_map.values()]
# Fill the rest of the row with the right color
widgets.append(AttrMap(Text(""), "cyan"))
super(ButtonBar, self).__init__(widgets)

def refresh(self):
# type: () -> None
"""
Refreshes the texts of the buttons.
"""
for action, btn in zip(self._actions.values(),
self._key_button_map.values()):
btn.set_label(("cyan", action.text))

def keypress(self, size, key):
# type: (int, str) -> None
"""
Handle editing keystrokes, return None to not forward key press.
:param size:
:param key: Name of key pressed.
"""
if key in self._actions:
self._execute_and_change_state(key)

def _execute_and_change_state(self, key):
# type: (str) -> None
"""
Executes action for a key and updates the according button text
:param key: Key to execute
"""
action = self._actions[key]
action.execute()

btn = self._key_button_map[key]
btn.set_label(("cyan", action.text))

# noinspection PyProtectedMember
def _create_button(self, cmd):
# type: (Tuple[str, Action]) -> Button
"""
Helper function to create a Button object for a command
:param cmd: Tuple of key and Action object
:return: Button for this Action
"""
key, action = cmd

btn = Button(("cyan", action.text),
on_press=lambda _sender, k:
self._execute_and_change_state(k),
user_data=key)
# We need to access the underlying Columns widget
cols = btn._w
# We don't want any dividing chars
cols.dividechars = 0
# Set the prefix and make it pack instead of "<" and fixed length
cols.contents[0] = (Text(key.upper()), cols.options("pack"))
# Remove the ">" behind the actual button text
del cols.contents[2]
# len(text) + 1 hides the cursor
cols.contents[1][0]._cursor_position = len(btn.label) + 1
# Ensure buttons won't gain focus but they are still clickable
cols._selectable = False
return btn


class Action(object):
"""
Helper class to store a list of texts and functions. On every execute,
the internal index increases. The internal index points to the current
text and function. If the index points to the last function, the next
execution causes a roll-over to index zero.
"""
def __init__(self, texts, funcs, state_index=0):
# type: (List[str], List[Callable[[], None]], int) -> None # noqa: E501
"""
Initialize an Action object
:param texts: A list of texts. Has to have the same order as funcs.
:param funcs: A list of functions. Has to have the same order as texts.
:param state_index: initial index if necessary
"""
self._texts = texts
self._funcs = funcs
self._state_index = state_index
if len(self._texts) != len(self._funcs):
raise AssertionError("The lists texts and funcs need to have "
"the same length")
if self._state_index > len(self._texts):
raise AssertionError("State index can't be greater than length "
akorb marked this conversation as resolved.
Show resolved Hide resolved
"of texts or funcs")

def execute(self):
# type: () -> None
"""
Executes the function selected by the current index. Afterwards the
index is increased.
"""
self._funcs[self._state_index]()
self._state_index += 1
self._state_index %= len(self._funcs)

def reset(self):
# type: () -> None
"""
Resets internal index back to zero.
"""
self._state_index = 0

@property
def text(self):
# type: () -> str
"""
Get the text selected by the current index.
:return: text selected.
"""
text_width = 12
return self._texts[self._state_index].ljust(text_width)[:text_width]
22 changes: 22 additions & 0 deletions scapy/modules/packet_viewer/column_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# This file is part of Scapy
# See http://www.secdev.org/projects/scapy for more information
# Copyright (C) Andreas Korb <andreas.d.korb@gmail.com>
# Copyright (C) Nils Weiss <nils@we155.de>
# This program is published under a GPLv2 license
from typing import List, Tuple, Callable

from scapy.config import conf
from scapy.packet import Packet

if "packet_viewer_columns" not in conf.contribs:
conf.contribs["packet_viewer_columns"] = dict()

payload_column = [("PAYLOAD", 50, lambda p: repr(p.payload))]
repr_column = [("REPR", 50, repr)] # type: List[Tuple[str, int, Callable[[Packet], str]]] # noqa: E501

# ############### ISOTP ###################

conf.contribs["packet_viewer_columns"]["ISOTP"] = [
("SRC", 6, lambda p: format(p.src, "03X")),
("DST", 6, lambda p: format(p.dst, "03X")),
("DATA", 100, lambda p: repr(p.data))]
Loading