2025-12-25 upload
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from mitmproxy.tools.console import master
|
||||
|
||||
__all__ = ["master"]
|
||||
@@ -0,0 +1,260 @@
|
||||
import abc
|
||||
from collections.abc import Sequence
|
||||
from typing import NamedTuple
|
||||
|
||||
import urwid
|
||||
from urwid.text_layout import calc_coords
|
||||
|
||||
import mitmproxy.command
|
||||
import mitmproxy.flow
|
||||
import mitmproxy.master
|
||||
import mitmproxy.types
|
||||
|
||||
|
||||
class Completer:
|
||||
@abc.abstractmethod
|
||||
def cycle(self, forward: bool = True) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ListCompleter(Completer):
|
||||
def __init__(
|
||||
self,
|
||||
start: str,
|
||||
options: Sequence[str],
|
||||
) -> None:
|
||||
self.start = start
|
||||
self.options: list[str] = []
|
||||
for o in options:
|
||||
if o.startswith(start):
|
||||
self.options.append(o)
|
||||
self.options.sort()
|
||||
self.pos = -1
|
||||
|
||||
def cycle(self, forward: bool = True) -> str:
|
||||
if not self.options:
|
||||
return self.start
|
||||
if self.pos == -1:
|
||||
self.pos = 0 if forward else len(self.options) - 1
|
||||
else:
|
||||
delta = 1 if forward else -1
|
||||
self.pos = (self.pos + delta) % len(self.options)
|
||||
return self.options[self.pos]
|
||||
|
||||
|
||||
class CompletionState(NamedTuple):
|
||||
completer: Completer
|
||||
parsed: Sequence[mitmproxy.command.ParseResult]
|
||||
|
||||
|
||||
class CommandBuffer:
|
||||
def __init__(self, master: mitmproxy.master.Master, start: str = "") -> None:
|
||||
self.master = master
|
||||
self.text = start
|
||||
# Cursor is always within the range [0:len(buffer)].
|
||||
self._cursor = len(self.text)
|
||||
self.completion: CompletionState | None = None
|
||||
|
||||
@property
|
||||
def cursor(self) -> int:
|
||||
return self._cursor
|
||||
|
||||
@cursor.setter
|
||||
def cursor(self, x) -> None:
|
||||
if x < 0:
|
||||
self._cursor = 0
|
||||
elif x > len(self.text):
|
||||
self._cursor = len(self.text)
|
||||
else:
|
||||
self._cursor = x
|
||||
|
||||
def set_text(self, text: str) -> None:
|
||||
self.text = text
|
||||
self._cursor = len(self.text)
|
||||
self.render()
|
||||
|
||||
def render(self):
|
||||
parts, remaining = self.master.commands.parse_partial(self.text)
|
||||
ret = []
|
||||
if not parts:
|
||||
# Means we just received the leader, so we need to give a blank
|
||||
# text to the widget to render or it crashes
|
||||
ret.append(("text", ""))
|
||||
else:
|
||||
for p in parts:
|
||||
if p.valid:
|
||||
if p.type == mitmproxy.types.Cmd:
|
||||
ret.append(("commander_command", p.value))
|
||||
else:
|
||||
ret.append(("text", p.value))
|
||||
elif p.value:
|
||||
ret.append(("commander_invalid", p.value))
|
||||
|
||||
if remaining:
|
||||
if parts[-1].type != mitmproxy.types.Space:
|
||||
ret.append(("text", " "))
|
||||
for param in remaining:
|
||||
ret.append(("commander_hint", f"{param} "))
|
||||
|
||||
return ret
|
||||
|
||||
def left(self) -> None:
|
||||
self.cursor = self.cursor - 1
|
||||
|
||||
def right(self) -> None:
|
||||
self.cursor = self.cursor + 1
|
||||
|
||||
def cycle_completion(self, forward: bool = True) -> None:
|
||||
if not self.completion:
|
||||
parts, remaining = self.master.commands.parse_partial(
|
||||
self.text[: self.cursor]
|
||||
)
|
||||
if parts and parts[-1].type != mitmproxy.types.Space:
|
||||
type_to_complete = parts[-1].type
|
||||
cycle_prefix = parts[-1].value
|
||||
parsed = parts[:-1]
|
||||
elif remaining:
|
||||
type_to_complete = remaining[0].type
|
||||
cycle_prefix = ""
|
||||
parsed = parts
|
||||
else:
|
||||
return
|
||||
ct = mitmproxy.types.CommandTypes.get(type_to_complete, None)
|
||||
if ct:
|
||||
self.completion = CompletionState(
|
||||
completer=ListCompleter(
|
||||
cycle_prefix,
|
||||
ct.completion(
|
||||
self.master.commands, type_to_complete, cycle_prefix
|
||||
),
|
||||
),
|
||||
parsed=parsed,
|
||||
)
|
||||
if self.completion:
|
||||
nxt = self.completion.completer.cycle(forward)
|
||||
buf = "".join([i.value for i in self.completion.parsed]) + nxt
|
||||
self.text = buf
|
||||
self.cursor = len(self.text)
|
||||
|
||||
def backspace(self) -> None:
|
||||
if self.cursor == 0:
|
||||
return
|
||||
self.text = self.text[: self.cursor - 1] + self.text[self.cursor :]
|
||||
self.cursor = self.cursor - 1
|
||||
self.completion = None
|
||||
|
||||
def delete(self) -> None:
|
||||
if self.cursor == len(self.text):
|
||||
return
|
||||
self.text = self.text[: self.cursor] + self.text[self.cursor + 1 :]
|
||||
self.completion = None
|
||||
|
||||
def insert(self, k: str) -> None:
|
||||
"""
|
||||
Inserts text at the cursor.
|
||||
"""
|
||||
|
||||
# We don't want to insert a space before the command
|
||||
if k == " " and self.text[0 : self.cursor].strip() == "":
|
||||
return
|
||||
|
||||
self.text = self.text[: self.cursor] + k + self.text[self.cursor :]
|
||||
self.cursor += len(k)
|
||||
self.completion = None
|
||||
|
||||
|
||||
class CommandEdit(urwid.WidgetWrap):
|
||||
leader = ": "
|
||||
|
||||
def __init__(self, master: mitmproxy.master.Master, text: str) -> None:
|
||||
super().__init__(urwid.Text(self.leader))
|
||||
self.master = master
|
||||
self.active_filter = False
|
||||
self.filter_str = ""
|
||||
self.cbuf = CommandBuffer(master, text)
|
||||
self.update()
|
||||
|
||||
def keypress(self, size, key) -> None:
|
||||
if key == "delete":
|
||||
self.cbuf.delete()
|
||||
elif key == "ctrl a" or key == "home":
|
||||
self.cbuf.cursor = 0
|
||||
elif key == "ctrl e" or key == "end":
|
||||
self.cbuf.cursor = len(self.cbuf.text)
|
||||
elif key == "meta b":
|
||||
self.cbuf.cursor = self.cbuf.text.rfind(" ", 0, self.cbuf.cursor)
|
||||
elif key == "meta f":
|
||||
pos = self.cbuf.text.find(" ", self.cbuf.cursor + 1)
|
||||
if pos == -1:
|
||||
pos = len(self.cbuf.text)
|
||||
self.cbuf.cursor = pos
|
||||
elif key == "ctrl w":
|
||||
prev_cursor = self.cbuf.cursor
|
||||
pos = self.cbuf.text.rfind(" ", 0, self.cbuf.cursor - 1)
|
||||
if pos == -1:
|
||||
new_text = self.cbuf.text[self.cbuf.cursor :]
|
||||
cursor_pos = 0
|
||||
else:
|
||||
txt_after = self.cbuf.text[self.cbuf.cursor :]
|
||||
txt_before = self.cbuf.text[0:pos]
|
||||
new_text = f"{txt_before} {txt_after}"
|
||||
cursor_pos = prev_cursor - (prev_cursor - pos) + 1
|
||||
self.cbuf.set_text(new_text)
|
||||
self.cbuf.cursor = cursor_pos
|
||||
elif key == "backspace":
|
||||
self.cbuf.backspace()
|
||||
if self.cbuf.text == "":
|
||||
self.active_filter = False
|
||||
self.master.commands.call("commands.history.filter", "")
|
||||
self.filter_str = ""
|
||||
elif key == "left" or key == "ctrl b":
|
||||
self.cbuf.left()
|
||||
elif key == "right" or key == "ctrl f":
|
||||
self.cbuf.right()
|
||||
elif key == "up" or key == "ctrl p":
|
||||
if self.active_filter is False:
|
||||
self.active_filter = True
|
||||
self.filter_str = self.cbuf.text
|
||||
self.master.commands.call("commands.history.filter", self.cbuf.text)
|
||||
cmd = self.master.commands.execute("commands.history.prev")
|
||||
self.cbuf = CommandBuffer(self.master, cmd)
|
||||
elif key == "down" or key == "ctrl n":
|
||||
prev_cmd = self.cbuf.text
|
||||
cmd = self.master.commands.execute("commands.history.next")
|
||||
|
||||
if cmd == "":
|
||||
if prev_cmd == self.filter_str:
|
||||
self.cbuf = CommandBuffer(self.master, prev_cmd)
|
||||
else:
|
||||
self.active_filter = False
|
||||
self.master.commands.call("commands.history.filter", "")
|
||||
self.filter_str = ""
|
||||
self.cbuf = CommandBuffer(self.master, "")
|
||||
else:
|
||||
self.cbuf = CommandBuffer(self.master, cmd)
|
||||
elif key == "shift tab":
|
||||
self.cbuf.cycle_completion(False)
|
||||
elif key == "tab":
|
||||
self.cbuf.cycle_completion()
|
||||
elif len(key) == 1:
|
||||
self.cbuf.insert(key)
|
||||
self.update()
|
||||
|
||||
def update(self) -> None:
|
||||
self._w.set_text([self.leader, self.cbuf.render()])
|
||||
|
||||
def render(self, size, focus=False) -> urwid.Canvas:
|
||||
(maxcol,) = size
|
||||
canv = self._w.render((maxcol,))
|
||||
canv = urwid.CompositeCanvas(canv)
|
||||
canv.cursor = self.get_cursor_coords((maxcol,))
|
||||
return canv
|
||||
|
||||
def get_cursor_coords(self, size) -> tuple[int, int]:
|
||||
p = self.cbuf.cursor + len(self.leader)
|
||||
trans = self._w.get_line_translation(size[0])
|
||||
x, y = calc_coords(self._w.get_text()[0], trans, p)
|
||||
return x, y
|
||||
|
||||
def get_edit_text(self) -> str:
|
||||
return self.cbuf.text
|
||||
@@ -0,0 +1,35 @@
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
|
||||
from mitmproxy import exceptions
|
||||
from mitmproxy import flow
|
||||
from mitmproxy.tools.console import overlay
|
||||
from mitmproxy.tools.console import signals
|
||||
|
||||
|
||||
class CommandExecutor:
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
|
||||
def __call__(self, cmd: str) -> None:
|
||||
if cmd.strip():
|
||||
try:
|
||||
ret = self.master.commands.execute(cmd)
|
||||
except exceptions.CommandError as e:
|
||||
logging.error(str(e))
|
||||
else:
|
||||
if ret is not None:
|
||||
if type(ret) == Sequence[flow.Flow]: # noqa: E721
|
||||
signals.status_message.send(
|
||||
message="Command returned %s flows" % len(ret)
|
||||
)
|
||||
elif type(ret) is flow.Flow:
|
||||
signals.status_message.send(message="Command returned 1 flow")
|
||||
else:
|
||||
self.master.overlay(
|
||||
overlay.DataViewerOverlay(
|
||||
self.master,
|
||||
ret,
|
||||
),
|
||||
valign="top",
|
||||
)
|
||||
156
venv/Lib/site-packages/mitmproxy/tools/console/commands.py
Normal file
156
venv/Lib/site-packages/mitmproxy/tools/console/commands.py
Normal file
@@ -0,0 +1,156 @@
|
||||
import textwrap
|
||||
|
||||
import urwid
|
||||
|
||||
from mitmproxy import command
|
||||
from mitmproxy.tools.console import layoutwidget
|
||||
from mitmproxy.tools.console import signals
|
||||
from mitmproxy.utils import signals as utils_signals
|
||||
|
||||
HELP_HEIGHT = 5
|
||||
|
||||
command_focus_change = utils_signals.SyncSignal(lambda text: None)
|
||||
|
||||
|
||||
class CommandItem(urwid.WidgetWrap):
|
||||
def __init__(self, walker, cmd: command.Command, focused: bool):
|
||||
self.walker, self.cmd, self.focused = walker, cmd, focused
|
||||
super().__init__(self.get_widget())
|
||||
|
||||
def get_widget(self):
|
||||
parts = [("focus", ">> " if self.focused else " "), ("title", self.cmd.name)]
|
||||
if self.cmd.parameters:
|
||||
parts += [
|
||||
("text", " "),
|
||||
("text", " ".join(str(param) for param in self.cmd.parameters)),
|
||||
]
|
||||
if self.cmd.return_type:
|
||||
parts += [
|
||||
("title", " -> "),
|
||||
("text", command.typename(self.cmd.return_type)),
|
||||
]
|
||||
|
||||
return urwid.AttrMap(urwid.Padding(urwid.Text(parts)), "text")
|
||||
|
||||
def get_edit_text(self):
|
||||
return self._w[1].get_edit_text()
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def keypress(self, size, key):
|
||||
return key
|
||||
|
||||
|
||||
class CommandListWalker(urwid.ListWalker):
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
self.index = 0
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
self.cmds = list(self.master.commands.commands.values())
|
||||
self.cmds.sort(key=lambda x: x.signature_help())
|
||||
self.set_focus(self.index)
|
||||
|
||||
def get_edit_text(self):
|
||||
return self.focus_obj.get_edit_text()
|
||||
|
||||
def _get(self, pos):
|
||||
cmd = self.cmds[pos]
|
||||
return CommandItem(self, cmd, pos == self.index)
|
||||
|
||||
def get_focus(self):
|
||||
return self.focus_obj, self.index
|
||||
|
||||
def set_focus(self, index: int) -> None:
|
||||
cmd = self.cmds[index]
|
||||
self.index = index
|
||||
self.focus_obj = self._get(self.index)
|
||||
command_focus_change.send(cmd.help or "")
|
||||
|
||||
def get_next(self, pos):
|
||||
if pos >= len(self.cmds) - 1:
|
||||
return None, None
|
||||
pos = pos + 1
|
||||
return self._get(pos), pos
|
||||
|
||||
def get_prev(self, pos):
|
||||
pos = pos - 1
|
||||
if pos < 0:
|
||||
return None, None
|
||||
return self._get(pos), pos
|
||||
|
||||
|
||||
class CommandsList(urwid.ListBox):
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
self.walker = CommandListWalker(master)
|
||||
super().__init__(self.walker)
|
||||
|
||||
def keypress(self, size: int, key: str):
|
||||
if key == "m_select":
|
||||
foc, idx = self.get_focus()
|
||||
signals.status_prompt_command.send(partial=foc.cmd.name + " ")
|
||||
elif key == "m_start":
|
||||
self.set_focus(0)
|
||||
self.walker._modified()
|
||||
elif key == "m_end":
|
||||
self.set_focus(len(self.walker.cmds) - 1)
|
||||
self.walker._modified()
|
||||
return super().keypress(size, key)
|
||||
|
||||
|
||||
class CommandHelp(urwid.Frame):
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
super().__init__(self.widget(""))
|
||||
self.set_active(False)
|
||||
command_focus_change.connect(self.sig_mod)
|
||||
|
||||
def set_active(self, val):
|
||||
h = urwid.Text("Command Help")
|
||||
style = "heading" if val else "heading_inactive"
|
||||
self.header = urwid.AttrMap(h, style)
|
||||
|
||||
def widget(self, txt):
|
||||
cols, _ = self.master.ui.get_cols_rows()
|
||||
return urwid.ListBox([urwid.Text(i) for i in textwrap.wrap(txt, cols)])
|
||||
|
||||
def sig_mod(self, txt):
|
||||
self.body = self.widget(txt)
|
||||
|
||||
|
||||
class Commands(urwid.Pile, layoutwidget.LayoutWidget):
|
||||
title = "Command Reference"
|
||||
keyctx = "commands"
|
||||
|
||||
focus_position: int
|
||||
|
||||
def __init__(self, master):
|
||||
oh = CommandHelp(master)
|
||||
super().__init__(
|
||||
[
|
||||
CommandsList(master),
|
||||
(HELP_HEIGHT, oh),
|
||||
]
|
||||
)
|
||||
self.master = master
|
||||
|
||||
def layout_pushed(self, prev):
|
||||
self.widget_list[0].walker.refresh()
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "m_next":
|
||||
self.focus_position = (self.focus_position + 1) % len(self.widget_list)
|
||||
self.widget_list[1].set_active(self.focus_position == 1)
|
||||
key = None
|
||||
|
||||
# This is essentially a copypasta from urwid.Pile's keypress handler.
|
||||
# So much for "closed for modification, but open for extension".
|
||||
item_rows = None
|
||||
if len(size) == 2:
|
||||
item_rows = self.get_item_rows(size, focus=True)
|
||||
i = self.widget_list.index(self.focus_item)
|
||||
tsize = self.get_item_size(size, i, True, item_rows)
|
||||
return self.focus_item.keypress(tsize, key)
|
||||
877
venv/Lib/site-packages/mitmproxy/tools/console/common.py
Normal file
877
venv/Lib/site-packages/mitmproxy/tools/console/common.py
Normal file
@@ -0,0 +1,877 @@
|
||||
import enum
|
||||
import math
|
||||
import platform
|
||||
from collections.abc import Iterable
|
||||
from functools import lru_cache
|
||||
|
||||
import urwid.util
|
||||
from publicsuffix2 import get_sld
|
||||
from publicsuffix2 import get_tld
|
||||
|
||||
from mitmproxy import dns
|
||||
from mitmproxy import flow
|
||||
from mitmproxy.dns import DNSFlow
|
||||
from mitmproxy.http import HTTPFlow
|
||||
from mitmproxy.tcp import TCPFlow
|
||||
from mitmproxy.udp import UDPFlow
|
||||
from mitmproxy.utils import emoji
|
||||
from mitmproxy.utils import human
|
||||
|
||||
# Detect Windows Subsystem for Linux and Windows
|
||||
IS_WINDOWS_OR_WSL = (
|
||||
"Microsoft" in platform.platform() or "Windows" in platform.platform()
|
||||
)
|
||||
|
||||
|
||||
def is_keypress(k):
|
||||
"""
|
||||
Is this input event a keypress?
|
||||
"""
|
||||
if isinstance(k, str):
|
||||
return True
|
||||
|
||||
|
||||
def highlight_key(text, key, textattr="text", keyattr="key"):
|
||||
lst = []
|
||||
parts = text.split(key, 1)
|
||||
if parts[0]:
|
||||
lst.append((textattr, parts[0]))
|
||||
lst.append((keyattr, key))
|
||||
if parts[1]:
|
||||
lst.append((textattr, parts[1]))
|
||||
return lst
|
||||
|
||||
|
||||
KEY_MAX = 30
|
||||
|
||||
|
||||
def format_keyvals(
|
||||
entries: Iterable[tuple[str, None | str | urwid.Widget]],
|
||||
key_format: str = "key",
|
||||
value_format: str = "text",
|
||||
indent: int = 0,
|
||||
) -> list[urwid.Columns]:
|
||||
"""
|
||||
Format a list of (key, value) tuples.
|
||||
|
||||
Args:
|
||||
entries: The list to format. keys must be strings, values can also be None or urwid widgets.
|
||||
The latter makes it possible to use the result of format_keyvals() as a value.
|
||||
key_format: The display attribute for the key.
|
||||
value_format: The display attribute for the value.
|
||||
indent: Additional indent to apply.
|
||||
"""
|
||||
max_key_len = max((len(k) for k, v in entries if k is not None), default=0)
|
||||
max_key_len = min(max_key_len, KEY_MAX)
|
||||
|
||||
if indent > 2:
|
||||
indent -= 2 # We use dividechars=2 below, which already adds two empty spaces
|
||||
|
||||
ret = []
|
||||
for k, v in entries:
|
||||
if v is None:
|
||||
v = urwid.Text("")
|
||||
elif not isinstance(v, urwid.Widget):
|
||||
v = urwid.Text([(value_format, v)])
|
||||
ret.append(
|
||||
urwid.Columns(
|
||||
[
|
||||
("fixed", indent, urwid.Text("")),
|
||||
("fixed", max_key_len, urwid.Text([(key_format, k)])),
|
||||
v,
|
||||
],
|
||||
dividechars=2,
|
||||
)
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
def fcol(s: str, attr: str) -> tuple[str, int, urwid.Text]:
|
||||
s = str(s)
|
||||
return ("fixed", len(s), urwid.Text([(attr, s)]))
|
||||
|
||||
|
||||
if urwid.util.detected_encoding:
|
||||
SYMBOL_REPLAY = "\u21ba"
|
||||
SYMBOL_RETURN = "\u2190"
|
||||
SYMBOL_MARK = "\u25cf"
|
||||
SYMBOL_UP = "\u21e7"
|
||||
SYMBOL_DOWN = "\u21e9"
|
||||
SYMBOL_ELLIPSIS = "\u2026"
|
||||
SYMBOL_FROM_CLIENT = "\u21d2"
|
||||
SYMBOL_TO_CLIENT = "\u21d0"
|
||||
else:
|
||||
SYMBOL_REPLAY = "[r]"
|
||||
SYMBOL_RETURN = "<-"
|
||||
SYMBOL_MARK = "#"
|
||||
SYMBOL_UP = "^"
|
||||
SYMBOL_DOWN = " "
|
||||
SYMBOL_ELLIPSIS = "~"
|
||||
SYMBOL_FROM_CLIENT = "->"
|
||||
SYMBOL_TO_CLIENT = "<-"
|
||||
|
||||
SCHEME_STYLES = {
|
||||
"http": "scheme_http",
|
||||
"https": "scheme_https",
|
||||
"ws": "scheme_ws",
|
||||
"wss": "scheme_wss",
|
||||
"tcp": "scheme_tcp",
|
||||
"udp": "scheme_udp",
|
||||
"dns": "scheme_dns",
|
||||
"quic": "scheme_quic",
|
||||
}
|
||||
HTTP_REQUEST_METHOD_STYLES = {
|
||||
"GET": "method_get",
|
||||
"POST": "method_post",
|
||||
"DELETE": "method_delete",
|
||||
"HEAD": "method_head",
|
||||
"PUT": "method_put",
|
||||
}
|
||||
HTTP_RESPONSE_CODE_STYLE = {
|
||||
2: "code_200",
|
||||
3: "code_300",
|
||||
4: "code_400",
|
||||
5: "code_500",
|
||||
}
|
||||
|
||||
|
||||
class RenderMode(enum.Enum):
|
||||
TABLE = 1
|
||||
"""The flow list in table format, i.e. one row per flow."""
|
||||
LIST = 2
|
||||
"""The flow list in list format, i.e. potentially multiple rows per flow."""
|
||||
DETAILVIEW = 3
|
||||
"""The top lines in the detail view."""
|
||||
|
||||
|
||||
def fixlen(s: str, maxlen: int) -> str:
|
||||
if len(s) <= maxlen:
|
||||
return s.ljust(maxlen)
|
||||
else:
|
||||
return s[0 : maxlen - len(SYMBOL_ELLIPSIS)] + SYMBOL_ELLIPSIS
|
||||
|
||||
|
||||
def fixlen_r(s: str, maxlen: int) -> str:
|
||||
if len(s) <= maxlen:
|
||||
return s.rjust(maxlen)
|
||||
else:
|
||||
return SYMBOL_ELLIPSIS + s[len(s) - maxlen + len(SYMBOL_ELLIPSIS) :]
|
||||
|
||||
|
||||
def render_marker(marker: str) -> str:
|
||||
rendered = emoji.emoji.get(marker, SYMBOL_MARK)
|
||||
|
||||
# The marker can only be one glyph. Some emoji that use zero-width joiners (ZWJ)
|
||||
# will not be rendered as a single glyph and instead will show
|
||||
# multiple glyphs. Just use the first glyph as a fallback.
|
||||
# https://emojipedia.org/emoji-zwj-sequence/
|
||||
return rendered[0]
|
||||
|
||||
|
||||
class TruncatedText(urwid.Widget):
|
||||
def __init__(self, text, attr, align="left"):
|
||||
self.text = text
|
||||
self.attr = attr
|
||||
self.align = align
|
||||
super().__init__()
|
||||
|
||||
def pack(self, size, focus=False):
|
||||
return (len(self.text), 1)
|
||||
|
||||
def rows(self, size, focus=False):
|
||||
return 1
|
||||
|
||||
def render(self, size, focus=False):
|
||||
text = self.text
|
||||
attr = self.attr
|
||||
if self.align == "right":
|
||||
text = text[::-1]
|
||||
attr = attr[::-1]
|
||||
|
||||
text_len = urwid.calc_width(text, 0, len(text))
|
||||
if size is not None and len(size) > 0:
|
||||
width = size[0]
|
||||
else:
|
||||
width = text_len
|
||||
|
||||
if width >= text_len:
|
||||
remaining = width - text_len
|
||||
if remaining > 0:
|
||||
c_text = text + " " * remaining
|
||||
c_attr = attr + [("text", remaining)]
|
||||
else:
|
||||
c_text = text
|
||||
c_attr = attr
|
||||
else:
|
||||
trim = urwid.util.calc_trim_text(text, 0, width - 1, 0, width - 1)
|
||||
visible_text = text[0 : trim[1]]
|
||||
if trim[3] == 1:
|
||||
visible_text += " "
|
||||
c_text = visible_text + SYMBOL_ELLIPSIS
|
||||
c_attr = urwid.util.rle_subseg(attr, 0, len(visible_text.encode())) + [
|
||||
("focus", len(SYMBOL_ELLIPSIS.encode()))
|
||||
]
|
||||
|
||||
if self.align == "right":
|
||||
c_text = c_text[::-1]
|
||||
c_attr = c_attr[::-1]
|
||||
|
||||
return urwid.TextCanvas([c_text.encode()], [c_attr], maxcol=width)
|
||||
|
||||
|
||||
def truncated_plain(text, attr, align="left"):
|
||||
return TruncatedText(text, [(attr, len(text.encode()))], align)
|
||||
|
||||
|
||||
# Work around https://github.com/urwid/urwid/pull/330
|
||||
def rle_append_beginning_modify(rle, a_r):
|
||||
"""
|
||||
Append (a, r) (unpacked from *a_r*) to BEGINNING of rle.
|
||||
Merge with first run when possible
|
||||
|
||||
MODIFIES rle parameter contents. Returns None.
|
||||
"""
|
||||
a, r = a_r
|
||||
if not rle:
|
||||
rle[:] = [(a, r)]
|
||||
else:
|
||||
al, run = rle[0]
|
||||
if a == al:
|
||||
rle[0] = (a, run + r)
|
||||
else:
|
||||
rle[0:0] = [(a, r)]
|
||||
|
||||
|
||||
def colorize_host(host: str):
|
||||
if not host:
|
||||
return []
|
||||
|
||||
tld = get_tld(host)
|
||||
sld = get_sld(host)
|
||||
|
||||
attr: list = []
|
||||
|
||||
tld_size = len(tld)
|
||||
sld_size = len(sld) - tld_size
|
||||
|
||||
for letter in reversed(range(len(host))):
|
||||
character = host[letter]
|
||||
if tld_size > 0:
|
||||
style = "url_domain"
|
||||
tld_size -= 1
|
||||
elif tld_size == 0:
|
||||
style = "text"
|
||||
tld_size -= 1
|
||||
elif sld_size > 0:
|
||||
sld_size -= 1
|
||||
style = "url_extension"
|
||||
else:
|
||||
style = "text"
|
||||
rle_append_beginning_modify(attr, (style, len(character.encode())))
|
||||
return attr
|
||||
|
||||
|
||||
def colorize_req(s: str):
|
||||
path = s.split("?", 2)[0]
|
||||
i_query = len(path)
|
||||
i_last_slash = path.rfind("/")
|
||||
i_ext = path[i_last_slash + 1 :].rfind(".")
|
||||
i_ext = i_last_slash + i_ext if i_ext >= 0 else len(s)
|
||||
in_val = False
|
||||
attr: list = []
|
||||
for i in range(len(s)):
|
||||
c = s[i]
|
||||
if (
|
||||
(i < i_query and c == "/")
|
||||
or (i < i_query and i > i_last_slash and c == ".")
|
||||
or (i == i_query)
|
||||
):
|
||||
a = "url_punctuation"
|
||||
elif i > i_query:
|
||||
if in_val:
|
||||
if c == "&":
|
||||
in_val = False
|
||||
a = "url_punctuation"
|
||||
else:
|
||||
a = "url_query_value"
|
||||
else:
|
||||
if c == "=":
|
||||
in_val = True
|
||||
a = "url_punctuation"
|
||||
else:
|
||||
a = "url_query_key"
|
||||
elif i > i_ext:
|
||||
a = "url_extension"
|
||||
elif i > i_last_slash:
|
||||
a = "url_filename"
|
||||
else:
|
||||
a = "text"
|
||||
urwid.util.rle_append_modify(attr, (a, len(c.encode())))
|
||||
return attr
|
||||
|
||||
|
||||
def colorize_url(url):
|
||||
parts = url.split("/", 3)
|
||||
if len(parts) < 4 or len(parts[1]) > 0 or parts[0][-1:] != ":":
|
||||
return [("error", len(url))] # bad URL
|
||||
return (
|
||||
[
|
||||
(SCHEME_STYLES.get(parts[0], "scheme_other"), len(parts[0]) - 1),
|
||||
("url_punctuation", 3), # ://
|
||||
]
|
||||
+ colorize_host(parts[2])
|
||||
+ colorize_req("/" + parts[3])
|
||||
)
|
||||
|
||||
|
||||
def format_http_content_type(content_type: str) -> tuple[str, str]:
|
||||
content_type = content_type.split(";")[0]
|
||||
if content_type.endswith("/javascript"):
|
||||
style = "content_script"
|
||||
elif content_type.startswith("text/"):
|
||||
style = "content_text"
|
||||
elif (
|
||||
content_type.startswith("image/")
|
||||
or content_type.startswith("video/")
|
||||
or content_type.startswith("font/")
|
||||
or "/x-font-" in content_type
|
||||
):
|
||||
style = "content_media"
|
||||
elif content_type.endswith("/json") or content_type.endswith("/xml"):
|
||||
style = "content_data"
|
||||
elif content_type.startswith("application/"):
|
||||
style = "content_raw"
|
||||
else:
|
||||
style = "content_other"
|
||||
return content_type, style
|
||||
|
||||
|
||||
def format_duration(duration: float) -> tuple[str, str]:
|
||||
pretty_duration = human.pretty_duration(duration)
|
||||
style = "gradient_%02d" % int(
|
||||
99 - 100 * min(math.log2(max(1.0, 1000 * duration)) / 12, 0.99)
|
||||
)
|
||||
return pretty_duration, style
|
||||
|
||||
|
||||
def format_size(num_bytes: int) -> tuple[str, str]:
|
||||
pretty_size = human.pretty_size(num_bytes)
|
||||
style = "gradient_%02d" % int(99 - 100 * min(math.log2(1 + num_bytes) / 20, 0.99))
|
||||
return pretty_size, style
|
||||
|
||||
|
||||
def format_left_indicators(*, focused: bool, intercepted: bool, timestamp: float):
|
||||
indicators: list[str | tuple[str, str]] = []
|
||||
if focused:
|
||||
indicators.append(("focus", ">>"))
|
||||
else:
|
||||
indicators.append(" ")
|
||||
pretty_timestamp = human.format_timestamp(timestamp)[-8:]
|
||||
if intercepted:
|
||||
indicators.append(("intercept", pretty_timestamp))
|
||||
else:
|
||||
indicators.append(("text", pretty_timestamp))
|
||||
return "fixed", 10, urwid.Text(indicators)
|
||||
|
||||
|
||||
def format_right_indicators(
|
||||
*,
|
||||
replay: bool,
|
||||
marked: str,
|
||||
):
|
||||
indicators: list[str | tuple[str, str]] = []
|
||||
if replay:
|
||||
indicators.append(("replay", SYMBOL_REPLAY))
|
||||
else:
|
||||
indicators.append(" ")
|
||||
if bool(marked):
|
||||
indicators.append(("mark", render_marker(marked)))
|
||||
else:
|
||||
indicators.append(" ")
|
||||
return "fixed", 3, urwid.Text(indicators)
|
||||
|
||||
|
||||
@lru_cache(maxsize=800)
|
||||
def format_http_flow_list(
|
||||
*,
|
||||
render_mode: RenderMode,
|
||||
focused: bool,
|
||||
marked: str,
|
||||
is_replay: bool,
|
||||
request_method: str,
|
||||
request_scheme: str,
|
||||
request_host: str,
|
||||
request_path: str,
|
||||
request_url: str,
|
||||
request_http_version: str,
|
||||
request_timestamp: float,
|
||||
request_is_push_promise: bool,
|
||||
intercepted: bool,
|
||||
response_code: int | None,
|
||||
response_reason: str | None,
|
||||
response_content_length: int | None,
|
||||
response_content_type: str | None,
|
||||
duration: float | None,
|
||||
error_message: str | None,
|
||||
) -> urwid.Widget:
|
||||
req = []
|
||||
|
||||
if render_mode is RenderMode.DETAILVIEW:
|
||||
req.append(fcol(human.format_timestamp(request_timestamp), "highlight"))
|
||||
else:
|
||||
if focused:
|
||||
req.append(fcol(">>", "focus"))
|
||||
else:
|
||||
req.append(fcol(" ", "focus"))
|
||||
|
||||
method_style = HTTP_REQUEST_METHOD_STYLES.get(request_method, "method_other")
|
||||
req.append(fcol(request_method, method_style))
|
||||
|
||||
if request_is_push_promise:
|
||||
req.append(fcol("PUSH_PROMISE", "method_http2_push"))
|
||||
|
||||
preamble_len = sum(x[1] for x in req) + len(req) - 1
|
||||
|
||||
if request_http_version not in ("HTTP/1.0", "HTTP/1.1"):
|
||||
request_url += " " + request_http_version
|
||||
if intercepted and not response_code:
|
||||
url_style = "intercept"
|
||||
elif response_code or error_message:
|
||||
url_style = "text"
|
||||
else:
|
||||
url_style = "title"
|
||||
|
||||
if render_mode is RenderMode.DETAILVIEW:
|
||||
req.append(urwid.Text([(url_style, request_url)]))
|
||||
else:
|
||||
req.append(truncated_plain(request_url, url_style))
|
||||
|
||||
req.append(format_right_indicators(replay=is_replay, marked=marked))
|
||||
|
||||
resp = [("fixed", preamble_len, urwid.Text(""))]
|
||||
if response_code:
|
||||
if intercepted:
|
||||
style = "intercept"
|
||||
else:
|
||||
style = ""
|
||||
|
||||
status_style = style or HTTP_RESPONSE_CODE_STYLE.get(
|
||||
response_code // 100, "code_other"
|
||||
)
|
||||
resp.append(fcol(SYMBOL_RETURN, status_style))
|
||||
resp.append(fcol(str(response_code), status_style))
|
||||
if response_reason and render_mode is RenderMode.DETAILVIEW:
|
||||
resp.append(fcol(response_reason, status_style))
|
||||
|
||||
if response_content_type:
|
||||
ct, ct_style = format_http_content_type(response_content_type)
|
||||
resp.append(fcol(ct, style or ct_style))
|
||||
|
||||
if response_content_length:
|
||||
size, size_style = format_size(response_content_length)
|
||||
elif response_content_length == 0:
|
||||
size = "[no content]"
|
||||
size_style = "text"
|
||||
else:
|
||||
size = "[content missing]"
|
||||
size_style = "text"
|
||||
resp.append(fcol(size, style or size_style))
|
||||
|
||||
if duration:
|
||||
dur, dur_style = format_duration(duration)
|
||||
resp.append(fcol(dur, style or dur_style))
|
||||
elif error_message:
|
||||
resp.append(fcol(SYMBOL_RETURN, "error"))
|
||||
resp.append(urwid.Text([("error", error_message)]))
|
||||
|
||||
return urwid.Pile(
|
||||
[urwid.Columns(req, dividechars=1), urwid.Columns(resp, dividechars=1)]
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=800)
|
||||
def format_http_flow_table(
|
||||
*,
|
||||
render_mode: RenderMode,
|
||||
focused: bool,
|
||||
marked: str,
|
||||
is_replay: str | None,
|
||||
request_method: str,
|
||||
request_scheme: str,
|
||||
request_host: str,
|
||||
request_path: str,
|
||||
request_url: str,
|
||||
request_http_version: str,
|
||||
request_timestamp: float,
|
||||
request_is_push_promise: bool,
|
||||
intercepted: bool,
|
||||
response_code: int | None,
|
||||
response_reason: str | None,
|
||||
response_content_length: int | None,
|
||||
response_content_type: str | None,
|
||||
duration: float | None,
|
||||
error_message: str | None,
|
||||
) -> urwid.Widget:
|
||||
items = [
|
||||
format_left_indicators(
|
||||
focused=focused, intercepted=intercepted, timestamp=request_timestamp
|
||||
)
|
||||
]
|
||||
|
||||
if intercepted and not response_code:
|
||||
request_style = "intercept"
|
||||
else:
|
||||
request_style = ""
|
||||
|
||||
scheme_style = request_style or SCHEME_STYLES.get(request_scheme, "scheme_other")
|
||||
items.append(fcol(fixlen(request_scheme.upper(), 5), scheme_style))
|
||||
|
||||
if request_is_push_promise:
|
||||
method_style = "method_http2_push"
|
||||
else:
|
||||
method_style = request_style or HTTP_REQUEST_METHOD_STYLES.get(
|
||||
request_method, "method_other"
|
||||
)
|
||||
items.append(fcol(fixlen(request_method, 4), method_style))
|
||||
|
||||
items.append(
|
||||
(
|
||||
"weight",
|
||||
0.25,
|
||||
TruncatedText(request_host, colorize_host(request_host), "right"),
|
||||
)
|
||||
)
|
||||
items.append(
|
||||
("weight", 1.0, TruncatedText(request_path, colorize_req(request_path), "left"))
|
||||
)
|
||||
|
||||
if intercepted and response_code:
|
||||
response_style = "intercept"
|
||||
else:
|
||||
response_style = ""
|
||||
|
||||
if response_code:
|
||||
status = str(response_code)
|
||||
status_style = response_style or HTTP_RESPONSE_CODE_STYLE.get(
|
||||
response_code // 100, "code_other"
|
||||
)
|
||||
|
||||
if response_content_length and response_content_type:
|
||||
content, content_style = format_http_content_type(response_content_type)
|
||||
content_style = response_style or content_style
|
||||
elif response_content_length:
|
||||
content = ""
|
||||
content_style = "content_none"
|
||||
elif response_content_length == 0:
|
||||
content = "[no content]"
|
||||
content_style = "content_none"
|
||||
else:
|
||||
content = "[content missing]"
|
||||
content_style = "content_none"
|
||||
|
||||
elif error_message:
|
||||
status = "err"
|
||||
status_style = "error"
|
||||
content = error_message
|
||||
content_style = "error"
|
||||
|
||||
else:
|
||||
status = ""
|
||||
status_style = "text"
|
||||
content = ""
|
||||
content_style = ""
|
||||
|
||||
items.append(fcol(fixlen(status, 3), status_style))
|
||||
items.append(("weight", 0.15, truncated_plain(content, content_style, "right")))
|
||||
|
||||
if response_content_length:
|
||||
size, size_style = format_size(response_content_length)
|
||||
items.append(fcol(fixlen_r(size, 5), response_style or size_style))
|
||||
else:
|
||||
items.append(("fixed", 5, urwid.Text("")))
|
||||
|
||||
if duration:
|
||||
duration_pretty, duration_style = format_duration(duration)
|
||||
items.append(
|
||||
fcol(fixlen_r(duration_pretty, 5), response_style or duration_style)
|
||||
)
|
||||
else:
|
||||
items.append(("fixed", 5, urwid.Text("")))
|
||||
|
||||
items.append(
|
||||
format_right_indicators(
|
||||
replay=bool(is_replay),
|
||||
marked=marked,
|
||||
)
|
||||
)
|
||||
return urwid.Columns(items, dividechars=1, min_width=15)
|
||||
|
||||
|
||||
@lru_cache(maxsize=800)
|
||||
def format_message_flow(
|
||||
*,
|
||||
render_mode: RenderMode,
|
||||
focused: bool,
|
||||
timestamp_start: float,
|
||||
marked: str,
|
||||
protocol: str,
|
||||
client_address,
|
||||
server_address,
|
||||
total_size: int,
|
||||
duration: float | None,
|
||||
error_message: str | None,
|
||||
):
|
||||
conn = f"{human.format_address(client_address)} <-> {human.format_address(server_address)}"
|
||||
|
||||
items = []
|
||||
|
||||
if render_mode in (RenderMode.TABLE, RenderMode.DETAILVIEW):
|
||||
items.append(
|
||||
format_left_indicators(
|
||||
focused=focused, intercepted=False, timestamp=timestamp_start
|
||||
)
|
||||
)
|
||||
else:
|
||||
if focused:
|
||||
items.append(fcol(">>", "focus"))
|
||||
else:
|
||||
items.append(fcol(" ", "focus"))
|
||||
|
||||
if render_mode is RenderMode.TABLE:
|
||||
items.append(fcol(fixlen(protocol.upper(), 5), SCHEME_STYLES[protocol]))
|
||||
else:
|
||||
items.append(fcol(protocol.upper(), SCHEME_STYLES[protocol]))
|
||||
|
||||
items.append(("weight", 1.0, truncated_plain(conn, "text", "left")))
|
||||
if error_message:
|
||||
items.append(("weight", 1.0, truncated_plain(error_message, "error", "left")))
|
||||
|
||||
if total_size:
|
||||
size, size_style = format_size(total_size)
|
||||
items.append(fcol(fixlen_r(size, 5), size_style))
|
||||
else:
|
||||
items.append(("fixed", 5, urwid.Text("")))
|
||||
|
||||
if duration:
|
||||
duration_pretty, duration_style = format_duration(duration)
|
||||
items.append(fcol(fixlen_r(duration_pretty, 5), duration_style))
|
||||
else:
|
||||
items.append(("fixed", 5, urwid.Text("")))
|
||||
|
||||
items.append(format_right_indicators(replay=False, marked=marked))
|
||||
|
||||
return urwid.Pile([urwid.Columns(items, dividechars=1, min_width=15)])
|
||||
|
||||
|
||||
@lru_cache(maxsize=800)
|
||||
def format_dns_flow(
|
||||
*,
|
||||
render_mode: RenderMode,
|
||||
focused: bool,
|
||||
intercepted: bool,
|
||||
marked: str,
|
||||
is_replay: str | None,
|
||||
op_code: str,
|
||||
request_timestamp: float,
|
||||
domain: str,
|
||||
type: str,
|
||||
response_code: str | None,
|
||||
response_code_http_equiv: int,
|
||||
answer: str | None,
|
||||
error_message: str,
|
||||
duration: float | None,
|
||||
):
|
||||
items = []
|
||||
|
||||
if render_mode in (RenderMode.TABLE, RenderMode.DETAILVIEW):
|
||||
items.append(
|
||||
format_left_indicators(
|
||||
focused=focused, intercepted=intercepted, timestamp=request_timestamp
|
||||
)
|
||||
)
|
||||
else:
|
||||
items.append(fcol(">>" if focused else " ", "focus"))
|
||||
|
||||
scheme_style = "intercepted" if intercepted else SCHEME_STYLES["dns"]
|
||||
t = f"DNS {op_code}"
|
||||
if render_mode is RenderMode.TABLE:
|
||||
t = fixlen(t, 10)
|
||||
items.append(fcol(t, scheme_style))
|
||||
items.append(("weight", 0.5, TruncatedText(domain, colorize_host(domain), "right")))
|
||||
items.append(fcol("(" + fixlen(type, 5)[: len(type)] + ") =", "text"))
|
||||
|
||||
items.append(
|
||||
(
|
||||
"weight",
|
||||
1,
|
||||
(
|
||||
truncated_plain(
|
||||
"..." if answer is None else "?" if not answer else answer, "text"
|
||||
)
|
||||
if error_message is None
|
||||
else truncated_plain(error_message, "error")
|
||||
),
|
||||
)
|
||||
)
|
||||
status_style = (
|
||||
"intercepted"
|
||||
if intercepted
|
||||
else HTTP_RESPONSE_CODE_STYLE.get(response_code_http_equiv // 100, "code_other")
|
||||
)
|
||||
items.append(
|
||||
fcol(fixlen("" if response_code is None else response_code, 9), status_style)
|
||||
)
|
||||
|
||||
if duration:
|
||||
duration_pretty, duration_style = format_duration(duration)
|
||||
items.append(fcol(fixlen_r(duration_pretty, 5), duration_style))
|
||||
else:
|
||||
items.append(("fixed", 5, urwid.Text("")))
|
||||
|
||||
items.append(
|
||||
format_right_indicators(
|
||||
replay=bool(is_replay),
|
||||
marked=marked,
|
||||
)
|
||||
)
|
||||
return urwid.Pile([urwid.Columns(items, dividechars=1, min_width=15)])
|
||||
|
||||
|
||||
def format_flow(
|
||||
f: flow.Flow,
|
||||
*,
|
||||
render_mode: RenderMode,
|
||||
hostheader: bool = False, # pass options directly if we need more stuff from them
|
||||
focused: bool = True,
|
||||
) -> urwid.Widget:
|
||||
"""
|
||||
This functions calls the proper renderer depending on the flow type.
|
||||
We also want to cache the renderer output, so we extract all attributes
|
||||
relevant for display and call the render with only that. This assures that rows
|
||||
are updated if the flow is changed.
|
||||
"""
|
||||
duration: float | None
|
||||
error_message: str | None
|
||||
if f.error:
|
||||
error_message = f.error.msg
|
||||
else:
|
||||
error_message = None
|
||||
|
||||
if isinstance(f, (TCPFlow, UDPFlow)):
|
||||
total_size = 0
|
||||
for message in f.messages:
|
||||
total_size += len(message.content)
|
||||
if f.messages:
|
||||
duration = f.messages[-1].timestamp - f.client_conn.timestamp_start
|
||||
else:
|
||||
duration = None
|
||||
if f.client_conn.tls_version == "QUICv1":
|
||||
protocol = "quic"
|
||||
else:
|
||||
protocol = f.type
|
||||
return format_message_flow(
|
||||
render_mode=render_mode,
|
||||
focused=focused,
|
||||
timestamp_start=f.client_conn.timestamp_start,
|
||||
marked=f.marked,
|
||||
protocol=protocol,
|
||||
client_address=f.client_conn.peername,
|
||||
server_address=f.server_conn.address,
|
||||
total_size=total_size,
|
||||
duration=duration,
|
||||
error_message=error_message,
|
||||
)
|
||||
elif isinstance(f, DNSFlow):
|
||||
if f.request.timestamp and f.response and f.response.timestamp:
|
||||
duration = f.response.timestamp - f.request.timestamp
|
||||
else:
|
||||
duration = None
|
||||
if f.response:
|
||||
response_code_str: str | None = dns.response_codes.to_str(
|
||||
f.response.response_code
|
||||
)
|
||||
response_code_http_equiv = dns.response_codes.http_equiv_status_code(
|
||||
f.response.response_code
|
||||
)
|
||||
answer = ", ".join(str(x) for x in f.response.answers)
|
||||
else:
|
||||
response_code_str = None
|
||||
response_code_http_equiv = 0
|
||||
answer = None
|
||||
return format_dns_flow(
|
||||
render_mode=render_mode,
|
||||
focused=focused,
|
||||
intercepted=f.intercepted,
|
||||
marked=f.marked,
|
||||
is_replay=f.is_replay,
|
||||
op_code=dns.op_codes.to_str(f.request.op_code),
|
||||
request_timestamp=f.request.timestamp,
|
||||
domain=f.request.questions[0].name if f.request.questions else "",
|
||||
type=dns.types.to_str(f.request.questions[0].type)
|
||||
if f.request.questions
|
||||
else "",
|
||||
response_code=response_code_str,
|
||||
response_code_http_equiv=response_code_http_equiv,
|
||||
answer=answer,
|
||||
error_message=error_message,
|
||||
duration=duration,
|
||||
)
|
||||
elif isinstance(f, HTTPFlow):
|
||||
intercepted = f.intercepted
|
||||
response_content_length: int | None
|
||||
if f.response:
|
||||
if f.response.raw_content is not None:
|
||||
response_content_length = len(f.response.raw_content)
|
||||
else:
|
||||
response_content_length = None
|
||||
response_code: int | None = f.response.status_code
|
||||
response_reason: str | None = f.response.reason
|
||||
response_content_type = f.response.headers.get("content-type")
|
||||
if f.response.timestamp_end:
|
||||
duration = max(
|
||||
[f.response.timestamp_end - f.request.timestamp_start, 0]
|
||||
)
|
||||
else:
|
||||
duration = None
|
||||
else:
|
||||
response_content_length = None
|
||||
response_code = None
|
||||
response_reason = None
|
||||
response_content_type = None
|
||||
duration = None
|
||||
|
||||
scheme = f.request.scheme
|
||||
if f.websocket is not None:
|
||||
if scheme == "https":
|
||||
scheme = "wss"
|
||||
elif scheme == "http":
|
||||
scheme = "ws"
|
||||
|
||||
if render_mode in (RenderMode.LIST, RenderMode.DETAILVIEW):
|
||||
render_func = format_http_flow_list
|
||||
else:
|
||||
render_func = format_http_flow_table
|
||||
return render_func(
|
||||
render_mode=render_mode,
|
||||
focused=focused,
|
||||
marked=f.marked,
|
||||
is_replay=f.is_replay,
|
||||
request_method=f.request.method,
|
||||
request_scheme=scheme,
|
||||
request_host=f.request.pretty_host if hostheader else f.request.host,
|
||||
request_path=f.request.path,
|
||||
request_url=f.request.pretty_url if hostheader else f.request.url,
|
||||
request_http_version=f.request.http_version,
|
||||
request_timestamp=f.request.timestamp_start,
|
||||
request_is_push_promise="h2-pushed-stream" in f.metadata,
|
||||
intercepted=intercepted,
|
||||
response_code=response_code,
|
||||
response_reason=response_reason,
|
||||
response_content_length=response_content_length,
|
||||
response_content_type=response_content_type,
|
||||
duration=duration,
|
||||
error_message=error_message,
|
||||
)
|
||||
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
758
venv/Lib/site-packages/mitmproxy/tools/console/consoleaddons.py
Normal file
758
venv/Lib/site-packages/mitmproxy/tools/console/consoleaddons.py
Normal file
@@ -0,0 +1,758 @@
|
||||
import csv
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
|
||||
import mitmproxy.types
|
||||
from mitmproxy import command
|
||||
from mitmproxy import command_lexer
|
||||
from mitmproxy import contentviews
|
||||
from mitmproxy import ctx
|
||||
from mitmproxy import dns
|
||||
from mitmproxy import exceptions
|
||||
from mitmproxy import flow
|
||||
from mitmproxy import http
|
||||
from mitmproxy import log
|
||||
from mitmproxy import tcp
|
||||
from mitmproxy import udp
|
||||
from mitmproxy.contentviews import ContentviewMessage
|
||||
from mitmproxy.exceptions import CommandError
|
||||
from mitmproxy.log import ALERT
|
||||
from mitmproxy.tools.console import keymap
|
||||
from mitmproxy.tools.console import overlay
|
||||
from mitmproxy.tools.console import signals
|
||||
from mitmproxy.utils import strutils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
console_palettes = [
|
||||
"lowlight",
|
||||
"lowdark",
|
||||
"light",
|
||||
"dark",
|
||||
"solarized_light",
|
||||
"solarized_dark",
|
||||
]
|
||||
view_orders = [
|
||||
"time",
|
||||
"method",
|
||||
"url",
|
||||
"size",
|
||||
]
|
||||
console_layouts = [
|
||||
"single",
|
||||
"vertical",
|
||||
"horizontal",
|
||||
]
|
||||
|
||||
console_flowlist_layout = ["default", "table", "list"]
|
||||
|
||||
|
||||
class ConsoleAddon:
|
||||
"""
|
||||
An addon that exposes console-specific commands, and hooks into required
|
||||
events.
|
||||
"""
|
||||
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
self.started = False
|
||||
|
||||
def load(self, loader):
|
||||
loader.add_option(
|
||||
"console_default_contentview",
|
||||
str,
|
||||
"auto",
|
||||
"The default content view mode.",
|
||||
choices=contentviews.registry.available_views(),
|
||||
)
|
||||
loader.add_option(
|
||||
"console_eventlog_verbosity",
|
||||
str,
|
||||
"info",
|
||||
"EventLog verbosity.",
|
||||
choices=log.LogLevels,
|
||||
)
|
||||
loader.add_option(
|
||||
"console_layout",
|
||||
str,
|
||||
"single",
|
||||
"Console layout.",
|
||||
choices=sorted(console_layouts),
|
||||
)
|
||||
loader.add_option(
|
||||
"console_layout_headers",
|
||||
bool,
|
||||
True,
|
||||
"Show layout component headers",
|
||||
)
|
||||
loader.add_option(
|
||||
"console_focus_follow", bool, False, "Focus follows new flows."
|
||||
)
|
||||
loader.add_option(
|
||||
"console_palette",
|
||||
str,
|
||||
"solarized_dark",
|
||||
"Color palette.",
|
||||
choices=sorted(console_palettes),
|
||||
)
|
||||
loader.add_option(
|
||||
"console_palette_transparent",
|
||||
bool,
|
||||
True,
|
||||
"Set transparent background for palette.",
|
||||
)
|
||||
loader.add_option("console_mouse", bool, True, "Console mouse interaction.")
|
||||
loader.add_option(
|
||||
"console_flowlist_layout",
|
||||
str,
|
||||
"default",
|
||||
"Set the flowlist layout",
|
||||
choices=sorted(console_flowlist_layout),
|
||||
)
|
||||
loader.add_option(
|
||||
"console_strip_trailing_newlines",
|
||||
bool,
|
||||
False,
|
||||
"Strip trailing newlines from edited request/response bodies.",
|
||||
)
|
||||
|
||||
@command.command("console.layout.options")
|
||||
def layout_options(self) -> Sequence[str]:
|
||||
"""
|
||||
Returns the available options for the console_layout option.
|
||||
"""
|
||||
return ["single", "vertical", "horizontal"]
|
||||
|
||||
@command.command("console.layout.cycle")
|
||||
def layout_cycle(self) -> None:
|
||||
"""
|
||||
Cycle through the console layout options.
|
||||
"""
|
||||
opts = self.layout_options()
|
||||
off = self.layout_options().index(ctx.options.console_layout)
|
||||
ctx.options.update(console_layout=opts[(off + 1) % len(opts)])
|
||||
|
||||
@command.command("console.panes.next")
|
||||
def panes_next(self) -> None:
|
||||
"""
|
||||
Go to the next layout pane.
|
||||
"""
|
||||
self.master.window.switch()
|
||||
|
||||
@command.command("console.panes.prev")
|
||||
def panes_prev(self) -> None:
|
||||
"""
|
||||
Go to the previous layout pane.
|
||||
"""
|
||||
return self.panes_next()
|
||||
|
||||
@command.command("console.options.reset.focus")
|
||||
def options_reset_current(self) -> None:
|
||||
"""
|
||||
Reset the current option in the options editor.
|
||||
"""
|
||||
fv = self.master.window.current("options")
|
||||
if not fv:
|
||||
raise exceptions.CommandError("Not viewing options.")
|
||||
self.master.commands.call_strings("options.reset.one", [fv.current_name()])
|
||||
|
||||
@command.command("console.nav.start")
|
||||
def nav_start(self) -> None:
|
||||
"""
|
||||
Go to the start of a list or scrollable.
|
||||
"""
|
||||
self.master.inject_key("m_start")
|
||||
|
||||
@command.command("console.nav.end")
|
||||
def nav_end(self) -> None:
|
||||
"""
|
||||
Go to the end of a list or scrollable.
|
||||
"""
|
||||
self.master.inject_key("m_end")
|
||||
|
||||
@command.command("console.nav.next")
|
||||
def nav_next(self) -> None:
|
||||
"""
|
||||
Go to the next navigatable item.
|
||||
"""
|
||||
self.master.inject_key("m_next")
|
||||
|
||||
@command.command("console.nav.select")
|
||||
def nav_select(self) -> None:
|
||||
"""
|
||||
Select a navigable item for viewing or editing.
|
||||
"""
|
||||
self.master.inject_key("m_select")
|
||||
|
||||
@command.command("console.nav.up")
|
||||
def nav_up(self) -> None:
|
||||
"""
|
||||
Go up.
|
||||
"""
|
||||
self.master.inject_key("up")
|
||||
|
||||
@command.command("console.nav.down")
|
||||
def nav_down(self) -> None:
|
||||
"""
|
||||
Go down.
|
||||
"""
|
||||
self.master.inject_key("down")
|
||||
|
||||
@command.command("console.nav.pageup")
|
||||
def nav_pageup(self) -> None:
|
||||
"""
|
||||
Go up.
|
||||
"""
|
||||
self.master.inject_key("page up")
|
||||
|
||||
@command.command("console.nav.pagedown")
|
||||
def nav_pagedown(self) -> None:
|
||||
"""
|
||||
Go down.
|
||||
"""
|
||||
self.master.inject_key("page down")
|
||||
|
||||
@command.command("console.nav.left")
|
||||
def nav_left(self) -> None:
|
||||
"""
|
||||
Go left.
|
||||
"""
|
||||
self.master.inject_key("left")
|
||||
|
||||
@command.command("console.nav.right")
|
||||
def nav_right(self) -> None:
|
||||
"""
|
||||
Go right.
|
||||
"""
|
||||
self.master.inject_key("right")
|
||||
|
||||
@command.command("console.choose")
|
||||
def console_choose(
|
||||
self,
|
||||
prompt: str,
|
||||
choices: Sequence[str],
|
||||
cmd: mitmproxy.types.Cmd,
|
||||
*args: mitmproxy.types.CmdArgs,
|
||||
) -> None:
|
||||
"""
|
||||
Prompt the user to choose from a specified list of strings, then
|
||||
invoke another command with all occurrences of {choice} replaced by
|
||||
the choice the user made.
|
||||
"""
|
||||
|
||||
def callback(opt):
|
||||
# We're now outside of the call context...
|
||||
repl = [arg.replace("{choice}", opt) for arg in args]
|
||||
try:
|
||||
self.master.commands.call_strings(cmd, repl)
|
||||
except exceptions.CommandError as e:
|
||||
logger.error(str(e))
|
||||
|
||||
self.master.overlay(overlay.Chooser(self.master, prompt, choices, "", callback))
|
||||
|
||||
@command.command("console.choose.cmd")
|
||||
def console_choose_cmd(
|
||||
self,
|
||||
prompt: str,
|
||||
choicecmd: mitmproxy.types.Cmd,
|
||||
subcmd: mitmproxy.types.Cmd,
|
||||
*args: mitmproxy.types.CmdArgs,
|
||||
) -> None:
|
||||
"""
|
||||
Prompt the user to choose from a list of strings returned by a
|
||||
command, then invoke another command with all occurrences of {choice}
|
||||
replaced by the choice the user made.
|
||||
"""
|
||||
choices = ctx.master.commands.execute(choicecmd)
|
||||
|
||||
def callback(opt):
|
||||
# We're now outside of the call context...
|
||||
repl = [arg.replace("{choice}", opt) for arg in args]
|
||||
try:
|
||||
self.master.commands.call_strings(subcmd, repl)
|
||||
except exceptions.CommandError as e:
|
||||
logger.error(str(e))
|
||||
|
||||
self.master.overlay(overlay.Chooser(self.master, prompt, choices, "", callback))
|
||||
|
||||
@command.command("console.command")
|
||||
def console_command(self, *command_str: str) -> None:
|
||||
"""
|
||||
Prompt the user to edit a command with a (possibly empty) starting value.
|
||||
"""
|
||||
quoted = " ".join(command_lexer.quote(x) for x in command_str)
|
||||
if quoted:
|
||||
quoted += " "
|
||||
signals.status_prompt_command.send(partial=quoted)
|
||||
|
||||
@command.command("console.command.confirm")
|
||||
def console_command_confirm(
|
||||
self,
|
||||
prompt: str,
|
||||
cmd: mitmproxy.types.Cmd,
|
||||
*args: mitmproxy.types.CmdArgs,
|
||||
) -> None:
|
||||
"""
|
||||
Prompt the user before running the specified command.
|
||||
"""
|
||||
|
||||
def callback(opt):
|
||||
if opt == "n":
|
||||
return
|
||||
try:
|
||||
self.master.commands.call_strings(cmd, args)
|
||||
except exceptions.CommandError as e:
|
||||
logger.exception(str(e))
|
||||
|
||||
self.master.prompt_for_user_choice(prompt, callback)
|
||||
|
||||
@command.command("console.command.set")
|
||||
def console_command_set(self, option_name: str) -> None:
|
||||
"""
|
||||
Prompt the user to set an option.
|
||||
"""
|
||||
option_value = getattr(self.master.options, option_name, None) or ""
|
||||
set_command = f"set {option_name} {option_value!r}"
|
||||
cursor = len(set_command) - 1
|
||||
signals.status_prompt_command.send(partial=set_command, cursor=cursor)
|
||||
|
||||
@command.command("console.view.keybindings")
|
||||
def view_keybindings(self) -> None:
|
||||
"""View the commands list."""
|
||||
self.master.switch_view("keybindings")
|
||||
|
||||
@command.command("console.view.commands")
|
||||
def view_commands(self) -> None:
|
||||
"""View the commands list."""
|
||||
self.master.switch_view("commands")
|
||||
|
||||
@command.command("console.view.options")
|
||||
def view_options(self) -> None:
|
||||
"""View the options editor."""
|
||||
self.master.switch_view("options")
|
||||
|
||||
@command.command("console.view.eventlog")
|
||||
def view_eventlog(self) -> None:
|
||||
"""View the event log."""
|
||||
self.master.switch_view("eventlog")
|
||||
|
||||
@command.command("console.view.help")
|
||||
def view_help(self) -> None:
|
||||
"""View help."""
|
||||
self.master.switch_view("help")
|
||||
|
||||
@command.command("console.view.flow")
|
||||
def view_flow(self, flow: flow.Flow) -> None:
|
||||
"""View a flow."""
|
||||
if isinstance(flow, (http.HTTPFlow, tcp.TCPFlow, udp.UDPFlow, dns.DNSFlow)):
|
||||
self.master.switch_view("flowview")
|
||||
else:
|
||||
logger.warning(f"No detail view for {type(flow).__name__}.")
|
||||
|
||||
@command.command("console.exit")
|
||||
def exit(self) -> None:
|
||||
"""Exit mitmproxy."""
|
||||
self.master.shutdown()
|
||||
|
||||
@command.command("console.view.pop")
|
||||
def view_pop(self) -> None:
|
||||
"""
|
||||
Pop a view off the console stack. At the top level, this prompts the
|
||||
user to exit mitmproxy.
|
||||
"""
|
||||
signals.pop_view_state.send()
|
||||
|
||||
@command.command("console.bodyview")
|
||||
@command.argument("part", type=mitmproxy.types.Choice("console.bodyview.options"))
|
||||
def bodyview(self, flow: flow.Flow, part: str) -> None:
|
||||
"""
|
||||
Spawn an external viewer for a flow request or response body based
|
||||
on the detected MIME type. We use the mailcap system to find the
|
||||
correct viewer, and fall back to the programs in $PAGER or $EDITOR
|
||||
if necessary.
|
||||
"""
|
||||
fpart = getattr(flow, part, None)
|
||||
if not fpart:
|
||||
raise exceptions.CommandError(
|
||||
"Part must be either request or response, not %s." % part
|
||||
)
|
||||
t = fpart.headers.get("content-type")
|
||||
content = fpart.get_content(strict=False)
|
||||
if not content:
|
||||
raise exceptions.CommandError("No content to view.")
|
||||
self.master.spawn_external_viewer(content, t)
|
||||
|
||||
@command.command("console.bodyview.options")
|
||||
def bodyview_options(self) -> Sequence[str]:
|
||||
"""
|
||||
Possible parts for console.bodyview.
|
||||
"""
|
||||
return ["request", "response"]
|
||||
|
||||
@command.command("console.edit.focus.options")
|
||||
def edit_focus_options(self) -> Sequence[str]:
|
||||
"""
|
||||
Possible components for console.edit.focus.
|
||||
"""
|
||||
flow = self.master.view.focus.flow
|
||||
focus_options = []
|
||||
|
||||
try:
|
||||
view_name = self.master.commands.call("console.flowview.mode")
|
||||
except CommandError:
|
||||
view_name = "auto"
|
||||
|
||||
def add_message_edit_option(
|
||||
message_name: str, message: ContentviewMessage | None
|
||||
) -> None:
|
||||
if message is None:
|
||||
return
|
||||
data, _ = contentviews.get_data(message)
|
||||
cv = contentviews.registry.get_view(
|
||||
data or b"",
|
||||
contentviews.make_metadata(message, flow),
|
||||
view_name,
|
||||
)
|
||||
if isinstance(cv, contentviews.InteractiveContentview):
|
||||
focus_options.append(f"{message_name} ({cv.name})")
|
||||
|
||||
if flow is None:
|
||||
raise exceptions.CommandError("No flow selected.")
|
||||
elif isinstance(flow, tcp.TCPFlow):
|
||||
focus_options.append("tcp-message")
|
||||
add_message_edit_option("tcp-message", flow.messages[-1])
|
||||
elif isinstance(flow, udp.UDPFlow):
|
||||
focus_options.append("udp-message")
|
||||
add_message_edit_option("udp-message", flow.messages[-1])
|
||||
elif isinstance(flow, http.HTTPFlow):
|
||||
focus_options.extend(
|
||||
[
|
||||
"cookies",
|
||||
"urlencoded form",
|
||||
"multipart form",
|
||||
"path",
|
||||
"method",
|
||||
"query",
|
||||
"reason",
|
||||
"request-headers",
|
||||
"response-headers",
|
||||
"request-body",
|
||||
"response-body",
|
||||
"status_code",
|
||||
"set-cookies",
|
||||
"url",
|
||||
]
|
||||
)
|
||||
add_message_edit_option("request-body", flow.request)
|
||||
add_message_edit_option("response-body", flow.response)
|
||||
if flow.websocket:
|
||||
add_message_edit_option(
|
||||
"websocket-message", flow.websocket.messages[-1]
|
||||
)
|
||||
elif isinstance(flow, dns.DNSFlow):
|
||||
raise exceptions.CommandError(
|
||||
"Cannot edit DNS flows yet, please submit a patch."
|
||||
)
|
||||
|
||||
return focus_options
|
||||
|
||||
@command.command("console.edit.focus")
|
||||
@command.argument(
|
||||
"flow_part", type=mitmproxy.types.Choice("console.edit.focus.options")
|
||||
)
|
||||
def edit_focus(self, flow_part: str) -> None:
|
||||
"""
|
||||
Edit a component of the currently focused flow.
|
||||
"""
|
||||
flow = self.master.view.focus.flow
|
||||
# This shouldn't be necessary once this command is "console.edit @focus",
|
||||
# but for now it is.
|
||||
if not flow:
|
||||
raise exceptions.CommandError("No flow selected.")
|
||||
flow.backup()
|
||||
|
||||
require_dummy_response = (
|
||||
flow_part in ("response-headers", "response-body", "set-cookies")
|
||||
and flow.response is None
|
||||
)
|
||||
if require_dummy_response:
|
||||
flow.response = http.Response.make()
|
||||
if flow_part == "cookies":
|
||||
self.master.switch_view("edit_focus_cookies")
|
||||
elif flow_part == "urlencoded form":
|
||||
self.master.switch_view("edit_focus_urlencoded_form")
|
||||
elif flow_part == "multipart form":
|
||||
self.master.switch_view("edit_focus_multipart_form")
|
||||
elif flow_part == "path":
|
||||
self.master.switch_view("edit_focus_path")
|
||||
elif flow_part == "query":
|
||||
self.master.switch_view("edit_focus_query")
|
||||
elif flow_part == "request-headers":
|
||||
self.master.switch_view("edit_focus_request_headers")
|
||||
elif flow_part == "response-headers":
|
||||
self.master.switch_view("edit_focus_response_headers")
|
||||
elif m := re.match(
|
||||
r"(?P<message>(request|response)-body|(tcp|udp|websocket)-message) \((?P<contentview>.+)\)",
|
||||
flow_part,
|
||||
):
|
||||
match m["message"]:
|
||||
case "request-body":
|
||||
message = flow.request
|
||||
case "response-body":
|
||||
message = flow.response
|
||||
case "tcp-message" | "udp-message":
|
||||
message = flow.messages[-1]
|
||||
case "websocket-message":
|
||||
message = flow.websocket.messages[-1]
|
||||
case _:
|
||||
assert False, "should be exhaustive"
|
||||
|
||||
cv = contentviews.registry.get(m["contentview"])
|
||||
if not cv or not isinstance(cv, contentviews.InteractiveContentview):
|
||||
raise CommandError(
|
||||
f"Contentview {m['contentview']} is not bidirectional."
|
||||
)
|
||||
|
||||
pretty = contentviews.prettify_message(message, flow, cv.name)
|
||||
prettified = self.master.spawn_editor(pretty.text)
|
||||
|
||||
message.content = contentviews.reencode_message(
|
||||
prettified,
|
||||
message,
|
||||
flow,
|
||||
cv.name,
|
||||
)
|
||||
|
||||
elif flow_part in ("request-body", "response-body"):
|
||||
if flow_part == "request-body":
|
||||
message = flow.request
|
||||
else:
|
||||
message = flow.response
|
||||
c = self.master.spawn_editor(message.get_content(strict=False) or b"")
|
||||
# Many editors make it hard to save a file without a terminating
|
||||
# newline on the last line. When editing message bodies, this can
|
||||
# cause problems. We strip trailing newlines by default, but this
|
||||
# behavior is configurable.
|
||||
if self.master.options.console_strip_trailing_newlines:
|
||||
c = c.rstrip(b"\n")
|
||||
message.content = c
|
||||
elif flow_part == "set-cookies":
|
||||
self.master.switch_view("edit_focus_setcookies")
|
||||
elif flow_part == "url":
|
||||
url = flow.request.url.encode()
|
||||
edited_url = self.master.spawn_editor(url)
|
||||
url = edited_url.rstrip(b"\n")
|
||||
flow.request.url = url.decode()
|
||||
elif flow_part in ["method", "status_code", "reason"]:
|
||||
self.master.commands.call_strings(
|
||||
"console.command", ["flow.set", "@focus", flow_part]
|
||||
)
|
||||
elif flow_part in ["tcp-message", "udp-message", "websocket-message"]:
|
||||
if flow_part == "websocket-message":
|
||||
message = flow.websocket.messages[-1]
|
||||
else:
|
||||
message = flow.messages[-1]
|
||||
c = self.master.spawn_editor(message.content or b"")
|
||||
if self.master.options.console_strip_trailing_newlines:
|
||||
c = c.rstrip(b"\n")
|
||||
message.content = c.rstrip(b"\n")
|
||||
|
||||
def _grideditor(self):
|
||||
gewidget = self.master.window.current("grideditor")
|
||||
if not gewidget:
|
||||
raise exceptions.CommandError("Not in a grideditor.")
|
||||
return gewidget.key_responder()
|
||||
|
||||
@command.command("console.grideditor.add")
|
||||
def grideditor_add(self) -> None:
|
||||
"""
|
||||
Add a row after the cursor.
|
||||
"""
|
||||
self._grideditor().cmd_add()
|
||||
|
||||
@command.command("console.grideditor.insert")
|
||||
def grideditor_insert(self) -> None:
|
||||
"""
|
||||
Insert a row before the cursor.
|
||||
"""
|
||||
self._grideditor().cmd_insert()
|
||||
|
||||
@command.command("console.grideditor.delete")
|
||||
def grideditor_delete(self) -> None:
|
||||
"""
|
||||
Delete row
|
||||
"""
|
||||
self._grideditor().cmd_delete()
|
||||
|
||||
@command.command("console.grideditor.load")
|
||||
def grideditor_load(self, path: mitmproxy.types.Path) -> None:
|
||||
"""
|
||||
Read a file into the currrent cell.
|
||||
"""
|
||||
self._grideditor().cmd_read_file(path)
|
||||
|
||||
@command.command("console.grideditor.load_escaped")
|
||||
def grideditor_load_escaped(self, path: mitmproxy.types.Path) -> None:
|
||||
"""
|
||||
Read a file containing a Python-style escaped string into the
|
||||
currrent cell.
|
||||
"""
|
||||
self._grideditor().cmd_read_file_escaped(path)
|
||||
|
||||
@command.command("console.grideditor.save")
|
||||
def grideditor_save(self, path: mitmproxy.types.Path) -> None:
|
||||
"""
|
||||
Save data to file as a CSV.
|
||||
"""
|
||||
rows = self._grideditor().value
|
||||
try:
|
||||
with open(path, "w", newline="", encoding="utf8") as fp:
|
||||
writer = csv.writer(fp)
|
||||
for row in rows:
|
||||
writer.writerow(
|
||||
[strutils.always_str(x) or "" for x in row] # type: ignore
|
||||
)
|
||||
logger.log(ALERT, "Saved %s rows as CSV." % (len(rows)))
|
||||
except OSError as e:
|
||||
logger.error(str(e))
|
||||
|
||||
@command.command("console.grideditor.editor")
|
||||
def grideditor_editor(self) -> None:
|
||||
"""
|
||||
Spawn an external editor on the current cell.
|
||||
"""
|
||||
self._grideditor().cmd_spawn_editor()
|
||||
|
||||
@command.command("console.flowview.mode.set")
|
||||
@command.argument(
|
||||
"mode", type=mitmproxy.types.Choice("console.flowview.mode.options")
|
||||
)
|
||||
def flowview_mode_set(self, mode: str) -> None:
|
||||
"""
|
||||
Set the display mode for the current flow view.
|
||||
"""
|
||||
fv = self.master.window.current_window("flowview")
|
||||
if not fv:
|
||||
raise exceptions.CommandError("Not viewing a flow.")
|
||||
idx = fv.body.tab_offset
|
||||
|
||||
if mode.lower() not in self.flowview_mode_options():
|
||||
raise exceptions.CommandError("Invalid flowview mode.")
|
||||
|
||||
try:
|
||||
self.master.commands.call_strings(
|
||||
"view.settings.setval", ["@focus", f"flowview_mode_{idx}", mode]
|
||||
)
|
||||
except exceptions.CommandError as e:
|
||||
logger.error(str(e))
|
||||
|
||||
@command.command("console.flowview.mode.options")
|
||||
def flowview_mode_options(self) -> Sequence[str]:
|
||||
"""
|
||||
Returns the valid options for the flowview mode.
|
||||
"""
|
||||
return contentviews.registry.available_views()
|
||||
|
||||
@command.command("console.flowview.mode")
|
||||
def flowview_mode(self) -> str:
|
||||
"""
|
||||
Get the display mode for the current flow view.
|
||||
"""
|
||||
fv = self.master.window.current_window("flowview")
|
||||
if not fv:
|
||||
raise exceptions.CommandError("Not viewing a flow.")
|
||||
idx = fv.body.tab_offset
|
||||
|
||||
return self.master.commands.call_strings(
|
||||
"view.settings.getval",
|
||||
[
|
||||
"@focus",
|
||||
f"flowview_mode_{idx}",
|
||||
self.master.options.console_default_contentview,
|
||||
],
|
||||
)
|
||||
|
||||
@command.command("console.key.contexts")
|
||||
def key_contexts(self) -> Sequence[str]:
|
||||
"""
|
||||
The available contexts for key binding.
|
||||
"""
|
||||
return list(sorted(keymap.Contexts))
|
||||
|
||||
@command.command("console.key.bind")
|
||||
def key_bind(
|
||||
self,
|
||||
contexts: Sequence[str],
|
||||
key: str,
|
||||
cmd: mitmproxy.types.Cmd,
|
||||
*args: mitmproxy.types.CmdArgs,
|
||||
) -> None:
|
||||
"""
|
||||
Bind a shortcut key.
|
||||
"""
|
||||
try:
|
||||
self.master.keymap.add(key, cmd + " " + " ".join(args), contexts, "")
|
||||
except ValueError as v:
|
||||
raise exceptions.CommandError(v)
|
||||
|
||||
@command.command("console.key.unbind")
|
||||
def key_unbind(self, contexts: Sequence[str], key: str) -> None:
|
||||
"""
|
||||
Un-bind a shortcut key.
|
||||
"""
|
||||
try:
|
||||
self.master.keymap.remove(key, contexts)
|
||||
except ValueError as v:
|
||||
raise exceptions.CommandError(v)
|
||||
|
||||
def _keyfocus(self):
|
||||
kwidget = self.master.window.current("keybindings")
|
||||
if not kwidget:
|
||||
raise exceptions.CommandError("Not viewing key bindings.")
|
||||
f = kwidget.get_focused_binding()
|
||||
if not f:
|
||||
raise exceptions.CommandError("No key binding focused")
|
||||
return f
|
||||
|
||||
@command.command("console.key.unbind.focus")
|
||||
def key_unbind_focus(self) -> None:
|
||||
"""
|
||||
Un-bind the shortcut key currently focused in the key binding viewer.
|
||||
"""
|
||||
b = self._keyfocus()
|
||||
try:
|
||||
self.master.keymap.remove(b.key, b.contexts)
|
||||
except ValueError as v:
|
||||
raise exceptions.CommandError(v)
|
||||
|
||||
@command.command("console.key.execute.focus")
|
||||
def key_execute_focus(self) -> None:
|
||||
"""
|
||||
Execute the currently focused key binding.
|
||||
"""
|
||||
b = self._keyfocus()
|
||||
self.console_command(b.command)
|
||||
|
||||
@command.command("console.key.edit.focus")
|
||||
def key_edit_focus(self) -> None:
|
||||
"""
|
||||
Execute the currently focused key binding.
|
||||
"""
|
||||
b = self._keyfocus()
|
||||
self.console_command(
|
||||
"console.key.bind",
|
||||
",".join(b.contexts),
|
||||
b.key,
|
||||
b.command,
|
||||
)
|
||||
|
||||
def running(self):
|
||||
self.started = True
|
||||
|
||||
def update(self, flows) -> None:
|
||||
if not flows:
|
||||
signals.update_settings.send()
|
||||
for f in flows:
|
||||
signals.flow_change.send(flow=f)
|
||||
271
venv/Lib/site-packages/mitmproxy/tools/console/defaultkeys.py
Normal file
271
venv/Lib/site-packages/mitmproxy/tools/console/defaultkeys.py
Normal file
@@ -0,0 +1,271 @@
|
||||
from mitmproxy.tools.console.keymap import Keymap
|
||||
|
||||
|
||||
def map(km: Keymap) -> None:
|
||||
km.add(":", "console.command ", ["commonkey", "global"], "Command prompt")
|
||||
km.add(
|
||||
";",
|
||||
"console.command flow.comment @focus ''",
|
||||
["flowlist", "flowview"],
|
||||
"Add comment to flow",
|
||||
)
|
||||
km.add("?", "console.view.help", ["global"], "View help")
|
||||
km.add("B", "browser.start", ["global"], "Start an attached browser")
|
||||
km.add("C", "console.view.commands", ["global"], "View commands")
|
||||
km.add("K", "console.view.keybindings", ["global"], "View key bindings")
|
||||
km.add("O", "console.view.options", ["commonkey", "global"], "View options")
|
||||
km.add("E", "console.view.eventlog", ["commonkey", "global"], "View event log")
|
||||
km.add("Q", "console.exit", ["global"], "Exit immediately")
|
||||
km.add("q", "console.view.pop", ["commonkey", "global"], "Exit the current view")
|
||||
km.add("esc", "console.view.pop", ["commonkey", "global"], "Exit the current view")
|
||||
km.add("-", "console.layout.cycle", ["global"], "Cycle to next layout")
|
||||
km.add("ctrl right", "console.panes.next", ["global"], "Focus next layout pane")
|
||||
km.add("ctrl left", "console.panes.prev", ["global"], "Focus previous layout pane")
|
||||
km.add("shift tab", "console.panes.next", ["global"], "Focus next layout pane")
|
||||
km.add("P", "console.view.flow @focus", ["global"], "View flow details")
|
||||
|
||||
km.add("?", "console.view.pop", ["help"], "Exit help")
|
||||
|
||||
km.add("g", "console.nav.start", ["global"], "Go to start")
|
||||
km.add("G", "console.nav.end", ["global"], "Go to end")
|
||||
km.add("k", "console.nav.up", ["global"], "Up")
|
||||
km.add("j", "console.nav.down", ["global"], "Down")
|
||||
km.add("l", "console.nav.right", ["global"], "Right")
|
||||
km.add("h", "console.nav.left", ["global"], "Left")
|
||||
km.add("tab", "console.nav.next", ["commonkey", "global"], "Next")
|
||||
km.add("enter", "console.nav.select", ["commonkey", "global"], "Select")
|
||||
km.add("space", "console.nav.pagedown", ["global"], "Page down")
|
||||
km.add("ctrl f", "console.nav.pagedown", ["global"], "Page down")
|
||||
km.add("ctrl b", "console.nav.pageup", ["global"], "Page up")
|
||||
|
||||
km.add(
|
||||
"I",
|
||||
"set intercept_active toggle",
|
||||
["global"],
|
||||
"Toggle whether the filtering via the intercept option is enabled",
|
||||
)
|
||||
km.add("i", "console.command.set intercept", ["global"], "Set intercept")
|
||||
km.add("W", "console.command.set save_stream_file", ["global"], "Stream to file")
|
||||
km.add(
|
||||
"A",
|
||||
"flow.resume @all",
|
||||
["flowlist", "flowview"],
|
||||
"Resume all intercepted flows",
|
||||
)
|
||||
km.add(
|
||||
"a",
|
||||
"flow.resume @focus",
|
||||
["flowlist", "flowview"],
|
||||
"Resume this intercepted flow",
|
||||
)
|
||||
km.add(
|
||||
"b",
|
||||
"console.command cut.save @focus response.content ",
|
||||
["flowlist", "flowview"],
|
||||
"Save response body to file",
|
||||
)
|
||||
km.add(
|
||||
"d",
|
||||
"view.flows.remove @focus",
|
||||
["flowlist", "flowview"],
|
||||
"Delete flow from view",
|
||||
)
|
||||
km.add(
|
||||
"D", "view.flows.duplicate @focus", ["flowlist", "flowview"], "Duplicate flow"
|
||||
)
|
||||
km.add(
|
||||
"x",
|
||||
"""
|
||||
console.choose.cmd "Export as..." export.formats
|
||||
console.command export.file {choice} @focus
|
||||
""",
|
||||
["flowlist", "flowview"],
|
||||
"Export this flow to file",
|
||||
)
|
||||
km.add("f", "console.command.set view_filter", ["flowlist"], "Set view filter")
|
||||
km.add(
|
||||
"F",
|
||||
"set console_focus_follow toggle",
|
||||
["flowlist", "flowview"],
|
||||
"Set focus follow",
|
||||
)
|
||||
km.add(
|
||||
"ctrl l",
|
||||
"console.command cut.clip ",
|
||||
["flowlist", "flowview"],
|
||||
"Send cuts to clipboard",
|
||||
)
|
||||
km.add(
|
||||
"L", "console.command view.flows.load ", ["flowlist"], "Load flows from file"
|
||||
)
|
||||
km.add("m", "flow.mark.toggle @focus", ["flowlist"], "Toggle mark on this flow")
|
||||
km.add(
|
||||
"M",
|
||||
"view.properties.marked.toggle",
|
||||
["flowlist"],
|
||||
"Toggle viewing marked flows",
|
||||
)
|
||||
km.add(
|
||||
"n",
|
||||
"console.command view.flows.create get https://example.com/",
|
||||
["flowlist"],
|
||||
"Create a new flow",
|
||||
)
|
||||
km.add(
|
||||
"o",
|
||||
"""
|
||||
console.choose.cmd "Order flows by..." view.order.options
|
||||
set view_order {choice}
|
||||
""",
|
||||
["flowlist"],
|
||||
"Set flow list order",
|
||||
)
|
||||
km.add("r", "replay.client @focus", ["flowlist", "flowview"], "Replay this flow")
|
||||
km.add("S", "console.command replay.server ", ["flowlist"], "Start server replay")
|
||||
km.add(
|
||||
"v", "set view_order_reversed toggle", ["flowlist"], "Reverse flow list order"
|
||||
)
|
||||
km.add("U", "flow.mark @all false", ["flowlist"], "Un-set all marks")
|
||||
km.add(
|
||||
"w",
|
||||
"console.command save.file @shown ",
|
||||
["flowlist"],
|
||||
"Save listed flows to file",
|
||||
)
|
||||
km.add(
|
||||
"V",
|
||||
"flow.revert @focus",
|
||||
["flowlist", "flowview"],
|
||||
"Revert changes to this flow",
|
||||
)
|
||||
km.add("X", "flow.kill @focus", ["flowlist"], "Kill this flow")
|
||||
km.add(
|
||||
"z",
|
||||
'console.command.confirm "Delete all flows" view.flows.remove @all',
|
||||
["flowlist"],
|
||||
"Clear flow list",
|
||||
)
|
||||
km.add(
|
||||
"Z",
|
||||
'console.command.confirm "Purge all hidden flows" view.flows.remove @hidden',
|
||||
["flowlist"],
|
||||
"Purge all flows not showing",
|
||||
)
|
||||
km.add(
|
||||
"|",
|
||||
"console.command script.run @focus ",
|
||||
["flowlist", "flowview"],
|
||||
"Run a script on this flow",
|
||||
)
|
||||
|
||||
km.add(
|
||||
"e",
|
||||
"""
|
||||
console.choose.cmd "Edit..." console.edit.focus.options
|
||||
console.edit.focus {choice}
|
||||
""",
|
||||
["flowlist", "flowview"],
|
||||
"Edit a flow component",
|
||||
)
|
||||
km.add(
|
||||
"f",
|
||||
"view.settings.setval.toggle @focus fullcontents",
|
||||
["flowview"],
|
||||
"Toggle viewing full contents on this flow",
|
||||
)
|
||||
km.add("w", "console.command save.file @focus ", ["flowview"], "Save flow to file")
|
||||
km.add("space", "view.focus.next", ["flowview"], "Go to next flow")
|
||||
|
||||
km.add(
|
||||
"v",
|
||||
"""
|
||||
console.choose "View..." request,response
|
||||
console.bodyview @focus {choice}
|
||||
""",
|
||||
["flowview"],
|
||||
"View flow body in an external viewer",
|
||||
)
|
||||
km.add("p", "view.focus.prev", ["flowview"], "Go to previous flow")
|
||||
km.add(
|
||||
"m",
|
||||
"""
|
||||
console.choose.cmd "Set contentview..." console.flowview.mode.options
|
||||
console.flowview.mode.set {choice}
|
||||
""",
|
||||
["flowview"],
|
||||
"Set flow view mode",
|
||||
)
|
||||
km.add(
|
||||
"z",
|
||||
"""
|
||||
console.choose "Encode/decode..." request,response
|
||||
flow.encode.toggle @focus {choice}
|
||||
""",
|
||||
["flowview"],
|
||||
"Encode/decode flow body",
|
||||
)
|
||||
|
||||
km.add("L", "console.command options.load ", ["options"], "Load from file")
|
||||
km.add("S", "console.command options.save ", ["options"], "Save to file")
|
||||
km.add("D", "options.reset", ["options"], "Reset all options")
|
||||
km.add("d", "console.options.reset.focus", ["options"], "Reset this option")
|
||||
|
||||
km.add("a", "console.grideditor.add", ["grideditor"], "Add a row after cursor")
|
||||
km.add(
|
||||
"A", "console.grideditor.insert", ["grideditor"], "Insert a row before cursor"
|
||||
)
|
||||
km.add("d", "console.grideditor.delete", ["grideditor"], "Delete this row")
|
||||
km.add(
|
||||
"r",
|
||||
"console.command console.grideditor.load",
|
||||
["grideditor"],
|
||||
"Read unescaped data into the current cell from file",
|
||||
)
|
||||
km.add(
|
||||
"R",
|
||||
"console.command console.grideditor.load_escaped",
|
||||
["grideditor"],
|
||||
"Load a Python-style escaped string into the current cell from file",
|
||||
)
|
||||
km.add("e", "console.grideditor.editor", ["grideditor"], "Edit in external editor")
|
||||
km.add(
|
||||
"w",
|
||||
"console.command console.grideditor.save ",
|
||||
["grideditor"],
|
||||
"Save data to file as CSV",
|
||||
)
|
||||
|
||||
km.add(
|
||||
"z",
|
||||
'console.command.confirm "Clear event log" eventstore.clear',
|
||||
["eventlog"],
|
||||
"Clear",
|
||||
)
|
||||
|
||||
km.add(
|
||||
"a",
|
||||
"""
|
||||
console.choose.cmd "Context" console.key.contexts
|
||||
console.command console.key.bind {choice}
|
||||
""",
|
||||
["keybindings"],
|
||||
"Add a key binding",
|
||||
)
|
||||
km.add(
|
||||
"d",
|
||||
"console.key.unbind.focus",
|
||||
["keybindings"],
|
||||
"Unbind the currently focused key binding",
|
||||
)
|
||||
km.add(
|
||||
"x",
|
||||
"console.key.execute.focus",
|
||||
["keybindings"],
|
||||
"Execute the currently focused key binding",
|
||||
)
|
||||
km.add(
|
||||
"enter",
|
||||
"console.key.edit.focus",
|
||||
["keybindings"],
|
||||
"Edit the currently focused key binding",
|
||||
)
|
||||
63
venv/Lib/site-packages/mitmproxy/tools/console/eventlog.py
Normal file
63
venv/Lib/site-packages/mitmproxy/tools/console/eventlog.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import collections
|
||||
|
||||
import urwid
|
||||
|
||||
from mitmproxy import log
|
||||
from mitmproxy.tools.console import layoutwidget
|
||||
|
||||
|
||||
class LogBufferWalker(urwid.SimpleListWalker):
|
||||
pass
|
||||
|
||||
|
||||
class EventLog(urwid.ListBox, layoutwidget.LayoutWidget):
|
||||
keyctx = "eventlog"
|
||||
title = "Events"
|
||||
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
self.walker = LogBufferWalker(collections.deque(maxlen=self.master.events.size))
|
||||
|
||||
master.events.sig_add.connect(self.add_event)
|
||||
master.events.sig_refresh.connect(self.refresh_events)
|
||||
self.master.options.subscribe(
|
||||
self.refresh_events, ["console_eventlog_verbosity"]
|
||||
)
|
||||
self.refresh_events()
|
||||
|
||||
super().__init__(self.walker)
|
||||
|
||||
def load(self, loader):
|
||||
loader.add_option(
|
||||
"console_focus_follow", bool, False, "Focus follows new flows."
|
||||
)
|
||||
|
||||
def set_focus(self, index):
|
||||
if 0 <= index < len(self.walker):
|
||||
super().set_focus(index)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "m_end":
|
||||
self.set_focus(len(self.walker) - 1)
|
||||
elif key == "m_start":
|
||||
self.set_focus(0)
|
||||
return super().keypress(size, key)
|
||||
|
||||
def add_event(self, entry: log.LogEntry):
|
||||
if log.log_tier(self.master.options.console_eventlog_verbosity) < log.log_tier(
|
||||
entry.level
|
||||
):
|
||||
return
|
||||
txt = f"{entry.level}: {entry.msg}"
|
||||
if entry.level in ("error", "warn", "alert"):
|
||||
e = urwid.Text((entry.level, txt))
|
||||
else:
|
||||
e = urwid.Text(txt)
|
||||
self.walker.append(e)
|
||||
if self.master.options.console_focus_follow:
|
||||
self.walker.set_focus(len(self.walker) - 1)
|
||||
|
||||
def refresh_events(self, *_) -> None:
|
||||
self.walker.clear()
|
||||
for event in self.master.events.data:
|
||||
self.add_event(event)
|
||||
149
venv/Lib/site-packages/mitmproxy/tools/console/flowdetailview.py
Normal file
149
venv/Lib/site-packages/mitmproxy/tools/console/flowdetailview.py
Normal file
@@ -0,0 +1,149 @@
|
||||
import urwid
|
||||
|
||||
import mitmproxy.flow
|
||||
from mitmproxy import http
|
||||
from mitmproxy.tools.console import common
|
||||
from mitmproxy.tools.console import searchable
|
||||
from mitmproxy.utils import human
|
||||
from mitmproxy.utils import strutils
|
||||
|
||||
|
||||
def maybe_timestamp(base, attr):
|
||||
if base is not None and getattr(base, attr):
|
||||
return human.format_timestamp_with_milli(getattr(base, attr))
|
||||
else:
|
||||
# in mitmdump we serialize before a connection is closed.
|
||||
# loading those flows at a later point shouldn't display "active".
|
||||
# We also use a ndash (and not a regular dash) so that it is sorted
|
||||
# after other timestamps. We may need to revisit that in the future if it turns out
|
||||
# to render ugly in consoles.
|
||||
return "–"
|
||||
|
||||
|
||||
def flowdetails(state, flow: mitmproxy.flow.Flow):
|
||||
text = []
|
||||
|
||||
sc = flow.server_conn
|
||||
cc = flow.client_conn
|
||||
req: http.Request | None
|
||||
resp: http.Response | None
|
||||
if isinstance(flow, http.HTTPFlow):
|
||||
req = flow.request
|
||||
resp = flow.response
|
||||
else:
|
||||
req = None
|
||||
resp = None
|
||||
metadata = flow.metadata
|
||||
comment = flow.comment
|
||||
|
||||
if comment:
|
||||
text.append(urwid.Text([("head", "Comment: "), ("text", comment)]))
|
||||
|
||||
if metadata is not None and len(metadata) > 0:
|
||||
parts = [(str(k), repr(v)) for k, v in metadata.items()]
|
||||
text.append(urwid.Text([("head", "Metadata:")]))
|
||||
text.extend(common.format_keyvals(parts, indent=4))
|
||||
|
||||
if sc is not None and sc.peername:
|
||||
text.append(urwid.Text([("head", "Server Connection:")]))
|
||||
parts = [
|
||||
("Address", human.format_address(sc.address)),
|
||||
]
|
||||
if sc.peername:
|
||||
parts.append(("Resolved Address", human.format_address(sc.peername)))
|
||||
if resp:
|
||||
parts.append(("HTTP Version", resp.http_version))
|
||||
if sc.alpn:
|
||||
parts.append(("ALPN", strutils.bytes_to_escaped_str(sc.alpn)))
|
||||
|
||||
text.extend(common.format_keyvals(parts, indent=4))
|
||||
|
||||
if sc.certificate_list:
|
||||
c = sc.certificate_list[0]
|
||||
text.append(urwid.Text([("head", "Server Certificate:")]))
|
||||
parts = [
|
||||
("Type", "%s, %s bits" % c.keyinfo),
|
||||
("SHA256 digest", c.fingerprint().hex(" ")),
|
||||
("Valid from", str(c.notbefore)),
|
||||
("Valid to", str(c.notafter)),
|
||||
("Serial", str(c.serial)),
|
||||
(
|
||||
"Subject",
|
||||
urwid.Pile(
|
||||
common.format_keyvals(c.subject, key_format="highlight")
|
||||
),
|
||||
),
|
||||
(
|
||||
"Issuer",
|
||||
urwid.Pile(common.format_keyvals(c.issuer, key_format="highlight")),
|
||||
),
|
||||
]
|
||||
|
||||
if c.altnames:
|
||||
parts.append(("Alt names", ", ".join(str(x.value) for x in c.altnames)))
|
||||
text.extend(common.format_keyvals(parts, indent=4))
|
||||
|
||||
if cc is not None:
|
||||
text.append(urwid.Text([("head", "Client Connection:")]))
|
||||
|
||||
parts = [
|
||||
("Address", human.format_address(cc.peername)),
|
||||
]
|
||||
if req:
|
||||
parts.append(("HTTP Version", req.http_version))
|
||||
if cc.tls_version:
|
||||
parts.append(("TLS Version", cc.tls_version))
|
||||
if cc.sni:
|
||||
parts.append(("Server Name Indication", cc.sni))
|
||||
if cc.cipher:
|
||||
parts.append(("Cipher Name", cc.cipher))
|
||||
if cc.alpn:
|
||||
parts.append(("ALPN", strutils.bytes_to_escaped_str(cc.alpn)))
|
||||
|
||||
text.extend(common.format_keyvals(parts, indent=4))
|
||||
|
||||
parts = []
|
||||
|
||||
if cc is not None and cc.timestamp_start:
|
||||
parts.append(
|
||||
("Client conn. established", maybe_timestamp(cc, "timestamp_start"))
|
||||
)
|
||||
if cc.tls_established:
|
||||
parts.append(
|
||||
(
|
||||
"Client conn. TLS handshake",
|
||||
maybe_timestamp(cc, "timestamp_tls_setup"),
|
||||
)
|
||||
)
|
||||
parts.append(("Client conn. closed", maybe_timestamp(cc, "timestamp_end")))
|
||||
|
||||
if sc is not None and sc.timestamp_start:
|
||||
parts.append(("Server conn. initiated", maybe_timestamp(sc, "timestamp_start")))
|
||||
parts.append(
|
||||
("Server conn. TCP handshake", maybe_timestamp(sc, "timestamp_tcp_setup"))
|
||||
)
|
||||
if sc.tls_established:
|
||||
parts.append(
|
||||
(
|
||||
"Server conn. TLS handshake",
|
||||
maybe_timestamp(sc, "timestamp_tls_setup"),
|
||||
)
|
||||
)
|
||||
parts.append(("Server conn. closed", maybe_timestamp(sc, "timestamp_end")))
|
||||
|
||||
if req is not None and req.timestamp_start:
|
||||
parts.append(("First request byte", maybe_timestamp(req, "timestamp_start")))
|
||||
parts.append(("Request complete", maybe_timestamp(req, "timestamp_end")))
|
||||
|
||||
if resp is not None and resp.timestamp_start:
|
||||
parts.append(("First response byte", maybe_timestamp(resp, "timestamp_start")))
|
||||
parts.append(("Response complete", maybe_timestamp(resp, "timestamp_end")))
|
||||
|
||||
if parts:
|
||||
# sort operations by timestamp
|
||||
parts = sorted(parts, key=lambda p: p[1])
|
||||
|
||||
text.append(urwid.Text([("head", "Timing:")]))
|
||||
text.extend(common.format_keyvals(parts, indent=4))
|
||||
|
||||
return searchable.Searchable(text)
|
||||
108
venv/Lib/site-packages/mitmproxy/tools/console/flowlist.py
Normal file
108
venv/Lib/site-packages/mitmproxy/tools/console/flowlist.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from functools import lru_cache
|
||||
|
||||
import urwid
|
||||
|
||||
import mitmproxy.tools.console.master
|
||||
from mitmproxy.tools.console import common
|
||||
from mitmproxy.tools.console import layoutwidget
|
||||
|
||||
|
||||
class FlowItem(urwid.WidgetWrap):
|
||||
def __init__(self, master, flow):
|
||||
self.master, self.flow = master, flow
|
||||
w = self.get_text()
|
||||
urwid.WidgetWrap.__init__(self, w)
|
||||
|
||||
def get_text(self):
|
||||
cols, _ = self.master.ui.get_cols_rows()
|
||||
layout = self.master.options.console_flowlist_layout
|
||||
if layout == "list" or (layout == "default" and cols < 100):
|
||||
render_mode = common.RenderMode.LIST
|
||||
else:
|
||||
render_mode = common.RenderMode.TABLE
|
||||
|
||||
return common.format_flow(
|
||||
self.flow,
|
||||
render_mode=render_mode,
|
||||
focused=self.flow is self.master.view.focus.flow,
|
||||
hostheader=self.master.options.showhost,
|
||||
)
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
if event == "mouse press" and button == 1:
|
||||
self.master.commands.execute("console.view.flow @focus")
|
||||
return True
|
||||
|
||||
def keypress(self, size, key):
|
||||
return key
|
||||
|
||||
|
||||
class FlowListWalker(urwid.ListWalker):
|
||||
master: "mitmproxy.tools.console.master.ConsoleMaster"
|
||||
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
|
||||
def positions(self, reverse=False):
|
||||
# The stub implementation of positions can go once this issue is resolved:
|
||||
# https://github.com/urwid/urwid/issues/294
|
||||
ret = range(self.master.view.get_length())
|
||||
if reverse:
|
||||
return reversed(ret)
|
||||
return ret
|
||||
|
||||
def view_changed(self):
|
||||
self._modified()
|
||||
self._get.cache_clear()
|
||||
|
||||
def get_focus(self):
|
||||
if not self.master.view.focus.flow:
|
||||
return None, 0
|
||||
f = FlowItem(self.master, self.master.view.focus.flow)
|
||||
return f, self.master.view.focus.index
|
||||
|
||||
def set_focus(self, index):
|
||||
if self.master.commands.execute("view.properties.inbounds %d" % index):
|
||||
self.master.view.focus.index = index
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _get(self, pos: int) -> tuple[FlowItem | None, int | None]:
|
||||
if not self.master.view.inbounds(pos):
|
||||
return None, None
|
||||
return FlowItem(self.master, self.master.view[pos]), pos
|
||||
|
||||
def get_next(self, pos):
|
||||
return self._get(pos + 1)
|
||||
|
||||
def get_prev(self, pos):
|
||||
return self._get(pos - 1)
|
||||
|
||||
|
||||
class FlowListBox(urwid.ListBox, layoutwidget.LayoutWidget):
|
||||
title = "Flows"
|
||||
keyctx = "flowlist"
|
||||
|
||||
def __init__(self, master: "mitmproxy.tools.console.master.ConsoleMaster") -> None:
|
||||
self.master: "mitmproxy.tools.console.master.ConsoleMaster" = master
|
||||
super().__init__(FlowListWalker(master))
|
||||
self.master.options.subscribe(
|
||||
self.set_flowlist_layout, ["console_flowlist_layout"]
|
||||
)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "m_start":
|
||||
self.master.commands.execute("view.focus.go 0")
|
||||
elif key == "m_end":
|
||||
self.master.commands.execute("view.focus.go -1")
|
||||
elif key == "m_select":
|
||||
self.master.commands.execute("console.view.flow @focus")
|
||||
return urwid.ListBox.keypress(self, size, key)
|
||||
|
||||
def view_changed(self):
|
||||
self.body.view_changed()
|
||||
|
||||
def set_flowlist_layout(self, *_) -> None:
|
||||
self.master.ui.clear()
|
||||
501
venv/Lib/site-packages/mitmproxy/tools/console/flowview.py
Normal file
501
venv/Lib/site-packages/mitmproxy/tools/console/flowview.py
Normal file
@@ -0,0 +1,501 @@
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
|
||||
import urwid
|
||||
|
||||
import mitmproxy.flow
|
||||
import mitmproxy.tools.console.master
|
||||
import mitmproxy_rs.syntax_highlight
|
||||
from mitmproxy import contentviews
|
||||
from mitmproxy import ctx
|
||||
from mitmproxy import dns
|
||||
from mitmproxy import http
|
||||
from mitmproxy import tcp
|
||||
from mitmproxy import udp
|
||||
from mitmproxy.dns import DNSMessage
|
||||
from mitmproxy.tools.console import common
|
||||
from mitmproxy.tools.console import flowdetailview
|
||||
from mitmproxy.tools.console import layoutwidget
|
||||
from mitmproxy.tools.console import searchable
|
||||
from mitmproxy.tools.console import tabs
|
||||
from mitmproxy.utils import strutils
|
||||
|
||||
|
||||
class SearchError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FlowViewHeader(urwid.WidgetWrap):
|
||||
def __init__(
|
||||
self,
|
||||
master: "mitmproxy.tools.console.master.ConsoleMaster",
|
||||
) -> None:
|
||||
self.master = master
|
||||
self.focus_changed()
|
||||
|
||||
def focus_changed(self):
|
||||
cols, _ = self.master.ui.get_cols_rows()
|
||||
if self.master.view.focus.flow:
|
||||
self._w = common.format_flow(
|
||||
self.master.view.focus.flow,
|
||||
render_mode=common.RenderMode.DETAILVIEW,
|
||||
hostheader=self.master.options.showhost,
|
||||
)
|
||||
else:
|
||||
self._w = urwid.Pile([])
|
||||
|
||||
|
||||
class FlowDetails(tabs.Tabs):
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
super().__init__([])
|
||||
self.show()
|
||||
self.last_displayed_body = None
|
||||
self.last_displayed_websocket_messages = None
|
||||
contentviews.registry.on_change.connect(self.contentview_changed)
|
||||
|
||||
@property
|
||||
def view(self):
|
||||
return self.master.view
|
||||
|
||||
@property
|
||||
def flow(self) -> mitmproxy.flow.Flow:
|
||||
return self.master.view.focus.flow
|
||||
|
||||
def contentview_changed(self, view):
|
||||
# this is called when a contentview addon is live-reloaded.
|
||||
# we clear our cache and then rerender
|
||||
self._get_content_view.cache_clear()
|
||||
if self.master.window.current_window("flowview"):
|
||||
self.show()
|
||||
|
||||
def focus_changed(self):
|
||||
f = self.flow
|
||||
if f:
|
||||
if isinstance(f, http.HTTPFlow):
|
||||
if f.websocket:
|
||||
self.tabs = [
|
||||
(self.tab_http_request, self.view_request),
|
||||
(self.tab_http_response, self.view_response),
|
||||
(self.tab_websocket_messages, self.view_websocket_messages),
|
||||
(self.tab_details, self.view_details),
|
||||
]
|
||||
else:
|
||||
self.tabs = [
|
||||
(self.tab_http_request, self.view_request),
|
||||
(self.tab_http_response, self.view_response),
|
||||
(self.tab_details, self.view_details),
|
||||
]
|
||||
elif isinstance(f, tcp.TCPFlow):
|
||||
self.tabs = [
|
||||
(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):
|
||||
self.tabs = [
|
||||
(self.tab_dns_request, self.view_dns_request),
|
||||
(self.tab_dns_response, self.view_dns_response),
|
||||
(self.tab_details, self.view_details),
|
||||
]
|
||||
self.show()
|
||||
else:
|
||||
# Get the top window from the focus stack (the currently active view).
|
||||
# If it's NOT the "flowlist", it's safe to pop back to the previous view.
|
||||
if self.master.window.focus_stack().stack[-1] != "flowlist":
|
||||
self.master.window.pop()
|
||||
# If it is the "flowlist", we’re already at the main view with no flows to show.
|
||||
# Popping now would close the last window and prompt app exit, so we remain on the empty flow list screen instead.
|
||||
|
||||
def tab_http_request(self):
|
||||
flow = self.flow
|
||||
assert isinstance(flow, http.HTTPFlow)
|
||||
if self.flow.intercepted and not flow.response:
|
||||
return "Request intercepted"
|
||||
else:
|
||||
return "Request"
|
||||
|
||||
def tab_http_response(self):
|
||||
flow = self.flow
|
||||
assert isinstance(flow, http.HTTPFlow)
|
||||
|
||||
# there is no good way to detect what part of the flow is intercepted,
|
||||
# so we apply some heuristics to see if it's the HTTP response.
|
||||
websocket_started = flow.websocket and len(flow.websocket.messages) != 0
|
||||
response_is_intercepted = (
|
||||
self.flow.intercepted and flow.response and not websocket_started
|
||||
)
|
||||
if response_is_intercepted:
|
||||
return "Response intercepted"
|
||||
else:
|
||||
return "Response"
|
||||
|
||||
def tab_dns_request(self) -> str:
|
||||
flow = self.flow
|
||||
assert isinstance(flow, dns.DNSFlow)
|
||||
if self.flow.intercepted and not flow.response:
|
||||
return "Request intercepted"
|
||||
else:
|
||||
return "Request"
|
||||
|
||||
def tab_dns_response(self) -> str:
|
||||
flow = self.flow
|
||||
assert isinstance(flow, dns.DNSFlow)
|
||||
if self.flow.intercepted and flow.response:
|
||||
return "Response intercepted"
|
||||
else:
|
||||
return "Response"
|
||||
|
||||
def tab_tcp_stream(self):
|
||||
return "TCP Stream"
|
||||
|
||||
def tab_udp_stream(self):
|
||||
return "UDP Stream"
|
||||
|
||||
def tab_websocket_messages(self):
|
||||
flow = self.flow
|
||||
assert isinstance(flow, http.HTTPFlow)
|
||||
assert flow.websocket
|
||||
|
||||
if self.flow.intercepted and len(flow.websocket.messages) != 0:
|
||||
return "WebSocket Messages intercepted"
|
||||
else:
|
||||
return "WebSocket Messages"
|
||||
|
||||
def tab_details(self):
|
||||
return "Detail"
|
||||
|
||||
def view_request(self):
|
||||
flow = self.flow
|
||||
assert isinstance(flow, http.HTTPFlow)
|
||||
return self.conn_text(flow.request)
|
||||
|
||||
def view_response(self):
|
||||
flow = self.flow
|
||||
assert isinstance(flow, http.HTTPFlow)
|
||||
return self.conn_text(flow.response)
|
||||
|
||||
def view_dns_request(self):
|
||||
flow = self.flow
|
||||
assert isinstance(flow, dns.DNSFlow)
|
||||
return self.dns_message_text("request", flow.request)
|
||||
|
||||
def view_dns_response(self):
|
||||
flow = self.flow
|
||||
assert isinstance(flow, dns.DNSFlow)
|
||||
return self.dns_message_text("response", flow.response)
|
||||
|
||||
def _contentview_status_bar(self, description: str, viewmode: str):
|
||||
cols = [
|
||||
urwid.Text(
|
||||
[
|
||||
("heading", description),
|
||||
]
|
||||
),
|
||||
urwid.Text(
|
||||
[
|
||||
" ",
|
||||
("heading", "["),
|
||||
("heading_key", "m"),
|
||||
("heading", (":%s]" % viewmode)),
|
||||
],
|
||||
align="right",
|
||||
),
|
||||
]
|
||||
contentview_status_bar = urwid.AttrMap(urwid.Columns(cols), "heading")
|
||||
return contentview_status_bar
|
||||
|
||||
FROM_CLIENT_MARKER = ("from_client", f"{common.SYMBOL_FROM_CLIENT} ")
|
||||
TO_CLIENT_MARKER = ("to_client", f"{common.SYMBOL_TO_CLIENT} ")
|
||||
|
||||
def view_websocket_messages(self):
|
||||
flow = self.flow
|
||||
assert isinstance(flow, http.HTTPFlow)
|
||||
assert flow.websocket is not None
|
||||
|
||||
if not flow.websocket.messages:
|
||||
return searchable.Searchable([urwid.Text(("highlight", "No messages."))])
|
||||
|
||||
viewmode = self.master.commands.call("console.flowview.mode")
|
||||
|
||||
widget_lines = []
|
||||
for m in flow.websocket.messages:
|
||||
pretty = contentviews.prettify_message(m, flow, viewmode)
|
||||
chunks = mitmproxy_rs.syntax_highlight.highlight(
|
||||
pretty.text,
|
||||
language=pretty.syntax_highlight,
|
||||
)
|
||||
if m.from_client:
|
||||
marker = self.FROM_CLIENT_MARKER
|
||||
else:
|
||||
marker = self.TO_CLIENT_MARKER
|
||||
widget_lines.append(urwid.Text([marker, *chunks]))
|
||||
|
||||
if flow.websocket.closed_by_client is not None:
|
||||
widget_lines.append(
|
||||
urwid.Text(
|
||||
[
|
||||
(
|
||||
self.FROM_CLIENT_MARKER
|
||||
if flow.websocket.closed_by_client
|
||||
else self.TO_CLIENT_MARKER
|
||||
),
|
||||
(
|
||||
"alert"
|
||||
if flow.websocket.close_code in (1000, 1001, 1005)
|
||||
else "error",
|
||||
f"Connection closed: {flow.websocket.close_code} {flow.websocket.close_reason}",
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
if flow.intercepted:
|
||||
markup = widget_lines[-1].get_text()[0]
|
||||
widget_lines[-1].set_text(("intercept", markup))
|
||||
|
||||
widget_lines.insert(
|
||||
0, self._contentview_status_bar(viewmode.capitalize(), viewmode)
|
||||
)
|
||||
|
||||
if (last_view := self.last_displayed_websocket_messages) is not None:
|
||||
last_view.walker[:] = widget_lines
|
||||
view = last_view
|
||||
else:
|
||||
view = searchable.Searchable(widget_lines)
|
||||
self.last_displayed_websocket_messages = view
|
||||
|
||||
return view
|
||||
|
||||
def view_message_stream(self) -> urwid.Widget:
|
||||
flow = self.flow
|
||||
assert isinstance(flow, (tcp.TCPFlow, udp.UDPFlow))
|
||||
|
||||
if not flow.messages:
|
||||
return searchable.Searchable([urwid.Text(("highlight", "No messages."))])
|
||||
|
||||
viewmode = self.master.commands.call("console.flowview.mode")
|
||||
|
||||
widget_lines = []
|
||||
for m in flow.messages:
|
||||
if m.from_client:
|
||||
marker = self.FROM_CLIENT_MARKER
|
||||
else:
|
||||
marker = self.TO_CLIENT_MARKER
|
||||
pretty = contentviews.prettify_message(m, flow, viewmode)
|
||||
chunks = mitmproxy_rs.syntax_highlight.highlight(
|
||||
pretty.text,
|
||||
language=pretty.syntax_highlight,
|
||||
)
|
||||
|
||||
widget_lines.append(urwid.Text([marker, *chunks]))
|
||||
|
||||
if flow.intercepted:
|
||||
markup = widget_lines[-1].get_text()[0]
|
||||
widget_lines[-1].set_text(("intercept", markup))
|
||||
|
||||
widget_lines.insert(
|
||||
0, self._contentview_status_bar(viewmode.capitalize(), viewmode)
|
||||
)
|
||||
|
||||
return searchable.Searchable(widget_lines)
|
||||
|
||||
def view_details(self):
|
||||
return flowdetailview.flowdetails(self.view, self.flow)
|
||||
|
||||
def content_view(
|
||||
self, viewmode: str, message: http.Message
|
||||
) -> tuple[str, list[urwid.Text]]:
|
||||
if message.raw_content is None:
|
||||
return "", [urwid.Text([("error", "[content missing]")])]
|
||||
|
||||
if message.raw_content == b"":
|
||||
if isinstance(message, http.Request):
|
||||
query = getattr(message, "query", "")
|
||||
if not query:
|
||||
# No body and no query params
|
||||
return "", [urwid.Text("No request content")]
|
||||
# else: there are query params -> fall through to render them
|
||||
else:
|
||||
return "", [urwid.Text("No content")]
|
||||
|
||||
full = self.master.commands.execute(
|
||||
"view.settings.getval @focus fullcontents false"
|
||||
)
|
||||
|
||||
if full == "true":
|
||||
limit = sys.maxsize
|
||||
else:
|
||||
limit = ctx.options.content_view_lines_cutoff
|
||||
|
||||
flow_modify_cache_invalidation = hash(
|
||||
(
|
||||
message.raw_content,
|
||||
message.headers.fields,
|
||||
getattr(message, "path", None),
|
||||
)
|
||||
)
|
||||
# we need to pass the message off-band because it's not hashable
|
||||
self._get_content_view_message = message
|
||||
return self._get_content_view(viewmode, limit, flow_modify_cache_invalidation)
|
||||
|
||||
@lru_cache(maxsize=200)
|
||||
def _get_content_view(
|
||||
self, viewmode: str, max_lines: int, _
|
||||
) -> tuple[str, list[urwid.Text]]:
|
||||
message: http.Message = self._get_content_view_message
|
||||
self._get_content_view_message = None # type: ignore[assignment]
|
||||
|
||||
pretty = contentviews.prettify_message(message, self.flow, viewmode)
|
||||
cut_off = strutils.cut_after_n_lines(pretty.text, max_lines)
|
||||
|
||||
chunks = mitmproxy_rs.syntax_highlight.highlight(
|
||||
cut_off,
|
||||
language=pretty.syntax_highlight,
|
||||
)
|
||||
|
||||
text_objects = [urwid.Text(chunks)]
|
||||
if len(cut_off) < len(pretty.text):
|
||||
text_objects.append(
|
||||
urwid.Text(
|
||||
[
|
||||
(
|
||||
"highlight",
|
||||
"Stopped displaying data after %d lines. Press "
|
||||
% max_lines,
|
||||
),
|
||||
("key", "f"),
|
||||
("highlight", " to load all data."),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
return f"{pretty.view_name} {pretty.description}", text_objects
|
||||
|
||||
def conn_text(self, conn):
|
||||
if conn:
|
||||
hdrs = []
|
||||
for k, v in conn.headers.fields:
|
||||
# This will always force an ascii representation of headers. For example, if the server sends a
|
||||
#
|
||||
# X-Authors: Made with ❤ in Hamburg
|
||||
#
|
||||
# header, mitmproxy will display the following:
|
||||
#
|
||||
# X-Authors: Made with \xe2\x9d\xa4 in Hamburg.
|
||||
#
|
||||
# The alternative would be to just use the header's UTF-8 representation and maybe
|
||||
# do `str.replace("\t", "\\t")` to exempt tabs from urwid's special characters escaping [1].
|
||||
# That would in some terminals allow rendering UTF-8 characters, but the mapping
|
||||
# wouldn't be bijective, i.e. a user couldn't distinguish "\\t" and "\t".
|
||||
# Also, from a security perspective, a mitmproxy user couldn't be fooled by homoglyphs.
|
||||
#
|
||||
# 1) https://github.com/mitmproxy/mitmproxy/issues/1833
|
||||
# https://github.com/urwid/urwid/blob/6608ee2c9932d264abd1171468d833b7a4082e13/urwid/display_common.py#L35-L36,
|
||||
|
||||
k = strutils.bytes_to_escaped_str(k) + ":"
|
||||
v = strutils.bytes_to_escaped_str(v)
|
||||
hdrs.append((k, v))
|
||||
txt = common.format_keyvals(hdrs, key_format="header")
|
||||
viewmode = self.master.commands.call("console.flowview.mode")
|
||||
msg, body = self.content_view(viewmode, conn)
|
||||
|
||||
txt.append(self._contentview_status_bar(msg, viewmode))
|
||||
txt.extend(body)
|
||||
else:
|
||||
txt = [
|
||||
urwid.Text(""),
|
||||
urwid.Text(
|
||||
[
|
||||
("highlight", "No response. Press "),
|
||||
("key", "e"),
|
||||
("highlight", " and edit any aspect to add one."),
|
||||
]
|
||||
),
|
||||
]
|
||||
return searchable.Searchable(txt)
|
||||
|
||||
def dns_message_text(
|
||||
self, type: str, message: DNSMessage | None
|
||||
) -> searchable.Searchable:
|
||||
"""
|
||||
Alternative:
|
||||
if not message:
|
||||
return searchable.Searchable([urwid.Text(("highlight", f"No {typ}."))])
|
||||
|
||||
viewmode = self.master.commands.call("console.flowview.mode")
|
||||
pretty = contentviews.prettify_message(message, flow, viewmode)
|
||||
chunks = mitmproxy_rs.syntax_highlight.highlight(
|
||||
pretty.text,
|
||||
language=pretty.syntax_highlight,
|
||||
)
|
||||
|
||||
widget_lines = [
|
||||
self._contentview_status_bar(viewmode.capitalize(), viewmode),
|
||||
urwid.Text(chunks)
|
||||
]
|
||||
return searchable.Searchable(widget_lines)
|
||||
"""
|
||||
# Keep in sync with web/src/js/components/FlowView/DnsMessages.tsx
|
||||
if message:
|
||||
|
||||
def rr_text(rr: dns.ResourceRecord):
|
||||
return urwid.Text(
|
||||
f" {rr.name} {dns.types.to_str(rr.type)} {dns.classes.to_str(rr.class_)} {rr.ttl} {rr}"
|
||||
)
|
||||
|
||||
txt = []
|
||||
txt.append(
|
||||
urwid.Text(
|
||||
"{recursive}Question".format(
|
||||
recursive="Recursive " if message.recursion_desired else "",
|
||||
)
|
||||
)
|
||||
)
|
||||
txt.extend(
|
||||
urwid.Text(
|
||||
f" {q.name} {dns.types.to_str(q.type)} {dns.classes.to_str(q.class_)}"
|
||||
)
|
||||
for q in message.questions
|
||||
)
|
||||
txt.append(urwid.Text(""))
|
||||
txt.append(
|
||||
urwid.Text(
|
||||
"{authoritative}{recursive}Answer".format(
|
||||
authoritative="Authoritative "
|
||||
if message.authoritative_answer
|
||||
else "",
|
||||
recursive="Recursive " if message.recursion_available else "",
|
||||
)
|
||||
)
|
||||
)
|
||||
txt.extend(map(rr_text, message.answers))
|
||||
txt.append(urwid.Text(""))
|
||||
txt.append(urwid.Text("Authority"))
|
||||
txt.extend(map(rr_text, message.authorities))
|
||||
txt.append(urwid.Text(""))
|
||||
txt.append(urwid.Text("Addition"))
|
||||
txt.extend(map(rr_text, message.additionals))
|
||||
return searchable.Searchable(txt)
|
||||
else:
|
||||
return searchable.Searchable([urwid.Text(("highlight", f"No {type}."))])
|
||||
|
||||
|
||||
class FlowView(urwid.Frame, layoutwidget.LayoutWidget):
|
||||
keyctx = "flowview"
|
||||
title = "Flow Details"
|
||||
|
||||
def __init__(self, master):
|
||||
super().__init__(
|
||||
FlowDetails(master),
|
||||
header=FlowViewHeader(master),
|
||||
)
|
||||
self.master = master
|
||||
|
||||
def focus_changed(self, *args, **kwargs):
|
||||
self.body.focus_changed()
|
||||
self.header.focus_changed()
|
||||
@@ -0,0 +1,27 @@
|
||||
from . import base
|
||||
from .editors import CookieAttributeEditor
|
||||
from .editors import CookieEditor
|
||||
from .editors import DataViewer
|
||||
from .editors import OptionsEditor
|
||||
from .editors import PathEditor
|
||||
from .editors import QueryEditor
|
||||
from .editors import RequestHeaderEditor
|
||||
from .editors import RequestMultipartEditor
|
||||
from .editors import RequestUrlEncodedEditor
|
||||
from .editors import ResponseHeaderEditor
|
||||
from .editors import SetCookieEditor
|
||||
|
||||
__all__ = [
|
||||
"base",
|
||||
"QueryEditor",
|
||||
"RequestHeaderEditor",
|
||||
"ResponseHeaderEditor",
|
||||
"RequestMultipartEditor",
|
||||
"RequestUrlEncodedEditor",
|
||||
"PathEditor",
|
||||
"CookieEditor",
|
||||
"CookieAttributeEditor",
|
||||
"SetCookieEditor",
|
||||
"OptionsEditor",
|
||||
"DataViewer",
|
||||
]
|
||||
@@ -0,0 +1,469 @@
|
||||
import abc
|
||||
import copy
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Container
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import MutableSequence
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
from typing import ClassVar
|
||||
from typing import Literal
|
||||
from typing import overload
|
||||
|
||||
import urwid
|
||||
|
||||
import mitmproxy.tools.console.master
|
||||
from mitmproxy import exceptions
|
||||
from mitmproxy.tools.console import layoutwidget
|
||||
from mitmproxy.tools.console import signals
|
||||
from mitmproxy.utils import strutils
|
||||
|
||||
|
||||
@overload
|
||||
def read_file(filename: str, escaped: Literal[True]) -> bytes: ...
|
||||
|
||||
|
||||
@overload
|
||||
def read_file(filename: str, escaped: Literal[False]) -> str: ...
|
||||
|
||||
|
||||
def read_file(filename: str, escaped: bool) -> bytes | str:
|
||||
filename = os.path.expanduser(filename)
|
||||
try:
|
||||
with open(filename, "r" if escaped else "rb") as f:
|
||||
d = f.read()
|
||||
except OSError as v:
|
||||
raise exceptions.CommandError(v)
|
||||
if escaped:
|
||||
try:
|
||||
d = strutils.escaped_str_to_bytes(d)
|
||||
except ValueError:
|
||||
raise exceptions.CommandError("Invalid Python-style string encoding.")
|
||||
return d
|
||||
|
||||
|
||||
class Cell(urwid.WidgetWrap):
|
||||
def get_data(self):
|
||||
"""
|
||||
Raises:
|
||||
ValueError, if the current content is invalid.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
|
||||
class Column(metaclass=abc.ABCMeta):
|
||||
subeditor: urwid.Edit = None
|
||||
|
||||
def __init__(self, heading):
|
||||
self.heading = heading
|
||||
|
||||
@abc.abstractmethod
|
||||
def Display(self, data) -> Cell:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def Edit(self, data) -> Cell:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def blank(self) -> Any:
|
||||
pass
|
||||
|
||||
def keypress(self, key: str, editor: "GridEditor") -> str | None:
|
||||
return key
|
||||
|
||||
|
||||
class GridRow(urwid.WidgetWrap):
|
||||
def __init__(
|
||||
self,
|
||||
focused: int | None,
|
||||
editing: bool,
|
||||
editor: "GridEditor",
|
||||
values: tuple[Iterable[bytes], Container[int]],
|
||||
) -> None:
|
||||
self.focused = focused
|
||||
self.editor = editor
|
||||
self.edit_col: Cell | None = None
|
||||
|
||||
errors = values[1]
|
||||
self.fields: Sequence[Any] = []
|
||||
for i, v in enumerate(values[0]):
|
||||
if focused == i and editing:
|
||||
self.edit_col = self.editor.columns[i].Edit(v)
|
||||
self.fields.append(self.edit_col)
|
||||
else:
|
||||
w = self.editor.columns[i].Display(v)
|
||||
if focused == i:
|
||||
if i in errors:
|
||||
w = urwid.AttrMap(w, "focusfield_error")
|
||||
else:
|
||||
w = urwid.AttrMap(w, "focusfield")
|
||||
elif i in errors:
|
||||
w = urwid.AttrMap(w, "field_error")
|
||||
self.fields.append(w)
|
||||
|
||||
fspecs = self.fields[:]
|
||||
if len(self.fields) > 1:
|
||||
fspecs[0] = ("fixed", self.editor.first_width + 2, fspecs[0])
|
||||
w = urwid.Columns(fspecs, dividechars=2)
|
||||
if focused is not None:
|
||||
w.focus_position = focused
|
||||
super().__init__(w)
|
||||
|
||||
def keypress(self, s, k):
|
||||
if self.edit_col:
|
||||
w = self._w.column_widths(s)[self.focused]
|
||||
k = self.edit_col.keypress((w,), k)
|
||||
return k
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
|
||||
class GridWalker(urwid.ListWalker):
|
||||
"""
|
||||
Stores rows as a list of (rows, errors) tuples, where rows is a list
|
||||
and errors is a set with an entry of each offset in rows that is an
|
||||
error.
|
||||
"""
|
||||
|
||||
def __init__(self, lst: Iterable[list], editor: "GridEditor") -> None:
|
||||
self.lst: MutableSequence[tuple[Any, set]] = [(i, set()) for i in lst]
|
||||
self.editor = editor
|
||||
self.focus = 0
|
||||
self.focus_col = 0
|
||||
self.edit_row: GridRow | None = None
|
||||
|
||||
def _modified(self):
|
||||
self.editor.show_empty_msg()
|
||||
return super()._modified()
|
||||
|
||||
def add_value(self, lst):
|
||||
self.lst.append((lst[:], set()))
|
||||
self._modified()
|
||||
|
||||
def get_current_value(self):
|
||||
if self.lst:
|
||||
return self.lst[self.focus][0][self.focus_col]
|
||||
|
||||
def set_current_value(self, val) -> None:
|
||||
errors = self.lst[self.focus][1]
|
||||
emsg = self.editor.is_error(self.focus_col, val)
|
||||
if emsg:
|
||||
signals.status_message.send(message=emsg)
|
||||
errors.add(self.focus_col)
|
||||
else:
|
||||
errors.discard(self.focus_col)
|
||||
self.set_value(val, self.focus, self.focus_col, errors)
|
||||
|
||||
def set_value(self, val, focus, focus_col, errors=None):
|
||||
if not errors:
|
||||
errors = set()
|
||||
row = list(self.lst[focus][0])
|
||||
row[focus_col] = val
|
||||
self.lst[focus] = [tuple(row), errors] # type: ignore
|
||||
self._modified()
|
||||
|
||||
def delete_focus(self):
|
||||
if self.lst:
|
||||
del self.lst[self.focus]
|
||||
self.focus = min(len(self.lst) - 1, self.focus)
|
||||
self._modified()
|
||||
|
||||
def _insert(self, pos):
|
||||
self.focus = pos
|
||||
self.lst.insert(self.focus, ([c.blank() for c in self.editor.columns], set()))
|
||||
self.focus_col = 0
|
||||
self.start_edit()
|
||||
|
||||
def insert(self):
|
||||
return self._insert(self.focus)
|
||||
|
||||
def add(self):
|
||||
return self._insert(min(self.focus + 1, len(self.lst)))
|
||||
|
||||
def start_edit(self):
|
||||
col = self.editor.columns[self.focus_col]
|
||||
if self.lst and not col.subeditor:
|
||||
self.edit_row = GridRow(
|
||||
self.focus_col, True, self.editor, self.lst[self.focus]
|
||||
)
|
||||
self._modified()
|
||||
|
||||
def stop_edit(self):
|
||||
if self.edit_row and self.edit_row.edit_col:
|
||||
try:
|
||||
val = self.edit_row.edit_col.get_data()
|
||||
except ValueError:
|
||||
return
|
||||
self.edit_row = None
|
||||
self.set_current_value(val)
|
||||
|
||||
def left(self):
|
||||
self.focus_col = max(self.focus_col - 1, 0)
|
||||
self._modified()
|
||||
|
||||
def right(self):
|
||||
self.focus_col = min(self.focus_col + 1, len(self.editor.columns) - 1)
|
||||
self._modified()
|
||||
|
||||
def tab_next(self):
|
||||
self.stop_edit()
|
||||
if self.focus_col < len(self.editor.columns) - 1:
|
||||
self.focus_col += 1
|
||||
elif self.focus != len(self.lst) - 1:
|
||||
self.focus_col = 0
|
||||
self.focus += 1
|
||||
self._modified()
|
||||
|
||||
def get_focus(self):
|
||||
if self.edit_row:
|
||||
return self.edit_row, self.focus
|
||||
elif self.lst:
|
||||
return (
|
||||
GridRow(self.focus_col, False, self.editor, self.lst[self.focus]),
|
||||
self.focus,
|
||||
)
|
||||
else:
|
||||
return None, None
|
||||
|
||||
def set_focus(self, focus):
|
||||
self.stop_edit()
|
||||
self.focus = focus
|
||||
self._modified()
|
||||
|
||||
def get_next(self, pos):
|
||||
if pos + 1 >= len(self.lst):
|
||||
return None, None
|
||||
return GridRow(None, False, self.editor, self.lst[pos + 1]), pos + 1
|
||||
|
||||
def get_prev(self, pos):
|
||||
if pos - 1 < 0:
|
||||
return None, None
|
||||
return GridRow(None, False, self.editor, self.lst[pos - 1]), pos - 1
|
||||
|
||||
|
||||
class GridListBox(urwid.ListBox):
|
||||
def __init__(self, lw):
|
||||
super().__init__(lw)
|
||||
|
||||
|
||||
FIRST_WIDTH_MAX = 40
|
||||
|
||||
|
||||
class BaseGridEditor(urwid.WidgetWrap):
|
||||
title: str = ""
|
||||
keyctx: ClassVar[str] = "grideditor"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
master: "mitmproxy.tools.console.master.ConsoleMaster",
|
||||
title,
|
||||
columns,
|
||||
value: Any,
|
||||
callback: Callable[..., None],
|
||||
*cb_args,
|
||||
**cb_kwargs,
|
||||
) -> None:
|
||||
value = self.data_in(copy.deepcopy(value))
|
||||
self.master = master
|
||||
self.title = title
|
||||
self.columns = columns
|
||||
self.value = value
|
||||
self.callback = callback
|
||||
self.cb_args = cb_args
|
||||
self.cb_kwargs = cb_kwargs
|
||||
|
||||
first_width = 20
|
||||
if value:
|
||||
for r in value:
|
||||
assert len(r) == len(self.columns)
|
||||
first_width = max(len(r), first_width)
|
||||
self.first_width = min(first_width, FIRST_WIDTH_MAX)
|
||||
|
||||
h = None
|
||||
if any(col.heading for col in self.columns):
|
||||
headings = []
|
||||
for i, col in enumerate(self.columns):
|
||||
c = urwid.Text(col.heading)
|
||||
if i == 0 and len(self.columns) > 1:
|
||||
headings.append(("fixed", first_width + 2, c))
|
||||
else:
|
||||
headings.append(c)
|
||||
h = urwid.Columns(headings, dividechars=2)
|
||||
h = urwid.AttrMap(h, "heading")
|
||||
|
||||
self.walker = GridWalker(self.value, self)
|
||||
self.lb = GridListBox(self.walker)
|
||||
w = urwid.Frame(self.lb, header=h)
|
||||
|
||||
super().__init__(w)
|
||||
self.show_empty_msg()
|
||||
|
||||
def layout_popping(self):
|
||||
res = []
|
||||
for i in self.walker.lst:
|
||||
if not i[1] and any([x for x in i[0]]):
|
||||
res.append(i[0])
|
||||
self.callback(self.data_out(res), *self.cb_args, **self.cb_kwargs)
|
||||
|
||||
def show_empty_msg(self):
|
||||
if self.walker.lst:
|
||||
self._w.footer = None
|
||||
else:
|
||||
self._w.footer = urwid.Text(
|
||||
[
|
||||
("highlight", "No values - you should add some. Press "),
|
||||
("key", "?"),
|
||||
("highlight", " for help."),
|
||||
]
|
||||
)
|
||||
|
||||
def set_subeditor_value(self, val, focus, focus_col):
|
||||
self.walker.set_value(val, focus, focus_col)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if self.walker.edit_row:
|
||||
if key == "esc":
|
||||
self.walker.stop_edit()
|
||||
elif key == "tab":
|
||||
pf, pfc = self.walker.focus, self.walker.focus_col
|
||||
self.walker.tab_next()
|
||||
if self.walker.focus == pf and self.walker.focus_col != pfc:
|
||||
self.walker.start_edit()
|
||||
else:
|
||||
self._w.keypress(size, key)
|
||||
return None
|
||||
|
||||
column = self.columns[self.walker.focus_col]
|
||||
if key == "m_start":
|
||||
self.walker.set_focus(0)
|
||||
elif key == "m_next":
|
||||
self.walker.tab_next()
|
||||
elif key == "m_end":
|
||||
self.walker.set_focus(len(self.walker.lst) - 1)
|
||||
elif key == "left":
|
||||
self.walker.left()
|
||||
elif key == "right":
|
||||
self.walker.right()
|
||||
elif column.keypress(key, self) and not self.handle_key(key):
|
||||
return self._w.keypress(size, key)
|
||||
|
||||
def data_out(self, data: Sequence[list]) -> Any:
|
||||
"""
|
||||
Called on raw list data, before data is returned through the
|
||||
callback.
|
||||
"""
|
||||
return data
|
||||
|
||||
def data_in(self, data: Any) -> Iterable[list]:
|
||||
"""
|
||||
Called to prepare provided data.
|
||||
"""
|
||||
return data
|
||||
|
||||
def is_error(self, col: int, val: Any) -> str | None:
|
||||
"""
|
||||
Return None, or a string error message.
|
||||
"""
|
||||
return None
|
||||
|
||||
def handle_key(self, key):
|
||||
if key == "?":
|
||||
signals.pop_view_state.send()
|
||||
return False
|
||||
|
||||
def cmd_add(self):
|
||||
self.walker.add()
|
||||
|
||||
def cmd_insert(self):
|
||||
self.walker.insert()
|
||||
|
||||
def cmd_delete(self):
|
||||
self.walker.delete_focus()
|
||||
|
||||
def cmd_read_file(self, path):
|
||||
self.walker.set_current_value(read_file(path, False))
|
||||
|
||||
def cmd_read_file_escaped(self, path):
|
||||
self.walker.set_current_value(read_file(path, True))
|
||||
|
||||
def cmd_spawn_editor(self):
|
||||
o = self.walker.get_current_value()
|
||||
if o is not None:
|
||||
n = self.master.spawn_editor(o)
|
||||
n = strutils.clean_hanging_newline(n)
|
||||
self.walker.set_current_value(n)
|
||||
|
||||
|
||||
class GridEditor(BaseGridEditor):
|
||||
title = ""
|
||||
columns: Sequence[Column] = ()
|
||||
keyctx: ClassVar[str] = "grideditor"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
master: "mitmproxy.tools.console.master.ConsoleMaster",
|
||||
value: Any,
|
||||
callback: Callable[..., None],
|
||||
*cb_args,
|
||||
**cb_kwargs,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
master, self.title, self.columns, value, callback, *cb_args, **cb_kwargs
|
||||
)
|
||||
|
||||
|
||||
class FocusEditor(urwid.WidgetWrap, layoutwidget.LayoutWidget):
|
||||
"""
|
||||
A specialised GridEditor that edits the current focused flow.
|
||||
"""
|
||||
|
||||
keyctx: ClassVar[str] = "grideditor"
|
||||
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
|
||||
def call(self, v, name, *args, **kwargs):
|
||||
f = getattr(v, name, None)
|
||||
if f:
|
||||
f(*args, **kwargs)
|
||||
|
||||
def get_data(self, flow):
|
||||
"""
|
||||
Retrieve the data to edit from the current flow.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_data(self, vals, flow):
|
||||
"""
|
||||
Set the current data on the flow.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_data_update(self, vals, flow) -> None:
|
||||
self.set_data(vals, flow)
|
||||
signals.flow_change.send(flow=flow)
|
||||
|
||||
def key_responder(self):
|
||||
return self._w
|
||||
|
||||
def layout_popping(self):
|
||||
self.call(self._w, "layout_popping")
|
||||
|
||||
def layout_pushed(self, prev):
|
||||
if self.master.view.focus.flow:
|
||||
self._w = BaseGridEditor(
|
||||
self.master,
|
||||
self.title,
|
||||
self.columns,
|
||||
self.get_data(self.master.view.focus.flow),
|
||||
self.set_data_update,
|
||||
self.master.view.focus.flow,
|
||||
)
|
||||
else:
|
||||
self._w = urwid.Pile([])
|
||||
@@ -0,0 +1,49 @@
|
||||
import urwid
|
||||
|
||||
from mitmproxy.tools.console import signals
|
||||
from mitmproxy.tools.console.grideditor import base
|
||||
from mitmproxy.utils import strutils
|
||||
|
||||
|
||||
class Column(base.Column):
|
||||
def Display(self, data):
|
||||
return Display(data)
|
||||
|
||||
def Edit(self, data):
|
||||
return Edit(data)
|
||||
|
||||
def blank(self):
|
||||
return b""
|
||||
|
||||
def keypress(self, key, editor):
|
||||
if key in ["m_select"]:
|
||||
editor.walker.start_edit()
|
||||
else:
|
||||
return key
|
||||
|
||||
|
||||
class Display(base.Cell):
|
||||
def __init__(self, data: bytes) -> None:
|
||||
self.data = data
|
||||
escaped = strutils.bytes_to_escaped_str(data)
|
||||
w = urwid.Text(escaped, wrap="any")
|
||||
super().__init__(w)
|
||||
|
||||
def get_data(self) -> bytes:
|
||||
return self.data
|
||||
|
||||
|
||||
class Edit(base.Cell):
|
||||
def __init__(self, data: bytes) -> None:
|
||||
d = strutils.bytes_to_escaped_str(data)
|
||||
w = urwid.Edit(edit_text=d, wrap="any", multiline=True)
|
||||
w = urwid.AttrMap(w, "editfield")
|
||||
super().__init__(w)
|
||||
|
||||
def get_data(self) -> bytes:
|
||||
txt = self._w.base_widget.get_text()[0].strip()
|
||||
try:
|
||||
return strutils.escaped_str_to_bytes(txt)
|
||||
except ValueError:
|
||||
signals.status_message.send(message="Invalid data.")
|
||||
raise
|
||||
@@ -0,0 +1,40 @@
|
||||
import urwid
|
||||
|
||||
from mitmproxy.net.http import cookies
|
||||
from mitmproxy.tools.console import signals
|
||||
from mitmproxy.tools.console.grideditor import base
|
||||
|
||||
|
||||
class Column(base.Column):
|
||||
def __init__(self, heading, subeditor):
|
||||
super().__init__(heading)
|
||||
self.subeditor = subeditor
|
||||
|
||||
def Edit(self, data):
|
||||
raise RuntimeError("SubgridColumn should handle edits itself")
|
||||
|
||||
def Display(self, data):
|
||||
return Display(data)
|
||||
|
||||
def blank(self):
|
||||
return []
|
||||
|
||||
def keypress(self, key: str, editor):
|
||||
if key in "rRe":
|
||||
signals.status_message.send(message="Press enter to edit this field.")
|
||||
return
|
||||
elif key == "m_select":
|
||||
self.subeditor.grideditor = editor
|
||||
editor.master.switch_view("edit_focus_setcookie_attrs")
|
||||
else:
|
||||
return key
|
||||
|
||||
|
||||
class Display(base.Cell):
|
||||
def __init__(self, data):
|
||||
p = cookies._format_pairs(data, sep="\n")
|
||||
w = urwid.Text(p)
|
||||
super().__init__(w)
|
||||
|
||||
def get_data(self):
|
||||
pass
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Welcome to the encoding dance!
|
||||
|
||||
In a nutshell, text columns are actually a proxy class for byte columns,
|
||||
which just encode/decodes contents.
|
||||
"""
|
||||
|
||||
from mitmproxy.tools.console import signals
|
||||
from mitmproxy.tools.console.grideditor import col_bytes
|
||||
|
||||
|
||||
class Column(col_bytes.Column):
|
||||
def __init__(self, heading, encoding="utf8", errors="surrogateescape"):
|
||||
super().__init__(heading)
|
||||
self.encoding_args = encoding, errors
|
||||
|
||||
def Display(self, data):
|
||||
return TDisplay(data, self.encoding_args)
|
||||
|
||||
def Edit(self, data):
|
||||
return TEdit(data, self.encoding_args)
|
||||
|
||||
def blank(self):
|
||||
return ""
|
||||
|
||||
|
||||
# This is the same for both edit and display.
|
||||
class EncodingMixin:
|
||||
def __init__(self, data, encoding_args):
|
||||
self.encoding_args = encoding_args
|
||||
super().__init__(str(data).encode(*self.encoding_args)) # type: ignore
|
||||
|
||||
def get_data(self):
|
||||
data = super().get_data() # type: ignore
|
||||
try:
|
||||
return data.decode(*self.encoding_args)
|
||||
except ValueError:
|
||||
signals.status_message.send(message="Invalid encoding.")
|
||||
raise
|
||||
|
||||
|
||||
# urwid forces a different name for a subclass.
|
||||
class TDisplay(EncodingMixin, col_bytes.Display):
|
||||
pass
|
||||
|
||||
|
||||
class TEdit(EncodingMixin, col_bytes.Edit):
|
||||
pass
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
A display-only column that displays any data type.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import urwid
|
||||
|
||||
from mitmproxy.tools.console.grideditor import base
|
||||
from mitmproxy.utils import strutils
|
||||
|
||||
|
||||
class Column(base.Column):
|
||||
def Display(self, data):
|
||||
return Display(data)
|
||||
|
||||
Edit = Display
|
||||
|
||||
def blank(self):
|
||||
return ""
|
||||
|
||||
|
||||
class Display(base.Cell):
|
||||
def __init__(self, data: Any) -> None:
|
||||
self.data = data
|
||||
if isinstance(data, bytes):
|
||||
data = strutils.bytes_to_escaped_str(data)
|
||||
if not isinstance(data, str):
|
||||
data = repr(data)
|
||||
w = urwid.Text(data, wrap="any")
|
||||
super().__init__(w)
|
||||
|
||||
def get_data(self) -> Any:
|
||||
return self.data
|
||||
@@ -0,0 +1,209 @@
|
||||
from typing import Any
|
||||
|
||||
import urwid
|
||||
|
||||
from mitmproxy import exceptions
|
||||
from mitmproxy.http import Headers
|
||||
from mitmproxy.tools.console import layoutwidget
|
||||
from mitmproxy.tools.console import signals
|
||||
from mitmproxy.tools.console.grideditor import base
|
||||
from mitmproxy.tools.console.grideditor import col_bytes
|
||||
from mitmproxy.tools.console.grideditor import col_subgrid
|
||||
from mitmproxy.tools.console.grideditor import col_text
|
||||
from mitmproxy.tools.console.grideditor import col_viewany
|
||||
|
||||
|
||||
class QueryEditor(base.FocusEditor):
|
||||
title = "Edit Query"
|
||||
columns = [col_text.Column("Key"), col_text.Column("Value")]
|
||||
|
||||
def get_data(self, flow):
|
||||
return flow.request.query.items(multi=True)
|
||||
|
||||
def set_data(self, vals, flow):
|
||||
flow.request.query = vals
|
||||
|
||||
|
||||
class HeaderEditor(base.FocusEditor):
|
||||
columns = [col_bytes.Column("Key"), col_bytes.Column("Value")]
|
||||
|
||||
|
||||
class RequestHeaderEditor(HeaderEditor):
|
||||
title = "Edit Request Headers"
|
||||
|
||||
def get_data(self, flow):
|
||||
return flow.request.headers.fields
|
||||
|
||||
def set_data(self, vals, flow):
|
||||
flow.request.headers = Headers(vals)
|
||||
|
||||
|
||||
class ResponseHeaderEditor(HeaderEditor):
|
||||
title = "Edit Response Headers"
|
||||
|
||||
def get_data(self, flow):
|
||||
return flow.response.headers.fields
|
||||
|
||||
def set_data(self, vals, flow):
|
||||
flow.response.headers = Headers(vals)
|
||||
|
||||
|
||||
class RequestMultipartEditor(base.FocusEditor):
|
||||
title = "Edit Multipart Form"
|
||||
columns = [col_bytes.Column("Key"), col_bytes.Column("Value")]
|
||||
|
||||
def get_data(self, flow):
|
||||
return flow.request.multipart_form.items(multi=True)
|
||||
|
||||
def set_data(self, vals, flow):
|
||||
flow.request.multipart_form = vals
|
||||
|
||||
|
||||
class RequestUrlEncodedEditor(base.FocusEditor):
|
||||
title = "Edit UrlEncoded Form"
|
||||
columns = [col_text.Column("Key"), col_text.Column("Value")]
|
||||
|
||||
def get_data(self, flow):
|
||||
return flow.request.urlencoded_form.items(multi=True)
|
||||
|
||||
def set_data(self, vals, flow):
|
||||
flow.request.urlencoded_form = vals
|
||||
|
||||
|
||||
class PathEditor(base.FocusEditor):
|
||||
# TODO: Next row on enter?
|
||||
title = "Edit Path Components"
|
||||
columns = [
|
||||
col_text.Column("Component"),
|
||||
]
|
||||
|
||||
def data_in(self, data):
|
||||
return [[i] for i in data]
|
||||
|
||||
def data_out(self, data):
|
||||
return [i[0] for i in data]
|
||||
|
||||
def get_data(self, flow):
|
||||
return self.data_in(flow.request.path_components)
|
||||
|
||||
def set_data(self, vals, flow):
|
||||
flow.request.path_components = self.data_out(vals)
|
||||
|
||||
|
||||
class CookieEditor(base.FocusEditor):
|
||||
title = "Edit Cookies"
|
||||
columns = [
|
||||
col_text.Column("Name"),
|
||||
col_text.Column("Value"),
|
||||
]
|
||||
|
||||
def get_data(self, flow):
|
||||
return flow.request.cookies.items(multi=True)
|
||||
|
||||
def set_data(self, vals, flow):
|
||||
flow.request.cookies = vals
|
||||
|
||||
|
||||
class CookieAttributeEditor(base.FocusEditor):
|
||||
title = "Editing Set-Cookie attributes"
|
||||
columns = [
|
||||
col_text.Column("Name"),
|
||||
col_text.Column("Value"),
|
||||
]
|
||||
grideditor: base.BaseGridEditor
|
||||
|
||||
def data_in(self, data):
|
||||
return [(k, v or "") for k, v in data]
|
||||
|
||||
def data_out(self, data):
|
||||
ret = []
|
||||
for i in data:
|
||||
if not i[1]:
|
||||
ret.append([i[0], None])
|
||||
else:
|
||||
ret.append(i)
|
||||
return ret
|
||||
|
||||
def layout_pushed(self, prev):
|
||||
if self.grideditor.master.view.focus.flow:
|
||||
self._w = base.BaseGridEditor(
|
||||
self.grideditor.master,
|
||||
self.title,
|
||||
self.columns,
|
||||
self.grideditor.walker.get_current_value(),
|
||||
self.grideditor.set_subeditor_value,
|
||||
self.grideditor.walker.focus,
|
||||
self.grideditor.walker.focus_col,
|
||||
)
|
||||
else:
|
||||
self._w = urwid.Pile([])
|
||||
|
||||
|
||||
class SetCookieEditor(base.FocusEditor):
|
||||
title = "Edit SetCookie Header"
|
||||
columns = [
|
||||
col_text.Column("Name"),
|
||||
col_text.Column("Value"),
|
||||
col_subgrid.Column("Attributes", CookieAttributeEditor),
|
||||
]
|
||||
|
||||
def data_in(self, data):
|
||||
flattened = []
|
||||
for key, (value, attrs) in data:
|
||||
flattened.append([key, value, attrs.items(multi=True)])
|
||||
return flattened
|
||||
|
||||
def data_out(self, data):
|
||||
vals = []
|
||||
for key, value, attrs in data:
|
||||
vals.append([key, (value, attrs)])
|
||||
return vals
|
||||
|
||||
def get_data(self, flow):
|
||||
return self.data_in(flow.response.cookies.items(multi=True))
|
||||
|
||||
def set_data(self, vals, flow):
|
||||
flow.response.cookies = self.data_out(vals)
|
||||
|
||||
|
||||
class OptionsEditor(base.GridEditor, layoutwidget.LayoutWidget):
|
||||
title = ""
|
||||
columns = [col_text.Column("")]
|
||||
|
||||
def __init__(self, master, name, vals):
|
||||
self.name = name
|
||||
super().__init__(master, [[i] for i in vals], self.callback)
|
||||
|
||||
def callback(self, vals) -> None:
|
||||
try:
|
||||
setattr(self.master.options, self.name, [i[0] for i in vals])
|
||||
except exceptions.OptionsError as v:
|
||||
signals.status_message.send(message=str(v))
|
||||
|
||||
def is_error(self, col, val):
|
||||
pass
|
||||
|
||||
|
||||
class DataViewer(base.GridEditor, layoutwidget.LayoutWidget):
|
||||
title = ""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
master,
|
||||
vals: (list[list[Any]] | list[Any] | Any),
|
||||
) -> None:
|
||||
if vals is not None:
|
||||
# Whatever vals is, make it a list of rows containing lists of column values.
|
||||
if not isinstance(vals, list):
|
||||
vals = [vals]
|
||||
if not isinstance(vals[0], list):
|
||||
vals = [[i] for i in vals]
|
||||
|
||||
self.columns = [col_viewany.Column("")] * len(vals[0])
|
||||
super().__init__(master, vals, self.callback)
|
||||
|
||||
def callback(self, vals):
|
||||
pass
|
||||
|
||||
def is_error(self, col, val):
|
||||
pass
|
||||
113
venv/Lib/site-packages/mitmproxy/tools/console/help.py
Normal file
113
venv/Lib/site-packages/mitmproxy/tools/console/help.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import urwid
|
||||
|
||||
from mitmproxy import flowfilter
|
||||
from mitmproxy.tools.console import common
|
||||
from mitmproxy.tools.console import layoutwidget
|
||||
from mitmproxy.tools.console import tabs
|
||||
|
||||
|
||||
class CListBox(urwid.ListBox):
|
||||
def __init__(self, contents):
|
||||
self.length = len(contents)
|
||||
contents = contents[:] + [urwid.Text(["\n"])] * 5
|
||||
super().__init__(contents)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "m_end":
|
||||
self.set_focus(self.length - 1)
|
||||
elif key == "m_start":
|
||||
self.set_focus(0)
|
||||
else:
|
||||
return super().keypress(size, key)
|
||||
|
||||
|
||||
class HelpView(tabs.Tabs, layoutwidget.LayoutWidget):
|
||||
title = "Help"
|
||||
keyctx = "help"
|
||||
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
self.helpctx = ""
|
||||
super().__init__(
|
||||
[
|
||||
[self.keybindings_title, self.keybindings],
|
||||
[self.filtexp_title, self.filtexp],
|
||||
]
|
||||
)
|
||||
|
||||
def keybindings_title(self):
|
||||
return "Key Bindings"
|
||||
|
||||
def format_keys(self, binds):
|
||||
kvs = []
|
||||
for b in binds:
|
||||
k = b.key
|
||||
if b.key == " ":
|
||||
k = "space"
|
||||
kvs.append((k, b.help or b.command))
|
||||
return common.format_keyvals(kvs)
|
||||
|
||||
def keybindings(self):
|
||||
text = [urwid.Text([("title", "Common Keybindings")])]
|
||||
|
||||
text.extend(self.format_keys(self.master.keymap.list("commonkey")))
|
||||
|
||||
text.append(urwid.Text(["\n", ("title", "Keybindings for this view")]))
|
||||
if self.helpctx:
|
||||
text.extend(self.format_keys(self.master.keymap.list(self.helpctx)))
|
||||
|
||||
text.append(
|
||||
urwid.Text(
|
||||
[
|
||||
"\n",
|
||||
("title", "Global Keybindings"),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
text.extend(self.format_keys(self.master.keymap.list("global")))
|
||||
|
||||
return CListBox(text)
|
||||
|
||||
def filtexp_title(self):
|
||||
return "Filter Expressions"
|
||||
|
||||
def filtexp(self):
|
||||
text = []
|
||||
text.extend(common.format_keyvals(flowfilter.help, indent=4))
|
||||
text.append(
|
||||
urwid.Text(
|
||||
[
|
||||
"\n",
|
||||
("text", " Regexes are Python-style.\n"),
|
||||
("text", " Regexes can be specified as quoted strings.\n"),
|
||||
(
|
||||
"text",
|
||||
' Header matching (~h, ~hq, ~hs) is against a string of the form "name: value".\n',
|
||||
),
|
||||
(
|
||||
"text",
|
||||
" Expressions with no operators are regex matches against URL.\n",
|
||||
),
|
||||
("text", " Default binary operator is &.\n"),
|
||||
("head", "\n Examples:\n"),
|
||||
]
|
||||
)
|
||||
)
|
||||
examples = [
|
||||
(r"google\.com", r"Url containing \"google.com"),
|
||||
("~q ~b test", r"Requests where body contains \"test\""),
|
||||
(
|
||||
r"!(~q & ~t \"text/html\")",
|
||||
"Anything but requests with a text/html content type.",
|
||||
),
|
||||
]
|
||||
text.extend(common.format_keyvals(examples, indent=4))
|
||||
return CListBox(text)
|
||||
|
||||
def layout_pushed(self, prev):
|
||||
"""
|
||||
We are just about to push a window onto the stack.
|
||||
"""
|
||||
self.helpctx = prev.keyctx
|
||||
self.show()
|
||||
163
venv/Lib/site-packages/mitmproxy/tools/console/keybindings.py
Normal file
163
venv/Lib/site-packages/mitmproxy/tools/console/keybindings.py
Normal file
@@ -0,0 +1,163 @@
|
||||
import textwrap
|
||||
|
||||
import urwid
|
||||
|
||||
from mitmproxy.tools.console import layoutwidget
|
||||
from mitmproxy.tools.console import signals
|
||||
from mitmproxy.utils import signals as utils_signals
|
||||
|
||||
HELP_HEIGHT = 5
|
||||
|
||||
|
||||
class KeyItem(urwid.WidgetWrap):
|
||||
def __init__(self, walker, binding, focused):
|
||||
self.walker, self.binding, self.focused = walker, binding, focused
|
||||
super().__init__(self.get_widget())
|
||||
|
||||
def get_widget(self):
|
||||
cmd = textwrap.dedent(self.binding.command).strip()
|
||||
parts = [
|
||||
(4, urwid.Text([("focus", ">> " if self.focused else " ")])),
|
||||
(10, urwid.Text([("title", self.binding.key)])),
|
||||
(12, urwid.Text([("highlight", "\n".join(self.binding.contexts))])),
|
||||
urwid.Text([("text", cmd)]),
|
||||
]
|
||||
return urwid.Columns(parts)
|
||||
|
||||
def get_edit_text(self):
|
||||
return self._w[1].get_edit_text()
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def keypress(self, size, key):
|
||||
return key
|
||||
|
||||
|
||||
class KeyListWalker(urwid.ListWalker):
|
||||
def __init__(self, master, keybinding_focus_change):
|
||||
self.keybinding_focus_change = keybinding_focus_change
|
||||
self.master = master
|
||||
|
||||
self.index = 0
|
||||
self.focusobj = None
|
||||
self.bindings = list(master.keymap.list("all"))
|
||||
self.set_focus(0)
|
||||
signals.keybindings_change.connect(self.sig_modified)
|
||||
|
||||
def sig_modified(self):
|
||||
self.bindings = list(self.master.keymap.list("all"))
|
||||
self.set_focus(min(self.index, len(self.bindings) - 1))
|
||||
self._modified()
|
||||
|
||||
def get_edit_text(self):
|
||||
return self.focus_obj.get_edit_text()
|
||||
|
||||
def _get(self, pos):
|
||||
binding = self.bindings[pos]
|
||||
return KeyItem(self, binding, pos == self.index)
|
||||
|
||||
def get_focus(self):
|
||||
return self.focus_obj, self.index
|
||||
|
||||
def set_focus(self, index):
|
||||
binding = self.bindings[index]
|
||||
self.index = index
|
||||
self.focus_obj = self._get(self.index)
|
||||
self.keybinding_focus_change.send(binding.help or "")
|
||||
self._modified()
|
||||
|
||||
def get_next(self, pos):
|
||||
if pos >= len(self.bindings) - 1:
|
||||
return None, None
|
||||
pos = pos + 1
|
||||
return self._get(pos), pos
|
||||
|
||||
def get_prev(self, pos):
|
||||
pos = pos - 1
|
||||
if pos < 0:
|
||||
return None, None
|
||||
return self._get(pos), pos
|
||||
|
||||
def positions(self, reverse=False):
|
||||
if reverse:
|
||||
return reversed(range(len(self.bindings)))
|
||||
else:
|
||||
return range(len(self.bindings))
|
||||
|
||||
|
||||
class KeyList(urwid.ListBox):
|
||||
def __init__(self, master, keybinding_focus_change):
|
||||
self.master = master
|
||||
self.walker = KeyListWalker(master, keybinding_focus_change)
|
||||
super().__init__(self.walker)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "m_select":
|
||||
foc, idx = self.get_focus()
|
||||
# Act here
|
||||
elif key == "m_start":
|
||||
self.set_focus(0)
|
||||
self.walker._modified()
|
||||
elif key == "m_end":
|
||||
self.set_focus(len(self.walker.bindings) - 1)
|
||||
self.walker._modified()
|
||||
return super().keypress(size, key)
|
||||
|
||||
|
||||
class KeyHelp(urwid.Frame):
|
||||
def __init__(self, master, keybinding_focus_change):
|
||||
self.master = master
|
||||
super().__init__(self.widget(""))
|
||||
self.set_active(False)
|
||||
keybinding_focus_change.connect(self.sig_mod)
|
||||
|
||||
def set_active(self, val):
|
||||
h = urwid.Text("Key Binding Help")
|
||||
style = "heading" if val else "heading_inactive"
|
||||
self.header = urwid.AttrMap(h, style)
|
||||
|
||||
def widget(self, txt):
|
||||
cols, _ = self.master.ui.get_cols_rows()
|
||||
return urwid.ListBox([urwid.Text(i) for i in textwrap.wrap(txt, cols)])
|
||||
|
||||
def sig_mod(self, txt):
|
||||
self.body = self.widget(txt)
|
||||
|
||||
|
||||
class KeyBindings(urwid.Pile, layoutwidget.LayoutWidget):
|
||||
title = "Key Bindings"
|
||||
keyctx = "keybindings"
|
||||
focus_position: int
|
||||
|
||||
def __init__(self, master):
|
||||
keybinding_focus_change = utils_signals.SyncSignal(lambda text: None)
|
||||
|
||||
oh = KeyHelp(master, keybinding_focus_change)
|
||||
super().__init__(
|
||||
[
|
||||
KeyList(master, keybinding_focus_change),
|
||||
(HELP_HEIGHT, oh),
|
||||
]
|
||||
)
|
||||
self.master = master
|
||||
|
||||
def get_focused_binding(self):
|
||||
if self.focus_position != 0:
|
||||
return None
|
||||
f = self.contents[0][0]
|
||||
return f.walker.get_focus()[0].binding
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "m_next":
|
||||
self.focus_position = (self.focus_position + 1) % len(self.widget_list)
|
||||
self.contents[1][0].set_active(self.focus_position == 1)
|
||||
key = None
|
||||
|
||||
# This is essentially a copypasta from urwid.Pile's keypress handler.
|
||||
# So much for "closed for modification, but open for extension".
|
||||
item_rows = None
|
||||
if len(size) == 2:
|
||||
item_rows = self.get_item_rows(size, focus=True)
|
||||
tsize = self.get_item_size(size, self.focus_position, True, item_rows)
|
||||
return self.focus.keypress(tsize, key)
|
||||
262
venv/Lib/site-packages/mitmproxy/tools/console/keymap.py
Normal file
262
venv/Lib/site-packages/mitmproxy/tools/console/keymap.py
Normal file
@@ -0,0 +1,262 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from collections.abc import Sequence
|
||||
from functools import cache
|
||||
|
||||
import ruamel.yaml.error
|
||||
|
||||
import mitmproxy.types
|
||||
from mitmproxy import command
|
||||
from mitmproxy import ctx
|
||||
from mitmproxy import exceptions
|
||||
from mitmproxy.tools.console import commandexecutor
|
||||
from mitmproxy.tools.console import signals
|
||||
|
||||
|
||||
class KeyBindingError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
Contexts = {
|
||||
"chooser",
|
||||
"commands",
|
||||
"commonkey",
|
||||
"dataviewer",
|
||||
"eventlog",
|
||||
"flowlist",
|
||||
"flowview",
|
||||
"global",
|
||||
"grideditor",
|
||||
"help",
|
||||
"keybindings",
|
||||
"options",
|
||||
}
|
||||
|
||||
|
||||
navkeys = [
|
||||
"m_start",
|
||||
"m_end",
|
||||
"m_next",
|
||||
"m_select",
|
||||
"up",
|
||||
"down",
|
||||
"page_up",
|
||||
"page_down",
|
||||
"left",
|
||||
"right",
|
||||
]
|
||||
|
||||
|
||||
class Binding:
|
||||
def __init__(self, key, command, contexts, help):
|
||||
self.key, self.command, self.contexts = key, command, sorted(contexts)
|
||||
self.help = help
|
||||
|
||||
def keyspec(self):
|
||||
"""
|
||||
Translate the key spec from a convenient user specification to one
|
||||
Urwid understands.
|
||||
"""
|
||||
return self.key.replace("space", " ")
|
||||
|
||||
def key_short(self) -> str:
|
||||
return (
|
||||
self.key.replace("enter", "⏎").replace("right", "→").replace("space", "␣")
|
||||
)
|
||||
|
||||
def sortkey(self):
|
||||
return self.key + ",".join(self.contexts)
|
||||
|
||||
|
||||
class Keymap:
|
||||
def __init__(self, master):
|
||||
self.executor = commandexecutor.CommandExecutor(master)
|
||||
self.keys: dict[str, dict[str, Binding]] = defaultdict(dict)
|
||||
self.bindings = []
|
||||
|
||||
def _check_contexts(self, contexts):
|
||||
if not contexts:
|
||||
raise ValueError("Must specify at least one context.")
|
||||
for c in contexts:
|
||||
if c not in Contexts:
|
||||
raise ValueError("Unsupported context: %s" % c)
|
||||
|
||||
def _on_change(self) -> None:
|
||||
signals.keybindings_change.send()
|
||||
self.binding_for_help.cache_clear()
|
||||
|
||||
def add(self, key: str, command: str, contexts: Sequence[str], help="") -> None:
|
||||
"""
|
||||
Add a key to the key map.
|
||||
"""
|
||||
self._check_contexts(contexts)
|
||||
|
||||
for b in self.bindings:
|
||||
if b.key == key and b.command.strip() == command.strip():
|
||||
b.contexts = sorted(list(set(b.contexts + contexts)))
|
||||
if help:
|
||||
b.help = help
|
||||
self.bind(b)
|
||||
break
|
||||
else:
|
||||
self.remove(key, contexts)
|
||||
b = Binding(key=key, command=command, contexts=contexts, help=help)
|
||||
self.bindings.append(b)
|
||||
self.bind(b)
|
||||
self._on_change()
|
||||
|
||||
def remove(self, key: str, contexts: Sequence[str]) -> None:
|
||||
"""
|
||||
Remove a key from the key map.
|
||||
"""
|
||||
self._check_contexts(contexts)
|
||||
for c in contexts:
|
||||
b = self.get(c, key)
|
||||
if b:
|
||||
self.unbind(b)
|
||||
b.contexts = [x for x in b.contexts if x != c]
|
||||
if b.contexts:
|
||||
self.bindings.append(b)
|
||||
self.bind(b)
|
||||
self._on_change()
|
||||
|
||||
def bind(self, binding: Binding) -> None:
|
||||
for c in binding.contexts:
|
||||
self.keys[c][binding.keyspec()] = binding
|
||||
|
||||
def unbind(self, binding: Binding) -> None:
|
||||
"""
|
||||
Unbind also removes the binding from the list.
|
||||
"""
|
||||
for c in binding.contexts:
|
||||
del self.keys[c][binding.keyspec()]
|
||||
self.bindings = [b for b in self.bindings if b != binding]
|
||||
self._on_change()
|
||||
|
||||
def get(self, context: str, key: str) -> Binding | None:
|
||||
if context in self.keys:
|
||||
return self.keys[context].get(key, None)
|
||||
return None
|
||||
|
||||
@cache
|
||||
def binding_for_help(self, help: str) -> Binding | None:
|
||||
for b in self.bindings:
|
||||
if b.help == help:
|
||||
return b
|
||||
return None
|
||||
|
||||
def list(self, context: str) -> Sequence[Binding]:
|
||||
b = [x for x in self.bindings if context in x.contexts or context == "all"]
|
||||
single = [x for x in b if len(x.key.split()) == 1]
|
||||
multi = [x for x in b if len(x.key.split()) != 1]
|
||||
single.sort(key=lambda x: x.sortkey())
|
||||
multi.sort(key=lambda x: x.sortkey())
|
||||
return single + multi
|
||||
|
||||
def handle(self, context: str, key: str) -> str | None:
|
||||
"""
|
||||
Returns the key if it has not been handled, or None.
|
||||
"""
|
||||
b = self.get(context, key) or self.get("global", key)
|
||||
if b:
|
||||
self.executor(b.command)
|
||||
return None
|
||||
return key
|
||||
|
||||
def handle_only(self, context: str, key: str) -> str | None:
|
||||
"""
|
||||
Like handle, but ignores global bindings. Returns the key if it has
|
||||
not been handled, or None.
|
||||
"""
|
||||
b = self.get(context, key)
|
||||
if b:
|
||||
self.executor(b.command)
|
||||
return None
|
||||
return key
|
||||
|
||||
|
||||
keyAttrs = {
|
||||
"key": lambda x: isinstance(x, str),
|
||||
"cmd": lambda x: isinstance(x, str),
|
||||
"ctx": lambda x: isinstance(x, list) and [isinstance(v, str) for v in x],
|
||||
"help": lambda x: isinstance(x, str),
|
||||
}
|
||||
requiredKeyAttrs = {"key", "cmd"}
|
||||
|
||||
|
||||
class KeymapConfig:
|
||||
defaultFile = "keys.yaml"
|
||||
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
|
||||
@command.command("console.keymap.load")
|
||||
def keymap_load_path(self, path: mitmproxy.types.Path) -> None:
|
||||
try:
|
||||
self.load_path(self.master.keymap, path) # type: ignore
|
||||
except (OSError, KeyBindingError) as e:
|
||||
raise exceptions.CommandError("Could not load key bindings - %s" % e) from e
|
||||
|
||||
def running(self):
|
||||
p = os.path.join(os.path.expanduser(ctx.options.confdir), self.defaultFile)
|
||||
if os.path.exists(p):
|
||||
try:
|
||||
self.load_path(self.master.keymap, p)
|
||||
except KeyBindingError as e:
|
||||
logging.error(e)
|
||||
|
||||
def load_path(self, km, p):
|
||||
if os.path.exists(p) and os.path.isfile(p):
|
||||
with open(p, encoding="utf8") as f:
|
||||
try:
|
||||
txt = f.read()
|
||||
except UnicodeDecodeError as e:
|
||||
raise KeyBindingError(f"Encoding error - expected UTF8: {p}: {e}")
|
||||
try:
|
||||
vals = self.parse(txt)
|
||||
except KeyBindingError as e:
|
||||
raise KeyBindingError(f"Error reading {p}: {e}") from e
|
||||
for v in vals:
|
||||
user_ctxs = v.get("ctx", ["global"])
|
||||
try:
|
||||
km._check_contexts(user_ctxs)
|
||||
km.remove(v["key"], user_ctxs)
|
||||
km.add(
|
||||
key=v["key"],
|
||||
command=v["cmd"],
|
||||
contexts=user_ctxs,
|
||||
help=v.get("help", None),
|
||||
)
|
||||
except ValueError as e:
|
||||
raise KeyBindingError(f"Error reading {p}: {e}") from e
|
||||
|
||||
def parse(self, text):
|
||||
try:
|
||||
data = ruamel.yaml.YAML(typ="safe", pure=True).load(text)
|
||||
except ruamel.yaml.error.MarkedYAMLError as v:
|
||||
if hasattr(v, "problem_mark"):
|
||||
snip = v.problem_mark.get_snippet()
|
||||
raise KeyBindingError(
|
||||
"Key binding config error at line %s:\n%s\n%s"
|
||||
% (v.problem_mark.line + 1, snip, v.problem)
|
||||
)
|
||||
else:
|
||||
raise KeyBindingError("Could not parse key bindings.")
|
||||
if not data:
|
||||
return []
|
||||
if not isinstance(data, list):
|
||||
raise KeyBindingError("Invalid keybinding config - expected a list of keys")
|
||||
|
||||
for k in data:
|
||||
unknown = k.keys() - keyAttrs.keys()
|
||||
if unknown:
|
||||
raise KeyBindingError("Unknown key attributes: %s" % unknown)
|
||||
missing = requiredKeyAttrs - k.keys()
|
||||
if missing:
|
||||
raise KeyBindingError("Missing required key attributes: %s" % unknown)
|
||||
for attr in k.keys():
|
||||
if not keyAttrs[attr](k[attr]):
|
||||
raise KeyBindingError("Invalid type for %s" % attr)
|
||||
|
||||
return data
|
||||
@@ -0,0 +1,40 @@
|
||||
from typing import ClassVar
|
||||
|
||||
|
||||
class LayoutWidget:
|
||||
"""
|
||||
All top-level layout widgets and all widgets that may be set in an
|
||||
overlay must comply with this API.
|
||||
"""
|
||||
|
||||
# Title is only required for windows, not overlay components
|
||||
title = ""
|
||||
keyctx: ClassVar[str] = ""
|
||||
|
||||
def key_responder(self):
|
||||
"""
|
||||
Returns the object responding to key input. Usually self, but may be
|
||||
a wrapped object.
|
||||
"""
|
||||
return self
|
||||
|
||||
def focus_changed(self):
|
||||
"""
|
||||
The view focus has changed. Layout objects should implement the API
|
||||
rather than directly subscribing to events.
|
||||
"""
|
||||
|
||||
def view_changed(self):
|
||||
"""
|
||||
The view list has changed.
|
||||
"""
|
||||
|
||||
def layout_popping(self):
|
||||
"""
|
||||
We are just about to pop a window off the stack, or exit an overlay.
|
||||
"""
|
||||
|
||||
def layout_pushed(self, prev):
|
||||
"""
|
||||
We have just pushed a window onto the stack.
|
||||
"""
|
||||
263
venv/Lib/site-packages/mitmproxy/tools/console/master.py
Normal file
263
venv/Lib/site-packages/mitmproxy/tools/console/master.py
Normal file
@@ -0,0 +1,263 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import mimetypes
|
||||
import os.path
|
||||
import shlex
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
from typing import TypeVar
|
||||
|
||||
import urwid
|
||||
from tornado.platform.asyncio import AddThreadSelectorEventLoop
|
||||
|
||||
from mitmproxy import addons
|
||||
from mitmproxy import log
|
||||
from mitmproxy import master
|
||||
from mitmproxy import options
|
||||
from mitmproxy.addons import errorcheck
|
||||
from mitmproxy.addons import eventstore
|
||||
from mitmproxy.addons import intercept
|
||||
from mitmproxy.addons import readfile
|
||||
from mitmproxy.addons import view
|
||||
from mitmproxy.tools.console import consoleaddons
|
||||
from mitmproxy.tools.console import defaultkeys
|
||||
from mitmproxy.tools.console import keymap
|
||||
from mitmproxy.tools.console import palettes
|
||||
from mitmproxy.tools.console import signals
|
||||
from mitmproxy.tools.console import window
|
||||
from mitmproxy.utils import strutils
|
||||
|
||||
T = TypeVar("T", str, bytes)
|
||||
|
||||
|
||||
class ConsoleMaster(master.Master):
|
||||
def __init__(self, opts: options.Options) -> None:
|
||||
super().__init__(opts)
|
||||
|
||||
self.view: view.View = view.View()
|
||||
self.events = eventstore.EventStore()
|
||||
self.events.sig_add.connect(self.sig_add_log)
|
||||
|
||||
self.stream_path = None
|
||||
self.keymap = keymap.Keymap(self)
|
||||
defaultkeys.map(self.keymap)
|
||||
self.options.errored.connect(self.options_error)
|
||||
|
||||
self.addons.add(*addons.default_addons())
|
||||
self.addons.add(
|
||||
intercept.Intercept(),
|
||||
self.view,
|
||||
self.events,
|
||||
readfile.ReadFile(),
|
||||
consoleaddons.ConsoleAddon(self),
|
||||
keymap.KeymapConfig(self),
|
||||
errorcheck.ErrorCheck(repeat_errors_on_stderr=True),
|
||||
)
|
||||
|
||||
self.window: window.Window | None = None
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
super().__setattr__(name, value)
|
||||
signals.update_settings.send()
|
||||
|
||||
def options_error(self, exc) -> None:
|
||||
signals.status_message.send(message=str(exc), expire=1)
|
||||
|
||||
def prompt_for_user_choice(self, prompt, callback) -> None:
|
||||
signals.status_prompt_onekey.send(
|
||||
prompt=prompt,
|
||||
keys=[
|
||||
("yes", "y"),
|
||||
("no", "n"),
|
||||
],
|
||||
callback=callback,
|
||||
)
|
||||
|
||||
def prompt_for_exit(self) -> None:
|
||||
self.prompt_for_user_choice("Quit", self.quit)
|
||||
|
||||
def sig_add_log(self, entry: log.LogEntry):
|
||||
if log.log_tier(self.options.console_eventlog_verbosity) < log.log_tier(
|
||||
entry.level
|
||||
):
|
||||
return
|
||||
if entry.level in ("error", "warn", "alert"):
|
||||
signals.status_message.send(
|
||||
message=(
|
||||
entry.level,
|
||||
f"{entry.level.title()}: {entry.msg.lstrip()}",
|
||||
),
|
||||
expire=5,
|
||||
)
|
||||
|
||||
def sig_call_in(self, seconds, callback):
|
||||
def cb(*_):
|
||||
return callback()
|
||||
|
||||
self.loop.set_alarm_in(seconds, cb)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def uistopped(self):
|
||||
self.loop.stop()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.loop.start()
|
||||
self.loop.screen_size = None
|
||||
self.loop.draw_screen()
|
||||
|
||||
def get_editor(self) -> str:
|
||||
# based upon https://github.com/pallets/click/blob/main/src/click/_termui_impl.py
|
||||
if m := os.environ.get("MITMPROXY_EDITOR"):
|
||||
return m
|
||||
if m := os.environ.get("EDITOR"):
|
||||
return m
|
||||
for editor in "sensible-editor", "nano", "vim":
|
||||
if shutil.which(editor):
|
||||
return editor
|
||||
if os.name == "nt":
|
||||
return "notepad"
|
||||
else:
|
||||
return "vi"
|
||||
|
||||
def get_hex_editor(self) -> str:
|
||||
editors = ["ghex", "hexedit", "hxd", "hexer", "hexcurse"]
|
||||
for editor in editors:
|
||||
if shutil.which(editor):
|
||||
return editor
|
||||
return self.get_editor()
|
||||
|
||||
def spawn_editor(self, data: T) -> T:
|
||||
text = isinstance(data, str)
|
||||
fd, name = tempfile.mkstemp("", "mitmproxy", text=text)
|
||||
with_hexeditor = isinstance(data, bytes) and strutils.is_mostly_bin(data)
|
||||
with open(fd, "w" if text else "wb") as f:
|
||||
f.write(data)
|
||||
if with_hexeditor:
|
||||
c = self.get_hex_editor()
|
||||
else:
|
||||
c = self.get_editor()
|
||||
cmd = shlex.split(c)
|
||||
cmd.append(name)
|
||||
with self.uistopped():
|
||||
try:
|
||||
subprocess.call(cmd)
|
||||
except Exception:
|
||||
signals.status_message.send(message="Can't start editor: %s" % c)
|
||||
else:
|
||||
with open(name, "r" if text else "rb") as f:
|
||||
data = f.read()
|
||||
os.unlink(name)
|
||||
return data
|
||||
|
||||
def spawn_external_viewer(self, data, contenttype):
|
||||
if contenttype:
|
||||
contenttype = contenttype.split(";")[0]
|
||||
ext = mimetypes.guess_extension(contenttype) or ""
|
||||
else:
|
||||
ext = ""
|
||||
fd, name = tempfile.mkstemp(ext, "mproxy")
|
||||
os.write(fd, data)
|
||||
os.close(fd)
|
||||
|
||||
# read-only to remind the user that this is a view function
|
||||
os.chmod(name, stat.S_IREAD)
|
||||
|
||||
# hm which one should get priority?
|
||||
c = (
|
||||
os.environ.get("MITMPROXY_EDITOR")
|
||||
or os.environ.get("PAGER")
|
||||
or os.environ.get("EDITOR")
|
||||
)
|
||||
if not c:
|
||||
c = "less"
|
||||
cmd = shlex.split(c)
|
||||
cmd.append(name)
|
||||
|
||||
with self.uistopped():
|
||||
try:
|
||||
subprocess.call(cmd, shell=False)
|
||||
except Exception:
|
||||
signals.status_message.send(
|
||||
message="Can't start external viewer: %s" % " ".join(c)
|
||||
)
|
||||
# add a small delay before deletion so that the file is not removed before being loaded by the viewer
|
||||
t = threading.Timer(1.0, os.unlink, args=[name])
|
||||
t.start()
|
||||
|
||||
def set_palette(self, *_) -> None:
|
||||
self.ui.register_palette(
|
||||
palettes.palettes[self.options.console_palette].palette(
|
||||
self.options.console_palette_transparent
|
||||
)
|
||||
)
|
||||
self.ui.clear()
|
||||
|
||||
def inject_key(self, key):
|
||||
self.loop.process_input([key])
|
||||
|
||||
async def running(self) -> None:
|
||||
if not sys.stdout.isatty():
|
||||
print(
|
||||
"Error: mitmproxy's console interface requires a tty. "
|
||||
"Please run mitmproxy in an interactive shell environment.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
detected_encoding = urwid.detected_encoding.lower()
|
||||
if os.name != "nt" and detected_encoding and "utf" not in detected_encoding:
|
||||
print(
|
||||
f"mitmproxy expects a UTF-8 console environment, not {urwid.detected_encoding!r}. "
|
||||
f"Set your LANG environment variable to something like en_US.UTF-8.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
# Experimental (04/2022): We just don't exit here and see if/how that affects users.
|
||||
# sys.exit(1)
|
||||
urwid.set_encoding("utf8")
|
||||
|
||||
signals.call_in.connect(self.sig_call_in)
|
||||
self.ui = window.Screen()
|
||||
self.ui.set_terminal_properties(256)
|
||||
self.set_palette(None)
|
||||
self.options.subscribe(
|
||||
self.set_palette, ["console_palette", "console_palette_transparent"]
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
if isinstance(loop, getattr(asyncio, "ProactorEventLoop", tuple())):
|
||||
# fix for https://bugs.python.org/issue37373
|
||||
loop = AddThreadSelectorEventLoop(loop) # type: ignore
|
||||
self.loop = urwid.MainLoop(
|
||||
urwid.SolidFill("x"),
|
||||
event_loop=urwid.AsyncioEventLoop(loop=loop),
|
||||
screen=self.ui,
|
||||
handle_mouse=self.options.console_mouse,
|
||||
)
|
||||
self.window = window.Window(self)
|
||||
self.loop.widget = self.window
|
||||
self.window.refresh()
|
||||
|
||||
self.loop.start()
|
||||
|
||||
await super().running()
|
||||
|
||||
async def done(self):
|
||||
self.loop.stop()
|
||||
await super().done()
|
||||
|
||||
def overlay(self, widget, **kwargs):
|
||||
assert self.window
|
||||
self.window.set_overlay(widget, **kwargs)
|
||||
|
||||
def switch_view(self, name):
|
||||
assert self.window
|
||||
self.window.push(name)
|
||||
|
||||
def quit(self, a):
|
||||
if a != "n":
|
||||
self.shutdown()
|
||||
277
venv/Lib/site-packages/mitmproxy/tools/console/options.py
Normal file
277
venv/Lib/site-packages/mitmproxy/tools/console/options.py
Normal file
@@ -0,0 +1,277 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pprint
|
||||
import textwrap
|
||||
import typing
|
||||
from collections.abc import Sequence
|
||||
from typing import Optional
|
||||
|
||||
import urwid
|
||||
|
||||
from mitmproxy import exceptions
|
||||
from mitmproxy import optmanager
|
||||
from mitmproxy.tools.console import layoutwidget
|
||||
from mitmproxy.tools.console import overlay
|
||||
from mitmproxy.tools.console import signals
|
||||
|
||||
HELP_HEIGHT = 5
|
||||
|
||||
|
||||
def can_edit_inplace(opt):
|
||||
if opt.choices:
|
||||
return False
|
||||
if opt.typespec in [str, int, Optional[str], Optional[int]]:
|
||||
return True
|
||||
|
||||
|
||||
def fcol(s, width, attr):
|
||||
s = str(s)
|
||||
return ("fixed", width, urwid.Text((attr, s)))
|
||||
|
||||
|
||||
class OptionItem(urwid.WidgetWrap):
|
||||
def __init__(self, walker, opt, focused, namewidth, editing):
|
||||
self.walker, self.opt, self.focused = walker, opt, focused
|
||||
self.namewidth = namewidth
|
||||
self.editing = editing
|
||||
super().__init__(self.get_widget())
|
||||
|
||||
def get_widget(self):
|
||||
val = self.opt.current()
|
||||
if self.opt.typespec is bool:
|
||||
displayval = "true" if val else "false"
|
||||
elif not val:
|
||||
displayval = ""
|
||||
elif self.opt.typespec == Sequence[str]:
|
||||
displayval = pprint.pformat(val, indent=1)
|
||||
else:
|
||||
displayval = str(val)
|
||||
|
||||
changed = self.walker.master.options.has_changed(self.opt.name)
|
||||
if self.focused:
|
||||
valstyle = "option_active_selected" if changed else "option_selected"
|
||||
else:
|
||||
valstyle = "option_active" if changed else "text"
|
||||
|
||||
if self.editing:
|
||||
valw = urwid.Edit(edit_text=displayval)
|
||||
else:
|
||||
valw = urwid.AttrMap(
|
||||
urwid.Padding(urwid.Text([(valstyle, displayval)])), valstyle
|
||||
)
|
||||
|
||||
return urwid.Columns(
|
||||
[
|
||||
(
|
||||
self.namewidth,
|
||||
urwid.Text([("title", self.opt.name.ljust(self.namewidth))]),
|
||||
),
|
||||
valw,
|
||||
],
|
||||
dividechars=2,
|
||||
focus_column=1,
|
||||
)
|
||||
|
||||
def get_edit_text(self):
|
||||
return self._w[1].get_edit_text()
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def keypress(self, size, key):
|
||||
if self.editing:
|
||||
self._w[1].keypress(size, key)
|
||||
return
|
||||
return key
|
||||
|
||||
|
||||
class OptionListWalker(urwid.ListWalker):
|
||||
def __init__(self, master, help_widget: OptionHelp):
|
||||
self.master = master
|
||||
self.help_widget = help_widget
|
||||
|
||||
self.index = 0
|
||||
self.focusobj = None
|
||||
|
||||
self.opts = sorted(master.options.keys())
|
||||
self.maxlen = max(len(i) for i in self.opts)
|
||||
self.editing = False
|
||||
self.set_focus(0)
|
||||
self.master.options.changed.connect(self.sig_mod)
|
||||
|
||||
def sig_mod(self, *args, **kwargs):
|
||||
self.opts = sorted(self.master.options.keys())
|
||||
self.maxlen = max(len(i) for i in self.opts)
|
||||
self._modified()
|
||||
self.set_focus(self.index)
|
||||
|
||||
def start_editing(self):
|
||||
self.editing = True
|
||||
self.focus_obj = self._get(self.index, True)
|
||||
self._modified()
|
||||
|
||||
def stop_editing(self):
|
||||
self.editing = False
|
||||
self.focus_obj = self._get(self.index, False)
|
||||
self.set_focus(self.index)
|
||||
self._modified()
|
||||
|
||||
def get_edit_text(self):
|
||||
return self.focus_obj.get_edit_text()
|
||||
|
||||
def _get(self, pos, editing):
|
||||
name = self.opts[pos]
|
||||
opt = self.master.options._options[name]
|
||||
return OptionItem(self, opt, pos == self.index, self.maxlen, editing)
|
||||
|
||||
def get_focus(self):
|
||||
return self.focus_obj, self.index
|
||||
|
||||
def set_focus(self, index):
|
||||
self.editing = False
|
||||
name = self.opts[index]
|
||||
opt = self.master.options._options[name]
|
||||
self.index = index
|
||||
self.focus_obj = self._get(self.index, self.editing)
|
||||
self.help_widget.update_help_text(opt.help)
|
||||
self._modified()
|
||||
|
||||
def get_next(self, pos):
|
||||
if pos >= len(self.opts) - 1:
|
||||
return None, None
|
||||
pos = pos + 1
|
||||
return self._get(pos, False), pos
|
||||
|
||||
def get_prev(self, pos):
|
||||
pos = pos - 1
|
||||
if pos < 0:
|
||||
return None, None
|
||||
return self._get(pos, False), pos
|
||||
|
||||
def positions(self, reverse=False):
|
||||
if reverse:
|
||||
return reversed(range(len(self.opts)))
|
||||
else:
|
||||
return range(len(self.opts))
|
||||
|
||||
|
||||
class OptionsList(urwid.ListBox):
|
||||
def __init__(self, master, help_widget: OptionHelp):
|
||||
self.master = master
|
||||
self.walker = OptionListWalker(master, help_widget)
|
||||
super().__init__(self.walker)
|
||||
|
||||
def save_config(self, path):
|
||||
try:
|
||||
optmanager.save(self.master.options, path)
|
||||
except exceptions.OptionsError as e:
|
||||
signals.status_message.send(message=str(e))
|
||||
|
||||
def keypress(self, size, key):
|
||||
if self.walker.editing:
|
||||
if key == "enter":
|
||||
foc, idx = self.get_focus()
|
||||
v = self.walker.get_edit_text()
|
||||
try:
|
||||
self.master.options.set(f"{foc.opt.name}={v}")
|
||||
except exceptions.OptionsError as v:
|
||||
signals.status_message.send(message=str(v))
|
||||
self.walker.stop_editing()
|
||||
return None
|
||||
elif key == "esc":
|
||||
self.walker.stop_editing()
|
||||
return None
|
||||
else:
|
||||
if key == "m_start":
|
||||
self.set_focus(0)
|
||||
self.walker._modified()
|
||||
elif key == "m_end":
|
||||
self.set_focus(len(self.walker.opts) - 1)
|
||||
self.walker._modified()
|
||||
elif key == "m_select":
|
||||
foc, idx = self.get_focus()
|
||||
if foc.opt.typespec is bool:
|
||||
self.master.options.toggler(foc.opt.name)()
|
||||
# Bust the focus widget cache
|
||||
self.set_focus(self.walker.index)
|
||||
elif can_edit_inplace(foc.opt):
|
||||
self.walker.start_editing()
|
||||
self.walker._modified()
|
||||
elif foc.opt.choices:
|
||||
self.master.overlay(
|
||||
overlay.Chooser(
|
||||
self.master,
|
||||
foc.opt.name,
|
||||
foc.opt.choices,
|
||||
foc.opt.current(),
|
||||
self.master.options.setter(foc.opt.name),
|
||||
)
|
||||
)
|
||||
elif foc.opt.typespec in (Sequence[str], typing.Sequence[str]):
|
||||
self.master.overlay(
|
||||
overlay.OptionsOverlay(
|
||||
self.master,
|
||||
foc.opt.name,
|
||||
foc.opt.current(),
|
||||
HELP_HEIGHT + 5,
|
||||
),
|
||||
valign="top",
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
return super().keypress(size, key)
|
||||
|
||||
|
||||
class OptionHelp(urwid.Frame):
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
super().__init__(self.widget(""))
|
||||
self.set_active(False)
|
||||
|
||||
def set_active(self, val):
|
||||
h = urwid.Text("Option Help")
|
||||
style = "heading" if val else "heading_inactive"
|
||||
self.header = urwid.AttrMap(h, style)
|
||||
|
||||
def widget(self, txt):
|
||||
cols, _ = self.master.ui.get_cols_rows()
|
||||
return urwid.ListBox([urwid.Text(i) for i in textwrap.wrap(txt, cols)])
|
||||
|
||||
def update_help_text(self, txt: str) -> None:
|
||||
self.body = self.widget(txt)
|
||||
|
||||
|
||||
class Options(urwid.Pile, layoutwidget.LayoutWidget):
|
||||
title = "Options"
|
||||
keyctx = "options"
|
||||
|
||||
focus_position: int
|
||||
|
||||
def __init__(self, master):
|
||||
oh = OptionHelp(master)
|
||||
self.optionslist = OptionsList(master, oh)
|
||||
super().__init__(
|
||||
[
|
||||
self.optionslist,
|
||||
(HELP_HEIGHT, oh),
|
||||
]
|
||||
)
|
||||
self.master = master
|
||||
|
||||
def current_name(self):
|
||||
foc, idx = self.optionslist.get_focus()
|
||||
return foc.opt.name
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "m_next":
|
||||
self.focus_position = (self.focus_position + 1) % len(self.widget_list)
|
||||
self.widget_list[1].set_active(self.focus_position == 1)
|
||||
key = None
|
||||
|
||||
# This is essentially a copypasta from urwid.Pile's keypress handler.
|
||||
# So much for "closed for modification, but open for extension".
|
||||
item_rows = None
|
||||
if len(size) == 2:
|
||||
item_rows = self.get_item_rows(size, focus=True)
|
||||
tsize = self.get_item_size(size, self.focus_position, True, item_rows)
|
||||
return self.focus.keypress(tsize, key)
|
||||
195
venv/Lib/site-packages/mitmproxy/tools/console/overlay.py
Normal file
195
venv/Lib/site-packages/mitmproxy/tools/console/overlay.py
Normal file
@@ -0,0 +1,195 @@
|
||||
import math
|
||||
|
||||
import urwid
|
||||
|
||||
from mitmproxy.tools.console import grideditor
|
||||
from mitmproxy.tools.console import keymap
|
||||
from mitmproxy.tools.console import layoutwidget
|
||||
from mitmproxy.tools.console import signals
|
||||
|
||||
|
||||
class SimpleOverlay(urwid.Overlay, layoutwidget.LayoutWidget):
|
||||
def __init__(self, master, widget, parent, width, valign="middle"):
|
||||
self.widget = widget
|
||||
self.master = master
|
||||
super().__init__(
|
||||
widget, parent, align="center", width=width, valign=valign, height="pack"
|
||||
)
|
||||
|
||||
@property
|
||||
def keyctx(self):
|
||||
return getattr(self.widget, "keyctx")
|
||||
|
||||
# mypy: Cannot override writeable attribute with read-only property
|
||||
@keyctx.setter
|
||||
def keyctx(self, value):
|
||||
raise RuntimeError # pragma: no cover
|
||||
|
||||
def key_responder(self):
|
||||
return self.widget.key_responder()
|
||||
|
||||
def focus_changed(self):
|
||||
return self.widget.focus_changed()
|
||||
|
||||
def view_changed(self):
|
||||
return self.widget.view_changed()
|
||||
|
||||
def layout_popping(self):
|
||||
return self.widget.layout_popping()
|
||||
|
||||
|
||||
class Choice(urwid.WidgetWrap):
|
||||
def __init__(self, txt, focus, current, shortcut):
|
||||
if shortcut:
|
||||
selection_type = "option_selected_key" if focus else "key"
|
||||
txt = [(selection_type, shortcut), ") ", txt]
|
||||
else:
|
||||
txt = " " + txt
|
||||
if current:
|
||||
s = "option_active_selected" if focus else "option_active"
|
||||
else:
|
||||
s = "option_selected" if focus else "text"
|
||||
super().__init__(
|
||||
urwid.AttrMap(
|
||||
urwid.Padding(urwid.Text(txt)),
|
||||
s,
|
||||
)
|
||||
)
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def keypress(self, size, key):
|
||||
return key
|
||||
|
||||
|
||||
class ChooserListWalker(urwid.ListWalker):
|
||||
shortcuts = "123456789abcdefghijklmnoprstuvwxyz"
|
||||
|
||||
def __init__(self, choices, current):
|
||||
self.index = 0
|
||||
self.choices = choices
|
||||
self.current = current
|
||||
|
||||
def _get(self, idx, focus):
|
||||
c = self.choices[idx]
|
||||
return Choice(c, focus, c == self.current, self.shortcuts[idx : idx + 1])
|
||||
|
||||
def set_focus(self, index):
|
||||
self.index = index
|
||||
|
||||
def get_focus(self):
|
||||
return self._get(self.index, True), self.index
|
||||
|
||||
def get_next(self, pos):
|
||||
if pos >= len(self.choices) - 1:
|
||||
return None, None
|
||||
pos = pos + 1
|
||||
return self._get(pos, False), pos
|
||||
|
||||
def get_prev(self, pos):
|
||||
pos = pos - 1
|
||||
if pos < 0:
|
||||
return None, None
|
||||
return self._get(pos, False), pos
|
||||
|
||||
def choice_by_shortcut(self, shortcut):
|
||||
for i, choice in enumerate(self.choices):
|
||||
if shortcut == self.shortcuts[i : i + 1]:
|
||||
return choice
|
||||
return None
|
||||
|
||||
|
||||
class Chooser(urwid.WidgetWrap, layoutwidget.LayoutWidget):
|
||||
keyctx = "chooser"
|
||||
|
||||
def __init__(self, master, title, choices, current, callback):
|
||||
self.master = master
|
||||
self.choices = choices
|
||||
self.callback = callback
|
||||
choicewidth = max(len(i) for i in choices)
|
||||
self.width = max(choicewidth, len(title)) + 7
|
||||
|
||||
self.walker = ChooserListWalker(choices, current)
|
||||
super().__init__(
|
||||
urwid.AttrMap(
|
||||
urwid.LineBox(
|
||||
urwid.BoxAdapter(urwid.ListBox(self.walker), len(choices)),
|
||||
title=title,
|
||||
),
|
||||
"background",
|
||||
)
|
||||
)
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def keypress(self, size, key):
|
||||
key = self.master.keymap.handle_only("chooser", key)
|
||||
choice = self.walker.choice_by_shortcut(key)
|
||||
if choice:
|
||||
self.callback(choice)
|
||||
signals.pop_view_state.send()
|
||||
return
|
||||
if key == "m_select":
|
||||
self.callback(self.choices[self.walker.index])
|
||||
signals.pop_view_state.send()
|
||||
return
|
||||
elif key in ["q", "esc"]:
|
||||
signals.pop_view_state.send()
|
||||
return
|
||||
|
||||
binding = self.master.keymap.get("global", key)
|
||||
# This is extremely awkward. We need a better way to match nav keys only.
|
||||
if binding and binding.command.startswith("console.nav"):
|
||||
self.master.keymap.handle("global", key)
|
||||
elif key in keymap.navkeys:
|
||||
return super().keypress(size, key)
|
||||
|
||||
|
||||
class OptionsOverlay(urwid.WidgetWrap, layoutwidget.LayoutWidget):
|
||||
keyctx = "grideditor"
|
||||
|
||||
def __init__(self, master, name, vals, vspace):
|
||||
"""
|
||||
vspace: how much vertical space to keep clear
|
||||
"""
|
||||
cols, rows = master.ui.get_cols_rows()
|
||||
self.ge = grideditor.OptionsEditor(master, name, vals)
|
||||
super().__init__(
|
||||
urwid.AttrMap(
|
||||
urwid.LineBox(urwid.BoxAdapter(self.ge, rows - vspace), title=name),
|
||||
"background",
|
||||
)
|
||||
)
|
||||
self.width = math.ceil(cols * 0.8)
|
||||
|
||||
def key_responder(self):
|
||||
return self.ge.key_responder()
|
||||
|
||||
def layout_popping(self):
|
||||
return self.ge.layout_popping()
|
||||
|
||||
|
||||
class DataViewerOverlay(urwid.WidgetWrap, layoutwidget.LayoutWidget):
|
||||
keyctx = "dataviewer"
|
||||
|
||||
def __init__(self, master, vals):
|
||||
"""
|
||||
vspace: how much vertical space to keep clear
|
||||
"""
|
||||
cols, rows = master.ui.get_cols_rows()
|
||||
self.ge = grideditor.DataViewer(master, vals)
|
||||
super().__init__(
|
||||
urwid.AttrMap(
|
||||
urwid.LineBox(urwid.BoxAdapter(self.ge, rows - 5), title="Data viewer"),
|
||||
"background",
|
||||
)
|
||||
)
|
||||
self.width = math.ceil(cols * 0.8)
|
||||
|
||||
def key_responder(self):
|
||||
return self.ge.key_responder()
|
||||
|
||||
def layout_popping(self):
|
||||
return self.ge.layout_popping()
|
||||
546
venv/Lib/site-packages/mitmproxy/tools/console/palettes.py
Normal file
546
venv/Lib/site-packages/mitmproxy/tools/console/palettes.py
Normal file
@@ -0,0 +1,546 @@
|
||||
# Low-color themes should ONLY use the standard foreground and background
|
||||
# colours listed here:
|
||||
#
|
||||
# http://urwid.org/manual/displayattributes.html
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
class Palette:
|
||||
_fields = [
|
||||
"background",
|
||||
"title",
|
||||
# Status bar & heading
|
||||
"heading",
|
||||
"heading_key",
|
||||
"heading_inactive",
|
||||
# Help
|
||||
"key",
|
||||
"head",
|
||||
"text",
|
||||
# Options
|
||||
"option_selected",
|
||||
"option_active",
|
||||
"option_active_selected",
|
||||
"option_selected_key",
|
||||
# List and Connections
|
||||
"method_get",
|
||||
"method_post",
|
||||
"method_delete",
|
||||
"method_other",
|
||||
"method_head",
|
||||
"method_put",
|
||||
"method_http2_push",
|
||||
"scheme_http",
|
||||
"scheme_https",
|
||||
"scheme_ws",
|
||||
"scheme_wss",
|
||||
"scheme_tcp",
|
||||
"scheme_udp",
|
||||
"scheme_dns",
|
||||
"scheme_quic",
|
||||
"scheme_other",
|
||||
"url_punctuation",
|
||||
"url_domain",
|
||||
"url_filename",
|
||||
"url_extension",
|
||||
"url_query_key",
|
||||
"url_query_value",
|
||||
"content_none",
|
||||
"content_text",
|
||||
"content_script",
|
||||
"content_media",
|
||||
"content_data",
|
||||
"content_raw",
|
||||
"content_other",
|
||||
"focus",
|
||||
"code_200",
|
||||
"code_300",
|
||||
"code_400",
|
||||
"code_500",
|
||||
"code_other",
|
||||
"error",
|
||||
"warn",
|
||||
"alert",
|
||||
"header",
|
||||
"highlight",
|
||||
"intercept",
|
||||
"replay",
|
||||
"mark",
|
||||
# Contentview Syntax Highlighting
|
||||
"name",
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"comment",
|
||||
"error",
|
||||
# TCP flow details
|
||||
"from_client",
|
||||
"to_client",
|
||||
# Grid Editor
|
||||
"focusfield",
|
||||
"focusfield_error",
|
||||
"field_error",
|
||||
"editfield",
|
||||
# Commander
|
||||
"commander_command",
|
||||
"commander_invalid",
|
||||
"commander_hint",
|
||||
]
|
||||
_fields.extend(["gradient_%02d" % i for i in range(100)])
|
||||
high: Mapping[str, Sequence[str]] | None = None
|
||||
low: Mapping[str, Sequence[str]]
|
||||
|
||||
def palette(self, transparent: bool):
|
||||
lst: list[Sequence[str | None]] = []
|
||||
highback, lowback = None, None
|
||||
if not transparent:
|
||||
if self.high and self.high.get("background"):
|
||||
highback = self.high["background"][1]
|
||||
lowback = self.low["background"][1]
|
||||
|
||||
for i in self._fields:
|
||||
if transparent and i == "background":
|
||||
lst.append(["background", "default", "default"])
|
||||
else:
|
||||
v: list[str | None] = [i]
|
||||
low = list(self.low[i])
|
||||
if lowback and low[1] == "default":
|
||||
low[1] = lowback
|
||||
v.extend(low)
|
||||
if self.high and i in self.high:
|
||||
v.append(None)
|
||||
high: list[str | None] = list(self.high[i])
|
||||
if highback and high[1] == "default":
|
||||
high[1] = highback
|
||||
v.extend(high)
|
||||
elif highback and self.low[i][1] == "default":
|
||||
high = [None, low[0], highback]
|
||||
v.extend(high)
|
||||
lst.append(tuple(v))
|
||||
return lst
|
||||
|
||||
|
||||
def gen_gradient(palette, cols):
|
||||
for i in range(100):
|
||||
palette["gradient_%02d" % i] = (cols[i * len(cols) // 100], "default")
|
||||
|
||||
|
||||
def gen_rgb_gradient(palette, cols):
|
||||
parts = len(cols) - 1
|
||||
for i in range(100):
|
||||
p = i / 100
|
||||
idx = int(p * parts)
|
||||
t0 = cols[idx]
|
||||
t1 = cols[idx + 1]
|
||||
pp = p * parts % 1
|
||||
t = (
|
||||
round(t0[0] + (t1[0] - t0[0]) * pp),
|
||||
round(t0[1] + (t1[1] - t0[1]) * pp),
|
||||
round(t0[2] + (t1[2] - t0[2]) * pp),
|
||||
)
|
||||
palette["gradient_%02d" % i] = ("#%x%x%x" % t, "default")
|
||||
|
||||
|
||||
class LowDark(Palette):
|
||||
"""
|
||||
Low-color dark background
|
||||
"""
|
||||
|
||||
low = dict(
|
||||
background=("white", "black"),
|
||||
title=("white,bold", "default"),
|
||||
# Status bar & heading
|
||||
heading=("white", "dark blue"),
|
||||
heading_key=("light cyan", "dark blue"),
|
||||
heading_inactive=("dark gray", "light gray"),
|
||||
# Help
|
||||
key=("light cyan", "default"),
|
||||
head=("white,bold", "default"),
|
||||
text=("light gray", "default"),
|
||||
# Options
|
||||
option_selected=("black", "light gray"),
|
||||
option_selected_key=("light cyan", "light gray"),
|
||||
option_active=("light red", "default"),
|
||||
option_active_selected=("light red", "light gray"),
|
||||
# List and Connections
|
||||
method_get=("light green", "default"),
|
||||
method_post=("brown", "default"),
|
||||
method_delete=("light red", "default"),
|
||||
method_head=("dark cyan", "default"),
|
||||
method_put=("dark red", "default"),
|
||||
method_other=("dark magenta", "default"),
|
||||
method_http2_push=("dark gray", "default"),
|
||||
scheme_http=("dark cyan", "default"),
|
||||
scheme_https=("dark green", "default"),
|
||||
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_quic=("brown", "default"),
|
||||
scheme_other=("dark magenta", "default"),
|
||||
url_punctuation=("light gray", "default"),
|
||||
url_domain=("white", "default"),
|
||||
url_filename=("dark cyan", "default"),
|
||||
url_extension=("light gray", "default"),
|
||||
url_query_key=("white", "default"),
|
||||
url_query_value=("light gray", "default"),
|
||||
content_none=("dark gray", "default"),
|
||||
content_text=("light gray", "default"),
|
||||
content_script=("dark green", "default"),
|
||||
content_media=("light blue", "default"),
|
||||
content_data=("brown", "default"),
|
||||
content_raw=("dark red", "default"),
|
||||
content_other=("dark magenta", "default"),
|
||||
focus=("yellow", "default"),
|
||||
code_200=("dark green", "default"),
|
||||
code_300=("light blue", "default"),
|
||||
code_400=("light red", "default"),
|
||||
code_500=("light red", "default"),
|
||||
code_other=("dark red", "default"),
|
||||
alert=("light magenta", "default"),
|
||||
warn=("brown", "default"),
|
||||
error=("light red", "default"),
|
||||
header=("dark cyan", "default"),
|
||||
highlight=("white,bold", "default"),
|
||||
intercept=("brown", "default"),
|
||||
replay=("light green", "default"),
|
||||
mark=("light red", "default"),
|
||||
# Contentview Syntax Highlighting
|
||||
name=("dark green", "default"),
|
||||
string=("dark blue", "default"),
|
||||
number=("light magenta", "default"),
|
||||
boolean=("dark magenta", "default"),
|
||||
comment=("dark gray", "default"),
|
||||
# TCP flow details
|
||||
from_client=("light blue", "default"),
|
||||
to_client=("light red", "default"),
|
||||
# Grid Editor
|
||||
focusfield=("black", "light gray"),
|
||||
focusfield_error=("dark red", "light gray"),
|
||||
field_error=("dark red", "default"),
|
||||
editfield=("white", "default"),
|
||||
commander_command=("white,bold", "default"),
|
||||
commander_invalid=("light red", "default"),
|
||||
commander_hint=("dark gray", "default"),
|
||||
)
|
||||
gen_gradient(
|
||||
low,
|
||||
["light red", "yellow", "light green", "dark green", "dark cyan", "dark blue"],
|
||||
)
|
||||
|
||||
|
||||
class Dark(LowDark):
|
||||
high = dict(
|
||||
heading_inactive=("g58", "g11"),
|
||||
intercept=("#f60", "default"),
|
||||
option_selected=("g85", "g45"),
|
||||
option_selected_key=("light cyan", "g50"),
|
||||
option_active_selected=("light red", "g50"),
|
||||
)
|
||||
|
||||
|
||||
class LowLight(Palette):
|
||||
"""
|
||||
Low-color light background
|
||||
"""
|
||||
|
||||
low = dict(
|
||||
background=("black", "white"),
|
||||
title=("dark magenta", "default"),
|
||||
# Status bar & heading
|
||||
heading=("white", "black"),
|
||||
heading_key=("dark blue", "black"),
|
||||
heading_inactive=("black", "light gray"),
|
||||
# Help
|
||||
key=("dark blue", "default"),
|
||||
head=("black", "default"),
|
||||
text=("dark gray", "default"),
|
||||
# Options
|
||||
option_selected=("black", "light gray"),
|
||||
option_selected_key=("dark blue", "light gray"),
|
||||
option_active=("light red", "default"),
|
||||
option_active_selected=("light red", "light gray"),
|
||||
# List and Connections
|
||||
method_get=("dark green", "default"),
|
||||
method_post=("brown", "default"),
|
||||
method_head=("dark cyan", "default"),
|
||||
method_put=("light red", "default"),
|
||||
method_delete=("dark red", "default"),
|
||||
method_other=("light magenta", "default"),
|
||||
method_http2_push=("light gray", "default"),
|
||||
scheme_http=("dark cyan", "default"),
|
||||
scheme_https=("light green", "default"),
|
||||
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_quic=("brown", "default"),
|
||||
scheme_other=("light magenta", "default"),
|
||||
url_punctuation=("dark gray", "default"),
|
||||
url_domain=("dark gray", "default"),
|
||||
url_filename=("black", "default"),
|
||||
url_extension=("dark gray", "default"),
|
||||
url_query_key=("light blue", "default"),
|
||||
url_query_value=("dark blue", "default"),
|
||||
content_none=("black", "default"),
|
||||
content_text=("dark gray", "default"),
|
||||
content_script=("light green", "default"),
|
||||
content_media=("light blue", "default"),
|
||||
content_data=("brown", "default"),
|
||||
content_raw=("light red", "default"),
|
||||
content_other=("light magenta", "default"),
|
||||
focus=("black", "default"),
|
||||
code_200=("dark green", "default"),
|
||||
code_300=("light blue", "default"),
|
||||
code_400=("dark red", "default"),
|
||||
code_500=("dark red", "default"),
|
||||
code_other=("light red", "default"),
|
||||
error=("light red", "default"),
|
||||
warn=("brown", "default"),
|
||||
alert=("light magenta", "default"),
|
||||
header=("dark blue", "default"),
|
||||
highlight=("black,bold", "default"),
|
||||
intercept=("brown", "default"),
|
||||
replay=("dark green", "default"),
|
||||
mark=("dark red", "default"),
|
||||
# Contentview Syntax Highlighting
|
||||
name=("dark green", "default"),
|
||||
string=("dark blue", "default"),
|
||||
number=("light magenta", "default"),
|
||||
boolean=("dark magenta", "default"),
|
||||
comment=("dark gray", "default"),
|
||||
# TCP flow details
|
||||
from_client=("dark blue", "default"),
|
||||
to_client=("dark red", "default"),
|
||||
# Grid Editor
|
||||
focusfield=("black", "light gray"),
|
||||
focusfield_error=("dark red", "light gray"),
|
||||
field_error=("dark red", "black"),
|
||||
editfield=("black", "default"),
|
||||
commander_command=("dark magenta", "default"),
|
||||
commander_invalid=("light red", "default"),
|
||||
commander_hint=("light gray", "default"),
|
||||
)
|
||||
gen_gradient(
|
||||
low,
|
||||
["light red", "yellow", "light green", "dark green", "dark cyan", "dark blue"],
|
||||
)
|
||||
|
||||
|
||||
class Light(LowLight):
|
||||
high = dict(
|
||||
background=("black", "g100"),
|
||||
heading=("g99", "#08f"),
|
||||
heading_key=("#0ff,bold", "#08f"),
|
||||
heading_inactive=("g35", "g85"),
|
||||
replay=("#0a0,bold", "default"),
|
||||
option_selected=("black", "g85"),
|
||||
option_selected_key=("dark blue", "g85"),
|
||||
option_active_selected=("light red", "g85"),
|
||||
)
|
||||
|
||||
|
||||
# Solarized palette in Urwid-style terminal high-colour offsets
|
||||
# See: http://ethanschoonover.com/solarized
|
||||
sol_base03 = "h234"
|
||||
sol_base02 = "h235"
|
||||
sol_base01 = "h240"
|
||||
sol_base00 = "h241"
|
||||
sol_base0 = "h244"
|
||||
sol_base1 = "h245"
|
||||
sol_base2 = "h254"
|
||||
sol_base3 = "h230"
|
||||
sol_yellow = "h136"
|
||||
sol_orange = "h166"
|
||||
sol_red = "h160"
|
||||
sol_magenta = "h125"
|
||||
sol_violet = "h61"
|
||||
sol_blue = "h33"
|
||||
sol_cyan = "h37"
|
||||
sol_green = "h64"
|
||||
|
||||
|
||||
class SolarizedLight(LowLight):
|
||||
high = dict(
|
||||
background=(sol_base00, sol_base3),
|
||||
title=(sol_cyan, "default"),
|
||||
text=(sol_base00, "default"),
|
||||
# Status bar & heading
|
||||
heading=(sol_base2, sol_base02),
|
||||
heading_key=(sol_blue, sol_base03),
|
||||
heading_inactive=(sol_base03, sol_base1),
|
||||
# Help
|
||||
key=(
|
||||
sol_blue,
|
||||
"default",
|
||||
),
|
||||
head=(sol_base00, "default"),
|
||||
# Options
|
||||
option_selected=(sol_base03, sol_base2),
|
||||
option_selected_key=(sol_blue, sol_base2),
|
||||
option_active=(sol_orange, "default"),
|
||||
option_active_selected=(sol_orange, sol_base2),
|
||||
# List and Connections
|
||||
method_get=(sol_green, "default"),
|
||||
method_post=(sol_orange, "default"),
|
||||
method_head=(sol_cyan, "default"),
|
||||
method_put=(sol_red, "default"),
|
||||
method_delete=(sol_red, "default"),
|
||||
method_other=(sol_magenta, "default"),
|
||||
method_http2_push=("light gray", "default"),
|
||||
scheme_http=(sol_cyan, "default"),
|
||||
scheme_https=("light green", "default"),
|
||||
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_quic=(sol_orange, "default"),
|
||||
scheme_other=("light magenta", "default"),
|
||||
url_punctuation=("dark gray", "default"),
|
||||
url_domain=("dark gray", "default"),
|
||||
url_filename=("black", "default"),
|
||||
url_extension=("dark gray", "default"),
|
||||
url_query_key=(sol_blue, "default"),
|
||||
url_query_value=("dark blue", "default"),
|
||||
focus=(sol_base01, "default"),
|
||||
code_200=(sol_green, "default"),
|
||||
code_300=(sol_blue, "default"),
|
||||
code_400=(
|
||||
sol_orange,
|
||||
"default",
|
||||
),
|
||||
code_500=(sol_red, "default"),
|
||||
code_other=(sol_magenta, "default"),
|
||||
error=(sol_red, "default"),
|
||||
warn=(sol_orange, "default"),
|
||||
alert=(sol_magenta, "default"),
|
||||
header=(sol_blue, "default"),
|
||||
highlight=(sol_base01, "default"),
|
||||
intercept=(
|
||||
sol_red,
|
||||
"default",
|
||||
),
|
||||
replay=(
|
||||
sol_green,
|
||||
"default",
|
||||
),
|
||||
mark=(sol_base01, "default"),
|
||||
# Contentview Syntax Highlighting
|
||||
name=(sol_green, "default"),
|
||||
string=(sol_cyan, "default"),
|
||||
number=(sol_blue, "default"),
|
||||
boolean=(sol_magenta, "default"),
|
||||
comment=(sol_base1, "default"),
|
||||
# TCP flow details
|
||||
from_client=(sol_blue, "default"),
|
||||
to_client=(sol_red, "default"),
|
||||
# Grid Editor
|
||||
focusfield=(sol_base00, sol_base2),
|
||||
focusfield_error=(sol_red, sol_base2),
|
||||
field_error=(sol_red, "default"),
|
||||
editfield=(sol_base01, "default"),
|
||||
commander_command=(sol_cyan, "default"),
|
||||
commander_invalid=(sol_orange, "default"),
|
||||
commander_hint=(sol_base1, "default"),
|
||||
)
|
||||
|
||||
|
||||
class SolarizedDark(LowDark):
|
||||
high = dict(
|
||||
background=(sol_base2, sol_base03),
|
||||
title=(sol_blue, "default"),
|
||||
text=(sol_base1, "default"),
|
||||
# Status bar & heading
|
||||
heading=(sol_base2, sol_base01),
|
||||
heading_key=(sol_blue + ",bold", sol_base01),
|
||||
heading_inactive=(sol_base1, sol_base02),
|
||||
# Help
|
||||
key=(
|
||||
sol_blue,
|
||||
"default",
|
||||
),
|
||||
head=(sol_base2, "default"),
|
||||
# Options
|
||||
option_selected=(sol_base03, sol_base00),
|
||||
option_selected_key=(sol_blue, sol_base00),
|
||||
option_active=(sol_orange, "default"),
|
||||
option_active_selected=(sol_orange, sol_base00),
|
||||
# List and Connections
|
||||
focus=(sol_base1, "default"),
|
||||
method_get=(sol_green, "default"),
|
||||
method_post=(sol_orange, "default"),
|
||||
method_delete=(sol_red, "default"),
|
||||
method_head=(sol_cyan, "default"),
|
||||
method_put=(sol_red, "default"),
|
||||
method_other=(sol_magenta, "default"),
|
||||
method_http2_push=(sol_base01, "default"),
|
||||
url_punctuation=("h242", "default"),
|
||||
url_domain=("h252", "default"),
|
||||
url_filename=("h132", "default"),
|
||||
url_extension=("h96", "default"),
|
||||
url_query_key=("h37", "default"),
|
||||
url_query_value=("h30", "default"),
|
||||
content_none=(sol_base01, "default"),
|
||||
content_text=(sol_base1, "default"),
|
||||
content_media=(sol_blue, "default"),
|
||||
code_200=(sol_green, "default"),
|
||||
code_300=(sol_blue, "default"),
|
||||
code_400=(
|
||||
sol_orange,
|
||||
"default",
|
||||
),
|
||||
code_500=(sol_red, "default"),
|
||||
code_other=(sol_magenta, "default"),
|
||||
error=(sol_red, "default"),
|
||||
warn=(sol_orange, "default"),
|
||||
alert=(sol_magenta, "default"),
|
||||
header=(sol_blue, "default"),
|
||||
highlight=(sol_base01, "default"),
|
||||
intercept=(
|
||||
sol_red,
|
||||
"default",
|
||||
),
|
||||
replay=(
|
||||
sol_green,
|
||||
"default",
|
||||
),
|
||||
mark=(sol_base01, "default"),
|
||||
# Contentview Syntax Highlighting
|
||||
name=(sol_green, "default"),
|
||||
string=(sol_cyan, "default"),
|
||||
number=(sol_blue, "default"),
|
||||
boolean=(sol_magenta, "default"),
|
||||
comment=(sol_base00, "default"),
|
||||
# TCP flow details
|
||||
from_client=(sol_blue, "default"),
|
||||
to_client=(sol_red, "default"),
|
||||
# Grid Editor
|
||||
focusfield=(sol_base0, sol_base02),
|
||||
focusfield_error=(sol_red, sol_base02),
|
||||
field_error=(sol_red, "default"),
|
||||
editfield=(sol_base1, "default"),
|
||||
commander_command=(sol_blue, "default"),
|
||||
commander_invalid=(sol_orange, "default"),
|
||||
commander_hint=(sol_base00, "default"),
|
||||
)
|
||||
gen_rgb_gradient(
|
||||
high, [(15, 0, 0), (15, 15, 0), (0, 15, 0), (0, 15, 15), (0, 0, 15)]
|
||||
)
|
||||
|
||||
|
||||
DEFAULT = "dark"
|
||||
palettes = {
|
||||
"lowlight": LowLight(),
|
||||
"lowdark": LowDark(),
|
||||
"light": Light(),
|
||||
"dark": Dark(),
|
||||
"solarized_light": SolarizedLight(),
|
||||
"solarized_dark": SolarizedDark(),
|
||||
}
|
||||
191
venv/Lib/site-packages/mitmproxy/tools/console/quickhelp.py
Normal file
191
venv/Lib/site-packages/mitmproxy/tools/console/quickhelp.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
This module is reponsible for drawing the quick key help at the bottom of mitmproxy.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
import urwid
|
||||
|
||||
from mitmproxy import flow
|
||||
from mitmproxy.http import HTTPFlow
|
||||
from mitmproxy.tools.console.eventlog import EventLog
|
||||
from mitmproxy.tools.console.flowlist import FlowListBox
|
||||
from mitmproxy.tools.console.flowview import FlowView
|
||||
from mitmproxy.tools.console.grideditor.base import FocusEditor
|
||||
from mitmproxy.tools.console.help import HelpView
|
||||
from mitmproxy.tools.console.keybindings import KeyBindings
|
||||
from mitmproxy.tools.console.keymap import Keymap
|
||||
from mitmproxy.tools.console.options import Options
|
||||
|
||||
|
||||
@dataclass
|
||||
class BasicKeyHelp:
|
||||
"""Quick help for urwid-builtin keybindings (i.e. those keys that do not appear in the keymap)"""
|
||||
|
||||
key: str
|
||||
|
||||
|
||||
HelpItems = dict[str, Union[str, BasicKeyHelp]]
|
||||
"""
|
||||
A mapping from the short text that should be displayed in the help bar to the full help text provided for the key
|
||||
binding. The order of the items in the dictionary determines the order in which they are displayed in the help bar.
|
||||
|
||||
Some help items explain builtin urwid functionality, so there is no key binding for them. In this case, the value
|
||||
is a BasicKeyHelp object.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class QuickHelp:
|
||||
top_label: str
|
||||
top_items: HelpItems
|
||||
bottom_label: str
|
||||
bottom_items: HelpItems
|
||||
|
||||
def make_rows(self, keymap: Keymap) -> tuple[urwid.Columns, urwid.Columns]:
|
||||
top = _make_row(self.top_label, self.top_items, keymap)
|
||||
bottom = _make_row(self.bottom_label, self.bottom_items, keymap)
|
||||
return top, bottom
|
||||
|
||||
|
||||
def make(
|
||||
widget: type[urwid.Widget],
|
||||
focused_flow: flow.Flow | None,
|
||||
is_root_widget: bool,
|
||||
) -> QuickHelp:
|
||||
top_label = ""
|
||||
top_items: HelpItems = {}
|
||||
if widget in (FlowListBox, FlowView):
|
||||
top_label = "Flow:"
|
||||
if focused_flow:
|
||||
if widget == FlowListBox:
|
||||
top_items["Select"] = "Select"
|
||||
else:
|
||||
top_items["Edit"] = "Edit a flow component"
|
||||
top_items |= {
|
||||
"Duplicate": "Duplicate flow",
|
||||
"Replay": "Replay this flow",
|
||||
"Export": "Export this flow to file",
|
||||
"Delete": "Delete flow from view",
|
||||
}
|
||||
if widget == FlowListBox:
|
||||
if focused_flow.marked:
|
||||
top_items["Unmark"] = "Toggle mark on this flow"
|
||||
else:
|
||||
top_items["Mark"] = "Toggle mark on this flow"
|
||||
top_items["Edit"] = "Edit a flow component"
|
||||
if focused_flow.intercepted:
|
||||
top_items["Resume"] = "Resume this intercepted flow"
|
||||
if focused_flow.modified():
|
||||
top_items["Restore"] = "Revert changes to this flow"
|
||||
if isinstance(focused_flow, HTTPFlow) and focused_flow.response:
|
||||
top_items["Save body"] = "Save response body to file"
|
||||
if widget == FlowView:
|
||||
top_items |= {
|
||||
"Next flow": "Go to next flow",
|
||||
"Prev flow": "Go to previous flow",
|
||||
}
|
||||
else:
|
||||
top_items |= {
|
||||
"Load flows": "Load flows from file",
|
||||
"Create new": "Create a new flow",
|
||||
}
|
||||
elif widget == KeyBindings:
|
||||
top_label = "Keybindings:"
|
||||
top_items |= {
|
||||
"Add": "Add a key binding",
|
||||
"Edit": "Edit the currently focused key binding",
|
||||
"Delete": "Unbind the currently focused key binding",
|
||||
"Execute": "Execute the currently focused key binding",
|
||||
}
|
||||
elif widget == Options:
|
||||
top_label = "Options:"
|
||||
top_items |= {
|
||||
"Edit": BasicKeyHelp("⏎"),
|
||||
"Reset": "Reset this option",
|
||||
"Reset all": "Reset all options",
|
||||
"Load file": "Load from file",
|
||||
"Save file": "Save to file",
|
||||
}
|
||||
elif widget == HelpView:
|
||||
top_label = "Help:"
|
||||
top_items |= {
|
||||
"Scroll down": BasicKeyHelp("↓"),
|
||||
"Scroll up": BasicKeyHelp("↑"),
|
||||
"Exit help": "Exit help",
|
||||
"Next tab": BasicKeyHelp("tab"),
|
||||
}
|
||||
elif widget == EventLog:
|
||||
top_label = "Events:"
|
||||
top_items |= {
|
||||
"Scroll down": BasicKeyHelp("↓"),
|
||||
"Scroll up": BasicKeyHelp("↑"),
|
||||
"Clear": "Clear",
|
||||
}
|
||||
elif issubclass(widget, FocusEditor):
|
||||
top_label = f"Edit:"
|
||||
top_items |= {
|
||||
"Start edit": BasicKeyHelp("⏎"),
|
||||
"Stop edit": BasicKeyHelp("esc"),
|
||||
"Add row": "Add a row after cursor",
|
||||
"Delete row": "Delete this row",
|
||||
}
|
||||
else:
|
||||
pass
|
||||
|
||||
bottom_label = "Proxy:"
|
||||
bottom_items: HelpItems = {
|
||||
"Help": "View help",
|
||||
}
|
||||
if is_root_widget:
|
||||
bottom_items["Quit"] = "Exit the current view"
|
||||
else:
|
||||
bottom_items["Back"] = "Exit the current view"
|
||||
bottom_items |= {
|
||||
"Events": "View event log",
|
||||
"Options": "View options",
|
||||
"Intercept": "Set intercept",
|
||||
"Filter": "Set view filter",
|
||||
}
|
||||
if focused_flow:
|
||||
bottom_items |= {
|
||||
"Save flows": "Save listed flows to file",
|
||||
"Clear list": "Clear flow list",
|
||||
}
|
||||
bottom_items |= {
|
||||
"Layout": "Cycle to next layout",
|
||||
"Switch": "Focus next layout pane",
|
||||
"Follow new": "Set focus follow",
|
||||
}
|
||||
|
||||
label_len = max(len(top_label), len(bottom_label), 8) + 1
|
||||
top_label = top_label.ljust(label_len)
|
||||
bottom_label = bottom_label.ljust(label_len)
|
||||
|
||||
return QuickHelp(top_label, top_items, bottom_label, bottom_items)
|
||||
|
||||
|
||||
def _make_row(label: str, items: HelpItems, keymap: Keymap) -> urwid.Columns:
|
||||
cols = [
|
||||
(len(label), urwid.Text(label)),
|
||||
]
|
||||
for short, long in items.items():
|
||||
if isinstance(long, BasicKeyHelp):
|
||||
key_short = long.key
|
||||
else:
|
||||
b = keymap.binding_for_help(long)
|
||||
if b is None:
|
||||
continue
|
||||
key_short = b.key_short()
|
||||
txt = urwid.Text(
|
||||
[
|
||||
("heading_inactive", key_short),
|
||||
" ",
|
||||
short,
|
||||
],
|
||||
wrap="clip",
|
||||
)
|
||||
cols.append((14, txt))
|
||||
|
||||
return urwid.Columns(cols)
|
||||
89
venv/Lib/site-packages/mitmproxy/tools/console/searchable.py
Normal file
89
venv/Lib/site-packages/mitmproxy/tools/console/searchable.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import urwid
|
||||
|
||||
from mitmproxy.tools.console import signals
|
||||
|
||||
|
||||
class Highlight(urwid.AttrMap):
|
||||
def __init__(self, t):
|
||||
urwid.AttrMap.__init__(
|
||||
self,
|
||||
urwid.Text(t.text),
|
||||
"focusfield",
|
||||
)
|
||||
self.backup = t
|
||||
|
||||
|
||||
class Searchable(urwid.ListBox):
|
||||
def __init__(self, contents):
|
||||
self.walker = urwid.SimpleFocusListWalker(contents)
|
||||
urwid.ListBox.__init__(self, self.walker)
|
||||
self.search_offset = 0
|
||||
self.current_highlight = None
|
||||
self.search_term = None
|
||||
self.last_search = None
|
||||
|
||||
def keypress(self, size, key: str):
|
||||
if key == "/":
|
||||
signals.status_prompt.send(
|
||||
prompt="Search for", text="", callback=self.set_search
|
||||
)
|
||||
elif key == "n":
|
||||
self.find_next(False)
|
||||
elif key == "N":
|
||||
self.find_next(True)
|
||||
elif key == "m_start":
|
||||
self.set_focus(0)
|
||||
self.walker._modified()
|
||||
elif key == "m_end":
|
||||
self.set_focus(len(self.walker) - 1)
|
||||
self.walker._modified()
|
||||
else:
|
||||
return super().keypress(size, key)
|
||||
|
||||
def set_search(self, text):
|
||||
self.last_search = text
|
||||
self.search_term = text or None
|
||||
self.find_next(False)
|
||||
|
||||
def set_highlight(self, offset):
|
||||
if self.current_highlight is not None:
|
||||
old = self.body[self.current_highlight]
|
||||
self.body[self.current_highlight] = old.backup
|
||||
if offset is None:
|
||||
self.current_highlight = None
|
||||
else:
|
||||
self.body[offset] = Highlight(self.body[offset])
|
||||
self.current_highlight = offset
|
||||
|
||||
def get_text(self, w):
|
||||
if isinstance(w, urwid.Text):
|
||||
return w.text
|
||||
elif isinstance(w, Highlight):
|
||||
return w.backup.text
|
||||
else:
|
||||
return None
|
||||
|
||||
def find_next(self, backwards: bool):
|
||||
if not self.search_term:
|
||||
if self.last_search:
|
||||
self.search_term = self.last_search
|
||||
else:
|
||||
self.set_highlight(None)
|
||||
return
|
||||
# Start search at focus + 1
|
||||
if backwards:
|
||||
rng = range(len(self.body) - 1, -1, -1)
|
||||
else:
|
||||
rng = range(1, len(self.body) + 1)
|
||||
for i in rng:
|
||||
off = (self.focus_position + i) % len(self.body)
|
||||
w = self.body[off]
|
||||
txt = self.get_text(w)
|
||||
if txt and self.search_term in txt:
|
||||
self.set_highlight(off)
|
||||
self.set_focus(off, coming_from="above")
|
||||
self.body._modified()
|
||||
return
|
||||
else:
|
||||
self.set_highlight(None)
|
||||
signals.status_message.send(message="Search not found.", expire=1)
|
||||
66
venv/Lib/site-packages/mitmproxy/tools/console/signals.py
Normal file
66
venv/Lib/site-packages/mitmproxy/tools/console/signals.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Union
|
||||
|
||||
from mitmproxy.utils import signals
|
||||
|
||||
StatusMessage = Union[tuple[str, str], str]
|
||||
|
||||
|
||||
# Show a status message in the action bar
|
||||
# Instead of using this signal directly, consider emitting a log event.
|
||||
def _status_message(message: StatusMessage, expire: int = 5) -> None: ...
|
||||
|
||||
|
||||
status_message = signals.SyncSignal(_status_message)
|
||||
|
||||
|
||||
# Prompt for input
|
||||
def _status_prompt(
|
||||
prompt: str, text: str | None, callback: Callable[[str], None]
|
||||
) -> None: ...
|
||||
|
||||
|
||||
status_prompt = signals.SyncSignal(_status_prompt)
|
||||
|
||||
|
||||
# Prompt for a single keystroke
|
||||
def _status_prompt_onekey(
|
||||
prompt: str, keys: list[tuple[str, str]], callback: Callable[[str], None]
|
||||
) -> None: ...
|
||||
|
||||
|
||||
status_prompt_onekey = signals.SyncSignal(_status_prompt_onekey)
|
||||
|
||||
|
||||
# Prompt for a command
|
||||
def _status_prompt_command(partial: str = "", cursor: int | None = None) -> None: ...
|
||||
|
||||
|
||||
status_prompt_command = signals.SyncSignal(_status_prompt_command)
|
||||
|
||||
|
||||
# Call a callback in N seconds
|
||||
def _call_in(seconds: float, callback: Callable[[], None]) -> None: ...
|
||||
|
||||
|
||||
call_in = signals.SyncSignal(_call_in)
|
||||
|
||||
# Focus the body, footer or header of the main window
|
||||
focus = signals.SyncSignal(lambda section: None)
|
||||
|
||||
# Fired when settings change
|
||||
update_settings = signals.SyncSignal(lambda: None)
|
||||
|
||||
# Fired when a flow changes
|
||||
flow_change = signals.SyncSignal(lambda flow: None)
|
||||
|
||||
# Pop and push view state onto a stack
|
||||
pop_view_state = signals.SyncSignal(lambda: None)
|
||||
|
||||
# Fired when the window state changes
|
||||
window_refresh = signals.SyncSignal(lambda: None)
|
||||
|
||||
# Fired when the key bindings change
|
||||
keybindings_change = signals.SyncSignal(lambda: None)
|
||||
347
venv/Lib/site-packages/mitmproxy/tools/console/statusbar.py
Normal file
347
venv/Lib/site-packages/mitmproxy/tools/console/statusbar.py
Normal file
@@ -0,0 +1,347 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
|
||||
import urwid
|
||||
|
||||
import mitmproxy.tools.console.master
|
||||
from mitmproxy.tools.console import commandexecutor
|
||||
from mitmproxy.tools.console import common
|
||||
from mitmproxy.tools.console import flowlist
|
||||
from mitmproxy.tools.console import quickhelp
|
||||
from mitmproxy.tools.console import signals
|
||||
from mitmproxy.tools.console.commander import commander
|
||||
from mitmproxy.utils import human
|
||||
|
||||
|
||||
@lru_cache
|
||||
def shorten_message(
|
||||
msg: tuple[str, str] | str, max_width: int
|
||||
) -> list[tuple[str, str]]:
|
||||
"""
|
||||
Shorten message so that it fits into a single line in the statusbar.
|
||||
"""
|
||||
if isinstance(msg, tuple):
|
||||
disp_attr, msg_text = msg
|
||||
elif isinstance(msg, str):
|
||||
msg_text = msg
|
||||
disp_attr = ""
|
||||
else:
|
||||
raise AssertionError(f"Unexpected message type: {type(msg)}")
|
||||
msg_end = "\u2026" # unicode ellipsis for the end of shortened message
|
||||
prompt = "(more in eventlog)"
|
||||
|
||||
msg_lines = msg_text.split("\n")
|
||||
first_line = msg_lines[0]
|
||||
if len(msg_lines) > 1:
|
||||
# First line of messages with a few lines must end with prompt.
|
||||
line_length = len(first_line) + len(prompt)
|
||||
else:
|
||||
line_length = len(first_line)
|
||||
|
||||
if line_length > max_width:
|
||||
shortening_index = max(0, max_width - len(prompt) - len(msg_end))
|
||||
first_line = first_line[:shortening_index] + msg_end
|
||||
else:
|
||||
if len(msg_lines) == 1:
|
||||
prompt = ""
|
||||
|
||||
return [(disp_attr, first_line), ("warn", prompt)]
|
||||
|
||||
|
||||
class ActionBar(urwid.WidgetWrap):
|
||||
def __init__(self, master: mitmproxy.tools.console.master.ConsoleMaster) -> None:
|
||||
self.master = master
|
||||
self.top = urwid.WidgetWrap(urwid.Text(""))
|
||||
self.bottom = urwid.WidgetWrap(urwid.Text(""))
|
||||
super().__init__(urwid.Pile([self.top, self.bottom]))
|
||||
self.show_quickhelp()
|
||||
signals.status_message.connect(self.sig_message)
|
||||
signals.status_prompt.connect(self.sig_prompt)
|
||||
signals.status_prompt_onekey.connect(self.sig_prompt_onekey)
|
||||
signals.status_prompt_command.connect(self.sig_prompt_command)
|
||||
signals.window_refresh.connect(self.sig_update)
|
||||
master.view.focus.sig_change.connect(self.sig_update)
|
||||
master.view.sig_view_update.connect(self.sig_update)
|
||||
|
||||
self.prompting: Callable[[str], None] | None = None
|
||||
|
||||
self.onekey: set[str] | None = None
|
||||
|
||||
def sig_update(self, flow=None) -> None:
|
||||
if not self.prompting and flow is None or flow == self.master.view.focus.flow:
|
||||
self.show_quickhelp()
|
||||
|
||||
def sig_message(
|
||||
self, message: tuple[str, str] | str, expire: int | None = 1
|
||||
) -> None:
|
||||
if self.prompting:
|
||||
return
|
||||
cols, _ = self.master.ui.get_cols_rows()
|
||||
w = urwid.Text(shorten_message(message, cols))
|
||||
self.top._w = w
|
||||
self.bottom._w = urwid.Text("")
|
||||
if expire:
|
||||
|
||||
def cb():
|
||||
if w == self.top._w:
|
||||
self.show_quickhelp()
|
||||
|
||||
signals.call_in.send(seconds=expire, callback=cb)
|
||||
|
||||
def sig_prompt(
|
||||
self, prompt: str, text: str | None, callback: Callable[[str], None]
|
||||
) -> None:
|
||||
signals.focus.send(section="footer")
|
||||
self.top._w = urwid.Edit(f"{prompt.strip()}: ", text or "")
|
||||
self.bottom._w = urwid.Text("")
|
||||
self.prompting = callback
|
||||
|
||||
def sig_prompt_command(self, partial: str = "", cursor: int | None = None) -> None:
|
||||
signals.focus.send(section="footer")
|
||||
self.top._w = commander.CommandEdit(
|
||||
self.master,
|
||||
partial,
|
||||
)
|
||||
if cursor is not None:
|
||||
self.top._w.cbuf.cursor = cursor
|
||||
self.bottom._w = urwid.Text("")
|
||||
self.prompting = self.execute_command
|
||||
|
||||
def execute_command(self, txt: str) -> None:
|
||||
if txt.strip():
|
||||
self.master.commands.call("commands.history.add", txt)
|
||||
execute = commandexecutor.CommandExecutor(self.master)
|
||||
execute(txt)
|
||||
|
||||
def sig_prompt_onekey(
|
||||
self, prompt: str, keys: list[tuple[str, str]], callback: Callable[[str], None]
|
||||
) -> None:
|
||||
"""
|
||||
Keys are a set of (word, key) tuples. The appropriate key in the
|
||||
word is highlighted.
|
||||
"""
|
||||
signals.focus.send(section="footer")
|
||||
parts = [prompt, " ("]
|
||||
mkup = []
|
||||
for i, e in enumerate(keys):
|
||||
mkup.extend(common.highlight_key(e[0], e[1]))
|
||||
if i < len(keys) - 1:
|
||||
mkup.append(",")
|
||||
parts.extend(mkup)
|
||||
parts.append(")? ")
|
||||
self.onekey = {i[1] for i in keys}
|
||||
self.top._w = urwid.Edit(parts, "")
|
||||
self.bottom._w = urwid.Text("")
|
||||
self.prompting = callback
|
||||
|
||||
def selectable(self) -> bool:
|
||||
return True
|
||||
|
||||
def keypress(self, size, k):
|
||||
if self.prompting:
|
||||
if k == "esc":
|
||||
self.prompt_done()
|
||||
elif self.onekey:
|
||||
if k == "enter":
|
||||
self.prompt_done()
|
||||
elif k in self.onekey:
|
||||
self.prompt_execute(k)
|
||||
elif k == "enter":
|
||||
text = self.top._w.get_edit_text()
|
||||
self.prompt_execute(text)
|
||||
else:
|
||||
if common.is_keypress(k):
|
||||
self.top._w.keypress(size, k)
|
||||
else:
|
||||
return k
|
||||
|
||||
def show_quickhelp(self) -> None:
|
||||
if w := self.master.window:
|
||||
s = w.focus_stack()
|
||||
focused_widget = type(s.top_widget())
|
||||
is_top_widget = len(s.stack) == 1
|
||||
else: # on startup
|
||||
focused_widget = flowlist.FlowListBox
|
||||
is_top_widget = True
|
||||
focused_flow = self.master.view.focus.flow
|
||||
qh = quickhelp.make(focused_widget, focused_flow, is_top_widget)
|
||||
self.top._w, self.bottom._w = qh.make_rows(self.master.keymap)
|
||||
|
||||
def prompt_done(self) -> None:
|
||||
self.prompting = None
|
||||
self.onekey = None
|
||||
self.show_quickhelp()
|
||||
signals.focus.send(section="body")
|
||||
|
||||
def prompt_execute(self, txt) -> None:
|
||||
callback = self.prompting
|
||||
assert callback is not None
|
||||
self.prompt_done()
|
||||
msg = callback(txt)
|
||||
if msg:
|
||||
signals.status_message.send(message=msg, expire=1)
|
||||
|
||||
|
||||
class StatusBar(urwid.WidgetWrap):
|
||||
REFRESHTIME = 0.5 # Timed refresh time in seconds
|
||||
keyctx = ""
|
||||
|
||||
def __init__(self, master: mitmproxy.tools.console.master.ConsoleMaster) -> None:
|
||||
self.master = master
|
||||
self.ib = urwid.WidgetWrap(urwid.Text(""))
|
||||
self.ab = ActionBar(self.master)
|
||||
super().__init__(urwid.Pile([self.ib, self.ab]))
|
||||
signals.flow_change.connect(self.sig_update)
|
||||
signals.update_settings.connect(self.sig_update)
|
||||
master.options.changed.connect(self.sig_update)
|
||||
master.view.focus.sig_change.connect(self.sig_update)
|
||||
master.view.sig_view_add.connect(self.sig_update)
|
||||
self.refresh()
|
||||
|
||||
def refresh(self) -> None:
|
||||
self.redraw()
|
||||
signals.call_in.send(seconds=self.REFRESHTIME, callback=self.refresh)
|
||||
|
||||
def sig_update(self, *args, **kwargs) -> None:
|
||||
self.redraw()
|
||||
|
||||
def keypress(self, *args, **kwargs):
|
||||
return self.ab.keypress(*args, **kwargs)
|
||||
|
||||
def get_status(self) -> list[tuple[str, str] | str]:
|
||||
r: list[tuple[str, str] | str] = []
|
||||
|
||||
sreplay = self.master.commands.call("replay.server.count")
|
||||
creplay = self.master.commands.call("replay.client.count")
|
||||
|
||||
if len(self.master.options.modify_headers):
|
||||
r.append("[")
|
||||
r.append(("heading_key", "H"))
|
||||
r.append("eaders]")
|
||||
if len(self.master.options.modify_body):
|
||||
r.append("[%d body modifications]" % len(self.master.options.modify_body))
|
||||
if creplay:
|
||||
r.append("[")
|
||||
r.append(("heading_key", "cplayback"))
|
||||
r.append(":%s]" % creplay)
|
||||
if sreplay:
|
||||
r.append("[")
|
||||
r.append(("heading_key", "splayback"))
|
||||
r.append(":%s]" % sreplay)
|
||||
if self.master.options.ignore_hosts:
|
||||
r.append("[")
|
||||
r.append(("heading_key", "I"))
|
||||
r.append("gnore:%d]" % len(self.master.options.ignore_hosts))
|
||||
elif self.master.options.allow_hosts:
|
||||
r.append("[")
|
||||
r.append(("heading_key", "A"))
|
||||
r.append("llow:%d]" % len(self.master.options.allow_hosts))
|
||||
if self.master.options.tcp_hosts:
|
||||
r.append("[")
|
||||
r.append(("heading_key", "T"))
|
||||
r.append("CP:%d]" % len(self.master.options.tcp_hosts))
|
||||
if self.master.options.intercept:
|
||||
r.append("[")
|
||||
if not self.master.options.intercept_active:
|
||||
r.append("X")
|
||||
r.append(("heading_key", "i"))
|
||||
r.append(":%s]" % self.master.options.intercept)
|
||||
if self.master.options.view_filter:
|
||||
r.append("[")
|
||||
r.append(("heading_key", "f"))
|
||||
r.append(":%s]" % self.master.options.view_filter)
|
||||
if self.master.options.stickycookie:
|
||||
r.append("[")
|
||||
r.append(("heading_key", "t"))
|
||||
r.append(":%s]" % self.master.options.stickycookie)
|
||||
if self.master.options.stickyauth:
|
||||
r.append("[")
|
||||
r.append(("heading_key", "u"))
|
||||
r.append(":%s]" % self.master.options.stickyauth)
|
||||
if self.master.options.console_default_contentview != "auto":
|
||||
r.append(
|
||||
"[contentview:%s]" % (self.master.options.console_default_contentview)
|
||||
)
|
||||
if self.master.options.has_changed("view_order"):
|
||||
r.append("[")
|
||||
r.append(("heading_key", "o"))
|
||||
r.append(":%s]" % self.master.options.view_order)
|
||||
|
||||
opts = []
|
||||
if self.master.options.anticache:
|
||||
opts.append("anticache")
|
||||
if self.master.options.anticomp:
|
||||
opts.append("anticomp")
|
||||
if self.master.options.showhost:
|
||||
opts.append("showhost")
|
||||
if not self.master.options.server_replay_refresh:
|
||||
opts.append("norefresh")
|
||||
if not self.master.options.upstream_cert:
|
||||
opts.append("no-upstream-cert")
|
||||
if self.master.options.console_focus_follow:
|
||||
opts.append("following")
|
||||
if self.master.options.stream_large_bodies:
|
||||
opts.append(self.master.options.stream_large_bodies)
|
||||
|
||||
if opts:
|
||||
r.append("[%s]" % (":".join(opts)))
|
||||
|
||||
if self.master.options.mode != ["regular"]:
|
||||
if len(self.master.options.mode) == 1:
|
||||
r.append(f"[{self.master.options.mode[0]}]")
|
||||
else:
|
||||
r.append(f"[modes:{len(self.master.options.mode)}]")
|
||||
if self.master.options.scripts:
|
||||
r.append("[scripts:%s]" % len(self.master.options.scripts))
|
||||
|
||||
if self.master.options.save_stream_file:
|
||||
r.append("[W:%s]" % self.master.options.save_stream_file)
|
||||
|
||||
return r
|
||||
|
||||
def redraw(self) -> None:
|
||||
fc = self.master.commands.execute("view.properties.length")
|
||||
if self.master.view.focus.index is None:
|
||||
offset = 0
|
||||
else:
|
||||
offset = self.master.view.focus.index + 1
|
||||
|
||||
if self.master.options.view_order_reversed:
|
||||
arrow = common.SYMBOL_UP
|
||||
else:
|
||||
arrow = common.SYMBOL_DOWN
|
||||
|
||||
marked = ""
|
||||
if self.master.commands.execute("view.properties.marked"):
|
||||
marked = "M"
|
||||
|
||||
t: list[tuple[str, str] | str] = [
|
||||
("heading", f"{arrow} {marked} [{offset}/{fc}]".ljust(11)),
|
||||
]
|
||||
|
||||
listen_addrs: list[str] = list(
|
||||
dict.fromkeys(
|
||||
human.format_address(a)
|
||||
for a in self.master.addons.get("proxyserver").listen_addrs()
|
||||
)
|
||||
)
|
||||
if listen_addrs:
|
||||
boundaddr = f"[{', '.join(listen_addrs)}]"
|
||||
else:
|
||||
boundaddr = ""
|
||||
t.extend(self.get_status())
|
||||
status = urwid.AttrMap(
|
||||
urwid.Columns(
|
||||
[
|
||||
urwid.Text(t),
|
||||
urwid.Text(boundaddr, align="right"),
|
||||
]
|
||||
),
|
||||
"heading",
|
||||
)
|
||||
self.ib._w = status
|
||||
|
||||
def selectable(self) -> bool:
|
||||
return True
|
||||
58
venv/Lib/site-packages/mitmproxy/tools/console/tabs.py
Normal file
58
venv/Lib/site-packages/mitmproxy/tools/console/tabs.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import urwid
|
||||
|
||||
|
||||
class Tab(urwid.WidgetWrap):
|
||||
def __init__(self, offset, content, attr, onclick):
|
||||
"""
|
||||
onclick is called on click with the tab offset as argument
|
||||
"""
|
||||
p = urwid.Text(content, align="center")
|
||||
p = urwid.Padding(p, align="center", width=("relative", 100))
|
||||
p = urwid.AttrMap(p, attr)
|
||||
urwid.WidgetWrap.__init__(self, p)
|
||||
self.offset = offset
|
||||
self.onclick = onclick
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
if event == "mouse press" and button == 1:
|
||||
self.onclick(self.offset)
|
||||
return True
|
||||
|
||||
|
||||
class Tabs(urwid.WidgetWrap):
|
||||
def __init__(self, tabs, tab_offset=0):
|
||||
super().__init__(urwid.Pile([]))
|
||||
self.tab_offset = tab_offset
|
||||
self.tabs = tabs
|
||||
self.show()
|
||||
|
||||
def change_tab(self, offset):
|
||||
self.tab_offset = offset
|
||||
self.show()
|
||||
|
||||
def keypress(self, size, key):
|
||||
n = len(self.tabs)
|
||||
if key == "m_next":
|
||||
self.change_tab((self.tab_offset + 1) % n)
|
||||
elif key == "right":
|
||||
self.change_tab((self.tab_offset + 1) % n)
|
||||
elif key == "left":
|
||||
self.change_tab((self.tab_offset - 1) % n)
|
||||
return self._w.keypress(size, key)
|
||||
|
||||
def show(self):
|
||||
if not self.tabs:
|
||||
return
|
||||
|
||||
headers = []
|
||||
for i in range(len(self.tabs)):
|
||||
txt = self.tabs[i][0]()
|
||||
if i == self.tab_offset % len(self.tabs):
|
||||
headers.append(Tab(i, txt, "heading", self.change_tab))
|
||||
else:
|
||||
headers.append(Tab(i, txt, "heading_inactive", self.change_tab))
|
||||
headers = urwid.Columns(headers, dividechars=1)
|
||||
self._w = urwid.Frame(
|
||||
body=self.tabs[self.tab_offset % len(self.tabs)][1](), header=headers
|
||||
)
|
||||
self._w.focus_position = "body"
|
||||
317
venv/Lib/site-packages/mitmproxy/tools/console/window.py
Normal file
317
venv/Lib/site-packages/mitmproxy/tools/console/window.py
Normal file
@@ -0,0 +1,317 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
import urwid
|
||||
|
||||
from mitmproxy import flow
|
||||
from mitmproxy.tools.console import commands
|
||||
from mitmproxy.tools.console import common
|
||||
from mitmproxy.tools.console import eventlog
|
||||
from mitmproxy.tools.console import flowlist
|
||||
from mitmproxy.tools.console import flowview
|
||||
from mitmproxy.tools.console import grideditor
|
||||
from mitmproxy.tools.console import help
|
||||
from mitmproxy.tools.console import keybindings
|
||||
from mitmproxy.tools.console import options
|
||||
from mitmproxy.tools.console import overlay
|
||||
from mitmproxy.tools.console import signals
|
||||
from mitmproxy.tools.console import statusbar
|
||||
|
||||
|
||||
class StackWidget(urwid.Frame):
|
||||
def __init__(self, window, widget, title, focus):
|
||||
self.is_focused = focus
|
||||
self.window = window
|
||||
|
||||
if title:
|
||||
header = urwid.AttrMap(
|
||||
urwid.Text(title), "heading" if focus else "heading_inactive"
|
||||
)
|
||||
else:
|
||||
header = None
|
||||
super().__init__(widget, header=header)
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
if event == "mouse press" and button == 1 and not self.is_focused:
|
||||
self.window.switch()
|
||||
return super().mouse_event(size, event, button, col, row, focus)
|
||||
|
||||
def keypress(self, size, key):
|
||||
# Make sure that we don't propagate cursor events outside of the widget.
|
||||
# Otherwise, in a horizontal layout, urwid's Pile would change the focused widget
|
||||
# if we cannot scroll any further.
|
||||
ret = super().keypress(size, key)
|
||||
command = self._command_map[
|
||||
ret
|
||||
] # awkward as they don't implement a full dict api
|
||||
if command and command.startswith("cursor"):
|
||||
return None
|
||||
return ret
|
||||
|
||||
|
||||
class WindowStack:
|
||||
def __init__(self, master, base):
|
||||
self.master = master
|
||||
self.windows = dict(
|
||||
flowlist=flowlist.FlowListBox(master),
|
||||
flowview=flowview.FlowView(master),
|
||||
commands=commands.Commands(master),
|
||||
keybindings=keybindings.KeyBindings(master),
|
||||
options=options.Options(master),
|
||||
help=help.HelpView(master),
|
||||
eventlog=eventlog.EventLog(master),
|
||||
edit_focus_query=grideditor.QueryEditor(master),
|
||||
edit_focus_cookies=grideditor.CookieEditor(master),
|
||||
edit_focus_setcookies=grideditor.SetCookieEditor(master),
|
||||
edit_focus_setcookie_attrs=grideditor.CookieAttributeEditor(master),
|
||||
edit_focus_multipart_form=grideditor.RequestMultipartEditor(master),
|
||||
edit_focus_urlencoded_form=grideditor.RequestUrlEncodedEditor(master),
|
||||
edit_focus_path=grideditor.PathEditor(master),
|
||||
edit_focus_request_headers=grideditor.RequestHeaderEditor(master),
|
||||
edit_focus_response_headers=grideditor.ResponseHeaderEditor(master),
|
||||
)
|
||||
self.stack = [base]
|
||||
self.overlay = None
|
||||
|
||||
def set_overlay(self, o, **kwargs):
|
||||
self.overlay = overlay.SimpleOverlay(
|
||||
self,
|
||||
o,
|
||||
self.top_widget(),
|
||||
o.width,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def top_window(self):
|
||||
"""
|
||||
The current top window, ignoring overlays.
|
||||
"""
|
||||
return self.windows[self.stack[-1]]
|
||||
|
||||
def top_widget(self):
|
||||
"""
|
||||
The current top widget - either a window or the active overlay.
|
||||
"""
|
||||
if self.overlay:
|
||||
return self.overlay
|
||||
return self.top_window()
|
||||
|
||||
def push(self, wname):
|
||||
if self.stack[-1] == wname:
|
||||
return
|
||||
prev = self.top_window()
|
||||
self.stack.append(wname)
|
||||
self.call("layout_pushed", prev)
|
||||
|
||||
def pop(self, *args, **kwargs):
|
||||
"""
|
||||
Pop off the stack, return True if we're already at the top.
|
||||
"""
|
||||
if not self.overlay and len(self.stack) == 1:
|
||||
return True
|
||||
self.call("layout_popping")
|
||||
if self.overlay:
|
||||
self.overlay = None
|
||||
else:
|
||||
self.stack.pop()
|
||||
|
||||
def call(self, name, *args, **kwargs):
|
||||
"""
|
||||
Call a function on both the top window, and the overlay if there is
|
||||
one. If the widget has a key_responder, we call the function on the
|
||||
responder instead.
|
||||
"""
|
||||
getattr(self.top_window(), name)(*args, **kwargs)
|
||||
if self.overlay:
|
||||
getattr(self.overlay, name)(*args, **kwargs)
|
||||
|
||||
|
||||
class Window(urwid.Frame):
|
||||
def __init__(self, master):
|
||||
self.statusbar = statusbar.StatusBar(master)
|
||||
super().__init__(
|
||||
None, header=None, footer=urwid.AttrMap(self.statusbar, "background")
|
||||
)
|
||||
self.master = master
|
||||
self.master.view.sig_view_refresh.connect(self.view_changed)
|
||||
self.master.view.sig_view_add.connect(self.view_changed)
|
||||
self.master.view.sig_view_remove.connect(self.view_changed)
|
||||
self.master.view.sig_view_update.connect(self.view_changed)
|
||||
self.master.view.focus.sig_change.connect(self.view_changed)
|
||||
self.master.view.focus.sig_change.connect(self.focus_changed)
|
||||
|
||||
signals.focus.connect(self.sig_focus)
|
||||
signals.flow_change.connect(self.flow_changed)
|
||||
signals.pop_view_state.connect(self.pop)
|
||||
|
||||
self.master.options.subscribe(
|
||||
self.configure, ["console_layout", "console_layout_headers"]
|
||||
)
|
||||
self.pane = 0
|
||||
self.stacks = [WindowStack(master, "flowlist"), WindowStack(master, "eventlog")]
|
||||
|
||||
def focus_stack(self):
|
||||
return self.stacks[self.pane]
|
||||
|
||||
def configure(self, options, updated):
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
Redraw the layout.
|
||||
"""
|
||||
c = self.master.options.console_layout
|
||||
if c == "single":
|
||||
self.pane = 0
|
||||
|
||||
def wrapped(idx):
|
||||
widget = self.stacks[idx].top_widget()
|
||||
if self.master.options.console_layout_headers:
|
||||
title = self.stacks[idx].top_window().title
|
||||
else:
|
||||
title = None
|
||||
return StackWidget(self, widget, title, self.pane == idx)
|
||||
|
||||
w = None
|
||||
if c == "single":
|
||||
w = wrapped(0)
|
||||
elif c == "vertical":
|
||||
w = urwid.Pile(
|
||||
[wrapped(i) for i, s in enumerate(self.stacks)], focus_item=self.pane
|
||||
)
|
||||
else:
|
||||
w = urwid.Columns(
|
||||
[wrapped(i) for i, s in enumerate(self.stacks)],
|
||||
dividechars=1,
|
||||
focus_column=self.pane,
|
||||
)
|
||||
|
||||
self.body = urwid.AttrMap(w, "background")
|
||||
signals.window_refresh.send()
|
||||
|
||||
def flow_changed(self, flow: flow.Flow) -> None:
|
||||
if self.master.view.focus.flow:
|
||||
if flow.id == self.master.view.focus.flow.id:
|
||||
self.focus_changed()
|
||||
|
||||
def focus_changed(self, *args, **kwargs):
|
||||
"""
|
||||
Triggered when the focus changes - either when it's modified, or
|
||||
when it changes to a different flow altogether.
|
||||
"""
|
||||
for i in self.stacks:
|
||||
i.call("focus_changed")
|
||||
|
||||
def view_changed(self, *args, **kwargs):
|
||||
"""
|
||||
Triggered when the view list has changed.
|
||||
"""
|
||||
for i in self.stacks:
|
||||
i.call("view_changed")
|
||||
|
||||
def set_overlay(self, o, **kwargs):
|
||||
"""
|
||||
Set an overlay on the currently focused stack.
|
||||
"""
|
||||
self.focus_stack().set_overlay(o, **kwargs)
|
||||
self.refresh()
|
||||
|
||||
def push(self, wname):
|
||||
"""
|
||||
Push a window onto the currently focused stack.
|
||||
"""
|
||||
self.focus_stack().push(wname)
|
||||
self.refresh()
|
||||
self.view_changed()
|
||||
self.focus_changed()
|
||||
|
||||
def pop(self) -> None:
|
||||
"""
|
||||
Pop a window from the currently focused stack. If there is only one
|
||||
window on the stack, this prompts for exit.
|
||||
"""
|
||||
if self.focus_stack().pop():
|
||||
self.master.prompt_for_exit()
|
||||
else:
|
||||
self.refresh()
|
||||
self.view_changed()
|
||||
self.focus_changed()
|
||||
|
||||
def stacks_sorted_by_focus(self):
|
||||
"""
|
||||
Returns:
|
||||
self.stacks, with the focused stack first.
|
||||
"""
|
||||
stacks = self.stacks.copy()
|
||||
stacks.insert(0, stacks.pop(self.pane))
|
||||
return stacks
|
||||
|
||||
def current(self, keyctx):
|
||||
"""
|
||||
Returns the active widget with a matching key context, including overlays.
|
||||
If multiple stacks have an active widget with a matching key context,
|
||||
the currently focused stack is preferred.
|
||||
"""
|
||||
for s in self.stacks_sorted_by_focus():
|
||||
t = s.top_widget()
|
||||
if t.keyctx == keyctx:
|
||||
return t
|
||||
|
||||
def current_window(self, keyctx):
|
||||
"""
|
||||
Returns the active window with a matching key context, ignoring overlays.
|
||||
If multiple stacks have an active widget with a matching key context,
|
||||
the currently focused stack is preferred.
|
||||
"""
|
||||
for s in self.stacks_sorted_by_focus():
|
||||
t = s.top_window()
|
||||
if t.keyctx == keyctx:
|
||||
return t
|
||||
|
||||
def sig_focus(self, section):
|
||||
self.focus_position = section
|
||||
|
||||
def switch(self):
|
||||
"""
|
||||
Switch between the two panes.
|
||||
"""
|
||||
if self.master.options.console_layout == "single":
|
||||
self.pane = 0
|
||||
else:
|
||||
self.pane = (self.pane + 1) % len(self.stacks)
|
||||
self.refresh()
|
||||
|
||||
def mouse_event(self, *args, **kwargs):
|
||||
# args: (size, event, button, col, row)
|
||||
k = super().mouse_event(*args, **kwargs)
|
||||
if not k:
|
||||
if args[1] == "mouse drag":
|
||||
signals.status_message.send(
|
||||
message="Hold down fn, shift, alt or ctrl to select text or use the --set console_mouse=false parameter.",
|
||||
expire=1,
|
||||
)
|
||||
elif args[1] == "mouse press" and args[2] == 4:
|
||||
self.keypress(args[0], "up")
|
||||
elif args[1] == "mouse press" and args[2] == 5:
|
||||
self.keypress(args[0], "down")
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
def keypress(self, size, k):
|
||||
k = super().keypress(size, k)
|
||||
if k:
|
||||
return self.master.keymap.handle(self.focus_stack().top_widget().keyctx, k)
|
||||
|
||||
|
||||
class Screen(urwid.raw_display.Screen):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger("urwid")
|
||||
|
||||
def write(self, data):
|
||||
if common.IS_WINDOWS_OR_WSL:
|
||||
# replace urwid's SI/SO, which produce artifacts under WSL.
|
||||
# at some point we may figure out what they actually do.
|
||||
data = re.sub("[\x0e\x0f]", "", data)
|
||||
super().write(data)
|
||||
Reference in New Issue
Block a user