diff options
-rw-r--r-- | mitmproxy/command.py | 99 | ||||
-rw-r--r-- | mitmproxy/tools/console/commander/commander.py | 62 | ||||
-rw-r--r-- | test/mitmproxy/test_command.py | 167 | ||||
-rw-r--r-- | test/mitmproxy/tools/console/test_commander.py | 19 | ||||
-rw-r--r-- | test/mitmproxy/tools/console/test_defaultkeys.py | 13 |
5 files changed, 252 insertions, 108 deletions
diff --git a/mitmproxy/command.py b/mitmproxy/command.py index 0998601c..c7a45587 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -3,12 +3,11 @@ """ import inspect import types -import io import typing -import shlex import textwrap import functools import sys +import pyparsing from mitmproxy import exceptions import mitmproxy.types @@ -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]) -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: """ Translates a type to an explanatory string. @@ -86,6 +76,20 @@ class Command: return "%s %s%s" % (self.path, params, ret) def prepare_args(self, args: typing.Sequence[str]) -> typing.List[typing.Any]: + + # Arguments that are just blank spaces aren't really arguments + # We need to get rid of those. If the user intended to pass a sequence + # of spaces, it would come between quotes + clean_args = [] + for a in args: + if isinstance(a, str): + if a.strip() != '': + clean_args.append(a) + else: + clean_args.append(a) + + args = clean_args + verify_arg_signature(self.func, list(args), {}) remainder: typing.Sequence[str] = [] @@ -159,23 +163,36 @@ class CommandManager(mitmproxy.types._CommandBase): """ 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) + + rex = pyparsing.QuotedString("\"", escChar='\\', unquoteResults=False) |\ + pyparsing.QuotedString("'", escChar='\\', unquoteResults=False) |\ + pyparsing.Combine(pyparsing.Literal('"') + pyparsing.Word(pyparsing.printables + " ") + pyparsing.StringEnd()) |\ + pyparsing.Word(pyparsing.printables) |\ + pyparsing.Word(' ') + + rex = rex.copy().leaveWhitespace() + + remainder = cmdstr + + for t, start, end in rex.scanString(cmdstr): + + remainder = cmdstr[end:] + parts.append(t[0]) + + if remainder != '': + parts.append(remainder) + if not parts: - parts = [""] - elif cmdstr.endswith(" "): - parts.append("") + parts = [] + + # First item in parts has always to be the command + # so we remove any blank tokens from the start of it + while True: + if parts and parts[0].strip() == '': + del parts[0] + else: + break parse: typing.List[ParseResult] = [] params: typing.List[type] = [] @@ -186,10 +203,15 @@ class CommandManager(mitmproxy.types._CommandBase): if parts[i] in self.commands: params.extend(self.commands[parts[i]].paramtypes) elif params: - typ = params.pop(0) - if typ == mitmproxy.types.Cmd and params and params[0] == mitmproxy.types.Arg: - if parts[i] in self.commands: - params[:] = self.commands[parts[i]].paramtypes + if parts[i].strip() != '': + typ = params.pop(0) + if typ == mitmproxy.types.Cmd and params and params[0] == mitmproxy.types.Arg: + if parts[i] in self.commands: + params[:] = self.commands[parts[i]].paramtypes + else: + # If the token is just a bunch of spaces, then we don't + # want to count it against the arguments of the command + typ = mitmproxy.types.Unknown else: typ = mitmproxy.types.Unknown @@ -203,6 +225,8 @@ class CommandManager(mitmproxy.types._CommandBase): else: valid = True + # if ctx.log: + # ctx.log.info('[gilga] before parse.append. value = %s' % parts[i]) parse.append( ParseResult( value=parts[i], @@ -232,19 +256,22 @@ class CommandManager(mitmproxy.types._CommandBase): """ if path not in self.commands: raise exceptions.CommandError("Unknown command: %s" % path) + return self.commands[path].call(args) def execute(self, cmdstr: str): """ Execute a command string. May raise CommandError. """ - try: - parts = list(lexer(cmdstr)) - except ValueError as e: - raise exceptions.CommandError("Command error: %s" % e) - if not len(parts) >= 1: + parts, _ = self.parse_partial(cmdstr) + params = [] + for p in parts: + params.append(p.value) + + if len(parts) == 0: raise exceptions.CommandError("Invalid command: %s" % cmdstr) - return self.call_strings(parts[0], parts[1:]) + + return self.call_strings(params[0], params[1:]) def dump(self, out=sys.stdout) -> None: cmds = list(self.commands.values()) diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index f291b8fd..4909348f 100644 --- a/mitmproxy/tools/console/commander/commander.py +++ b/mitmproxy/tools/console/commander/commander.py @@ -52,7 +52,7 @@ CompletionState = typing.NamedTuple( class CommandBuffer: def __init__(self, master: mitmproxy.master.Master, start: str = "") -> None: self.master = master - self.text = self.flatten(start) + self.text = start # Cursor is always within the range [0:len(buffer)]. self._cursor = len(self.text) self.completion: typing.Optional[CompletionState] = None @@ -70,16 +70,11 @@ class CommandBuffer: else: 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), + value = p.value, type = p.type, valid = p.valid ) @@ -94,27 +89,29 @@ class CommandBuffer: """ parts, remhelp = self.parse_quoted(self.text) ret = [] - for p in parts: - if p.valid: - if p.type == mitmproxy.types.Cmd: - ret.append(("commander_command", p.value)) - else: - ret.append(("text", p.value)) - elif p.value: - ret.append(("commander_invalid", p.value)) - else: - ret.append(("text", "")) + if 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", "")) ret.append(("text", " ")) - if remhelp: - ret.append(("text", " ")) - for v in remhelp: - ret.append(("commander_hint", "%s " % v)) - return ret + else: + for p in parts: + if p.valid: + if p.type == mitmproxy.types.Cmd: + ret.append(("commander_command", p.value)) + else: + ret.append(("text", p.value)) + elif p.value: + ret.append(("commander_invalid", p.value)) + else: + ret.append(("text", "")) + + if remhelp: + ret.append(("text", " ")) + for v in remhelp: + ret.append(("commander_hint", "%s " % v)) - def flatten(self, txt): - parts, _ = self.parse_quoted(txt) - ret = [x.value for x in parts] - return " ".join(ret) + return ret def left(self) -> None: self.cursor = self.cursor - 1 @@ -139,13 +136,13 @@ class CommandBuffer: nxt = self.completion.completer.cycle() buf = " ".join([i.value for i in self.completion.parse[:-1]]) + " " + nxt buf = buf.strip() - self.text = self.flatten(buf) + self.text = buf self.cursor = len(self.text) def backspace(self) -> None: if self.cursor == 0: 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.completion = None @@ -153,8 +150,13 @@ class CommandBuffer: """ 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 diff --git a/test/mitmproxy/test_command.py b/test/mitmproxy/test_command.py index d9dcf5f9..ae4c400c 100644 --- a/test/mitmproxy/test_command.py +++ b/test/mitmproxy/test_command.py @@ -115,12 +115,9 @@ class TestCommand: [ "foo bar", [ - command.ParseResult( - value = "foo", type = mitmproxy.types.Cmd, valid = False - ), - command.ParseResult( - value = "bar", type = mitmproxy.types.Unknown, valid = False - ) + command.ParseResult(value = "foo", type = mitmproxy.types.Cmd, valid = False), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), + command.ParseResult(value = "bar", type = mitmproxy.types.Unknown, valid = False) ], [], ], @@ -128,6 +125,7 @@ class TestCommand: "cmd1 'bar", [ command.ParseResult(value = "cmd1", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), command.ParseResult(value = "'bar", type = str, valid = True) ], [], @@ -139,13 +137,14 @@ class TestCommand: ], [ "", - [command.ParseResult(value = "", type = mitmproxy.types.Cmd, valid = False)], + [], [] ], [ "cmd3 1", [ command.ParseResult(value = "cmd3", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), command.ParseResult(value = "1", type = int, valid = True), ], [] @@ -154,28 +153,27 @@ class TestCommand: "cmd3 ", [ command.ParseResult(value = "cmd3", type = mitmproxy.types.Cmd, valid = True), - command.ParseResult(value = "", type = int, valid = False), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), ], - [] + ['int'] ], [ "subcommand ", [ - command.ParseResult( - value = "subcommand", type = mitmproxy.types.Cmd, valid = True, - ), - command.ParseResult(value = "", type = mitmproxy.types.Cmd, valid = False), + command.ParseResult(value = "subcommand", type = mitmproxy.types.Cmd, valid = True,), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), ], - ["arg"], + ["cmd", "arg"], ], [ "subcommand cmd3 ", [ command.ParseResult(value = "subcommand", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), command.ParseResult(value = "cmd3", type = mitmproxy.types.Cmd, valid = True), - command.ParseResult(value = "", type = int, valid = False), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), ], - [] + ["int"] ], [ "cmd4", @@ -188,22 +186,15 @@ class TestCommand: "cmd4 ", [ command.ParseResult(value = "cmd4", type = mitmproxy.types.Cmd, valid = True), - command.ParseResult(value = "", type = int, valid = False), - ], - ["str", "path"] - ], - [ - "cmd4 1", - [ - command.ParseResult(value = "cmd4", type = mitmproxy.types.Cmd, valid = True), - command.ParseResult(value = "1", type = int, valid = True), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), ], - ["str", "path"] + ["int", "str", "path"] ], [ "cmd4 1", [ command.ParseResult(value = "cmd4", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), command.ParseResult(value = "1", type = int, valid = True), ], ["str", "path"] @@ -219,14 +210,15 @@ class TestCommand: "flow ", [ command.ParseResult(value = "flow", type = mitmproxy.types.Cmd, valid = True), - command.ParseResult(value = "", type = flow.Flow, valid = False), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), ], - ["str"] + ["flow", "str"] ], [ "flow x", [ command.ParseResult(value = "flow", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), command.ParseResult(value = "x", type = flow.Flow, valid = False), ], ["str"] @@ -235,28 +227,131 @@ class TestCommand: "flow x ", [ command.ParseResult(value = "flow", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), command.ParseResult(value = "x", type = flow.Flow, valid = False), - command.ParseResult(value = "", type = str, valid = True), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), ], - [] + ["str"] ], [ "flow \"one two", [ command.ParseResult(value = "flow", type = mitmproxy.types.Cmd, valid = True), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), command.ParseResult(value = "\"one two", type = flow.Flow, valid = False), ], ["str"] ], [ - "flow \"one two\"", + "flow \"three four\"", [ 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.Unknown, valid = False), + command.ParseResult(value = '"three four"', type = flow.Flow, valid = False), ], ["str"] ], + [ + "spaces ' '", + [ + command.ParseResult(value = "spaces", type = mitmproxy.types.Cmd, valid = False), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), + 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.Unknown, valid = False), + 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.Unknown, valid = False), + command.ParseResult(value = "'a'", type = mitmproxy.types.Unknown, valid = False), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), + command.ParseResult(value = '"b"', type = mitmproxy.types.Unknown, valid = False), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), + 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.Unknown, valid = False), + 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.Unknown, valid = False), + 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.Unknown, valid = False), + 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.Unknown, valid = False), + command.ParseResult(value = r"'a \'b\' c'", type = mitmproxy.types.Unknown, valid = False), + ], + [], + ], + [ + " spaces_at_the_begining_are_stripped", + [ + command.ParseResult(value = "spaces_at_the_begining_are_stripped", type = mitmproxy.types.Cmd, valid = False), + ], + [], + ], + [ + " spaces_at_the_begining_are_stripped but_not_at_the_end ", + [ + command.ParseResult(value = "spaces_at_the_begining_are_stripped", type = mitmproxy.types.Cmd, valid = False), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), + command.ParseResult(value = "but_not_at_the_end", type = mitmproxy.types.Unknown, valid = False), + command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), + ], + [], + ], + ] + with taddons.context() as tctx: tctx.master.addons.add(TAddon()) for s, expected, expectedremain in tests: @@ -270,7 +365,7 @@ def test_simple(): c = command.CommandManager(tctx.master) a = TAddon() 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.call("one.two", "foo") == "ret foo") with pytest.raises(exceptions.CommandError, match="Unknown"): @@ -281,8 +376,12 @@ def test_simple(): c.execute("one.two too many args") with pytest.raises(exceptions.CommandError, match="Unknown"): c.call("nonexistent") - with pytest.raises(exceptions.CommandError, match="No escaped"): + with pytest.raises(exceptions.CommandError, match="Unknown"): c.execute("\\") + 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.execute("empty") diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py index b5e226fe..798ca5fe 100644 --- a/test/mitmproxy/tools/console/test_commander.py +++ b/test/mitmproxy/tools/console/test_commander.py @@ -1,6 +1,6 @@ - from mitmproxy.tools.console.commander import commander from mitmproxy.test import taddons +import pytest class TestListCompleter: @@ -28,6 +28,18 @@ class TestListCompleter: 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") + + class TestCommandHistory: def fill_history(self, commands): with taddons.context() as tctx: @@ -153,8 +165,3 @@ class TestCommandBuffer: cb = commander.CommandBuffer(tctx.master) cb.text = "foo" assert cb.render() - - def test_flatten(self): - with taddons.context() as tctx: - cb = commander.CommandBuffer(tctx.master) - assert cb.flatten("foo bar") == "foo bar" diff --git a/test/mitmproxy/tools/console/test_defaultkeys.py b/test/mitmproxy/tools/console/test_defaultkeys.py index 52075c84..7e8df6b6 100644 --- a/test/mitmproxy/tools/console/test_defaultkeys.py +++ b/test/mitmproxy/tools/console/test_defaultkeys.py @@ -3,12 +3,14 @@ from mitmproxy.tools.console import defaultkeys from mitmproxy.tools.console import keymap from mitmproxy.tools.console import master from mitmproxy import command - +from mitmproxy import ctx import pytest @pytest.mark.asyncio async def test_commands_exist(): + command_manager = command.CommandManager(ctx) + km = keymap.Keymap(None) defaultkeys.map(km) assert km.bindings @@ -16,11 +18,18 @@ async def test_commands_exist(): await m.load_flow(tflow()) for binding in km.bindings: - cmd, *args = command.lexer(binding.command) + results = command_manager.parse_partial(binding.command) + + cmd = results[0][0].value + args = [a.value for a in results[0][1:]] + assert cmd in m.commands.commands cmd_obj = m.commands.commands[cmd] try: cmd_obj.prepare_args(args) except Exception as e: + + import pdb + pdb.set_trace() raise ValueError("Invalid command: {}".format(binding.command)) from e |