Skip to content

Commit 02ddb9d

Browse files
authored
fix: do not validate utf8 for partial text frames (#123)
1 parent bc39c1f commit 02ddb9d

3 files changed

Lines changed: 172 additions & 7 deletions

File tree

src/fragment.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,18 @@ impl Fragments {
229229
if self.fragments.is_some() {
230230
return Err(WebSocketError::InvalidFragment);
231231
}
232+
// Validate UTF-8 for unfragmented text messages
233+
if frame.opcode == OpCode::Text {
234+
match utf8::decode(&frame.payload) {
235+
Ok(_) => {}
236+
Err(utf8::DecodeError::Incomplete { .. }) => {
237+
return Err(WebSocketError::InvalidUTF8);
238+
}
239+
Err(utf8::DecodeError::Invalid { .. }) => {
240+
return Err(WebSocketError::InvalidUTF8);
241+
}
242+
}
243+
}
232244
return Ok(Some(Frame::new(true, frame.opcode, None, frame.payload)));
233245
} else {
234246
if frame.payload.len() >= max_message_size {

src/lib.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -645,13 +645,7 @@ impl ReadHalf {
645645
OpCode::Ping if self.auto_pong => {
646646
(Ok(None), Some(Frame::pong(frame.payload)))
647647
}
648-
OpCode::Text => {
649-
if frame.fin && !frame.is_utf8() {
650-
(Err(WebSocketError::InvalidUTF8), None)
651-
} else {
652-
(Ok(Some(frame)), None)
653-
}
654-
}
648+
OpCode::Text => (Ok(Some(frame)), None),
655649
_ => (Ok(Some(frame)), None),
656650
}
657651
}

tests/fragment_utf8.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Test for fragmented text frames with partial UTF-8 characters
2+
// https://github.com/denoland/fastwebsockets/issues/122
3+
4+
use tokio::io::AsyncWriteExt;
5+
use tokio::io::DuplexStream;
6+
7+
use fastwebsockets::FragmentCollector;
8+
use fastwebsockets::Frame;
9+
use fastwebsockets::OpCode;
10+
use fastwebsockets::Role;
11+
use fastwebsockets::WebSocket;
12+
13+
#[tokio::test]
14+
async fn test_fragmented_text_with_partial_utf8() {
15+
let (client, server) = tokio::io::duplex(1024);
16+
17+
let server_task = tokio::spawn(async move {
18+
handle_server(server).await.unwrap();
19+
});
20+
let client_task = tokio::spawn(async move {
21+
handle_client(client).await.unwrap();
22+
});
23+
24+
server_task.await.unwrap();
25+
client_task.await.unwrap();
26+
}
27+
28+
async fn handle_server(
29+
stream: DuplexStream,
30+
) -> Result<(), Box<dyn std::error::Error>> {
31+
let ws = WebSocket::after_handshake(stream, Role::Server);
32+
let mut ws = FragmentCollector::new(ws);
33+
34+
let frame = ws.read_frame().await?;
35+
assert_eq!(frame.opcode, OpCode::Text);
36+
assert_eq!(frame.fin, true);
37+
let text = std::str::from_utf8(&frame.payload)?;
38+
assert_eq!(text, "Hello 😀!");
39+
40+
Ok(())
41+
}
42+
43+
async fn handle_client(
44+
mut stream: DuplexStream,
45+
) -> Result<(), Box<dyn std::error::Error>> {
46+
// "Hello 😀!" where 😀 is U+1F600 (4 bytes in UTF-8: F0 9F 98 80)
47+
48+
// Frame 1: "Hello " + first 2 bytes of emoji (fin=false)
49+
let mut frame1_payload = b"Hello ".to_vec();
50+
frame1_payload.extend_from_slice(&[0xF0, 0x9F]); // First 2 bytes of 😀
51+
let frame1 = create_raw_frame(false, OpCode::Text, &frame1_payload);
52+
stream.write_all(&frame1).await?;
53+
54+
// Frame 2: last 2 bytes of emoji + "!" (fin=true, continuation)
55+
let mut frame2_payload = vec![0x98, 0x80]; // Last 2 bytes of 😀
56+
frame2_payload.extend_from_slice(b"!");
57+
let frame2 = create_raw_frame(true, OpCode::Continuation, &frame2_payload);
58+
stream.write_all(&frame2).await?;
59+
60+
Ok(())
61+
}
62+
63+
fn create_raw_frame(fin: bool, opcode: OpCode, payload: &[u8]) -> Vec<u8> {
64+
let mut frame = Vec::new();
65+
66+
// First byte: FIN + opcode
67+
let first_byte = if fin { 0x80 } else { 0x00 } | (opcode as u8);
68+
frame.push(first_byte);
69+
// Second byte: MASK bit (0 for server->client) + payload length
70+
let len = payload.len();
71+
if len < 126 {
72+
frame.push(len as u8);
73+
} else if len < 65536 {
74+
frame.push(126);
75+
frame.extend_from_slice(&(len as u16).to_be_bytes());
76+
} else {
77+
frame.push(127);
78+
frame.extend_from_slice(&(len as u64).to_be_bytes());
79+
}
80+
frame.extend_from_slice(payload);
81+
82+
frame
83+
}
84+
85+
#[tokio::test]
86+
async fn test_low_level_fragmented_text_with_partial_utf8() {
87+
// Test that the low-level WebSocket API doesn't validate UTF-8 on individual frames
88+
let (client, server) = tokio::io::duplex(1024);
89+
90+
let server_task = tokio::spawn(async move {
91+
handle_server_low_level(server).await.unwrap();
92+
});
93+
94+
let client_task = tokio::spawn(async move {
95+
handle_client(client).await.unwrap();
96+
});
97+
98+
server_task.await.unwrap();
99+
client_task.await.unwrap();
100+
}
101+
102+
async fn handle_server_low_level(
103+
stream: DuplexStream,
104+
) -> Result<(), Box<dyn std::error::Error>> {
105+
let mut ws = WebSocket::after_handshake(stream, Role::Server);
106+
107+
// should succeed even though it contains partial UTF-8
108+
let frame1 = ws.read_frame().await?;
109+
assert_eq!(frame1.opcode, OpCode::Text);
110+
assert_eq!(frame1.fin, false);
111+
112+
// should succeed even though it starts with partial UTF-8
113+
let frame2 = ws.read_frame().await?;
114+
assert_eq!(frame2.opcode, OpCode::Continuation);
115+
assert_eq!(frame2.fin, true);
116+
117+
// When combined, they should form valid UTF-8
118+
let mut combined = frame1.payload.to_vec();
119+
combined.extend_from_slice(&frame2.payload);
120+
let text = std::str::from_utf8(&combined)?;
121+
assert_eq!(text, "Hello 😀!");
122+
123+
Ok(())
124+
}
125+
126+
#[tokio::test]
127+
async fn test_invalid_unfragmented_utf8() {
128+
// Test that FragmentCollector rejects unfragmented text with invalid UTF-8
129+
// This corresponds to Autobahn test case 6.3.1
130+
let (client, server) = tokio::io::duplex(1024);
131+
132+
let server_task = tokio::spawn(async move {
133+
let ws = WebSocket::after_handshake(server, Role::Server);
134+
let mut ws = FragmentCollector::new(ws);
135+
136+
// Should fail with InvalidUTF8 error
137+
let result = ws.read_frame().await;
138+
assert!(result.is_err());
139+
match result {
140+
Err(fastwebsockets::WebSocketError::InvalidUTF8) => {}
141+
_ => panic!("Expected InvalidUTF8 error"),
142+
}
143+
});
144+
145+
let client_task = tokio::spawn(async move {
146+
let mut stream = client;
147+
// Send invalid UTF-8: κόσμε���edited (from Autobahn test 6.3.1)
148+
// Hex: cebae1bdb9cf83cebcceb5eda080656469746564
149+
let invalid_utf8 = vec![
150+
0xce, 0xba, 0xe1, 0xbd, 0xb9, 0xcf, 0x83, 0xce, 0xbc, 0xce, 0xb5, 0xed,
151+
0xa0, 0x80, 0x65, 0x64, 0x69, 0x74, 0x65, 0x64,
152+
];
153+
let frame = create_raw_frame(true, OpCode::Text, &invalid_utf8);
154+
stream.write_all(&frame).await.unwrap();
155+
});
156+
157+
server_task.await.unwrap();
158+
client_task.await.unwrap();
159+
}

0 commit comments

Comments
 (0)