Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recognizes non-ASCII filenames in RFC 2231, and suport filename length is zero for multipart/form-data. #1497

Merged
merged 3 commits into from
Feb 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions sanic/request.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -356,28 +357,35 @@ 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*"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the form_parameters lookup is being done twice, maybe we can move the lookup to a single line and use a variable instead? (this is just a good to have change)

filename_rfc5987 = form_parameters.get("filename*")

if not file_name and filename_rfc5987:
    encoding, _, value = email.utils.decode_rfc5987(filename_rfc2231)
    file_name = unquote(value, encoding=encoding)

Also the format you are using is RFC 5987 standard and not RFC2231 if I remember correctly. (Sorry, for the nitpicking)

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this can be changed to if not file_name? Also, is there a reason to switch and invert the existing check here?

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
)
if field_name in files:
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 "
Expand Down
46 changes: 33 additions & 13 deletions tests/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,29 +432,49 @@ async def get(request):


@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")

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):
Expand Down