diff --git a/sanic/request.py b/sanic/request.py index 4402dae5..90c9de19 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -1,11 +1,12 @@ import asyncio +import email.utils import json import sys from cgi import parse_header from collections import namedtuple from http.cookies import SimpleCookie -from urllib.parse import parse_qs, urlunparse +from urllib.parse import parse_qs, unquote, urlunparse from httptools import parse_url @@ -356,15 +357,28 @@ def parse_multipart_form(body, boundary): ) if form_header_field == "content-disposition": - file_name = form_parameters.get("filename") field_name = form_parameters.get("name") + file_name = form_parameters.get("filename") + + # non-ASCII filenames in RFC2231, "filename*" format + if file_name is None and form_parameters.get("filename*"): + encoding, _, value = email.utils.decode_rfc2231( + form_parameters["filename*"] + ) + file_name = unquote(value, encoding=encoding) elif form_header_field == "content-type": content_type = form_header_value content_charset = form_parameters.get("charset", "utf-8") if field_name: post_data = form_part[line_index:-4] - if file_name: + if file_name is None: + value = post_data.decode(content_charset) + if field_name in fields: + fields[field_name].append(value) + else: + fields[field_name] = [value] + else: form_file = File( type=content_type, name=file_name, body=post_data ) @@ -372,12 +386,6 @@ def parse_multipart_form(body, boundary): files[field_name].append(form_file) else: files[field_name] = [form_file] - else: - value = post_data.decode(content_charset) - if field_name in fields: - fields[field_name].append(value) - else: - fields[field_name] = [value] else: logger.debug( "Form-data field does not have a 'name' parameter " diff --git a/tests/test_requests.py b/tests/test_requests.py index a7ebc831..a98a992e 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -432,21 +432,41 @@ def test_request_string_representation(app): @pytest.mark.parametrize( - "payload", + "payload,filename", [ - "------sanic\r\n" - 'Content-Disposition: form-data; filename="filename"; name="test"\r\n' - "\r\n" - "OK\r\n" - "------sanic--\r\n", - "------sanic\r\n" - 'content-disposition: form-data; filename="filename"; name="test"\r\n' - "\r\n" - 'content-type: application/json; {"field": "value"}\r\n' - "------sanic--\r\n", + ("------sanic\r\n" + 'Content-Disposition: form-data; filename="filename"; name="test"\r\n' + "\r\n" + "OK\r\n" + "------sanic--\r\n", "filename"), + ("------sanic\r\n" + 'content-disposition: form-data; filename="filename"; name="test"\r\n' + "\r\n" + 'content-type: application/json; {"field": "value"}\r\n' + "------sanic--\r\n", "filename"), + ("------sanic\r\n" + 'Content-Disposition: form-data; filename=""; name="test"\r\n' + "\r\n" + "OK\r\n" + "------sanic--\r\n", ""), + ("------sanic\r\n" + 'content-disposition: form-data; filename=""; name="test"\r\n' + "\r\n" + 'content-type: application/json; {"field": "value"}\r\n' + "------sanic--\r\n", ""), + ("------sanic\r\n" + 'Content-Disposition: form-data; filename*="utf-8\'\'filename_%C2%A0_test"; name="test"\r\n' + "\r\n" + "OK\r\n" + "------sanic--\r\n", "filename_\u00A0_test"), + ("------sanic\r\n" + 'content-disposition: form-data; filename*="utf-8\'\'filename_%C2%A0_test"; name="test"\r\n' + "\r\n" + 'content-type: application/json; {"field": "value"}\r\n' + "------sanic--\r\n", "filename_\u00A0_test"), ], ) -def test_request_multipart_files(app, payload): +def test_request_multipart_files(app, payload, filename): @app.route("/", methods=["POST"]) async def post(request): return text("OK") @@ -454,7 +474,7 @@ def test_request_multipart_files(app, payload): headers = {"content-type": "multipart/form-data; boundary=----sanic"} request, _ = app.test_client.post(data=payload, headers=headers) - assert request.files.get("test").name == "filename" + assert request.files.get("test").name == filename def test_request_multipart_file_with_json_content_type(app):