diff --git a/mitmproxy/addons/export.py b/mitmproxy/addons/export.py index 271fc49db..761b3915b 100644 --- a/mitmproxy/addons/export.py +++ b/mitmproxy/addons/export.py @@ -44,17 +44,21 @@ def curl_command(f: flow.Flow) -> str: def httpie_command(f: flow.Flow) -> str: raise_if_missing_request(f) request = f.request.copy() # type: ignore - data = "http %s " % request.method + args = ["http"] + args.append(shlex.quote(request.method)) request.decode(strict=False) - data += "%s" % request.url + args.append(shlex.quote(request.url)) for k, v in request.headers.items(multi=True): - data += " '%s:%s'" % (k, v) + args.append(shlex.quote("%s:%s" % (k, v))) if request.content: - data += " <<< '%s'" % strutils.bytes_to_escaped_str( - request.content, - escape_single_quotes=True - ) - return data + try: + content = strutils.always_str(request.content) + except UnicodeDecodeError: + # shlex.quote doesn't support a bytes object + # see https://github.com/python/cpython/pull/10871 + raise exceptions.CommandError("Request content must be valid unicode") + args += ["<<<", shlex.quote(content)] + return ' '.join(args) def raw(f: flow.Flow) -> bytes: diff --git a/test/mitmproxy/addons/test_export.py b/test/mitmproxy/addons/test_export.py index 5c365135c..67d0fc99c 100644 --- a/test/mitmproxy/addons/test_export.py +++ b/test/mitmproxy/addons/test_export.py @@ -85,23 +85,40 @@ class TestExportCurlCommand: class TestExportHttpieCommand: def test_get(self, get_request): - result = """http GET http://address:22/path?a=foo&a=bar&b=baz 'header:qvalue' 'content-length:0'""" + result = """http GET 'http://address:22/path?a=foo&a=bar&b=baz' header:qvalue content-length:0""" assert export.httpie_command(get_request) == result def test_post(self, post_request): - result = "http POST http://address:22/path 'content-length:256' <<< '{}'".format( - str(bytes(range(256)))[2:-1] - ) + post_request.request.content = b'nobinarysupport' + result = "http POST http://address:22/path content-length:15 <<< nobinarysupport" assert export.httpie_command(post_request) == result + def test_fails_with_binary_data(self, post_request): + # shlex.quote doesn't support a bytes object + # see https://github.com/python/cpython/pull/10871 + with pytest.raises(exceptions.CommandError): + export.httpie_command(post_request) + def test_patch(self, patch_request): - result = """http PATCH http://address:22/path?query=param 'header:qvalue' 'content-length:7' <<< 'content'""" + result = """http PATCH 'http://address:22/path?query=param' header:qvalue content-length:7 <<< content""" assert export.httpie_command(patch_request) == result def test_tcp(self, tcp_flow): with pytest.raises(exceptions.CommandError): export.httpie_command(tcp_flow) + def test_escape_single_quotes_in_body(self): + request = tflow.tflow( + req=tutils.treq( + method=b'POST', + headers=(), + content=b"'&#" + ) + ) + command = export.httpie_command(request) + assert shlex.split(command)[-2] == '<<<' + assert shlex.split(command)[-1] == "'&#" + class TestRaw: def test_get(self, get_request):