Skip to content

Commit 4491058

Browse files
divybotlittledivy
andcommitted
fix: handle manual masks in fragment collector
Co-Authored-By: Divy Srivastava <me@littledivy.com>
1 parent 5f9c62c commit 4491058

4 files changed

Lines changed: 150 additions & 6 deletions

File tree

src/fragment.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ impl<'f, S> FragmentCollector<S> {
118118
if is_closed && frame.opcode != OpCode::Close {
119119
return Err(WebSocketError::ConnectionClosed);
120120
}
121-
if let Some(frame) = self.fragments.accumulate(frame)? {
121+
let manual_unmask = !self.read_half.auto_apply_mask;
122+
if let Some(frame) = self.fragments.accumulate(frame, manual_unmask)? {
122123
return Ok(frame);
123124
}
124125
}
@@ -191,7 +192,8 @@ impl<'f, S> FragmentCollectorRead<S> {
191192
let Some(frame) = res? else {
192193
continue;
193194
};
194-
if let Some(frame) = self.fragments.accumulate(frame)? {
195+
let manual_unmask = !self.read_half.auto_apply_mask;
196+
if let Some(frame) = self.fragments.accumulate(frame, manual_unmask)? {
195197
return Ok(frame);
196198
}
197199
}
@@ -214,16 +216,20 @@ impl Fragments {
214216

215217
pub fn accumulate<'f>(
216218
&mut self,
217-
frame: Frame<'f>,
219+
mut frame: Frame<'f>,
220+
manual_unmask: bool,
218221
) -> Result<Option<Frame<'f>>, WebSocketError> {
219222
match frame.opcode {
220223
OpCode::Text | OpCode::Binary => {
221224
if frame.fin {
222225
if self.fragments.is_some() {
223226
return Err(WebSocketError::InvalidFragment);
224227
}
225-
return Ok(Some(Frame::new(true, frame.opcode, None, frame.payload)));
228+
return Ok(Some(frame));
226229
} else {
230+
if manual_unmask {
231+
frame.unmask();
232+
}
227233
self.fragments = match frame.opcode {
228234
OpCode::Text => match utf8::decode(&frame.payload) {
229235
Ok(text) => Some(Fragment::Text(None, text.as_bytes().to_vec())),
@@ -249,6 +255,9 @@ impl Fragments {
249255
return Err(WebSocketError::InvalidContinuationFrame);
250256
}
251257
Some(Fragment::Text(data, input)) => {
258+
if manual_unmask {
259+
frame.unmask();
260+
}
252261
let mut tail = &frame.payload[..];
253262
if let Some(mut incomplete) = data.take() {
254263
if let Some((result, rest)) =
@@ -296,6 +305,9 @@ impl Fragments {
296305
}
297306
}
298307
Some(Fragment::Binary(data)) => {
308+
if manual_unmask {
309+
frame.unmask();
310+
}
299311
data.extend_from_slice(&frame.payload);
300312
if frame.fin {
301313
return Ok(Some(Frame::new(

src/frame.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,11 +261,15 @@ impl<'f> Frame<'f> {
261261
///
262262
/// Note: By default, the frame payload is unmasked by `WebSocket::read_frame`.
263263
pub fn unmask(&mut self) {
264-
if let Some(mask) = self.mask {
264+
if let Some(mask) = self.mask.take() {
265265
crate::mask::unmask(self.payload.to_mut(), mask);
266266
}
267267
}
268268

269+
pub(crate) fn is_masked(&self) -> bool {
270+
self.mask.is_some()
271+
}
272+
269273
/// Formats the frame header into the head buffer. Returns the size of the length field.
270274
///
271275
/// # Panics

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,7 @@ impl ReadHalf {
646646
(Ok(None), Some(Frame::pong(frame.payload)))
647647
}
648648
OpCode::Text => {
649-
if frame.fin && !frame.is_utf8() {
649+
if frame.fin && !frame.is_masked() && !frame.is_utf8() {
650650
(Err(WebSocketError::InvalidUTF8), None)
651651
} else {
652652
(Ok(Some(frame)), None)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright 2023-2026 Divy Srivastava <dj.srivastava23@gmail.com>
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
use anyhow::Result;
15+
use fastwebsockets::FragmentCollector;
16+
use fastwebsockets::Frame;
17+
use fastwebsockets::OpCode;
18+
use fastwebsockets::Role;
19+
use fastwebsockets::WebSocket;
20+
use tokio::io::AsyncWriteExt;
21+
use tokio::io::DuplexStream;
22+
23+
async fn write_masked_frame(
24+
stream: &mut DuplexStream,
25+
fin: bool,
26+
opcode: OpCode,
27+
mask: [u8; 4],
28+
payload: &[u8],
29+
) -> Result<()> {
30+
let mut masked = payload.to_vec();
31+
fastwebsockets::unmask(&mut masked, mask);
32+
33+
let mut frame = Frame::new(fin, opcode, Some(mask), masked.into());
34+
let mut buf = Vec::new();
35+
stream.write_all(frame.write(&mut buf)).await?;
36+
Ok(())
37+
}
38+
39+
#[tokio::test]
40+
async fn unfragmented_masked_text_can_be_manually_unmasked() -> Result<()> {
41+
let (mut client, server) = tokio::io::duplex(1024);
42+
let mut ws = WebSocket::after_handshake(server, Role::Server);
43+
ws.set_auto_apply_mask(false);
44+
let mut ws = FragmentCollector::new(ws);
45+
46+
write_masked_frame(
47+
&mut client,
48+
true,
49+
OpCode::Text,
50+
[0xff, 0xff, 0xff, 0xff],
51+
b"hello",
52+
)
53+
.await?;
54+
55+
let mut frame = ws.read_frame().await?;
56+
assert_eq!(frame.opcode, OpCode::Text);
57+
assert_ne!(&frame.payload[..], b"hello");
58+
59+
frame.unmask();
60+
assert_eq!(&frame.payload[..], b"hello");
61+
62+
Ok(())
63+
}
64+
65+
#[tokio::test]
66+
async fn fragmented_masked_text_is_unmasked_before_validation() -> Result<()> {
67+
let (mut client, server) = tokio::io::duplex(1024);
68+
let mut ws = WebSocket::after_handshake(server, Role::Server);
69+
ws.set_auto_apply_mask(false);
70+
let mut ws = FragmentCollector::new(ws);
71+
72+
write_masked_frame(
73+
&mut client,
74+
false,
75+
OpCode::Text,
76+
[0xff, 0xff, 0xff, 0xff],
77+
b"hello ",
78+
)
79+
.await?;
80+
write_masked_frame(
81+
&mut client,
82+
true,
83+
OpCode::Continuation,
84+
[0x80, 0x80, 0x80, 0x80],
85+
b"world",
86+
)
87+
.await?;
88+
89+
let mut frame = ws.read_frame().await?;
90+
assert_eq!(frame.opcode, OpCode::Text);
91+
assert_eq!(&frame.payload[..], b"hello world");
92+
93+
frame.unmask();
94+
assert_eq!(&frame.payload[..], b"hello world");
95+
96+
Ok(())
97+
}
98+
99+
#[tokio::test]
100+
async fn fragmented_masked_binary_is_unmasked() -> Result<()> {
101+
let (mut client, server) = tokio::io::duplex(1024);
102+
let mut ws = WebSocket::after_handshake(server, Role::Server);
103+
ws.set_auto_apply_mask(false);
104+
let mut ws = FragmentCollector::new(ws);
105+
106+
write_masked_frame(
107+
&mut client,
108+
false,
109+
OpCode::Binary,
110+
[0x7f, 0x7f, 0x7f, 0x7f],
111+
&[0, 1, 2, 3],
112+
)
113+
.await?;
114+
write_masked_frame(
115+
&mut client,
116+
true,
117+
OpCode::Continuation,
118+
[0x55, 0x55, 0x55, 0x55],
119+
&[4, 5, 6, 7],
120+
)
121+
.await?;
122+
123+
let frame = ws.read_frame().await?;
124+
assert_eq!(frame.opcode, OpCode::Binary);
125+
assert_eq!(&frame.payload[..], &[0, 1, 2, 3, 4, 5, 6, 7]);
126+
127+
Ok(())
128+
}

0 commit comments

Comments
 (0)