Add support for raw UDP. (#5414)

This commit is contained in:
Manuel Meitinger 2022-07-27 02:20:30 +02:00 committed by GitHub
parent 3f8da08796
commit cd4a74fae7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1290 additions and 245 deletions

View File

@ -2,6 +2,8 @@
## Unreleased: mitmproxy next
* Add UDP layer and flow support.
([#5414](https://github.com/mitmproxy/mitmproxy/pull/5414), @meitinger)
* Setting `connection_strategy` to `lazy` now also disables early
upstream connections to fetch TLS certificate details.
(@mhils)

View File

@ -6,7 +6,7 @@ from pathlib import Path
from mitmproxy import hooks, log, addonmanager
from mitmproxy.proxy import server_hooks, layer
from mitmproxy.proxy.layers import dns, http, modes, tcp, tls, websocket
from mitmproxy.proxy.layers import dns, http, modes, tcp, tls, udp, websocket
known = set()
@ -128,6 +128,17 @@ with outfile.open("w") as f, contextlib.redirect_stdout(f):
],
)
category(
"UDP",
"",
[
udp.UdpStartHook,
udp.UdpMessageHook,
udp.UdpEndHook,
udp.UdpErrorHook,
],
)
category(
"TLS",
"",

View File

@ -36,6 +36,7 @@ modules = [
"mitmproxy.proxy.server_hooks",
"mitmproxy.tcp",
"mitmproxy.tls",
"mitmproxy.udp",
"mitmproxy.websocket",
here / ".." / "src" / "generated" / "events.py",
]

View File

@ -0,0 +1,11 @@
---
title: "mitmproxy.udp"
url: "api/mitmproxy/udp.html"
menu:
addons:
parent: 'Event Hooks & API'
---
{{< readfile file="/generated/api/mitmproxy/udp.html" >}}

View File

@ -14,6 +14,7 @@ from mitmproxy import flowfilter
from mitmproxy import http
from mitmproxy.contrib import click as miniclick
from mitmproxy.tcp import TCPFlow, TCPMessage
from mitmproxy.udp import UDPFlow, UDPMessage
from mitmproxy.utils import human
from mitmproxy.utils import strutils
from mitmproxy.utils import vt_codes
@ -106,8 +107,8 @@ class Dumper:
def _echo_message(
self,
message: Union[http.Message, TCPMessage, WebSocketMessage],
flow: Union[http.HTTPFlow, TCPFlow],
message: Union[http.Message, TCPMessage, UDPMessage, WebSocketMessage],
flow: Union[http.HTTPFlow, TCPFlow, UDPFlow],
):
_, lines, error = contentviews.get_message_content_view(
ctx.options.dumper_default_contentview, message, flow
@ -318,27 +319,40 @@ class Dumper:
ret += f" (reason: {websocket.close_reason})"
return ret
def tcp_error(self, f):
def _proto_error(self, f):
if self.match(f):
self.echo(
f"Error in TCP connection to {human.format_address(f.server_conn.address)}: {f.error}",
f"Error in {f.type.upper()} connection to {human.format_address(f.server_conn.address)}: {f.error}",
fg="red",
)
def tcp_message(self, f):
def tcp_error(self, f):
self._proto_error(f)
def udp_error(self, f):
self._proto_error(f)
def _proto_message(self, f):
if self.match(f):
message = f.messages[-1]
direction = "->" if message.from_client else "<-"
self.echo(
"{client} {direction} tcp {direction} {server}".format(
"{client} {direction} {type} {direction} {server}".format(
client=human.format_address(f.client_conn.peername),
server=human.format_address(f.server_conn.address),
direction=direction,
type=f.type,
)
)
if ctx.options.flow_detail >= 3:
self._echo_message(message, f)
def tcp_message(self, f):
self._proto_message(f)
def udp_message(self, f):
self._proto_message(f)
def _echo_dns_query(self, f: dns.DNSFlow) -> None:
client = self._fmt_client(f)
opcode = dns.op_codes.to_str(f.request.op_code)

View File

@ -49,6 +49,9 @@ class Intercept:
def tcp_message(self, f):
self.process_flow(f)
def udp_message(self, f):
self.process_flow(f)
def dns_request(self, f):
self.process_flow(f)

View File

@ -7,7 +7,7 @@ from pathlib import Path
from typing import Literal, Optional
import mitmproxy.types
from mitmproxy import command, tcp
from mitmproxy import command, tcp, udp
from mitmproxy import ctx
from mitmproxy import dns
from mitmproxy import exceptions
@ -148,6 +148,16 @@ class Save:
def tcp_error(self, flow: tcp.TCPFlow):
self.tcp_end(flow)
def udp_start(self, flow: udp.UDPFlow):
if self.stream:
self.active_flows.add(flow)
def udp_end(self, flow: udp.UDPFlow):
self.save_flow(flow)
def udp_error(self, flow: udp.UDPFlow):
self.udp_end(flow)
def websocket_end(self, flow: http.HTTPFlow):
self.save_flow(flow)

View File

@ -27,6 +27,7 @@ from mitmproxy import flowfilter
from mitmproxy import http
from mitmproxy import io
from mitmproxy import tcp
from mitmproxy import udp
from mitmproxy.utils import human
@ -83,8 +84,8 @@ class OrderRequestMethod(_OrderKey):
def generate(self, f: mitmproxy.flow.Flow) -> str:
if isinstance(f, http.HTTPFlow):
return f.request.method
elif isinstance(f, tcp.TCPFlow):
return "TCP"
elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)):
return f.type.upper()
elif isinstance(f, dns.DNSFlow):
return dns.op_codes.to_str(f.request.op_code)
else:
@ -95,7 +96,7 @@ class OrderRequestURL(_OrderKey):
def generate(self, f: mitmproxy.flow.Flow) -> str:
if isinstance(f, http.HTTPFlow):
return f.request.url
elif isinstance(f, tcp.TCPFlow):
elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)):
return human.format_address(f.server_conn.address)
elif isinstance(f, dns.DNSFlow):
return f.request.questions[0].name if f.request.questions else ""
@ -112,7 +113,7 @@ class OrderKeySize(_OrderKey):
if f.response and f.response.raw_content:
size += len(f.response.raw_content)
return size
elif isinstance(f, tcp.TCPFlow):
elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)):
size = 0
for message in f.messages:
size += len(message.content)
@ -592,6 +593,18 @@ class View(collections.abc.Sequence):
def tcp_end(self, f):
self.update([f])
def udp_start(self, f):
self.add([f])
def udp_message(self, f):
self.update([f])
def udp_error(self, f):
self.update([f])
def udp_end(self, f):
self.update([f])
def dns_request(self, f):
self.add([f])

View File

