diff --git a/mitmproxy/addons/export.py b/mitmproxy/addons/export.py index 90e95d3ea..271fc49db 100644 --- a/mitmproxy/addons/export.py +++ b/mitmproxy/addons/export.py @@ -1,4 +1,5 @@ import typing +import shlex from mitmproxy import ctx from mitmproxy import command @@ -18,20 +19,26 @@ def raise_if_missing_request(f: flow.Flow) -> None: def curl_command(f: flow.Flow) -> str: raise_if_missing_request(f) - data = "curl " + args = ["curl"] request = f.request.copy() # type: ignore request.decode(strict=False) for k, v in request.headers.items(multi=True): - data += "-H '%s:%s' " % (k, v) + args += ["-H", shlex.quote("%s:%s" % (k, v))] if request.method != "GET": - data += "-X %s " % request.method - data += "'%s'" % request.url + args += ["-X", shlex.quote(request.method)] + args.append(shlex.quote(request.url)) if request.content: - data += " --data-binary '%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 += [ + "--data-binary", + shlex.quote(strutils.always_str(request.content)) + ] + return ' '.join(args) def httpie_command(f: flow.Flow) -> str: diff --git a/test/mitmproxy/addons/test_export.py b/test/mitmproxy/addons/test_export.py index f4bb0f648..5c365135c 100644 --- a/test/mitmproxy/addons/test_export.py +++ b/test/mitmproxy/addons/test_export.py @@ -1,4 +1,5 @@ import os +import shlex import pytest import pyperclip @@ -47,23 +48,40 @@ def tcp_flow(): class TestExportCurlCommand: def test_get(self, get_request): - result = """curl -H 'header:qvalue' -H 'content-length:0' 'http://address:22/path?a=foo&a=bar&b=baz'""" + result = """curl -H header:qvalue -H content-length:0 'http://address:22/path?a=foo&a=bar&b=baz'""" assert export.curl_command(get_request) == result def test_post(self, post_request): - result = "curl -H 'content-length:256' -X POST 'http://address:22/path' --data-binary '{}'".format( - str(bytes(range(256)))[2:-1] - ) + post_request.request.content = b'nobinarysupport' + result = "curl -H content-length:15 -X POST http://address:22/path --data-binary nobinarysupport" assert export.curl_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.curl_command(post_request) + def test_patch(self, patch_request): - result = """curl -H 'header:qvalue' -H 'content-length:7' -X PATCH 'http://address:22/path?query=param' --data-binary 'content'""" + result = """curl -H header:qvalue -H content-length:7 -X PATCH 'http://address:22/path?query=param' --data-binary content""" assert export.curl_command(patch_request) == result def test_tcp(self, tcp_flow): with pytest.raises(exceptions.CommandError): export.curl_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.curl_command(request) + assert shlex.split(command)[-2] == '--data-binary' + assert shlex.split(command)[-1] == "'&#" + class TestExportHttpieCommand: def test_get(self, get_request):