diff options
Diffstat (limited to 'meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-30251.patch')
| -rw-r--r-- | meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-30251.patch | 522 |
1 files changed, 522 insertions, 0 deletions
diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-30251.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-30251.patch new file mode 100644 index 0000000000..20226eb754 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-30251.patch | |||
| @@ -0,0 +1,522 @@ | |||
| 1 | From 44108afc0c0460d154216cab9aaa1d8f57edc3cc Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Sam Bull <git@sambull.org> | ||
| 3 | Date: Sun, 7 Apr 2024 13:19:31 +0100 | ||
| 4 | Subject: [PATCH] Fix handling of multipart/form-data (#8280) (#8302) | ||
| 5 | |||
| 6 | https://datatracker.ietf.org/doc/html/rfc7578 | ||
| 7 | (cherry picked from commit 7d0be3fee540a3d4161ac7dc76422f1f5ea60104) | ||
| 8 | |||
| 9 | The following commits are also included: | ||
| 10 | 7eecdff1 [PR #8332/482e6cdf backport][3.9] Add set_content_disposition test (#8333) | ||
| 11 | f21c6f2c [PR #8335/5a6949da backport][3.9] Add Content-Disposition automatically (#8336) | ||
| 12 | |||
| 13 | Upstream-Status: Backport | ||
| 14 | [https://github.com/aio-libs/aiohttp/commit/cebe526b9c34dc3a3da9140409db63014bc4cf19 | ||
| 15 | https://github.com/aio-libs/aiohttp/commit/7eecdff163ccf029fbb1ddc9de4169d4aaeb6597 | ||
| 16 | https://github.com/aio-libs/aiohttp/commit/f21c6f2ca512a026ce7f0f6c6311f62d6a638866] | ||
| 17 | |||
| 18 | CVE: CVE-2024-30251 | ||
| 19 | |||
| 20 | Signed-off-by: Jiaying Song <jiaying.song.cn@windriver.com> | ||
| 21 | --- | ||
| 22 | CHANGES/8280.bugfix.rst | 1 + | ||
| 23 | CHANGES/8280.deprecation.rst | 2 + | ||
| 24 | CHANGES/8332.bugfix.rst | 1 + | ||
| 25 | CHANGES/8335.bugfix.rst | 1 + | ||
| 26 | aiohttp/formdata.py | 12 +++- | ||
| 27 | aiohttp/multipart.py | 128 ++++++++++++++++++++++++----------- | ||
| 28 | tests/test_multipart.py | 87 ++++++++++++++++++++---- | ||
| 29 | tests/test_web_functional.py | 27 ++------ | ||
| 30 | 8 files changed, 182 insertions(+), 77 deletions(-) | ||
| 31 | create mode 100644 CHANGES/8280.bugfix.rst | ||
| 32 | create mode 100644 CHANGES/8280.deprecation.rst | ||
| 33 | create mode 100644 CHANGES/8332.bugfix.rst | ||
| 34 | create mode 100644 CHANGES/8335.bugfix.rst | ||
| 35 | |||
| 36 | diff --git a/CHANGES/8280.bugfix.rst b/CHANGES/8280.bugfix.rst | ||
| 37 | new file mode 100644 | ||
| 38 | index 0000000..3aebe36 | ||
| 39 | --- /dev/null | ||
| 40 | +++ b/CHANGES/8280.bugfix.rst | ||
| 41 | @@ -0,0 +1 @@ | ||
| 42 | +Fixed ``multipart/form-data`` compliance with :rfc:`7578` -- by :user:`Dreamsorcerer`. | ||
| 43 | diff --git a/CHANGES/8280.deprecation.rst b/CHANGES/8280.deprecation.rst | ||
| 44 | new file mode 100644 | ||
| 45 | index 0000000..302dbb2 | ||
| 46 | --- /dev/null | ||
| 47 | +++ b/CHANGES/8280.deprecation.rst | ||
| 48 | @@ -0,0 +1,2 @@ | ||
| 49 | +Deprecated ``content_transfer_encoding`` parameter in :py:meth:`FormData.add_field() | ||
| 50 | +<aiohttp.FormData.add_field>` -- by :user:`Dreamsorcerer`. | ||
| 51 | diff --git a/CHANGES/8332.bugfix.rst b/CHANGES/8332.bugfix.rst | ||
| 52 | new file mode 100644 | ||
| 53 | index 0000000..70cad26 | ||
| 54 | --- /dev/null | ||
| 55 | +++ b/CHANGES/8332.bugfix.rst | ||
| 56 | @@ -0,0 +1 @@ | ||
| 57 | +Fixed regression with adding Content-Disposition to form-data part after appending to writer -- by :user:`Dreamsorcerer`/:user:`Olegt0rr`. | ||
| 58 | diff --git a/CHANGES/8335.bugfix.rst b/CHANGES/8335.bugfix.rst | ||
| 59 | new file mode 100644 | ||
| 60 | index 0000000..cd93b86 | ||
| 61 | --- /dev/null | ||
| 62 | +++ b/CHANGES/8335.bugfix.rst | ||
| 63 | @@ -0,0 +1 @@ | ||
| 64 | +Added default Content-Disposition in multipart/form-data responses -- by :user:`Dreamsorcerer`. | ||
| 65 | diff --git a/aiohttp/formdata.py b/aiohttp/formdata.py | ||
| 66 | index e7cd24c..2b75b3d 100644 | ||
| 67 | --- a/aiohttp/formdata.py | ||
| 68 | +++ b/aiohttp/formdata.py | ||
| 69 | @@ -1,4 +1,5 @@ | ||
| 70 | import io | ||
| 71 | +import warnings | ||
| 72 | from typing import Any, Iterable, List, Optional | ||
| 73 | from urllib.parse import urlencode | ||
| 74 | |||
| 75 | @@ -53,7 +54,12 @@ class FormData: | ||
| 76 | if isinstance(value, io.IOBase): | ||
| 77 | self._is_multipart = True | ||
| 78 | elif isinstance(value, (bytes, bytearray, memoryview)): | ||
| 79 | + msg = ( | ||
| 80 | + "In v4, passing bytes will no longer create a file field. " | ||
| 81 | + "Please explicitly use the filename parameter or pass a BytesIO object." | ||
| 82 | + ) | ||
| 83 | if filename is None and content_transfer_encoding is None: | ||
| 84 | + warnings.warn(msg, DeprecationWarning) | ||
| 85 | filename = name | ||
| 86 | |||
| 87 | type_options: MultiDict[str] = MultiDict({"name": name}) | ||
| 88 | @@ -81,7 +87,11 @@ class FormData: | ||
| 89 | "content_transfer_encoding must be an instance" | ||
| 90 | " of str. Got: %s" % content_transfer_encoding | ||
| 91 | ) | ||
| 92 | - headers[hdrs.CONTENT_TRANSFER_ENCODING] = content_transfer_encoding | ||
| 93 | + msg = ( | ||
| 94 | + "content_transfer_encoding is deprecated. " | ||
| 95 | + "To maintain compatibility with v4 please pass a BytesPayload." | ||
| 96 | + ) | ||
| 97 | + warnings.warn(msg, DeprecationWarning) | ||
| 98 | self._is_multipart = True | ||
| 99 | |||
| 100 | self._fields.append((type_options, headers, value)) | ||
| 101 | diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py | ||
| 102 | index 73801f4..9cd49bb 100644 | ||
| 103 | --- a/aiohttp/multipart.py | ||
| 104 | +++ b/aiohttp/multipart.py | ||
| 105 | @@ -255,13 +255,22 @@ class BodyPartReader: | ||
| 106 | chunk_size = 8192 | ||
| 107 | |||
| 108 | def __init__( | ||
| 109 | - self, boundary: bytes, headers: "CIMultiDictProxy[str]", content: StreamReader | ||
| 110 | + self, | ||
| 111 | + boundary: bytes, | ||
| 112 | + headers: "CIMultiDictProxy[str]", | ||
| 113 | + content: StreamReader, | ||
| 114 | + *, | ||
| 115 | + subtype: str = "mixed", | ||
| 116 | + default_charset: Optional[str] = None, | ||
| 117 | ) -> None: | ||
| 118 | self.headers = headers | ||
| 119 | self._boundary = boundary | ||
| 120 | self._content = content | ||
| 121 | + self._default_charset = default_charset | ||
| 122 | self._at_eof = False | ||
| 123 | - length = self.headers.get(CONTENT_LENGTH, None) | ||
| 124 | + self._is_form_data = subtype == "form-data" | ||
| 125 | + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 | ||
| 126 | + length = None if self._is_form_data else self.headers.get(CONTENT_LENGTH, None) | ||
| 127 | self._length = int(length) if length is not None else None | ||
| 128 | self._read_bytes = 0 | ||
| 129 | # TODO: typeing.Deque is not supported by Python 3.5 | ||
| 130 | @@ -329,6 +338,8 @@ class BodyPartReader: | ||
| 131 | assert self._length is not None, "Content-Length required for chunked read" | ||
| 132 | chunk_size = min(size, self._length - self._read_bytes) | ||
| 133 | chunk = await self._content.read(chunk_size) | ||
| 134 | + if self._content.at_eof(): | ||
| 135 | + self._at_eof = True | ||
| 136 | return chunk | ||
| 137 | |||
| 138 | async def _read_chunk_from_stream(self, size: int) -> bytes: | ||
| 139 | @@ -444,7 +455,8 @@ class BodyPartReader: | ||
| 140 | """ | ||
| 141 | if CONTENT_TRANSFER_ENCODING in self.headers: | ||
| 142 | data = self._decode_content_transfer(data) | ||
| 143 | - if CONTENT_ENCODING in self.headers: | ||
| 144 | + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 | ||
| 145 | + if not self._is_form_data and CONTENT_ENCODING in self.headers: | ||
| 146 | return self._decode_content(data) | ||
| 147 | return data | ||
| 148 | |||
| 149 | @@ -478,7 +490,7 @@ class BodyPartReader: | ||
| 150 | """Returns charset parameter from Content-Type header or default.""" | ||
| 151 | ctype = self.headers.get(CONTENT_TYPE, "") | ||
| 152 | mimetype = parse_mimetype(ctype) | ||
| 153 | - return mimetype.parameters.get("charset", default) | ||
| 154 | + return mimetype.parameters.get("charset", self._default_charset or default) | ||
| 155 | |||
| 156 | @reify | ||
| 157 | def name(self) -> Optional[str]: | ||
| 158 | @@ -533,9 +545,17 @@ class MultipartReader: | ||
| 159 | part_reader_cls = BodyPartReader | ||
| 160 | |||
| 161 | def __init__(self, headers: Mapping[str, str], content: StreamReader) -> None: | ||
| 162 | + self._mimetype = parse_mimetype(headers[CONTENT_TYPE]) | ||
| 163 | + assert self._mimetype.type == "multipart", "multipart/* content type expected" | ||
| 164 | + if "boundary" not in self._mimetype.parameters: | ||
| 165 | + raise ValueError( | ||
| 166 | + "boundary missed for Content-Type: %s" % headers[CONTENT_TYPE] | ||
| 167 | + ) | ||
| 168 | + | ||
| 169 | self.headers = headers | ||
| 170 | self._boundary = ("--" + self._get_boundary()).encode() | ||
| 171 | self._content = content | ||
| 172 | + self._default_charset: Optional[str] = None | ||
| 173 | self._last_part: Optional[Union["MultipartReader", BodyPartReader]] = None | ||
| 174 | self._at_eof = False | ||
| 175 | self._at_bof = True | ||
| 176 | @@ -587,7 +607,24 @@ class MultipartReader: | ||
| 177 | await self._read_boundary() | ||
| 178 | if self._at_eof: # we just read the last boundary, nothing to do there | ||
| 179 | return None | ||
| 180 | - self._last_part = await self.fetch_next_part() | ||
| 181 | + | ||
| 182 | + part = await self.fetch_next_part() | ||
| 183 | + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.6 | ||
| 184 | + if ( | ||
| 185 | + self._last_part is None | ||
| 186 | + and self._mimetype.subtype == "form-data" | ||
| 187 | + and isinstance(part, BodyPartReader) | ||
| 188 | + ): | ||
| 189 | + _, params = parse_content_disposition(part.headers.get(CONTENT_DISPOSITION)) | ||
| 190 | + if params.get("name") == "_charset_": | ||
| 191 | + # Longest encoding in https://encoding.spec.whatwg.org/encodings.json | ||
| 192 | + # is 19 characters, so 32 should be more than enough for any valid encoding. | ||
| 193 | + charset = await part.read_chunk(32) | ||
| 194 | + if len(charset) > 31: | ||
| 195 | + raise RuntimeError("Invalid default charset") | ||
| 196 | + self._default_charset = charset.strip().decode() | ||
| 197 | + part = await self.fetch_next_part() | ||
| 198 | + self._last_part = part | ||
| 199 | return self._last_part | ||
| 200 | |||
| 201 | async def release(self) -> None: | ||
| 202 | @@ -623,19 +660,16 @@ class MultipartReader: | ||
| 203 | return type(self)(headers, self._content) | ||
| 204 | return self.multipart_reader_cls(headers, self._content) | ||
| 205 | else: | ||
| 206 | - return self.part_reader_cls(self._boundary, headers, self._content) | ||
| 207 | - | ||
| 208 | - def _get_boundary(self) -> str: | ||
| 209 | - mimetype = parse_mimetype(self.headers[CONTENT_TYPE]) | ||
| 210 | - | ||
| 211 | - assert mimetype.type == "multipart", "multipart/* content type expected" | ||
| 212 | - | ||
| 213 | - if "boundary" not in mimetype.parameters: | ||
| 214 | - raise ValueError( | ||
| 215 | - "boundary missed for Content-Type: %s" % self.headers[CONTENT_TYPE] | ||
| 216 | + return self.part_reader_cls( | ||
| 217 | + self._boundary, | ||
| 218 | + headers, | ||
| 219 | + self._content, | ||
| 220 | + subtype=self._mimetype.subtype, | ||
| 221 | + default_charset=self._default_charset, | ||
| 222 | ) | ||
| 223 | |||
| 224 | - boundary = mimetype.parameters["boundary"] | ||
| 225 | + def _get_boundary(self) -> str: | ||
| 226 | + boundary = self._mimetype.parameters["boundary"] | ||
| 227 | if len(boundary) > 70: | ||
| 228 | raise ValueError("boundary %r is too long (70 chars max)" % boundary) | ||
| 229 | |||
| 230 | @@ -726,6 +760,7 @@ class MultipartWriter(Payload): | ||
| 231 | super().__init__(None, content_type=ctype) | ||
| 232 | |||
| 233 | self._parts: List[_Part] = [] | ||
| 234 | + self._is_form_data = subtype == "form-data" | ||
| 235 | |||
| 236 | def __enter__(self) -> "MultipartWriter": | ||
| 237 | return self | ||
| 238 | @@ -803,32 +838,38 @@ class MultipartWriter(Payload): | ||
| 239 | |||
| 240 | def append_payload(self, payload: Payload) -> Payload: | ||
| 241 | """Adds a new body part to multipart writer.""" | ||
| 242 | - # compression | ||
| 243 | - encoding: Optional[str] = payload.headers.get( | ||
| 244 | - CONTENT_ENCODING, | ||
| 245 | - "", | ||
| 246 | - ).lower() | ||
| 247 | - if encoding and encoding not in ("deflate", "gzip", "identity"): | ||
| 248 | - raise RuntimeError(f"unknown content encoding: {encoding}") | ||
| 249 | - if encoding == "identity": | ||
| 250 | - encoding = None | ||
| 251 | - | ||
| 252 | - # te encoding | ||
| 253 | - te_encoding: Optional[str] = payload.headers.get( | ||
| 254 | - CONTENT_TRANSFER_ENCODING, | ||
| 255 | - "", | ||
| 256 | - ).lower() | ||
| 257 | - if te_encoding not in ("", "base64", "quoted-printable", "binary"): | ||
| 258 | - raise RuntimeError( | ||
| 259 | - "unknown content transfer encoding: {}" "".format(te_encoding) | ||
| 260 | + encoding: Optional[str] = None | ||
| 261 | + te_encoding: Optional[str] = None | ||
| 262 | + if self._is_form_data: | ||
| 263 | + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.7 | ||
| 264 | + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 | ||
| 265 | + assert ( | ||
| 266 | + not {CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TRANSFER_ENCODING} | ||
| 267 | + & payload.headers.keys() | ||
| 268 | ) | ||
| 269 | - if te_encoding == "binary": | ||
| 270 | - te_encoding = None | ||
| 271 | - | ||
| 272 | - # size | ||
| 273 | - size = payload.size | ||
| 274 | - if size is not None and not (encoding or te_encoding): | ||
| 275 | - payload.headers[CONTENT_LENGTH] = str(size) | ||
| 276 | + # Set default Content-Disposition in case user doesn't create one | ||
| 277 | + if CONTENT_DISPOSITION not in payload.headers: | ||
| 278 | + name = f"section-{len(self._parts)}" | ||
| 279 | + payload.set_content_disposition("form-data", name=name) | ||
| 280 | + else: | ||
| 281 | + # compression | ||
| 282 | + encoding = payload.headers.get(CONTENT_ENCODING, "").lower() | ||
| 283 | + if encoding and encoding not in ("deflate", "gzip", "identity"): | ||
| 284 | + raise RuntimeError(f"unknown content encoding: {encoding}") | ||
| 285 | + if encoding == "identity": | ||
| 286 | + encoding = None | ||
| 287 | + | ||
| 288 | + # te encoding | ||
| 289 | + te_encoding = payload.headers.get(CONTENT_TRANSFER_ENCODING, "").lower() | ||
| 290 | + if te_encoding not in ("", "base64", "quoted-printable", "binary"): | ||
| 291 | + raise RuntimeError(f"unknown content transfer encoding: {te_encoding}") | ||
| 292 | + if te_encoding == "binary": | ||
| 293 | + te_encoding = None | ||
| 294 | + | ||
| 295 | + # size | ||
| 296 | + size = payload.size | ||
| 297 | + if size is not None and not (encoding or te_encoding): | ||
| 298 | + payload.headers[CONTENT_LENGTH] = str(size) | ||
| 299 | |||
| 300 | self._parts.append((payload, encoding, te_encoding)) # type: ignore[arg-type] | ||
| 301 | return payload | ||
| 302 | @@ -886,6 +927,11 @@ class MultipartWriter(Payload): | ||
| 303 | async def write(self, writer: Any, close_boundary: bool = True) -> None: | ||
| 304 | """Write body.""" | ||
| 305 | for part, encoding, te_encoding in self._parts: | ||
| 306 | + if self._is_form_data: | ||
| 307 | + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 | ||
| 308 | + assert CONTENT_DISPOSITION in part.headers | ||
| 309 | + assert "name=" in part.headers[CONTENT_DISPOSITION] | ||
| 310 | + | ||
| 311 | await writer.write(b"--" + self._boundary + b"\r\n") | ||
| 312 | await writer.write(part._binary_headers) | ||
| 313 | |||
| 314 | diff --git a/tests/test_multipart.py b/tests/test_multipart.py | ||
| 315 | index cc3f5ff..1d036fb 100644 | ||
| 316 | --- a/tests/test_multipart.py | ||
| 317 | +++ b/tests/test_multipart.py | ||
| 318 | @@ -942,6 +942,58 @@ class TestMultipartReader: | ||
| 319 | assert first.at_eof() | ||
| 320 | assert not second.at_eof() | ||
| 321 | |||
| 322 | + async def test_read_form_default_encoding(self) -> None: | ||
| 323 | + with Stream( | ||
| 324 | + b"--:\r\n" | ||
| 325 | + b'Content-Disposition: form-data; name="_charset_"\r\n\r\n' | ||
| 326 | + b"ascii" | ||
| 327 | + b"\r\n" | ||
| 328 | + b"--:\r\n" | ||
| 329 | + b'Content-Disposition: form-data; name="field1"\r\n\r\n' | ||
| 330 | + b"foo" | ||
| 331 | + b"\r\n" | ||
| 332 | + b"--:\r\n" | ||
| 333 | + b"Content-Type: text/plain;charset=UTF-8\r\n" | ||
| 334 | + b'Content-Disposition: form-data; name="field2"\r\n\r\n' | ||
| 335 | + b"foo" | ||
| 336 | + b"\r\n" | ||
| 337 | + b"--:\r\n" | ||
| 338 | + b'Content-Disposition: form-data; name="field3"\r\n\r\n' | ||
| 339 | + b"foo" | ||
| 340 | + b"\r\n" | ||
| 341 | + ) as stream: | ||
| 342 | + reader = aiohttp.MultipartReader( | ||
| 343 | + {CONTENT_TYPE: 'multipart/form-data;boundary=":"'}, | ||
| 344 | + stream, | ||
| 345 | + ) | ||
| 346 | + field1 = await reader.next() | ||
| 347 | + assert field1.name == "field1" | ||
| 348 | + assert field1.get_charset("default") == "ascii" | ||
| 349 | + field2 = await reader.next() | ||
| 350 | + assert field2.name == "field2" | ||
| 351 | + assert field2.get_charset("default") == "UTF-8" | ||
| 352 | + field3 = await reader.next() | ||
| 353 | + assert field3.name == "field3" | ||
| 354 | + assert field3.get_charset("default") == "ascii" | ||
| 355 | + | ||
| 356 | + async def test_read_form_invalid_default_encoding(self) -> None: | ||
| 357 | + with Stream( | ||
| 358 | + b"--:\r\n" | ||
| 359 | + b'Content-Disposition: form-data; name="_charset_"\r\n\r\n' | ||
| 360 | + b"this-value-is-too-long-to-be-a-charset" | ||
| 361 | + b"\r\n" | ||
| 362 | + b"--:\r\n" | ||
| 363 | + b'Content-Disposition: form-data; name="field1"\r\n\r\n' | ||
| 364 | + b"foo" | ||
| 365 | + b"\r\n" | ||
| 366 | + ) as stream: | ||
| 367 | + reader = aiohttp.MultipartReader( | ||
| 368 | + {CONTENT_TYPE: 'multipart/form-data;boundary=":"'}, | ||
| 369 | + stream, | ||
| 370 | + ) | ||
| 371 | + with pytest.raises(RuntimeError, match="Invalid default charset"): | ||
| 372 | + await reader.next() | ||
| 373 | + | ||
| 374 | |||
| 375 | async def test_writer(writer) -> None: | ||
| 376 | assert writer.size == 7 | ||
| 377 | @@ -1228,6 +1280,25 @@ class TestMultipartWriter: | ||
| 378 | part = writer._parts[0][0] | ||
| 379 | assert part.headers[CONTENT_TYPE] == "test/passed" | ||
| 380 | |||
| 381 | + def test_set_content_disposition_after_append(self): | ||
| 382 | + writer = aiohttp.MultipartWriter("form-data") | ||
| 383 | + part = writer.append("some-data") | ||
| 384 | + part.set_content_disposition("form-data", name="method") | ||
| 385 | + assert 'name="method"' in part.headers[CONTENT_DISPOSITION] | ||
| 386 | + | ||
| 387 | + def test_automatic_content_disposition(self): | ||
| 388 | + writer = aiohttp.MultipartWriter("form-data") | ||
| 389 | + writer.append_json(()) | ||
| 390 | + part = payload.StringPayload("foo") | ||
| 391 | + part.set_content_disposition("form-data", name="second") | ||
| 392 | + writer.append_payload(part) | ||
| 393 | + writer.append("foo") | ||
| 394 | + | ||
| 395 | + disps = tuple(p[0].headers[CONTENT_DISPOSITION] for p in writer._parts) | ||
| 396 | + assert 'name="section-0"' in disps[0] | ||
| 397 | + assert 'name="second"' in disps[1] | ||
| 398 | + assert 'name="section-2"' in disps[2] | ||
| 399 | + | ||
| 400 | def test_with(self) -> None: | ||
| 401 | with aiohttp.MultipartWriter(boundary=":") as writer: | ||
| 402 | writer.append("foo") | ||
| 403 | @@ -1278,7 +1349,6 @@ class TestMultipartWriter: | ||
| 404 | CONTENT_TYPE: "text/python", | ||
| 405 | }, | ||
| 406 | ) | ||
| 407 | - content_length = part.size | ||
| 408 | await writer.write(stream) | ||
| 409 | |||
| 410 | assert part.headers[CONTENT_TYPE] == "text/python" | ||
| 411 | @@ -1289,9 +1359,7 @@ class TestMultipartWriter: | ||
| 412 | assert headers == ( | ||
| 413 | b"--:\r\n" | ||
| 414 | b"Content-Type: text/python\r\n" | ||
| 415 | - b'Content-Disposition: attachments; filename="bug.py"\r\n' | ||
| 416 | - b"Content-Length: %s" | ||
| 417 | - b"" % (str(content_length).encode(),) | ||
| 418 | + b'Content-Disposition: attachments; filename="bug.py"' | ||
| 419 | ) | ||
| 420 | |||
| 421 | async def test_set_content_disposition_override(self, buf, stream): | ||
| 422 | @@ -1305,7 +1373,6 @@ class TestMultipartWriter: | ||
| 423 | CONTENT_TYPE: "text/python", | ||
| 424 | }, | ||
| 425 | ) | ||
| 426 | - content_length = part.size | ||
| 427 | await writer.write(stream) | ||
| 428 | |||
| 429 | assert part.headers[CONTENT_TYPE] == "text/python" | ||
| 430 | @@ -1316,9 +1383,7 @@ class TestMultipartWriter: | ||
| 431 | assert headers == ( | ||
| 432 | b"--:\r\n" | ||
| 433 | b"Content-Type: text/python\r\n" | ||
| 434 | - b'Content-Disposition: attachments; filename="bug.py"\r\n' | ||
| 435 | - b"Content-Length: %s" | ||
| 436 | - b"" % (str(content_length).encode(),) | ||
| 437 | + b'Content-Disposition: attachments; filename="bug.py"' | ||
| 438 | ) | ||
| 439 | |||
| 440 | async def test_reset_content_disposition_header(self, buf, stream): | ||
| 441 | @@ -1330,8 +1395,6 @@ class TestMultipartWriter: | ||
| 442 | headers={CONTENT_TYPE: "text/plain"}, | ||
| 443 | ) | ||
| 444 | |||
| 445 | - content_length = part.size | ||
| 446 | - | ||
| 447 | assert CONTENT_DISPOSITION in part.headers | ||
| 448 | |||
| 449 | part.set_content_disposition("attachments", filename="bug.py") | ||
| 450 | @@ -1344,9 +1407,7 @@ class TestMultipartWriter: | ||
| 451 | b"--:\r\n" | ||
| 452 | b"Content-Type: text/plain\r\n" | ||
| 453 | b"Content-Disposition:" | ||
| 454 | - b' attachments; filename="bug.py"\r\n' | ||
| 455 | - b"Content-Length: %s" | ||
| 456 | - b"" % (str(content_length).encode(),) | ||
| 457 | + b' attachments; filename="bug.py"' | ||
| 458 | ) | ||
| 459 | |||
| 460 | |||
| 461 | diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py | ||
| 462 | index 5fdfb23..61739e9 100644 | ||
| 463 | --- a/tests/test_web_functional.py | ||
| 464 | +++ b/tests/test_web_functional.py | ||
| 465 | @@ -34,7 +34,8 @@ def fname(here): | ||
| 466 | |||
| 467 | def new_dummy_form(): | ||
| 468 | form = FormData() | ||
| 469 | - form.add_field("name", b"123", content_transfer_encoding="base64") | ||
| 470 | + with pytest.warns(DeprecationWarning, match="BytesPayload"): | ||
| 471 | + form.add_field("name", b"123", content_transfer_encoding="base64") | ||
| 472 | return form | ||
| 473 | |||
| 474 | |||
| 475 | @@ -429,25 +430,6 @@ async def test_release_post_data(aiohttp_client) -> None: | ||
| 476 | await resp.release() | ||
| 477 | |||
| 478 | |||
| 479 | -async def test_POST_DATA_with_content_transfer_encoding(aiohttp_client) -> None: | ||
| 480 | - async def handler(request): | ||
| 481 | - data = await request.post() | ||
| 482 | - assert b"123" == data["name"] | ||
| 483 | - return web.Response() | ||
| 484 | - | ||
| 485 | - app = web.Application() | ||
| 486 | - app.router.add_post("/", handler) | ||
| 487 | - client = await aiohttp_client(app) | ||
| 488 | - | ||
| 489 | - form = FormData() | ||
| 490 | - form.add_field("name", b"123", content_transfer_encoding="base64") | ||
| 491 | - | ||
| 492 | - resp = await client.post("/", data=form) | ||
| 493 | - assert 200 == resp.status | ||
| 494 | - | ||
| 495 | - await resp.release() | ||
| 496 | - | ||
| 497 | - | ||
| 498 | async def test_post_form_with_duplicate_keys(aiohttp_client) -> None: | ||
| 499 | async def handler(request): | ||
| 500 | data = await request.post() | ||
| 501 | @@ -505,7 +487,8 @@ async def test_100_continue(aiohttp_client) -> None: | ||
| 502 | return web.Response() | ||
| 503 | |||
| 504 | form = FormData() | ||
| 505 | - form.add_field("name", b"123", content_transfer_encoding="base64") | ||
| 506 | + with pytest.warns(DeprecationWarning, match="BytesPayload"): | ||
| 507 | + form.add_field("name", b"123", content_transfer_encoding="base64") | ||
| 508 | |||
| 509 | app = web.Application() | ||
| 510 | app.router.add_post("/", handler) | ||
| 511 | @@ -683,7 +666,7 @@ async def test_upload_file(aiohttp_client) -> None: | ||
| 512 | app.router.add_post("/", handler) | ||
| 513 | client = await aiohttp_client(app) | ||
| 514 | |||
| 515 | - resp = await client.post("/", data={"file": data}) | ||
| 516 | + resp = await client.post("/", data={"file": io.BytesIO(data)}) | ||
| 517 | assert 200 == resp.status | ||
| 518 | |||
| 519 | await resp.release() | ||
| 520 | -- | ||
| 521 | 2.25.1 | ||
| 522 | |||
