|
| 1 | +"""Parsing of the ``multipart/form-data`` bodies given to the query |
| 2 | +API. |
| 3 | +""" |
| 4 | + |
| 5 | +import io |
| 6 | +import logging |
| 7 | +from collections.abc import Mapping |
| 8 | +from email.message import EmailMessage |
| 9 | + |
| 10 | +from beartype import beartype |
| 11 | +from werkzeug.datastructures import FileStorage, MultiDict |
| 12 | +from werkzeug.formparser import MultiPartParser |
| 13 | + |
| 14 | +from mock_vws._query_validators.exceptions import NoContentDispositionError |
| 15 | + |
| 16 | +_LOGGER = logging.getLogger(name=__name__) |
| 17 | + |
| 18 | + |
| 19 | +@beartype |
| 20 | +def _parse_with_boundary( |
| 21 | + *, |
| 22 | + request_body: bytes, |
| 23 | + boundary: bytes, |
| 24 | +) -> tuple[MultiDict[str, str], MultiDict[str, FileStorage]]: |
| 25 | + """Parse a multipart body, requiring it to be complete. |
| 26 | +
|
| 27 | + Args: |
| 28 | + request_body: The body of the request. |
| 29 | + boundary: The multipart boundary, without its leading dashes. |
| 30 | +
|
| 31 | + Returns: |
| 32 | + The fields and the files parsed from the multipart body. |
| 33 | +
|
| 34 | + Raises: |
| 35 | + ValueError: The body is not a complete multipart body. |
| 36 | + """ |
| 37 | + parser = MultiPartParser() |
| 38 | + return parser.parse( |
| 39 | + stream=io.BytesIO(initial_bytes=request_body), |
| 40 | + boundary=boundary, |
| 41 | + content_length=len(request_body), |
| 42 | + ) |
| 43 | + |
| 44 | + |
| 45 | +@beartype |
| 46 | +def parse_multipart( |
| 47 | + *, |
| 48 | + request_headers: Mapping[str, str], |
| 49 | + request_body: bytes, |
| 50 | +) -> tuple[MultiDict[str, str], MultiDict[str, FileStorage]]: |
| 51 | + """Parse the multipart body of a query request. |
| 52 | +
|
| 53 | + Vuforia accepts a body which ends before its closing boundary, as a |
| 54 | + client which is cut off mid-upload sends, and treats the end of the body |
| 55 | + as the end of the part being uploaded. ``MultiPartParser`` rejects such a |
| 56 | + body, so we give it the closing boundary which the client did not send. |
| 57 | +
|
| 58 | + Args: |
| 59 | + request_headers: The headers sent with the request. |
| 60 | + request_body: The body of the request. |
| 61 | +
|
| 62 | + Returns: |
| 63 | + The fields and the files parsed from the multipart body. |
| 64 | +
|
| 65 | + Raises: |
| 66 | + NoContentDispositionError: The body ends within the headers of a part, |
| 67 | + or a part has no ``Content-Disposition`` header, so no part can be |
| 68 | + named. |
| 69 | + """ |
| 70 | + email_message = EmailMessage() |
| 71 | + email_message["Content-Type"] = request_headers["Content-Type"] |
| 72 | + boundary = email_message.get_boundary(failobj="").encode(encoding="utf-8") |
| 73 | + closing_boundary = b"\r\n--" + boundary + b"--\r\n" |
| 74 | + |
| 75 | + # A body which ends part-way through the closing boundary keeps those |
| 76 | + # bytes: Vuforia gives them to the part being uploaded rather than |
| 77 | + # ignoring them. We therefore try the body as it was sent before we try |
| 78 | + # it without its incomplete closing boundary. |
| 79 | + without_partial_boundary = request_body |
| 80 | + for length in reversed(range(1, len(closing_boundary))): |
| 81 | + if request_body.endswith(closing_boundary[:length]): |
| 82 | + without_partial_boundary = request_body[:-length] |
| 83 | + break |
| 84 | + |
| 85 | + candidates = ( |
| 86 | + request_body, |
| 87 | + # The body ended within the data of a part. |
| 88 | + request_body + closing_boundary, |
| 89 | + # The body ended part-way through a header of a part, so the blank |
| 90 | + # line which ends the headers of that part is missing too. |
| 91 | + request_body + b"\r\n" + closing_boundary, |
| 92 | + # The body ended part-way through the closing boundary. |
| 93 | + without_partial_boundary + closing_boundary, |
| 94 | + ) |
| 95 | + |
| 96 | + for candidate in candidates: |
| 97 | + try: |
| 98 | + return _parse_with_boundary( |
| 99 | + request_body=candidate, |
| 100 | + boundary=boundary, |
| 101 | + ) |
| 102 | + except ValueError: |
| 103 | + continue |
| 104 | + |
| 105 | + # Every remaining body is one in which a part has no usable |
| 106 | + # ``Content-Disposition`` header, either because the body ends before that |
| 107 | + # header is complete or because the part does not have one. |
| 108 | + _LOGGER.warning(msg="A part has no Content-Disposition header.") |
| 109 | + raise NoContentDispositionError |
0 commit comments