Skip to content

Commit e0cc020

Browse files
authored
Fix WebSocket reader with fragmented masked messages (#10764)
1 parent d884799 commit e0cc020

File tree

3 files changed

+72
-6
lines changed

3 files changed

+72
-6
lines changed

CHANGES/10764.bugfix.rst

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixed reading fragmented WebSocket messages when the payload was masked -- by :user:`bdraco`.
2+
3+
The problem first appeared in 3.11.17

aiohttp/_websocket/reader_py.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,7 @@ def _feed_data(self, data: bytes) -> None:
458458
self._payload_fragments.append(data_cstr[f_start_pos:f_end_pos])
459459
if self._has_mask:
460460
assert self._frame_mask is not None
461-
payload_bytearray = bytearray()
462-
payload_bytearray.join(self._payload_fragments)
461+
payload_bytearray = bytearray(b"".join(self._payload_fragments))
463462
websocket_mask(self._frame_mask, payload_bytearray)
464463
payload = payload_bytearray
465464
else:

tests/test_websocket_parser.py

+68-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import asyncio
22
import pickle
3+
import random
34
import struct
45
from typing import Optional, Union
56
from unittest import mock
67

78
import pytest
89

910
from aiohttp._websocket import helpers as _websocket_helpers
10-
from aiohttp._websocket.helpers import PACK_CLOSE_CODE, PACK_LEN1, PACK_LEN2
11+
from aiohttp._websocket.helpers import (
12+
PACK_CLOSE_CODE,
13+
PACK_LEN1,
14+
PACK_LEN2,
15+
PACK_LEN3,
16+
PACK_RANDBITS,
17+
websocket_mask,
18+
)
1119
from aiohttp._websocket.models import WS_DEFLATE_TRAILING
1220
from aiohttp._websocket.reader import WebSocketDataQueue
1321
from aiohttp.base_protocol import BaseProtocol
@@ -52,6 +60,7 @@ def build_frame(
5260
noheader: bool = False,
5361
is_fin: bool = True,
5462
ZLibBackend: Optional[ZLibBackendWrapper] = None,
63+
mask: bool = False,
5564
) -> bytes:
5665
# Send a frame over the websocket with message as its payload.
5766
compress = False
@@ -72,11 +81,21 @@ def build_frame(
7281
if compress:
7382
header_first_byte |= 0x40
7483

84+
mask_bit = 0x80 if mask else 0
85+
7586
if msg_length < 126:
76-
header = PACK_LEN1(header_first_byte, msg_length)
87+
header = PACK_LEN1(header_first_byte, msg_length | mask_bit)
88+
elif msg_length < 65536:
89+
header = PACK_LEN2(header_first_byte, 126 | mask_bit, msg_length)
7790
else:
78-
assert msg_length < (1 << 16)
79-
header = PACK_LEN2(header_first_byte, 126, msg_length)
91+
header = PACK_LEN3(header_first_byte, 127 | mask_bit, msg_length)
92+
93+
if mask:
94+
assert not noheader
95+
mask_bytes = PACK_RANDBITS(random.getrandbits(32))
96+
message_arr = bytearray(message)
97+
websocket_mask(mask_bytes, message_arr)
98+
return header + mask_bytes + message_arr
8099

81100
if noheader:
82101
return message
@@ -352,6 +371,51 @@ def test_fragmentation_header(
352371
assert res == WSMessageText(data="a", size=1, extra="")
353372

354373

374+
def test_large_message(
375+
out: WebSocketDataQueue, parser: PatchableWebSocketReader
376+
) -> None:
377+
large_payload = b"b" * 131072
378+
data = build_frame(large_payload, WSMsgType.BINARY)
379+
parser._feed_data(data)
380+
381+
res = out._buffer[0]
382+
assert res == WSMessageBinary(data=large_payload, size=131072, extra="")
383+
384+
385+
def test_large_masked_message(
386+
out: WebSocketDataQueue, parser: PatchableWebSocketReader
387+
) -> None:
388+
large_payload = b"b" * 131072
389+
data = build_frame(large_payload, WSMsgType.BINARY, mask=True)
390+
parser._feed_data(data)
391+
392+
res = out._buffer[0]
393+
assert res == WSMessageBinary(data=large_payload, size=131072, extra="")
394+
395+
396+
def test_fragmented_masked_message(
397+
out: WebSocketDataQueue, parser: PatchableWebSocketReader
398+
) -> None:
399+
large_payload = b"b" * 100
400+
data = build_frame(large_payload, WSMsgType.BINARY, mask=True)
401+
for i in range(len(data)):
402+
parser._feed_data(data[i : i + 1])
403+
404+
res = out._buffer[0]
405+
assert res == WSMessageBinary(data=large_payload, size=100, extra="")
406+
407+
408+
def test_large_fragmented_masked_message(
409+
out: WebSocketDataQueue, parser: PatchableWebSocketReader
410+
) -> None:
411+
large_payload = b"b" * 131072
412+
data = build_frame(large_payload, WSMsgType.BINARY, mask=True)
413+
for i in range(0, len(data), 16384):
414+
parser._feed_data(data[i : i + 16384])
415+
res = out._buffer[0]
416+
assert res == WSMessageBinary(data=large_payload, size=131072, extra="")
417+
418+
355419
def test_continuation(
356420
out: WebSocketDataQueue, parser: PatchableWebSocketReader
357421
) -> None:

0 commit comments

Comments
 (0)