diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java index b4a9f1574c..19318788d5 100644 --- a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java @@ -25,8 +25,6 @@ import org.jboss.netty.util.CharsetUtil; */ public class ContinuationWebSocketFrame extends WebSocketFrame { - private String aggregatedText; - /** * Creates a new empty continuation frame. */ @@ -61,26 +59,6 @@ public class ContinuationWebSocketFrame extends WebSocketFrame { setBinaryData(binaryData); } - /** - * Creates a new continuation frame with the specified binary data - * - * @param finalFragment - * flag indicating if this frame is the final fragment - * @param rsv - * reserved bits used for protocol extensions - * @param binaryData - * the content of the frame. - * @param aggregatedText - * Aggregated text set by decoder on the final continuation frame of a fragmented text message - */ - public ContinuationWebSocketFrame( - boolean finalFragment, int rsv, ChannelBuffer binaryData, String aggregatedText) { - setFinalFragment(finalFragment); - setRsv(rsv); - setBinaryData(binaryData); - this.aggregatedText = aggregatedText; - } - /** * Creates a new continuation frame with the specified text data * @@ -125,16 +103,4 @@ public class ContinuationWebSocketFrame extends WebSocketFrame { public String toString() { return getClass().getSimpleName() + "(data: " + getBinaryData() + ')'; } - - /** - * Aggregated text returned by decoder on the final continuation frame of a fragmented text message - */ - public String getAggregatedText() { - return aggregatedText; - } - - public void setAggregatedText(String aggregatedText) { - this.aggregatedText = aggregatedText; - } - } diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Exception.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Exception.java deleted file mode 100644 index 67c2de53ad..0000000000 --- a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Exception.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2012 The Netty Project - * - * The Netty Project licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -/* - * Adaptation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ - * - * Copyright (c) 2008-2009 Bjoern Hoehrmann - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software - * and associated documentation files (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, publish, distribute, - * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING - * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package org.jboss.netty.handler.codec.http.websocketx; - -/** - * Invalid UTF8 bytes encountered - */ -final class UTF8Exception extends RuntimeException { - private static final long serialVersionUID = 1L; - - UTF8Exception(String reason) { - super(reason); - } -} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Output.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/Utf8Validator.java similarity index 85% rename from src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Output.java rename to src/main/java/org/jboss/netty/handler/codec/http/websocketx/Utf8Validator.java index 387e06b19c..706d7023ec 100644 --- a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Output.java +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/Utf8Validator.java @@ -35,10 +35,12 @@ */ package org.jboss.netty.handler.codec.http.websocketx; +import org.jboss.netty.handler.codec.frame.CorruptedFrameException; + /** * Checks UTF8 bytes for validity before converting it into a string */ -final class UTF8Output { +final class Utf8Validator { private static final int UTF8_ACCEPT = 0; private static final int UTF8_REJECT = 12; @@ -63,39 +65,37 @@ final class UTF8Output { @SuppressWarnings("RedundantFieldInitialization") private int state = UTF8_ACCEPT; private int codep; + private boolean checking; - private final StringBuilder stringBuilder; - - UTF8Output(byte[] bytes) { - stringBuilder = new StringBuilder(bytes.length); - write(bytes); - } - - public void write(byte[] bytes) { + public void check(byte[] bytes) throws CorruptedFrameException { + checking = true; for (byte b : bytes) { write(b); } } - public void write(int b) { + private void write(int b) throws CorruptedFrameException { byte type = TYPES[b & 0xFF]; codep = state != UTF8_ACCEPT ? b & 0x3f | codep << 6 : 0xff >> type & b; state = STATES[state + type]; - if (state == UTF8_ACCEPT) { - stringBuilder.append((char) codep); - } else if (state == UTF8_REJECT) { - throw new UTF8Exception("bytes are not UTF-8"); + if (state == UTF8_REJECT) { + throw new CorruptedFrameException("bytes are not UTF-8"); } } - @Override - public String toString() { + public void finish() throws CorruptedFrameException { + checking = false; + codep = 0; if (state != UTF8_ACCEPT) { - throw new UTF8Exception("bytes are not UTF-8"); + state = UTF8_ACCEPT; + throw new CorruptedFrameException("bytes are not UTF-8"); } - return stringBuilder.toString(); + } + + public boolean isChecking() { + return checking; } } diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java index 2efdee329e..b0218e7ecb 100644 --- a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java @@ -79,7 +79,7 @@ public class WebSocket08FrameDecoder extends ReplayingDecoder 0"); + } + this.maxFrameSize = maxFrameSize; + } + + @Override + protected Object decode(ChannelHandlerContext ctx, Channel channel, Object message) throws Exception { + if (!(message instanceof WebSocketFrame)) { + return message; + } + WebSocketFrame msg = (WebSocketFrame) message; + if (currentFrame == null) { + tooLongFrameFound = false; + if (msg.isFinalFragment()) { + return msg; + } + ChannelBuffer buf = msg.getBinaryData(); + + if (msg instanceof TextWebSocketFrame) { + currentFrame = new TextWebSocketFrame(true, msg.getRsv(), buf); + } else if (msg instanceof BinaryWebSocketFrame) { + currentFrame = new BinaryWebSocketFrame(true, msg.getRsv(), buf); + } else { + throw new IllegalStateException( + "WebSocket frame was not of type TextWebSocketFrame or BinaryWebSocketFrame"); + } + return null; + } + if (msg instanceof ContinuationWebSocketFrame) { + if (tooLongFrameFound) { + if (msg.isFinalFragment()) { + currentFrame = null; + } + return null; + } + ChannelBuffer content = currentFrame.getBinaryData(); + if (content.readableBytes() > maxFrameSize - msg.getBinaryData().readableBytes()) { + tooLongFrameFound = true; + throw new TooLongFrameException( + "WebSocketFrame length exceeded " + content + + " bytes."); + } + currentFrame.setBinaryData(ChannelBuffers.wrappedBuffer(content, msg.getBinaryData())); + + if (msg.isFinalFragment()) { + WebSocketFrame currentFrame = this.currentFrame; + this.currentFrame = null; + return currentFrame; + } else { + return null; + } + } + // It is possible to receive CLOSE/PING/PONG frames during fragmented frames so just pass them to the next + // handler in the chain + return msg; + } +} diff --git a/src/test/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrameAggregatorTest.java b/src/test/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrameAggregatorTest.java new file mode 100644 index 0000000000..278ff35931 --- /dev/null +++ b/src/test/java/org/jboss/netty/handler/codec/http/websocketx/WebSocketFrameAggregatorTest.java @@ -0,0 +1,138 @@ + +/* + * Copyright 2013 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.handler.codec.embedder.CodecEmbedderException; +import org.jboss.netty.handler.codec.embedder.DecoderEmbedder; +import org.jboss.netty.util.CharsetUtil; +import org.junit.Assert; +import org.junit.Test; + +public class WebSocketFrameAggregatorTest { + private final ChannelBuffer content1 = ChannelBuffers.copiedBuffer("Content1", CharsetUtil.UTF_8); + private final ChannelBuffer content2 = ChannelBuffers.copiedBuffer("Content2", CharsetUtil.UTF_8); + private final ChannelBuffer content3 = ChannelBuffers.copiedBuffer("Content3", CharsetUtil.UTF_8); + private final ChannelBuffer aggregatedContent = ChannelBuffers.wrappedBuffer( + content1.duplicate(), content2.duplicate(), content3.duplicate()); + @Test + public void testAggregationBinary() { + DecoderEmbedder channel = new DecoderEmbedder( + new WebSocketFrameAggregator(Integer.MAX_VALUE)); + channel.offer(new BinaryWebSocketFrame(true, 1, content1.copy())); + channel.offer(new BinaryWebSocketFrame(false, 0, content1.copy())); + channel.offer(new ContinuationWebSocketFrame(false, 0, content2.copy())); + channel.offer(new PingWebSocketFrame(content1.copy())); + channel.offer(new PongWebSocketFrame(content1.copy())); + channel.offer(new ContinuationWebSocketFrame(true, 0, content3.copy())); + + Assert.assertTrue(channel.finish()); + + BinaryWebSocketFrame frame = (BinaryWebSocketFrame) channel.poll(); + Assert.assertTrue(frame.isFinalFragment()); + Assert.assertEquals(1, frame.getRsv()); + Assert.assertEquals(content1, frame.getBinaryData()); + + PingWebSocketFrame frame2 = (PingWebSocketFrame) channel.poll(); + Assert.assertTrue(frame2.isFinalFragment()); + Assert.assertEquals(0, frame2.getRsv()); + Assert.assertEquals(content1, frame2.getBinaryData()); + + PongWebSocketFrame frame3 = (PongWebSocketFrame) channel.poll(); + Assert.assertTrue(frame3.isFinalFragment()); + Assert.assertEquals(0, frame3.getRsv()); + Assert.assertEquals(content1, frame3.getBinaryData()); + + BinaryWebSocketFrame frame4 = (BinaryWebSocketFrame) channel.poll(); + Assert.assertTrue(frame4.isFinalFragment()); + Assert.assertEquals(0, frame4.getRsv()); + Assert.assertEquals(aggregatedContent, frame4.getBinaryData()); + + Assert.assertNull(channel.poll()); + } + + @Test + public void testAggregationText() { + DecoderEmbedder channel = new DecoderEmbedder( + new WebSocketFrameAggregator(Integer.MAX_VALUE)); + channel.offer(new TextWebSocketFrame(true, 1, content1.copy())); + channel.offer(new TextWebSocketFrame(false, 0, content1.copy())); + channel.offer(new ContinuationWebSocketFrame(false, 0, content2.copy())); + channel.offer(new PingWebSocketFrame(content1.copy())); + channel.offer(new PongWebSocketFrame(content1.copy())); + channel.offer(new ContinuationWebSocketFrame(true, 0, content3.copy())); + + Assert.assertTrue(channel.finish()); + + TextWebSocketFrame frame = (TextWebSocketFrame) channel.poll(); + Assert.assertTrue(frame.isFinalFragment()); + Assert.assertEquals(1, frame.getRsv()); + Assert.assertEquals(content1, frame.getBinaryData()); + + PingWebSocketFrame frame2 = (PingWebSocketFrame) channel.poll(); + Assert.assertTrue(frame2.isFinalFragment()); + Assert.assertEquals(0, frame2.getRsv()); + Assert.assertEquals(content1, frame2.getBinaryData()); + + PongWebSocketFrame frame3 = (PongWebSocketFrame) channel.poll(); + Assert.assertTrue(frame3.isFinalFragment()); + Assert.assertEquals(0, frame3.getRsv()); + Assert.assertEquals(content1, frame3.getBinaryData()); + + TextWebSocketFrame frame4 = (TextWebSocketFrame) channel.poll(); + Assert.assertTrue(frame4.isFinalFragment()); + Assert.assertEquals(0, frame4.getRsv()); + Assert.assertEquals(aggregatedContent, frame4.getBinaryData()); + + Assert.assertNull(channel.poll()); + } + + @Test + public void textFrameTooBig() throws Exception { + DecoderEmbedder channel = new DecoderEmbedder( + new WebSocketFrameAggregator(8)); + channel.offer(new BinaryWebSocketFrame(true, 1, content1.copy())); + channel.offer(new BinaryWebSocketFrame(false, 0, content1.copy())); + try { + channel.offer(new ContinuationWebSocketFrame(false, 0, content2.copy())); + Assert.fail(); + } catch (CodecEmbedderException e) { + // expected + } + channel.offer(new ContinuationWebSocketFrame(false, 0, content2.copy())); + channel.offer(new ContinuationWebSocketFrame(true, 0, content2.copy())); + + channel.offer(new BinaryWebSocketFrame(true, 1, content1.copy())); + channel.offer(new BinaryWebSocketFrame(false, 0, content1.copy())); + try { + channel.offer(new ContinuationWebSocketFrame(false, 0, content2.copy())); + Assert.fail(); + } catch (CodecEmbedderException e) { + // expected + } + channel.offer(new ContinuationWebSocketFrame(false, 0, content2.copy())); + channel.offer(new ContinuationWebSocketFrame(true, 0, content2.copy())); + for (;;) { + Object msg = channel.poll(); + if (msg == null) { + break; + } + } + channel.finish(); + } +} \ No newline at end of file