mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-25 09:37:37 +00:00
Merge pull request #3693 from typoon/fix-command-bar-issue-3259
Improve Command Bar UX
This commit is contained in:
commit
3550bdfe00
@ -15,5 +15,5 @@ Usage:
|
|||||||
|
|
||||||
|
|
||||||
def load(l):
|
def load(l):
|
||||||
import pydevd
|
import pydevd_pycharm
|
||||||
pydevd.settrace("localhost", port=5678, stdoutToServer=True, stderrToServer=True)
|
pydevd_pycharm.settrace("localhost", port=5678, stdoutToServer=True, stderrToServer=True, suspend=False)
|
||||||
|
@ -83,15 +83,14 @@ class Core:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@command.command("set")
|
@command.command("set")
|
||||||
def set(self, *spec: str) -> None:
|
def set(self, option: str, value: str = "") -> None:
|
||||||
"""
|
"""
|
||||||
Set an option of the form "key[=value]". When the value is omitted,
|
Set an option. When the value is omitted, booleans are set to true,
|
||||||
booleans are set to true, strings and integers are set to None (if
|
strings and integers are set to None (if permitted), and sequences
|
||||||
permitted), and sequences are emptied. Boolean values can be true,
|
are emptied. Boolean values can be true, false or toggle.
|
||||||
false or toggle. If multiple specs are passed, they are joined
|
Multiple values are concatenated with a single space.
|
||||||
into one separated by spaces.
|
|
||||||
"""
|
"""
|
||||||
strspec = " ".join(spec)
|
strspec = f"{option}={value}"
|
||||||
try:
|
try:
|
||||||
ctx.options.set(strspec)
|
ctx.options.set(strspec)
|
||||||
except exceptions.OptionsError as e:
|
except exceptions.OptionsError as e:
|
||||||
@ -109,14 +108,14 @@ class Core:
|
|||||||
|
|
||||||
# FIXME: this will become view.mark later
|
# FIXME: this will become view.mark later
|
||||||
@command.command("flow.mark")
|
@command.command("flow.mark")
|
||||||
def mark(self, flows: typing.Sequence[flow.Flow], val: bool) -> None:
|
def mark(self, flows: typing.Sequence[flow.Flow], boolean: bool) -> None:
|
||||||
"""
|
"""
|
||||||
Mark flows.
|
Mark flows.
|
||||||
"""
|
"""
|
||||||
updated = []
|
updated = []
|
||||||
for i in flows:
|
for i in flows:
|
||||||
if i.marked != val:
|
if i.marked != boolean:
|
||||||
i.marked = val
|
i.marked = boolean
|
||||||
updated.append(i)
|
updated.append(i)
|
||||||
ctx.master.addons.trigger("update", updated)
|
ctx.master.addons.trigger("update", updated)
|
||||||
|
|
||||||
@ -169,18 +168,18 @@ class Core:
|
|||||||
]
|
]
|
||||||
|
|
||||||
@command.command("flow.set")
|
@command.command("flow.set")
|
||||||
@command.argument("spec", type=mitmproxy.types.Choice("flow.set.options"))
|
@command.argument("attr", type=mitmproxy.types.Choice("flow.set.options"))
|
||||||
def flow_set(
|
def flow_set(
|
||||||
self,
|
self,
|
||||||
flows: typing.Sequence[flow.Flow],
|
flows: typing.Sequence[flow.Flow],
|
||||||
spec: str,
|
attr: str,
|
||||||
sval: str
|
value: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Quickly set a number of common values on flows.
|
Quickly set a number of common values on flows.
|
||||||
"""
|
"""
|
||||||
val: typing.Union[int, str] = sval
|
val: typing.Union[int, str] = value
|
||||||
if spec == "status_code":
|
if attr == "status_code":
|
||||||
try:
|
try:
|
||||||
val = int(val) # type: ignore
|
val = int(val) # type: ignore
|
||||||
except ValueError as v:
|
except ValueError as v:
|
||||||
@ -193,13 +192,13 @@ class Core:
|
|||||||
req = getattr(f, "request", None)
|
req = getattr(f, "request", None)
|
||||||
rupdate = True
|
rupdate = True
|
||||||
if req:
|
if req:
|
||||||
if spec == "method":
|
if attr == "method":
|
||||||
req.method = val
|
req.method = val
|
||||||
elif spec == "host":
|
elif attr == "host":
|
||||||
req.host = val
|
req.host = val
|
||||||
elif spec == "path":
|
elif attr == "path":
|
||||||
req.path = val
|
req.path = val
|
||||||
elif spec == "url":
|
elif attr == "url":
|
||||||
try:
|
try:
|
||||||
req.url = val
|
req.url = val
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@ -212,11 +211,11 @@ class Core:
|
|||||||
resp = getattr(f, "response", None)
|
resp = getattr(f, "response", None)
|
||||||
supdate = True
|
supdate = True
|
||||||
if resp:
|
if resp:
|
||||||
if spec == "status_code":
|
if attr == "status_code":
|
||||||
resp.status_code = val
|
resp.status_code = val
|
||||||
if val in status_codes.RESPONSES:
|
if val in status_codes.RESPONSES:
|
||||||
resp.reason = status_codes.RESPONSES[val] # type: ignore
|
resp.reason = status_codes.RESPONSES[val] # type: ignore
|
||||||
elif spec == "reason":
|
elif attr == "reason":
|
||||||
resp.reason = val
|
resp.reason = val
|
||||||
else:
|
else:
|
||||||
supdate = False
|
supdate = False
|
||||||
@ -225,7 +224,7 @@ class Core:
|
|||||||
updated.append(f)
|
updated.append(f)
|
||||||
|
|
||||||
ctx.master.addons.trigger("update", updated)
|
ctx.master.addons.trigger("update", updated)
|
||||||
ctx.log.alert("Set %s on %s flows." % (spec, len(updated)))
|
ctx.log.alert("Set %s on %s flows." % (attr, len(updated)))
|
||||||
|
|
||||||
@command.command("flow.decode")
|
@command.command("flow.decode")
|
||||||
def decode(self, flows: typing.Sequence[flow.Flow], part: str) -> None:
|
def decode(self, flows: typing.Sequence[flow.Flow], part: str) -> None:
|
||||||
@ -262,12 +261,12 @@ class Core:
|
|||||||
ctx.log.alert("Toggled encoding on %s flows." % len(updated))
|
ctx.log.alert("Toggled encoding on %s flows." % len(updated))
|
||||||
|
|
||||||
@command.command("flow.encode")
|
@command.command("flow.encode")
|
||||||
@command.argument("enc", type=mitmproxy.types.Choice("flow.encode.options"))
|
@command.argument("encoding", type=mitmproxy.types.Choice("flow.encode.options"))
|
||||||
def encode(
|
def encode(
|
||||||
self,
|
self,
|
||||||
flows: typing.Sequence[flow.Flow],
|
flows: typing.Sequence[flow.Flow],
|
||||||
part: str,
|
part: str,
|
||||||
enc: str,
|
encoding: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Encode flows with a specified encoding.
|
Encode flows with a specified encoding.
|
||||||
@ -279,7 +278,7 @@ class Core:
|
|||||||
current_enc = p.headers.get("content-encoding", "identity")
|
current_enc = p.headers.get("content-encoding", "identity")
|
||||||
if current_enc == "identity":
|
if current_enc == "identity":
|
||||||
f.backup()
|
f.backup()
|
||||||
p.encode(enc)
|
p.encode(encoding)
|
||||||
updated.append(f)
|
updated.append(f)
|
||||||
ctx.master.addons.trigger("update", updated)
|
ctx.master.addons.trigger("update", updated)
|
||||||
ctx.log.alert("Encoded %s flows." % len(updated))
|
ctx.log.alert("Encoded %s flows." % len(updated))
|
||||||
|
@ -121,14 +121,14 @@ class Export():
|
|||||||
return list(sorted(formats.keys()))
|
return list(sorted(formats.keys()))
|
||||||
|
|
||||||
@command.command("export.file")
|
@command.command("export.file")
|
||||||
def file(self, fmt: str, f: flow.Flow, path: mitmproxy.types.Path) -> None:
|
def file(self, format: str, flow: flow.Flow, path: mitmproxy.types.Path) -> None:
|
||||||
"""
|
"""
|
||||||
Export a flow to path.
|
Export a flow to path.
|
||||||
"""
|
"""
|
||||||
if fmt not in formats:
|
if format not in formats:
|
||||||
raise exceptions.CommandError("No such export format: %s" % fmt)
|
raise exceptions.CommandError("No such export format: %s" % format)
|
||||||
func: typing.Any = formats[fmt]
|
func: typing.Any = formats[format]
|
||||||
v = func(f)
|
v = func(flow)
|
||||||
try:
|
try:
|
||||||
with open(path, "wb") as fp:
|
with open(path, "wb") as fp:
|
||||||
if isinstance(v, bytes):
|
if isinstance(v, bytes):
|
||||||
@ -139,14 +139,14 @@ class Export():
|
|||||||
ctx.log.error(str(e))
|
ctx.log.error(str(e))
|
||||||
|
|
||||||
@command.command("export.clip")
|
@command.command("export.clip")
|
||||||
def clip(self, fmt: str, f: flow.Flow) -> None:
|
def clip(self, format: str, flow: flow.Flow) -> None:
|
||||||
"""
|
"""
|
||||||
Export a flow to the system clipboard.
|
Export a flow to the system clipboard.
|
||||||
"""
|
"""
|
||||||
if fmt not in formats:
|
if format not in formats:
|
||||||
raise exceptions.CommandError("No such export format: %s" % fmt)
|
raise exceptions.CommandError("No such export format: %s" % format)
|
||||||
func: typing.Any = formats[fmt]
|
func: typing.Any = formats[format]
|
||||||
v = strutils.always_str(func(f))
|
v = strutils.always_str(func(flow))
|
||||||
try:
|
try:
|
||||||
pyperclip.copy(v)
|
pyperclip.copy(v)
|
||||||
except pyperclip.PyperclipException as e:
|
except pyperclip.PyperclipException as e:
|
||||||
|
@ -217,7 +217,7 @@ class View(collections.abc.Sequence):
|
|||||||
|
|
||||||
# Focus
|
# Focus
|
||||||
@command.command("view.focus.go")
|
@command.command("view.focus.go")
|
||||||
def go(self, dst: int) -> None:
|
def go(self, offset: int) -> None:
|
||||||
"""
|
"""
|
||||||
Go to a specified offset. Positive offests are from the beginning of
|
Go to a specified offset. Positive offests are from the beginning of
|
||||||
the view, negative from the end of the view, so that 0 is the first
|
the view, negative from the end of the view, so that 0 is the first
|
||||||
@ -225,13 +225,13 @@ class View(collections.abc.Sequence):
|
|||||||
"""
|
"""
|
||||||
if len(self) == 0:
|
if len(self) == 0:
|
||||||
return
|
return
|
||||||
if dst < 0:
|
if offset < 0:
|
||||||
dst = len(self) + dst
|
offset = len(self) + offset
|
||||||
if dst < 0:
|
if offset < 0:
|
||||||
dst = 0
|
offset = 0
|
||||||
if dst > len(self) - 1:
|
if offset > len(self) - 1:
|
||||||
dst = len(self) - 1
|
offset = len(self) - 1
|
||||||
self.focus.flow = self[dst]
|
self.focus.flow = self[offset]
|
||||||
|
|
||||||
@command.command("view.focus.next")
|
@command.command("view.focus.next")
|
||||||
def focus_next(self) -> None:
|
def focus_next(self) -> None:
|
||||||
@ -266,20 +266,20 @@ class View(collections.abc.Sequence):
|
|||||||
return list(sorted(self.orders.keys()))
|
return list(sorted(self.orders.keys()))
|
||||||
|
|
||||||
@command.command("view.order.reverse")
|
@command.command("view.order.reverse")
|
||||||
def set_reversed(self, value: bool) -> None:
|
def set_reversed(self, boolean: bool) -> None:
|
||||||
self.order_reversed = value
|
self.order_reversed = boolean
|
||||||
self.sig_view_refresh.send(self)
|
self.sig_view_refresh.send(self)
|
||||||
|
|
||||||
@command.command("view.order.set")
|
@command.command("view.order.set")
|
||||||
def set_order(self, order: str) -> None:
|
def set_order(self, order_key: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sets the current view order.
|
Sets the current view order.
|
||||||
"""
|
"""
|
||||||
if order not in self.orders:
|
if order_key not in self.orders:
|
||||||
raise exceptions.CommandError(
|
raise exceptions.CommandError(
|
||||||
"Unknown flow order: %s" % order
|
"Unknown flow order: %s" % order_key
|
||||||
)
|
)
|
||||||
order_key = self.orders[order]
|
order_key = self.orders[order_key]
|
||||||
self.order_key = order_key
|
self.order_key = order_key
|
||||||
newview = sortedcontainers.SortedListWithKey(key=order_key)
|
newview = sortedcontainers.SortedListWithKey(key=order_key)
|
||||||
newview.update(self._view)
|
newview.update(self._view)
|
||||||
@ -298,16 +298,16 @@ class View(collections.abc.Sequence):
|
|||||||
|
|
||||||
# Filter
|
# Filter
|
||||||
@command.command("view.filter.set")
|
@command.command("view.filter.set")
|
||||||
def set_filter_cmd(self, f: str) -> None:
|
def set_filter_cmd(self, filter_expr: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sets the current view filter.
|
Sets the current view filter.
|
||||||
"""
|
"""
|
||||||
filt = None
|
filt = None
|
||||||
if f:
|
if filter_expr:
|
||||||
filt = flowfilter.parse(f)
|
filt = flowfilter.parse(filter_expr)
|
||||||
if not filt:
|
if not filt:
|
||||||
raise exceptions.CommandError(
|
raise exceptions.CommandError(
|
||||||
"Invalid interception filter: %s" % f
|
"Invalid interception filter: %s" % filter_expr
|
||||||
)
|
)
|
||||||
self.set_filter(filt)
|
self.set_filter(filt)
|
||||||
|
|
||||||
@ -340,11 +340,11 @@ class View(collections.abc.Sequence):
|
|||||||
|
|
||||||
# View Settings
|
# View Settings
|
||||||
@command.command("view.settings.getval")
|
@command.command("view.settings.getval")
|
||||||
def getvalue(self, f: mitmproxy.flow.Flow, key: str, default: str) -> str:
|
def getvalue(self, flow: mitmproxy.flow.Flow, key: str, default: str) -> str:
|
||||||
"""
|
"""
|
||||||
Get a value from the settings store for the specified flow.
|
Get a value from the settings store for the specified flow.
|
||||||
"""
|
"""
|
||||||
return self.settings[f].get(key, default)
|
return self.settings[flow].get(key, default)
|
||||||
|
|
||||||
@command.command("view.settings.setval.toggle")
|
@command.command("view.settings.setval.toggle")
|
||||||
def setvalue_toggle(
|
def setvalue_toggle(
|
||||||
@ -412,26 +412,26 @@ class View(collections.abc.Sequence):
|
|||||||
ctx.log.alert("Removed %s flows" % len(flows))
|
ctx.log.alert("Removed %s flows" % len(flows))
|
||||||
|
|
||||||
@command.command("view.flows.resolve")
|
@command.command("view.flows.resolve")
|
||||||
def resolve(self, spec: str) -> typing.Sequence[mitmproxy.flow.Flow]:
|
def resolve(self, flow_spec: str) -> typing.Sequence[mitmproxy.flow.Flow]:
|
||||||
"""
|
"""
|
||||||
Resolve a flow list specification to an actual list of flows.
|
Resolve a flow list specification to an actual list of flows.
|
||||||
"""
|
"""
|
||||||
if spec == "@all":
|
if flow_spec == "@all":
|
||||||
return [i for i in self._store.values()]
|
return [i for i in self._store.values()]
|
||||||
if spec == "@focus":
|
if flow_spec == "@focus":
|
||||||
return [self.focus.flow] if self.focus.flow else []
|
return [self.focus.flow] if self.focus.flow else []
|
||||||
elif spec == "@shown":
|
elif flow_spec == "@shown":
|
||||||
return [i for i in self]
|
return [i for i in self]
|
||||||
elif spec == "@hidden":
|
elif flow_spec == "@hidden":
|
||||||
return [i for i in self._store.values() if i not in self._view]
|
return [i for i in self._store.values() if i not in self._view]
|
||||||
elif spec == "@marked":
|
elif flow_spec == "@marked":
|
||||||
return [i for i in self._store.values() if i.marked]
|
return [i for i in self._store.values() if i.marked]
|
||||||
elif spec == "@unmarked":
|
elif flow_spec == "@unmarked":
|
||||||
return [i for i in self._store.values() if not i.marked]
|
return [i for i in self._store.values() if not i.marked]
|
||||||
else:
|
else:
|
||||||
filt = flowfilter.parse(spec)
|
filt = flowfilter.parse(flow_spec)
|
||||||
if not filt:
|
if not filt:
|
||||||
raise exceptions.CommandError("Invalid flow filter: %s" % spec)
|
raise exceptions.CommandError("Invalid flow filter: %s" % flow_spec)
|
||||||
return [i for i in self._store.values() if filt(i)]
|
return [i for i in self._store.values() if filt(i)]
|
||||||
|
|
||||||
@command.command("view.flows.create")
|
@command.command("view.flows.create")
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
"""
|
"""
|
||||||
This module manages and invokes typed commands.
|
This module manages and invokes typed commands.
|
||||||
"""
|
"""
|
||||||
import inspect
|
|
||||||
import types
|
|
||||||
import io
|
|
||||||
import typing
|
|
||||||
import shlex
|
|
||||||
import textwrap
|
|
||||||
import functools
|
import functools
|
||||||
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
|
import textwrap
|
||||||
|
import types
|
||||||
|
import typing
|
||||||
|
|
||||||
from mitmproxy import exceptions
|
|
||||||
import mitmproxy.types
|
import mitmproxy.types
|
||||||
|
from mitmproxy import exceptions, command_lexer
|
||||||
|
from mitmproxy.command_lexer import unquote
|
||||||
|
|
||||||
|
|
||||||
def verify_arg_signature(f: typing.Callable, args: list, kwargs: dict) -> None:
|
def verify_arg_signature(f: typing.Callable, args: typing.Iterable[typing.Any], kwargs: dict) -> None:
|
||||||
sig = inspect.signature(f)
|
sig = inspect.signature(f)
|
||||||
try:
|
try:
|
||||||
sig.bind(*args, **kwargs)
|
sig.bind(*args, **kwargs)
|
||||||
@ -22,15 +21,6 @@ def verify_arg_signature(f: typing.Callable, args: list, kwargs: dict) -> None:
|
|||||||
raise exceptions.CommandError("command argument mismatch: %s" % v.args[0])
|
raise exceptions.CommandError("command argument mismatch: %s" % v.args[0])
|
||||||
|
|
||||||
|
|
||||||
def lexer(s):
|
|
||||||
# mypy mis-identifies shlex.shlex as abstract
|
|
||||||
lex = shlex.shlex(s, posix=True) # type: ignore
|
|
||||||
lex.wordchars += "."
|
|
||||||
lex.whitespace_split = True
|
|
||||||
lex.commenters = ''
|
|
||||||
return lex
|
|
||||||
|
|
||||||
|
|
||||||
def typename(t: type) -> str:
|
def typename(t: type) -> str:
|
||||||
"""
|
"""
|
||||||
Translates a type to an explanatory string.
|
Translates a type to an explanatory string.
|
||||||
@ -43,208 +33,234 @@ def typename(t: type) -> str:
|
|||||||
return to.display
|
return to.display
|
||||||
|
|
||||||
|
|
||||||
class Command:
|
def _empty_as_none(x: typing.Any) -> typing.Any:
|
||||||
returntype: typing.Optional[typing.Type]
|
if x == inspect.Signature.empty:
|
||||||
|
return None
|
||||||
|
return x
|
||||||
|
|
||||||
def __init__(self, manager, path, func) -> None:
|
|
||||||
self.path = path
|
class CommandParameter(typing.NamedTuple):
|
||||||
|
name: str
|
||||||
|
type: typing.Type
|
||||||
|
kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_OR_KEYWORD
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.kind is inspect.Parameter.VAR_POSITIONAL:
|
||||||
|
return f"*{self.name}"
|
||||||
|
else:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Command:
|
||||||
|
name: str
|
||||||
|
manager: "CommandManager"
|
||||||
|
signature: inspect.Signature
|
||||||
|
help: typing.Optional[str]
|
||||||
|
|
||||||
|
def __init__(self, manager: "CommandManager", name: str, func: typing.Callable) -> None:
|
||||||
|
self.name = name
|
||||||
self.manager = manager
|
self.manager = manager
|
||||||
self.func = func
|
self.func = func
|
||||||
sig = inspect.signature(self.func)
|
self.signature = inspect.signature(self.func)
|
||||||
self.help = None
|
|
||||||
if func.__doc__:
|
if func.__doc__:
|
||||||
txt = func.__doc__.strip()
|
txt = func.__doc__.strip()
|
||||||
self.help = "\n".join(textwrap.wrap(txt))
|
self.help = "\n".join(textwrap.wrap(txt))
|
||||||
|
|
||||||
self.has_positional = False
|
|
||||||
for i in sig.parameters.values():
|
|
||||||
# This is the kind for *args parameters
|
|
||||||
if i.kind == i.VAR_POSITIONAL:
|
|
||||||
self.has_positional = True
|
|
||||||
self.paramtypes = [v.annotation for v in sig.parameters.values()]
|
|
||||||
if sig.return_annotation == inspect._empty: # type: ignore
|
|
||||||
self.returntype = None
|
|
||||||
else:
|
else:
|
||||||
self.returntype = sig.return_annotation
|
self.help = None
|
||||||
|
|
||||||
# This fails with a CommandException if types are invalid
|
# This fails with a CommandException if types are invalid
|
||||||
self.signature_help()
|
for name, parameter in self.signature.parameters.items():
|
||||||
|
t = parameter.annotation
|
||||||
|
if not mitmproxy.types.CommandTypes.get(parameter.annotation, None):
|
||||||
|
raise exceptions.CommandError(f"Argument {name} has an unknown type ({_empty_as_none(t)}) in {func}.")
|
||||||
|
if self.return_type and not mitmproxy.types.CommandTypes.get(self.return_type, None):
|
||||||
|
raise exceptions.CommandError(f"Return type has an unknown type ({self.return_type}) in {func}.")
|
||||||
|
|
||||||
def paramnames(self) -> typing.Sequence[str]:
|
@property
|
||||||
v = [typename(i) for i in self.paramtypes]
|
def return_type(self) -> typing.Optional[typing.Type]:
|
||||||
if self.has_positional:
|
return _empty_as_none(self.signature.return_annotation)
|
||||||
v[-1] = "*" + v[-1]
|
|
||||||
return v
|
|
||||||
|
|
||||||
def retname(self) -> str:
|
@property
|
||||||
return typename(self.returntype) if self.returntype else ""
|
def parameters(self) -> typing.List[CommandParameter]:
|
||||||
|
"""Returns a list of CommandParameters."""
|
||||||
|
ret = []
|
||||||
|
for name, param in self.signature.parameters.items():
|
||||||
|
ret.append(CommandParameter(name, param.annotation, param.kind))
|
||||||
|
return ret
|
||||||
|
|
||||||
def signature_help(self) -> str:
|
def signature_help(self) -> str:
|
||||||
params = " ".join(self.paramnames())
|
params = " ".join(str(param) for param in self.parameters)
|
||||||
ret = self.retname()
|
if self.return_type:
|
||||||
if ret:
|
ret = f" -> {typename(self.return_type)}"
|
||||||
ret = " -> " + ret
|
else:
|
||||||
return "%s %s%s" % (self.path, params, ret)
|
ret = ""
|
||||||
|
return f"{self.name} {params}{ret}"
|
||||||
|
|
||||||
def prepare_args(self, args: typing.Sequence[str]) -> typing.List[typing.Any]:
|
def prepare_args(self, args: typing.Sequence[str]) -> inspect.BoundArguments:
|
||||||
verify_arg_signature(self.func, list(args), {})
|
try:
|
||||||
|
bound_arguments = self.signature.bind(*args)
|
||||||
|
except TypeError as v:
|
||||||
|
raise exceptions.CommandError(f"Command argument mismatch: {v.args[0]}")
|
||||||
|
|
||||||
remainder: typing.Sequence[str] = []
|
for name, value in bound_arguments.arguments.items():
|
||||||
if self.has_positional:
|
convert_to = self.signature.parameters[name].annotation
|
||||||
remainder = args[len(self.paramtypes) - 1:]
|
bound_arguments.arguments[name] = parsearg(self.manager, value, convert_to)
|
||||||
args = args[:len(self.paramtypes) - 1]
|
|
||||||
|
|
||||||
pargs = []
|
bound_arguments.apply_defaults()
|
||||||
for arg, paramtype in zip(args, self.paramtypes):
|
|
||||||
pargs.append(parsearg(self.manager, arg, paramtype))
|
return bound_arguments
|
||||||
pargs.extend(remainder)
|
|
||||||
return pargs
|
|
||||||
|
|
||||||
def call(self, args: typing.Sequence[str]) -> typing.Any:
|
def call(self, args: typing.Sequence[str]) -> typing.Any:
|
||||||
"""
|
"""
|
||||||
Call the command with a list of arguments. At this point, all
|
Call the command with a list of arguments. At this point, all
|
||||||
arguments are strings.
|
arguments are strings.
|
||||||
"""
|
"""
|
||||||
ret = self.func(*self.prepare_args(args))
|
bound_args = self.prepare_args(args)
|
||||||
if ret is None and self.returntype is None:
|
ret = self.func(*bound_args.args, **bound_args.kwargs)
|
||||||
|
if ret is None and self.return_type is None:
|
||||||
return
|
return
|
||||||
typ = mitmproxy.types.CommandTypes.get(self.returntype)
|
typ = mitmproxy.types.CommandTypes.get(self.return_type)
|
||||||
|
assert typ
|
||||||
if not typ.is_valid(self.manager, typ, ret):
|
if not typ.is_valid(self.manager, typ, ret):
|
||||||
raise exceptions.CommandError(
|
raise exceptions.CommandError(
|
||||||
"%s returned unexpected data - expected %s" % (
|
f"{self.name} returned unexpected data - expected {typ.display}"
|
||||||
self.path, typ.display
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
ParseResult = typing.NamedTuple(
|
class ParseResult(typing.NamedTuple):
|
||||||
"ParseResult",
|
value: str
|
||||||
[
|
type: typing.Type
|
||||||
("value", str),
|
valid: bool
|
||||||
("type", typing.Type),
|
|
||||||
("valid", bool),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CommandManager(mitmproxy.types._CommandBase):
|
class CommandManager:
|
||||||
|
commands: typing.Dict[str, Command]
|
||||||
|
|
||||||
def __init__(self, master):
|
def __init__(self, master):
|
||||||
self.master = master
|
self.master = master
|
||||||
self.commands: typing.Dict[str, Command] = {}
|
self.commands = {}
|
||||||
|
|
||||||
def collect_commands(self, addon):
|
def collect_commands(self, addon):
|
||||||
for i in dir(addon):
|
for i in dir(addon):
|
||||||
if not i.startswith("__"):
|
if not i.startswith("__"):
|
||||||
o = getattr(addon, i)
|
o = getattr(addon, i)
|
||||||
try:
|
try:
|
||||||
is_command = hasattr(o, "command_path")
|
is_command = hasattr(o, "command_name")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # hasattr may raise if o implements __getattr__.
|
pass # hasattr may raise if o implements __getattr__.
|
||||||
else:
|
else:
|
||||||
if is_command:
|
if is_command:
|
||||||
try:
|
try:
|
||||||
self.add(o.command_path, o)
|
self.add(o.command_name, o)
|
||||||
except exceptions.CommandError as e:
|
except exceptions.CommandError as e:
|
||||||
self.master.log.warn(
|
self.master.log.warn(
|
||||||
"Could not load command %s: %s" % (o.command_path, e)
|
"Could not load command %s: %s" % (o.command_name, e)
|
||||||
)
|
)
|
||||||
|
|
||||||
def add(self, path: str, func: typing.Callable):
|
def add(self, path: str, func: typing.Callable):
|
||||||
self.commands[path] = Command(self, path, func)
|
self.commands[path] = Command(self, path, func)
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=128)
|
||||||
def parse_partial(
|
def parse_partial(
|
||||||
self,
|
self,
|
||||||
cmdstr: str
|
cmdstr: str
|
||||||
) -> typing.Tuple[typing.Sequence[ParseResult], typing.Sequence[str]]:
|
) -> typing.Tuple[typing.Sequence[ParseResult], typing.Sequence[CommandParameter]]:
|
||||||
"""
|
"""
|
||||||
Parse a possibly partial command. Return a sequence of ParseResults and a sequence of remainder type help items.
|
Parse a possibly partial command. Return a sequence of ParseResults and a sequence of remainder type help items.
|
||||||
"""
|
"""
|
||||||
buf = io.StringIO(cmdstr)
|
|
||||||
parts: typing.List[str] = []
|
|
||||||
lex = lexer(buf)
|
|
||||||
while 1:
|
|
||||||
remainder = cmdstr[buf.tell():]
|
|
||||||
try:
|
|
||||||
t = lex.get_token()
|
|
||||||
except ValueError:
|
|
||||||
parts.append(remainder)
|
|
||||||
break
|
|
||||||
if not t:
|
|
||||||
break
|
|
||||||
parts.append(t)
|
|
||||||
if not parts:
|
|
||||||
parts = [""]
|
|
||||||
elif cmdstr.endswith(" "):
|
|
||||||
parts.append("")
|
|
||||||
|
|
||||||
parse: typing.List[ParseResult] = []
|
parts: typing.List[str] = command_lexer.expr.parseString(cmdstr, parseAll=True)
|
||||||
params: typing.List[type] = []
|
|
||||||
typ: typing.Type
|
parsed: typing.List[ParseResult] = []
|
||||||
for i in range(len(parts)):
|
next_params: typing.List[CommandParameter] = [
|
||||||
if i == 0:
|
CommandParameter("", mitmproxy.types.Cmd),
|
||||||
typ = mitmproxy.types.Cmd
|
CommandParameter("", mitmproxy.types.CmdArgs),
|
||||||
if parts[i] in self.commands:
|
]
|
||||||
params.extend(self.commands[parts[i]].paramtypes)
|
expected: typing.Optional[CommandParameter] = None
|
||||||
elif params:
|
for part in parts:
|
||||||
typ = params.pop(0)
|
if part.isspace():
|
||||||
if typ == mitmproxy.types.Cmd and params and params[0] == mitmproxy.types.Arg:
|
parsed.append(
|
||||||
if parts[i] in self.commands:
|
ParseResult(
|
||||||
params[:] = self.commands[parts[i]].paramtypes
|
value=part,
|
||||||
|
type=mitmproxy.types.Space,
|
||||||
|
valid=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if expected and expected.kind is inspect.Parameter.VAR_POSITIONAL:
|
||||||
|
assert not next_params
|
||||||
|
elif next_params:
|
||||||
|
expected = next_params.pop(0)
|
||||||
else:
|
else:
|
||||||
typ = mitmproxy.types.Unknown
|
expected = CommandParameter("", mitmproxy.types.Unknown)
|
||||||
|
|
||||||
to = mitmproxy.types.CommandTypes.get(typ, None)
|
arg_is_known_command = (
|
||||||
|
expected.type == mitmproxy.types.Cmd and part in self.commands
|
||||||
|
)
|
||||||
|
arg_is_unknown_command = (
|
||||||
|
expected.type == mitmproxy.types.Cmd and part not in self.commands
|
||||||
|
)
|
||||||
|
command_args_following = (
|
||||||
|
next_params and next_params[0].type == mitmproxy.types.CmdArgs
|
||||||
|
)
|
||||||
|
if arg_is_known_command and command_args_following:
|
||||||
|
next_params = self.commands[part].parameters + next_params[1:]
|
||||||
|
if arg_is_unknown_command and command_args_following:
|
||||||
|
next_params.pop(0)
|
||||||
|
|
||||||
|
to = mitmproxy.types.CommandTypes.get(expected.type, None)
|
||||||
valid = False
|
valid = False
|
||||||
if to:
|
if to:
|
||||||
try:
|
try:
|
||||||
to.parse(self, typ, parts[i])
|
to.parse(self, expected.type, part)
|
||||||
except exceptions.TypeError:
|
except exceptions.TypeError:
|
||||||
valid = False
|
valid = False
|
||||||
else:
|
else:
|
||||||
valid = True
|
valid = True
|
||||||
|
|
||||||
parse.append(
|
parsed.append(
|
||||||
ParseResult(
|
ParseResult(
|
||||||
value=parts[i],
|
value=part,
|
||||||
type=typ,
|
type=expected.type,
|
||||||
valid=valid,
|
valid=valid,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
remhelp: typing.List[str] = []
|
return parsed, next_params
|
||||||
for x in params:
|
|
||||||
remt = mitmproxy.types.CommandTypes.get(x, None)
|
|
||||||
remhelp.append(remt.display)
|
|
||||||
|
|
||||||
return parse, remhelp
|
def call(self, command_name: str, *args: typing.Sequence[typing.Any]) -> typing.Any:
|
||||||
|
|
||||||
def call(self, path: str, *args: typing.Sequence[typing.Any]) -> typing.Any:
|
|
||||||
"""
|
"""
|
||||||
Call a command with native arguments. May raise CommandError.
|
Call a command with native arguments. May raise CommandError.
|
||||||
"""
|
"""
|
||||||
if path not in self.commands:
|
if command_name not in self.commands:
|
||||||
raise exceptions.CommandError("Unknown command: %s" % path)
|
raise exceptions.CommandError("Unknown command: %s" % command_name)
|
||||||
return self.commands[path].func(*args)
|
return self.commands[command_name].func(*args)
|
||||||
|
|
||||||
def call_strings(self, path: str, args: typing.Sequence[str]) -> typing.Any:
|
def _call_strings(self, command_name: str, args: typing.Sequence[str]) -> typing.Any:
|
||||||
"""
|
"""
|
||||||
Call a command using a list of string arguments. May raise CommandError.
|
Call a command using a list of string arguments. May raise CommandError.
|
||||||
"""
|
"""
|
||||||
if path not in self.commands:
|
if command_name not in self.commands:
|
||||||
raise exceptions.CommandError("Unknown command: %s" % path)
|
raise exceptions.CommandError("Unknown command: %s" % command_name)
|
||||||
return self.commands[path].call(args)
|
|
||||||
|
|
||||||
def execute(self, cmdstr: str):
|
return self.commands[command_name].call(args)
|
||||||
|
|
||||||
|
def execute(self, cmdstr: str) -> typing.Any:
|
||||||
"""
|
"""
|
||||||
Execute a command string. May raise CommandError.
|
Execute a command string. May raise CommandError.
|
||||||
"""
|
"""
|
||||||
try:
|
parts, _ = self.parse_partial(cmdstr)
|
||||||
parts = list(lexer(cmdstr))
|
if not parts:
|
||||||
except ValueError as e:
|
raise exceptions.CommandError(f"Invalid command: {cmdstr!r}")
|
||||||
raise exceptions.CommandError("Command error: %s" % e)
|
command_name, *args = [
|
||||||
if not len(parts) >= 1:
|
unquote(part.value)
|
||||||
raise exceptions.CommandError("Invalid command: %s" % cmdstr)
|
for part in parts
|
||||||
return self.call_strings(parts[0], parts[1:])
|
if part.type != mitmproxy.types.Space
|
||||||
|
]
|
||||||
|
return self._call_strings(command_name, args)
|
||||||
|
|
||||||
def dump(self, out=sys.stdout) -> None:
|
def dump(self, out=sys.stdout) -> None:
|
||||||
cmds = list(self.commands.values())
|
cmds = list(self.commands.values())
|
||||||
@ -262,21 +278,23 @@ def parsearg(manager: CommandManager, spec: str, argtype: type) -> typing.Any:
|
|||||||
"""
|
"""
|
||||||
t = mitmproxy.types.CommandTypes.get(argtype, None)
|
t = mitmproxy.types.CommandTypes.get(argtype, None)
|
||||||
if not t:
|
if not t:
|
||||||
raise exceptions.CommandError("Unsupported argument type: %s" % argtype)
|
raise exceptions.CommandError(f"Unsupported argument type: {argtype}")
|
||||||
try:
|
try:
|
||||||
return t.parse(manager, argtype, spec) # type: ignore
|
return t.parse(manager, argtype, spec)
|
||||||
except exceptions.TypeError as e:
|
except exceptions.TypeError as e:
|
||||||
raise exceptions.CommandError from e
|
raise exceptions.CommandError from e
|
||||||
|
|
||||||
|
|
||||||
def command(path):
|
def command(name: typing.Optional[str] = None):
|
||||||
def decorator(function):
|
def decorator(function):
|
||||||
@functools.wraps(function)
|
@functools.wraps(function)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
verify_arg_signature(function, args, kwargs)
|
verify_arg_signature(function, args, kwargs)
|
||||||
return function(*args, **kwargs)
|
return function(*args, **kwargs)
|
||||||
wrapper.__dict__["command_path"] = path
|
|
||||||
|
wrapper.__dict__["command_name"] = name or function.__name__.replace("_", ".")
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
@ -286,8 +304,10 @@ def argument(name, type):
|
|||||||
specific types such as mitmproxy.types.Choice, which we cannot annotate
|
specific types such as mitmproxy.types.Choice, which we cannot annotate
|
||||||
directly as mypy does not like that.
|
directly as mypy does not like that.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f: types.FunctionType) -> types.FunctionType:
|
def decorator(f: types.FunctionType) -> types.FunctionType:
|
||||||
assert name in f.__annotations__
|
assert name in f.__annotations__
|
||||||
f.__annotations__[name] = type
|
f.__annotations__[name] = type
|
||||||
return f
|
return f
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
49
mitmproxy/command_lexer.py
Normal file
49
mitmproxy/command_lexer.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import ast
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pyparsing
|
||||||
|
|
||||||
|
# TODO: There is a lot of work to be done here.
|
||||||
|
# The current implementation is written in a way that _any_ input is valid,
|
||||||
|
# which does not make sense once things get more complex.
|
||||||
|
|
||||||
|
PartialQuotedString = pyparsing.Regex(
|
||||||
|
re.compile(
|
||||||
|
r'''
|
||||||
|
(["']) # start quote
|
||||||
|
(?:
|
||||||
|
(?!\1)[^\\] # unescaped character that is not our quote nor the begin of an escape sequence. We can't use \1 in []
|
||||||
|
|
|
||||||
|
(?:\\.) # escape sequence
|
||||||
|
)*
|
||||||
|
(?:\1|$) # end quote
|
||||||
|
''',
|
||||||
|
re.VERBOSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
expr = pyparsing.ZeroOrMore(
|
||||||
|
PartialQuotedString
|
||||||
|
| pyparsing.Word(" \r\n\t")
|
||||||
|
| pyparsing.CharsNotIn("""'" \r\n\t""")
|
||||||
|
).leaveWhitespace()
|
||||||
|
|
||||||
|
|
||||||
|
def quote(val: str) -> str:
|
||||||
|
if val and all(char not in val for char in "'\" \r\n\t"):
|
||||||
|
return val
|
||||||
|
return repr(val) # TODO: More of a hack.
|
||||||
|
|
||||||
|
|
||||||
|
def unquote(x: str) -> str:
|
||||||
|
quoted = (
|
||||||
|
(x.startswith('"') and x.endswith('"'))
|
||||||
|
or
|
||||||
|
(x.startswith("'") and x.endswith("'"))
|
||||||
|
)
|
||||||
|
if quoted:
|
||||||
|
try:
|
||||||
|
x = ast.literal_eval(x)
|
||||||
|
except Exception:
|
||||||
|
x = x[1:-1]
|
||||||
|
return x
|
@ -1,21 +1,21 @@
|
|||||||
import abc
|
import abc
|
||||||
|
import collections
|
||||||
import copy
|
import copy
|
||||||
import typing
|
import typing
|
||||||
import collections
|
|
||||||
|
|
||||||
import urwid
|
import urwid
|
||||||
from urwid.text_layout import calc_coords
|
from urwid.text_layout import calc_coords
|
||||||
|
|
||||||
|
import mitmproxy.command
|
||||||
import mitmproxy.flow
|
import mitmproxy.flow
|
||||||
import mitmproxy.master
|
import mitmproxy.master
|
||||||
import mitmproxy.command
|
|
||||||
import mitmproxy.types
|
import mitmproxy.types
|
||||||
|
|
||||||
|
|
||||||
class Completer: # pragma: no cover
|
class Completer:
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def cycle(self) -> str:
|
def cycle(self, forward: bool = True) -> str:
|
||||||
pass
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class ListCompleter(Completer):
|
class ListCompleter(Completer):
|
||||||
@ -25,34 +25,31 @@ class ListCompleter(Completer):
|
|||||||
options: typing.Sequence[str],
|
options: typing.Sequence[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
self.start = start
|
self.start = start
|
||||||
self.options: typing.Sequence[str] = []
|
self.options: typing.List[str] = []
|
||||||
for o in options:
|
for o in options:
|
||||||
if o.startswith(start):
|
if o.startswith(start):
|
||||||
self.options.append(o)
|
self.options.append(o)
|
||||||
self.options.sort()
|
self.options.sort()
|
||||||
self.offset = 0
|
self.offset = 0
|
||||||
|
|
||||||
def cycle(self) -> str:
|
def cycle(self, forward: bool = True) -> str:
|
||||||
if not self.options:
|
if not self.options:
|
||||||
return self.start
|
return self.start
|
||||||
ret = self.options[self.offset]
|
ret = self.options[self.offset]
|
||||||
self.offset = (self.offset + 1) % len(self.options)
|
delta = 1 if forward else -1
|
||||||
|
self.offset = (self.offset + delta) % len(self.options)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
CompletionState = typing.NamedTuple(
|
class CompletionState(typing.NamedTuple):
|
||||||
"CompletionState",
|
completer: Completer
|
||||||
[
|
parsed: typing.Sequence[mitmproxy.command.ParseResult]
|
||||||
("completer", Completer),
|
|
||||||
("parse", typing.Sequence[mitmproxy.command.ParseResult])
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CommandBuffer:
|
class CommandBuffer:
|
||||||
def __init__(self, master: mitmproxy.master.Master, start: str = "") -> None:
|
def __init__(self, master: mitmproxy.master.Master, start: str = "") -> None:
|
||||||
self.master = master
|
self.master = master
|
||||||
self.text = self.flatten(start)
|
self.text = start
|
||||||
# Cursor is always within the range [0:len(buffer)].
|
# Cursor is always within the range [0:len(buffer)].
|
||||||
self._cursor = len(self.text)
|
self._cursor = len(self.text)
|
||||||
self.completion: typing.Optional[CompletionState] = None
|
self.completion: typing.Optional[CompletionState] = None
|
||||||
@ -70,30 +67,14 @@ class CommandBuffer:
|
|||||||
else:
|
else:
|
||||||
self._cursor = x
|
self._cursor = x
|
||||||
|
|
||||||
def maybequote(self, value):
|
|
||||||
if " " in value and not value.startswith("\""):
|
|
||||||
return "\"%s\"" % value
|
|
||||||
return value
|
|
||||||
|
|
||||||
def parse_quoted(self, txt):
|
|
||||||
parts, remhelp = self.master.commands.parse_partial(txt)
|
|
||||||
for i, p in enumerate(parts):
|
|
||||||
parts[i] = mitmproxy.command.ParseResult(
|
|
||||||
value = self.maybequote(p.value),
|
|
||||||
type = p.type,
|
|
||||||
valid = p.valid
|
|
||||||
)
|
|
||||||
return parts, remhelp
|
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
"""
|
parts, remaining = self.master.commands.parse_partial(self.text)
|
||||||
This function is somewhat tricky - in order to make the cursor
|
|
||||||
position valid, we have to make sure there is a
|
|
||||||
character-for-character offset match in the rendered output, up
|
|
||||||
to the cursor. Beyond that, we can add stuff.
|
|
||||||
"""
|
|
||||||
parts, remhelp = self.parse_quoted(self.text)
|
|
||||||
ret = []
|
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:
|
for p in parts:
|
||||||
if p.valid:
|
if p.valid:
|
||||||
if p.type == mitmproxy.types.Cmd:
|
if p.type == mitmproxy.types.Cmd:
|
||||||
@ -102,19 +83,14 @@ class CommandBuffer:
|
|||||||
ret.append(("text", p.value))
|
ret.append(("text", p.value))
|
||||||
elif p.value:
|
elif p.value:
|
||||||
ret.append(("commander_invalid", p.value))
|
ret.append(("commander_invalid", p.value))
|
||||||
else:
|
|
||||||
ret.append(("text", ""))
|
|
||||||
ret.append(("text", " "))
|
|
||||||
if remhelp:
|
|
||||||
ret.append(("text", " "))
|
|
||||||
for v in remhelp:
|
|
||||||
ret.append(("commander_hint", "%s " % v))
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def flatten(self, txt):
|
if remaining:
|
||||||
parts, _ = self.parse_quoted(txt)
|
if parts[-1].type != mitmproxy.types.Space:
|
||||||
ret = [x.value for x in parts]
|
ret.append(("text", " "))
|
||||||
return " ".join(ret)
|
for param in remaining:
|
||||||
|
ret.append(("commander_hint", f"{param} "))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
def left(self) -> None:
|
def left(self) -> None:
|
||||||
self.cursor = self.cursor - 1
|
self.cursor = self.cursor - 1
|
||||||
@ -122,30 +98,38 @@ class CommandBuffer:
|
|||||||
def right(self) -> None:
|
def right(self) -> None:
|
||||||
self.cursor = self.cursor + 1
|
self.cursor = self.cursor + 1
|
||||||
|
|
||||||
def cycle_completion(self) -> None:
|
def cycle_completion(self, forward: bool = True) -> None:
|
||||||
if not self.completion:
|
if not self.completion:
|
||||||
parts, remainhelp = self.master.commands.parse_partial(self.text[:self.cursor])
|
parts, remaining = self.master.commands.parse_partial(self.text[:self.cursor])
|
||||||
last = parts[-1]
|
if parts and parts[-1].type != mitmproxy.types.Space:
|
||||||
ct = mitmproxy.types.CommandTypes.get(last.type, None)
|
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:
|
if ct:
|
||||||
self.completion = CompletionState(
|
self.completion = CompletionState(
|
||||||
completer=ListCompleter(
|
completer=ListCompleter(
|
||||||
parts[-1].value,
|
cycle_prefix,
|
||||||
ct.completion(self.master.commands, last.type, parts[-1].value)
|
ct.completion(self.master.commands, type_to_complete, cycle_prefix)
|
||||||
),
|
),
|
||||||
parse = parts,
|
parsed=parsed,
|
||||||
)
|
)
|
||||||
if self.completion:
|
if self.completion:
|
||||||
nxt = self.completion.completer.cycle()
|
nxt = self.completion.completer.cycle(forward)
|
||||||
buf = " ".join([i.value for i in self.completion.parse[:-1]]) + " " + nxt
|
buf = "".join([i.value for i in self.completion.parsed]) + nxt
|
||||||
buf = buf.strip()
|
self.text = buf
|
||||||
self.text = self.flatten(buf)
|
|
||||||
self.cursor = len(self.text)
|
self.cursor = len(self.text)
|
||||||
|
|
||||||
def backspace(self) -> None:
|
def backspace(self) -> None:
|
||||||
if self.cursor == 0:
|
if self.cursor == 0:
|
||||||
return
|
return
|
||||||
self.text = self.flatten(self.text[:self.cursor - 1] + self.text[self.cursor:])
|
self.text = self.text[:self.cursor - 1] + self.text[self.cursor:]
|
||||||
self.cursor = self.cursor - 1
|
self.cursor = self.cursor - 1
|
||||||
self.completion = None
|
self.completion = None
|
||||||
|
|
||||||
@ -153,8 +137,13 @@ class CommandBuffer:
|
|||||||
"""
|
"""
|
||||||
Inserts text at the cursor.
|
Inserts text at the cursor.
|
||||||
"""
|
"""
|
||||||
self.text = self.flatten(self.text[:self.cursor] + k + self.text[self.cursor:])
|
|
||||||
self.cursor += 1
|
# 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
|
self.completion = None
|
||||||
|
|
||||||
|
|
||||||
@ -207,7 +196,7 @@ class CommandEdit(urwid.WidgetWrap):
|
|||||||
self.history = history
|
self.history = history
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key) -> None:
|
||||||
if key == "backspace":
|
if key == "backspace":
|
||||||
self.cbuf.backspace()
|
self.cbuf.backspace()
|
||||||
elif key == "left":
|
elif key == "left":
|
||||||
@ -219,27 +208,29 @@ class CommandEdit(urwid.WidgetWrap):
|
|||||||
self.cbuf = self.history.get_prev() or self.cbuf
|
self.cbuf = self.history.get_prev() or self.cbuf
|
||||||
elif key == "down":
|
elif key == "down":
|
||||||
self.cbuf = self.history.get_next() or self.cbuf
|
self.cbuf = self.history.get_next() or self.cbuf
|
||||||
|
elif key == "shift tab":
|
||||||
|
self.cbuf.cycle_completion(False)
|
||||||
elif key == "tab":
|
elif key == "tab":
|
||||||
self.cbuf.cycle_completion()
|
self.cbuf.cycle_completion()
|
||||||
elif len(key) == 1:
|
elif len(key) == 1:
|
||||||
self.cbuf.insert(key)
|
self.cbuf.insert(key)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def update(self):
|
def update(self) -> None:
|
||||||
self._w.set_text([self.leader, self.cbuf.render()])
|
self._w.set_text([self.leader, self.cbuf.render()])
|
||||||
|
|
||||||
def render(self, size, focus=False):
|
def render(self, size, focus=False) -> urwid.Canvas:
|
||||||
(maxcol,) = size
|
(maxcol,) = size
|
||||||
canv = self._w.render((maxcol,))
|
canv = self._w.render((maxcol,))
|
||||||
canv = urwid.CompositeCanvas(canv)
|
canv = urwid.CompositeCanvas(canv)
|
||||||
canv.cursor = self.get_cursor_coords((maxcol,))
|
canv.cursor = self.get_cursor_coords((maxcol,))
|
||||||
return canv
|
return canv
|
||||||
|
|
||||||
def get_cursor_coords(self, size):
|
def get_cursor_coords(self, size) -> typing.Tuple[int, int]:
|
||||||
p = self.cbuf.cursor + len(self.leader)
|
p = self.cbuf.cursor + len(self.leader)
|
||||||
trans = self._w.get_line_translation(size[0])
|
trans = self._w.get_line_translation(size[0])
|
||||||
x, y = calc_coords(self._w.get_text()[0], trans, p)
|
x, y = calc_coords(self._w.get_text()[0], trans, p)
|
||||||
return x, y
|
return x, y
|
||||||
|
|
||||||
def get_edit_text(self):
|
def get_edit_text(self) -> str:
|
||||||
return self.cbuf.text
|
return self.cbuf.text
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import urwid
|
import urwid
|
||||||
import blinker
|
import blinker
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
|
from mitmproxy import command
|
||||||
from mitmproxy.tools.console import layoutwidget
|
from mitmproxy.tools.console import layoutwidget
|
||||||
from mitmproxy.tools.console import signals
|
from mitmproxy.tools.console import signals
|
||||||
|
|
||||||
@ -10,7 +12,7 @@ command_focus_change = blinker.Signal()
|
|||||||
|
|
||||||
|
|
||||||
class CommandItem(urwid.WidgetWrap):
|
class CommandItem(urwid.WidgetWrap):
|
||||||
def __init__(self, walker, cmd, focused):
|
def __init__(self, walker, cmd: command.Command, focused: bool):
|
||||||
self.walker, self.cmd, self.focused = walker, cmd, focused
|
self.walker, self.cmd, self.focused = walker, cmd, focused
|
||||||
super().__init__(None)
|
super().__init__(None)
|
||||||
self._w = self.get_widget()
|
self._w = self.get_widget()
|
||||||
@ -18,15 +20,18 @@ class CommandItem(urwid.WidgetWrap):
|
|||||||
def get_widget(self):
|
def get_widget(self):
|
||||||
parts = [
|
parts = [
|
||||||
("focus", ">> " if self.focused else " "),
|
("focus", ">> " if self.focused else " "),
|
||||||
("title", self.cmd.path),
|
("title", self.cmd.name)
|
||||||
("text", " "),
|
|
||||||
("text", " ".join(self.cmd.paramnames())),
|
|
||||||
]
|
]
|
||||||
if self.cmd.returntype:
|
if self.cmd.parameters:
|
||||||
parts.append([
|
parts += [
|
||||||
|
("text", " "),
|
||||||
|
("text", " ".join(str(param) for param in self.cmd.parameters)),
|
||||||
|
]
|
||||||
|
if self.cmd.return_type:
|
||||||
|
parts += [
|
||||||
("title", " -> "),
|
("title", " -> "),
|
||||||
("text", self.cmd.retname()),
|
("text", command.typename(self.cmd.return_type)),
|
||||||
])
|
]
|
||||||
|
|
||||||
return urwid.AttrMap(
|
return urwid.AttrMap(
|
||||||
urwid.Padding(urwid.Text(parts)),
|
urwid.Padding(urwid.Text(parts)),
|
||||||
@ -92,7 +97,7 @@ class CommandsList(urwid.ListBox):
|
|||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
if key == "m_select":
|
if key == "m_select":
|
||||||
foc, idx = self.get_focus()
|
foc, idx = self.get_focus()
|
||||||
signals.status_prompt_command.send(partial=foc.cmd.path + " ")
|
signals.status_prompt_command.send(partial=foc.cmd.name + " ")
|
||||||
elif key == "m_start":
|
elif key == "m_start":
|
||||||
self.set_focus(0)
|
self.set_focus(0)
|
||||||
self.walker._modified()
|
self.walker._modified()
|
||||||
|
@ -1,21 +1,18 @@
|
|||||||
import csv
|
import csv
|
||||||
import shlex
|
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
import mitmproxy.types
|
||||||
|
from mitmproxy import command, command_lexer
|
||||||
|
from mitmproxy import contentviews
|
||||||
from mitmproxy import ctx
|
from mitmproxy import ctx
|
||||||
from mitmproxy import command
|
|
||||||
from mitmproxy import exceptions
|
from mitmproxy import exceptions
|
||||||
from mitmproxy import flow
|
from mitmproxy import flow
|
||||||
from mitmproxy import http
|
from mitmproxy import http
|
||||||
from mitmproxy import log
|
from mitmproxy import log
|
||||||
from mitmproxy import contentviews
|
from mitmproxy.tools.console import keymap
|
||||||
from mitmproxy.utils import strutils
|
|
||||||
import mitmproxy.types
|
|
||||||
|
|
||||||
|
|
||||||
from mitmproxy.tools.console import overlay
|
from mitmproxy.tools.console import overlay
|
||||||
from mitmproxy.tools.console import signals
|
from mitmproxy.tools.console import signals
|
||||||
from mitmproxy.tools.console import keymap
|
from mitmproxy.utils import strutils
|
||||||
|
|
||||||
console_palettes = [
|
console_palettes = [
|
||||||
"lowlight",
|
"lowlight",
|
||||||
@ -48,10 +45,12 @@ class UnsupportedLog:
|
|||||||
"""
|
"""
|
||||||
A small addon to dump info on flow types we don't support yet.
|
A small addon to dump info on flow types we don't support yet.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def websocket_message(self, f):
|
def websocket_message(self, f):
|
||||||
message = f.messages[-1]
|
message = f.messages[-1]
|
||||||
ctx.log.info(f.message_info(message))
|
ctx.log.info(f.message_info(message))
|
||||||
ctx.log.debug(message.content if isinstance(message.content, str) else strutils.bytes_to_escaped_str(message.content))
|
ctx.log.debug(
|
||||||
|
message.content if isinstance(message.content, str) else strutils.bytes_to_escaped_str(message.content))
|
||||||
|
|
||||||
def websocket_end(self, f):
|
def websocket_end(self, f):
|
||||||
ctx.log.info("WebSocket connection closed by {}: {} {}, {}".format(
|
ctx.log.info("WebSocket connection closed by {}: {} {}, {}".format(
|
||||||
@ -78,6 +77,7 @@ class ConsoleAddon:
|
|||||||
An addon that exposes console-specific commands, and hooks into required
|
An addon that exposes console-specific commands, and hooks into required
|
||||||
events.
|
events.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, master):
|
def __init__(self, master):
|
||||||
self.master = master
|
self.master = master
|
||||||
self.started = False
|
self.started = False
|
||||||
@ -238,13 +238,14 @@ class ConsoleAddon:
|
|||||||
prompt: str,
|
prompt: str,
|
||||||
choices: typing.Sequence[str],
|
choices: typing.Sequence[str],
|
||||||
cmd: mitmproxy.types.Cmd,
|
cmd: mitmproxy.types.Cmd,
|
||||||
*args: mitmproxy.types.Arg
|
*args: mitmproxy.types.CmdArgs
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Prompt the user to choose from a specified list of strings, then
|
Prompt the user to choose from a specified list of strings, then
|
||||||
invoke another command with all occurrences of {choice} replaced by
|
invoke another command with all occurrences of {choice} replaced by
|
||||||
the choice the user made.
|
the choice the user made.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def callback(opt):
|
def callback(opt):
|
||||||
# We're now outside of the call context...
|
# We're now outside of the call context...
|
||||||
repl = cmd + " " + " ".join(args)
|
repl = cmd + " " + " ".join(args)
|
||||||
@ -264,18 +265,18 @@ class ConsoleAddon:
|
|||||||
prompt: str,
|
prompt: str,
|
||||||
choicecmd: mitmproxy.types.Cmd,
|
choicecmd: mitmproxy.types.Cmd,
|
||||||
subcmd: mitmproxy.types.Cmd,
|
subcmd: mitmproxy.types.Cmd,
|
||||||
*args: mitmproxy.types.Arg
|
*args: mitmproxy.types.CmdArgs
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Prompt the user to choose from a list of strings returned by a
|
Prompt the user to choose from a list of strings returned by a
|
||||||
command, then invoke another command with all occurrences of {choice}
|
command, then invoke another command with all occurrences of {choice}
|
||||||
replaced by the choice the user made.
|
replaced by the choice the user made.
|
||||||
"""
|
"""
|
||||||
choices = ctx.master.commands.call_strings(choicecmd, [])
|
choices = ctx.master.commands.execute(choicecmd)
|
||||||
|
|
||||||
def callback(opt):
|
def callback(opt):
|
||||||
# We're now outside of the call context...
|
# We're now outside of the call context...
|
||||||
repl = shlex.quote(" ".join(args))
|
repl = " ".join(command_lexer.quote(x) for x in args)
|
||||||
repl = repl.replace("{choice}", opt)
|
repl = repl.replace("{choice}", opt)
|
||||||
try:
|
try:
|
||||||
self.master.commands.execute(subcmd + " " + repl)
|
self.master.commands.execute(subcmd + " " + repl)
|
||||||
@ -287,21 +288,24 @@ class ConsoleAddon:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@command.command("console.command")
|
@command.command("console.command")
|
||||||
def console_command(self, *partial: str) -> None:
|
def console_command(self, *command_str: str) -> None:
|
||||||
"""
|
"""
|
||||||
Prompt the user to edit a command with a (possibly empty) starting value.
|
Prompt the user to edit a command with a (possibly empty) starting value.
|
||||||
"""
|
"""
|
||||||
signals.status_prompt_command.send(partial=" ".join(partial)) # type: ignore
|
quoted = " ".join(command_lexer.quote(x) for x in command_str)
|
||||||
|
signals.status_prompt_command.send(partial=quoted)
|
||||||
|
|
||||||
@command.command("console.command.set")
|
@command.command("console.command.set")
|
||||||
def console_command_set(self, option: str) -> None:
|
def console_command_set(self, option_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Prompt the user to set an option of the form "key[=value]".
|
Prompt the user to set an option.
|
||||||
"""
|
"""
|
||||||
option_value = getattr(self.master.options, option, None)
|
option_value = getattr(self.master.options, option_name, None) or ""
|
||||||
current_value = option_value if option_value else ""
|
set_command = f"set {option_name} {option_value!r}"
|
||||||
self.master.commands.execute(
|
cursor = len(set_command) - 1
|
||||||
"console.command set %s=%s" % (option, current_value)
|
signals.status_prompt_command.send(
|
||||||
|
partial=set_command,
|
||||||
|
cursor=cursor
|
||||||
)
|
)
|
||||||
|
|
||||||
@command.command("console.view.keybindings")
|
@command.command("console.view.keybindings")
|
||||||
@ -351,14 +355,14 @@ class ConsoleAddon:
|
|||||||
|
|
||||||
@command.command("console.bodyview")
|
@command.command("console.bodyview")
|
||||||
@command.argument("part", type=mitmproxy.types.Choice("console.bodyview.options"))
|
@command.argument("part", type=mitmproxy.types.Choice("console.bodyview.options"))
|
||||||
def bodyview(self, f: flow.Flow, part: str) -> None:
|
def bodyview(self, flow: flow.Flow, part: str) -> None:
|
||||||
"""
|
"""
|
||||||
Spawn an external viewer for a flow request or response body based
|
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
|
on the detected MIME type. We use the mailcap system to find the
|
||||||
correct viewier, and fall back to the programs in $PAGER or $EDITOR
|
correct viewier, and fall back to the programs in $PAGER or $EDITOR
|
||||||
if necessary.
|
if necessary.
|
||||||
"""
|
"""
|
||||||
fpart = getattr(f, part, None)
|
fpart = getattr(flow, part, None)
|
||||||
if not fpart:
|
if not fpart:
|
||||||
raise exceptions.CommandError("Part must be either request or response, not %s." % part)
|
raise exceptions.CommandError("Part must be either request or response, not %s." % part)
|
||||||
t = fpart.headers.get("content-type")
|
t = fpart.headers.get("content-type")
|
||||||
@ -397,8 +401,8 @@ class ConsoleAddon:
|
|||||||
]
|
]
|
||||||
|
|
||||||
@command.command("console.edit.focus")
|
@command.command("console.edit.focus")
|
||||||
@command.argument("part", type=mitmproxy.types.Choice("console.edit.focus.options"))
|
@command.argument("flow_part", type=mitmproxy.types.Choice("console.edit.focus.options"))
|
||||||
def edit_focus(self, part: str) -> None:
|
def edit_focus(self, flow_part: str) -> None:
|
||||||
"""
|
"""
|
||||||
Edit a component of the currently focused flow.
|
Edit a component of the currently focused flow.
|
||||||
"""
|
"""
|
||||||
@ -410,27 +414,27 @@ class ConsoleAddon:
|
|||||||
flow.backup()
|
flow.backup()
|
||||||
|
|
||||||
require_dummy_response = (
|
require_dummy_response = (
|
||||||
part in ("response-headers", "response-body", "set-cookies") and
|
flow_part in ("response-headers", "response-body", "set-cookies") and
|
||||||
flow.response is None
|
flow.response is None
|
||||||
)
|
)
|
||||||
if require_dummy_response:
|
if require_dummy_response:
|
||||||
flow.response = http.HTTPResponse.make()
|
flow.response = http.HTTPResponse.make()
|
||||||
if part == "cookies":
|
if flow_part == "cookies":
|
||||||
self.master.switch_view("edit_focus_cookies")
|
self.master.switch_view("edit_focus_cookies")
|
||||||
elif part == "urlencoded form":
|
elif flow_part == "urlencoded form":
|
||||||
self.master.switch_view("edit_focus_urlencoded_form")
|
self.master.switch_view("edit_focus_urlencoded_form")
|
||||||
elif part == "multipart form":
|
elif flow_part == "multipart form":
|
||||||
self.master.switch_view("edit_focus_multipart_form")
|
self.master.switch_view("edit_focus_multipart_form")
|
||||||
elif part == "path":
|
elif flow_part == "path":
|
||||||
self.master.switch_view("edit_focus_path")
|
self.master.switch_view("edit_focus_path")
|
||||||
elif part == "query":
|
elif flow_part == "query":
|
||||||
self.master.switch_view("edit_focus_query")
|
self.master.switch_view("edit_focus_query")
|
||||||
elif part == "request-headers":
|
elif flow_part == "request-headers":
|
||||||
self.master.switch_view("edit_focus_request_headers")
|
self.master.switch_view("edit_focus_request_headers")
|
||||||
elif part == "response-headers":
|
elif flow_part == "response-headers":
|
||||||
self.master.switch_view("edit_focus_response_headers")
|
self.master.switch_view("edit_focus_response_headers")
|
||||||
elif part in ("request-body", "response-body"):
|
elif flow_part in ("request-body", "response-body"):
|
||||||
if part == "request-body":
|
if flow_part == "request-body":
|
||||||
message = flow.request
|
message = flow.request
|
||||||
else:
|
else:
|
||||||
message = flow.response
|
message = flow.response
|
||||||
@ -442,16 +446,16 @@ class ConsoleAddon:
|
|||||||
# just strip the newlines off the end of the body when we return
|
# just strip the newlines off the end of the body when we return
|
||||||
# from an editor.
|
# from an editor.
|
||||||
message.content = c.rstrip(b"\n")
|
message.content = c.rstrip(b"\n")
|
||||||
elif part == "set-cookies":
|
elif flow_part == "set-cookies":
|
||||||
self.master.switch_view("edit_focus_setcookies")
|
self.master.switch_view("edit_focus_setcookies")
|
||||||
elif part == "url":
|
elif flow_part == "url":
|
||||||
url = flow.request.url.encode()
|
url = flow.request.url.encode()
|
||||||
edited_url = self.master.spawn_editor(url)
|
edited_url = self.master.spawn_editor(url)
|
||||||
url = edited_url.rstrip(b"\n")
|
url = edited_url.rstrip(b"\n")
|
||||||
flow.request.url = url.decode()
|
flow.request.url = url.decode()
|
||||||
elif part in ["method", "status_code", "reason"]:
|
elif flow_part in ["method", "status_code", "reason"]:
|
||||||
self.master.commands.execute(
|
self.master.commands.execute(
|
||||||
"console.command flow.set @focus %s " % part
|
"console.command flow.set @focus %s " % flow_part
|
||||||
)
|
)
|
||||||
|
|
||||||
def _grideditor(self):
|
def _grideditor(self):
|
||||||
@ -535,10 +539,8 @@ class ConsoleAddon:
|
|||||||
raise exceptions.CommandError("Invalid flowview mode.")
|
raise exceptions.CommandError("Invalid flowview mode.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.master.commands.call_strings(
|
cmd = 'view.settings.setval @focus flowview_mode_%s %s' % (idx, mode)
|
||||||
"view.settings.setval",
|
self.master.commands.execute(cmd)
|
||||||
["@focus", "flowview_mode_%s" % idx, mode]
|
|
||||||
)
|
|
||||||
except exceptions.CommandError as e:
|
except exceptions.CommandError as e:
|
||||||
signals.status_message.send(message=str(e))
|
signals.status_message.send(message=str(e))
|
||||||
|
|
||||||
@ -558,14 +560,9 @@ class ConsoleAddon:
|
|||||||
if not fv:
|
if not fv:
|
||||||
raise exceptions.CommandError("Not viewing a flow.")
|
raise exceptions.CommandError("Not viewing a flow.")
|
||||||
idx = fv.body.tab_offset
|
idx = fv.body.tab_offset
|
||||||
return self.master.commands.call_strings(
|
|
||||||
"view.settings.getval",
|
cmd = 'view.settings.getval @focus flowview_mode_%s %s' % (idx, self.master.options.console_default_contentview)
|
||||||
[
|
return self.master.commands.execute(cmd)
|
||||||
"@focus",
|
|
||||||
"flowview_mode_%s" % idx,
|
|
||||||
self.master.options.console_default_contentview,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@command.command("console.key.contexts")
|
@command.command("console.key.contexts")
|
||||||
def key_contexts(self) -> typing.Sequence[str]:
|
def key_contexts(self) -> typing.Sequence[str]:
|
||||||
@ -580,7 +577,7 @@ class ConsoleAddon:
|
|||||||
contexts: typing.Sequence[str],
|
contexts: typing.Sequence[str],
|
||||||
key: str,
|
key: str,
|
||||||
cmd: mitmproxy.types.Cmd,
|
cmd: mitmproxy.types.Cmd,
|
||||||
*args: mitmproxy.types.Arg
|
*args: mitmproxy.types.CmdArgs
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Bind a shortcut key.
|
Bind a shortcut key.
|
||||||
|
@ -26,7 +26,7 @@ def map(km):
|
|||||||
km.add("ctrl f", "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("ctrl b", "console.nav.pageup", ["global"], "Page up")
|
||||||
|
|
||||||
km.add("I", "set intercept_active=toggle", ["global"], "Toggle intercept")
|
km.add("I", "set intercept_active toggle", ["global"], "Toggle intercept")
|
||||||
km.add("i", "console.command.set intercept", ["global"], "Set intercept")
|
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("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 @all", ["flowlist", "flowview"], "Resume all intercepted flows")
|
||||||
@ -48,14 +48,14 @@ def map(km):
|
|||||||
"Export this flow to file"
|
"Export this flow to file"
|
||||||
)
|
)
|
||||||
km.add("f", "console.command.set view_filter", ["flowlist"], "Set view filter")
|
km.add("f", "console.command.set view_filter", ["flowlist"], "Set view filter")
|
||||||
km.add("F", "set console_focus_follow=toggle", ["flowlist"], "Set focus follow")
|
km.add("F", "set console_focus_follow toggle", ["flowlist"], "Set focus follow")
|
||||||
km.add(
|
km.add(
|
||||||
"ctrl l",
|
"ctrl l",
|
||||||
"console.command cut.clip ",
|
"console.command cut.clip ",
|
||||||
["flowlist", "flowview"],
|
["flowlist", "flowview"],
|
||||||
"Send cuts to clipboard"
|
"Send cuts to clipboard"
|
||||||
)
|
)
|
||||||
km.add("L", "console.command view.load ", ["flowlist"], "Load flows from file")
|
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", "flow.mark.toggle @focus", ["flowlist"], "Toggle mark on this flow")
|
||||||
km.add("M", "view.properties.marked.toggle", ["flowlist"], "Toggle viewing marked flows")
|
km.add("M", "view.properties.marked.toggle", ["flowlist"], "Toggle viewing marked flows")
|
||||||
km.add(
|
km.add(
|
||||||
@ -68,14 +68,14 @@ def map(km):
|
|||||||
"o",
|
"o",
|
||||||
"""
|
"""
|
||||||
console.choose.cmd Order view.order.options
|
console.choose.cmd Order view.order.options
|
||||||
set view_order={choice}
|
set view_order {choice}
|
||||||
""",
|
""",
|
||||||
["flowlist"],
|
["flowlist"],
|
||||||
"Set flow list order"
|
"Set flow list order"
|
||||||
)
|
)
|
||||||
km.add("r", "replay.client @focus", ["flowlist", "flowview"], "Replay this flow")
|
km.add("r", "replay.client @focus", ["flowlist", "flowview"], "Replay this flow")
|
||||||
km.add("S", "console.command replay.server ", ["flowlist"], "Start server replay")
|
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("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("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("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("V", "flow.revert @focus", ["flowlist", "flowview"], "Revert changes to this flow")
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import os.path
|
import os.path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
@ -98,10 +99,15 @@ class ActionBar(urwid.WidgetWrap):
|
|||||||
self._w = urwid.Edit(self.prep_prompt(prompt), text or "")
|
self._w = urwid.Edit(self.prep_prompt(prompt), text or "")
|
||||||
self.prompting = PromptStub(callback, args)
|
self.prompting = PromptStub(callback, args)
|
||||||
|
|
||||||
def sig_prompt_command(self, sender, partial=""):
|
def sig_prompt_command(self, sender, partial: str = "", cursor: Optional[int] = None):
|
||||||
signals.focus.send(self, section="footer")
|
signals.focus.send(self, section="footer")
|
||||||
self._w = commander.CommandEdit(self.master, partial,
|
self._w = commander.CommandEdit(
|
||||||
self.command_history)
|
self.master,
|
||||||
|
partial,
|
||||||
|
self.command_history,
|
||||||
|
)
|
||||||
|
if cursor is not None:
|
||||||
|
self._w.cbuf.cursor = cursor
|
||||||
self.prompting = commandexecutor.CommandExecutor(self.master)
|
self.prompting = commandexecutor.CommandExecutor(self.master)
|
||||||
|
|
||||||
def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()):
|
def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()):
|
||||||
|
@ -5,6 +5,9 @@ import typing
|
|||||||
from mitmproxy import exceptions
|
from mitmproxy import exceptions
|
||||||
from mitmproxy import flow
|
from mitmproxy import flow
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from mitmproxy.command import CommandManager
|
||||||
|
|
||||||
|
|
||||||
class Path(str):
|
class Path(str):
|
||||||
pass
|
pass
|
||||||
@ -14,7 +17,7 @@ class Cmd(str):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Arg(str):
|
class CmdArgs(str):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -22,6 +25,10 @@ class Unknown(str):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Space(str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CutSpec(typing.Sequence[str]):
|
class CutSpec(typing.Sequence[str]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -40,27 +47,11 @@ class Choice:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# One of the many charming things about mypy is that introducing type
|
|
||||||
# annotations can cause circular dependencies where there were none before.
|
|
||||||
# Rather than putting types and the CommandManger in the same file, we introduce
|
|
||||||
# a stub type with the signature we use.
|
|
||||||
class _CommandBase:
|
|
||||||
commands: typing.MutableMapping[str, typing.Any] = {}
|
|
||||||
|
|
||||||
def call_strings(self, path: str, args: typing.Sequence[str]) -> typing.Any:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def execute(self, cmd: str) -> typing.Any:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class _BaseType:
|
class _BaseType:
|
||||||
typ: typing.Type = object
|
typ: typing.Type = object
|
||||||
display: str = ""
|
display: str = ""
|
||||||
|
|
||||||
def completion(
|
def completion(self, manager: "CommandManager", t: typing.Any, s: str) -> typing.Sequence[str]:
|
||||||
self, manager: _CommandBase, t: typing.Any, s: str
|
|
||||||
) -> typing.Sequence[str]:
|
|
||||||
"""
|
"""
|
||||||
Returns a list of completion strings for a given prefix. The strings
|
Returns a list of completion strings for a given prefix. The strings
|
||||||
returned don't necessarily need to be suffixes of the prefix, since
|
returned don't necessarily need to be suffixes of the prefix, since
|
||||||
@ -68,9 +59,7 @@ class _BaseType:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def parse(
|
def parse(self, manager: "CommandManager", typ: typing.Any, s: str) -> typing.Any:
|
||||||
self, manager: _CommandBase, typ: typing.Any, s: str
|
|
||||||
) -> typing.Any:
|
|
||||||
"""
|
"""
|
||||||
Parse a string, given the specific type instance (to allow rich type annotations like Choice) and a string.
|
Parse a string, given the specific type instance (to allow rich type annotations like Choice) and a string.
|
||||||
|
|
||||||
@ -78,7 +67,7 @@ class _BaseType:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if data is valid for this type.
|
Check if data is valid for this type.
|
||||||
"""
|
"""
|
||||||
@ -89,10 +78,10 @@ class _BoolType(_BaseType):
|
|||||||
typ = bool
|
typ = bool
|
||||||
display = "bool"
|
display = "bool"
|
||||||
|
|
||||||
def completion(self, manager: _CommandBase, t: type, s: str) -> typing.Sequence[str]:
|
def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]:
|
||||||
return ["false", "true"]
|
return ["false", "true"]
|
||||||
|
|
||||||
def parse(self, manager: _CommandBase, t: type, s: str) -> bool:
|
def parse(self, manager: "CommandManager", t: type, s: str) -> bool:
|
||||||
if s == "true":
|
if s == "true":
|
||||||
return True
|
return True
|
||||||
elif s == "false":
|
elif s == "false":
|
||||||
@ -102,7 +91,7 @@ class _BoolType(_BaseType):
|
|||||||
"Booleans are 'true' or 'false', got %s" % s
|
"Booleans are 'true' or 'false', got %s" % s
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
return val in [True, False]
|
return val in [True, False]
|
||||||
|
|
||||||
|
|
||||||
@ -110,13 +99,13 @@ class _StrType(_BaseType):
|
|||||||
typ = str
|
typ = str
|
||||||
display = "str"
|
display = "str"
|
||||||
|
|
||||||
def completion(self, manager: _CommandBase, t: type, s: str) -> typing.Sequence[str]:
|
def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def parse(self, manager: _CommandBase, t: type, s: str) -> str:
|
def parse(self, manager: "CommandManager", t: type, s: str) -> str:
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
return isinstance(val, str)
|
return isinstance(val, str)
|
||||||
|
|
||||||
|
|
||||||
@ -124,13 +113,13 @@ class _UnknownType(_BaseType):
|
|||||||
typ = Unknown
|
typ = Unknown
|
||||||
display = "unknown"
|
display = "unknown"
|
||||||
|
|
||||||
def completion(self, manager: _CommandBase, t: type, s: str) -> typing.Sequence[str]:
|
def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def parse(self, manager: _CommandBase, t: type, s: str) -> str:
|
def parse(self, manager: "CommandManager", t: type, s: str) -> str:
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -138,16 +127,16 @@ class _IntType(_BaseType):
|
|||||||
typ = int
|
typ = int
|
||||||
display = "int"
|
display = "int"
|
||||||
|
|
||||||
def completion(self, manager: _CommandBase, t: type, s: str) -> typing.Sequence[str]:
|
def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def parse(self, manager: _CommandBase, t: type, s: str) -> int:
|
def parse(self, manager: "CommandManager", t: type, s: str) -> int:
|
||||||
try:
|
try:
|
||||||
return int(s)
|
return int(s)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise exceptions.TypeError from e
|
raise exceptions.TypeError from e
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
return isinstance(val, int)
|
return isinstance(val, int)
|
||||||
|
|
||||||
|
|
||||||
@ -155,7 +144,7 @@ class _PathType(_BaseType):
|
|||||||
typ = Path
|
typ = Path
|
||||||
display = "path"
|
display = "path"
|
||||||
|
|
||||||
def completion(self, manager: _CommandBase, t: type, start: str) -> typing.Sequence[str]:
|
def completion(self, manager: "CommandManager", t: type, start: str) -> typing.Sequence[str]:
|
||||||
if not start:
|
if not start:
|
||||||
start = "./"
|
start = "./"
|
||||||
path = os.path.expanduser(start)
|
path = os.path.expanduser(start)
|
||||||
@ -177,10 +166,10 @@ class _PathType(_BaseType):
|
|||||||
ret.sort()
|
ret.sort()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def parse(self, manager: _CommandBase, t: type, s: str) -> str:
|
def parse(self, manager: "CommandManager", t: type, s: str) -> str:
|
||||||
return os.path.expanduser(s)
|
return os.path.expanduser(s)
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
return isinstance(val, str)
|
return isinstance(val, str)
|
||||||
|
|
||||||
|
|
||||||
@ -188,43 +177,43 @@ class _CmdType(_BaseType):
|
|||||||
typ = Cmd
|
typ = Cmd
|
||||||
display = "cmd"
|
display = "cmd"
|
||||||
|
|
||||||
def completion(self, manager: _CommandBase, t: type, s: str) -> typing.Sequence[str]:
|
def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]:
|
||||||
return list(manager.commands.keys())
|
return list(manager.commands.keys())
|
||||||
|
|
||||||
def parse(self, manager: _CommandBase, t: type, s: str) -> str:
|
def parse(self, manager: "CommandManager", t: type, s: str) -> str:
|
||||||
if s not in manager.commands:
|
if s not in manager.commands:
|
||||||
raise exceptions.TypeError("Unknown command: %s" % s)
|
raise exceptions.TypeError("Unknown command: %s" % s)
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
return val in manager.commands
|
return val in manager.commands
|
||||||
|
|
||||||
|
|
||||||
class _ArgType(_BaseType):
|
class _ArgType(_BaseType):
|
||||||
typ = Arg
|
typ = CmdArgs
|
||||||
display = "arg"
|
display = "arg"
|
||||||
|
|
||||||
def completion(self, manager: _CommandBase, t: type, s: str) -> typing.Sequence[str]:
|
def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def parse(self, manager: _CommandBase, t: type, s: str) -> str:
|
def parse(self, manager: "CommandManager", t: type, s: str) -> str:
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
return isinstance(val, str)
|
return isinstance(val, str)
|
||||||
|
|
||||||
|
|
||||||
class _StrSeqType(_BaseType):
|
class _StrSeqType(_BaseType):
|
||||||
typ = typing.Sequence[str]
|
typ = typing.Sequence[str]
|
||||||
display = "[str]"
|
display = "str[]"
|
||||||
|
|
||||||
def completion(self, manager: _CommandBase, t: type, s: str) -> typing.Sequence[str]:
|
def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def parse(self, manager: _CommandBase, t: type, s: str) -> typing.Sequence[str]:
|
def parse(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]:
|
||||||
return [x.strip() for x in s.split(",")]
|
return [x.strip() for x in s.split(",")]
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
if isinstance(val, str) or isinstance(val, bytes):
|
if isinstance(val, str) or isinstance(val, bytes):
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
@ -238,7 +227,7 @@ class _StrSeqType(_BaseType):
|
|||||||
|
|
||||||
class _CutSpecType(_BaseType):
|
class _CutSpecType(_BaseType):
|
||||||
typ = CutSpec
|
typ = CutSpec
|
||||||
display = "[cut]"
|
display = "cut[]"
|
||||||
valid_prefixes = [
|
valid_prefixes = [
|
||||||
"request.method",
|
"request.method",
|
||||||
"request.scheme",
|
"request.scheme",
|
||||||
@ -277,7 +266,7 @@ class _CutSpecType(_BaseType):
|
|||||||
"server_conn.tls_established",
|
"server_conn.tls_established",
|
||||||
]
|
]
|
||||||
|
|
||||||
def completion(self, manager: _CommandBase, t: type, s: str) -> typing.Sequence[str]:
|
def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]:
|
||||||
spec = s.split(",")
|
spec = s.split(",")
|
||||||
opts = []
|
opts = []
|
||||||
for pref in self.valid_prefixes:
|
for pref in self.valid_prefixes:
|
||||||
@ -285,11 +274,11 @@ class _CutSpecType(_BaseType):
|
|||||||
opts.append(",".join(spec))
|
opts.append(",".join(spec))
|
||||||
return opts
|
return opts
|
||||||
|
|
||||||
def parse(self, manager: _CommandBase, t: type, s: str) -> CutSpec:
|
def parse(self, manager: "CommandManager", t: type, s: str) -> CutSpec:
|
||||||
parts: typing.Any = s.split(",")
|
parts: typing.Any = s.split(",")
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
if not isinstance(val, str):
|
if not isinstance(val, str):
|
||||||
return False
|
return False
|
||||||
parts = [x.strip() for x in val.split(",")]
|
parts = [x.strip() for x in val.split(",")]
|
||||||
@ -327,7 +316,7 @@ class _BaseFlowType(_BaseType):
|
|||||||
"~c",
|
"~c",
|
||||||
]
|
]
|
||||||
|
|
||||||
def completion(self, manager: _CommandBase, t: type, s: str) -> typing.Sequence[str]:
|
def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]:
|
||||||
return self.valid_prefixes
|
return self.valid_prefixes
|
||||||
|
|
||||||
|
|
||||||
@ -335,9 +324,9 @@ class _FlowType(_BaseFlowType):
|
|||||||
typ = flow.Flow
|
typ = flow.Flow
|
||||||
display = "flow"
|
display = "flow"
|
||||||
|
|
||||||
def parse(self, manager: _CommandBase, t: type, s: str) -> flow.Flow:
|
def parse(self, manager: "CommandManager", t: type, s: str) -> flow.Flow:
|
||||||
try:
|
try:
|
||||||
flows = manager.call_strings("view.flows.resolve", [s])
|
flows = manager.execute("view.flows.resolve %s" % (s))
|
||||||
except exceptions.CommandError as e:
|
except exceptions.CommandError as e:
|
||||||
raise exceptions.TypeError from e
|
raise exceptions.TypeError from e
|
||||||
if len(flows) != 1:
|
if len(flows) != 1:
|
||||||
@ -346,21 +335,21 @@ class _FlowType(_BaseFlowType):
|
|||||||
)
|
)
|
||||||
return flows[0]
|
return flows[0]
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
return isinstance(val, flow.Flow)
|
return isinstance(val, flow.Flow)
|
||||||
|
|
||||||
|
|
||||||
class _FlowsType(_BaseFlowType):
|
class _FlowsType(_BaseFlowType):
|
||||||
typ = typing.Sequence[flow.Flow]
|
typ = typing.Sequence[flow.Flow]
|
||||||
display = "[flow]"
|
display = "flow[]"
|
||||||
|
|
||||||
def parse(self, manager: _CommandBase, t: type, s: str) -> typing.Sequence[flow.Flow]:
|
def parse(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[flow.Flow]:
|
||||||
try:
|
try:
|
||||||
return manager.call_strings("view.flows.resolve", [s])
|
return manager.execute("view.flows.resolve %s" % (s))
|
||||||
except exceptions.CommandError as e:
|
except exceptions.CommandError as e:
|
||||||
raise exceptions.TypeError from e
|
raise exceptions.TypeError from e
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
try:
|
try:
|
||||||
for v in val:
|
for v in val:
|
||||||
if not isinstance(v, flow.Flow):
|
if not isinstance(v, flow.Flow):
|
||||||
@ -372,19 +361,19 @@ class _FlowsType(_BaseFlowType):
|
|||||||
|
|
||||||
class _DataType(_BaseType):
|
class _DataType(_BaseType):
|
||||||
typ = Data
|
typ = Data
|
||||||
display = "[data]"
|
display = "data[][]"
|
||||||
|
|
||||||
def completion(
|
def completion(
|
||||||
self, manager: _CommandBase, t: type, s: str
|
self, manager: "CommandManager", t: type, s: str
|
||||||
) -> typing.Sequence[str]: # pragma: no cover
|
) -> typing.Sequence[str]: # pragma: no cover
|
||||||
raise exceptions.TypeError("data cannot be passed as argument")
|
raise exceptions.TypeError("data cannot be passed as argument")
|
||||||
|
|
||||||
def parse(
|
def parse(
|
||||||
self, manager: _CommandBase, t: type, s: str
|
self, manager: "CommandManager", t: type, s: str
|
||||||
) -> typing.Any: # pragma: no cover
|
) -> typing.Any: # pragma: no cover
|
||||||
raise exceptions.TypeError("data cannot be passed as argument")
|
raise exceptions.TypeError("data cannot be passed as argument")
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
# FIXME: validate that all rows have equal length, and all columns have equal types
|
# FIXME: validate that all rows have equal length, and all columns have equal types
|
||||||
try:
|
try:
|
||||||
for row in val:
|
for row in val:
|
||||||
@ -400,16 +389,16 @@ class _ChoiceType(_BaseType):
|
|||||||
typ = Choice
|
typ = Choice
|
||||||
display = "choice"
|
display = "choice"
|
||||||
|
|
||||||
def completion(self, manager: _CommandBase, t: Choice, s: str) -> typing.Sequence[str]:
|
def completion(self, manager: "CommandManager", t: Choice, s: str) -> typing.Sequence[str]:
|
||||||
return manager.execute(t.options_command)
|
return manager.execute(t.options_command)
|
||||||
|
|
||||||
def parse(self, manager: _CommandBase, t: Choice, s: str) -> str:
|
def parse(self, manager: "CommandManager", t: Choice, s: str) -> str:
|
||||||
opts = manager.execute(t.options_command)
|
opts = manager.execute(t.options_command)
|
||||||
if s not in opts:
|
if s not in opts:
|
||||||
raise exceptions.TypeError("Invalid choice.")
|
raise exceptions.TypeError("Invalid choice.")
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
|
def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool:
|
||||||
try:
|
try:
|
||||||
opts = manager.execute(typ.options_command)
|
opts = manager.execute(typ.options_command)
|
||||||
except exceptions.CommandError:
|
except exceptions.CommandError:
|
||||||
@ -423,7 +412,7 @@ class TypeManager:
|
|||||||
for t in types:
|
for t in types:
|
||||||
self.typemap[t.typ] = t()
|
self.typemap[t.typ] = t()
|
||||||
|
|
||||||
def get(self, t: typing.Optional[typing.Type], default=None) -> _BaseType:
|
def get(self, t: typing.Optional[typing.Type], default=None) -> typing.Optional[_BaseType]:
|
||||||
if type(t) in self.typemap:
|
if type(t) in self.typemap:
|
||||||
return self.typemap[type(t)]
|
return self.typemap[type(t)]
|
||||||
return self.typemap.get(t, default)
|
return self.typemap.get(t, default)
|
||||||
|
@ -18,6 +18,8 @@ show_missing = True
|
|||||||
exclude_lines =
|
exclude_lines =
|
||||||
pragma: no cover
|
pragma: no cover
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
[mypy]
|
[mypy]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
@ -11,7 +11,7 @@ def test_set():
|
|||||||
sa = core.Core()
|
sa = core.Core()
|
||||||
with taddons.context(loadcore=False) as tctx:
|
with taddons.context(loadcore=False) as tctx:
|
||||||
assert tctx.master.options.server
|
assert tctx.master.options.server
|
||||||
tctx.command(sa.set, "server=false")
|
tctx.command(sa.set, "server", "false")
|
||||||
assert not tctx.master.options.server
|
assert not tctx.master.options.server
|
||||||
|
|
||||||
with pytest.raises(exceptions.CommandError):
|
with pytest.raises(exceptions.CommandError):
|
||||||
|
@ -73,7 +73,7 @@ def test_save_command(tmpdir):
|
|||||||
v = view.View()
|
v = view.View()
|
||||||
tctx.master.addons.add(v)
|
tctx.master.addons.add(v)
|
||||||
tctx.master.addons.add(sa)
|
tctx.master.addons.add(sa)
|
||||||
tctx.master.commands.call_strings("save.file", ["@shown", p])
|
tctx.master.commands.execute("save.file @shown %s" % p)
|
||||||
|
|
||||||
|
|
||||||
def test_simple(tmpdir):
|
def test_simple(tmpdir):
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import typing
|
|
||||||
import inspect
|
import inspect
|
||||||
from mitmproxy import command
|
|
||||||
from mitmproxy import flow
|
|
||||||
from mitmproxy import exceptions
|
|
||||||
from mitmproxy.test import tflow
|
|
||||||
from mitmproxy.test import taddons
|
|
||||||
import mitmproxy.types
|
|
||||||
import io
|
import io
|
||||||
|
import typing
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import mitmproxy.types
|
||||||
|
from mitmproxy import command
|
||||||
|
from mitmproxy import exceptions
|
||||||
|
from mitmproxy import flow
|
||||||
|
from mitmproxy.test import taddons
|
||||||
|
from mitmproxy.test import tflow
|
||||||
|
|
||||||
|
|
||||||
class TAddon:
|
class TAddon:
|
||||||
@command.command("cmd1")
|
@command.command("cmd1")
|
||||||
@ -29,7 +31,7 @@ class TAddon:
|
|||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
@command.command("subcommand")
|
@command.command("subcommand")
|
||||||
def subcommand(self, cmd: mitmproxy.types.Cmd, *args: mitmproxy.types.Arg) -> str:
|
def subcommand(self, cmd: mitmproxy.types.Cmd, *args: mitmproxy.types.CmdArgs) -> str:
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
@command.command("empty")
|
@command.command("empty")
|
||||||
@ -83,17 +85,15 @@ class TestCommand:
|
|||||||
with pytest.raises(exceptions.CommandError):
|
with pytest.raises(exceptions.CommandError):
|
||||||
command.Command(cm, "invalidret", a.invalidret)
|
command.Command(cm, "invalidret", a.invalidret)
|
||||||
with pytest.raises(exceptions.CommandError):
|
with pytest.raises(exceptions.CommandError):
|
||||||
command.Command(cm, "invalidarg", a.invalidarg)
|
assert command.Command(cm, "invalidarg", a.invalidarg)
|
||||||
|
|
||||||
def test_varargs(self):
|
def test_varargs(self):
|
||||||
with taddons.context() as tctx:
|
with taddons.context() as tctx:
|
||||||
cm = command.CommandManager(tctx.master)
|
cm = command.CommandManager(tctx.master)
|
||||||
a = TAddon()
|
a = TAddon()
|
||||||
c = command.Command(cm, "varargs", a.varargs)
|
c = command.Command(cm, "varargs", a.varargs)
|
||||||
assert c.signature_help() == "varargs str *str -> [str]"
|
assert c.signature_help() == "varargs one *var -> str[]"
|
||||||
assert c.call(["one", "two", "three"]) == ["two", "three"]
|
assert c.call(["one", "two", "three"]) == ["two", "three"]
|
||||||
with pytest.raises(exceptions.CommandError):
|
|
||||||
c.call(["one", "two", 3])
|
|
||||||
|
|
||||||
def test_call(self):
|
def test_call(self):
|
||||||
with taddons.context() as tctx:
|
with taddons.context() as tctx:
|
||||||
@ -101,7 +101,7 @@ class TestCommand:
|
|||||||
a = TAddon()
|
a = TAddon()
|
||||||
c = command.Command(cm, "cmd.path", a.cmd1)
|
c = command.Command(cm, "cmd.path", a.cmd1)
|
||||||
assert c.call(["foo"]) == "ret foo"
|
assert c.call(["foo"]) == "ret foo"
|
||||||
assert c.signature_help() == "cmd.path str -> str"
|
assert c.signature_help() == "cmd.path foo -> str"
|
||||||
|
|
||||||
c = command.Command(cm, "cmd.two", a.cmd2)
|
c = command.Command(cm, "cmd.two", a.cmd2)
|
||||||
with pytest.raises(exceptions.CommandError):
|
with pytest.raises(exceptions.CommandError):
|
||||||
@ -115,12 +115,9 @@ class TestCommand:
|
|||||||
[
|
[
|
||||||
"foo bar",
|
"foo bar",
|
||||||
[
|
[
|
||||||
command.ParseResult(
|
command.ParseResult(value="foo", type=mitmproxy.types.Cmd, valid=False),
|
||||||
value = "foo", type = mitmproxy.types.Cmd, valid = False
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
),
|
command.ParseResult(value="bar", type=mitmproxy.types.Unknown, valid=False)
|
||||||
command.ParseResult(
|
|
||||||
value = "bar", type = mitmproxy.types.Unknown, valid = False
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
],
|
],
|
||||||
@ -128,6 +125,7 @@ class TestCommand:
|
|||||||
"cmd1 'bar",
|
"cmd1 'bar",
|
||||||
[
|
[
|
||||||
command.ParseResult(value="cmd1", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="cmd1", type=mitmproxy.types.Cmd, valid=True),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
command.ParseResult(value="'bar", type=str, valid=True)
|
command.ParseResult(value="'bar", type=str, valid=True)
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
@ -139,13 +137,17 @@ class TestCommand:
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
[command.ParseResult(value = "", type = mitmproxy.types.Cmd, valid = False)],
|
[],
|
||||||
[]
|
[
|
||||||
|
command.CommandParameter("", mitmproxy.types.Cmd),
|
||||||
|
command.CommandParameter("", mitmproxy.types.CmdArgs)
|
||||||
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"cmd3 1",
|
"cmd3 1",
|
||||||
[
|
[
|
||||||
command.ParseResult(value="cmd3", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="cmd3", type=mitmproxy.types.Cmd, valid=True),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
command.ParseResult(value="1", type=int, valid=True),
|
command.ParseResult(value="1", type=int, valid=True),
|
||||||
],
|
],
|
||||||
[]
|
[]
|
||||||
@ -154,115 +156,264 @@ class TestCommand:
|
|||||||
"cmd3 ",
|
"cmd3 ",
|
||||||
[
|
[
|
||||||
command.ParseResult(value="cmd3", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="cmd3", type=mitmproxy.types.Cmd, valid=True),
|
||||||
command.ParseResult(value = "", type = int, valid = False),
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
],
|
],
|
||||||
[]
|
[command.CommandParameter('foo', int)]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"subcommand ",
|
"subcommand ",
|
||||||
[
|
[
|
||||||
command.ParseResult(
|
command.ParseResult(value="subcommand", type=mitmproxy.types.Cmd, valid=True, ),
|
||||||
value = "subcommand", type = mitmproxy.types.Cmd, valid = True,
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
),
|
|
||||||
command.ParseResult(value = "", type = mitmproxy.types.Cmd, valid = False),
|
|
||||||
],
|
],
|
||||||
["arg"],
|
[
|
||||||
|
command.CommandParameter('cmd', mitmproxy.types.Cmd),
|
||||||
|
command.CommandParameter('args', mitmproxy.types.CmdArgs, kind=inspect.Parameter.VAR_POSITIONAL),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"varargs one",
|
||||||
|
[
|
||||||
|
command.ParseResult(value="varargs", type=mitmproxy.types.Cmd, valid=True),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value="one", type=str, valid=True),
|
||||||
|
],
|
||||||
|
[command.CommandParameter('var', str, kind=inspect.Parameter.VAR_POSITIONAL)]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"varargs one two three",
|
||||||
|
[
|
||||||
|
command.ParseResult(value="varargs", type=mitmproxy.types.Cmd, valid=True),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value="one", type=str, valid=True),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value="two", type=str, valid=True),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value="three", type=str, valid=True),
|
||||||
|
],
|
||||||
|
[],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"subcommand cmd3 ",
|
"subcommand cmd3 ",
|
||||||
[
|
[
|
||||||
command.ParseResult(value="subcommand", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="subcommand", type=mitmproxy.types.Cmd, valid=True),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
command.ParseResult(value="cmd3", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="cmd3", type=mitmproxy.types.Cmd, valid=True),
|
||||||
command.ParseResult(value = "", type = int, valid = False),
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
],
|
],
|
||||||
[]
|
[command.CommandParameter('foo', int)]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"cmd4",
|
"cmd4",
|
||||||
[
|
[
|
||||||
command.ParseResult(value="cmd4", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="cmd4", type=mitmproxy.types.Cmd, valid=True),
|
||||||
],
|
],
|
||||||
["int", "str", "path"]
|
[
|
||||||
|
command.CommandParameter('a', int),
|
||||||
|
command.CommandParameter('b', str),
|
||||||
|
command.CommandParameter('c', mitmproxy.types.Path),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"cmd4 ",
|
"cmd4 ",
|
||||||
[
|
[
|
||||||
command.ParseResult(value="cmd4", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="cmd4", type=mitmproxy.types.Cmd, valid=True),
|
||||||
command.ParseResult(value = "", type = int, valid = False),
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
],
|
],
|
||||||
["str", "path"]
|
[
|
||||||
|
command.CommandParameter('a', int),
|
||||||
|
command.CommandParameter('b', str),
|
||||||
|
command.CommandParameter('c', mitmproxy.types.Path),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"cmd4 1",
|
"cmd4 1",
|
||||||
[
|
[
|
||||||
command.ParseResult(value="cmd4", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="cmd4", type=mitmproxy.types.Cmd, valid=True),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
command.ParseResult(value="1", type=int, valid=True),
|
command.ParseResult(value="1", type=int, valid=True),
|
||||||
],
|
],
|
||||||
["str", "path"]
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"cmd4 1",
|
command.CommandParameter('b', str),
|
||||||
[
|
command.CommandParameter('c', mitmproxy.types.Path),
|
||||||
command.ParseResult(value = "cmd4", type = mitmproxy.types.Cmd, valid = True),
|
]
|
||||||
command.ParseResult(value = "1", type = int, valid = True),
|
|
||||||
],
|
|
||||||
["str", "path"]
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"flow",
|
"flow",
|
||||||
[
|
[
|
||||||
command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True),
|
||||||
],
|
],
|
||||||
["flow", "str"]
|
[
|
||||||
|
command.CommandParameter('f', flow.Flow),
|
||||||
|
command.CommandParameter('s', str),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"flow ",
|
"flow ",
|
||||||
[
|
[
|
||||||
command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True),
|
||||||
command.ParseResult(value = "", type = flow.Flow, valid = False),
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
],
|
],
|
||||||
["str"]
|
[
|
||||||
|
command.CommandParameter('f', flow.Flow),
|
||||||
|
command.CommandParameter('s', str),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"flow x",
|
"flow x",
|
||||||
[
|
[
|
||||||
command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
command.ParseResult(value="x", type=flow.Flow, valid=False),
|
command.ParseResult(value="x", type=flow.Flow, valid=False),
|
||||||
],
|
],
|
||||||
["str"]
|
[
|
||||||
|
command.CommandParameter('s', str),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"flow x ",
|
"flow x ",
|
||||||
[
|
[
|
||||||
command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
command.ParseResult(value="x", type=flow.Flow, valid=False),
|
command.ParseResult(value="x", type=flow.Flow, valid=False),
|
||||||
command.ParseResult(value = "", type = str, valid = True),
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
],
|
],
|
||||||
[]
|
[
|
||||||
|
command.CommandParameter('s', str),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"flow \"one two",
|
"flow \"one two",
|
||||||
[
|
[
|
||||||
command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
command.ParseResult(value="\"one two", type=flow.Flow, valid=False),
|
command.ParseResult(value="\"one two", type=flow.Flow, valid=False),
|
||||||
],
|
],
|
||||||
["str"]
|
[
|
||||||
|
command.CommandParameter('s', str),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"flow \"one two\"",
|
"flow \"three four\"",
|
||||||
[
|
[
|
||||||
command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True),
|
command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True),
|
||||||
command.ParseResult(value = "one two", type = flow.Flow, valid = False),
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
],
|
command.ParseResult(value='"three four"', type=flow.Flow, valid=False),
|
||||||
["str"]
|
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
command.CommandParameter('s', str),
|
||||||
]
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"spaces ' '",
|
||||||
|
[
|
||||||
|
command.ParseResult(value="spaces", type=mitmproxy.types.Cmd, valid=False),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value="' '", type=mitmproxy.types.Unknown, valid=False)
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'spaces2 " "',
|
||||||
|
[
|
||||||
|
command.ParseResult(value="spaces2", type=mitmproxy.types.Cmd, valid=False),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value='" "', type=mitmproxy.types.Unknown, valid=False)
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'"abc"',
|
||||||
|
[
|
||||||
|
command.ParseResult(value='"abc"', type=mitmproxy.types.Cmd, valid=False),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"'def'",
|
||||||
|
[
|
||||||
|
command.ParseResult(value="'def'", type=mitmproxy.types.Cmd, valid=False),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"cmd10 'a' \"b\" c",
|
||||||
|
[
|
||||||
|
command.ParseResult(value="cmd10", type=mitmproxy.types.Cmd, valid=False),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value="'a'", type=mitmproxy.types.Unknown, valid=False),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value='"b"', type=mitmproxy.types.Unknown, valid=False),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value="c", type=mitmproxy.types.Unknown, valid=False),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"cmd11 'a \"b\" c'",
|
||||||
|
[
|
||||||
|
command.ParseResult(value="cmd11", type=mitmproxy.types.Cmd, valid=False),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value="'a \"b\" c'", type=mitmproxy.types.Unknown, valid=False),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'cmd12 "a \'b\' c"',
|
||||||
|
[
|
||||||
|
command.ParseResult(value="cmd12", type=mitmproxy.types.Cmd, valid=False),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value='"a \'b\' c"', type=mitmproxy.types.Unknown, valid=False),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
r'cmd13 "a \"b\" c"',
|
||||||
|
[
|
||||||
|
command.ParseResult(value="cmd13", type=mitmproxy.types.Cmd, valid=False),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value=r'"a \"b\" c"', type=mitmproxy.types.Unknown, valid=False),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
r"cmd14 'a \'b\' c'",
|
||||||
|
[
|
||||||
|
command.ParseResult(value="cmd14", type=mitmproxy.types.Cmd, valid=False),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value=r"'a \'b\' c'", type=mitmproxy.types.Unknown, valid=False),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
" spaces_at_the_begining_are_not_stripped",
|
||||||
|
[
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value="spaces_at_the_begining_are_not_stripped", type=mitmproxy.types.Cmd,
|
||||||
|
valid=False),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
" spaces_at_the_begining_are_not_stripped neither_at_the_end ",
|
||||||
|
[
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value="spaces_at_the_begining_are_not_stripped", type=mitmproxy.types.Cmd,
|
||||||
|
valid=False),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
command.ParseResult(value="neither_at_the_end", type=mitmproxy.types.Unknown, valid=False),
|
||||||
|
command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
with taddons.context() as tctx:
|
with taddons.context() as tctx:
|
||||||
tctx.master.addons.add(TAddon())
|
tctx.master.addons.add(TAddon())
|
||||||
for s, expected, expectedremain in tests:
|
for s, expected, expectedremain in tests:
|
||||||
current, remain = tctx.master.commands.parse_partial(s)
|
current, remain = tctx.master.commands.parse_partial(s)
|
||||||
assert current == expected
|
assert (s, current, expectedremain) == (s, expected, remain)
|
||||||
assert expectedremain == remain
|
|
||||||
|
|
||||||
|
|
||||||
def test_simple():
|
def test_simple():
|
||||||
@ -270,8 +421,10 @@ def test_simple():
|
|||||||
c = command.CommandManager(tctx.master)
|
c = command.CommandManager(tctx.master)
|
||||||
a = TAddon()
|
a = TAddon()
|
||||||
c.add("one.two", a.cmd1)
|
c.add("one.two", a.cmd1)
|
||||||
assert c.commands["one.two"].help == "cmd1 help"
|
assert (c.commands["one.two"].help == "cmd1 help")
|
||||||
assert (c.execute("one.two foo") == "ret foo")
|
assert (c.execute("one.two foo") == "ret foo")
|
||||||
|
assert (c.execute("one.two \"foo\"") == "ret foo")
|
||||||
|
assert (c.execute("one.two 'foo bar'") == "ret foo bar")
|
||||||
assert (c.call("one.two", "foo") == "ret foo")
|
assert (c.call("one.two", "foo") == "ret foo")
|
||||||
with pytest.raises(exceptions.CommandError, match="Unknown"):
|
with pytest.raises(exceptions.CommandError, match="Unknown"):
|
||||||
c.execute("nonexistent")
|
c.execute("nonexistent")
|
||||||
@ -281,8 +434,14 @@ def test_simple():
|
|||||||
c.execute("one.two too many args")
|
c.execute("one.two too many args")
|
||||||
with pytest.raises(exceptions.CommandError, match="Unknown"):
|
with pytest.raises(exceptions.CommandError, match="Unknown"):
|
||||||
c.call("nonexistent")
|
c.call("nonexistent")
|
||||||
with pytest.raises(exceptions.CommandError, match="No escaped"):
|
with pytest.raises(exceptions.CommandError, match="Unknown"):
|
||||||
c.execute("\\")
|
c.execute("\\")
|
||||||
|
with pytest.raises(exceptions.CommandError, match="Unknown"):
|
||||||
|
c.execute(r"\'")
|
||||||
|
with pytest.raises(exceptions.CommandError, match="Unknown"):
|
||||||
|
c.execute(r"\"")
|
||||||
|
with pytest.raises(exceptions.CommandError, match="Unknown"):
|
||||||
|
c.execute(r"\"")
|
||||||
|
|
||||||
c.add("empty", a.empty)
|
c.add("empty", a.empty)
|
||||||
c.execute("empty")
|
c.execute("empty")
|
||||||
@ -294,13 +453,13 @@ def test_simple():
|
|||||||
|
|
||||||
def test_typename():
|
def test_typename():
|
||||||
assert command.typename(str) == "str"
|
assert command.typename(str) == "str"
|
||||||
assert command.typename(typing.Sequence[flow.Flow]) == "[flow]"
|
assert command.typename(typing.Sequence[flow.Flow]) == "flow[]"
|
||||||
|
|
||||||
assert command.typename(mitmproxy.types.Data) == "[data]"
|
assert command.typename(mitmproxy.types.Data) == "data[][]"
|
||||||
assert command.typename(mitmproxy.types.CutSpec) == "[cut]"
|
assert command.typename(mitmproxy.types.CutSpec) == "cut[]"
|
||||||
|
|
||||||
assert command.typename(flow.Flow) == "flow"
|
assert command.typename(flow.Flow) == "flow"
|
||||||
assert command.typename(typing.Sequence[str]) == "[str]"
|
assert command.typename(typing.Sequence[str]) == "str[]"
|
||||||
|
|
||||||
assert command.typename(mitmproxy.types.Choice("foo")) == "choice"
|
assert command.typename(mitmproxy.types.Choice("foo")) == "choice"
|
||||||
assert command.typename(mitmproxy.types.Path) == "path"
|
assert command.typename(mitmproxy.types.Path) == "path"
|
||||||
|
38
test/mitmproxy/test_command_lexer.py
Normal file
38
test/mitmproxy/test_command_lexer.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import pyparsing
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mitmproxy import command_lexer
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"test_input,valid", [
|
||||||
|
("'foo'", True),
|
||||||
|
('"foo"', True),
|
||||||
|
("'foo' bar'", False),
|
||||||
|
("'foo\\' bar'", True),
|
||||||
|
("'foo' 'bar'", False),
|
||||||
|
("'foo'x", False),
|
||||||
|
('''"foo ''', True),
|
||||||
|
('''"foo 'bar' ''', True),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_partial_quoted_string(test_input, valid):
|
||||||
|
if valid:
|
||||||
|
assert command_lexer.PartialQuotedString.parseString(test_input, parseAll=True)[0] == test_input
|
||||||
|
else:
|
||||||
|
with pytest.raises(pyparsing.ParseException):
|
||||||
|
command_lexer.PartialQuotedString.parseString(test_input, parseAll=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"test_input,expected", [
|
||||||
|
("'foo'", ["'foo'"]),
|
||||||
|
('"foo"', ['"foo"']),
|
||||||
|
("'foo' 'bar'", ["'foo'", ' ', "'bar'"]),
|
||||||
|
("'foo'x", ["'foo'", 'x']),
|
||||||
|
('''"foo''', ['"foo']),
|
||||||
|
('''"foo 'bar' ''', ['''"foo 'bar' ''']),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_expr(test_input, expected):
|
||||||
|
assert list(command_lexer.expr.parseString(test_input, parseAll=True)) == expected
|
@ -2,7 +2,6 @@ import pytest
|
|||||||
import os
|
import os
|
||||||
import typing
|
import typing
|
||||||
import contextlib
|
import contextlib
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
import mitmproxy.exceptions
|
import mitmproxy.exceptions
|
||||||
import mitmproxy.types
|
import mitmproxy.types
|
||||||
@ -64,12 +63,13 @@ def test_int():
|
|||||||
b.parse(tctx.master.commands, int, "foo")
|
b.parse(tctx.master.commands, int, "foo")
|
||||||
|
|
||||||
|
|
||||||
def test_path(tdata):
|
def test_path(tdata, monkeypatch):
|
||||||
with taddons.context() as tctx:
|
with taddons.context() as tctx:
|
||||||
b = mitmproxy.types._PathType()
|
b = mitmproxy.types._PathType()
|
||||||
assert b.parse(tctx.master.commands, mitmproxy.types.Path, "/foo") == "/foo"
|
assert b.parse(tctx.master.commands, mitmproxy.types.Path, "/foo") == "/foo"
|
||||||
assert b.parse(tctx.master.commands, mitmproxy.types.Path, "/bar") == "/bar"
|
assert b.parse(tctx.master.commands, mitmproxy.types.Path, "/bar") == "/bar"
|
||||||
with mock.patch.dict("os.environ", {"HOME": "/home/test"}):
|
monkeypatch.setenv("HOME", "/home/test")
|
||||||
|
monkeypatch.setenv("USERPROFILE", "/home/test")
|
||||||
assert b.parse(tctx.master.commands, mitmproxy.types.Path, "~/mitm") == "/home/test/mitm"
|
assert b.parse(tctx.master.commands, mitmproxy.types.Path, "~/mitm") == "/home/test/mitm"
|
||||||
assert b.is_valid(tctx.master.commands, mitmproxy.types.Path, "foo") is True
|
assert b.is_valid(tctx.master.commands, mitmproxy.types.Path, "foo") is True
|
||||||
assert b.is_valid(tctx.master.commands, mitmproxy.types.Path, "~/mitm") is True
|
assert b.is_valid(tctx.master.commands, mitmproxy.types.Path, "~/mitm") is True
|
||||||
@ -127,10 +127,9 @@ def test_cutspec():
|
|||||||
def test_arg():
|
def test_arg():
|
||||||
with taddons.context() as tctx:
|
with taddons.context() as tctx:
|
||||||
b = mitmproxy.types._ArgType()
|
b = mitmproxy.types._ArgType()
|
||||||
assert b.completion(tctx.master.commands, mitmproxy.types.Arg, "") == []
|
assert b.completion(tctx.master.commands, mitmproxy.types.CmdArgs, "") == []
|
||||||
assert b.parse(tctx.master.commands, mitmproxy.types.Arg, "foo") == "foo"
|
assert b.parse(tctx.master.commands, mitmproxy.types.CmdArgs, "foo") == "foo"
|
||||||
assert b.is_valid(tctx.master.commands, mitmproxy.types.Arg, "foo") is True
|
assert b.is_valid(tctx.master.commands, mitmproxy.types.CmdArgs, 1) is False
|
||||||
assert b.is_valid(tctx.master.commands, mitmproxy.types.Arg, 1) is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_strseq():
|
def test_strseq():
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
from mitmproxy.tools.console.commander import commander
|
|
||||||
from mitmproxy.test import taddons
|
from mitmproxy.test import taddons
|
||||||
|
from mitmproxy.tools.console.commander import commander
|
||||||
|
|
||||||
|
|
||||||
class TestListCompleter:
|
class TestListCompleter:
|
||||||
@ -28,6 +29,112 @@ class TestListCompleter:
|
|||||||
assert c.cycle() == expected
|
assert c.cycle() == expected
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommandEdit:
|
||||||
|
def test_open_command_bar(self):
|
||||||
|
with taddons.context() as tctx:
|
||||||
|
history = commander.CommandHistory(tctx.master, size=3)
|
||||||
|
edit = commander.CommandEdit(tctx.master, '', history)
|
||||||
|
|
||||||
|
try:
|
||||||
|
edit.update()
|
||||||
|
except IndexError:
|
||||||
|
pytest.faied("Unexpected IndexError")
|
||||||
|
|
||||||
|
def test_insert(self):
|
||||||
|
with taddons.context() as tctx:
|
||||||
|
history = commander.CommandHistory(tctx.master, size=3)
|
||||||
|
edit = commander.CommandEdit(tctx.master, '', history)
|
||||||
|
edit.keypress(1, 'a')
|
||||||
|
assert edit.get_edit_text() == 'a'
|
||||||
|
|
||||||
|
# Don't let users type a space before starting a command
|
||||||
|
# as a usability feature
|
||||||
|
history = commander.CommandHistory(tctx.master, size=3)
|
||||||
|
edit = commander.CommandEdit(tctx.master, '', history)
|
||||||
|
edit.keypress(1, ' ')
|
||||||
|
assert edit.get_edit_text() == ''
|
||||||
|
|
||||||
|
def test_backspace(self):
|
||||||
|
with taddons.context() as tctx:
|
||||||
|
history = commander.CommandHistory(tctx.master, size=3)
|
||||||
|
edit = commander.CommandEdit(tctx.master, '', history)
|
||||||
|
edit.keypress(1, 'a')
|
||||||
|
edit.keypress(1, 'b')
|
||||||
|
assert edit.get_edit_text() == 'ab'
|
||||||
|
edit.keypress(1, 'backspace')
|
||||||
|
assert edit.get_edit_text() == 'a'
|
||||||
|
|
||||||
|
def test_left(self):
|
||||||
|
with taddons.context() as tctx:
|
||||||
|
history = commander.CommandHistory(tctx.master, size=3)
|
||||||
|
edit = commander.CommandEdit(tctx.master, '', history)
|
||||||
|
edit.keypress(1, 'a')
|
||||||
|
assert edit.cbuf.cursor == 1
|
||||||
|
edit.keypress(1, 'left')
|
||||||
|
assert edit.cbuf.cursor == 0
|
||||||
|
|
||||||
|
# Do it again to make sure it won't go negative
|
||||||
|
edit.keypress(1, 'left')
|
||||||
|
assert edit.cbuf.cursor == 0
|
||||||
|
|
||||||
|
def test_right(self):
|
||||||
|
with taddons.context() as tctx:
|
||||||
|
history = commander.CommandHistory(tctx.master, size=3)
|
||||||
|
edit = commander.CommandEdit(tctx.master, '', history)
|
||||||
|
edit.keypress(1, 'a')
|
||||||
|
assert edit.cbuf.cursor == 1
|
||||||
|
|
||||||
|
# Make sure cursor won't go past the text
|
||||||
|
edit.keypress(1, 'right')
|
||||||
|
assert edit.cbuf.cursor == 1
|
||||||
|
|
||||||
|
# Make sure cursor goes left and then back right
|
||||||
|
edit.keypress(1, 'left')
|
||||||
|
assert edit.cbuf.cursor == 0
|
||||||
|
edit.keypress(1, 'right')
|
||||||
|
assert edit.cbuf.cursor == 1
|
||||||
|
|
||||||
|
def test_up_and_down(self):
|
||||||
|
with taddons.context() as tctx:
|
||||||
|
history = commander.CommandHistory(tctx.master, size=3)
|
||||||
|
edit = commander.CommandEdit(tctx.master, '', history)
|
||||||
|
|
||||||
|
buf = commander.CommandBuffer(tctx.master, 'cmd1')
|
||||||
|
history.add_command(buf)
|
||||||
|
buf = commander.CommandBuffer(tctx.master, 'cmd2')
|
||||||
|
history.add_command(buf)
|
||||||
|
|
||||||
|
edit.keypress(1, 'up')
|
||||||
|
assert edit.get_edit_text() == 'cmd2'
|
||||||
|
edit.keypress(1, 'up')
|
||||||
|
assert edit.get_edit_text() == 'cmd1'
|
||||||
|
edit.keypress(1, 'up')
|
||||||
|
assert edit.get_edit_text() == 'cmd1'
|
||||||
|
|
||||||
|
history = commander.CommandHistory(tctx.master, size=5)
|
||||||
|
edit = commander.CommandEdit(tctx.master, '', history)
|
||||||
|
edit.keypress(1, 'a')
|
||||||
|
edit.keypress(1, 'b')
|
||||||
|
edit.keypress(1, 'c')
|
||||||
|
assert edit.get_edit_text() == 'abc'
|
||||||
|
edit.keypress(1, 'up')
|
||||||
|
assert edit.get_edit_text() == ''
|
||||||
|
edit.keypress(1, 'down')
|
||||||
|
assert edit.get_edit_text() == 'abc'
|
||||||
|
edit.keypress(1, 'down')
|
||||||
|
assert edit.get_edit_text() == 'abc'
|
||||||
|
|
||||||
|
history = commander.CommandHistory(tctx.master, size=5)
|
||||||
|
edit = commander.CommandEdit(tctx.master, '', history)
|
||||||
|
buf = commander.CommandBuffer(tctx.master, 'cmd3')
|
||||||
|
history.add_command(buf)
|
||||||
|
edit.keypress(1, 'z')
|
||||||
|
edit.keypress(1, 'up')
|
||||||
|
assert edit.get_edit_text() == 'cmd3'
|
||||||
|
edit.keypress(1, 'down')
|
||||||
|
assert edit.get_edit_text() == 'z'
|
||||||
|
|
||||||
|
|
||||||
class TestCommandHistory:
|
class TestCommandHistory:
|
||||||
def fill_history(self, commands):
|
def fill_history(self, commands):
|
||||||
with taddons.context() as tctx:
|
with taddons.context() as tctx:
|
||||||
@ -148,13 +255,39 @@ class TestCommandBuffer:
|
|||||||
cb.cursor = len(cb.text)
|
cb.cursor = len(cb.text)
|
||||||
cb.cycle_completion()
|
cb.cycle_completion()
|
||||||
|
|
||||||
|
ch = commander.CommandHistory(tctx.master, 30)
|
||||||
|
ce = commander.CommandEdit(tctx.master, "se", ch)
|
||||||
|
ce.keypress(1, 'tab')
|
||||||
|
ce.update()
|
||||||
|
ret = ce.cbuf.render()
|
||||||
|
assert ret == [
|
||||||
|
('commander_command', 'set'),
|
||||||
|
('text', ' '),
|
||||||
|
('commander_hint', 'option '),
|
||||||
|
('commander_hint', 'value '),
|
||||||
|
]
|
||||||
|
|
||||||
def test_render(self):
|
def test_render(self):
|
||||||
with taddons.context() as tctx:
|
with taddons.context() as tctx:
|
||||||
cb = commander.CommandBuffer(tctx.master)
|
cb = commander.CommandBuffer(tctx.master)
|
||||||
cb.text = "foo"
|
cb.text = "foo"
|
||||||
assert cb.render()
|
assert cb.render()
|
||||||
|
|
||||||
def test_flatten(self):
|
cb.text = "set view_filter '~bq test'"
|
||||||
with taddons.context() as tctx:
|
ret = cb.render()
|
||||||
cb = commander.CommandBuffer(tctx.master)
|
assert ret == [
|
||||||
assert cb.flatten("foo bar") == "foo bar"
|
('commander_command', 'set'),
|
||||||
|
('text', ' '),
|
||||||
|
('text', 'view_filter'),
|
||||||
|
('text', ' '),
|
||||||
|
('text', "'~bq test'"),
|
||||||
|
]
|
||||||
|
|
||||||
|
cb.text = "set"
|
||||||
|
ret = cb.render()
|
||||||
|
assert ret == [
|
||||||
|
('commander_command', 'set'),
|
||||||
|
('text', ' '),
|
||||||
|
('commander_hint', 'option '),
|
||||||
|
('commander_hint', 'value '),
|
||||||
|
]
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
import mitmproxy.types
|
||||||
|
from mitmproxy import command
|
||||||
|
from mitmproxy import ctx
|
||||||
from mitmproxy.test.tflow import tflow
|
from mitmproxy.test.tflow import tflow
|
||||||
from mitmproxy.tools.console import defaultkeys
|
from mitmproxy.tools.console import defaultkeys
|
||||||
from mitmproxy.tools.console import keymap
|
from mitmproxy.tools.console import keymap
|
||||||
from mitmproxy.tools.console import master
|
from mitmproxy.tools.console import master
|
||||||
from mitmproxy import command
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_commands_exist():
|
async def test_commands_exist():
|
||||||
|
command_manager = command.CommandManager(ctx)
|
||||||
|
|
||||||
km = keymap.Keymap(None)
|
km = keymap.Keymap(None)
|
||||||
defaultkeys.map(km)
|
defaultkeys.map(km)
|
||||||
assert km.bindings
|
assert km.bindings
|
||||||
@ -16,7 +20,14 @@ async def test_commands_exist():
|
|||||||
await m.load_flow(tflow())
|
await m.load_flow(tflow())
|
||||||
|
|
||||||
for binding in km.bindings:
|
for binding in km.bindings:
|
||||||
cmd, *args = command.lexer(binding.command)
|
parsed, _ = command_manager.parse_partial(binding.command.strip())
|
||||||
|
|
||||||
|
cmd = parsed[0].value
|
||||||
|
args = [
|
||||||
|
a.value for a in parsed[1:]
|
||||||
|
if a.type != mitmproxy.types.Space
|
||||||
|
]
|
||||||
|
|
||||||
assert cmd in m.commands.commands
|
assert cmd in m.commands.commands
|
||||||
|
|
||||||
cmd_obj = m.commands.commands[cmd]
|
cmd_obj = m.commands.commands[cmd]
|
||||||
|
Loading…
Reference in New Issue
Block a user