Skip to content

Commit 73b26b7

Browse files
author
Norman Maurer
committed
Remove ContinuationWebSocketFrame.aggregatedText()
Motivation: Before we aggregated the full text in the WebSocket08FrameDecoder just to fill in the ContinuationWebSocketFrame.aggregatedText(). The problem was that there was no upper-limit and so it would be possible to see an OOME if the remote peer sends a TextWebSocketFrame + a never ending stream of ContinuationWebSocketFrames. Furthermore the aggregation does not really belong in the WebSocket08FrameDecoder, as we provide an extra ChannelHandler for this anyway (WebSocketFrameAggregator). Modification: Remove the ContinuationWebSocketFrame.aggregatedText() method and corresponding constructor. Also refactored WebSocket08FrameDecoder a bit to me more efficient which is now possible as we not need to aggregate here. Result: No more risk of OOME because of frames.
1 parent 9b980e2 commit 73b26b7

File tree

6 files changed

+276
-125
lines changed

6 files changed

+276
-125
lines changed

src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525
*/
2626
public class ContinuationWebSocketFrame extends WebSocketFrame {
2727

28-
private String aggregatedText;
29-
3028
/**
3129
* Creates a new empty continuation frame.
3230
*/
@@ -61,26 +59,6 @@ public ContinuationWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer
6159
setBinaryData(binaryData);
6260
}
6361

64-
/**
65-
* Creates a new continuation frame with the specified binary data
66-
*
67-
* @param finalFragment
68-
* flag indicating if this frame is the final fragment
69-
* @param rsv
70-
* reserved bits used for protocol extensions
71-
* @param binaryData
72-
* the content of the frame.
73-
* @param aggregatedText
74-
* Aggregated text set by decoder on the final continuation frame of a fragmented text message
75-
*/
76-
public ContinuationWebSocketFrame(
77-
boolean finalFragment, int rsv, ChannelBuffer binaryData, String aggregatedText) {
78-
setFinalFragment(finalFragment);
79-
setRsv(rsv);
80-
setBinaryData(binaryData);
81-
this.aggregatedText = aggregatedText;
82-
}
83-
8462
/**
8563
* Creates a new continuation frame with the specified text data
8664
*
@@ -125,16 +103,4 @@ public void setText(String text) {
125103
public String toString() {
126104
return getClass().getSimpleName() + "(data: " + getBinaryData() + ')';
127105
}
128-
129-
/**
130-
* Aggregated text returned by decoder on the final continuation frame of a fragmented text message
131-
*/
132-
public String getAggregatedText() {
133-
return aggregatedText;
134-
}
135-
136-
public void setAggregatedText(String aggregatedText) {
137-
this.aggregatedText = aggregatedText;
138-
}
139-
140106
}

src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Exception.java

Lines changed: 0 additions & 47 deletions
This file was deleted.

src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Output.java renamed to src/main/java/org/jboss/netty/handler/codec/http/websocketx/Utf8Validator.java

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@
3535
*/
3636
package org.jboss.netty.handler.codec.http.websocketx;
3737

