Merge pull request #3693 from typoon/fix-command-bar-issue-3259

Improve Command Bar UX
This commit is contained in:
Maximilian Hils 2019-11-21 14:13:08 +01:00 committed by GitHub
commit 3550bdfe00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 948 additions and 550 deletions

View File

@ -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)

View File

@ -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))

View File

@ -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:

View File

@ -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")

View File

@ -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

View 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

View File

@ -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

View File

@ -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()

View File

@ -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.

View File

@ -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")

View File

@ -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=()):

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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"

View 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

View File

@ -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():

View File

@ -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 '),
]

View File

@ -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]