From c7ffc228194574d1cff2c2fbb05fc3419c4805f4 Mon Sep 17 00:00:00 2001 From: Henrique Date: Tue, 12 Nov 2019 18:57:39 -0500 Subject: Fix for issues when using \ and " on the commander bar --- mitmproxy/command.py | 58 ++++++++++++++++++++++++-- mitmproxy/flowfilter.py | 1 + mitmproxy/tools/console/commander/commander.py | 13 ++---- test/mitmproxy/test_command.py | 8 ++-- 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index 27f0921d..b287c740 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -13,6 +13,46 @@ import sys from mitmproxy import exceptions import mitmproxy.types +def maybequote(value): + """ + This function takes the output from the lexer and puts it between quotes + in the following cases: + * There is a space in the string: The only way a token from the lexer can have a space in it is if it was between quotes + * There is one or more quotes in the middle of the string: The only way for a token to have a quote in it that is not escaped is if it was escaped prior to being processed by the lexer. For example, the string `"s1 \" s2"` would come back from the lexer as `s1 " s2`. + + Any quotes that are in the middle of the string and that are not escaped will also be escaped (by placing a \ in front of it). + This function only deals with double quotes and they are the only ones that should be used. + """ + + new_value = "" + last_pos = len(value) - 1 + + for pos, char in enumerate(value): + if pos == 0: + new_value += char + continue + + # if pos == last_pos: + # new_value += char + # break + + if char in " \n\r\t": + new_value += char + continue + + if char == '"': + if value[pos-1] != '\\': + new_value += '\\' + + new_value += char + + value = new_value + + if ((" " in value) or ('"' in value)) and not (value.startswith("\"") or value.startswith("'")): + return "\"%s\"" % value + + return value + def verify_arg_signature(f: typing.Callable, args: list, kwargs: dict) -> None: sig = inspect.signature(f) @@ -201,9 +241,11 @@ 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], + value=maybequote(parts[i]), type=typ, valid=valid, ) @@ -236,13 +278,21 @@ class CommandManager(mitmproxy.types._CommandBase): """ Execute a command string. May raise CommandError. """ + if cmdstr == '': + raise exceptions.CommandError("Invalid command: %s" % cmdstr) + try: - parts = list(lexer(cmdstr)) + parts, _ = self.parse_partial(cmdstr) except ValueError as e: raise exceptions.CommandError("Command error: %s" % e) - if not len(parts) >= 1: + if len(parts) == 0: raise exceptions.CommandError("Invalid command: %s" % cmdstr) - return self.call_strings(parts[0], parts[1:]) + + params = [] + for p in parts: + params.append(p.value) + + return self.call_strings(params[0], params[1:]) def dump(self, out=sys.stdout) -> None: cmds = list(self.commands.values()) diff --git a/mitmproxy/flowfilter.py b/mitmproxy/flowfilter.py index 0d8f1062..b3f143af 100644 --- a/mitmproxy/flowfilter.py +++ b/mitmproxy/flowfilter.py @@ -501,6 +501,7 @@ def _make(): pp.Word(alphlatinB) |\ pp.QuotedString("\"", escChar='\\') |\ pp.QuotedString("'", escChar='\\') + for klass in filter_rex: f = pp.Literal("~%s" % klass.code) + pp.WordEnd() + rex.copy() f.setParseAction(klass.make) diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index e8550f86..0c043081 100644 --- a/mitmproxy/tools/console/commander/commander.py +++ b/mitmproxy/tools/console/commander/commander.py @@ -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 ) @@ -145,7 +140,7 @@ class CommandBuffer: 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 +148,8 @@ class CommandBuffer: """ Inserts text at the cursor. """ - self.text = self.flatten(self.text[:self.cursor] + k + self.text[self.cursor:]) - self.cursor += 1 + 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..2785e28f 100644 --- a/test/mitmproxy/test_command.py +++ b/test/mitmproxy/test_command.py @@ -249,10 +249,10 @@ class TestCommand: ["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 = '"three four"', type = flow.Flow, valid = False), ], ["str"] ], @@ -270,7 +270,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,7 +281,7 @@ 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("\\") c.add("empty", a.empty) -- cgit v1.2.3 From b321e07279f8e1be4b76beb9dff608bb09ce485e Mon Sep 17 00:00:00 2001 From: Henrique Date: Tue, 12 Nov 2019 18:59:25 -0500 Subject: Renamed the `maybequote` function to something better --- mitmproxy/command.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index b287c740..cf345c22 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -13,7 +13,7 @@ import sys from mitmproxy import exceptions import mitmproxy.types -def maybequote(value): +def escape_and_quote(value): """ This function takes the output from the lexer and puts it between quotes in the following cases: @@ -245,7 +245,7 @@ class CommandManager(mitmproxy.types._CommandBase): # ctx.log.info('[gilga] before parse.append. value = %s' % parts[i]) parse.append( ParseResult( - value=maybequote(parts[i]), + value=escape_and_quote(parts[i]), type=typ, valid=valid, ) -- cgit v1.2.3 From 561415cea99c46dd5df892bcac148931f70ff3b0 Mon Sep 17 00:00:00 2001 From: Henrique Date: Tue, 12 Nov 2019 21:27:02 -0500 Subject: Created a lexer for the command bar --- mitmproxy/command.py | 55 ++---------------- mitmproxy/lexer.py | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 50 deletions(-) create mode 100644 mitmproxy/lexer.py diff --git a/mitmproxy/command.py b/mitmproxy/command.py index cf345c22..625e87e5 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -11,49 +11,9 @@ import functools import sys from mitmproxy import exceptions +from mitmproxy import lexer import mitmproxy.types -def escape_and_quote(value): - """ - This function takes the output from the lexer and puts it between quotes - in the following cases: - * There is a space in the string: The only way a token from the lexer can have a space in it is if it was between quotes - * There is one or more quotes in the middle of the string: The only way for a token to have a quote in it that is not escaped is if it was escaped prior to being processed by the lexer. For example, the string `"s1 \" s2"` would come back from the lexer as `s1 " s2`. - - Any quotes that are in the middle of the string and that are not escaped will also be escaped (by placing a \ in front of it). - This function only deals with double quotes and they are the only ones that should be used. - """ - - new_value = "" - last_pos = len(value) - 1 - - for pos, char in enumerate(value): - if pos == 0: - new_value += char - continue - - # if pos == last_pos: - # new_value += char - # break - - if char in " \n\r\t": - new_value += char - continue - - if char == '"': - if value[pos-1] != '\\': - new_value += '\\' - - new_value += char - - value = new_value - - if ((" " in value) or ('"' in value)) and not (value.startswith("\"") or value.startswith("'")): - return "\"%s\"" % value - - return value - - def verify_arg_signature(f: typing.Callable, args: list, kwargs: dict) -> None: sig = inspect.signature(f) try: @@ -62,13 +22,8 @@ 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 get_lexer(s): + return lexer.Lexer(s) def typename(t: type) -> str: @@ -199,7 +154,7 @@ class CommandManager(mitmproxy.types._CommandBase): """ buf = io.StringIO(cmdstr) parts: typing.List[str] = [] - lex = lexer(buf) + lex = get_lexer(buf) while 1: remainder = cmdstr[buf.tell():] try: @@ -245,7 +200,7 @@ class CommandManager(mitmproxy.types._CommandBase): # ctx.log.info('[gilga] before parse.append. value = %s' % parts[i]) parse.append( ParseResult( - value=escape_and_quote(parts[i]), + value=parts[i], type=typ, valid=valid, ) diff --git a/mitmproxy/lexer.py b/mitmproxy/lexer.py new file mode 100644 index 00000000..5187a718 --- /dev/null +++ b/mitmproxy/lexer.py @@ -0,0 +1,154 @@ +from enum import Enum +import io +from typing import Union +import pdb + + +class State(Enum): + QUOTE = 1 + ESCAPE = 2 + TEXT = 3 + + +class Lexer: + + def __init__(self, text: Union[str, io.StringIO]): + self._tokens = [] + self._count = 0 + self._parsed = False + + self._state = State.TEXT + self._states = [] + self._text_pos = 0 + self._quote_start_pos = 0 + + if isinstance(text, str): + self.text = io.StringIO(text) + else: + self.text = text + + def __iter__(self): + return self + + def __next__(self): + t = self.get_token() + + if t == '': + raise StopIteration + + return t + + def get_token(self): + + try: + return self.parse() + except ValueError as e: + raise + + if len(self._tokens) > 0: + ret = self._tokens[0] + self._tokens = self._tokens[1:] + else: + ret = None + return ret + + #def get_remainder(self): + # try: + # self.parse() + # except ValueError as e: + # return self.text + # + + # return ' '.join(self._tokens) + + def parse(self): + acc = '' + quote = '' # used by the parser + tokens = [] + self._state = State.TEXT + text = self.text + i = 0 + + #self.text.seek(self._text_pos) + + while True: + ch = self.text.read(1) + self._text_pos += 1 + + #pdb.set_trace() + + + # If this is the last char of the string, let's save the token + if ch == '' or ch is None: + break + + if self._state == State.QUOTE: + if ch == '\\': + self._states.append(self._state) + self._state = State.ESCAPE + acc += ch + elif ch == quote: + self._state = self._states.pop() + acc += ch + else: + acc += ch + + elif self._state == State.ESCAPE: + acc += ch + self._state = self._states.pop() + + elif self._state == State.TEXT: + if ch == ' ': + if acc != '': + break + elif ch == '"' or ch == "'": + quote = ch + self._quote_start_pos = self._text_pos + self._states.append(self._state) + self._state = State.QUOTE + acc += ch + elif ch == '\\': + # TODO: Does it make sense to go to State.ESCAPE from State.TEXT? + self._states.append(self._state) + self._state = State.ESCAPE + acc += ch + else: + acc += ch + else: + print("This shouldn't have happened") + exit(-1) + + self._token = acc + + if self._state == State.QUOTE: + raise ValueError("No closing quotation for quote in position %d" % self._quote_start_pos) + + return self._token + + +if __name__ == '__main__': + + cases = [] + cases.append(r'abc') + cases.append(r'Hello World') + cases.append(r'"Hello \" World"') + cases.append(r"'Hello \' World'") + cases.append(r'"\""') + cases.append(r'abc "def\" \x bla \z \\ \e \ " xpto') + cases.append(r'') + cases.append(r' ') + cases.append(r' ') + cases.append(r' ') + cases.append(r' ') + cases.append(r'Hello World ') + + for s in cases: + lex = Lexer(s) + tokens = list(lex) + + if len(tokens) == 1: + print('%s = %d token' % (str(tokens), len(tokens))) + else: + print('%s = %d tokens' % (str(tokens), len(tokens))) + + -- cgit v1.2.3 From a9596cabe34b07ac45df644c00d57bf1116b8c3e Mon Sep 17 00:00:00 2001 From: Henrique Date: Tue, 12 Nov 2019 21:44:07 -0500 Subject: Small fix to handle line breaks and tabs \r\n\t --- mitmproxy/lexer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mitmproxy/lexer.py b/mitmproxy/lexer.py index 5187a718..31ae382c 100644 --- a/mitmproxy/lexer.py +++ b/mitmproxy/lexer.py @@ -69,6 +69,8 @@ class Lexer: text = self.text i = 0 + whitespace = "\r\n\t " + #self.text.seek(self._text_pos) while True: @@ -98,7 +100,7 @@ class Lexer: self._state = self._states.pop() elif self._state == State.TEXT: - if ch == ' ': + if ch in whitespace: if acc != '': break elif ch == '"' or ch == "'": @@ -141,6 +143,7 @@ if __name__ == '__main__': cases.append(r' ') cases.append(r' ') cases.append(r'Hello World ') + cases.append('\n\n\rHello\n World With Spaces\n\n') for s in cases: lex = Lexer(s) -- cgit v1.2.3 From 561d6d91d126d644a5183af3deadf9f90e5dfc7f Mon Sep 17 00:00:00 2001 From: Henrique Date: Tue, 12 Nov 2019 22:08:10 -0500 Subject: Fixed test to use the new method to get the lexer --- test/mitmproxy/tools/console/test_defaultkeys.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/mitmproxy/tools/console/test_defaultkeys.py b/test/mitmproxy/tools/console/test_defaultkeys.py index 52075c84..be8e39f8 100644 --- a/test/mitmproxy/tools/console/test_defaultkeys.py +++ b/test/mitmproxy/tools/console/test_defaultkeys.py @@ -6,7 +6,6 @@ from mitmproxy import command import pytest - @pytest.mark.asyncio async def test_commands_exist(): km = keymap.Keymap(None) @@ -16,7 +15,8 @@ async def test_commands_exist(): await m.load_flow(tflow()) for binding in km.bindings: - cmd, *args = command.lexer(binding.command) + cmd, *args = command.get_lexer(binding.command) + assert cmd in m.commands.commands cmd_obj = m.commands.commands[cmd] -- cgit v1.2.3 From 55239a8a47746cca838f558ea4140d2187dfc8db Mon Sep 17 00:00:00 2001 From: Henrique Date: Tue, 12 Nov 2019 22:08:42 -0500 Subject: Forgot to remove the import for shlex --- mitmproxy/command.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index 625e87e5..4fbae533 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -5,7 +5,6 @@ import inspect import types import io import typing -import shlex import textwrap import functools import sys -- cgit v1.2.3 From 578eb7239cf073ee9dd526542ca19ff6c23ae61c Mon Sep 17 00:00:00 2001 From: Henrique Date: Tue, 12 Nov 2019 22:09:04 -0500 Subject: Tests for the new lexer --- test/mitmproxy/test_lexer.py | 64 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 test/mitmproxy/test_lexer.py diff --git a/test/mitmproxy/test_lexer.py b/test/mitmproxy/test_lexer.py new file mode 100644 index 00000000..c8b30fc6 --- /dev/null +++ b/test/mitmproxy/test_lexer.py @@ -0,0 +1,64 @@ +from mitmproxy import lexer +import pytest + + +class TestScripts: + + def test_simple(self): + + cases = [ + { + "text": r'abc', + "result": ['abc'] + }, + { + "text": r'"Hello \" Double Quotes"', + "result": ['"Hello \\" Double Quotes"'] + }, + { + "text": r"'Hello \' Single Quotes'", + "result": ["'Hello \\' Single Quotes'"] + }, + { + "text": r'"\""', + "result": ['"\\""'] + }, + { + "text": r'abc "def\" \x bla \z \\ \e \ " xpto', + "result": ['abc', '"def\\" \\x bla \\z \\\\ \\e \\ "', 'xpto'] + }, + { + "text": r'', + "result": [] + }, + { + "text": r' ', + "result": [] + }, + { + "text": r' ', + "result": [] + }, + { + "text": r'Space in the end ', + "result": ['Space', 'in', 'the', 'end'] + }, + { + "text": '\n\n\rHello\n World With Spaces\n\n', + "result": ['Hello', 'World', 'With', 'Spaces'] + }, + ] + + for t in cases: + + lex = lexer.Lexer(t['text']) + tokens = list(lex) + result = t['result'] + assert(tokens == result) + + def test_fail(self): + text = r'"should fail with missing closing quote' + lex = lexer.Lexer(text) + with pytest.raises(ValueError, match="No closing quotation"): + assert list(lex) + -- cgit v1.2.3 From eee4b24e98b76b1eb33804d21264c5117a5c913c Mon Sep 17 00:00:00 2001 From: Henrique Date: Tue, 12 Nov 2019 22:50:33 -0500 Subject: Fixing issues reported by the linter --- mitmproxy/command.py | 1 + mitmproxy/lexer.py | 67 +++--------------------- test/mitmproxy/test_lexer.py | 1 - test/mitmproxy/tools/console/test_defaultkeys.py | 1 + 4 files changed, 8 insertions(+), 62 deletions(-) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index 4fbae533..3da3a298 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -13,6 +13,7 @@ from mitmproxy import exceptions from mitmproxy import lexer import mitmproxy.types + def verify_arg_signature(f: typing.Callable, args: list, kwargs: dict) -> None: sig = inspect.signature(f) try: diff --git a/mitmproxy/lexer.py b/mitmproxy/lexer.py index 31ae382c..a7024ca2 100644 --- a/mitmproxy/lexer.py +++ b/mitmproxy/lexer.py @@ -1,7 +1,6 @@ from enum import Enum import io -from typing import Union -import pdb +from typing import Union, List class State(Enum): @@ -12,13 +11,12 @@ class State(Enum): class Lexer: - def __init__(self, text: Union[str, io.StringIO]): - self._tokens = [] + def __init__(self, text: Union[str, io.StringIO]) -> None: self._count = 0 self._parsed = False self._state = State.TEXT - self._states = [] + self._states: List[State] = [] self._text_pos = 0 self._quote_start_pos = 0 @@ -39,47 +37,24 @@ class Lexer: return t def get_token(self): - try: return self.parse() - except ValueError as e: + except ValueError: raise - if len(self._tokens) > 0: - ret = self._tokens[0] - self._tokens = self._tokens[1:] - else: - ret = None - return ret - - #def get_remainder(self): - # try: - # self.parse() - # except ValueError as e: - # return self.text - # - - # return ' '.join(self._tokens) - def parse(self): acc = '' - quote = '' # used by the parser - tokens = [] + quote = '' self._state = State.TEXT - text = self.text - i = 0 whitespace = "\r\n\t " - #self.text.seek(self._text_pos) + self.text.seek(self._text_pos) while True: ch = self.text.read(1) self._text_pos += 1 - #pdb.set_trace() - - # If this is the last char of the string, let's save the token if ch == '' or ch is None: break @@ -110,7 +85,6 @@ class Lexer: self._state = State.QUOTE acc += ch elif ch == '\\': - # TODO: Does it make sense to go to State.ESCAPE from State.TEXT? self._states.append(self._state) self._state = State.ESCAPE acc += ch @@ -126,32 +100,3 @@ class Lexer: raise ValueError("No closing quotation for quote in position %d" % self._quote_start_pos) return self._token - - -if __name__ == '__main__': - - cases = [] - cases.append(r'abc') - cases.append(r'Hello World') - cases.append(r'"Hello \" World"') - cases.append(r"'Hello \' World'") - cases.append(r'"\""') - cases.append(r'abc "def\" \x bla \z \\ \e \ " xpto') - cases.append(r'') - cases.append(r' ') - cases.append(r' ') - cases.append(r' ') - cases.append(r' ') - cases.append(r'Hello World ') - cases.append('\n\n\rHello\n World With Spaces\n\n') - - for s in cases: - lex = Lexer(s) - tokens = list(lex) - - if len(tokens) == 1: - print('%s = %d token' % (str(tokens), len(tokens))) - else: - print('%s = %d tokens' % (str(tokens), len(tokens))) - - diff --git a/test/mitmproxy/test_lexer.py b/test/mitmproxy/test_lexer.py index c8b30fc6..ae706407 100644 --- a/test/mitmproxy/test_lexer.py +++ b/test/mitmproxy/test_lexer.py @@ -61,4 +61,3 @@ class TestScripts: lex = lexer.Lexer(text) with pytest.raises(ValueError, match="No closing quotation"): assert list(lex) - diff --git a/test/mitmproxy/tools/console/test_defaultkeys.py b/test/mitmproxy/tools/console/test_defaultkeys.py index be8e39f8..035f71f7 100644 --- a/test/mitmproxy/tools/console/test_defaultkeys.py +++ b/test/mitmproxy/tools/console/test_defaultkeys.py @@ -6,6 +6,7 @@ from mitmproxy import command import pytest + @pytest.mark.asyncio async def test_commands_exist(): km = keymap.Keymap(None) -- cgit v1.2.3 From d90262ad35b25a7a4ec0e01a2dd4d4c813729030 Mon Sep 17 00:00:00 2001 From: Henrique Date: Tue, 12 Nov 2019 23:16:52 -0500 Subject: Getting 100% coverage in the lexer --- mitmproxy/lexer.py | 3 --- test/mitmproxy/test_lexer.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/mitmproxy/lexer.py b/mitmproxy/lexer.py index a7024ca2..f123a838 100644 --- a/mitmproxy/lexer.py +++ b/mitmproxy/lexer.py @@ -90,9 +90,6 @@ class Lexer: acc += ch else: acc += ch - else: - print("This shouldn't have happened") - exit(-1) self._token = acc diff --git a/test/mitmproxy/test_lexer.py b/test/mitmproxy/test_lexer.py index ae706407..a1898620 100644 --- a/test/mitmproxy/test_lexer.py +++ b/test/mitmproxy/test_lexer.py @@ -1,5 +1,6 @@ from mitmproxy import lexer import pytest +import io class TestScripts: @@ -47,6 +48,10 @@ class TestScripts: "text": '\n\n\rHello\n World With Spaces\n\n', "result": ['Hello', 'World', 'With', 'Spaces'] }, + { + "text": r'\" Escaping characters without reason', + "result": ['\\"', 'Escaping', 'characters', 'without', 'reason'] + }, ] for t in cases: @@ -61,3 +66,12 @@ class TestScripts: lex = lexer.Lexer(text) with pytest.raises(ValueError, match="No closing quotation"): assert list(lex) + + def test_stringio_text(self): + text = io.StringIO(r'Increase test coverage') + lex = lexer.Lexer(text) + tokens = list(lex) + result = ['Increase', 'test', 'coverage'] + assert(tokens == result) + + -- cgit v1.2.3 From cf6839a9d9e09e2bcb7fcc1acd3ecafa82415748 Mon Sep 17 00:00:00 2001 From: Henrique Date: Wed, 13 Nov 2019 09:26:37 -0500 Subject: Removed an exception handle that can't happen anymore due to the new lexer --- mitmproxy/command.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index 3da3a298..d9ba4055 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -165,8 +165,9 @@ class CommandManager(mitmproxy.types._CommandBase): if not t: break parts.append(t) + if not parts: - parts = [""] + parts = [] elif cmdstr.endswith(" "): parts.append("") @@ -233,20 +234,14 @@ class CommandManager(mitmproxy.types._CommandBase): """ Execute a command string. May raise CommandError. """ - if cmdstr == '': - raise exceptions.CommandError("Invalid command: %s" % cmdstr) - - try: - parts, _ = self.parse_partial(cmdstr) - except ValueError as e: - raise exceptions.CommandError("Command error: %s" % e) - if len(parts) == 0: - raise exceptions.CommandError("Invalid command: %s" % cmdstr) - + 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(params[0], params[1:]) def dump(self, out=sys.stdout) -> None: -- cgit v1.2.3 From 875adb2ba82bb6b94fec755d4f2ac9800066c47d Mon Sep 17 00:00:00 2001 From: Henrique Date: Wed, 13 Nov 2019 09:32:51 -0500 Subject: Added tests to reach 100% coverage --- test/mitmproxy/test_command.py | 6 +++++- test/mitmproxy/test_lexer.py | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/mitmproxy/test_command.py b/test/mitmproxy/test_command.py index 2785e28f..ad475fba 100644 --- a/test/mitmproxy/test_command.py +++ b/test/mitmproxy/test_command.py @@ -139,7 +139,7 @@ class TestCommand: ], [ "", - [command.ParseResult(value = "", type = mitmproxy.types.Cmd, valid = False)], + [], [] ], [ @@ -283,6 +283,10 @@ def test_simple(): c.call("nonexistent") 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/test_lexer.py b/test/mitmproxy/test_lexer.py index a1898620..19ef155b 100644 --- a/test/mitmproxy/test_lexer.py +++ b/test/mitmproxy/test_lexer.py @@ -73,5 +73,3 @@ class TestScripts: tokens = list(lex) result = ['Increase', 'test', 'coverage'] assert(tokens == result) - - -- cgit v1.2.3 From af7088d7f377ff32e2ff9ab197f6c899a835515d Mon Sep 17 00:00:00 2001 From: Henrique Date: Wed, 13 Nov 2019 10:17:07 -0500 Subject: Fixed issue introduced by change in the lexer that caused mitmproxy to crash when pressing `:`. --- mitmproxy/tools/console/commander/commander.py | 35 +++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index 0c043081..ee31e1e9 100644 --- a/mitmproxy/tools/console/commander/commander.py +++ b/mitmproxy/tools/console/commander/commander.py @@ -89,21 +89,28 @@ 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", "")) - ret.append(("text", " ")) - if remhelp: + 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", " ")) - for v in remhelp: - ret.append(("commander_hint", "%s " % v)) + 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", "")) + ret.append(("text", " ")) + if remhelp: + ret.append(("text", " ")) + for v in remhelp: + ret.append(("commander_hint", "%s " % v)) + return ret def flatten(self, txt): -- cgit v1.2.3 From f2b118817efa16c0d019b98cf2d6519b67fe7323 Mon Sep 17 00:00:00 2001 From: Henrique Date: Wed, 13 Nov 2019 10:32:17 -0500 Subject: Added a new test to test that the issue from the previous commit won't happen anymore --- test/mitmproxy/tools/console/test_commander.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py index b5e226fe..81e007f0 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: -- cgit v1.2.3 From 8972250167cfd55dcfcb93b2d3d7b33e0546629d Mon Sep 17 00:00:00 2001 From: Henrique Date: Fri, 15 Nov 2019 13:07:12 -0500 Subject: Removed the custom lexer in favor of using pyparsing. --- mitmproxy/command.py | 75 +++++++---- mitmproxy/lexer.py | 99 --------------- mitmproxy/tools/console/commander/commander.py | 16 +-- test/mitmproxy/test_command.py | 153 ++++++++++++++++++----- test/mitmproxy/test_lexer.py | 75 ----------- test/mitmproxy/tools/console/test_commander.py | 5 - test/mitmproxy/tools/console/test_defaultkeys.py | 12 +- 7 files changed, 193 insertions(+), 242 deletions(-) delete mode 100644 mitmproxy/lexer.py delete mode 100644 test/mitmproxy/test_lexer.py diff --git a/mitmproxy/command.py b/mitmproxy/command.py index d9ba4055..4aa6fdb2 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -3,14 +3,13 @@ """ import inspect import types -import io import typing import textwrap import functools import sys +import pyparsing from mitmproxy import exceptions -from mitmproxy import lexer import mitmproxy.types @@ -22,10 +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 get_lexer(s): - return lexer.Lexer(s) - - def typename(t: type) -> str: """ Translates a type to an explanatory string. @@ -79,6 +74,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] = [] @@ -152,24 +161,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 = get_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("") + + # 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] = [] @@ -180,10 +201,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 @@ -228,6 +254,7 @@ 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): diff --git a/mitmproxy/lexer.py b/mitmproxy/lexer.py deleted file mode 100644 index f123a838..00000000 --- a/mitmproxy/lexer.py +++ /dev/null @@ -1,99 +0,0 @@ -from enum import Enum -import io -from typing import Union, List - - -class State(Enum): - QUOTE = 1 - ESCAPE = 2 - TEXT = 3 - - -class Lexer: - - def __init__(self, text: Union[str, io.StringIO]) -> None: - self._count = 0 - self._parsed = False - - self._state = State.TEXT - self._states: List[State] = [] - self._text_pos = 0 - self._quote_start_pos = 0 - - if isinstance(text, str): - self.text = io.StringIO(text) - else: - self.text = text - - def __iter__(self): - return self - - def __next__(self): - t = self.get_token() - - if t == '': - raise StopIteration - - return t - - def get_token(self): - try: - return self.parse() - except ValueError: - raise - - def parse(self): - acc = '' - quote = '' - self._state = State.TEXT - - whitespace = "\r\n\t " - - self.text.seek(self._text_pos) - - while True: - ch = self.text.read(1) - self._text_pos += 1 - - # If this is the last char of the string, let's save the token - if ch == '' or ch is None: - break - - if self._state == State.QUOTE: - if ch == '\\': - self._states.append(self._state) - self._state = State.ESCAPE - acc += ch - elif ch == quote: - self._state = self._states.pop() - acc += ch - else: - acc += ch - - elif self._state == State.ESCAPE: - acc += ch - self._state = self._states.pop() - - elif self._state == State.TEXT: - if ch in whitespace: - if acc != '': - break - elif ch == '"' or ch == "'": - quote = ch - self._quote_start_pos = self._text_pos - self._states.append(self._state) - self._state = State.QUOTE - acc += ch - elif ch == '\\': - self._states.append(self._state) - self._state = State.ESCAPE - acc += ch - else: - acc += ch - - self._token = acc - - if self._state == State.QUOTE: - raise ValueError("No closing quotation for quote in position %d" % self._quote_start_pos) - - return self._token diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index ee31e1e9..fa67407e 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: CompletionState = None @@ -105,7 +105,7 @@ class CommandBuffer: ret.append(("commander_invalid", p.value)) else: ret.append(("text", "")) - ret.append(("text", " ")) + if remhelp: ret.append(("text", " ")) for v in remhelp: @@ -113,11 +113,6 @@ class CommandBuffer: return ret - def flatten(self, txt): - parts, _ = self.parse_quoted(txt) - ret = [x.value for x in parts] - return " ".join(ret) - def left(self) -> None: self.cursor = self.cursor - 1 @@ -141,7 +136,7 @@ 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: @@ -155,6 +150,11 @@ class CommandBuffer: """ Inserts text at the cursor. """ + + # We don't want to insert a space before the command + if k == ' ' and self.text[0:self.cursor].strip() == '': + return + self.text = self.text[:self.cursor] + k + self.text[self.cursor:] self.cursor += len(k) self.completion = None diff --git a/test/mitmproxy/test_command.py b/test/mitmproxy/test_command.py index ad475fba..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) ], [], @@ -146,6 +144,7 @@ class TestCommand: "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,15 +227,17 @@ 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"] @@ -252,11 +246,112 @@ class TestCommand: "flow \"three four\"", [ command.ParseResult(value = "flow", type = mitmproxy.types.Cmd, valid = True), + 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: diff --git a/test/mitmproxy/test_lexer.py b/test/mitmproxy/test_lexer.py deleted file mode 100644 index 19ef155b..00000000 --- a/test/mitmproxy/test_lexer.py +++ /dev/null @@ -1,75 +0,0 @@ -from mitmproxy import lexer -import pytest -import io - - -class TestScripts: - - def test_simple(self): - - cases = [ - { - "text": r'abc', - "result": ['abc'] - }, - { - "text": r'"Hello \" Double Quotes"', - "result": ['"Hello \\" Double Quotes"'] - }, - { - "text": r"'Hello \' Single Quotes'", - "result": ["'Hello \\' Single Quotes'"] - }, - { - "text": r'"\""', - "result": ['"\\""'] - }, - { - "text": r'abc "def\" \x bla \z \\ \e \ " xpto', - "result": ['abc', '"def\\" \\x bla \\z \\\\ \\e \\ "', 'xpto'] - }, - { - "text": r'', - "result": [] - }, - { - "text": r' ', - "result": [] - }, - { - "text": r' ', - "result": [] - }, - { - "text": r'Space in the end ', - "result": ['Space', 'in', 'the', 'end'] - }, - { - "text": '\n\n\rHello\n World With Spaces\n\n', - "result": ['Hello', 'World', 'With', 'Spaces'] - }, - { - "text": r'\" Escaping characters without reason', - "result": ['\\"', 'Escaping', 'characters', 'without', 'reason'] - }, - ] - - for t in cases: - - lex = lexer.Lexer(t['text']) - tokens = list(lex) - result = t['result'] - assert(tokens == result) - - def test_fail(self): - text = r'"should fail with missing closing quote' - lex = lexer.Lexer(text) - with pytest.raises(ValueError, match="No closing quotation"): - assert list(lex) - - def test_stringio_text(self): - text = io.StringIO(r'Increase test coverage') - lex = lexer.Lexer(text) - tokens = list(lex) - result = ['Increase', 'test', 'coverage'] - assert(tokens == result) diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py index 81e007f0..798ca5fe 100644 --- a/test/mitmproxy/tools/console/test_commander.py +++ b/test/mitmproxy/tools/console/test_commander.py @@ -165,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 035f71f7..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,7 +18,10 @@ async def test_commands_exist(): await m.load_flow(tflow()) for binding in km.bindings: - cmd, *args = command.get_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 @@ -24,4 +29,7 @@ async def test_commands_exist(): try: cmd_obj.prepare_args(args) except Exception as e: + + import pdb + pdb.set_trace() raise ValueError("Invalid command: {}".format(binding.command)) from e -- cgit v1.2.3 From 20bd33499fe581bf3dd39ed211b822ec63e14bee Mon Sep 17 00:00:00 2001 From: Henrique Date: Fri, 15 Nov 2019 14:09:05 -0500 Subject: Small fix for 100% coverage --- mitmproxy/command.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index c7a45587..e75e6d26 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -173,16 +173,9 @@ class CommandManager(mitmproxy.types._CommandBase): 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 = [] -- cgit v1.2.3 From a244ece0e55db1ac4895e3ee20b138ab3c617ca2 Mon Sep 17 00:00:00 2001 From: Henrique Date: Fri, 15 Nov 2019 18:15:56 -0500 Subject: Removed useless else that prevented 100% code coverage --- mitmproxy/tools/console/commander/commander.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index 4909348f..a13e5792 100644 --- a/mitmproxy/tools/console/commander/commander.py +++ b/mitmproxy/tools/console/commander/commander.py @@ -103,8 +103,6 @@ class CommandBuffer: ret.append(("text", p.value)) elif p.value: ret.append(("commander_invalid", p.value)) - else: - ret.append(("text", "")) if remhelp: ret.append(("text", " ")) -- cgit v1.2.3 From 79caf3a458a221763cbf389f8ac5cccd9221e290 Mon Sep 17 00:00:00 2001 From: Henrique Date: Sat, 16 Nov 2019 09:16:50 -0500 Subject: Fixing issues pointed during PR review --- mitmproxy/command.py | 70 ++++++++++-------------- test/mitmproxy/test_command.py | 14 ++--- test/mitmproxy/tools/console/test_commander.py | 18 ++++++ test/mitmproxy/tools/console/test_defaultkeys.py | 3 - 4 files changed, 55 insertions(+), 50 deletions(-) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index e75e6d26..24f0decf 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -13,6 +13,11 @@ from mitmproxy import exceptions import mitmproxy.types +@functools.lru_cache(maxsize=128) +def _parse_cmd(cmdstr: str): + return parts + + def verify_arg_signature(f: typing.Callable, args: list, kwargs: dict) -> None: sig = inspect.signature(f) try: @@ -80,16 +85,7 @@ class Command: # 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 - + args = [a for a in args if a.strip() != ''] verify_arg_signature(self.func, list(args), {}) remainder: typing.Sequence[str] = [] @@ -136,6 +132,13 @@ class CommandManager(mitmproxy.types._CommandBase): self.master = master self.commands: typing.Dict[str, Command] = {} + self.regex = 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(" \r\n\t") + self.regex = self.regex.leaveWhitespace() + def collect_commands(self, addon): for i in dir(addon): if not i.startswith("__"): @@ -156,6 +159,7 @@ class CommandManager(mitmproxy.types._CommandBase): def add(self, path: str, func: typing.Callable): self.commands[path] = Command(self, path, func) + @functools.lru_cache(maxsize=128) def parse_partial( self, cmdstr: str @@ -164,48 +168,36 @@ class CommandManager(mitmproxy.types._CommandBase): Parse a possibly partial command. Return a sequence of ParseResults and a sequence of remainder type help items. """ parts: typing.List[str] = [] - - 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() - - for t, start, end in rex.scanString(cmdstr): + for t, start, end in self.regex.scanString(cmdstr): parts.append(t[0]) - if not parts: - 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 + # while True: + # if parts and parts[0].strip() == '': + # del parts[0] + # else: + # break parse: typing.List[ParseResult] = [] params: typing.List[type] = [] typ: typing.Type + cmd_found: bool = False for i in range(len(parts)): - if i == 0: - typ = mitmproxy.types.Cmd - if parts[i] in self.commands: - params.extend(self.commands[parts[i]].paramtypes) - elif params: - if parts[i].strip() != '': + if not parts[i].isspace(): + if not cmd_found: + cmd_found = True + typ = mitmproxy.types.Cmd + 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 - 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: + # 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 to = mitmproxy.types.CommandTypes.get(typ, None) @@ -218,8 +210,6 @@ 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], diff --git a/test/mitmproxy/test_command.py b/test/mitmproxy/test_command.py index ae4c400c..f5a641a8 100644 --- a/test/mitmproxy/test_command.py +++ b/test/mitmproxy/test_command.py @@ -92,8 +92,6 @@ class TestCommand: c = command.Command(cm, "varargs", a.varargs) assert c.signature_help() == "varargs str *str -> [str]" assert c.call(["one", "two", "three"]) == ["two", "three"] - with pytest.raises(exceptions.CommandError): - c.call(["one", "two", 3]) def test_call(self): with taddons.context() as tctx: @@ -333,18 +331,20 @@ class TestCommand: [], ], [ - " spaces_at_the_begining_are_stripped", + " spaces_at_the_begining_are_not_stripped", [ - 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 = "spaces_at_the_begining_are_not_stripped", type = mitmproxy.types.Cmd, valid = False), ], [], ], [ - " spaces_at_the_begining_are_stripped but_not_at_the_end ", + " spaces_at_the_begining_are_not_stripped neither_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 = "spaces_at_the_begining_are_not_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 = "neither_at_the_end", type = mitmproxy.types.Unknown, valid = False), command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), ], [], diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py index 798ca5fe..8c3e6839 100644 --- a/test/mitmproxy/tools/console/test_commander.py +++ b/test/mitmproxy/tools/console/test_commander.py @@ -165,3 +165,21 @@ class TestCommandBuffer: cb = commander.CommandBuffer(tctx.master) cb.text = "foo" assert cb.render() + + cb.text = 'set view_filter=~bq test' + ret = cb.render() + assert ret[0] == ('commander_command', 'set') + assert ret[1] == ('commander_invalid', ' ') + assert ret[2] == ('text', 'view_filter=~bq') + assert ret[3] == ('commander_invalid', ' ') + assert ret[4] == ('commander_invalid', 'test') + + cb.text = "set" + ret = cb.render() + assert ret[0] == ('commander_command', 'set') + assert ret[1] == ('text', ' ') + assert ret[2] == ('commander_hint', 'str ') + + # import pdb + # pdb.set_trace() + # print('x') diff --git a/test/mitmproxy/tools/console/test_defaultkeys.py b/test/mitmproxy/tools/console/test_defaultkeys.py index 7e8df6b6..40e536b0 100644 --- a/test/mitmproxy/tools/console/test_defaultkeys.py +++ b/test/mitmproxy/tools/console/test_defaultkeys.py @@ -29,7 +29,4 @@ async def test_commands_exist(): try: cmd_obj.prepare_args(args) except Exception as e: - - import pdb - pdb.set_trace() raise ValueError("Invalid command: {}".format(binding.command)) from e -- cgit v1.2.3 From a0ef36727e8c7910ddec3941dcd0ead7cc3a44c7 Mon Sep 17 00:00:00 2001 From: Henrique Date: Sat, 16 Nov 2019 09:18:32 -0500 Subject: Forgot to remove the empty `_parse_cmd` function --- mitmproxy/command.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index 24f0decf..a8a2f3dc 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -13,11 +13,6 @@ from mitmproxy import exceptions import mitmproxy.types -@functools.lru_cache(maxsize=128) -def _parse_cmd(cmdstr: str): - return parts - - def verify_arg_signature(f: typing.Callable, args: list, kwargs: dict) -> None: sig = inspect.signature(f) try: -- cgit v1.2.3 From 373cc945c0fb15d0713166019ae0132f07c469e2 Mon Sep 17 00:00:00 2001 From: Henrique Date: Sat, 16 Nov 2019 13:14:57 -0500 Subject: Removing dead code --- mitmproxy/command.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index a8a2f3dc..32a9c9c1 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -166,14 +166,6 @@ class CommandManager(mitmproxy.types._CommandBase): for t, start, end in self.regex.scanString(cmdstr): parts.append(t[0]) - # 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] = [] typ: typing.Type -- cgit v1.2.3 From 7779eef572e8deeae895ea6d700265e6f9b432c8 Mon Sep 17 00:00:00 2001 From: Henrique Date: Sat, 16 Nov 2019 17:01:47 -0500 Subject: Various changes to address PR comments Made a change to make `CommandManager.execute` the main entry point for executing commands and made `call_strings` into a private method. --- mitmproxy/command.py | 15 ++++++++------- mitmproxy/tools/console/consoleaddons.py | 19 ++++++------------- mitmproxy/types.py | 6 +++--- test/mitmproxy/addons/test_save.py | 2 +- test/mitmproxy/tools/console/test_defaultkeys.py | 2 +- 5 files changed, 19 insertions(+), 25 deletions(-) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index 32a9c9c1..609e288c 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -128,10 +128,10 @@ class CommandManager(mitmproxy.types._CommandBase): self.commands: typing.Dict[str, Command] = {} self.regex = 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(" \r\n\t") + pyparsing.QuotedString("'", escChar='\\', unquoteResults=False) |\ + pyparsing.Combine(pyparsing.Literal('"') + pyparsing.Word(pyparsing.printables + " ") + pyparsing.StringEnd()) |\ + pyparsing.Word(pyparsing.printables) |\ + pyparsing.Word(" \r\n\t") self.regex = self.regex.leaveWhitespace() def collect_commands(self, addon): @@ -220,7 +220,7 @@ class CommandManager(mitmproxy.types._CommandBase): raise exceptions.CommandError("Unknown command: %s" % path) return self.commands[path].func(*args) - def call_strings(self, path: str, args: typing.Sequence[str]) -> typing.Any: + def _call_strings(self, path: str, args: typing.Sequence[str]) -> typing.Any: """ Call a command using a list of string arguments. May raise CommandError. """ @@ -236,12 +236,13 @@ class CommandManager(mitmproxy.types._CommandBase): parts, _ = self.parse_partial(cmdstr) params = [] for p in parts: - params.append(p.value) + if p.value.strip() != '': + params.append(p.value) if len(parts) == 0: raise exceptions.CommandError("Invalid command: %s" % cmdstr) - return self.call_strings(params[0], params[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/consoleaddons.py b/mitmproxy/tools/console/consoleaddons.py index 9f595b42..967c2a35 100644 --- a/mitmproxy/tools/console/consoleaddons.py +++ b/mitmproxy/tools/console/consoleaddons.py @@ -271,7 +271,7 @@ class ConsoleAddon: command, then invoke another command with all occurrences of {choice} replaced by the choice the user made. """ - choices = ctx.master.commands.call_strings(choicecmd, []) + choices = ctx.master.commands.execute(choicecmd) def callback(opt): # We're now outside of the call context... @@ -535,10 +535,8 @@ class ConsoleAddon: raise exceptions.CommandError("Invalid flowview mode.") try: - self.master.commands.call_strings( - "view.settings.setval", - ["@focus", "flowview_mode_%s" % idx, mode] - ) + cmd = 'view.settings.setval @focus flowview_mode_%s %s' % (idx, mode) + self.master.commands.execute(cmd) except exceptions.CommandError as e: signals.status_message.send(message=str(e)) @@ -558,14 +556,9 @@ class ConsoleAddon: if not fv: raise exceptions.CommandError("Not viewing a flow.") idx = fv.body.tab_offset - return self.master.commands.call_strings( - "view.settings.getval", - [ - "@focus", - "flowview_mode_%s" % idx, - self.master.options.console_default_contentview, - ] - ) + + cmd = 'view.settings.getval @focus flowview_mode_%s %s' % (idx, self.master.options.console_default_contentview) + return self.master.commands.execute(cmd) @command.command("console.key.contexts") def key_contexts(self) -> typing.Sequence[str]: diff --git a/mitmproxy/types.py b/mitmproxy/types.py index 0634e4d7..b48aef84 100644 --- a/mitmproxy/types.py +++ b/mitmproxy/types.py @@ -47,7 +47,7 @@ class Choice: class _CommandBase: commands: typing.MutableMapping[str, typing.Any] = {} - def call_strings(self, path: str, args: typing.Sequence[str]) -> typing.Any: + def _call_strings(self, path: str, args: typing.Sequence[str]) -> typing.Any: raise NotImplementedError def execute(self, cmd: str) -> typing.Any: @@ -337,7 +337,7 @@ class _FlowType(_BaseFlowType): def parse(self, manager: _CommandBase, t: type, s: str) -> flow.Flow: try: - flows = manager.call_strings("view.flows.resolve", [s]) + flows = manager.execute("view.flows.resolve %s" % (s)) except exceptions.CommandError as e: raise exceptions.TypeError from e if len(flows) != 1: @@ -356,7 +356,7 @@ class _FlowsType(_BaseFlowType): def parse(self, manager: _CommandBase, t: type, s: str) -> typing.Sequence[flow.Flow]: try: - return manager.call_strings("view.flows.resolve", [s]) + return manager.execute("view.flows.resolve %s" % (s)) except exceptions.CommandError as e: raise exceptions.TypeError from e diff --git a/test/mitmproxy/addons/test_save.py b/test/mitmproxy/addons/test_save.py index 4aa1f648..6727a96f 100644 --- a/test/mitmproxy/addons/test_save.py +++ b/test/mitmproxy/addons/test_save.py @@ -73,7 +73,7 @@ def test_save_command(tmpdir): v = view.View() tctx.master.addons.add(v) 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): diff --git a/test/mitmproxy/tools/console/test_defaultkeys.py b/test/mitmproxy/tools/console/test_defaultkeys.py index 40e536b0..9c79525b 100644 --- a/test/mitmproxy/tools/console/test_defaultkeys.py +++ b/test/mitmproxy/tools/console/test_defaultkeys.py @@ -18,7 +18,7 @@ async def test_commands_exist(): await m.load_flow(tflow()) for binding in km.bindings: - results = command_manager.parse_partial(binding.command) + results = command_manager.parse_partial(binding.command.strip()) cmd = results[0][0].value args = [a.value for a in results[0][1:]] -- cgit v1.2.3 From 13fe07f48f4fa191b8596aa94cbe743a3c3344fa Mon Sep 17 00:00:00 2001 From: Henrique Date: Sat, 16 Nov 2019 20:14:38 -0500 Subject: Brought coverage up to 94% on test_commander.py --- test/mitmproxy/tools/console/test_commander.py | 107 ++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 4 deletions(-) diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py index 8c3e6839..9a2ec102 100644 --- a/test/mitmproxy/tools/console/test_commander.py +++ b/test/mitmproxy/tools/console/test_commander.py @@ -39,6 +39,100 @@ class TestCommandEdit: 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: def fill_history(self, commands): @@ -160,6 +254,15 @@ class TestCommandBuffer: cb.cursor = len(cb.text) 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[0] == ('commander_command', 'set') + assert ret[1] == ('text', ' ') + assert ret[2] == ('commander_hint', 'str ') + def test_render(self): with taddons.context() as tctx: cb = commander.CommandBuffer(tctx.master) @@ -179,7 +282,3 @@ class TestCommandBuffer: assert ret[0] == ('commander_command', 'set') assert ret[1] == ('text', ' ') assert ret[2] == ('commander_hint', 'str ') - - # import pdb - # pdb.set_trace() - # print('x') -- cgit v1.2.3 From fbcaab2abaf713d9e11770b0cb22c9068dcef2ae Mon Sep 17 00:00:00 2001 From: Henrique Date: Sat, 16 Nov 2019 20:15:27 -0500 Subject: Added return signature to methods on commander.py --- mitmproxy/tools/console/commander/commander.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index a13e5792..bff58605 100644 --- a/mitmproxy/tools/console/commander/commander.py +++ b/mitmproxy/tools/console/commander/commander.py @@ -207,7 +207,7 @@ class CommandEdit(urwid.WidgetWrap): self.history = history self.update() - def keypress(self, size, key): + def keypress(self, size, key) -> None: if key == "backspace": self.cbuf.backspace() elif key == "left": @@ -225,21 +225,21 @@ class CommandEdit(urwid.WidgetWrap): self.cbuf.insert(key) self.update() - def update(self): + def update(self) -> None: 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 canv = self._w.render((maxcol,)) canv = urwid.CompositeCanvas(canv) canv.cursor = self.get_cursor_coords((maxcol,)) 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) trans = self._w.get_line_translation(size[0]) x, y = calc_coords(self._w.get_text()[0], trans, p) return x, y - def get_edit_text(self): + def get_edit_text(self) -> str: return self.cbuf.text -- cgit v1.2.3 From 39a6d4860ce48e4663a7aa91651dbea639e93e96 Mon Sep 17 00:00:00 2001 From: Henrique Date: Sun, 17 Nov 2019 10:24:49 -0500 Subject: Fixed issue with string parameters between quotes that do not have a space --- mitmproxy/command.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index 609e288c..f2a87e1e 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -236,8 +236,20 @@ class CommandManager(mitmproxy.types._CommandBase): parts, _ = self.parse_partial(cmdstr) params = [] for p in parts: - if p.value.strip() != '': - params.append(p.value) + v = p.value.strip() + if v != '': + if ((v.startswith("'") and v.endswith("'")) or + (v.startswith("\"") and v.endswith("\""))) and \ + len(v.split(' ')) == 1: + # If this parameter is between quotes but has no spaces in + # it, then it is safe to remove the quotes to pass it down + # This allows any commands that take a simple spaceless + # string as a parameter to work. For example + # view.flows.create get "http://www.example.com" won't work + # if the quotes are there as it won't see the param as a URL + v = v[1:-1] + + params.append(v) if len(parts) == 0: raise exceptions.CommandError("Invalid command: %s" % cmdstr) -- cgit v1.2.3 From 8b52ea248e690df0d9892e9c5d9f88eda2486275 Mon Sep 17 00:00:00 2001 From: Henrique Date: Sun, 17 Nov 2019 11:26:20 -0500 Subject: Added coverage for the changes made --- test/mitmproxy/test_command.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/mitmproxy/test_command.py b/test/mitmproxy/test_command.py index f5a641a8..eb3857bf 100644 --- a/test/mitmproxy/test_command.py +++ b/test/mitmproxy/test_command.py @@ -367,6 +367,8 @@ def test_simple(): c.add("one.two", a.cmd1) 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 bar\"") == "ret \"foo bar\"") assert(c.call("one.two", "foo") == "ret foo") with pytest.raises(exceptions.CommandError, match="Unknown"): c.execute("nonexistent") @@ -382,6 +384,8 @@ def test_simple(): 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.execute("empty") -- cgit v1.2.3 From cb22fc68d1666c54f55df2c4274a5bc63f7b2110 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 18 Nov 2019 02:52:20 +0100 Subject: adjust remote debug example to latest pycharm version --- examples/complex/remote_debug.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/complex/remote_debug.py b/examples/complex/remote_debug.py index fa6f3d33..4b117bdb 100644 --- a/examples/complex/remote_debug.py +++ b/examples/complex/remote_debug.py @@ -15,5 +15,5 @@ Usage: def load(l): - import pydevd - pydevd.settrace("localhost", port=5678, stdoutToServer=True, stderrToServer=True) + import pydevd_pycharm + pydevd_pycharm.settrace("localhost", port=5678, stdoutToServer=True, stderrToServer=True, suspend=False) -- cgit v1.2.3 From 8a6370f1c2ae55fb03f606cb055de455ffe25eec Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 18 Nov 2019 02:55:37 +0100 Subject: make command parameter names more descriptive --- mitmproxy/addons/core.py | 43 +++++++++++------------ mitmproxy/addons/export.py | 20 +++++------ mitmproxy/addons/view.py | 58 ++++++++++++++++---------------- mitmproxy/tools/console/consoleaddons.py | 48 +++++++++++++------------- 4 files changed, 85 insertions(+), 84 deletions(-) diff --git a/mitmproxy/addons/core.py b/mitmproxy/addons/core.py index 5c9bbcd0..1fbeb1e0 100644 --- a/mitmproxy/addons/core.py +++ b/mitmproxy/addons/core.py @@ -83,7 +83,7 @@ class Core: ) @command.command("set") - def set(self, *spec: str) -> None: + def set(self, *options: str) -> None: """ Set an option of the form "key[=value]". When the value is omitted, booleans are set to true, strings and integers are set to None (if @@ -91,7 +91,7 @@ class Core: false or toggle. If multiple specs are passed, they are joined into one separated by spaces. """ - strspec = " ".join(spec) + strspec = " ".join(options) try: ctx.options.set(strspec) except exceptions.OptionsError as e: @@ -109,14 +109,14 @@ class Core: # FIXME: this will become view.mark later @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. """ updated = [] for i in flows: - if i.marked != val: - i.marked = val + if i.marked != boolean: + i.marked = boolean updated.append(i) ctx.master.addons.trigger("update", updated) @@ -168,19 +168,20 @@ class Core: "reason", ] - @command.command("flow.set") - @command.argument("spec", type=mitmproxy.types.Choice("flow.set.options")) + @command.command( + "flow.set") + @command.argument("attr", type=mitmproxy.types.Choice("flow.set.options")) def flow_set( self, flows: typing.Sequence[flow.Flow], - spec: str, - sval: str + attr: str, + value: str ) -> None: """ Quickly set a number of common values on flows. """ - val: typing.Union[int, str] = sval - if spec == "status_code": + val: typing.Union[int, str] = value + if attr == "status_code": try: val = int(val) # type: ignore except ValueError as v: @@ -193,13 +194,13 @@ class Core: req = getattr(f, "request", None) rupdate = True if req: - if spec == "method": + if attr == "method": req.method = val - elif spec == "host": + elif attr == "host": req.host = val - elif spec == "path": + elif attr == "path": req.path = val - elif spec == "url": + elif attr == "url": try: req.url = val except ValueError as e: @@ -212,11 +213,11 @@ class Core: resp = getattr(f, "response", None) supdate = True if resp: - if spec == "status_code": + if attr == "status_code": resp.status_code = val if val in status_codes.RESPONSES: resp.reason = status_codes.RESPONSES[val] # type: ignore - elif spec == "reason": + elif attr == "reason": resp.reason = val else: supdate = False @@ -225,7 +226,7 @@ class Core: updated.append(f) 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") def decode(self, flows: typing.Sequence[flow.Flow], part: str) -> None: @@ -262,12 +263,12 @@ class Core: ctx.log.alert("Toggled encoding on %s flows." % len(updated)) @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( self, flows: typing.Sequence[flow.Flow], part: str, - enc: str, + encoding: str, ) -> None: """ Encode flows with a specified encoding. @@ -279,7 +280,7 @@ class Core: current_enc = p.headers.get("content-encoding", "identity") if current_enc == "identity": f.backup() - p.encode(enc) + p.encode(encoding) updated.append(f) ctx.master.addons.trigger("update", updated) ctx.log.alert("Encoded %s flows." % len(updated)) diff --git a/mitmproxy/addons/export.py b/mitmproxy/addons/export.py index 2776118a..74f36d03 100644 --- a/mitmproxy/addons/export.py +++ b/mitmproxy/addons/export.py @@ -73,14 +73,14 @@ class Export(): return list(sorted(formats.keys())) @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. """ - if fmt not in formats: - raise exceptions.CommandError("No such export format: %s" % fmt) - func: typing.Any = formats[fmt] - v = func(f) + if format not in formats: + raise exceptions.CommandError("No such export format: %s" % format) + func: typing.Any = formats[format] + v = func(flow) try: with open(path, "wb") as fp: if isinstance(v, bytes): @@ -91,14 +91,14 @@ class Export(): ctx.log.error(str(e)) @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. """ - if fmt not in formats: - raise exceptions.CommandError("No such export format: %s" % fmt) - func: typing.Any = formats[fmt] - v = strutils.always_str(func(f)) + if format not in formats: + raise exceptions.CommandError("No such export format: %s" % format) + func: typing.Any = formats[format] + v = strutils.always_str(func(flow)) try: pyperclip.copy(v) except pyperclip.PyperclipException as e: diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index da9d19f9..c57c34c8 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -217,7 +217,7 @@ class View(collections.abc.Sequence): # Focus @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 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: return - if dst < 0: - dst = len(self) + dst - if dst < 0: - dst = 0 - if dst > len(self) - 1: - dst = len(self) - 1 - self.focus.flow = self[dst] + if offset < 0: + offset = len(self) + offset + if offset < 0: + offset = 0 + if offset > len(self) - 1: + offset = len(self) - 1 + self.focus.flow = self[offset] @command.command("view.focus.next") def focus_next(self) -> None: @@ -266,20 +266,20 @@ class View(collections.abc.Sequence): return list(sorted(self.orders.keys())) @command.command("view.order.reverse") - def set_reversed(self, value: bool) -> None: - self.order_reversed = value + def set_reversed(self, boolean: bool) -> None: + self.order_reversed = boolean self.sig_view_refresh.send(self) @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. """ - if order not in self.orders: + if order_key not in self.orders: 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 newview = sortedcontainers.SortedListWithKey(key=order_key) newview.update(self._view) @@ -298,16 +298,16 @@ class View(collections.abc.Sequence): # Filter @command.command("view.filter.set") - def set_filter_cmd(self, f: str) -> None: + def set_filter_cmd(self, filtstr: str) -> None: """ Sets the current view filter. """ filt = None - if f: - filt = flowfilter.parse(f) + if filtstr: + filt = flowfilter.parse(filtstr) if not filt: raise exceptions.CommandError( - "Invalid interception filter: %s" % f + "Invalid interception filter: %s" % filtstr ) self.set_filter(filt) @@ -340,11 +340,11 @@ class View(collections.abc.Sequence): # View Settings @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. """ - return self.settings[f].get(key, default) + return self.settings[flow].get(key, default) @command.command("view.settings.setval.toggle") def setvalue_toggle( @@ -412,26 +412,26 @@ class View(collections.abc.Sequence): ctx.log.alert("Removed %s flows" % len(flows)) @command.command("view.flows.resolve") - def resolve(self, spec: str) -> typing.Sequence[mitmproxy.flow.Flow]: + def resolve(self, flowspec: str) -> typing.Sequence[mitmproxy.flow.Flow]: """ Resolve a flow list specification to an actual list of flows. """ - if spec == "@all": + if flowspec == "@all": return [i for i in self._store.values()] - if spec == "@focus": + if flowspec == "@focus": return [self.focus.flow] if self.focus.flow else [] - elif spec == "@shown": + elif flowspec == "@shown": return [i for i in self] - elif spec == "@hidden": + elif flowspec == "@hidden": return [i for i in self._store.values() if i not in self._view] - elif spec == "@marked": + elif flowspec == "@marked": return [i for i in self._store.values() if i.marked] - elif spec == "@unmarked": + elif flowspec == "@unmarked": return [i for i in self._store.values() if not i.marked] else: - filt = flowfilter.parse(spec) + filt = flowfilter.parse(flowspec) if not filt: - raise exceptions.CommandError("Invalid flow filter: %s" % spec) + raise exceptions.CommandError("Invalid flow filter: %s" % flowspec) return [i for i in self._store.values() if filt(i)] @command.command("view.flows.create") diff --git a/mitmproxy/tools/console/consoleaddons.py b/mitmproxy/tools/console/consoleaddons.py index 967c2a35..9e0533a4 100644 --- a/mitmproxy/tools/console/consoleaddons.py +++ b/mitmproxy/tools/console/consoleaddons.py @@ -287,21 +287,21 @@ class ConsoleAddon: ) @command.command("console.command") - def console_command(self, *partial: str) -> None: + def console_command(self, *cmdstr: str) -> None: """ Prompt the user to edit a command with a (possibly empty) starting value. """ - signals.status_prompt_command.send(partial=" ".join(partial)) # type: ignore + signals.status_prompt_command.send(partial=" ".join(cmdstr)) # type: ignore @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]". """ - option_value = getattr(self.master.options, option, None) + option_value = getattr(self.master.options, option_name, None) current_value = option_value if option_value else "" self.master.commands.execute( - "console.command set %s=%s" % (option, current_value) + "console.command set %s=%s" % (option_name, current_value) ) @command.command("console.view.keybindings") @@ -351,14 +351,14 @@ class ConsoleAddon: @command.command("console.bodyview") @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 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 if necessary. """ - fpart = getattr(f, part, None) + fpart = getattr(flow, part, None) if not fpart: raise exceptions.CommandError("Part must be either request or response, not %s." % part) t = fpart.headers.get("content-type") @@ -397,8 +397,8 @@ class ConsoleAddon: ] @command.command("console.edit.focus") - @command.argument("part", type=mitmproxy.types.Choice("console.edit.focus.options")) - def edit_focus(self, part: str) -> None: + @command.argument("flow_part", type=mitmproxy.types.Choice("console.edit.focus.options")) + def edit_focus(self, flow_part: str) -> None: """ Edit a component of the currently focused flow. """ @@ -410,27 +410,27 @@ class ConsoleAddon: flow.backup() require_dummy_response = ( - part in ("response-headers", "response-body", "set-cookies") and - flow.response is None + flow_part in ("response-headers", "response-body", "set-cookies") and + flow.response is None ) if require_dummy_response: flow.response = http.HTTPResponse.make() - if part == "cookies": + if flow_part == "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") - elif part == "multipart form": + elif flow_part == "multipart form": self.master.switch_view("edit_focus_multipart_form") - elif part == "path": + elif flow_part == "path": self.master.switch_view("edit_focus_path") - elif part == "query": + elif flow_part == "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") - elif part == "response-headers": + elif flow_part == "response-headers": self.master.switch_view("edit_focus_response_headers") - elif part in ("request-body", "response-body"): - if part == "request-body": + elif flow_part in ("request-body", "response-body"): + if flow_part == "request-body": message = flow.request else: message = flow.response @@ -442,16 +442,16 @@ class ConsoleAddon: # just strip the newlines off the end of the body when we return # from an editor. message.content = c.rstrip(b"\n") - elif part == "set-cookies": + elif flow_part == "set-cookies": self.master.switch_view("edit_focus_setcookies") - elif part == "url": + elif flow_part == "url": url = flow.request.url.encode() edited_url = self.master.spawn_editor(url) url = edited_url.rstrip(b"\n") flow.request.url = url.decode() - elif part in ["method", "status_code", "reason"]: + elif flow_part in ["method", "status_code", "reason"]: self.master.commands.execute( - "console.command flow.set @focus %s " % part + "console.command flow.set @focus %s " % flow_part ) def _grideditor(self): -- cgit v1.2.3 From cb723c53fab93dae67f68b4414a35c8bec7fc144 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 18 Nov 2019 02:55:51 +0100 Subject: revamp command processing - Display the parameter name instead of the parameter type whenver users interact with commands. This makes it easy to enter commands just by their signature. We may want to expose type information in the command list, but some quick testing showed that this are rather intuitive anyways. - Add shift tab backward cycling for the command completion. - Use inspect.Signature instead of homebrew argument matching solution. This gets rid of quite a bit of cruft. - Remove some type checking hacks in mitmproxy.types --- mitmproxy/command.py | 333 +++++++++++++------------ mitmproxy/tools/console/commander/commander.py | 101 ++++---- mitmproxy/tools/console/commands.py | 23 +- mitmproxy/tools/console/consoleaddons.py | 6 +- mitmproxy/types.py | 123 +++++---- test/mitmproxy/test_command.py | 250 +++++++++++-------- test/mitmproxy/test_types.py | 16 +- 7 files changed, 446 insertions(+), 406 deletions(-) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index f2a87e1e..a64c7404 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -1,19 +1,20 @@ """ This module manages and invokes typed commands. """ +import functools import inspect +import sys +import textwrap import types import typing -import textwrap -import functools -import sys + import pyparsing -from mitmproxy import exceptions import mitmproxy.types +from mitmproxy import exceptions -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) try: sig.bind(*args, **kwargs) @@ -33,122 +34,135 @@ def typename(t: type) -> str: return to.display +def _empty_as_none(x: typing.Any) -> typing.Any: + if x == inspect.Signature.empty: + return None + return x + + +class CommandParameter(typing.NamedTuple): + display_name: str + type: typing.Type + + class Command: - returntype: typing.Optional[typing.Type] + name: str + manager: "CommandManager" + signature: inspect.Signature + help: typing.Optional[str] - def __init__(self, manager, path, func) -> None: - self.path = path + def __init__(self, manager: "CommandManager", name: str, func: typing.Callable) -> None: + self.name = name self.manager = manager self.func = func - sig = inspect.signature(self.func) - self.help = None + self.signature = inspect.signature(self.func) + if func.__doc__: txt = func.__doc__.strip() 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: - self.returntype = sig.return_annotation + self.help = None + # 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}.") + + @property + def return_type(self) -> typing.Optional[typing.Type]: + return _empty_as_none(self.signature.return_annotation) + + @property + def parameters(self) -> typing.List[CommandParameter]: + """Returns a list of (display name, type) tuples.""" + ret = [] + for name, param in self.signature.parameters.items(): + if param.kind is param.VAR_POSITIONAL: + name = f"*{name}" + ret.append(CommandParameter(name, param.annotation)) + return ret - def paramnames(self) -> typing.Sequence[str]: - v = [typename(i) for i in self.paramtypes] - if self.has_positional: - v[-1] = "*" + v[-1] - return v + def signature_help(self) -> str: + params = " ".join(name for name, t in self.parameters) + if self.return_type: + ret = f" -> {typename(self.return_type)}" + else: + ret = "" + return f"{self.name} {params}{ret}" - def retname(self) -> str: - return typename(self.returntype) if self.returntype else "" + def prepare_args(self, args: typing.Sequence[str]) -> inspect.BoundArguments: + try: + bound_arguments = self.signature.bind(*args) + except TypeError as v: + raise exceptions.CommandError(f"Command argument mismatch: {v.args[0]}") - def signature_help(self) -> str: - params = " ".join(self.paramnames()) - ret = self.retname() - if ret: - ret = " -> " + ret - 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 - args = [a for a in args if a.strip() != ''] - verify_arg_signature(self.func, list(args), {}) - - remainder: typing.Sequence[str] = [] - if self.has_positional: - remainder = args[len(self.paramtypes) - 1:] - args = args[:len(self.paramtypes) - 1] - - pargs = [] - for arg, paramtype in zip(args, self.paramtypes): - pargs.append(parsearg(self.manager, arg, paramtype)) - pargs.extend(remainder) - return pargs + for name, value in bound_arguments.arguments.items(): + convert_to = self.signature.parameters[name].annotation + bound_arguments.arguments[name] = parsearg(self.manager, value, convert_to) + + bound_arguments.apply_defaults() + + return bound_arguments def call(self, args: typing.Sequence[str]) -> typing.Any: """ - Call the command with a list of arguments. At this point, all - arguments are strings. + Call the command with a list of arguments. At this point, all + arguments are strings. """ - ret = self.func(*self.prepare_args(args)) - if ret is None and self.returntype is None: + bound_args = self.prepare_args(args) + ret = self.func(*bound_args.args, **bound_args.kwargs) + if ret is None and self.return_type is None: 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): raise exceptions.CommandError( - "%s returned unexpected data - expected %s" % ( - self.path, typ.display - ) + f"{self.name} returned unexpected data - expected {typ.display}" ) return ret -ParseResult = typing.NamedTuple( - "ParseResult", - [ - ("value", str), - ("type", typing.Type), - ("valid", bool), - ], -) +class ParseResult(typing.NamedTuple): + value: str + type: typing.Type + valid: bool -class CommandManager(mitmproxy.types._CommandBase): +class CommandManager: + commands: typing.Dict[str, Command] + def __init__(self, master): self.master = master - self.commands: typing.Dict[str, Command] = {} - - self.regex = 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(" \r\n\t") - self.regex = self.regex.leaveWhitespace() + self.commands = {} + + self.expr_parser = pyparsing.ZeroOrMore( + 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(" \r\n\t") + ).leaveWhitespace() def collect_commands(self, addon): for i in dir(addon): if not i.startswith("__"): o = getattr(addon, i) try: - is_command = hasattr(o, "command_path") + is_command = hasattr(o, "command_name") except Exception: pass # hasattr may raise if o implements __getattr__. else: if is_command: try: - self.add(o.command_path, o) + self.add(o.command_name, o) except exceptions.CommandError as e: 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): @@ -156,105 +170,100 @@ class CommandManager(mitmproxy.types._CommandBase): @functools.lru_cache(maxsize=128) def parse_partial( - self, - cmdstr: str - ) -> typing.Tuple[typing.Sequence[ParseResult], typing.Sequence[str]]: + self, + cmdstr: 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. """ - parts: typing.List[str] = [] - for t, start, end in self.regex.scanString(cmdstr): - parts.append(t[0]) - - parse: typing.List[ParseResult] = [] - params: typing.List[type] = [] - typ: typing.Type - cmd_found: bool = False - for i in range(len(parts)): - if not parts[i].isspace(): - if not cmd_found: - cmd_found = True - typ = mitmproxy.types.Cmd - 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 + + parts: typing.List[str] = self.expr_parser.parseString(cmdstr) + + parsed: typing.List[ParseResult] = [] + next_params: typing.List[CommandParameter] = [ + CommandParameter("", mitmproxy.types.Cmd), + CommandParameter("", mitmproxy.types.CmdArgs), + ] + for part in parts: + if part.isspace(): + parsed.append( + ParseResult( + value=part, + type=mitmproxy.types.Space, + valid=True, + ) + ) + continue + + if next_params: + expected_type: typing.Type = next_params.pop(0).type 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 + expected_type = 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 if to: try: - to.parse(self, typ, parts[i]) + to.parse(self, expected_type, part) except exceptions.TypeError: valid = False else: valid = True - parse.append( + parsed.append( ParseResult( - value=parts[i], - type=typ, + value=part, + type=expected_type, valid=valid, ) ) - remhelp: typing.List[str] = [] - for x in params: - remt = mitmproxy.types.CommandTypes.get(x, None) - remhelp.append(remt.display) - - return parse, remhelp + return parsed, next_params - def call(self, path: str, *args: typing.Sequence[typing.Any]) -> typing.Any: + def call(self, command_name: 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: - raise exceptions.CommandError("Unknown command: %s" % path) - return self.commands[path].func(*args) + if command_name not in self.commands: + raise exceptions.CommandError("Unknown command: %s" % command_name) + 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: - raise exceptions.CommandError("Unknown command: %s" % path) + if command_name not in self.commands: + raise exceptions.CommandError("Unknown command: %s" % command_name) - return self.commands[path].call(args) + return self.commands[command_name].call(args) - def execute(self, cmdstr: str): + def execute(self, cmdstr: str) -> typing.Any: """ - Execute a command string. May raise CommandError. + Execute a command string. May raise CommandError. """ parts, _ = self.parse_partial(cmdstr) - params = [] - for p in parts: - v = p.value.strip() - if v != '': - if ((v.startswith("'") and v.endswith("'")) or - (v.startswith("\"") and v.endswith("\""))) and \ - len(v.split(' ')) == 1: - # If this parameter is between quotes but has no spaces in - # it, then it is safe to remove the quotes to pass it down - # This allows any commands that take a simple spaceless - # string as a parameter to work. For example - # view.flows.create get "http://www.example.com" won't work - # if the quotes are there as it won't see the param as a URL - v = v[1:-1] - - params.append(v) - - if len(parts) == 0: - raise exceptions.CommandError("Invalid command: %s" % cmdstr) - - return self._call_strings(params[0], params[1:]) + if not parts: + raise exceptions.CommandError(f"Invalid command: {cmdstr!r}") + command_name, *args = [ + unquote(part.value) + for part in parts + if part.type != mitmproxy.types.Space + ] + return self._call_strings(command_name, args) def dump(self, out=sys.stdout) -> None: cmds = list(self.commands.values()) @@ -266,27 +275,37 @@ class CommandManager(mitmproxy.types._CommandBase): print(file=out) +def unquote(x: str) -> str: + if x.startswith("'") and x.endswith("'"): + return x[1:-1] + if x.startswith('"') and x.endswith('"'): + return x[1:-1] + return x + + def parsearg(manager: CommandManager, spec: str, argtype: type) -> typing.Any: """ Convert a string to a argument to the appropriate type. """ t = mitmproxy.types.CommandTypes.get(argtype, None) if not t: - raise exceptions.CommandError("Unsupported argument type: %s" % argtype) + raise exceptions.CommandError(f"Unsupported argument type: {argtype}") try: - return t.parse(manager, argtype, spec) # type: ignore + return t.parse(manager, argtype, spec) except exceptions.TypeError as e: raise exceptions.CommandError from e -def command(path): +def command(name: typing.Optional[str]): def decorator(function): @functools.wraps(function) def wrapper(*args, **kwargs): verify_arg_signature(function, args, kwargs) return function(*args, **kwargs) - wrapper.__dict__["command_path"] = path + + wrapper.__dict__["command_name"] = name or function.__name__ return wrapper + return decorator @@ -296,8 +315,10 @@ def argument(name, type): specific types such as mitmproxy.types.Choice, which we cannot annotate directly as mypy does not like that. """ + def decorator(f: types.FunctionType) -> types.FunctionType: assert name in f.__annotations__ f.__annotations__[name] = type return f + return decorator diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index bff58605..ba3e601e 100644 --- a/mitmproxy/tools/console/commander/commander.py +++ b/mitmproxy/tools/console/commander/commander.py @@ -1,52 +1,49 @@ import abc +import collections import copy import typing -import collections import urwid from urwid.text_layout import calc_coords +import mitmproxy.command import mitmproxy.flow import mitmproxy.master -import mitmproxy.command import mitmproxy.types -class Completer: # pragma: no cover +class Completer: @abc.abstractmethod - def cycle(self) -> str: - pass + def cycle(self, forward: bool) -> str: + raise NotImplementedError() class ListCompleter(Completer): def __init__( - self, - start: str, - options: typing.Sequence[str], + self, + start: str, + options: typing.Sequence[str], ) -> None: self.start = start - self.options: typing.Sequence[str] = [] + self.options: typing.List[str] = [] for o in options: if o.startswith(start): self.options.append(o) self.options.sort() self.offset = 0 - def cycle(self) -> str: + def cycle(self, forward: bool) -> str: if not self.options: return self.start 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 -CompletionState = typing.NamedTuple( - "CompletionState", - [ - ("completer", Completer), - ("parse", typing.Sequence[mitmproxy.command.ParseResult]) - ] -) +class CompletionState(typing.NamedTuple): + completer: Completer + parsed: typing.Sequence[mitmproxy.command.ParseResult] class CommandBuffer: @@ -70,30 +67,13 @@ class CommandBuffer: else: self._cursor = x - 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 = p.value, - type = p.type, - valid = p.valid - ) - return parts, remhelp - def render(self): - """ - 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) + parts, remaining = self.master.commands.parse_partial(self.text) ret = [] - if parts == []: + 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", "")) - ret.append(("text", " ")) else: for p in parts: if p.valid: @@ -104,10 +84,11 @@ class CommandBuffer: elif p.value: ret.append(("commander_invalid", p.value)) - if remhelp: - ret.append(("text", " ")) - for v in remhelp: - ret.append(("commander_hint", "%s " % v)) + if remaining: + if parts[-1].type != mitmproxy.types.Space: + ret.append(("text", " ")) + for param in remaining: + ret.append(("commander_hint", f"{param.display_name} ")) return ret @@ -117,23 +98,31 @@ class CommandBuffer: def right(self) -> None: self.cursor = self.cursor + 1 - def cycle_completion(self) -> None: + def cycle_completion(self, forward: bool) -> None: if not self.completion: - parts, remainhelp = self.master.commands.parse_partial(self.text[:self.cursor]) - last = parts[-1] - ct = mitmproxy.types.CommandTypes.get(last.type, None) + parts, remaining = self.master.commands.parse_partial(self.text[:self.cursor]) + if parts and parts[-1].type != mitmproxy.types.Space: + type_to_complete = parts[-1].type + cycle_prefix = parts[-1].value + parsed = parts[:-1] + elif remaining: + type_to_complete = remaining[0].type + cycle_prefix = "" + parsed = parts + else: + return + ct = mitmproxy.types.CommandTypes.get(type_to_complete, None) if ct: self.completion = CompletionState( - completer = ListCompleter( - parts[-1].value, - ct.completion(self.master.commands, last.type, parts[-1].value) + completer=ListCompleter( + cycle_prefix, + ct.completion(self.master.commands, type_to_complete, cycle_prefix) ), - parse = parts, + parsed=parsed, ) if self.completion: - nxt = self.completion.completer.cycle() - buf = " ".join([i.value for i in self.completion.parse[:-1]]) + " " + nxt - buf = buf.strip() + nxt = self.completion.completer.cycle(forward) + buf = "".join([i.value for i in self.completion.parsed]) + nxt self.text = buf self.cursor = len(self.text) @@ -159,7 +148,7 @@ class CommandBuffer: class CommandHistory: - def __init__(self, master: mitmproxy.master.Master, size: int=30) -> None: + def __init__(self, master: mitmproxy.master.Master, size: int = 30) -> None: self.saved_commands: collections.deque = collections.deque( [CommandBuffer(master, "")], maxlen=size @@ -182,7 +171,7 @@ class CommandHistory: return self.saved_commands[self.index] return None - def add_command(self, command: CommandBuffer, execution: bool=False) -> None: + def add_command(self, command: CommandBuffer, execution: bool = False) -> None: if self.index == self.last_index or execution: last_item = self.saved_commands[-1] last_item_empty = not last_item.text @@ -219,8 +208,10 @@ class CommandEdit(urwid.WidgetWrap): self.cbuf = self.history.get_prev() or self.cbuf elif key == "down": self.cbuf = self.history.get_next() or self.cbuf + elif key == "shift tab": + self.cbuf.cycle_completion(False) elif key == "tab": - self.cbuf.cycle_completion() + self.cbuf.cycle_completion(True) elif len(key) == 1: self.cbuf.insert(key) self.update() diff --git a/mitmproxy/tools/console/commands.py b/mitmproxy/tools/console/commands.py index 0f35742b..d35a6b8a 100644 --- a/mitmproxy/tools/console/commands.py +++ b/mitmproxy/tools/console/commands.py @@ -1,6 +1,8 @@ import urwid import blinker import textwrap + +from mitmproxy import command from mitmproxy.tools.console import layoutwidget from mitmproxy.tools.console import signals @@ -10,7 +12,7 @@ command_focus_change = blinker.Signal() 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 super().__init__(None) self._w = self.get_widget() @@ -18,15 +20,18 @@ class CommandItem(urwid.WidgetWrap): def get_widget(self): parts = [ ("focus", ">> " if self.focused else " "), - ("title", self.cmd.path), - ("text", " "), - ("text", " ".join(self.cmd.paramnames())), + ("title", self.cmd.name) ] - if self.cmd.returntype: - parts.append([ + if self.cmd.parameters: + parts += [ + ("text", " "), + ("text", " ".join(name for name, t in self.cmd.parameters)), + ] + if self.cmd.return_type: + parts += [ ("title", " -> "), - ("text", self.cmd.retname()), - ]) + ("text", command.typename(self.cmd.return_type)), + ] return urwid.AttrMap( urwid.Padding(urwid.Text(parts)), @@ -92,7 +97,7 @@ class CommandsList(urwid.ListBox): def keypress(self, size, key): if key == "m_select": 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": self.set_focus(0) self.walker._modified() diff --git a/mitmproxy/tools/console/consoleaddons.py b/mitmproxy/tools/console/consoleaddons.py index 9e0533a4..b5263f6f 100644 --- a/mitmproxy/tools/console/consoleaddons.py +++ b/mitmproxy/tools/console/consoleaddons.py @@ -238,7 +238,7 @@ class ConsoleAddon: prompt: str, choices: typing.Sequence[str], cmd: mitmproxy.types.Cmd, - *args: mitmproxy.types.Arg + *args: mitmproxy.types.CmdArgs ) -> None: """ Prompt the user to choose from a specified list of strings, then @@ -264,7 +264,7 @@ class ConsoleAddon: prompt: str, choicecmd: mitmproxy.types.Cmd, subcmd: mitmproxy.types.Cmd, - *args: mitmproxy.types.Arg + *args: mitmproxy.types.CmdArgs ) -> None: """ Prompt the user to choose from a list of strings returned by a @@ -573,7 +573,7 @@ class ConsoleAddon: contexts: typing.Sequence[str], key: str, cmd: mitmproxy.types.Cmd, - *args: mitmproxy.types.Arg + *args: mitmproxy.types.CmdArgs ) -> None: """ Bind a shortcut key. diff --git a/mitmproxy/types.py b/mitmproxy/types.py index b48aef84..1f1c503b 100644 --- a/mitmproxy/types.py +++ b/mitmproxy/types.py @@ -5,6 +5,9 @@ import typing from mitmproxy import exceptions from mitmproxy import flow +if typing.TYPE_CHECKING: + from mitmproxy.command import CommandManager + class Path(str): pass @@ -14,7 +17,7 @@ class Cmd(str): pass -class Arg(str): +class CmdArgs(str): pass @@ -22,6 +25,10 @@ class Unknown(str): pass +class Space(str): + pass + + class CutSpec(typing.Sequence[str]): pass @@ -40,27 +47,11 @@ class Choice: 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: typ: typing.Type = object display: str = "" - def completion( - self, manager: _CommandBase, t: typing.Any, s: str - ) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: typing.Any, s: str) -> typing.Sequence[str]: """ Returns a list of completion strings for a given prefix. The strings returned don't necessarily need to be suffixes of the prefix, since @@ -68,9 +59,7 @@ class _BaseType: """ raise NotImplementedError - def parse( - self, manager: _CommandBase, typ: typing.Any, s: str - ) -> typing.Any: + def parse(self, manager: "CommandManager", 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. @@ -78,7 +67,7 @@ class _BaseType: """ 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. """ @@ -89,10 +78,10 @@ class _BoolType(_BaseType): typ = 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"] - def parse(self, manager: _CommandBase, t: type, s: str) -> bool: + def parse(self, manager: "CommandManager", t: type, s: str) -> bool: if s == "true": return True elif s == "false": @@ -102,7 +91,7 @@ class _BoolType(_BaseType): "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] @@ -110,13 +99,13 @@ class _StrType(_BaseType): typ = 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 [] - def parse(self, manager: _CommandBase, t: type, s: str) -> str: + def parse(self, manager: "CommandManager", t: type, s: str) -> str: 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) @@ -124,13 +113,13 @@ class _UnknownType(_BaseType): typ = 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 [] - def parse(self, manager: _CommandBase, t: type, s: str) -> str: + def parse(self, manager: "CommandManager", t: type, s: str) -> str: 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 @@ -138,16 +127,16 @@ class _IntType(_BaseType): typ = 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 [] - def parse(self, manager: _CommandBase, t: type, s: str) -> int: + def parse(self, manager: "CommandManager", t: type, s: str) -> int: try: return int(s) except ValueError as 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) @@ -155,7 +144,7 @@ class _PathType(_BaseType): typ = 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: start = "./" path = os.path.expanduser(start) @@ -177,10 +166,10 @@ class _PathType(_BaseType): ret.sort() 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) - 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) @@ -188,43 +177,43 @@ class _CmdType(_BaseType): typ = 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()) - 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: raise exceptions.TypeError("Unknown command: %s" % 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 class _ArgType(_BaseType): - typ = Arg + typ = CmdArgs 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 [] - def parse(self, manager: _CommandBase, t: type, s: str) -> str: - return s + def parse(self, manager: "CommandManager", t: type, s: str) -> str: + raise exceptions.TypeError("Arguments for unknown command.") - 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) class _StrSeqType(_BaseType): 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 [] - 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(",")] - 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): return False try: @@ -238,7 +227,7 @@ class _StrSeqType(_BaseType): class _CutSpecType(_BaseType): typ = CutSpec - display = "[cut]" + display = "cut[]" valid_prefixes = [ "request.method", "request.scheme", @@ -277,7 +266,7 @@ class _CutSpecType(_BaseType): "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(",") opts = [] for pref in self.valid_prefixes: @@ -285,11 +274,11 @@ class _CutSpecType(_BaseType): opts.append(",".join(spec)) 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(",") 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): return False parts = [x.strip() for x in val.split(",")] @@ -327,7 +316,7 @@ class _BaseFlowType(_BaseType): "~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 @@ -335,7 +324,7 @@ class _FlowType(_BaseFlowType): typ = flow.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: flows = manager.execute("view.flows.resolve %s" % (s)) except exceptions.CommandError as e: @@ -346,21 +335,21 @@ class _FlowType(_BaseFlowType): ) 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) class _FlowsType(_BaseFlowType): 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: return manager.execute("view.flows.resolve %s" % (s)) except exceptions.CommandError as 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: for v in val: if not isinstance(v, flow.Flow): @@ -372,19 +361,19 @@ class _FlowsType(_BaseFlowType): class _DataType(_BaseType): typ = Data - display = "[data]" + display = "data[][]" def completion( - self, manager: _CommandBase, t: type, s: str + self, manager: "CommandManager", t: type, s: str ) -> typing.Sequence[str]: # pragma: no cover raise exceptions.TypeError("data cannot be passed as argument") def parse( - self, manager: _CommandBase, t: type, s: str + self, manager: "CommandManager", t: type, s: str ) -> typing.Any: # pragma: no cover 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 try: for row in val: @@ -400,16 +389,16 @@ class _ChoiceType(_BaseType): typ = 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) - 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) if s not in opts: raise exceptions.TypeError("Invalid choice.") 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: opts = manager.execute(typ.options_command) except exceptions.CommandError: @@ -423,7 +412,7 @@ class TypeManager: for t in types: 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: return self.typemap[type(t)] return self.typemap.get(t, default) diff --git a/test/mitmproxy/test_command.py b/test/mitmproxy/test_command.py index eb3857bf..2a1dfd08 100644 --- a/test/mitmproxy/test_command.py +++ b/test/mitmproxy/test_command.py @@ -1,13 +1,15 @@ -import typing import inspect +import io +import typing + +import pytest + +import mitmproxy.types from mitmproxy import command -from mitmproxy import flow from mitmproxy import exceptions -from mitmproxy.test import tflow +from mitmproxy import flow from mitmproxy.test import taddons -import mitmproxy.types -import io -import pytest +from mitmproxy.test import tflow class TAddon: @@ -29,7 +31,7 @@ class TAddon: return "ok" @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" @command.command("empty") @@ -83,14 +85,14 @@ class TestCommand: with pytest.raises(exceptions.CommandError): command.Command(cm, "invalidret", a.invalidret) with pytest.raises(exceptions.CommandError): - command.Command(cm, "invalidarg", a.invalidarg) + assert command.Command(cm, "invalidarg", a.invalidarg) def test_varargs(self): with taddons.context() as tctx: cm = command.CommandManager(tctx.master) a = TAddon() 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"] def test_call(self): @@ -99,7 +101,7 @@ class TestCommand: a = TAddon() c = command.Command(cm, "cmd.path", a.cmd1) 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) with pytest.raises(exceptions.CommandError): @@ -113,239 +115,272 @@ class TestCommand: [ "foo bar", [ - 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) + command.ParseResult(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) ], [], ], [ "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) + 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) ], [], ], [ "a", - [command.ParseResult(value = "a", type = mitmproxy.types.Cmd, valid = False)], + [command.ParseResult(value="a", type=mitmproxy.types.Cmd, valid=False)], [], ], [ "", [], - [] + [ + command.CommandParameter("", mitmproxy.types.Cmd), + command.CommandParameter("", mitmproxy.types.CmdArgs) + ] ], [ "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), + 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), ], [] ], [ "cmd3 ", [ - command.ParseResult(value = "cmd3", 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=mitmproxy.types.Space, valid=True), ], - ['int'] + [command.CommandParameter('foo', int)] ], [ "subcommand ", [ - command.ParseResult(value = "subcommand", type = mitmproxy.types.Cmd, valid = True,), - command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), + command.ParseResult(value="subcommand", type=mitmproxy.types.Cmd, valid=True, ), + command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + ], + [ + command.CommandParameter('cmd', mitmproxy.types.Cmd), + command.CommandParameter('*args', mitmproxy.types.CmdArgs), ], - ["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 = mitmproxy.types.Unknown, valid = False), + 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=" ", type=mitmproxy.types.Space, valid=True), ], - ["int"] + [command.CommandParameter('foo', int)] ], [ "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 ", [ - command.ParseResult(value = "cmd4", type = mitmproxy.types.Cmd, valid = True), - command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), + command.ParseResult(value="cmd4", type=mitmproxy.types.Cmd, valid=True), + command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), ], - ["int", "str", "path"] + [ + command.CommandParameter('a', int), + command.CommandParameter('b', str), + command.CommandParameter('c', mitmproxy.types.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), + 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), ], - ["str", "path"] + [ + command.CommandParameter('b', str), + command.CommandParameter('c', mitmproxy.types.Path), + ] ], [ "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 ", [ - command.ParseResult(value = "flow", type = mitmproxy.types.Cmd, valid = True), - command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), + command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True), + command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), ], - ["flow", "str"] + [ + command.CommandParameter('f', flow.Flow), + command.CommandParameter('s', 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), + 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), ], - ["str"] + [ + command.CommandParameter('s', 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), - command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), + 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=" ", type=mitmproxy.types.Space, valid=True), ], - ["str"] + [ + command.CommandParameter('s', 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), + 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), ], - ["str"] + [ + command.CommandParameter('s', str), + ] ], [ "flow \"three four\"", [ - command.ParseResult(value = "flow", type = mitmproxy.types.Cmd, valid = True), - command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), - command.ParseResult(value = '"three four"', type = flow.Flow, valid = False), + command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True), + 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.Unknown, valid = False), - command.ParseResult(value = "' '", type = mitmproxy.types.Unknown, valid = False) + 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.Unknown, valid = False), - command.ParseResult(value = '" "', type = mitmproxy.types.Unknown, valid = False) + 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), + command.ParseResult(value='"abc"', type=mitmproxy.types.Cmd, valid=False), ], [], ], [ "'def'", [ - command.ParseResult(value = "'def'", type = mitmproxy.types.Cmd, valid = False), + 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), + 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.Unknown, valid = False), - command.ParseResult(value = "'a \"b\" c'", type = mitmproxy.types.Unknown, valid = False), + 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.Unknown, valid = False), - command.ParseResult(value = '"a \'b\' c"', type = mitmproxy.types.Unknown, valid = False), + 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.Unknown, valid = False), - command.ParseResult(value = r'"a \"b\" c"', type = mitmproxy.types.Unknown, valid = False), + 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.Unknown, valid = False), - command.ParseResult(value = r"'a \'b\' c'", type = mitmproxy.types.Unknown, valid = False), + 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.Unknown, valid = False), - 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="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.Unknown, valid = False), - command.ParseResult(value = "spaces_at_the_begining_are_not_stripped", type = mitmproxy.types.Cmd, valid = False), - command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), - command.ParseResult(value = "neither_at_the_end", type = mitmproxy.types.Unknown, valid = False), - command.ParseResult(value = " ", type = mitmproxy.types.Unknown, valid = False), + 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), ], [], ], @@ -356,8 +391,7 @@ class TestCommand: tctx.master.addons.add(TAddon()) for s, expected, expectedremain in tests: current, remain = tctx.master.commands.parse_partial(s) - assert current == expected - assert expectedremain == remain + assert (s, current, expectedremain) == (s, expected, remain) def test_simple(): @@ -365,11 +399,11 @@ 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.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.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 bar\"") == "ret foo bar") + assert (c.call("one.two", "foo") == "ret foo") with pytest.raises(exceptions.CommandError, match="Unknown"): c.execute("nonexistent") with pytest.raises(exceptions.CommandError, match="Invalid"): @@ -397,13 +431,13 @@ def test_simple(): def test_typename(): 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.CutSpec) == "[cut]" + assert command.typename(mitmproxy.types.Data) == "data[][]" + assert command.typename(mitmproxy.types.CutSpec) == "cut[]" 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.Path) == "path" diff --git a/test/mitmproxy/test_types.py b/test/mitmproxy/test_types.py index 571985fb..c8f7afde 100644 --- a/test/mitmproxy/test_types.py +++ b/test/mitmproxy/test_types.py @@ -2,7 +2,6 @@ import pytest import os import typing import contextlib -from unittest import mock import mitmproxy.exceptions import mitmproxy.types @@ -64,13 +63,14 @@ def test_int(): b.parse(tctx.master.commands, int, "foo") -def test_path(tdata): +def test_path(tdata, monkeypatch): with taddons.context() as tctx: b = mitmproxy.types._PathType() assert b.parse(tctx.master.commands, mitmproxy.types.Path, "/foo") == "/foo" assert b.parse(tctx.master.commands, mitmproxy.types.Path, "/bar") == "/bar" - with mock.patch.dict("os.environ", {"HOME": "/home/test"}): - assert b.parse(tctx.master.commands, mitmproxy.types.Path, "~/mitm") == "/home/test/mitm" + 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.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, 3) is False @@ -127,10 +127,10 @@ def test_cutspec(): def test_arg(): with taddons.context() as tctx: b = mitmproxy.types._ArgType() - assert b.completion(tctx.master.commands, mitmproxy.types.Arg, "") == [] - assert b.parse(tctx.master.commands, mitmproxy.types.Arg, "foo") == "foo" - assert b.is_valid(tctx.master.commands, mitmproxy.types.Arg, "foo") is True - assert b.is_valid(tctx.master.commands, mitmproxy.types.Arg, 1) is False + assert b.completion(tctx.master.commands, mitmproxy.types.CmdArgs, "") == [] + with pytest.raises(mitmproxy.exceptions.TypeError): + b.parse(tctx.master.commands, mitmproxy.types.CmdArgs, "foo") + assert b.is_valid(tctx.master.commands, mitmproxy.types.CmdArgs, 1) is False def test_strseq(): -- cgit v1.2.3 From f75a95acea6772457b25395b0fdd2c97bfebb936 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 18 Nov 2019 03:45:16 +0100 Subject: fix vararg handling --- mitmproxy/command.py | 36 +++++++++++++++--------- mitmproxy/tools/console/commander/commander.py | 2 +- mitmproxy/types.py | 2 +- test/mitmproxy/test_types.py | 3 +- test/mitmproxy/tools/console/test_commander.py | 14 ++++----- test/mitmproxy/tools/console/test_defaultkeys.py | 17 +++++++---- 6 files changed, 43 insertions(+), 31 deletions(-) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index a64c7404..00238f46 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -41,8 +41,15 @@ def _empty_as_none(x: typing.Any) -> typing.Any: class CommandParameter(typing.NamedTuple): - display_name: str + 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: @@ -77,16 +84,14 @@ class Command: @property def parameters(self) -> typing.List[CommandParameter]: - """Returns a list of (display name, type) tuples.""" + """Returns a list of CommandParameters.""" ret = [] for name, param in self.signature.parameters.items(): - if param.kind is param.VAR_POSITIONAL: - name = f"*{name}" - ret.append(CommandParameter(name, param.annotation)) + ret.append(CommandParameter(name, param.annotation, param.kind)) return ret def signature_help(self) -> str: - params = " ".join(name for name, t in self.parameters) + params = " ".join(str(param) for param in self.parameters) if self.return_type: ret = f" -> {typename(self.return_type)}" else: @@ -184,6 +189,7 @@ class CommandManager: CommandParameter("", mitmproxy.types.Cmd), CommandParameter("", mitmproxy.types.CmdArgs), ] + expected: typing.Optional[CommandParameter] = None for part in parts: if part.isspace(): parsed.append( @@ -195,16 +201,18 @@ class CommandManager: ) continue - if next_params: - expected_type: typing.Type = next_params.pop(0).type + if expected and expected.kind is inspect.Parameter.VAR_POSITIONAL: + assert not next_params + elif next_params: + expected = next_params.pop(0) else: - expected_type = mitmproxy.types.Unknown + expected = CommandParameter("", mitmproxy.types.Unknown) arg_is_known_command = ( - expected_type == mitmproxy.types.Cmd and part in self.commands + 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 + expected.type == mitmproxy.types.Cmd and part not in self.commands ) command_args_following = ( next_params and next_params[0].type == mitmproxy.types.CmdArgs @@ -214,11 +222,11 @@ class CommandManager: if arg_is_unknown_command and command_args_following: next_params.pop(0) - to = mitmproxy.types.CommandTypes.get(expected_type, None) + to = mitmproxy.types.CommandTypes.get(expected.type, None) valid = False if to: try: - to.parse(self, expected_type, part) + to.parse(self, expected.type, part) except exceptions.TypeError: valid = False else: @@ -227,7 +235,7 @@ class CommandManager: parsed.append( ParseResult( value=part, - type=expected_type, + type=expected.type, valid=valid, ) ) diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index ba3e601e..f826b984 100644 --- a/mitmproxy/tools/console/commander/commander.py +++ b/mitmproxy/tools/console/commander/commander.py @@ -88,7 +88,7 @@ class CommandBuffer: if parts[-1].type != mitmproxy.types.Space: ret.append(("text", " ")) for param in remaining: - ret.append(("commander_hint", f"{param.display_name} ")) + ret.append(("commander_hint", f"{param} ")) return ret diff --git a/mitmproxy/types.py b/mitmproxy/types.py index 1f1c503b..24a1172b 100644 --- a/mitmproxy/types.py +++ b/mitmproxy/types.py @@ -197,7 +197,7 @@ class _ArgType(_BaseType): return [] def parse(self, manager: "CommandManager", t: type, s: str) -> str: - raise exceptions.TypeError("Arguments for unknown command.") + return s def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: return isinstance(val, str) diff --git a/test/mitmproxy/test_types.py b/test/mitmproxy/test_types.py index c8f7afde..2cd17d87 100644 --- a/test/mitmproxy/test_types.py +++ b/test/mitmproxy/test_types.py @@ -128,8 +128,7 @@ def test_arg(): with taddons.context() as tctx: b = mitmproxy.types._ArgType() assert b.completion(tctx.master.commands, mitmproxy.types.CmdArgs, "") == [] - with pytest.raises(mitmproxy.exceptions.TypeError): - b.parse(tctx.master.commands, mitmproxy.types.CmdArgs, "foo") + assert b.parse(tctx.master.commands, mitmproxy.types.CmdArgs, "foo") == "foo" assert b.is_valid(tctx.master.commands, mitmproxy.types.CmdArgs, 1) is False diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py index 9a2ec102..ce789d30 100644 --- a/test/mitmproxy/tools/console/test_commander.py +++ b/test/mitmproxy/tools/console/test_commander.py @@ -25,7 +25,7 @@ class TestListCompleter: for start, options, cycle in tests: c = commander.ListCompleter(start, options) for expected in cycle: - assert c.cycle() == expected + assert c.cycle(True) == expected class TestCommandEdit: @@ -252,7 +252,7 @@ class TestCommandBuffer: cb = commander.CommandBuffer(tctx.master) cb.text = "foo bar" cb.cursor = len(cb.text) - cb.cycle_completion() + cb.cycle_completion(True) ch = commander.CommandHistory(tctx.master, 30) ce = commander.CommandEdit(tctx.master, "se", ch) @@ -261,7 +261,7 @@ class TestCommandBuffer: ret = ce.cbuf.render() assert ret[0] == ('commander_command', 'set') assert ret[1] == ('text', ' ') - assert ret[2] == ('commander_hint', 'str ') + assert ret[2] == ('commander_hint', '*options ') def test_render(self): with taddons.context() as tctx: @@ -272,13 +272,13 @@ class TestCommandBuffer: cb.text = 'set view_filter=~bq test' ret = cb.render() assert ret[0] == ('commander_command', 'set') - assert ret[1] == ('commander_invalid', ' ') + assert ret[1] == ('text', ' ') assert ret[2] == ('text', 'view_filter=~bq') - assert ret[3] == ('commander_invalid', ' ') - assert ret[4] == ('commander_invalid', 'test') + assert ret[3] == ('text', ' ') + assert ret[4] == ('text', 'test') cb.text = "set" ret = cb.render() assert ret[0] == ('commander_command', 'set') assert ret[1] == ('text', ' ') - assert ret[2] == ('commander_hint', 'str ') + assert ret[2] == ('commander_hint', '*options ') diff --git a/test/mitmproxy/tools/console/test_defaultkeys.py b/test/mitmproxy/tools/console/test_defaultkeys.py index 9c79525b..58a0a585 100644 --- a/test/mitmproxy/tools/console/test_defaultkeys.py +++ b/test/mitmproxy/tools/console/test_defaultkeys.py @@ -1,10 +1,12 @@ +import pytest + +import mitmproxy.types +from mitmproxy import command +from mitmproxy import ctx from mitmproxy.test.tflow import tflow 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 @@ -18,10 +20,13 @@ async def test_commands_exist(): await m.load_flow(tflow()) for binding in km.bindings: - results = command_manager.parse_partial(binding.command.strip()) + parsed, _ = command_manager.parse_partial(binding.command.strip()) - cmd = results[0][0].value - args = [a.value for a in results[0][1:]] + cmd = parsed[0].value + args = [ + a.value for a in parsed[1:] + if a.type != mitmproxy.types.Space + ] assert cmd in m.commands.commands -- cgit v1.2.3 From da0755106d76a9f45f281d2e65df38f83ae888c7 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 18 Nov 2019 03:54:18 +0100 Subject: adjust test --- test/mitmproxy/test_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mitmproxy/test_command.py b/test/mitmproxy/test_command.py index 2a1dfd08..a34a8cf6 100644 --- a/test/mitmproxy/test_command.py +++ b/test/mitmproxy/test_command.py @@ -168,7 +168,7 @@ class TestCommand: ], [ command.CommandParameter('cmd', mitmproxy.types.Cmd), - command.CommandParameter('*args', mitmproxy.types.CmdArgs), + command.CommandParameter('args', mitmproxy.types.CmdArgs, kind=inspect.Parameter.VAR_POSITIONAL), ], ], [ -- cgit v1.2.3 From dd556f052b0045b5e8f14b4810e302b4c2efc81f Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 18 Nov 2019 04:34:23 +0100 Subject: coverage++ --- test/mitmproxy/test_command.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/test/mitmproxy/test_command.py b/test/mitmproxy/test_command.py index a34a8cf6..a432f9e3 100644 --- a/test/mitmproxy/test_command.py +++ b/test/mitmproxy/test_command.py @@ -171,6 +171,28 @@ class TestCommand: 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 ", [ @@ -402,7 +424,7 @@ def test_simple(): 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 bar\"") == "ret foo bar") + assert (c.execute("one.two 'foo bar'") == "ret foo bar") assert (c.call("one.two", "foo") == "ret foo") with pytest.raises(exceptions.CommandError, match="Unknown"): c.execute("nonexistent") -- cgit v1.2.3 From 7bf06f8ae04697305731ec89a43ebea6da4376b8 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 18 Nov 2019 05:19:06 +0100 Subject: fix coverage --- mitmproxy/types.py | 2 +- setup.cfg | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mitmproxy/types.py b/mitmproxy/types.py index 24a1172b..ac992217 100644 --- a/mitmproxy/types.py +++ b/mitmproxy/types.py @@ -5,7 +5,7 @@ import typing from mitmproxy import exceptions from mitmproxy import flow -if typing.TYPE_CHECKING: +if typing.TYPE_CHECKING: # pragma: no cover from mitmproxy.command import CommandManager diff --git a/setup.cfg b/setup.cfg index e7643b08..a1ba1610 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,6 +18,8 @@ show_missing = True exclude_lines = pragma: no cover raise NotImplementedError() + if typing.TYPE_CHECKING: + if TYPE_CHECKING: [mypy-mitmproxy.contrib.*] ignore_errors = True -- cgit v1.2.3 From 74f5fa6a7736f116416c242b159e6b0525e6fe5b Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 18 Nov 2019 22:03:51 +0100 Subject: wip --- mitmproxy/addons/core.py | 17 ++++++++-------- mitmproxy/addons/view.py | 26 ++++++++++++------------- mitmproxy/command.py | 27 ++++++++++++++++++++------ mitmproxy/tools/console/commander/commander.py | 8 ++++---- mitmproxy/tools/console/commands.py | 2 +- mitmproxy/tools/console/consoleaddons.py | 11 ++++++----- test/mitmproxy/tools/console/test_commander.py | 4 ++-- 7 files changed, 55 insertions(+), 40 deletions(-) diff --git a/mitmproxy/addons/core.py b/mitmproxy/addons/core.py index 1fbeb1e0..55e2e129 100644 --- a/mitmproxy/addons/core.py +++ b/mitmproxy/addons/core.py @@ -83,15 +83,15 @@ class Core: ) @command.command("set") - def set(self, *options: str) -> None: + def set(self, option: str, *value: str) -> None: """ - Set an option of the form "key[=value]". When the value is omitted, - booleans are set to true, strings and integers are set to None (if - permitted), and sequences are emptied. Boolean values can be true, - false or toggle. If multiple specs are passed, they are joined - into one separated by spaces. + Set an option. When the value is omitted, booleans are set to true, + strings and integers are set to None (if permitted), and sequences + are emptied. Boolean values can be true, false or toggle. + Multiple values are concatenated with a single space. """ - strspec = " ".join(options) + value = " ".join(value) + strspec = f"{option}={value}" try: ctx.options.set(strspec) except exceptions.OptionsError as e: @@ -168,8 +168,7 @@ class Core: "reason", ] - @command.command( - "flow.set") + @command.command("flow.set") @command.argument("attr", type=mitmproxy.types.Choice("flow.set.options")) def flow_set( self, diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index c57c34c8..1d57d781 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -298,16 +298,16 @@ class View(collections.abc.Sequence): # Filter @command.command("view.filter.set") - def set_filter_cmd(self, filtstr: str) -> None: + def set_filter_cmd(self, filter_expr: str) -> None: """ Sets the current view filter. """ filt = None - if filtstr: - filt = flowfilter.parse(filtstr) + if filter_expr: + filt = flowfilter.parse(filter_expr) if not filt: raise exceptions.CommandError( - "Invalid interception filter: %s" % filtstr + "Invalid interception filter: %s" % filter_expr ) self.set_filter(filt) @@ -412,26 +412,26 @@ class View(collections.abc.Sequence): ctx.log.alert("Removed %s flows" % len(flows)) @command.command("view.flows.resolve") - def resolve(self, flowspec: 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. """ - if flowspec == "@all": + if flow_spec == "@all": return [i for i in self._store.values()] - if flowspec == "@focus": + if flow_spec == "@focus": return [self.focus.flow] if self.focus.flow else [] - elif flowspec == "@shown": + elif flow_spec == "@shown": return [i for i in self] - elif flowspec == "@hidden": + elif flow_spec == "@hidden": return [i for i in self._store.values() if i not in self._view] - elif flowspec == "@marked": + elif flow_spec == "@marked": return [i for i in self._store.values() if i.marked] - elif flowspec == "@unmarked": + elif flow_spec == "@unmarked": return [i for i in self._store.values() if not i.marked] else: - filt = flowfilter.parse(flowspec) + filt = flowfilter.parse(flow_spec) if not filt: - raise exceptions.CommandError("Invalid flow filter: %s" % flowspec) + raise exceptions.CommandError("Invalid flow filter: %s" % flow_spec) return [i for i in self._store.values() if filt(i)] @command.command("view.flows.create") diff --git a/mitmproxy/command.py b/mitmproxy/command.py index 00238f46..7203fe42 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -3,6 +3,7 @@ """ import functools import inspect +import re import sys import textwrap import types @@ -284,13 +285,27 @@ class CommandManager: def unquote(x: str) -> str: - if x.startswith("'") and x.endswith("'"): - return x[1:-1] - if x.startswith('"') and x.endswith('"'): - return x[1:-1] + quoted = ( + (x.startswith('"') and x.endswith('"')) + or + (x.startswith("'") and x.endswith("'")) + ) + if quoted: + x = x[1:-1] + # not sure if this is the right place, but pypyarsing doesn't process escape sequences. + x = re.sub(r"\\(.)", r"\g<1>", x) + return x return x +def quote(val: str) -> str: + if not val: + return '""' + if all(ws not in val for ws in " \r\n\t"): + return val + return repr(val) + + def parsearg(manager: CommandManager, spec: str, argtype: type) -> typing.Any: """ Convert a string to a argument to the appropriate type. @@ -304,14 +319,14 @@ def parsearg(manager: CommandManager, spec: str, argtype: type) -> typing.Any: raise exceptions.CommandError from e -def command(name: typing.Optional[str]): +def command(name: typing.Optional[str] = None): def decorator(function): @functools.wraps(function) def wrapper(*args, **kwargs): verify_arg_signature(function, args, kwargs) return function(*args, **kwargs) - wrapper.__dict__["command_name"] = name or function.__name__ + wrapper.__dict__["command_name"] = name or function.__name__.replace("_", ".") return wrapper return decorator diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index f826b984..d751422b 100644 --- a/mitmproxy/tools/console/commander/commander.py +++ b/mitmproxy/tools/console/commander/commander.py @@ -14,7 +14,7 @@ import mitmproxy.types class Completer: @abc.abstractmethod - def cycle(self, forward: bool) -> str: + def cycle(self, forward: bool = True) -> str: raise NotImplementedError() @@ -32,7 +32,7 @@ class ListCompleter(Completer): self.options.sort() self.offset = 0 - def cycle(self, forward: bool) -> str: + def cycle(self, forward: bool = True) -> str: if not self.options: return self.start ret = self.options[self.offset] @@ -98,7 +98,7 @@ class CommandBuffer: def right(self) -> None: self.cursor = self.cursor + 1 - def cycle_completion(self, forward: bool) -> None: + def cycle_completion(self, forward: bool = True) -> None: if not self.completion: parts, remaining = self.master.commands.parse_partial(self.text[:self.cursor]) if parts and parts[-1].type != mitmproxy.types.Space: @@ -211,7 +211,7 @@ class CommandEdit(urwid.WidgetWrap): elif key == "shift tab": self.cbuf.cycle_completion(False) elif key == "tab": - self.cbuf.cycle_completion(True) + self.cbuf.cycle_completion() elif len(key) == 1: self.cbuf.insert(key) self.update() diff --git a/mitmproxy/tools/console/commands.py b/mitmproxy/tools/console/commands.py index d35a6b8a..26a99b14 100644 --- a/mitmproxy/tools/console/commands.py +++ b/mitmproxy/tools/console/commands.py @@ -25,7 +25,7 @@ class CommandItem(urwid.WidgetWrap): if self.cmd.parameters: parts += [ ("text", " "), - ("text", " ".join(name for name, t in self.cmd.parameters)), + ("text", " ".join(str(param) for param in self.cmd.parameters)), ] if self.cmd.return_type: parts += [ diff --git a/mitmproxy/tools/console/consoleaddons.py b/mitmproxy/tools/console/consoleaddons.py index b5263f6f..4288696a 100644 --- a/mitmproxy/tools/console/consoleaddons.py +++ b/mitmproxy/tools/console/consoleaddons.py @@ -287,21 +287,22 @@ class ConsoleAddon: ) @command.command("console.command") - def console_command(self, *cmdstr: str) -> None: + def console_command(self, *cmd_str: str) -> None: """ Prompt the user to edit a command with a (possibly empty) starting value. """ - signals.status_prompt_command.send(partial=" ".join(cmdstr)) # type: ignore + cmd_str = (command.quote(x) if x else "" for x in cmd_str) + signals.status_prompt_command.send(partial=" ".join(cmd_str)) # type: ignore @command.command("console.command.set") 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_name, None) - current_value = option_value if option_value else "" + option_value = command.quote(option_value) self.master.commands.execute( - "console.command set %s=%s" % (option_name, current_value) + f"console.command set {option_name} {option_value}" ) @command.command("console.view.keybindings") diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py index ce789d30..6b42de76 100644 --- a/test/mitmproxy/tools/console/test_commander.py +++ b/test/mitmproxy/tools/console/test_commander.py @@ -25,7 +25,7 @@ class TestListCompleter: for start, options, cycle in tests: c = commander.ListCompleter(start, options) for expected in cycle: - assert c.cycle(True) == expected + assert c.cycle() == expected class TestCommandEdit: @@ -252,7 +252,7 @@ class TestCommandBuffer: cb = commander.CommandBuffer(tctx.master) cb.text = "foo bar" cb.cursor = len(cb.text) - cb.cycle_completion(True) + cb.cycle_completion() ch = commander.CommandHistory(tctx.master, 30) ce = commander.CommandEdit(tctx.master, "se", ch) -- cgit v1.2.3 From 76e648410745c61f7a659e864230b6154dc43ced Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 19 Nov 2019 18:14:00 +0100 Subject: fix lexing, sort of --- mitmproxy/addons/core.py | 3 +- mitmproxy/command.py | 40 ++------------- mitmproxy/command_lexer.py | 49 ++++++++++++++++++ mitmproxy/tools/console/consoleaddons.py | 71 ++++++++++++++------------ mitmproxy/tools/console/defaultkeys.py | 8 +-- mitmproxy/tools/console/statusbar.py | 12 +++-- test/mitmproxy/test_command_lexer.py | 38 ++++++++++++++ test/mitmproxy/tools/console/test_commander.py | 2 +- 8 files changed, 142 insertions(+), 81 deletions(-) create mode 100644 mitmproxy/command_lexer.py create mode 100644 test/mitmproxy/test_command_lexer.py diff --git a/mitmproxy/addons/core.py b/mitmproxy/addons/core.py index 55e2e129..6fb2bf1e 100644 --- a/mitmproxy/addons/core.py +++ b/mitmproxy/addons/core.py @@ -90,8 +90,7 @@ class Core: are emptied. Boolean values can be true, false or toggle. Multiple values are concatenated with a single space. """ - value = " ".join(value) - strspec = f"{option}={value}" + strspec = f"{option}={' '.join(value)}" try: ctx.options.set(strspec) except exceptions.OptionsError as e: diff --git a/mitmproxy/command.py b/mitmproxy/command.py index 7203fe42..6977ff91 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -3,16 +3,14 @@ """ import functools import inspect -import re import sys import textwrap import types import typing -import pyparsing - import mitmproxy.types -from mitmproxy import exceptions +from mitmproxy import exceptions, command_lexer +from mitmproxy.command_lexer import unquote def verify_arg_signature(f: typing.Callable, args: typing.Iterable[typing.Any], kwargs: dict) -> None: @@ -144,16 +142,6 @@ class CommandManager: self.master = master self.commands = {} - self.expr_parser = pyparsing.ZeroOrMore( - 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(" \r\n\t") - ).leaveWhitespace() - def collect_commands(self, addon): for i in dir(addon): if not i.startswith("__"): @@ -183,7 +171,7 @@ class CommandManager: Parse a possibly partial command. Return a sequence of ParseResults and a sequence of remainder type help items. """ - parts: typing.List[str] = self.expr_parser.parseString(cmdstr) + parts: typing.List[str] = command_lexer.expr.parseString(cmdstr, parseAll=True) parsed: typing.List[ParseResult] = [] next_params: typing.List[CommandParameter] = [ @@ -284,28 +272,6 @@ class CommandManager: print(file=out) -def unquote(x: str) -> str: - quoted = ( - (x.startswith('"') and x.endswith('"')) - or - (x.startswith("'") and x.endswith("'")) - ) - if quoted: - x = x[1:-1] - # not sure if this is the right place, but pypyarsing doesn't process escape sequences. - x = re.sub(r"\\(.)", r"\g<1>", x) - return x - return x - - -def quote(val: str) -> str: - if not val: - return '""' - if all(ws not in val for ws in " \r\n\t"): - return val - return repr(val) - - def parsearg(manager: CommandManager, spec: str, argtype: type) -> typing.Any: """ Convert a string to a argument to the appropriate type. diff --git a/mitmproxy/command_lexer.py b/mitmproxy/command_lexer.py new file mode 100644 index 00000000..f042f3c9 --- /dev/null +++ b/mitmproxy/command_lexer.py @@ -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 diff --git a/mitmproxy/tools/console/consoleaddons.py b/mitmproxy/tools/console/consoleaddons.py index 4288696a..7fcd9b48 100644 --- a/mitmproxy/tools/console/consoleaddons.py +++ b/mitmproxy/tools/console/consoleaddons.py @@ -1,21 +1,18 @@ import csv -import shlex import typing +import mitmproxy.types +from mitmproxy import command, command_lexer +from mitmproxy import contentviews from mitmproxy import ctx -from mitmproxy import command from mitmproxy import exceptions from mitmproxy import flow from mitmproxy import http from mitmproxy import log -from mitmproxy import contentviews -from mitmproxy.utils import strutils -import mitmproxy.types - - +from mitmproxy.tools.console import keymap from mitmproxy.tools.console import overlay from mitmproxy.tools.console import signals -from mitmproxy.tools.console import keymap +from mitmproxy.utils import strutils console_palettes = [ "lowlight", @@ -48,10 +45,12 @@ class UnsupportedLog: """ A small addon to dump info on flow types we don't support yet. """ + def websocket_message(self, f): message = f.messages[-1] 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): 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 events. """ + def __init__(self, master): self.master = master self.started = False @@ -86,7 +86,7 @@ class ConsoleAddon: loader.add_option( "console_default_contentview", str, "auto", "The default content view mode.", - choices = [i.name.lower() for i in contentviews.views] + choices=[i.name.lower() for i in contentviews.views] ) loader.add_option( "console_eventlog_verbosity", str, 'info', @@ -142,7 +142,7 @@ class ConsoleAddon: opts = self.layout_options() off = self.layout_options().index(ctx.options.console_layout) ctx.options.update( - console_layout = opts[(off + 1) % len(opts)] + console_layout=opts[(off + 1) % len(opts)] ) @command.command("console.panes.next") @@ -234,17 +234,18 @@ class ConsoleAddon: @command.command("console.choose") def console_choose( - self, - prompt: str, - choices: typing.Sequence[str], - cmd: mitmproxy.types.Cmd, - *args: mitmproxy.types.CmdArgs + self, + prompt: str, + choices: typing.Sequence[str], + cmd: mitmproxy.types.Cmd, + *args: mitmproxy.types.CmdArgs ) -> None: """ Prompt the user to choose from a specified list of strings, then invoke another command with all occurrences of {choice} replaced by the choice the user made. """ + def callback(opt): # We're now outside of the call context... repl = cmd + " " + " ".join(args) @@ -260,11 +261,11 @@ class ConsoleAddon: @command.command("console.choose.cmd") def console_choose_cmd( - self, - prompt: str, - choicecmd: mitmproxy.types.Cmd, - subcmd: mitmproxy.types.Cmd, - *args: mitmproxy.types.CmdArgs + self, + prompt: str, + choicecmd: mitmproxy.types.Cmd, + subcmd: mitmproxy.types.Cmd, + *args: mitmproxy.types.CmdArgs ) -> None: """ Prompt the user to choose from a list of strings returned by a @@ -275,7 +276,7 @@ class ConsoleAddon: def callback(opt): # 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) try: self.master.commands.execute(subcmd + " " + repl) @@ -287,22 +288,24 @@ class ConsoleAddon: ) @command.command("console.command") - def console_command(self, *cmd_str: str) -> None: + def console_command(self, *command_str: str) -> None: """ Prompt the user to edit a command with a (possibly empty) starting value. """ - cmd_str = (command.quote(x) if x else "" for x in cmd_str) - signals.status_prompt_command.send(partial=" ".join(cmd_str)) # 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") def console_command_set(self, option_name: str) -> None: """ Prompt the user to set an option. """ - option_value = getattr(self.master.options, option_name, None) - option_value = command.quote(option_value) - self.master.commands.execute( - f"console.command set {option_name} {option_value}" + option_value = getattr(self.master.options, option_name, None) or "" + set_command = f"set {option_name} {option_value!r}" + cursor = len(set_command) - 1 + signals.status_prompt_command.send( + partial=set_command, + cursor=cursor ) @command.command("console.view.keybindings") @@ -570,11 +573,11 @@ class ConsoleAddon: @command.command("console.key.bind") def key_bind( - self, - contexts: typing.Sequence[str], - key: str, - cmd: mitmproxy.types.Cmd, - *args: mitmproxy.types.CmdArgs + self, + contexts: typing.Sequence[str], + key: str, + cmd: mitmproxy.types.Cmd, + *args: mitmproxy.types.CmdArgs ) -> None: """ Bind a shortcut key. diff --git a/mitmproxy/tools/console/defaultkeys.py b/mitmproxy/tools/console/defaultkeys.py index 0a6c5561..5e9f1f3c 100644 --- a/mitmproxy/tools/console/defaultkeys.py +++ b/mitmproxy/tools/console/defaultkeys.py @@ -26,7 +26,7 @@ def map(km): km.add("ctrl f", "console.nav.pagedown", ["global"], "Page down") km.add("ctrl b", "console.nav.pageup", ["global"], "Page up") - km.add("I", "set intercept_active=toggle", ["global"], "Toggle intercept") + km.add("I", "set intercept_active toggle", ["global"], "Toggle 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("A", "flow.resume @all", ["flowlist", "flowview"], "Resume all intercepted flows") @@ -48,7 +48,7 @@ def map(km): "Export this flow to file" ) km.add("f", "console.command.set view_filter", ["flowlist"], "Set view filter") - km.add("F", "set console_focus_follow=toggle", ["flowlist"], "Set focus follow") + km.add("F", "set console_focus_follow toggle", ["flowlist"], "Set focus follow") km.add( "ctrl l", "console.command cut.clip ", @@ -68,14 +68,14 @@ def map(km): "o", """ console.choose.cmd Order view.order.options - set view_order={choice} + set view_order {choice} """, ["flowlist"], "Set flow list order" ) km.add("r", "replay.client @focus", ["flowlist", "flowview"], "Replay this flow") km.add("S", "console.command replay.server ", ["flowlist"], "Start server replay") - km.add("v", "set view_order_reversed=toggle", ["flowlist"], "Reverse flow list order") + km.add("v", "set view_order_reversed toggle", ["flowlist"], "Reverse flow list order") km.add("U", "flow.mark @all false", ["flowlist"], "Un-set all marks") km.add("w", "console.command save.file @shown ", ["flowlist"], "Save listed flows to file") km.add("V", "flow.revert @focus", ["flowlist", "flowview"], "Revert changes to this flow") diff --git a/mitmproxy/tools/console/statusbar.py b/mitmproxy/tools/console/statusbar.py index 56f0674f..43f5170d 100644 --- a/mitmproxy/tools/console/statusbar.py +++ b/mitmproxy/tools/console/statusbar.py @@ -1,4 +1,5 @@ import os.path +from typing import Optional import urwid @@ -98,10 +99,15 @@ class ActionBar(urwid.WidgetWrap): self._w = urwid.Edit(self.prep_prompt(prompt), text or "") 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") - self._w = commander.CommandEdit(self.master, partial, - self.command_history) + self._w = commander.CommandEdit( + self.master, + partial, + self.command_history, + ) + if cursor is not None: + self._w.cbuf.cursor = cursor self.prompting = commandexecutor.CommandExecutor(self.master) def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()): diff --git a/test/mitmproxy/test_command_lexer.py b/test/mitmproxy/test_command_lexer.py new file mode 100644 index 00000000..3f009f88 --- /dev/null +++ b/test/mitmproxy/test_command_lexer.py @@ -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 diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py index 6b42de76..060e4b9b 100644 --- a/test/mitmproxy/tools/console/test_commander.py +++ b/test/mitmproxy/tools/console/test_commander.py @@ -269,7 +269,7 @@ class TestCommandBuffer: cb.text = "foo" assert cb.render() - cb.text = 'set view_filter=~bq test' + cb.text = 'set view_filter ~bq test' ret = cb.render() assert ret[0] == ('commander_command', 'set') assert ret[1] == ('text', ' ') -- cgit v1.2.3 From c7eedcbc1a90e9705172ffd6333cb740a5e9883f Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 19 Nov 2019 18:29:22 +0100 Subject: fix 'set' to only accept a single argument --- mitmproxy/addons/core.py | 4 ++-- test/mitmproxy/addons/test_core.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mitmproxy/addons/core.py b/mitmproxy/addons/core.py index 6fb2bf1e..8a3acedb 100644 --- a/mitmproxy/addons/core.py +++ b/mitmproxy/addons/core.py @@ -83,14 +83,14 @@ class Core: ) @command.command("set") - def set(self, option: str, *value: str) -> None: + def set(self, option: str, value: str = "") -> None: """ Set an option. When the value is omitted, booleans are set to true, strings and integers are set to None (if permitted), and sequences are emptied. Boolean values can be true, false or toggle. Multiple values are concatenated with a single space. """ - strspec = f"{option}={' '.join(value)}" + strspec = f"{option}={value}" try: ctx.options.set(strspec) except exceptions.OptionsError as e: diff --git a/test/mitmproxy/addons/test_core.py b/test/mitmproxy/addons/test_core.py index 59875c2b..e6924ead 100644 --- a/test/mitmproxy/addons/test_core.py +++ b/test/mitmproxy/addons/test_core.py @@ -11,7 +11,7 @@ def test_set(): sa = core.Core() with taddons.context(loadcore=False) as tctx: assert tctx.master.options.server - tctx.command(sa.set, "server=false") + tctx.command(sa.set, "server", "false") assert not tctx.master.options.server with pytest.raises(exceptions.CommandError): -- cgit v1.2.3 From 228e1c74c20c8db13d2cf5489321cd2975c6c56f Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 19 Nov 2019 18:37:47 +0100 Subject: fix tests --- test/mitmproxy/tools/console/test_commander.py | 33 ++++++++++++++++---------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py index 060e4b9b..8fc678fb 100644 --- a/test/mitmproxy/tools/console/test_commander.py +++ b/test/mitmproxy/tools/console/test_commander.py @@ -259,26 +259,33 @@ class TestCommandBuffer: ce.keypress(1, 'tab') ce.update() ret = ce.cbuf.render() - assert ret[0] == ('commander_command', 'set') - assert ret[1] == ('text', ' ') - assert ret[2] == ('commander_hint', '*options ') - + assert ret == [ + ('commander_command', 'set'), + ('text', ' '), + ('commander_hint', 'option '), + ('commander_hint', 'value '), + ] def test_render(self): with taddons.context() as tctx: cb = commander.CommandBuffer(tctx.master) cb.text = "foo" assert cb.render() - cb.text = 'set view_filter ~bq test' + cb.text = "set view_filter '~bq test'" ret = cb.render() - assert ret[0] == ('commander_command', 'set') - assert ret[1] == ('text', ' ') - assert ret[2] == ('text', 'view_filter=~bq') - assert ret[3] == ('text', ' ') - assert ret[4] == ('text', 'test') + assert ret == [ + ('commander_command', 'set'), + ('text', ' '), + ('text', 'view_filter'), + ('text', ' '), + ('text', "'~bq test'"), + ] cb.text = "set" ret = cb.render() - assert ret[0] == ('commander_command', 'set') - assert ret[1] == ('text', ' ') - assert ret[2] == ('commander_hint', '*options ') + assert ret == [ + ('commander_command', 'set'), + ('text', ' '), + ('commander_hint', 'option '), + ('commander_hint', 'value '), + ] \ No newline at end of file -- cgit v1.2.3 From fa100b9d16e3c521460801f2c3d6d3cc656abe75 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 19 Nov 2019 21:11:49 +0100 Subject: lint! --- test/mitmproxy/tools/console/test_commander.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py index 8fc678fb..a77be043 100644 --- a/test/mitmproxy/tools/console/test_commander.py +++ b/test/mitmproxy/tools/console/test_commander.py @@ -1,7 +1,8 @@ -from mitmproxy.tools.console.commander import commander -from mitmproxy.test import taddons import pytest +from mitmproxy.test import taddons +from mitmproxy.tools.console.commander import commander + class TestListCompleter: def test_cycle(self): @@ -265,6 +266,7 @@ class TestCommandBuffer: ('commander_hint', 'option '), ('commander_hint', 'value '), ] + def test_render(self): with taddons.context() as tctx: cb = commander.CommandBuffer(tctx.master) @@ -288,4 +290,4 @@ class TestCommandBuffer: ('text', ' '), ('commander_hint', 'option '), ('commander_hint', 'value '), - ] \ No newline at end of file + ] -- cgit v1.2.3 From f7a3e903ac69950ef862757886ed83506b7d1bd9 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 21 Nov 2019 14:06:03 +0100 Subject: fix keybind for loading flows --- mitmproxy/tools/console/defaultkeys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mitmproxy/tools/console/defaultkeys.py b/mitmproxy/tools/console/defaultkeys.py index 5e9f1f3c..a0f27625 100644 --- a/mitmproxy/tools/console/defaultkeys.py +++ b/mitmproxy/tools/console/defaultkeys.py @@ -55,7 +55,7 @@ def map(km): ["flowlist", "flowview"], "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", "view.properties.marked.toggle", ["flowlist"], "Toggle viewing marked flows") km.add( -- cgit v1.2.3