Repository: mitmproxy/mitmproxy

Issue:
MITM Proxy Crashes on when editing a "form"
Stack trace: 
```
Traceback (most recent call last):
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/master.py", line 202, in run
    self.loop.run()
  File "/Users/ograff/mitmproxy/venv/lib/python3.6/site-packages/urwid/main_loop.py", line 278, in run
    self._run()
  File "/Users/ograff/mitmproxy/venv/lib/python3.6/site-packages/urwid/main_loop.py", line 376, in _run
    self.event_loop.run()
  File "/Users/ograff/mitmproxy/venv/lib/python3.6/site-packages/urwid/main_loop.py", line 682, in run
    self._loop()
  File "/Users/ograff/mitmproxy/venv/lib/python3.6/site-packages/urwid/main_loop.py", line 719, in _loop
    self._watch_files[fd]()
  File "/Users/ograff/mitmproxy/venv/lib/python3.6/site-packages/urwid/raw_display.py", line 393, in <lambda>
    event_loop, callback, self.get_available_raw_input())
  File "/Users/ograff/mitmproxy/venv/lib/python3.6/site-packages/urwid/raw_display.py", line 493, in parse_input
    callback(processed, processed_codes)
  File "/Users/ograff/mitmproxy/venv/lib/python3.6/site-packages/urwid/main_loop.py", line 403, in _update
    self.process_input(keys)
  File "/Users/ograff/mitmproxy/venv/lib/python3.6/site-packages/urwid/main_loop.py", line 503, in process_input
    k = self._topmost_widget.keypress(self.screen_size, k)
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/window.py", line 274, in keypress
    k = fs.keypress(size, k)
  File "/Users/ograff/mitmproxy/venv/lib/python3.6/site-packages/urwid/container.py", line 592, in keypress
    *self.calculate_padding_filler(size, True)), key)
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/overlay.py", line 117, in keypress
    key = self.master.keymap.handle("chooser", key)
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/keymap.py", line 123, in handle
    return self.executor(b.command)
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/commandeditor.py", line 24, in __call__
    ret = self.master.commands.call(cmd)
  File "/Users/ograff/mitmproxy/mitmproxy/command.py", line 144, in call
    return self.call_args(parts[0], parts[1:])
  File "/Users/ograff/mitmproxy/mitmproxy/command.py", line 135, in call_args
    return self.commands[path].call(args)
  File "/Users/ograff/mitmproxy/mitmproxy/command.py", line 106, in call
    ret = self.func(*pargs)
  File "/Users/ograff/mitmproxy/mitmproxy/command.py", line 197, in wrapper
    return function(*args, **kwargs)
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/consoleaddons.py", line 125, in nav_select
    self.master.inject_key("m_select")
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/master.py", line 178, in inject_key
    self.loop.process_input([key])
  File "/Users/ograff/mitmproxy/venv/lib/python3.6/site-packages/urwid/main_loop.py", line 503, in process_input
    k = self._topmost_widget.keypress(self.screen_size, k)
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/window.py", line 274, in keypress
    k = fs.keypress(size, k)
  File "/Users/ograff/mitmproxy/venv/lib/python3.6/site-packages/urwid/container.py", line 592, in keypress
    *self.calculate_padding_filler(size, True)), key)
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/overlay.py", line 120, in keypress
    signals.pop_view_state.send(self)
  File "/Users/ograff/mitmproxy/venv/lib/python3.6/site-packages/blinker/base.py", line 267, in send
    for receiver in self.receivers_for(sender)]
  File "/Users/ograff/mitmproxy/venv/lib/python3.6/site-packages/blinker/base.py", line 267, in <listcomp>
    for receiver in self.receivers_for(sender)]
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/window.py", line 207, in pop
    if self.focus_stack().pop():
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/window.py", line 81, in pop
    self.call("layout_popping")
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/window.py", line 93, in call
    getattr(self.top_window(), name)(*args, **kwargs)
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/grideditor/base.py", line 463, in layout_popping
    self.call(self._w, "layout_popping")
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/grideditor/base.py", line 441, in call
    f(*args, **kwargs)
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/grideditor/base.py", line 313, in layout_popping
    self.callback(self.data_out(res), *self.cb_args, **self.cb_kwargs)
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/grideditor/base.py", line 456, in set_data_update
    self.set_data(vals, flow)
  File "/Users/ograff/mitmproxy/mitmproxy/tools/console/grideditor/editors.py", line 64, in set_data
    flow.request.urlencoded_form = vals
  File "/Users/ograff/mitmproxy/mitmproxy/net/http/request.py", line 462, in urlencoded_form
    self._set_urlencoded_form(value)
  File "/Users/ograff/mitmproxy/mitmproxy/net/http/request.py", line 444, in _set_urlencoded_form
    self.content = mitmproxy.net.http.url.encode(form_data, self.content.decode()).encode()
  File "/Users/ograff/mitmproxy/mitmproxy/net/http/url.py", line 81, in encode
    if encoded[-1] == '=':
IndexError: string index out of range
```

mitmproxy has crashed!
Please lodge a bug report at:
	https://github.com/mitmproxy/mitmproxy
Shutting down...

##### Steps to reproduce the problem:

1. Enter a flow
2. Press "e"
3. Select "form"


##### Any other comments? What have you tried so far?
`encoded` is an empty string but probably shouldn't be. 
Changing `mitmproxy/net/http/url.py` to just check for an empty string and not index into it if its empty results in an empty request body after exiting the editor.

##### System information


```
Mitmproxy version: 3.0.0 (2.0.0dev0631-0x30927468)
Python version: 3.6.0
Platform: Darwin-16.5.0-x86_64-i386-64bit
SSL version: OpenSSL 1.1.0f  25 May 2017
Mac version: 10.12.4 ('', '', '') x86_64
```


Discussion:
I have just reproduced it with `pathoc`. 
1. Run Mitmproxy.
2. `pathoc -c example.com:80 localhost:8080 'post:/:b200'`.
3. Enter the corresponding flow.
4. Press `e`.
5. `Form`.
The same traceback.
  

Resolving patch:
diff --git a/mitmproxy/net/http/url.py b/mitmproxy/net/http/url.py
--- a/mitmproxy/net/http/url.py
+++ b/mitmproxy/net/http/url.py
@@ -76,7 +76,7 @@ def encode(s: Sequence[Tuple[str, str]], similar_to: str=None) -> str:
 
     encoded = urllib.parse.urlencode(s, False, errors="surrogateescape")
 
-    if remove_trailing_equal:
+    if encoded and remove_trailing_equal:
         encoded = encoded.replace("=&", "&")
         if encoded[-1] == '=':
             encoded = encoded[:-1]


Test patch:
diff --git a/test/mitmproxy/net/http/test_url.py b/test/mitmproxy/net/http/test_url.py
--- a/test/mitmproxy/net/http/test_url.py
+++ b/test/mitmproxy/net/http/test_url.py
@@ -108,6 +108,7 @@ def test_empty_key_trailing_equal_sign():
 def test_encode():
     assert url.encode([('foo', 'bar')])
     assert url.encode([('foo', surrogates)])
+    assert not url.encode([], similar_to="justatext")
 
 
 def test_decode():