Repository: mitmproxy/mitmproxy

Issue:
Mitmproxy crashes, when trying to use command, which contains '\'
##### Steps to reproduce the problem:

1. Run mitmproxy.
2. Press `:`
3. Input `\` and press `Enter`.

I am seeing:
```
Traceback (most recent call last):
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/master.py", line 216, in run
    self.loop.run()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 278, in run
    self._run()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 376, in _run
    self.event_loop.run()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 682, in run
    self._loop()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 719, in _loop
    self._watch_files[fd]()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/raw_display.py", line 393, in <lambda>
    event_loop, callback, self.get_available_raw_input())
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/raw_display.py", line 493, in parse_input
    callback(processed, processed_codes)
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 403, in _update
    self.process_input(keys)
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 503, in process_input
    k = self._topmost_widget.keypress(self.screen_size, k)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/window.py", line 308, in keypress
    k = super().keypress(size, k)
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/container.py", line 1116, in keypress
    return self.footer.keypress((maxcol,),key)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/statusbar.py", line 149, in keypress
    return self.ab.keypress(*args, **kwargs)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/statusbar.py", line 104, in keypress
    self.prompt_execute(self._w.get_edit_text())
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/statusbar.py", line 124, in prompt_execute
    msg = p(txt)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/commandexecutor.py", line 17, in __call__
    ret = self.master.commands.call(cmd)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/command.py", line 218, in call
    parts = list(lexer(cmdstr))
  File "/usr/lib/python3.5/shlex.py", line 263, in __next__
    token = self.get_token()
  File "/usr/lib/python3.5/shlex.py", line 90, in get_token
    raw = self.read_token()
  File "/usr/lib/python3.5/shlex.py", line 185, in read_token
    raise ValueError("No escaped character")
ValueError: No escaped character
```
##### Any other comments? What have you tried so far?

The exception is raised each time, when command line contains `\` (no matter where it is located.)


##### System information

Mitmproxy: 3.0.0.dev64 (commit 6dd336f) 
Python:    3.5.2
OpenSSL:   OpenSSL 1.1.0g  2 Nov 2017
Platform:  Linux-4.4.0-109-generic-x86_64-with-Ubuntu-16.04-xenial


Discussion:
**Update:**
Similar traceback appears, when we try to input `'` or `"` without closing quotation:

```
Traceback (most recent call last):
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/master.py", line 216, in run
    self.loop.run()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 278, in run
    self._run()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 376, in _run
    self.event_loop.run()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 682, in run
    self._loop()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 719, in _loop
    self._watch_files[fd]()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/raw_display.py", line 393, in <lambda>
    event_loop, callback, self.get_available_raw_input())
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/raw_display.py", line 493, in parse_input
    callback(processed, processed_codes)
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 403, in _update
    self.process_input(keys)
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 503, in process_input
    k = self._topmost_widget.keypress(self.screen_size, k)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/window.py", line 309, in keypress
    k = super().keypress(size, k)
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/container.py", line 1116, in keypress
    return self.footer.keypress((maxcol,),key)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/statusbar.py", line 150, in keypress
    return self.ab.keypress(*args, **kwargs)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/statusbar.py", line 104, in keypress
    self.prompt_execute(self._w.get_edit_text())
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/statusbar.py", line 124, in prompt_execute
    msg = p(txt)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/commandexecutor.py", line 17, in __call__
    ret = self.master.commands.call(cmd)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/command.py", line 218, in call
    parts = list(lexer(cmdstr))
  File "/usr/lib/python3.5/shlex.py", line 263, in __next__
    token = self.get_token()
  File "/usr/lib/python3.5/shlex.py", line 90, in get_token
    raw = self.read_token()
  File "/usr/lib/python3.5/shlex.py", line 166, in read_token
    raise ValueError("No closing quotation")
ValueError: No closing quotation
```
This is a problem with `shlex` as the exceptions suggest.
I have looked into it and one solution for this would be to just catch `ValueError` exception raised, which is not a bad idea as we don't know where else it might fail. Another option is to check for the cases we know about and pass the string accordingly dealing with them to `shlex.shlex`( [Shlex Parsing rules](https://docs.python.org/2/library/shlex.html#parsing-rules)) along with catching the exception.
@mhils @cortesi what would be the preferred way?

Resolving patch:
diff --git a/mitmproxy/addons/clientplayback.py b/mitmproxy/addons/clientplayback.py
--- a/mitmproxy/addons/clientplayback.py
+++ b/mitmproxy/addons/clientplayback.py
@@ -59,9 +59,7 @@ def replay(self, f):  # pragma: no cover
                 # In all modes, we directly connect to the server displayed
                 if self.options.mode.startswith("upstream:"):
                     server_address = server_spec.parse_with_mode(self.options.mode)[1].address
-                    server = connections.ServerConnection(
-                        server_address, (self.options.listen_host, 0)
-                    )
+                    server = connections.ServerConnection(server_address)
                     server.connect()
                     if r.scheme == "https":
                         connect_request = http.make_connect_request((r.data.host, r.port))
@@ -85,10 +83,7 @@ def replay(self, f):  # pragma: no cover
                         r.first_line_format = "absolute"
                 else:
                     server_address = (r.host, r.port)
-                    server = connections.ServerConnection(
-                        server_address,
-                        (self.options.listen_host, 0)
-                    )
+                    server = connections.ServerConnection(server_address)
                     server.connect()
                     if r.scheme == "https":
                         server.establish_tls(
diff --git a/mitmproxy/command.py b/mitmproxy/command.py
--- a/mitmproxy/command.py
+++ b/mitmproxy/command.py
@@ -236,7 +236,10 @@ def execute(self, cmdstr: str):
         """
             Execute a command string. May raise CommandError.
         """
-        parts = list(lexer(cmdstr))
+        try:
+            parts = list(lexer(cmdstr))
+        except ValueError as e:
+            raise exceptions.CommandError("Command error: %s" % e)
         if not len(parts) >= 1:
             raise exceptions.CommandError("Invalid command: %s" % cmdstr)
         return self.call_strings(parts[0], parts[1:])
diff --git a/mitmproxy/controller.py b/mitmproxy/controller.py
--- a/mitmproxy/controller.py
+++ b/mitmproxy/controller.py
@@ -96,8 +96,8 @@ def take(self):
     def commit(self):
         """
         Ultimately, messages are committed. This is done either automatically by
-        if the message is not taken or manually by the entity which called
-        .take().
+        the handler if the message is not taken or manually by the entity which
+        called .take().
         """
         if self.state != "taken":
             raise exceptions.ControlException(


Test patch:
diff --git a/test/mitmproxy/test_command.py b/test/mitmproxy/test_command.py
--- a/test/mitmproxy/test_command.py
+++ b/test/mitmproxy/test_command.py
@@ -281,6 +281,8 @@ 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"):
+            c.execute("\\")
 
         c.add("empty", a.empty)
         c.execute("empty")