diff --git a/mitmproxy/net/http/multipart.py b/mitmproxy/net/http/multipart.py index a854d47fd..4edf76acd 100644 --- a/mitmproxy/net/http/multipart.py +++ b/mitmproxy/net/http/multipart.py @@ -1,8 +1,43 @@ import re - +import mimetypes +from urllib.parse import quote from mitmproxy.net.http import headers +def encode(head, l): + + k = head.get("content-type") + if k: + k = headers.parse_content_type(k) + if k is not None: + try: + boundary = k[2]["boundary"].encode("ascii") + boundary = quote(boundary) + except (KeyError, UnicodeError): + return b"" + hdrs = [] + for key, value in l: + file_type = mimetypes.guess_type(str(key))[0] or "text/plain; charset=utf-8" + + if key: + hdrs.append(b"--%b" % boundary.encode('utf-8')) + disposition = b'form-data; name="%b"' % key + hdrs.append(b"Content-Disposition: %b" % disposition) + hdrs.append(b"Content-Type: %b" % file_type.encode('utf-8')) + hdrs.append(b'') + hdrs.append(value) + hdrs.append(b'') + + if value is not None: + # If boundary is found in value then raise ValueError + if re.search(rb"^--%b$" % re.escape(boundary.encode('utf-8')), value): + raise ValueError(b"boundary found in encoded string") + + hdrs.append(b"--%b--\r\n" % boundary.encode('utf-8')) + temp = b"\r\n".join(hdrs) + return temp + + def decode(hdrs, content): """ Takes a multipart boundary encoded string and returns list of (key, value) tuples. @@ -19,14 +54,14 @@ def decode(hdrs, content): rx = re.compile(br'\bname="([^"]+)"') r = [] - - for i in content.split(b"--" + boundary): - parts = i.splitlines() - if len(parts) > 1 and parts[0][0:2] != b"--": - match = rx.search(parts[1]) - if match: - key = match.group(1) - value = b"".join(parts[3 + parts[2:].index(b""):]) - r.append((key, value)) + if content is not None: + for i in content.split(b"--" + boundary): + parts = i.splitlines() + if len(parts) > 1 and parts[0][0:2] != b"--": + match = rx.search(parts[1]) + if match: + key = match.group(1) + value = b"".join(parts[3 + parts[2:].index(b""):]) + r.append((key, value)) return r return [] diff --git a/mitmproxy/net/http/request.py b/mitmproxy/net/http/request.py index 1569ea726..ba699e2aa 100644 --- a/mitmproxy/net/http/request.py +++ b/mitmproxy/net/http/request.py @@ -472,7 +472,8 @@ class Request(message.Message): return () def _set_multipart_form(self, value): - raise NotImplementedError() + self.content = mitmproxy.net.http.multipart.encode(self.headers, value) + self.headers["content-type"] = "multipart/form-data" @property def multipart_form(self): diff --git a/mitmproxy/tools/console/consoleaddons.py b/mitmproxy/tools/console/consoleaddons.py index b6602413e..9f595b422 100644 --- a/mitmproxy/tools/console/consoleaddons.py +++ b/mitmproxy/tools/console/consoleaddons.py @@ -381,7 +381,8 @@ class ConsoleAddon: """ return [ "cookies", - "form", + "urlencoded form", + "multipart form", "path", "method", "query", @@ -416,8 +417,10 @@ class ConsoleAddon: flow.response = http.HTTPResponse.make() if part == "cookies": self.master.switch_view("edit_focus_cookies") - elif part == "form": - self.master.switch_view("edit_focus_form") + elif part == "urlencoded form": + self.master.switch_view("edit_focus_urlencoded_form") + elif part == "multipart form": + self.master.switch_view("edit_focus_multipart_form") elif part == "path": self.master.switch_view("edit_focus_path") elif part == "query": diff --git a/mitmproxy/tools/console/grideditor/editors.py b/mitmproxy/tools/console/grideditor/editors.py index b4d593848..a4b46a516 100644 --- a/mitmproxy/tools/console/grideditor/editors.py +++ b/mitmproxy/tools/console/grideditor/editors.py @@ -53,14 +53,30 @@ class ResponseHeaderEditor(HeaderEditor): flow.response.headers = Headers(vals) -class RequestFormEditor(base.FocusEditor): - title = "Edit URL-encoded Form" +class RequestMultipartEditor(base.FocusEditor): + title = "Edit Multipart Form" columns = [ col_text.Column("Key"), col_text.Column("Value") ] def get_data(self, flow): + + return flow.request.multipart_form.items(multi=True) + + def set_data(self, vals, flow): + flow.request.multipart_form = vals + + +class RequestUrlEncodedEditor(base.FocusEditor): + title = "Edit UrlEncoded Form" + columns = [ + col_text.Column("Key"), + col_text.Column("Value") + ] + + def get_data(self, flow): + return flow.request.urlencoded_form.items(multi=True) def set_data(self, vals, flow): diff --git a/mitmproxy/tools/console/window.py b/mitmproxy/tools/console/window.py index 7669299c7..fb2e8c1e6 100644 --- a/mitmproxy/tools/console/window.py +++ b/mitmproxy/tools/console/window.py @@ -64,7 +64,8 @@ class WindowStack: edit_focus_cookies = grideditor.CookieEditor(master), edit_focus_setcookies = grideditor.SetCookieEditor(master), edit_focus_setcookie_attrs = grideditor.CookieAttributeEditor(master), - edit_focus_form = grideditor.RequestFormEditor(master), + edit_focus_multipart_form=grideditor.RequestMultipartEditor(master), + edit_focus_urlencoded_form=grideditor.RequestUrlEncodedEditor(master), edit_focus_path = grideditor.PathEditor(master), edit_focus_request_headers = grideditor.RequestHeaderEditor(master), edit_focus_response_headers = grideditor.ResponseHeaderEditor(master), diff --git a/test/mitmproxy/net/http/test_multipart.py b/test/mitmproxy/net/http/test_multipart.py index 68ae6bbdf..6d2e50170 100644 --- a/test/mitmproxy/net/http/test_multipart.py +++ b/test/mitmproxy/net/http/test_multipart.py @@ -1,5 +1,6 @@ from mitmproxy.net.http import Headers from mitmproxy.net.http import multipart +import pytest def test_decode(): @@ -22,3 +23,39 @@ def test_decode(): assert len(form) == 2 assert form[0] == (b"field1", b"value1") assert form[1] == (b"field2", b"value2") + + boundary = 'boundary茅莽' + headers = Headers( + content_type='multipart/form-data; boundary=' + boundary + ) + result = multipart.decode(headers, content) + assert result == [] + + headers = Headers( + content_type='' + ) + assert multipart.decode(headers, content) == [] + + +def test_encode(): + data = [("file".encode('utf-8'), "shell.jpg".encode('utf-8')), + ("file_size".encode('utf-8'), "1000".encode('utf-8'))] + headers = Headers( + content_type='multipart/form-data; boundary=127824672498' + ) + content = multipart.encode(headers, data) + + assert b'Content-Disposition: form-data; name="file"' in content + assert b'Content-Type: text/plain; charset=utf-8\r\n\r\nshell.jpg\r\n\r\n--127824672498\r\n' in content + assert b'1000\r\n\r\n--127824672498--\r\n' + assert len(content) == 252 + + with pytest.raises(ValueError, match=r"boundary found in encoded string"): + multipart.encode(headers, [("key".encode('utf-8'), "--127824672498".encode('utf-8'))]) + + boundary = 'boundary茅莽' + headers = Headers( + content_type='multipart/form-data; boundary=' + boundary + ) + result = multipart.encode(headers, data) + assert result == b'' diff --git a/test/mitmproxy/net/http/test_request.py b/test/mitmproxy/net/http/test_request.py index ef581a914..71d5c7a12 100644 --- a/test/mitmproxy/net/http/test_request.py +++ b/test/mitmproxy/net/http/test_request.py @@ -371,6 +371,7 @@ class TestRequestUtils: assert list(request.multipart_form.items()) == [] def test_set_multipart_form(self): - request = treq(content=b"foobar") - with pytest.raises(NotImplementedError): - request.multipart_form = "foobar" + request = treq() + request.multipart_form = [("file", "shell.jpg"), ("file_size", "1000")] + assert request.headers["Content-Type"] == 'multipart/form-data' + assert request.content is None