@ -41,6 +41,7 @@ from . import (
from .base import View, KEY_MAX, format_text, format_dict, TViewResult
from ..http import HTTPFlow
from ..tcp import TCPMessage, TCPFlow
from ..udp import UDPMessage, UDPFlow
from ..websocket import WebSocketMessage
views: list[View] = []
@ -89,8 +90,8 @@ def safe_to_print(lines, encoding="utf8"):
def get_message_content_view(
viewname: str,
message: Union[http.Message, TCPMessage, WebSocketMessage],
flow: Union[HTTPFlow, TCPFlow],
message: Union[http.Message, TCPMessage, UDPMessage, WebSocketMessage],
flow: Union[HTTPFlow, TCPFlow, UDPFlow],
):
"""
Like get_content_view, but also handles message encoding.
@ -138,10 +139,10 @@ def get_message_content_view(
return description, lines, error
def get_tcp_content_view(
def get_proto_content_view(
viewname: str,
data: bytes,
flow: TCPFlow,
flow: Union[TCPFlow, UDPFlow],
):
viewmode = get(viewname)
if not viewmode:

View File

@ -5,6 +5,7 @@ from mitmproxy import flow
from mitmproxy import hooks
from mitmproxy import http
from mitmproxy import tcp
from mitmproxy import udp
from mitmproxy.proxy import layers
TEventGenerator = Iterator[hooks.Hook]
@ -42,6 +43,19 @@ def _iterate_tcp(f: tcp.TCPFlow) -> TEventGenerator:
yield layers.tcp.TcpEndHook(f)
def _iterate_udp(f: udp.UDPFlow) -> TEventGenerator:
messages = f.messages
f.messages = []
yield layers.udp.UdpStartHook(f)
while messages:
f.messages.append(messages.pop(0))
yield layers.udp.UdpMessageHook(f)
if f.error:
yield layers.udp.UdpErrorHook(f)
else:
yield layers.udp.UdpEndHook(f)
def _iterate_dns(f: dns.DNSFlow) -> TEventGenerator:
if f.request:
yield layers.dns.DnsRequestHook(f)
@ -54,6 +68,7 @@ def _iterate_dns(f: dns.DNSFlow) -> TEventGenerator:
_iterate_map: dict[type[flow.Flow], Callable[[Any], TEventGenerator]] = {
http.HTTPFlow: _iterate_http,
tcp.TCPFlow: _iterate_tcp,
udp.UDPFlow: _iterate_udp,
dns.DNSFlow: _iterate_dns,
}

View File

@ -57,6 +57,7 @@ class Flow(stateobject.StateObject):
See also:
- mitmproxy.http.HTTPFlow
- mitmproxy.tcp.TCPFlow
- mitmproxy.udp.UDPFlow
"""
client_conn: connection.Client

View File

@ -40,7 +40,7 @@ from collections.abc import Sequence
from typing import ClassVar, Protocol, Union
import pyparsing as pp
from mitmproxy import dns, flow, http, tcp
from mitmproxy import dns, flow, http, tcp, udp
def only(*types):
@ -120,6 +120,15 @@ class FTCP(_Action):
return True
class FUDP(_Action):
code = "udp"
help = "Match UDP flows"
@only(udp.UDPFlow)
def __call__(self, f):
return True
class FDNS(_Action):
code = "dns"
help = "Match DNS flows"
@ -276,7 +285,7 @@ class FBod(_Rex):
help = "Body"
flags = re.DOTALL
@only(http.HTTPFlow, tcp.TCPFlow, dns.DNSFlow)
@only(http.HTTPFlow, tcp.TCPFlow, udp.UDPFlow, dns.DNSFlow)
def __call__(self, f):
if isinstance(f, http.HTTPFlow):
if f.request and f.request.raw_content:
@ -289,7 +298,7 @@ class FBod(_Rex):
for msg in f.websocket.messages:
if self.re.search(msg.content):
return True
elif isinstance(f, tcp.TCPFlow):
elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)):
for msg in f.messages:
if self.re.search(msg.content):
return True
@ -306,7 +315,7 @@ class FBodRequest(_Rex):
help = "Request body"
flags = re.DOTALL
@only(http.HTTPFlow, tcp.TCPFlow, dns.DNSFlow)
@only(http.HTTPFlow, tcp.TCPFlow, udp.UDPFlow, dns.DNSFlow)
def __call__(self, f):
if isinstance(f, http.HTTPFlow):
if f.request and f.request.raw_content:
@ -316,7 +325,7 @@ class FBodRequest(_Rex):
for msg in f.websocket.messages:
if msg.from_client and self.re.search(msg.content):
return True
elif isinstance(f, tcp.TCPFlow):
elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)):
for msg in f.messages:
if msg.from_client and self.re.search(msg.content):
return True
@ -330,7 +339,7 @@ class FBodResponse(_Rex):
help = "Response body"
flags = re.DOTALL
@only(http.HTTPFlow, tcp.TCPFlow, dns.DNSFlow)
@only(http.HTTPFlow, tcp.TCPFlow, udp.UDPFlow, dns.DNSFlow)
def __call__(self, f):
if isinstance(f, http.HTTPFlow):
if f.response and f.response.raw_content:
@ -340,7 +349,7 @@ class FBodResponse(_Rex):
for msg in f.websocket.messages:
if not msg.from_client and self.re.search(msg.content):
return True
elif isinstance(f, tcp.TCPFlow):
elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)):
for msg in f.messages:
if not msg.from_client and self.re.search(msg.content):
return True
@ -537,6 +546,7 @@ filter_unary: Sequence[type[_Action]] = [
FReq,
FResp,
FTCP,
FUDP,
FDNS,
FWebSocket,
FAll,

View File

@ -304,7 +304,7 @@ class DatagramWriter:
self._transport.sendto(data, self._remote_addr)
def write_eof(self) -> None:
raise NotImplementedError("UDP does not support half-closing.")
raise OSError("UDP does not support half-closing.")
def get_extra_info(self, name: str, default: Any = None) -> Any:
if name == "peername":

View File

@ -2,6 +2,7 @@ from . import modes
from .dns import DNSLayer
from .http import HttpLayer
from .tcp import TCPLayer
from .udp import UDPLayer
from .tls import ClientTLSLayer, ServerTLSLayer
from .websocket import WebsocketLayer
@ -10,6 +11,7 @@ __all__ = [
"DNSLayer",
"HttpLayer",
"TCPLayer",
"UDPLayer",
"ClientTLSLayer",
"ServerTLSLayer",
"WebsocketLayer",

View File

@ -0,0 +1,133 @@
from dataclasses import dataclass
from typing import Optional
from mitmproxy import flow, udp
from mitmproxy.proxy import commands, events, layer
from mitmproxy.proxy.commands import StartHook
from mitmproxy.connection import Connection
from mitmproxy.proxy.context import Context
from mitmproxy.proxy.events import MessageInjected
from mitmproxy.proxy.utils import expect
@dataclass
class UdpStartHook(StartHook):
"""
A UDP connection has started.
"""
flow: udp.UDPFlow
@dataclass
class UdpMessageHook(StartHook):
"""
A UDP connection has received a message. The most recent message
will be flow.messages[-1]. The message is user-modifiable.
"""
flow: udp.UDPFlow
@dataclass
class UdpEndHook(StartHook):
"""
A UDP connection has ended.
"""
flow: udp.UDPFlow
@dataclass
class UdpErrorHook(StartHook):
"""
A UDP error has occurred.
Every UDP flow will receive either a udp_error or a udp_end event, but not both.
"""
flow: udp.UDPFlow
class UdpMessageInjected(MessageInjected[udp.UDPMessage]):
"""
The user has injected a custom UDP message.
"""
class UDPLayer(layer.Layer):
"""
Simple UDP layer that just relays messages right now.
"""
flow: Optional[udp.UDPFlow]
def __init__(self, context: Context, ignore: bool = False):
super().__init__(context)
if ignore:
self.flow = None
else:
self.flow = udp.UDPFlow(self.context.client, self.context.server, True)
@expect(events.Start)
def start(self, _) -> layer.CommandGenerator[None]:
if self.flow:
yield UdpStartHook(self.flow)
if self.context.server.timestamp_start is None:
err = yield commands.OpenConnection(self.context.server)
if err:
if self.flow:
self.flow.error = flow.Error(str(err))
yield UdpErrorHook(self.flow)
yield commands.CloseConnection(self.context.client)
self._handle_event = self.done
return
self._handle_event = self.relay_messages
_handle_event = start
@expect(events.DataReceived, events.ConnectionClosed, UdpMessageInjected)
def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]:
if isinstance(event, UdpMessageInjected):
# we just spoof that we received data here and then process that regularly.
event = events.DataReceived(
self.context.client
if event.message.from_client
else self.context.server,
event.message.content,
)
assert isinstance(event, events.ConnectionEvent)
from_client = event.connection == self.context.client
send_to: Connection
if from_client:
send_to = self.context.server
else:
send_to = self.context.client
if isinstance(event, events.DataReceived):
if self.flow:
udp_message = udp.UDPMessage(from_client, event.data)
self.flow.messages.append(udp_message)
yield UdpMessageHook(self.flow)
yield commands.SendData(send_to, udp_message.content)
else:
yield commands.SendData(send_to, event.data)
elif isinstance(event, events.ConnectionClosed):
if send_to.connected:
yield commands.CloseConnection(send_to)
else:
self._handle_event = self.done
if self.flow:
yield UdpEndHook(self.flow)
self.flow.live = False
else:
raise AssertionError(f"Unexpected event: {event}")
@expect(events.DataReceived, events.ConnectionClosed, UdpMessageInjected)
def done(self, _) -> layer.CommandGenerator[None]:
yield from ()

View File

@ -6,6 +6,7 @@ from mitmproxy import dns
from mitmproxy import flow
from mitmproxy import http
from mitmproxy import tcp
from mitmproxy import udp
from mitmproxy import websocket
from mitmproxy.proxy.mode_specs import ProxyMode
from mitmproxy.test.tutils import tdnsreq, tdnsresp
@ -36,6 +37,29 @@ def ttcpflow(
return f
def tudpflow(
client_conn=True, server_conn=True, messages=True, err=None
) -> udp.UDPFlow:
if client_conn is True:
client_conn = tclient_conn()
if server_conn is True:
server_conn = tserver_conn()
if messages is True:
messages = [
udp.UDPMessage(True, b"hello", 946681204.2),
udp.UDPMessage(False, b"it's me", 946681204.5),
]
if err is True:
err = terr()
f = udp.UDPFlow(client_conn, server_conn)
f.timestamp_created = client_conn.timestamp_start
f.messages = messages
f.error = err
f.live = True
return f
def twebsocketflow(
messages=True, err=None, close_code=None, close_reason=""
) -> http.HTTPFlow:
@ -272,6 +296,8 @@ def tflows() -> list[flow.Flow]:
tflow(ws=True),
ttcpflow(),
ttcpflow(err=True),
tudpflow(),
tudpflow(err=True),
tdnsflow(resp=True),
tdnsflow(err=True),
]

View File

@ -14,6 +14,7 @@ from mitmproxy import flow
from mitmproxy.http import HTTPFlow
from mitmproxy.utils import human, emoji
from mitmproxy.tcp import TCPFlow
from mitmproxy.udp import UDPFlow
from mitmproxy import dns
from mitmproxy.dns import DNSFlow
@ -116,6 +117,7 @@ SCHEME_STYLES = {
"ws": "scheme_ws",
"wss": "scheme_wss",
"tcp": "scheme_tcp",
"udp": "scheme_udp",
"dns": "scheme_dns",
}
HTTP_REQUEST_METHOD_STYLES = {
@ -604,12 +606,13 @@ def format_http_flow_table(
@lru_cache(maxsize=800)
def format_tcp_flow(
def format_message_flow(
*,
render_mode: RenderMode,
focused: bool,
timestamp_start: float,
marked: str,
protocol: str,
client_address,
server_address,
total_size: int,
@ -633,9 +636,9 @@ def format_tcp_flow(
items.append(fcol(" ", "focus"))
if render_mode is RenderMode.TABLE:
items.append(fcol("TCP ", SCHEME_STYLES["tcp"]))
items.append(fcol(fixlen(protocol.upper(), 5), SCHEME_STYLES[protocol]))
else:
items.append(fcol("TCP", SCHEME_STYLES["tcp"]))
items.append(fcol(protocol.upper(), SCHEME_STYLES[protocol]))
items.append(("weight", 1.0, truncated_plain(conn, "text", "left")))
if error_message:
@ -752,7 +755,7 @@ def format_flow(
else:
error_message = None
if isinstance(f, TCPFlow):
if isinstance(f, (TCPFlow, UDPFlow)):
total_size = 0
for message in f.messages:
total_size += len(message.content)
@ -760,11 +763,12 @@ def format_flow(
duration = f.messages[-1].timestamp - f.client_conn.timestamp_start
else:
duration = None
return format_tcp_flow(
return format_message_flow(
render_mode=render_mode,
focused=focused,
timestamp_start=f.client_conn.timestamp_start,
marked=f.marked,
protocol=f.type,
client_address=f.client_conn.peername,
server_address=f.server_conn.address,
total_size=total_size,

View File

@ -11,6 +11,7 @@ from mitmproxy import flow
from mitmproxy import http
from mitmproxy import log
from mitmproxy import tcp
from mitmproxy import udp
from mitmproxy.tools.console import keymap
from mitmproxy.tools.console import overlay
from mitmproxy.tools.console import signals
@ -308,7 +309,7 @@ class ConsoleAddon:
@command.command("console.view.flow")
def view_flow(self, flow: flow.Flow) -> None:
"""View a flow."""
if isinstance(flow, (http.HTTPFlow, tcp.TCPFlow, dns.DNSFlow)):
if isinstance(flow, (http.HTTPFlow, tcp.TCPFlow, udp.UDPFlow, dns.DNSFlow)):
self.master.switch_view("flowview")
else:
ctx.log.warn(f"No detail view for {type(flow).__name__}.")
@ -363,6 +364,8 @@ class ConsoleAddon:
if isinstance(flow, tcp.TCPFlow):
focus_options = ["tcp-message"]
elif isinstance(flow, udp.UDPFlow):
focus_options = ["udp-message"]
elif isinstance(flow, http.HTTPFlow):
focus_options = [
"cookies",
@ -447,7 +450,7 @@ class ConsoleAddon:
self.master.commands.call_strings(
"console.command", ["flow.set", "@focus", flow_part]
)
elif flow_part == "tcp-message":
elif flow_part in ["tcp-message", "udp-message"]:
message = flow.messages[-1]
c = self.master.spawn_editor(message.content or b"")
message.content = c.rstrip(b"\n")

View File

@ -12,6 +12,7 @@ from mitmproxy import ctx
from mitmproxy import dns
from mitmproxy import http
from mitmproxy import tcp
from mitmproxy import udp
from mitmproxy.tools.console import common
from mitmproxy.tools.console import flowdetailview
from mitmproxy.tools.console import layoutwidget
@ -87,7 +88,12 @@ class FlowDetails(tabs.Tabs):
]
elif isinstance(f, tcp.TCPFlow):
self.tabs = [
(self.tab_tcp_stream, self.view_tcp_stream),
(self.tab_tcp_stream, self.view_message_stream),
(self.tab_details, self.view_details),
]
elif isinstance(f, udp.UDPFlow):
self.tabs = [
(self.tab_udp_stream, self.view_message_stream),
(self.tab_details, self.view_details),
]
elif isinstance(f, dns.DNSFlow):
@ -135,6 +141,9 @@ class FlowDetails(tabs.Tabs):
def tab_tcp_stream(self):
return "TCP Stream"
def tab_udp_stream(self):
return "UDP Stream"
def tab_websocket_messages(self):
return "WebSocket Messages"
@ -235,9 +244,9 @@ class FlowDetails(tabs.Tabs):
return searchable.Searchable(widget_lines)
def view_tcp_stream(self) -> urwid.Widget:
def view_message_stream(self) -> urwid.Widget:
flow = self.flow
assert isinstance(flow, tcp.TCPFlow)
assert isinstance(flow, (tcp.TCPFlow, udp.UDPFlow))
if not flow.messages:
return searchable.Searchable([urwid.Text(("highlight", "No messages."))])
@ -259,7 +268,7 @@ class FlowDetails(tabs.Tabs):
from_client = flow.messages[0].from_client
for m in messages:
_, lines, _ = contentviews.get_tcp_content_view(viewmode, m, flow)
_, lines, _ = contentviews.get_proto_content_view(viewmode, m, flow)
for line in lines:
if from_client:

View File

@ -37,6 +37,7 @@ class Palette:
"scheme_ws",
"scheme_wss",
"scheme_tcp",
"scheme_udp",
"scheme_dns",
"scheme_other",
"url_punctuation",
@ -174,6 +175,7 @@ class LowDark(Palette):
scheme_ws=("brown", "default"),
scheme_wss=("dark magenta", "default"),
scheme_tcp=("dark magenta", "default"),
scheme_udp=("dark magenta", "default"),
scheme_dns=("dark blue", "default"),
scheme_other=("dark magenta", "default"),
url_punctuation=("light gray", "default"),
@ -272,6 +274,7 @@ class LowLight(Palette):
scheme_ws=("brown", "default"),
scheme_wss=("light magenta", "default"),
scheme_tcp=("light magenta", "default"),
scheme_udp=("light magenta", "default"),
scheme_dns=("light blue", "default"),
scheme_other=("light magenta", "default"),
url_punctuation=("dark gray", "default"),
@ -391,6 +394,7 @@ class SolarizedLight(LowLight):
scheme_ws=(sol_orange, "default"),
scheme_wss=("light magenta", "default"),
scheme_tcp=("light magenta", "default"),
scheme_udp=("light magenta", "default"),
scheme_dns=("light blue", "default"),
scheme_other=("light magenta", "default"),
url_punctuation=("dark gray", "default"),

View File

@ -25,6 +25,7 @@ from mitmproxy import version
from mitmproxy.dns import DNSFlow
from mitmproxy.http import HTTPFlow
from mitmproxy.tcp import TCPFlow, TCPMessage
from mitmproxy.udp import UDPFlow, UDPMessage
from mitmproxy.utils.emoji import emoji
from mitmproxy.utils.strutils import always_str
from mitmproxy.websocket import WebSocketMessage
@ -162,7 +163,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
"close_reason": flow.websocket.close_reason,
"timestamp_end": flow.websocket.timestamp_end,
}
elif isinstance(flow, TCPFlow):
elif isinstance(flow, (TCPFlow, UDPFlow)):
f["messages_meta"] = {
"contentLength": sum(len(x.content) for x in flow.messages),
"count": len(flow.messages),
@ -477,8 +478,8 @@ class FlowContentView(RequestHandler):
def message_to_json(
self,
viewname: str,
message: Union[http.Message, TCPMessage, WebSocketMessage],
flow: Union[HTTPFlow, TCPFlow],
message: Union[http.Message, TCPMessage, UDPMessage, WebSocketMessage],
flow: Union[HTTPFlow, TCPFlow, UDPFlow],
max_lines: Optional[int] = None,
):
description, lines, error = contentviews.get_message_content_view(
@ -496,7 +497,7 @@ class FlowContentView(RequestHandler):
def get(self, flow_id, message, content_view):
flow = self.flow
assert isinstance(flow, (HTTPFlow, TCPFlow))
assert isinstance(flow, (HTTPFlow, TCPFlow, UDPFlow))
if self.request.arguments.get("lines"):
max_lines = int(self.request.arguments["lines"][0])
@ -506,7 +507,7 @@ class FlowContentView(RequestHandler):
if message == "messages":
if isinstance(flow, HTTPFlow) and flow.websocket:
messages = flow.websocket.messages
elif isinstance(flow, TCPFlow):
elif isinstance(flow, (TCPFlow, UDPFlow)):
messages = flow.messages
else:
raise APIError(400, f"This flow has no messages.")

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

64
mitmproxy/udp.py Normal file
View File

@ -0,0 +1,64 @@
import time
from mitmproxy import connection, flow
from mitmproxy.coretypes import serializable
class UDPMessage(serializable.Serializable):
"""
An individual UDP datagram.
"""
def __init__(self, from_client, content, timestamp=None):
self.from_client = from_client
self.content = content
self.timestamp = timestamp or time.time()
@classmethod
def from_state(cls, state):
return cls(*state)
def get_state(self):
return self.from_client, self.content, self.timestamp
def set_state(self, state):
self.from_client, self.content, self.timestamp = state
def __repr__(self):
return "{direction} {content}".format(
direction="->" if self.from_client else "<-", content=repr(self.content)
)
class UDPFlow(flow.Flow):
"""
A UDPFlow is a representation of a UDP session.
"""
messages: list[UDPMessage]
"""
The messages transmitted over this connection.
The latest message can be accessed as `flow.messages[-1]` in event hooks.
"""
def __init__(
self,
client_conn: connection.Client,
server_conn: connection.Server,
live: bool = False,
):
super().__init__(client_conn, server_conn, live)
self.messages = []
_stateobject_attributes = flow.Flow._stateobject_attributes.copy()
_stateobject_attributes["messages"] = list[UDPMessage]
def __repr__(self):
return f"<UDPFlow ({len(self.messages)} messages)>"
__all__ = [
"UDPFlow",
"UDPMessage",
]

View File

@ -39,6 +39,13 @@ def tcp(level):
show(level, [f1])
@cli.command()
@click.option("--level", default=1, help="Detail level")
def udp(level):
f1 = tflow.tudpflow()
show(level, [f1])
@cli.command()
@click.option("--level", default=1, help="Detail level")
def large(level):

View File

@ -131,9 +131,9 @@ def test_check():
f.request.raw_content = None
assert "missing content" in cp.check(f)
f = tflow.ttcpflow()
f.live = False
assert "Can only replay HTTP" in cp.check(f)
for f in (tflow.ttcpflow(), tflow.tudpflow()):
f.live = False
assert "Can only replay HTTP" in cp.check(f)
async def test_start_stop(tdata):

View File

@ -171,8 +171,9 @@ def test_cut():
assert c.cut(tflows, ["response.reason"]) == [[""]]
assert c.cut(tflows, ["response.header[key]"]) == [[""]]
c = cut.Cut()
with taddons.context():
tflows = [tflow.ttcpflow()]
assert c.cut(tflows, ["request.method"]) == [[""]]
assert c.cut(tflows, ["response.status"]) == [[""]]
for f in (tflow.ttcpflow(), tflow.tudpflow()):
c = cut.Cut()
with taddons.context():
tflows = [f]
assert c.cut(tflows, ["request.method"]) == [[""]]
assert c.cut(tflows, ["response.status"]) == [[""]]

View File

@ -199,6 +199,21 @@ def test_tcp():
assert "Error in TCP" in sio.getvalue()
def test_udp():
sio = io.StringIO()
d = dumper.Dumper(sio)
with taddons.context(d) as ctx:
ctx.configure(d, flow_detail=3, showhost=True)
f = tflow.tudpflow()
d.udp_message(f)
assert "it's me" in sio.getvalue()
sio.truncate(0)
f = tflow.tudpflow(client_conn=True, err=True)
d.udp_error(f)
assert "Error in UDP" in sio.getvalue()
def test_dns():
sio = io.StringIO()
d = dumper.Dumper(sio)

View File

@ -53,6 +53,11 @@ def tcp_flow():
return tflow.ttcpflow()
@pytest.fixture
def udp_flow():
return tflow.tudpflow()
@pytest.fixture(scope="module")
def export_curl():
e = export.Export()
@ -88,6 +93,10 @@ class TestExportCurlCommand:
with pytest.raises(exceptions.CommandError):
export_curl(tcp_flow)
def test_udp(self, export_curl, udp_flow):
with pytest.raises(exceptions.CommandError):
export_curl(udp_flow)
def test_escape_single_quotes_in_body(self, export_curl):
request = tflow.tflow(
req=tutils.treq(method=b"POST", headers=(), content=b"'&#")
@ -153,6 +162,10 @@ class TestExportHttpieCommand:
with pytest.raises(exceptions.CommandError):
export.httpie_command(tcp_flow)
def test_udp(self, udp_flow):
with pytest.raises(exceptions.CommandError):
export.httpie_command(udp_flow)
def test_escape_single_quotes_in_body(self):
request = tflow.tflow(
req=tutils.treq(method=b"POST", headers=(), content=b"'&#")
@ -197,6 +210,13 @@ class TestRaw:
):
export.raw(tcp_flow)
def test_udp(self, udp_flow):
with pytest.raises(
exceptions.CommandError,
match="Can't export flow with no request or response",
):
export.raw(udp_flow)
class TestRawRequest:
def test_get(self, get_request):
@ -212,6 +232,10 @@ class TestRawRequest:
with pytest.raises(exceptions.CommandError):
export.raw_request(tcp_flow)
def test_udp(self, udp_flow):
with pytest.raises(exceptions.CommandError):
export.raw_request(udp_flow)
class TestRawResponse:
def test_get(self, get_response):
@ -226,6 +250,10 @@ class TestRawResponse:
with pytest.raises(exceptions.CommandError):
export.raw_response(tcp_flow)
def test_udp(self, udp_flow):
with pytest.raises(exceptions.CommandError):
export.raw_response(udp_flow)
def qr(f):
with open(f, "rb") as fp:

View File

@ -75,3 +75,17 @@ async def test_tcp():
f = tflow.ttcpflow()
await tctx.cycle(r, f)
assert not f.intercepted
async def test_udp():
r = intercept.Intercept()
with taddons.context(r) as tctx:
tctx.configure(r, intercept="~udp")
f = tflow.tudpflow()
await tctx.cycle(r, f)
assert f.intercepted
tctx.configure(r, intercept_active=False)
f = tflow.tudpflow()
await tctx.cycle(r, f)
assert not f.intercepted

View File

@ -47,6 +47,24 @@ def test_tcp(tmp_path):
assert len(rd(p)) == 2
def test_udp(tmp_path):
sa = save.Save()
with taddons.context(sa) as tctx:
p = str(tmp_path / "foo")
tctx.configure(sa, save_stream_file=p)
tt = tflow.tudpflow()
sa.udp_start(tt)
sa.udp_end(tt)
tt = tflow.tudpflow()
sa.udp_start(tt)
sa.udp_error(tt)
tctx.configure(sa, save_stream_file=None)
assert len(rd(p)) == 2
def test_dns(tmp_path):
sa = save.Save()
with taddons.context(sa) as tctx:

View File

@ -74,15 +74,13 @@ def test_order_generators_dns():
assert sz.generate(tf) == 0
def test_order_generators_tcp():
def order_generators_proto(tf, name):
v = view.View()
tf = tflow.ttcpflow()
rs = view.OrderRequestStart(v)
assert rs.generate(tf) == 946681200
rm = view.OrderRequestMethod(v)
assert rm.generate(tf) == "TCP"
assert rm.generate(tf) == name
ru = view.OrderRequestURL(v)
assert ru.generate(tf) == "address:22"
@ -91,6 +89,14 @@ def test_order_generators_tcp():
assert sz.generate(tf) == sum(len(m.content) for m in tf.messages)
def test_order_generators_tcp():
order_generators_proto(tflow.ttcpflow(), "TCP")
def test_order_generators_udp():
order_generators_proto(tflow.tudpflow(), "UDP")
def test_simple():
v = view.View()
f = tft(start=1)
@ -158,6 +164,21 @@ def test_simple_tcp():
assert list(v) == [f]
def test_simple_udp():
v = view.View()
f = tflow.tudpflow()
assert v.store_count() == 0
v.udp_start(f)
assert list(v) == [f]
# These all just call update
v.udp_start(f)
v.udp_message(f)
v.udp_error(f)
v.udp_end(f)
assert list(v) == [f]
def test_simple_dns():
v = view.View()
f = tflow.tdnsflow(resp=True, err=True)

View File

@ -0,0 +1,124 @@
import pytest
from mitmproxy.proxy.commands import CloseConnection, OpenConnection, SendData
from mitmproxy.proxy.events import ConnectionClosed, DataReceived
from mitmproxy.proxy.layers import udp
from mitmproxy.proxy.layers.udp import UdpMessageInjected
from mitmproxy.udp import UDPFlow, UDPMessage
from ..tutils import Placeholder, Playbook, reply
def test_open_connection(tctx):
"""
If there is no server connection yet, establish one,
because the server may send data first.
"""
assert Playbook(udp.UDPLayer(tctx, True)) << OpenConnection(tctx.server)
tctx.server.timestamp_start = 1624544785
assert Playbook(udp.UDPLayer(tctx, True)) << None
def test_open_connection_err(tctx):
f = Placeholder(UDPFlow)
assert (
Playbook(udp.UDPLayer(tctx))
<< udp.UdpStartHook(f)
>> reply()
<< OpenConnection(tctx.server)
>> reply("Connect call failed")
<< udp.UdpErrorHook(f)
>> reply()
<< CloseConnection(tctx.client)
)
def test_simple(tctx):
"""open connection, receive data, send it to peer"""
f = Placeholder(UDPFlow)
assert (
Playbook(udp.UDPLayer(tctx))
<< udp.UdpStartHook(f)
>> reply()
<< OpenConnection(tctx.server)
>> reply(None)
>> DataReceived(tctx.client, b"hello!")
<< udp.UdpMessageHook(f)
>> reply()
<< SendData(tctx.server, b"hello!")
>> DataReceived(tctx.server, b"hi")
<< udp.UdpMessageHook(f)
>> reply()
<< SendData(tctx.client, b"hi")
>> ConnectionClosed(tctx.server)
<< CloseConnection(tctx.client)
>> ConnectionClosed(tctx.client)
<< udp.UdpEndHook(f)
>> reply()
>> DataReceived(tctx.server, b"ignored")
<< None
)
assert len(f().messages) == 2
def test_receive_data_before_server_connected(tctx):
"""
assert that data received before a server connection is established
will still be forwarded.
"""
assert (
Playbook(udp.UDPLayer(tctx), hooks=False)
<< OpenConnection(tctx.server)
>> DataReceived(tctx.client, b"hello!")
>> reply(None, to=-2)
<< SendData(tctx.server, b"hello!")
)
@pytest.mark.parametrize("ignore", [True, False])
def test_ignore(tctx, ignore):
"""
no flow hooks when we set ignore.
"""
def no_flow_hooks():
assert (
Playbook(udp.UDPLayer(tctx, ignore=ignore), hooks=True)
<< OpenConnection(tctx.server)
>> reply(None)
>> DataReceived(tctx.client, b"hello!")
<< SendData(tctx.server, b"hello!")
)
if ignore:
no_flow_hooks()
else:
with pytest.raises(AssertionError):
no_flow_hooks()
def test_inject(tctx):
"""inject data into an open connection."""
f = Placeholder(UDPFlow)
assert (
Playbook(udp.UDPLayer(tctx))
<< udp.UdpStartHook(f)
>> UdpMessageInjected(f, UDPMessage(True, b"hello!"))
>> reply(to=-2)
<< OpenConnection(tctx.server)
>> reply(None)
<< udp.UdpMessageHook(f)
>> reply()
<< SendData(tctx.server, b"hello!")
# and the other way...
>> UdpMessageInjected(
f, UDPMessage(False, b"I have already done the greeting for you.")
)
<< udp.UdpMessageHook(f)
>> reply()
<< SendData(tctx.client, b"I have already done the greeting for you.")
<< None
)
assert len(f().messages) == 2

View File

@ -62,6 +62,22 @@ def test_tcp_flow(err):
assert isinstance(next(i), layers.tcp.TcpEndHook)
@pytest.mark.parametrize("err", [False, True])
def test_udp_flow(err):
f = tflow.tudpflow(err=err)
i = eventsequence.iterate(f)
assert isinstance(next(i), layers.udp.UdpStartHook)
assert len(f.messages) == 0
assert isinstance(next(i), layers.udp.UdpMessageHook)
assert len(f.messages) == 1
assert isinstance(next(i), layers.udp.UdpMessageHook)
assert len(f.messages) == 2
if err:
assert isinstance(next(i), layers.udp.UdpErrorHook)
else:
assert isinstance(next(i), layers.udp.UdpEndHook)
@pytest.mark.parametrize(
"resp, err",
[

View File

@ -400,6 +400,132 @@ class TestMatchingTCPFlow:
def test_tcp(self):
f = self.flow()
assert self.q("~tcp", f)
assert not self.q("~udp", f)
assert not self.q("~http", f)
assert not self.q("~websocket", f)
def test_ferr(self):
e = self.err()
assert self.q("~e", e)
def test_body(self):
f = self.flow()
# Messages sent by client or server
assert self.q("~b hello", f)
assert self.q("~b me", f)
assert not self.q("~b nonexistent", f)
# Messages sent by client
assert self.q("~bq hello", f)
assert not self.q("~bq me", f)
assert not self.q("~bq nonexistent", f)
# Messages sent by server
assert self.q("~bs me", f)
assert not self.q("~bs hello", f)
assert not self.q("~bs nonexistent", f)
def test_src(self):
f = self.flow()
assert self.q("~src 127.0.0.1", f)
assert not self.q("~src foobar", f)
assert self.q("~src :22", f)
assert not self.q("~src :99", f)
assert self.q("~src 127.0.0.1:22", f)
def test_dst(self):
f = self.flow()
f.server_conn = tflow.tserver_conn()
assert self.q("~dst address", f)
assert not self.q("~dst foobar", f)
assert self.q("~dst :22", f)
assert not self.q("~dst :99", f)
assert self.q("~dst address:22", f)
def test_and(self):
f = self.flow()
f.server_conn = tflow.tserver_conn()
assert self.q("~b hello & ~b me", f)
assert not self.q("~src wrongaddress & ~b hello", f)
assert self.q("(~src :22 & ~dst :22) & ~b hello", f)
assert not self.q("(~src address:22 & ~dst :22) & ~b nonexistent", f)
assert not self.q("(~src address:22 & ~dst :99) & ~b hello", f)
def test_or(self):
f = self.flow()
f.server_conn = tflow.tserver_conn()
assert self.q("~b hello | ~b me", f)
assert self.q("~src :22 | ~b me", f)
assert not self.q("~src :99 | ~dst :99", f)
assert self.q("(~src :22 | ~dst :22) | ~b me", f)
def test_not(self):
f = self.flow()
assert not self.q("! ~src :22", f)
assert self.q("! ~src :99", f)
assert self.q("!~src :99 !~src :99", f)
assert not self.q("!~src :99 !~src :22", f)
def test_request(self):
f = self.flow()
assert not self.q("~q", f)
def test_response(self):
f = self.flow()
assert not self.q("~s", f)
def test_headers(self):
f = self.flow()
assert not self.q("~h whatever", f)
# Request headers
assert not self.q("~hq whatever", f)
# Response headers
assert not self.q("~hs whatever", f)
def test_content_type(self):
f = self.flow()
assert not self.q("~t whatever", f)
# Request content-type
assert not self.q("~tq whatever", f)
# Response content-type
assert not self.q("~ts whatever", f)
def test_code(self):
f = self.flow()
assert not self.q("~c 200", f)
def test_domain(self):
f = self.flow()
assert not self.q("~d whatever", f)
def test_method(self):
f = self.flow()
assert not self.q("~m whatever", f)
def test_url(self):
f = self.flow()
assert not self.q("~u whatever", f)
class TestMatchingUDPFlow:
def flow(self):
return tflow.tudpflow()
def err(self):
return tflow.tudpflow(err=True)
def q(self, q, o):
return flowfilter.parse(q)(o)
def test_tcp(self):
f = self.flow()
assert self.q("~udp", f)
assert not self.q("~tcp", f)
assert not self.q("~http", f)
assert not self.q("~websocket", f)

View File

@ -0,0 +1,58 @@
import pytest
from mitmproxy import udp
from mitmproxy import flowfilter
from mitmproxy.test import tflow
class TestUDPFlow:
def test_copy(self):
f = tflow.tudpflow()
f.get_state()
f2 = f.copy()
a = f.get_state()
b = f2.get_state()
del a["id"]
del b["id"]
assert a == b
assert not f == f2
assert f is not f2
assert f.messages is not f2.messages
for m in f.messages:
assert m.get_state()
m2 = m.copy()
assert not m == m2
assert m is not m2
a = m.get_state()
b = m2.get_state()
assert a == b
m = udp.UDPMessage(False, "foo")
m.set_state(f.messages[0].get_state())
assert m.timestamp == f.messages[0].timestamp
f = tflow.tudpflow(err=True)
f2 = f.copy()
assert f is not f2
assert f.error.get_state() == f2.error.get_state()
assert f.error is not f2.error
def test_match(self):
f = tflow.tudpflow()
assert not flowfilter.match("~b nonexistent", f)
assert flowfilter.match(None, f)
assert not flowfilter.match("~b nonexistent", f)
f = tflow.tudpflow(err=True)
assert flowfilter.match("~e", f)
with pytest.raises(ValueError):
flowfilter.match("~", f)
def test_repr(self):
f = tflow.tudpflow()
assert "UDPFlow" in repr(f)
assert "-> " in repr(f.messages[0])

View File

@ -57,6 +57,11 @@ def test_generate_tflow_js(tdata):
tf_tcp.client_conn.id = "8be32b99-a0b3-446e-93bc-b29982fe1322"
tf_tcp.server_conn.id = "e33bb2cd-c07e-4214-9a8e-3a8f85f25200"
tf_udp = tflow.tudpflow(err=True)
tf_udp.id = "f9f7b2b9-7727-4477-822d-d3526e5b8951"
tf_udp.client_conn.id = "0a8833da-88e4-429d-ac54-61cda8a7f91c"
tf_udp.server_conn.id = "c49f9c2b-a729-4b16-9212-d181717e294b"
tf_dns = tflow.tdnsflow(resp=True, err=True)
tf_dns.id = "5434da94-1017-42fa-872d-a189508d48e4"
tf_dns.client_conn.id = "0b4cc0a3-6acb-4880-81c0-1644084126fc"
@ -65,13 +70,16 @@ def test_generate_tflow_js(tdata):
# language=TypeScript
content = (
"/** Auto-generated by test_app.py:test_generate_tflow_js */\n"
"import {HTTPFlow, TCPFlow, DNSFlow} from '../../flow';\n"
"import {HTTPFlow, TCPFlow, UDPFlow, DNSFlow} from '../../flow';\n"
"export function THTTPFlow(): Required<HTTPFlow> {\n"
" return %s\n"
"}\n"
"export function TTCPFlow(): Required<TCPFlow> {\n"
" return %s\n"
"}\n"
"export function TUDPFlow(): Required<UDPFlow> {\n"
" return %s\n"
"}\n"
"export function TDNSFlow(): Required<DNSFlow> {\n"
" return %s\n"
"}\n"
@ -82,6 +90,9 @@ def test_generate_tflow_js(tdata):
textwrap.indent(
json.dumps(app.flow_to_json(tf_tcp), indent=4, sort_keys=True), " "
),
textwrap.indent(
json.dumps(app.flow_to_json(tf_udp), indent=4, sort_keys=True), " "
),
textwrap.indent(
json.dumps(app.flow_to_json(tf_dns), indent=4, sort_keys=True), " "
),

View File

@ -53,6 +53,10 @@
background-image: url(images/resourceTcpIcon.png);
}
.resource-icon-udp {
background-image: url(images/resourceUdpIcon.png);
}
.resource-icon-dns {
background-image: url(images/resourceDnsIcon.png);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

View File

@ -46,4 +46,12 @@ test("FlowView", async () => {
fireEvent.click(screen.getByText("Error"));
expect(asFragment()).toMatchSnapshot();
store.dispatch(flowActions.select(store.getState().flows.list[4].id));
fireEvent.click(screen.getByText("UDP Messages"));
expect(asFragment()).toMatchSnapshot();
fireEvent.click(screen.getByText("Error"));
expect(asFragment()).toMatchSnapshot();
});

View File

@ -26,7 +26,7 @@ exports[`MainMenu 1`] = `
class="form-control"
placeholder="Search"
type="text"
value="~u /second | ~tcp | ~dns"
value="~u /second | ~tcp | ~dns | ~udp"
/>
</div>
<div

View File

@ -1430,3 +1430,127 @@ exports[`FlowView 11`] = `
</div>
</DocumentFragment>
`;
exports[`FlowView 12`] = `
<DocumentFragment>
<div
class="flow-detail"
>
<nav
class="nav-tabs nav-tabs-sm"
>
<a
class="active"
href="#"
>
UDP Messages
</a>
<a
class=""
href="#"
>
Error
</a>
<a
class=""
href="#"
>
Connection
</a>
<a
class=""
href="#"
>
Timing
</a>
</nav>
<section
class="udp"
>
<h4>
UDP Data
</h4>
<div
class="contentview"
>
<div
class="controls"
>
<h5>
2 Messages
</h5>
<a
class="btn btn-default btn-xs"
href="#"
>
<span>
<i
class="fa fa-fw fa-files-o"
/>
 
<b>
View:
</b>
auto
<span
class="caret"
/>
</span>
</a>
</div>
</div>
</section>
</div>
</DocumentFragment>
`;
exports[`FlowView 13`] = `
<DocumentFragment>
<div
class="flow-detail"
>
<nav
class="nav-tabs nav-tabs-sm"
>
<a
class=""
href="#"
>
UDP Messages
</a>
<a
class="active"
href="#"
>
Error
</a>
<a
class=""
href="#"
>
Connection
</a>
<a
class=""
href="#"
>
Timing
</a>
</nav>
<section
class="error"
>
<div
class="alert alert-warning"
>
error
<div>
<small>
1999-12-31 23:00:07.000
</small>
</div>
</div>
</section>
</div>
</DocumentFragment>
`;

View File

@ -1,5 +1,5 @@
/** Auto-generated by test_app.py:test_generate_tflow_js */
import {HTTPFlow, TCPFlow, DNSFlow} from '../../flow';
import {HTTPFlow, TCPFlow, UDPFlow, DNSFlow} from '../../flow';
export function THTTPFlow(): Required<HTTPFlow> {
return {
"client_conn": {
@ -226,6 +226,72 @@ export function TTCPFlow(): Required<TCPFlow> {
"type": "tcp"
}
}
export function TUDPFlow(): Required<UDPFlow> {
return {
"client_conn": {
"alpn": "http/1.1",
"cert": undefined,
"cipher": "cipher",
"id": "0a8833da-88e4-429d-ac54-61cda8a7f91c",
"peername": [
"127.0.0.1",
22
],
"sni": "address",
"sockname": [
"",
0
],
"timestamp_end": 946681206,
"timestamp_start": 946681200,
"timestamp_tls_setup": 946681201,
"tls_established": true,
"tls_version": "TLSv1.2"
},
"comment": "",
"error": {
"msg": "error",
"timestamp": 946681207.0
},
"id": "f9f7b2b9-7727-4477-822d-d3526e5b8951",
"intercepted": false,
"is_replay": undefined,
"marked": "",
"messages_meta": {
"contentLength": 12,
"count": 2,
"timestamp_last": 946681204.5
},
"modified": false,
"server_conn": {
"address": [
"address",
22
],
"alpn": undefined,
"cert": undefined,
"cipher": undefined,
"id": "c49f9c2b-a729-4b16-9212-d181717e294b",
"peername": [
"192.168.0.1",
22
],
"sni": "address",
"sockname": [
"address",
22
],
"timestamp_end": 946681205,
"timestamp_start": 946681202,
"timestamp_tcp_setup": 946681203,
"timestamp_tls_setup": 946681204,
"tls_established": true,
"tls_version": "TLSv1.2"
},
"timestamp_created": 946681200,
"type": "udp"
}
}
export function TDNSFlow(): Required<DNSFlow> {
return {
"client_conn": {

View File

@ -1,20 +1,21 @@
import thunk from 'redux-thunk'
import configureStore, {MockStoreCreator, MockStoreEnhanced} from 'redux-mock-store'
import {ConnectionState} from '../../ducks/connection'
import {TDNSFlow, THTTPFlow, TTCPFlow} from './_tflow'
import {TDNSFlow, THTTPFlow, TTCPFlow, TUDPFlow} from './_tflow'
import {AppDispatch, RootState} from "../../ducks";
import {DNSFlow, HTTPFlow, TCPFlow} from "../../flow";
import {DNSFlow, HTTPFlow, TCPFlow, UDPFlow} from "../../flow";
import {defaultState as defaultConf} from "../../ducks/conf"
import {defaultState as defaultOptions} from "../../ducks/options"
const mockStoreCreator: MockStoreCreator<RootState, AppDispatch> = configureStore([thunk])
export {THTTPFlow as TFlow, TTCPFlow}
export {THTTPFlow as TFlow, TTCPFlow, TUDPFlow}
const tflow0: HTTPFlow = THTTPFlow();
const tflow1: HTTPFlow = THTTPFlow();
const tflow2: TCPFlow = TTCPFlow();
const tflow3: DNSFlow = TDNSFlow();
const tflow4: UDPFlow = TUDPFlow();
tflow0.modified = true
tflow0.intercepted = true
tflow1.id = "flow2";
@ -78,25 +79,28 @@ export const testState: RootState = {
[tflow1.id]: tflow1,
[tflow2.id]: tflow2,
[tflow3.id]: tflow3,
[tflow4.id]: tflow4,
},
filter: '~u /second | ~tcp | ~dns',
filter: '~u /second | ~tcp | ~dns | ~udp',
highlight: '~u /path',
sort: {
desc: true,
column: "path"
},
view: [tflow1, tflow2, tflow3],
list: [tflow0, tflow1, tflow2, tflow3],
view: [tflow1, tflow2, tflow3, tflow4],
list: [tflow0, tflow1, tflow2, tflow3, tflow4],
listIndex: {
[tflow0.id]: 0,
[tflow1.id]: 1,
[tflow2.id]: 2,
[tflow3.id]: 3
[tflow3.id]: 3,
[tflow4.id]: 4,
},
viewIndex: {
[tflow1.id]: 0,
[tflow2.id]: 1,
[tflow3.id]: 2,
[tflow4.id]: 3,
},
},
connection: {

View File

@ -1,5 +1,5 @@
import * as utils from '../../flow/utils'
import {TFlow, TTCPFlow} from "../ducks/tutils";
import {TFlow, TTCPFlow, TUDPFlow} from "../ducks/tutils";
import {TDNSFlow, THTTPFlow} from "../ducks/_tflow";
import {HTTPFlow} from "../../flow";
@ -73,6 +73,7 @@ describe('isValidHttpVersion', () => {
it('should be possible to get a start time', () => {
expect(utils.startTime(THTTPFlow())).toEqual(946681200);
expect(utils.startTime(TTCPFlow())).toEqual(946681200);
expect(utils.startTime(TUDPFlow())).toEqual(946681200);
expect(utils.startTime(TDNSFlow())).toEqual(946681200);
})
@ -82,11 +83,13 @@ it('should be possible to get an end time', () => {
f.websocket = undefined;
expect(utils.endTime(f)).toEqual(946681203);
expect(utils.endTime(TTCPFlow())).toEqual(946681205);
expect(utils.endTime(TUDPFlow())).toEqual(946681205);
expect(utils.endTime(TDNSFlow())).toEqual(946681201);
})
it('should be possible to get a total size', () => {
expect(utils.getTotalSize(THTTPFlow())).toEqual(43);
expect(utils.getTotalSize(TTCPFlow())).toEqual(12);
expect(utils.getTotalSize(TUDPFlow())).toEqual(12);
expect(utils.getTotalSize(TDNSFlow())).toEqual(8);
})

View File

@ -37,7 +37,7 @@ icon.headerName = ''
icon.sortKey = flow => getIcon(flow)
const getIcon = (flow: Flow): string => {
if (flow.type === "tcp" || flow.type === "dns") {
if (flow.type !== "http") {
return `resource-icon-${flow.type}`
}
if (flow.websocket) {
@ -76,6 +76,7 @@ const mainPath = (flow: Flow): string => {
case "http":
return RequestUtils.pretty_url(flow.request)
case "tcp":
case "udp":
return `${flow.client_conn.peername.join(':')}${flow.server_conn?.address?.join(':')}`
case "dns":
return `${flow.request.questions.map(q => `${q.name} ${q.type}`).join(", ")} = ${(flow.response?.answers.map(q => q.data).join(", ") ?? "...") || "?"}`

View File

@ -12,6 +12,7 @@ import {useAppDispatch, useAppSelector} from "../ducks";
import {Flow} from "../flow";
import classnames from "classnames";
import TcpMessages from "./FlowView/TcpMessages";
import UdpMessages from "./FlowView/UdpMessages";
type TabProps = {
flow: Flow
@ -24,7 +25,8 @@ export const allTabs: { [name: string]: FunctionComponent<TabProps> & { displayN
connection: Connection,
timing: Timing,
websocket: WebSocket,
messages: TcpMessages,
tcpmessages: TcpMessages,
udpmessages: UdpMessages,
dnsrequest: DnsRequest,
dnsresponse: DnsResponse,
}
@ -36,7 +38,10 @@ export function tabsForFlow(flow: Flow): string[] {
tabs = ['request', 'response', 'websocket'].filter(k => flow[k])
break
case "tcp":
tabs = ["messages"]
tabs = ["tcpmessages"]
break
case "udp":
tabs = ["udpmessages"]
break
case "dns":
tabs = ['request', 'response'].filter(k => flow[k]).map(s => "dns" + s)

View File

@ -27,8 +27,8 @@ const Questions: React.FC<{
</tr>
</thead>
<tbody>
{message.questions.map(question => (
<tr key={question.name}>
{message.questions.map((question, index) => (
<tr key={index}>
<td>{question.name}</td>
<td>{question.type}</td>
<td>{question.class}</td>
@ -57,8 +57,8 @@ const ResourceRecords: React.FC<{
</tr>
</thead>
<tbody>
{values.map(rr => (
<tr key={rr.name}>
{values.map((rr, index) => (
<tr key={index}>
<td>{rr.name}</td>
<td>{rr.type}</td>
<td>{rr.class}</td>

View File

@ -0,0 +1,14 @@
import {UDPFlow} from "../../flow";
import * as React from "react";
import Messages from "./Messages";
export default function UdpMessages({flow}: { flow: UDPFlow }) {
return (
<section className="udp">
<h4>UDP Data</h4>
<Messages flow={flow} messages_meta={flow.messages_meta}/>
</section>
)
}
UdpMessages.displayName = "UDP Messages"

View File

@ -135,52 +135,55 @@ export default (function() {
peg$c95 = "~tcp",
peg$c96 = { type: "literal", value: "~tcp", description: "\"~tcp\"" },
peg$c97 = function() { return tcpFilter; },
peg$c98 = "~tq",
peg$c99 = { type: "literal", value: "~tq", description: "\"~tq\"" },
peg$c100 = function(s) { return requestContentType(s); },
peg$c101 = "~ts",
peg$c102 = { type: "literal", value: "~ts", description: "\"~ts\"" },
peg$c103 = function(s) { return responseContentType(s); },
peg$c104 = "~t",
peg$c105 = { type: "literal", value: "~t", description: "\"~t\"" },
peg$c106 = function(s) { return contentType(s); },
peg$c107 = "~u",
peg$c108 = { type: "literal", value: "~u", description: "\"~u\"" },
peg$c109 = function(s) { return url(s); },
peg$c110 = "~websocket",
peg$c111 = { type: "literal", value: "~websocket", description: "\"~websocket\"" },
peg$c112 = function() { return websocketFilter; },
peg$c113 = { type: "other", description: "integer" },
peg$c114 = /^['"]/,
peg$c115 = { type: "class", value: "['\"]", description: "['\"]" },
peg$c116 = /^[0-9]/,
peg$c117 = { type: "class", value: "[0-9]", description: "[0-9]" },
peg$c118 = function(digits) { return parseInt(digits.join(""), 10); },
peg$c119 = { type: "other", description: "string" },
peg$c120 = "\"",
peg$c121 = { type: "literal", value: "\"", description: "\"\\\"\"" },
peg$c122 = function(chars) { return chars.join(""); },
peg$c123 = "'",
peg$c124 = { type: "literal", value: "'", description: "\"'\"" },
peg$c125 = /^["\\]/,
peg$c126 = { type: "class", value: "[\"\\\\]", description: "[\"\\\\]" },
peg$c127 = { type: "any", description: "any character" },
peg$c128 = function(char) { return char; },
peg$c129 = "\\",
peg$c130 = { type: "literal", value: "\\", description: "\"\\\\\"" },
peg$c131 = /^['\\]/,
peg$c132 = { type: "class", value: "['\\\\]", description: "['\\\\]" },
peg$c133 = /^['"\\]/,
peg$c134 = { type: "class", value: "['\"\\\\]", description: "['\"\\\\]" },
peg$c135 = "n",
peg$c136 = { type: "literal", value: "n", description: "\"n\"" },
peg$c137 = function() { return "\n"; },
peg$c138 = "r",
peg$c139 = { type: "literal", value: "r", description: "\"r\"" },
peg$c140 = function() { return "\r"; },
peg$c141 = "t",
peg$c142 = { type: "literal", value: "t", description: "\"t\"" },
peg$c143 = function() { return "\t"; },
peg$c98 = "~udp",
peg$c99 = { type: "literal", value: "~udp", description: "\"~udp\"" },
peg$c100 = function() { return udpFilter; },
peg$c101 = "~tq",
peg$c102 = { type: "literal", value: "~tq", description: "\"~tq\"" },
peg$c103 = function(s) { return requestContentType(s); },
peg$c104 = "~ts",
peg$c105 = { type: "literal", value: "~ts", description: "\"~ts\"" },
peg$c106 = function(s) { return responseContentType(s); },
peg$c107 = "~t",
peg$c108 = { type: "literal", value: "~t", description: "\"~t\"" },
peg$c109 = function(s) { return contentType(s); },
peg$c110 = "~u",
peg$c111 = { type: "literal", value: "~u", description: "\"~u\"" },
peg$c112 = function(s) { return url(s); },
peg$c113 = "~websocket",
peg$c114 = { type: "literal", value: "~websocket", description: "\"~websocket\"" },
peg$c115 = function() { return websocketFilter; },
peg$c116 = { type: "other", description: "integer" },
peg$c117 = /^['"]/,
peg$c118 = { type: "class", value: "['\"]", description: "['\"]" },
peg$c119 = /^[0-9]/,
peg$c120 = { type: "class", value: "[0-9]", description: "[0-9]" },
peg$c121 = function(digits) { return parseInt(digits.join(""), 10); },
peg$c122 = { type: "other", description: "string" },
peg$c123 = "\"",
peg$c124 = { type: "literal", value: "\"", description: "\"\\\"\"" },
peg$c125 = function(chars) { return chars.join(""); },
peg$c126 = "'",
peg$c127 = { type: "literal", value: "'", description: "\"'\"" },
peg$c128 = /^["\\]/,
peg$c129 = { type: "class", value: "[\"\\\\]", description: "[\"\\\\]" },
peg$c130 = { type: "any", description: "any character" },
peg$c131 = function(char) { return char; },
peg$c132 = "\\",
peg$c133 = { type: "literal", value: "\\", description: "\"\\\\\"" },
peg$c134 = /^['\\]/,
peg$c135 = { type: "class", value: "['\\\\]", description: "['\\\\]" },
peg$c136 = /^['"\\]/,
peg$c137 = { type: "class", value: "['\"\\\\]", description: "['\"\\\\]" },
peg$c138 = "n",
peg$c139 = { type: "literal", value: "n", description: "\"n\"" },
peg$c140 = function() { return "\n"; },
peg$c141 = "r",
peg$c142 = { type: "literal", value: "r", description: "\"r\"" },
peg$c143 = function() { return "\r"; },
peg$c144 = "t",
peg$c145 = { type: "literal", value: "t", description: "\"t\"" },
peg$c146 = function() { return "\t"; },
peg$currPos = 0,
peg$savedPos = 0,
@ -1354,42 +1357,18 @@ export default (function() {
s0 = s1;
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.substr(peg$currPos, 3) === peg$c98) {
if (input.substr(peg$currPos, 4) === peg$c98) {
s1 = peg$c98;
peg$currPos += 3;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c99); }
}
if (s1 !== peg$FAILED) {
s2 = [];
s3 = peg$parsews();
if (s3 !== peg$FAILED) {
while (s3 !== peg$FAILED) {
s2.push(s3);
s3 = peg$parsews();
}
} else {
s2 = peg$FAILED;
}
if (s2 !== peg$FAILED) {
s3 = peg$parseStringLiteral();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c100(s3);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
peg$savedPos = s0;
s1 = peg$c100();
}
s0 = s1;
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.substr(peg$currPos, 3) === peg$c101) {
@ -1430,9 +1409,9 @@ export default (function() {
}
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.substr(peg$currPos, 2) === peg$c104) {
if (input.substr(peg$currPos, 3) === peg$c104) {
s1 = peg$c104;
peg$currPos += 2;
peg$currPos += 3;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c105); }
@ -1506,26 +1485,65 @@ export default (function() {
}
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.substr(peg$currPos, 10) === peg$c110) {
if (input.substr(peg$currPos, 2) === peg$c110) {
s1 = peg$c110;
peg$currPos += 10;
peg$currPos += 2;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c111); }
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c112();
s2 = [];
s3 = peg$parsews();
if (s3 !== peg$FAILED) {
while (s3 !== peg$FAILED) {
s2.push(s3);
s3 = peg$parsews();
}
} else {
s2 = peg$FAILED;
}
if (s2 !== peg$FAILED) {
s3 = peg$parseStringLiteral();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c112(s3);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
s0 = s1;
if (s0 === peg$FAILED) {
s0 = peg$currPos;
s1 = peg$parseStringLiteral();
if (input.substr(peg$currPos, 10) === peg$c113) {
s1 = peg$c113;
peg$currPos += 10;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c114); }
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c109(s1);
s1 = peg$c115();
}
s0 = s1;
if (s0 === peg$FAILED) {
s0 = peg$currPos;
s1 = peg$parseStringLiteral();
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c112(s1);
}
s0 = s1;
}
}
}
}
@ -1565,53 +1583,53 @@ export default (function() {
peg$silentFails++;
s0 = peg$currPos;
if (peg$c114.test(input.charAt(peg$currPos))) {
if (peg$c117.test(input.charAt(peg$currPos))) {
s1 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c115); }
if (peg$silentFails === 0) { peg$fail(peg$c118); }
}
if (s1 === peg$FAILED) {
s1 = null;
}
if (s1 !== peg$FAILED) {
s2 = [];
if (peg$c116.test(input.charAt(peg$currPos))) {
if (peg$c119.test(input.charAt(peg$currPos))) {
s3 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c117); }
if (peg$silentFails === 0) { peg$fail(peg$c120); }
}
if (s3 !== peg$FAILED) {
while (s3 !== peg$FAILED) {
s2.push(s3);
if (peg$c116.test(input.charAt(peg$currPos))) {
if (peg$c119.test(input.charAt(peg$currPos))) {
s3 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c117); }
if (peg$silentFails === 0) { peg$fail(peg$c120); }
}
}
} else {
s2 = peg$FAILED;
}
if (s2 !== peg$FAILED) {
if (peg$c114.test(input.charAt(peg$currPos))) {
if (peg$c117.test(input.charAt(peg$currPos))) {
s3 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c115); }
if (peg$silentFails === 0) { peg$fail(peg$c118); }
}
if (s3 === peg$FAILED) {
s3 = null;
}
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c118(s2);
s1 = peg$c121(s2);
s0 = s1;
} else {
peg$currPos = s0;
@ -1628,7 +1646,7 @@ export default (function() {
peg$silentFails--;
if (s0 === peg$FAILED) {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c113); }
if (peg$silentFails === 0) { peg$fail(peg$c116); }
}
return s0;
@ -1640,11 +1658,11 @@ export default (function() {
peg$silentFails++;
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 34) {
s1 = peg$c120;
s1 = peg$c123;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c121); }
if (peg$silentFails === 0) { peg$fail(peg$c124); }
}
if (s1 !== peg$FAILED) {
s2 = [];
@ -1655,15 +1673,15 @@ export default (function() {
}
if (s2 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 34) {
s3 = peg$c120;
s3 = peg$c123;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c121); }
if (peg$silentFails === 0) { peg$fail(peg$c124); }
}
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c122(s2);
s1 = peg$c125(s2);
s0 = s1;
} else {
peg$currPos = s0;
@ -1680,11 +1698,11 @@ export default (function() {
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 39) {
s1 = peg$c123;
s1 = peg$c126;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c124); }
if (peg$silentFails === 0) { peg$fail(peg$c127); }
}
if (s1 !== peg$FAILED) {
s2 = [];
@ -1695,15 +1713,15 @@ export default (function() {
}
if (s2 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 39) {
s3 = peg$c123;
s3 = peg$c126;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c124); }
if (peg$silentFails === 0) { peg$fail(peg$c127); }
}
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c122(s2);
s1 = peg$c125(s2);
s0 = s1;
} else {
peg$currPos = s0;
@ -1742,7 +1760,7 @@ export default (function() {
}
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c122(s2);
s1 = peg$c125(s2);
s0 = s1;
} else {
peg$currPos = s0;
@ -1757,7 +1775,7 @@ export default (function() {
peg$silentFails--;
if (s0 === peg$FAILED) {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c119); }
if (peg$silentFails === 0) { peg$fail(peg$c122); }
}
return s0;
@ -1769,12 +1787,12 @@ export default (function() {
s0 = peg$currPos;
s1 = peg$currPos;
peg$silentFails++;
if (peg$c125.test(input.charAt(peg$currPos))) {
if (peg$c128.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c126); }
if (peg$silentFails === 0) { peg$fail(peg$c129); }
}
peg$silentFails--;
if (s2 === peg$FAILED) {
@ -1789,11 +1807,11 @@ export default (function() {
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c127); }
if (peg$silentFails === 0) { peg$fail(peg$c130); }
}
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c128(s2);
s1 = peg$c131(s2);
s0 = s1;
} else {
peg$currPos = s0;
@ -1806,17 +1824,17 @@ export default (function() {
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 92) {
s1 = peg$c129;
s1 = peg$c132;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c130); }
if (peg$silentFails === 0) { peg$fail(peg$c133); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseEscapeSequence();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c128(s2);
s1 = peg$c131(s2);
s0 = s1;
} else {
peg$currPos = s0;
@ -1837,12 +1855,12 @@ export default (function() {
s0 = peg$currPos;
s1 = peg$currPos;
peg$silentFails++;
if (peg$c131.test(input.charAt(peg$currPos))) {
if (peg$c134.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c132); }
if (peg$silentFails === 0) { peg$fail(peg$c135); }
}
peg$silentFails--;
if (s2 === peg$FAILED) {
@ -1857,11 +1875,11 @@ export default (function() {
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c127); }
if (peg$silentFails === 0) { peg$fail(peg$c130); }
}
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c128(s2);
s1 = peg$c131(s2);
s0 = s1;
} else {
peg$currPos = s0;
@ -1874,17 +1892,17 @@ export default (function() {
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 92) {
s1 = peg$c129;
s1 = peg$c132;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c130); }
if (peg$silentFails === 0) { peg$fail(peg$c133); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseEscapeSequence();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c128(s2);
s1 = peg$c131(s2);
s0 = s1;
} else {
peg$currPos = s0;
@ -1919,11 +1937,11 @@ export default (function() {
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c127); }
if (peg$silentFails === 0) { peg$fail(peg$c130); }
}
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c128(s2);
s1 = peg$c131(s2);
s0 = s1;
} else {
peg$currPos = s0;
@ -1940,53 +1958,53 @@ export default (function() {
function peg$parseEscapeSequence() {
var s0, s1;
if (peg$c133.test(input.charAt(peg$currPos))) {
if (peg$c136.test(input.charAt(peg$currPos))) {
s0 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c134); }
if (peg$silentFails === 0) { peg$fail(peg$c137); }
}
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 110) {
s1 = peg$c135;
s1 = peg$c138;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c136); }
if (peg$silentFails === 0) { peg$fail(peg$c139); }
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c137();
s1 = peg$c140();
}
s0 = s1;
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 114) {
s1 = peg$c138;
s1 = peg$c141;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c139); }
if (peg$silentFails === 0) { peg$fail(peg$c142); }
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c140();
s1 = peg$c143();
}
s0 = s1;
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 116) {
s1 = peg$c141;
s1 = peg$c144;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c142); }
if (peg$silentFails === 0) { peg$fail(peg$c145); }
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c143();
s1 = peg$c146();
}
s0 = s1;
}
@ -2257,6 +2275,12 @@ export default (function() {
}
tcpFilter.desc = "is a TCP Flow";
// ~udp
function udpFilter(flow){
return flow.type === "udp";
}
udpFilter.desc = "is a UDP Flow";
// ~tq
function requestContentType(regex){
regex = new RegExp(regex, "i");

View File

@ -260,6 +260,12 @@ function tcpFilter(flow){
}
tcpFilter.desc = "is a TCP Flow";
// ~udp
function udpFilter(flow){
return flow.type === "udp";
}
udpFilter.desc = "is a UDP Flow";
// ~tq
function requestContentType(regex){
regex = new RegExp(regex, "i");
@ -374,6 +380,7 @@ Expr
/ "~src" ws+ s:StringLiteral { return source(s); }
/ "~s" { return responseFilter; }
/ "~tcp" { return tcpFilter; }
/ "~udp" { return udpFilter; }
/ "~tq" ws+ s:StringLiteral { return requestContentType(s); }
/ "~ts" ws+ s:StringLiteral { return responseContentType(s); }
/ "~t" ws+ s:StringLiteral { return contentType(s); }

View File

@ -15,7 +15,7 @@ interface _Flow {
error?: Error
}
export type Flow = HTTPFlow | TCPFlow | DNSFlow;
export type Flow = HTTPFlow | TCPFlow | UDPFlow | DNSFlow;
export interface HTTPFlow extends _Flow {
type: "http"
@ -29,6 +29,11 @@ export interface TCPFlow extends _Flow {
messages_meta: MessagesMeta,
}
export interface UDPFlow extends _Flow {
type: "udp"
messages_meta: MessagesMeta,
}
export interface Error {
msg: string
timestamp: number

View File

@ -133,6 +133,7 @@ export function startTime(flow: Flow): number | undefined {
case "http":
return flow.request.timestamp_start
case "tcp":
case "udp":
return flow.client_conn.timestamp_start
case "dns":
return flow.request.timestamp
@ -153,6 +154,7 @@ export function endTime(flow: Flow): number | undefined {
}
return undefined
case "tcp":
case "udp":
return flow.server_conn?.timestamp_end
case "dns":
return flow.response?.timestamp
@ -172,6 +174,7 @@ export const getTotalSize = (flow: Flow): number => {
}
return total
case "tcp":
case "udp":
return flow.messages_meta.contentLength || 0
case "dns":
return flow.response?.size ?? 0