38+
import org.jboss.netty.handler.codec.frame.CorruptedFrameException;
39+
3840
/**
3941
* Checks UTF8 bytes for validity before converting it into a string
4042
*/
41-
final class UTF8Output {
43+
final class Utf8Validator {
4244
private static final int UTF8_ACCEPT = 0;
4345
private static final int UTF8_REJECT = 12;
4446

@@ -63,39 +65,37 @@ final class UTF8Output {
6365
@SuppressWarnings("RedundantFieldInitialization")
6466
private int state = UTF8_ACCEPT;
6567
private int codep;
68+
private boolean checking;
6669

67-
private final StringBuilder stringBuilder;
68-
69-
UTF8Output(byte[] bytes) {
70-
stringBuilder = new StringBuilder(bytes.length);
71-
write(bytes);
72-
}
73-
74-
public void write(byte[] bytes) {
70+
public void check(byte[] bytes) throws CorruptedFrameException {
71+
checking = true;
7572
for (byte b : bytes) {
7673
write(b);
7774
}
7875
}
7976

80-
public void write(int b) {
77+
private void write(int b) throws CorruptedFrameException {
8178
byte type = TYPES[b & 0xFF];
8279

8380
codep = state != UTF8_ACCEPT ? b & 0x3f | codep << 6 : 0xff >> type & b;
8481

8582
state = STATES[state + type];
8683

87-
if (state == UTF8_ACCEPT) {
88-
stringBuilder.append((char) codep);
89-
} else if (state == UTF8_REJECT) {
90-
throw new UTF8Exception("bytes are not UTF-8");
84+
if (state == UTF8_REJECT) {
85+
throw new CorruptedFrameException("bytes are not UTF-8");
9186
}
9287
}
9388

94-
@Override
95-
public String toString() {
89+
public void finish() throws CorruptedFrameException {
90+
checking = false;
91+
codep = 0;
9692
if (state != UTF8_ACCEPT) {
97-
throw new UTF8Exception("bytes are not UTF-8");
93+
state = UTF8_ACCEPT;
94+
throw new CorruptedFrameException("bytes are not UTF-8");
9895
}
99-
return stringBuilder.toString();
96+
}
97+
98+
public boolean isChecking() {
99+
return checking;
100100
}
101101
}

src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public class WebSocket08FrameDecoder extends ReplayingDecoder<WebSocket08FrameDe
7979
private static final byte OPCODE_PING = 0x9;
8080
private static final byte OPCODE_PONG = 0xA;
8181

82-
private UTF8Output fragmentedFramesText;
82+
private Utf8Validator utf8Validator;
8383
private int fragmentedFramesCount;
8484

8585
private final long maxFramePayloadLength;
@@ -315,37 +315,33 @@ protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffe
315315

316316
// Processing for possible fragmented messages for text and binary
317317
// frames
318-
String aggregatedText = null;
319318
if (frameFinalFlag) {
320319
// Final frame of the sequence. Apparently ping frames are
321320
// allowed in the middle of a fragmented message
322321
if (frameOpcode != OPCODE_PING) {
323322
fragmentedFramesCount = 0;
324323

325324
// Check text for UTF8 correctness
326-
if (frameOpcode == OPCODE_TEXT || fragmentedFramesText != null) {
325+
if (frameOpcode == OPCODE_TEXT || (utf8Validator != null && utf8Validator.isChecking())) {
327326
// Check UTF-8 correctness for this payload
328327
checkUTF8String(channel, framePayload.array());
329328

330329
// This does a second check to make sure UTF-8
331330
// correctness for entire text message
332-
aggregatedText = fragmentedFramesText.toString();
333-
334-
fragmentedFramesText = null;
331+
utf8Validator.finish();
335332
}
336333
}
337334
} else {
338335
// Not final frame so we can expect more frames in the
339336
// fragmented sequence
340337
if (fragmentedFramesCount == 0) {
341338
// First text or binary frame for a fragmented set
342-
fragmentedFramesText = null;
343339
if (frameOpcode == OPCODE_TEXT) {
344340
checkUTF8String(channel, framePayload.array());
345341
}
346342
} else {
347343
// Subsequent frames - only check if init frame is text
348-
if (fragmentedFramesText != null) {
344+
if (utf8Validator != null && utf8Validator.isChecking()) {
349345
checkUTF8String(channel, framePayload.array());
350346
}
351347
}
@@ -360,7 +356,7 @@ protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffe
360356
} else if (frameOpcode == OPCODE_BINARY) {
361357
return new BinaryWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
362358
} else if (frameOpcode == OPCODE_CONT) {
363-
return new ContinuationWebSocketFrame(frameFinalFlag, frameRsv, framePayload, aggregatedText);
359+
return new ContinuationWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
364360
} else {
365361
throw new UnsupportedOperationException("Cannot decode web socket frame with opcode: " + frameOpcode);
366362
}
@@ -382,11 +378,15 @@ private void unmask(ChannelBuffer frame) {
382378
}
383379

384380
private void protocolViolation(Channel channel, String reason) throws CorruptedFrameException {
381+
protocolViolation(channel, new CorruptedFrameException(reason));
382+
}
383+
384+
private void protocolViolation(Channel channel, CorruptedFrameException ex) throws CorruptedFrameException {
385385
checkpoint(State.CORRUPT);
386386
if (channel.isConnected()) {
387387
channel.write(ChannelBuffers.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
388388
}
389-
throw new CorruptedFrameException(reason);
389+
throw ex;
390390
}
391391

392392
private static int toFrameLength(long l) throws TooLongFrameException {
@@ -399,20 +399,12 @@ private static int toFrameLength(long l) throws TooLongFrameException {
399399

400400
private void checkUTF8String(Channel channel, byte[] bytes) throws CorruptedFrameException {
401401
try {
402-
// StringBuilder sb = new StringBuilder("UTF8 " + bytes.length +
403-
// " bytes: ");
404-
// for (byte b : bytes) {
405-
// sb.append(Integer.toHexString(b)).append(" ");
406-
// }
407-
// logger.debug(sb.toString());
408-
409-
if (fragmentedFramesText == null) {
410-
fragmentedFramesText = new UTF8Output(bytes);
411-
} else {
412-
fragmentedFramesText.write(bytes);
402+
if (utf8Validator == null) {
403+
utf8Validator = new Utf8Validator();
413404
}
414-
} catch (UTF8Exception ex) {
415-
protocolViolation(channel, "invalid UTF-8 bytes");
405+
utf8Validator.check(bytes);
406+
} catch (CorruptedFrameException ex) {
407+
protocolViolation(channel, ex);
416408
}
417409
}
418410

@@ -440,9 +432,10 @@ protected void checkCloseFrameBody(Channel channel, ChannelBuffer buffer) throws
440432
byte[] b = new byte[buffer.readableBytes()];
441433
buffer.readBytes(b);
442434
try {
443-
new UTF8Output(b);
444-
} catch (UTF8Exception ex) {
445-
protocolViolation(channel, "Invalid close frame reason text. Invalid UTF-8 bytes");
435+
Utf8Validator validator = new Utf8Validator();
436+
validator.check(b);
437+
} catch (CorruptedFrameException ex) {
438+
protocolViolation(channel, ex);
446439
}
447440
}
448441

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2013 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package org.jboss.netty.handler.codec.http.websocketx;
17+
18+
19+
import org.jboss.netty.buffer.ChannelBuffer;
20+
import org.jboss.netty.buffer.ChannelBuffers;
21+
import org.jboss.netty.channel.Channel;
22+
import org.jboss.netty.channel.ChannelHandlerContext;
23+
import org.jboss.netty.handler.codec.frame.TooLongFrameException;
24+
import org.jboss.netty.handler.codec.oneone.OneToOneDecoder;
25+
26+
/**
27+
* Handler that aggregate fragmented WebSocketFrame's.
28+
*
29+
* Be aware if PING/PONG/CLOSE frames are send in the middle of a fragmented {@link WebSocketFrame} they will
30+
* just get forwarded to the next handler in the pipeline.
31+
*/
32+
public class WebSocketFrameAggregator extends OneToOneDecoder {
33+
private final int maxFrameSize;
34+
private WebSocketFrame currentFrame;
35+
private boolean tooLongFrameFound;
36+
37+
/**
38+
* Construct a new instance
39+
*
40+
* @param maxFrameSize If the size of the aggregated frame exceeds this value,
41+
* a {@link TooLongFrameException} is thrown.
42+
*/
43+
public WebSocketFrameAggregator(int maxFrameSize) {
44+
if (maxFrameSize < 1) {
45+
throw new IllegalArgumentException("maxFrameSize must be > 0");
46+
}
47+
this.maxFrameSize = maxFrameSize;
48+
}
49+
50+
@Override
51+
protected Object decode(ChannelHandlerContext ctx, Channel channel, Object message) throws Exception {
52+
if (!(message instanceof WebSocketFrame)) {
53+
return message;
54+
}
55+
WebSocketFrame msg = (WebSocketFrame) message;
56+
if (currentFrame == null) {
57+
tooLongFrameFound = false;
58+
if (msg.isFinalFragment()) {
59+
return msg;
60+
}
61+
ChannelBuffer buf = msg.getBinaryData();
62+
63+
if (msg instanceof TextWebSocketFrame) {
64+
currentFrame = new TextWebSocketFrame(true, msg.getRsv(), buf);
65+
} else if (msg instanceof BinaryWebSocketFrame) {
66+
currentFrame = new BinaryWebSocketFrame(true, msg.getRsv(), buf);
67+
} else {
68+
throw new IllegalStateException(
69+
"WebSocket frame was not of type TextWebSocketFrame or BinaryWebSocketFrame");
70+
}
71+
return null;
72+
}
73+
if (msg instanceof ContinuationWebSocketFrame) {
74+
if (tooLongFrameFound) {
75+
if (msg.isFinalFragment()) {
76+
currentFrame = null;
77+
}
78+
return null;
79+
}
80+
ChannelBuffer content = currentFrame.getBinaryData();
81+
if (content.readableBytes() > maxFrameSize - msg.getBinaryData().readableBytes()) {
82+
tooLongFrameFound = true;
83+
throw new TooLongFrameException(
84+
"WebSocketFrame length exceeded " + content +
85+
" bytes.");
86+
}
87+
currentFrame.setBinaryData(ChannelBuffers.wrappedBuffer(content, msg.getBinaryData()));
88+
89+
if (msg.isFinalFragment()) {
90+
WebSocketFrame currentFrame = this.currentFrame;
91+
this.currentFrame = null;
92+
return currentFrame;
93+
} else {
94+
return null;
95+
}
96+
}
97+
// It is possible to receive CLOSE/PING/PONG frames during fragmented frames so just pass them to the next
98+
// handler in the chain
99+
return msg;
100+
}
101+
}

0 commit comments

Comments
 (0)