From dc615ecaaf9749edd227413c32c2f288dfc1309e Mon Sep 17 00:00:00 2001 From: Norman Maurer Date: Thu, 17 Dec 2015 14:57:36 +0100 Subject: [PATCH] [#4212] Backport WebSocket Extension handlers for client and server. Motivation: We have websocket extension support (with compression) in old master. We should port this to 4.1 Modifications: Backport relevant code. Result: websocket extension support (with compression) is now in 4.1. --- .../http/websocketx/TextWebSocketFrame.java | 6 +- .../http/websocketx/Utf8FrameValidator.java | 103 +++++++ .../websocketx/WebSocket08FrameDecoder.java | 37 --- .../WebSocketClientProtocolHandler.java | 5 + .../WebSocketServerProtocolHandler.java | 5 + .../extensions/WebSocketClientExtension.java | 23 ++ .../WebSocketClientExtensionHandler.java | 131 +++++++++ .../WebSocketClientExtensionHandshaker.java | 41 +++ .../extensions/WebSocketExtension.java | 42 +++ .../extensions/WebSocketExtensionData.java | 55 ++++ .../extensions/WebSocketExtensionDecoder.java | 26 ++ .../extensions/WebSocketExtensionEncoder.java | 26 ++ .../extensions/WebSocketExtensionUtil.java | 106 ++++++++ .../extensions/WebSocketServerExtension.java | 31 +++ .../WebSocketServerExtensionHandler.java | 141 ++++++++++ .../WebSocketServerExtensionHandshaker.java | 33 +++ .../compression/DeflateDecoder.java | 144 ++++++++++ .../compression/DeflateEncoder.java | 146 ++++++++++ ...DeflateFrameClientExtensionHandshaker.java | 104 +++++++ ...DeflateFrameServerExtensionHandshaker.java | 103 +++++++ .../compression/PerFrameDeflateDecoder.java | 54 ++++ .../compression/PerFrameDeflateEncoder.java | 58 ++++ ...ssageDeflateClientExtensionHandshaker.java | 197 ++++++++++++++ .../compression/PerMessageDeflateDecoder.java | 72 +++++ .../compression/PerMessageDeflateEncoder.java | 75 ++++++ ...ssageDeflateServerExtensionHandshaker.java | 196 ++++++++++++++ .../WebSocketClientCompressionHandler.java | 40 +++ .../WebSocketServerCompressionHandler.java | 36 +++ .../extensions/compression/package-info.java | 33 +++ .../websocketx/extensions/package-info.java | 23 ++ .../WebSocketClientExtensionHandlerTest.java | 253 ++++++++++++++++++ .../WebSocketExtensionTestUtil.java | 129 +++++++++ .../WebSocketServerExtensionHandlerTest.java | 176 ++++++++++++ ...ateFrameClientExtensionHandshakerTest.java | 84 ++++++ ...ateFrameServerExtensionHandshakerTest.java | 84 ++++++ .../PerFrameDeflateDecoderTest.java | 123 +++++++++ .../PerFrameDeflateEncoderTest.java | 162 +++++++++++ ...eDeflateClientExtensionHandshakerTest.java | 122 +++++++++ .../PerMessageDeflateDecoderTest.java | 194 ++++++++++++++ .../PerMessageDeflateEncoderTest.java | 160 +++++++++++ ...eDeflateServerExtensionHandshakerTest.java | 172 ++++++++++++ ...WebSocketServerCompressionHandlerTest.java | 196 ++++++++++++++ .../codec/compression/ZlibCodecFactory.java | 4 +- .../websocketx/client/WebSocketClient.java | 2 + .../server/WebSocketServerInitializer.java | 2 + 45 files changed, 3913 insertions(+), 42 deletions(-) create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8FrameValidator.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtension.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandler.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandshaker.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtension.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionData.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionEncoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionUtil.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtension.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandler.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandshaker.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshaker.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshaker.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshaker.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshaker.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/WebSocketClientCompressionHandler.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/WebSocketServerCompressionHandler.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/package-info.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/package-info.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandlerTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionTestUtil.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandlerTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshakerTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshakerTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoderTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoderTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshakerTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshakerTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/WebSocketServerCompressionHandlerTest.java diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/TextWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/TextWebSocketFrame.java index 7dca7ba87f..a6199a7e07 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/TextWebSocketFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/TextWebSocketFrame.java @@ -20,7 +20,7 @@ import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; /** - * Web Socket text frame with assumed UTF-8 encoding + * Web Socket text frame */ public class TextWebSocketFrame extends WebSocketFrame { @@ -45,7 +45,7 @@ public class TextWebSocketFrame extends WebSocketFrame { * Creates a new text frame with the specified binary data. The final fragment flag is set to true. * * @param binaryData - * the content of the frame. Must be UTF-8 encoded + * the content of the frame. */ public TextWebSocketFrame(ByteBuf binaryData) { super(binaryData); @@ -81,7 +81,7 @@ public class TextWebSocketFrame extends WebSocketFrame { * @param rsv * reserved bits used for protocol extensions * @param binaryData - * the content of the frame. Must be UTF-8 encoded + * the content of the frame. */ public TextWebSocketFrame(boolean finalFragment, int rsv, ByteBuf binaryData) { super(finalFragment, rsv, binaryData); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8FrameValidator.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8FrameValidator.java new file mode 100644 index 0000000000..3d09f6b7d0 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8FrameValidator.java @@ -0,0 +1,103 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.CorruptedFrameException; +import io.netty.handler.codec.TooLongFrameException; +import io.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder.State; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.util.List; + +import static io.netty.buffer.ByteBufUtil.readBytes; + +/** + * + */ +public class Utf8FrameValidator extends ChannelInboundHandlerAdapter { + + private int fragmentedFramesCount; + private Utf8Validator utf8Validator; + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof WebSocketFrame) { + WebSocketFrame frame = (WebSocketFrame) msg; + + // Processing for possible fragmented messages for text and binary + // frames + if (((WebSocketFrame) msg).isFinalFragment()) { + // Final frame of the sequence. Apparently ping frames are + // allowed in the middle of a fragmented message + if (!(frame instanceof PingWebSocketFrame)) { + fragmentedFramesCount = 0; + + // Check text for UTF8 correctness + if ((frame instanceof TextWebSocketFrame) || + (utf8Validator != null && utf8Validator.isChecking())) { + // Check UTF-8 correctness for this payload + checkUTF8String(ctx, frame.content()); + + // This does a second check to make sure UTF-8 + // correctness for entire text message + utf8Validator.finish(); + } + } + } else { + // Not final frame so we can expect more frames in the + // fragmented sequence + if (fragmentedFramesCount == 0) { + // First text or binary frame for a fragmented set + if (frame instanceof TextWebSocketFrame) { + checkUTF8String(ctx, frame.content()); + } + } else { + // Subsequent frames - only check if init frame is text + if (utf8Validator != null && utf8Validator.isChecking()) { + checkUTF8String(ctx, frame.content()); + } + } + + // Increment counter + fragmentedFramesCount++; + } + } + + super.channelRead(ctx, msg); + } + + private void checkUTF8String(ChannelHandlerContext ctx, ByteBuf buffer) { + try { + if (utf8Validator == null) { + utf8Validator = new Utf8Validator(); + } + utf8Validator.check(buffer); + } catch (CorruptedFrameException ex) { + if (ctx.channel().isActive()) { + ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + } + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java index 2ec1d6b201..35d2930fd8 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java @@ -107,7 +107,6 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder private byte[] maskingKey; private int framePayloadLen1; private boolean receivedClosingHandshake; - private Utf8Validator utf8Validator; private State state = State.READING_FIRST; /** @@ -340,33 +339,8 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder // allowed in the middle of a fragmented message if (frameOpcode != OPCODE_PING) { fragmentedFramesCount = 0; - - // Check text for UTF8 correctness - if (frameOpcode == OPCODE_TEXT || - utf8Validator != null && utf8Validator.isChecking()) { - // Check UTF-8 correctness for this payload - checkUTF8String(ctx, payloadBuffer); - - // This does a second check to make sure UTF-8 - // correctness for entire text message - utf8Validator.finish(); - } } } else { - // Not final frame so we can expect more frames in the - // fragmented sequence - if (fragmentedFramesCount == 0) { - // First text or binary frame for a fragmented set - if (frameOpcode == OPCODE_TEXT) { - checkUTF8String(ctx, payloadBuffer); - } - } else { - // Subsequent frames - only check if init frame is text - if (utf8Validator != null && utf8Validator.isChecking()) { - checkUTF8String(ctx, payloadBuffer); - } - } - // Increment counter fragmentedFramesCount++; } @@ -460,17 +434,6 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder } } - private void checkUTF8String(ChannelHandlerContext ctx, ByteBuf buffer) { - try { - if (utf8Validator == null) { - utf8Validator = new Utf8Validator(); - } - utf8Validator.check(buffer); - } catch (CorruptedFrameException ex) { - protocolViolation(ctx, ex); - } - } - /** */ protected void checkCloseFrameBody( ChannelHandlerContext ctx, ByteBuf buffer) { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandler.java index 5e65bea66b..b6ede9931c 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandler.java @@ -183,5 +183,10 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler { ctx.pipeline().addBefore(ctx.name(), WebSocketClientProtocolHandshakeHandler.class.getName(), new WebSocketClientProtocolHandshakeHandler(handshaker)); } + if (cp.get(Utf8FrameValidator.class) == null) { + // Add the UFT8 checking before this one. + ctx.pipeline().addBefore(ctx.name(), Utf8FrameValidator.class.getName(), + new Utf8FrameValidator()); + } } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandler.java index aacac1cabc..1b302800f3 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandler.java @@ -105,6 +105,11 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler { new WebSocketServerProtocolHandshakeHandler(websocketPath, subprotocols, allowExtensions, maxFramePayloadLength, allowMaskMismatch)); } + if (cp.get(Utf8FrameValidator.class) == null) { + // Add the UFT8 checking before this one. + ctx.pipeline().addBefore(ctx.name(), Utf8FrameValidator.class.getName(), + new Utf8FrameValidator()); + } } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtension.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtension.java new file mode 100644 index 0000000000..6917b1fda3 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtension.java @@ -0,0 +1,23 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions; + +/** + * Created once the handshake phase is done. + */ +public interface WebSocketClientExtension extends WebSocketExtension { + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandler.java new file mode 100644 index 0000000000..bec6f10ee3 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandler.java @@ -0,0 +1,131 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.CodecException; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + * This handler negotiates and initializes the WebSocket Extensions. + * + * This implementation negotiates the extension with the server in a defined order, + * ensures that the successfully negotiated extensions are consistent between them, + * and initializes the channel pipeline with the extension decoder and encoder. + * + * Find a basic implementation for compression extensions at + * io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler. + */ +public class WebSocketClientExtensionHandler extends ChannelDuplexHandler { + + private final List extensionHandshakers; + + /** + * Constructor + * + * @param extensionHandshakers + * The extension handshaker in priority order. A handshaker could be repeated many times + * with fallback configuration. + */ + public WebSocketClientExtensionHandler(WebSocketClientExtensionHandshaker... extensionHandshakers) { + if (extensionHandshakers == null) { + throw new NullPointerException("extensionHandshakers"); + } + if (extensionHandshakers.length == 0) { + throw new IllegalArgumentException("extensionHandshakers must contains at least one handshaker"); + } + this.extensionHandshakers = Arrays.asList(extensionHandshakers); + } + + @Override + public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof HttpRequest && WebSocketExtensionUtil.isWebsocketUpgrade((HttpRequest) msg)) { + HttpRequest request = (HttpRequest) msg; + String headerValue = request.headers().getAsString(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS); + + for (WebSocketClientExtensionHandshaker extentionHandshaker : extensionHandshakers) { + WebSocketExtensionData extensionData = extentionHandshaker.newRequestData(); + headerValue = WebSocketExtensionUtil.appendExtension(headerValue, + extensionData.name(), extensionData.parameters()); + } + + request.headers().set(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS, headerValue); + } + + super.write(ctx, msg, promise); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) + throws Exception { + if (msg instanceof HttpResponse) { + HttpResponse response = (HttpResponse) msg; + + if (WebSocketExtensionUtil.isWebsocketUpgrade(response)) { + String extensionsHeader = response.headers().getAsString(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS); + + if (extensionsHeader != null) { + List extensions = + WebSocketExtensionUtil.extractExtensions(extensionsHeader); + List validExtensions = + new ArrayList(extensions.size()); + int rsv = 0; + + for (WebSocketExtensionData extensionData : extensions) { + Iterator extensionHandshakersIterator = + extensionHandshakers.iterator(); + WebSocketClientExtension validExtension = null; + + while (validExtension == null && extensionHandshakersIterator.hasNext()) { + WebSocketClientExtensionHandshaker extensionHandshaker = + extensionHandshakersIterator.next(); + validExtension = extensionHandshaker.handshakeExtension(extensionData); + } + + if (validExtension != null && ((validExtension.rsv() & rsv) == 0)) { + rsv = rsv | validExtension.rsv(); + validExtensions.add(validExtension); + } else { + throw new CodecException( + "invalid WebSocket Extension handhshake for \"" + extensionsHeader + "\""); + } + } + + for (WebSocketClientExtension validExtension : validExtensions) { + WebSocketExtensionDecoder decoder = validExtension.newExtensionDecoder(); + WebSocketExtensionEncoder encoder = validExtension.newExtensionEncoder(); + ctx.pipeline().addAfter(ctx.name(), decoder.getClass().getName(), decoder); + ctx.pipeline().addAfter(ctx.name(), encoder.getClass().getName(), encoder); + } + } + + ctx.pipeline().remove(ctx.name()); + } + } + + super.channelRead(ctx, msg); + } +} + diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandshaker.java new file mode 100644 index 0000000000..70514a326e --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandshaker.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions; + + +/** + * Handshakes a client extension with the server. + */ +public interface WebSocketClientExtensionHandshaker { + + /** + * Return extension configuration to submit to the server. + * + * @return the desired extension configuration. + */ + WebSocketExtensionData newRequestData(); + + /** + * Handshake based on server response. It should always succeed because server response + * should be a request acknowledge. + * + * @param extensionData + * the extension configuration sent by the server. + * @return an initialized extension if handshake phase succeed or null if failed. + */ + WebSocketClientExtension handshakeExtension(WebSocketExtensionData extensionData); + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtension.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtension.java new file mode 100644 index 0000000000..e5605e986e --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtension.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions; + +/** + * Created once the handshake phase is done. + */ +public interface WebSocketExtension { + + int RSV1 = 0x04; + int RSV2 = 0x02; + int RSV3 = 0x01; + + /** + * @return the reserved bit value to ensure that no other extension should interfere. + */ + int rsv(); + + /** + * @return create the extension encoder. + */ + WebSocketExtensionEncoder newExtensionEncoder(); + + /** + * @return create the extension decoder. + */ + WebSocketExtensionDecoder newExtensionDecoder(); + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionData.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionData.java new file mode 100644 index 0000000000..eb3c5864de --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionData.java @@ -0,0 +1,55 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions; + +import java.util.Collections; +import java.util.Map; + +/** + * A WebSocket Extension data from the Sec-WebSocket-Extensions header. + * + * See io.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_EXTENSIONS. + */ +public final class WebSocketExtensionData { + + private final String name; + private final Map parameters; + + public WebSocketExtensionData(String name, Map parameters) { + if (name == null) { + throw new NullPointerException("name"); + } + if (parameters == null) { + throw new NullPointerException("parameters"); + } + this.name = name; + this.parameters = Collections.unmodifiableMap(parameters); + } + + /** + * @return the extension name. + */ + public String name() { + return name; + } + + /** + * @return the extension optional parameters. + */ + public Map parameters() { + return parameters; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionDecoder.java new file mode 100644 index 0000000000..0223cb4679 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionDecoder.java @@ -0,0 +1,26 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions; + +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; + +/** + * Convenient class for io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension decoder. + */ +public abstract class WebSocketExtensionDecoder extends MessageToMessageDecoder { + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionEncoder.java new file mode 100644 index 0000000000..de505bba12 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionEncoder.java @@ -0,0 +1,26 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions; + +import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; + +/** + * Convenient class for io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension encoder. + */ +public abstract class WebSocketExtensionEncoder extends MessageToMessageEncoder { + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionUtil.java new file mode 100644 index 0000000000..ccd8c4a8fe --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionUtil.java @@ -0,0 +1,106 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.util.internal.StringUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class WebSocketExtensionUtil { + + private static final char EXTENSION_SEPARATOR = ','; + private static final char PARAMETER_SEPARATOR = ';'; + private static final char PARAMETER_EQUAL = '='; + + private static final Pattern PARAMETER = Pattern.compile("^([^=]+)(=[\\\"]?([^\\\"]+)[\\\"]?)?$"); + + static boolean isWebsocketUpgrade(HttpMessage httpMessage) { + if (httpMessage == null) { + throw new NullPointerException("httpMessage"); + } + return httpMessage.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE, true) && + httpMessage.headers().contains(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET, true); + } + + public static List extractExtensions(String extensionHeader) { + String[] rawExtensions = StringUtil.split(extensionHeader, EXTENSION_SEPARATOR); + if (rawExtensions.length > 0) { + List extensions = new ArrayList(rawExtensions.length); + for (String rawExtension : rawExtensions) { + String[] extensionParameters = StringUtil.split(rawExtension, PARAMETER_SEPARATOR); + String name = extensionParameters[0].trim(); + Map parameters; + if (extensionParameters.length > 1) { + parameters = new HashMap(extensionParameters.length - 1); + for (int i = 1; i < extensionParameters.length; i++) { + String parameter = extensionParameters[i].trim(); + Matcher parameterMatcher = PARAMETER.matcher(parameter); + if (parameterMatcher.matches() && parameterMatcher.group(1) != null) { + parameters.put(parameterMatcher.group(1), parameterMatcher.group(3)); + } + } + } else { + parameters = Collections.emptyMap(); + } + extensions.add(new WebSocketExtensionData(name, parameters)); + } + return extensions; + } else { + return Collections.emptyList(); + } + } + + static String appendExtension(String currentHeaderValue, String extensionName, + Map extensionParameters) { + + StringBuilder newHeaderValue = new StringBuilder( + currentHeaderValue != null ? currentHeaderValue.length() : 0 + extensionName.length() + 1); + if (currentHeaderValue != null && !currentHeaderValue.trim().isEmpty()) { + newHeaderValue.append(currentHeaderValue); + newHeaderValue.append(EXTENSION_SEPARATOR); + } + newHeaderValue.append(extensionName); + boolean isFirst = true; + for (Entry extensionParameter : extensionParameters.entrySet()) { + if (isFirst) { + newHeaderValue.append(PARAMETER_SEPARATOR); + } else { + isFirst = false; + } + newHeaderValue.append(extensionParameter.getKey()); + if (extensionParameter.getValue() != null) { + newHeaderValue.append(PARAMETER_EQUAL); + newHeaderValue.append(extensionParameter.getValue()); + } + } + return newHeaderValue.toString(); + } + + private WebSocketExtensionUtil() { + // Unused + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtension.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtension.java new file mode 100644 index 0000000000..f3b1e95275 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtension.java @@ -0,0 +1,31 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions; + + +/** + * Created once the handshake phase is done. + */ +public interface WebSocketServerExtension extends WebSocketExtension { + + /** + * Return an extension configuration to submit to the client as an acknowledge. + * + * @return the acknowledged extension configuration. + */ + WebSocketExtensionData newReponseData(); + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandler.java new file mode 100644 index 0000000000..862b3896a4 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandler.java @@ -0,0 +1,141 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + * This handler negotiates and initializes the WebSocket Extensions. + * + * It negotiates the extensions based on the client desired order, + * ensures that the successfully negotiated extensions are consistent between them, + * and initializes the channel pipeline with the extension decoder and encoder. + * + * Find a basic implementation for compression extensions at + * io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler. + */ +public class WebSocketServerExtensionHandler extends ChannelDuplexHandler { + + private final List extensionHandshakers; + + private List validExtensions; + + /** + * Constructor + * + * @param extensionHandshakers + * The extension handshaker in priority order. A handshaker could be repeated many times + * with fallback configuration. + */ + public WebSocketServerExtensionHandler(WebSocketServerExtensionHandshaker... extensionHandshakers) { + if (extensionHandshakers == null) { + throw new NullPointerException("extensionHandshakers"); + } + if (extensionHandshakers.length == 0) { + throw new IllegalArgumentException("extensionHandshakers must contains at least one handshaker"); + } + this.extensionHandshakers = Arrays.asList(extensionHandshakers); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) + throws Exception { + if (msg instanceof HttpRequest) { + HttpRequest request = (HttpRequest) msg; + + if (WebSocketExtensionUtil.isWebsocketUpgrade(request)) { + String extensionsHeader = request.headers().getAsString(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS); + + if (extensionsHeader != null) { + List extensions = + WebSocketExtensionUtil.extractExtensions(extensionsHeader); + int rsv = 0; + + for (WebSocketExtensionData extensionData : extensions) { + Iterator extensionHandshakersIterator = + extensionHandshakers.iterator(); + WebSocketServerExtension validExtension = null; + + while (validExtension == null && extensionHandshakersIterator.hasNext()) { + WebSocketServerExtensionHandshaker extensionHandshaker = + extensionHandshakersIterator.next(); + validExtension = extensionHandshaker.handshakeExtension(extensionData); + } + + if (validExtension != null && ((validExtension.rsv() & rsv) == 0)) { + if (validExtensions == null) { + validExtensions = new ArrayList(1); + } + rsv = rsv | validExtension.rsv(); + validExtensions.add(validExtension); + } + } + } + } + } + + super.channelRead(ctx, msg); + } + + @Override + public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof HttpResponse && + WebSocketExtensionUtil.isWebsocketUpgrade((HttpResponse) msg) && validExtensions != null) { + HttpResponse response = (HttpResponse) msg; + String headerValue = response.headers().getAsString(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS); + + for (WebSocketServerExtension extension : validExtensions) { + WebSocketExtensionData extensionData = extension.newReponseData(); + headerValue = WebSocketExtensionUtil.appendExtension(headerValue, + extensionData.name(), extensionData.parameters()); + } + + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (future.isSuccess()) { + for (WebSocketServerExtension extension : validExtensions) { + WebSocketExtensionDecoder decoder = extension.newExtensionDecoder(); + WebSocketExtensionEncoder encoder = extension.newExtensionEncoder(); + ctx.pipeline().addAfter(ctx.name(), decoder.getClass().getName(), decoder); + ctx.pipeline().addAfter(ctx.name(), encoder.getClass().getName(), encoder); + } + } + + ctx.pipeline().remove(ctx.name()); + } + }); + + if (headerValue != null) { + response.headers().set(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS, headerValue); + } + } + + super.write(ctx, msg, promise); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandshaker.java new file mode 100644 index 0000000000..de02e546d1 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandshaker.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions; + + +/** + * Handshakes a client extension based on this server capabilities. + */ +public interface WebSocketServerExtensionHandshaker { + + /** + * Handshake based on client request. It must failed with null if server cannot handle it. + * + * @param extensionData + * the extension configuration sent by the client. + * @return an initialized extension if handshake phase succeed or null if failed. + */ + WebSocketServerExtension handshakeExtension(WebSocketExtensionData extensionData); + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java new file mode 100644 index 0000000000..8612bf3a72 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java @@ -0,0 +1,144 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.CodecException; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.compression.ZlibWrapper; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder; +import io.netty.util.ReferenceCountUtil; + +import java.util.List; + +/** + * Deflate implementation of a payload decompressor for + * io.netty.handler.codec.http.websocketx.WebSocketFrame. + */ +abstract class DeflateDecoder extends WebSocketExtensionDecoder { + + static final byte[] FRAME_TAIL = new byte[] {0x00, 0x00, (byte) 0xff, (byte) 0xff}; + + private final boolean noContext; + + private EmbeddedChannel decoder; + + /** + * Constructor + * @param noContext true to disable context takeover. + */ + public DeflateDecoder(boolean noContext) { + this.noContext = noContext; + } + + protected abstract boolean appendFrameTail(WebSocketFrame msg); + + protected abstract int newRsv(WebSocketFrame msg); + + @Override + protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, List out) throws Exception { + if (decoder == null) { + if (!(msg instanceof TextWebSocketFrame) && !(msg instanceof BinaryWebSocketFrame)) { + throw new CodecException("unexpected initial frame type: " + msg.getClass().getName()); + } + decoder = new EmbeddedChannel(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.NONE)); + } + + boolean readable = msg.content().isReadable(); + decoder.writeInbound(msg.content().retain()); + if (appendFrameTail(msg)) { + decoder.writeInbound(Unpooled.wrappedBuffer(FRAME_TAIL)); + } + + CompositeByteBuf compositeUncompressedContent = ctx.alloc().compositeBuffer(); + for (;;) { + ByteBuf partUncompressedContent = decoder.readInbound(); + if (partUncompressedContent == null) { + break; + } + if (!partUncompressedContent.isReadable()) { + partUncompressedContent.release(); + continue; + } + compositeUncompressedContent.addComponent(partUncompressedContent); + compositeUncompressedContent.writerIndex(compositeUncompressedContent.writerIndex() + + partUncompressedContent.readableBytes()); + } + // Correctly handle empty frames + // See https://github.com/netty/netty/issues/4348 + if (readable && compositeUncompressedContent.numComponents() <= 0) { + compositeUncompressedContent.release(); + throw new CodecException("cannot read uncompressed buffer"); + } + + if (msg.isFinalFragment() && noContext) { + cleanup(); + } + + WebSocketFrame outMsg; + if (msg instanceof TextWebSocketFrame) { + outMsg = new TextWebSocketFrame(msg.isFinalFragment(), newRsv(msg), compositeUncompressedContent); + } else if (msg instanceof BinaryWebSocketFrame) { + outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), newRsv(msg), compositeUncompressedContent); + } else if (msg instanceof ContinuationWebSocketFrame) { + outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), newRsv(msg), + compositeUncompressedContent); + } else { + throw new CodecException("unexpected frame type: " + msg.getClass().getName()); + } + out.add(outMsg); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + cleanup(); + super.handlerRemoved(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + cleanup(); + super.channelInactive(ctx); + } + + private void cleanup() { + if (decoder != null) { + // Clean-up the previous encoder if not cleaned up correctly. + if (decoder.finish()) { + for (;;) { + ByteBuf buf = decoder.readOutbound(); + if (buf == null) { + break; + } + // Release the buffer + buf.release(); + } + } + decoder = null; + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java new file mode 100644 index 0000000000..5111e23280 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java @@ -0,0 +1,146 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateDecoder.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.CodecException; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.compression.ZlibWrapper; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder; + +import java.util.List; + +/** + * Deflate implementation of a payload compressor for + * io.netty.handler.codec.http.websocketx.WebSocketFrame. + */ +abstract class DeflateEncoder extends WebSocketExtensionEncoder { + + private final int compressionLevel; + private final int windowSize; + private final boolean noContext; + + private EmbeddedChannel encoder; + + /** + * Constructor + * @param compressionLevel compression level of the compressor. + * @param windowSize maximum size of the window compressor buffer. + * @param noContext true to disable context takeover. + */ + public DeflateEncoder(int compressionLevel, int windowSize, boolean noContext) { + this.compressionLevel = compressionLevel; + this.windowSize = windowSize; + this.noContext = noContext; + } + + /** + * @param msg the current frame. + * @return the rsv bits to set in the compressed frame. + */ + protected abstract int rsv(WebSocketFrame msg); + + /** + * @param msg the current frame. + * @return true if compressed payload tail needs to be removed. + */ + protected abstract boolean removeFrameTail(WebSocketFrame msg); + + @Override + protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg, + List out) throws Exception { + if (encoder == null) { + encoder = new EmbeddedChannel(ZlibCodecFactory.newZlibEncoder( + ZlibWrapper.NONE, compressionLevel, windowSize, 8)); + } + + encoder.writeOutbound(msg.content().retain()); + + CompositeByteBuf fullCompressedContent = ctx.alloc().compositeBuffer(); + for (;;) { + ByteBuf partCompressedContent = encoder.readOutbound(); + if (partCompressedContent == null) { + break; + } + if (!partCompressedContent.isReadable()) { + partCompressedContent.release(); + continue; + } + fullCompressedContent.addComponent(partCompressedContent); + fullCompressedContent.writerIndex(fullCompressedContent.writerIndex() + + partCompressedContent.readableBytes()); + } + if (fullCompressedContent.numComponents() <= 0) { + fullCompressedContent.release(); + throw new CodecException("cannot read compressed buffer"); + } + + if (msg.isFinalFragment() && noContext) { + cleanup(); + } + + ByteBuf compressedContent; + if (removeFrameTail(msg)) { + int realLength = fullCompressedContent.readableBytes() - FRAME_TAIL.length; + compressedContent = fullCompressedContent.slice(0, realLength); + } else { + compressedContent = fullCompressedContent; + } + + WebSocketFrame outMsg; + if (msg instanceof TextWebSocketFrame) { + outMsg = new TextWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent); + } else if (msg instanceof BinaryWebSocketFrame) { + outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent); + } else if (msg instanceof ContinuationWebSocketFrame) { + outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent); + } else { + throw new CodecException("unexpected frame type: " + msg.getClass().getName()); + } + out.add(outMsg); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + cleanup(); + super.handlerRemoved(ctx); + } + + private void cleanup() { + if (encoder != null) { + // Clean-up the previous encoder if not cleaned up correctly. + if (encoder.finish()) { + for (;;) { + ByteBuf buf = encoder.readOutbound(); + if (buf == null) { + break; + } + // Release the buffer + buf.release(); + } + } + encoder = null; + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshaker.java new file mode 100644 index 0000000000..6671d1f1af --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshaker.java @@ -0,0 +1,104 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import static io.netty.handler.codec.http.websocketx.extensions.compression. + DeflateFrameServerExtensionHandshaker.*; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandshaker; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder; + +import java.util.Collections; + +/** + * perframe-deflate + * handshake implementation. + */ +public final class DeflateFrameClientExtensionHandshaker implements WebSocketClientExtensionHandshaker { + + private final int compressionLevel; + private final boolean useWebkitExtensionName; + + /** + * Constructor with default configuration. + */ + public DeflateFrameClientExtensionHandshaker(boolean useWebkitExtensionName) { + this(6, useWebkitExtensionName); + } + + /** + * Constructor with custom configuration. + * + * @param compressionLevel + * Compression level between 0 and 9 (default is 6). + */ + public DeflateFrameClientExtensionHandshaker(int compressionLevel, boolean useWebkitExtensionName) { + if (compressionLevel < 0 || compressionLevel > 9) { + throw new IllegalArgumentException( + "compressionLevel: " + compressionLevel + " (expected: 0-9)"); + } + this.compressionLevel = compressionLevel; + this.useWebkitExtensionName = useWebkitExtensionName; + } + + @Override + public WebSocketExtensionData newRequestData() { + return new WebSocketExtensionData( + useWebkitExtensionName ? X_WEBKIT_DEFLATE_FRAME_EXTENSION : DEFLATE_FRAME_EXTENSION, + Collections.emptyMap()); + } + + @Override + public WebSocketClientExtension handshakeExtension(WebSocketExtensionData extensionData) { + if (!X_WEBKIT_DEFLATE_FRAME_EXTENSION.equals(extensionData.name()) && + !DEFLATE_FRAME_EXTENSION.equals(extensionData.name())) { + return null; + } + + if (extensionData.parameters().isEmpty()) { + return new DeflateFrameClientExtension(compressionLevel); + } else { + return null; + } + } + + private static class DeflateFrameClientExtension implements WebSocketClientExtension { + + private final int compressionLevel; + + public DeflateFrameClientExtension(int compressionLevel) { + this.compressionLevel = compressionLevel; + } + + @Override + public int rsv() { + return RSV1; + } + + @Override + public WebSocketExtensionEncoder newExtensionEncoder() { + return new PerFrameDeflateEncoder(compressionLevel, 15, false); + } + + @Override + public WebSocketExtensionDecoder newExtensionDecoder() { + return new PerFrameDeflateDecoder(false); + } + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshaker.java new file mode 100644 index 0000000000..e7ea9f3571 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshaker.java @@ -0,0 +1,103 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandshaker; + +import java.util.Collections; + +/** + * perframe-deflate + * handshake implementation. + */ +public final class DeflateFrameServerExtensionHandshaker implements WebSocketServerExtensionHandshaker { + + static final String X_WEBKIT_DEFLATE_FRAME_EXTENSION = "x-webkit-deflate-frame"; + static final String DEFLATE_FRAME_EXTENSION = "deflate-frame"; + + private final int compressionLevel; + + /** + * Constructor with default configuration. + */ + public DeflateFrameServerExtensionHandshaker() { + this(6); + } + + /** + * Constructor with custom configuration. + * + * @param compressionLevel + * Compression level between 0 and 9 (default is 6). + */ + public DeflateFrameServerExtensionHandshaker(int compressionLevel) { + if (compressionLevel < 0 || compressionLevel > 9) { + throw new IllegalArgumentException( + "compressionLevel: " + compressionLevel + " (expected: 0-9)"); + } + this.compressionLevel = compressionLevel; + } + + @Override + public WebSocketServerExtension handshakeExtension(WebSocketExtensionData extensionData) { + if (!X_WEBKIT_DEFLATE_FRAME_EXTENSION.equals(extensionData.name()) && + !DEFLATE_FRAME_EXTENSION.equals(extensionData.name())) { + return null; + } + + if (extensionData.parameters().isEmpty()) { + return new DeflateFrameServerExtension(compressionLevel, extensionData.name()); + } else { + return null; + } + } + + private static class DeflateFrameServerExtension implements WebSocketServerExtension { + + private final String extensionName; + private final int compressionLevel; + + public DeflateFrameServerExtension(int compressionLevel, String extensionName) { + this.extensionName = extensionName; + this.compressionLevel = compressionLevel; + } + + @Override + public int rsv() { + return RSV1; + } + + @Override + public WebSocketExtensionEncoder newExtensionEncoder() { + return new PerFrameDeflateEncoder(compressionLevel, 15, false); + } + + @Override + public WebSocketExtensionDecoder newExtensionDecoder() { + return new PerFrameDeflateDecoder(false); + } + + @Override + public WebSocketExtensionData newReponseData() { + return new WebSocketExtensionData(extensionName, Collections.emptyMap()); + } + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoder.java new file mode 100644 index 0000000000..ad95544250 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoder.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; + +/** + * Per-frame implementation of deflate decompressor. + */ +class PerFrameDeflateDecoder extends DeflateDecoder { + + /** + * Constructor + * @param noContext true to disable context takeover. + */ + public PerFrameDeflateDecoder(boolean noContext) { + super(noContext); + } + + @Override + public boolean acceptInboundMessage(Object msg) throws Exception { + return (msg instanceof TextWebSocketFrame || + msg instanceof BinaryWebSocketFrame || + msg instanceof ContinuationWebSocketFrame) && + (((WebSocketFrame) msg).rsv() & WebSocketExtension.RSV1) > 0; + } + + @Override + protected int newRsv(WebSocketFrame msg) { + return msg.rsv() ^ WebSocketExtension.RSV1; + } + + @Override + protected boolean appendFrameTail(WebSocketFrame msg) { + return true; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoder.java new file mode 100644 index 0000000000..aaffd8dc03 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoder.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; + +/** + * Per-frame implementation of deflate compressor. + */ +class PerFrameDeflateEncoder extends DeflateEncoder { + + /** + * Constructor + * @param compressionLevel compression level of the compressor. + * @param windowSize maximum size of the window compressor buffer. + * @param noContext true to disable context takeover. + */ + public PerFrameDeflateEncoder(int compressionLevel, int windowSize, boolean noContext) { + super(compressionLevel, windowSize, noContext); + } + + @Override + public boolean acceptOutboundMessage(Object msg) throws Exception { + return (msg instanceof TextWebSocketFrame || + msg instanceof BinaryWebSocketFrame || + msg instanceof ContinuationWebSocketFrame) && + ((WebSocketFrame) msg).content().readableBytes() > 0 && + (((WebSocketFrame) msg).rsv() & WebSocketExtension.RSV1) == 0; + } + + @Override + protected int rsv(WebSocketFrame msg) { + return msg.rsv() | WebSocketExtension.RSV1; + } + + @Override + protected boolean removeFrameTail(WebSocketFrame msg) { + return true; + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshaker.java new file mode 100644 index 0000000000..0d19984402 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshaker.java @@ -0,0 +1,197 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import static io.netty.handler.codec.http.websocketx.extensions.compression. + PerMessageDeflateServerExtensionHandshaker.*; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandshaker; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +/** + * permessage-deflate + * handshake implementation. + */ +public final class PerMessageDeflateClientExtensionHandshaker implements WebSocketClientExtensionHandshaker { + + private final int compressionLevel; + private final boolean allowClientWindowSize; + private final int requestedServerWindowSize; + private final boolean allowClientNoContext; + private final boolean requestedServerNoContext; + + /** + * Constructor with default configuration. + */ + public PerMessageDeflateClientExtensionHandshaker() { + this(6, false, MAX_WINDOW_SIZE, false, false); + } + + /** + * Constructor with custom configuration. + * + * @param compressionLevel + * Compression level between 0 and 9 (default is 6). + * @param allowClientWindowSize + * allows WebSocket server to customize the client inflater window size + * (default is false). + * @param requestedServerWindowSize + * indicates the requested sever window size to use if server inflater is customizable. + * @param allowClientNoContext + * allows WebSocket server to activate client_no_context_takeover + * (default is false). + * @param requestedServerNoContext + * indicates if client needs to activate server_no_context_takeover + * if server is compatible with (default is false). + */ + public PerMessageDeflateClientExtensionHandshaker(int compressionLevel, + boolean allowClientWindowSize, int requestedServerWindowSize, + boolean allowClientNoContext, boolean requestedServerNoContext) { + if (requestedServerWindowSize > MAX_WINDOW_SIZE || requestedServerWindowSize < MIN_WINDOW_SIZE) { + throw new IllegalArgumentException( + "requestedServerWindowSize: " + requestedServerWindowSize + " (expected: 8-15)"); + } + if (compressionLevel < 0 || compressionLevel > 9) { + throw new IllegalArgumentException( + "compressionLevel: " + compressionLevel + " (expected: 0-9)"); + } + this.compressionLevel = compressionLevel; + this.allowClientWindowSize = allowClientWindowSize; + this.requestedServerWindowSize = requestedServerWindowSize; + this.allowClientNoContext = allowClientNoContext; + this.requestedServerNoContext = requestedServerNoContext; + } + + @Override + public WebSocketExtensionData newRequestData() { + HashMap parameters = new HashMap(4); + if (requestedServerWindowSize != MAX_WINDOW_SIZE) { + parameters.put(SERVER_NO_CONTEXT, null); + } + if (allowClientNoContext) { + parameters.put(CLIENT_NO_CONTEXT, null); + } + if (requestedServerWindowSize != MAX_WINDOW_SIZE) { + parameters.put(SERVER_MAX_WINDOW, Integer.toString(requestedServerWindowSize)); + } + if (allowClientWindowSize) { + parameters.put(CLIENT_MAX_WINDOW, null); + } + return new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, parameters); + } + + @Override + public WebSocketClientExtension handshakeExtension(WebSocketExtensionData extensionData) { + if (!PERMESSAGE_DEFLATE_EXTENSION.equals(extensionData.name())) { + return null; + } + + boolean succeed = true; + int clientWindowSize = MAX_WINDOW_SIZE; + int serverWindowSize = MAX_WINDOW_SIZE; + boolean serverNoContext = false; + boolean clientNoContext = false; + + Iterator> parametersIterator = + extensionData.parameters().entrySet().iterator(); + while (succeed && parametersIterator.hasNext()) { + Entry parameter = parametersIterator.next(); + + if (CLIENT_MAX_WINDOW.equalsIgnoreCase(parameter.getKey())) { + // allowed client_window_size_bits + if (allowClientWindowSize) { + clientWindowSize = Integer.parseInt(parameter.getValue()); + } else { + succeed = false; + } + } else if (SERVER_MAX_WINDOW.equalsIgnoreCase(parameter.getKey())) { + // acknowledged server_window_size_bits + serverWindowSize = Integer.parseInt(parameter.getValue()); + if (clientWindowSize > MAX_WINDOW_SIZE || clientWindowSize < MIN_WINDOW_SIZE) { + succeed = false; + } + } else if (CLIENT_NO_CONTEXT.equalsIgnoreCase(parameter.getKey())) { + // allowed client_no_context_takeover + if (allowClientNoContext) { + clientNoContext = true; + } else { + succeed = false; + } + } else if (SERVER_NO_CONTEXT.equalsIgnoreCase(parameter.getKey())) { + // acknowledged server_no_context_takeover + if (requestedServerNoContext) { + serverNoContext = true; + } else { + succeed = false; + } + } else { + // unknown parameter + succeed = false; + } + } + + if ((requestedServerNoContext && !serverNoContext) || + requestedServerWindowSize != serverWindowSize) { + succeed = false; + } + + if (succeed) { + return new PermessageDeflateExtension(serverNoContext, serverWindowSize, + clientNoContext, clientWindowSize); + } else { + return null; + } + } + + private final class PermessageDeflateExtension implements WebSocketClientExtension { + + private final boolean serverNoContext; + private final int serverWindowSize; + private final boolean clientNoContext; + private final int clientWindowSize; + + @Override + public int rsv() { + return RSV1; + } + + public PermessageDeflateExtension(boolean serverNoContext, int serverWindowSize, + boolean clientNoContext, int clientWindowSize) { + this.serverNoContext = serverNoContext; + this.serverWindowSize = serverWindowSize; + this.clientNoContext = clientNoContext; + this.clientWindowSize = clientWindowSize; + } + + @Override + public WebSocketExtensionEncoder newExtensionEncoder() { + return new PerMessageDeflateEncoder(compressionLevel, serverWindowSize, serverNoContext); + } + + @Override + public WebSocketExtensionDecoder newExtensionDecoder() { + return new PerMessageDeflateDecoder(clientNoContext); + } + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoder.java new file mode 100644 index 0000000000..29c9424ca8 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoder.java @@ -0,0 +1,72 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; + +import java.util.List; + +/** + * Per-message implementation of deflate decompressor. + */ +class PerMessageDeflateDecoder extends DeflateDecoder { + + private boolean compressing; + + /** + * Constructor + * @param noContext true to disable context takeover. + */ + public PerMessageDeflateDecoder(boolean noContext) { + super(noContext); + } + + @Override + public boolean acceptInboundMessage(Object msg) throws Exception { + return ((msg instanceof TextWebSocketFrame || + msg instanceof BinaryWebSocketFrame) && + (((WebSocketFrame) msg).rsv() & WebSocketExtension.RSV1) > 0) || + (msg instanceof ContinuationWebSocketFrame && compressing); + } + + @Override + protected int newRsv(WebSocketFrame msg) { + return (((WebSocketFrame) msg).rsv() & WebSocketExtension.RSV1) > 0 ? + msg.rsv() ^ WebSocketExtension.RSV1 : msg.rsv(); + } + + @Override + protected boolean appendFrameTail(WebSocketFrame msg) { + return msg.isFinalFragment(); + } + + @Override + protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, + List out) throws Exception { + super.decode(ctx, msg, out); + + if (msg.isFinalFragment()) { + compressing = false; + } else if (msg instanceof TextWebSocketFrame || msg instanceof BinaryWebSocketFrame) { + compressing = true; + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoder.java new file mode 100644 index 0000000000..b1cdc660fe --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoder.java @@ -0,0 +1,75 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; + +import java.util.List; + +/** + * Per-message implementation of deflate compressor. + */ +class PerMessageDeflateEncoder extends DeflateEncoder { + + private boolean compressing; + + /** + * Constructor + * @param compressionLevel compression level of the compressor. + * @param windowSize maximum size of the window compressor buffer. + * @param noContext true to disable context takeover. + */ + public PerMessageDeflateEncoder(int compressionLevel, int windowSize, boolean noContext) { + super(compressionLevel, windowSize, noContext); + } + + @Override + public boolean acceptOutboundMessage(Object msg) throws Exception { + return ((msg instanceof TextWebSocketFrame || + msg instanceof BinaryWebSocketFrame) && + (((WebSocketFrame) msg).rsv() & WebSocketExtension.RSV1) == 0) || + (msg instanceof ContinuationWebSocketFrame && compressing); + } + + @Override + protected int rsv(WebSocketFrame msg) { + return msg instanceof TextWebSocketFrame || msg instanceof BinaryWebSocketFrame ? + msg.rsv() | WebSocketExtension.RSV1 : msg.rsv(); + } + + @Override + protected boolean removeFrameTail(WebSocketFrame msg) { + return msg.isFinalFragment(); + } + + @Override + protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg, + List out) throws Exception { + super.encode(ctx, msg, out); + + if (msg.isFinalFragment()) { + compressing = false; + } else if (msg instanceof TextWebSocketFrame || msg instanceof BinaryWebSocketFrame) { + compressing = true; + } + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshaker.java new file mode 100644 index 0000000000..685e6d6d1c --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshaker.java @@ -0,0 +1,196 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandshaker; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map.Entry; + +/** + * permessage-deflate + * handshake implementation. + */ +public final class PerMessageDeflateServerExtensionHandshaker implements WebSocketServerExtensionHandshaker { + + public static final int MIN_WINDOW_SIZE = 8; + public static final int MAX_WINDOW_SIZE = 15; + + static final String PERMESSAGE_DEFLATE_EXTENSION = "permessage-deflate"; + static final String CLIENT_MAX_WINDOW = "client_max_window_bits"; + static final String SERVER_MAX_WINDOW = "server_max_window_bits"; + static final String CLIENT_NO_CONTEXT = "client_no_context_takeover"; + static final String SERVER_NO_CONTEXT = "server_no_context_takeover"; + + private final int compressionLevel; + private final boolean allowServerWindowSize; + private final int preferredClientWindowSize; + private final boolean allowServerNoContext; + private final boolean preferredClientNoContext; + + /** + * Constructor with default configuration. + */ + public PerMessageDeflateServerExtensionHandshaker() { + this(6, false, MAX_WINDOW_SIZE, false, false); + } + + /** + * Constructor with custom configuration. + * + * @param compressionLevel + * Compression level between 0 and 9 (default is 6). + * @param allowServerWindowSize + * allows WebSocket client to customize the server inflater window size + * (default is false). + * @param preferredClientWindowSize + * indicates the preferred client window size to use if client inflater is customizable. + * @param allowServerNoContext + * allows WebSocket client to activate server_no_context_takeover + * (default is false). + * @param preferredClientNoContext + * indicates if server prefers to activate client_no_context_takeover + * if client is compatible with (default is false). + */ + public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, + boolean allowServerWindowSize, int preferredClientWindowSize, + boolean allowServerNoContext, boolean preferredClientNoContext) { + if (preferredClientWindowSize > MAX_WINDOW_SIZE || preferredClientWindowSize < MIN_WINDOW_SIZE) { + throw new IllegalArgumentException( + "preferredServerWindowSize: " + preferredClientWindowSize + " (expected: 8-15)"); + } + if (compressionLevel < 0 || compressionLevel > 9) { + throw new IllegalArgumentException( + "compressionLevel: " + compressionLevel + " (expected: 0-9)"); + } + this.compressionLevel = compressionLevel; + this.allowServerWindowSize = allowServerWindowSize; + this.preferredClientWindowSize = preferredClientWindowSize; + this.allowServerNoContext = allowServerNoContext; + this.preferredClientNoContext = preferredClientNoContext; + } + + @Override + public WebSocketServerExtension handshakeExtension(WebSocketExtensionData extensionData) { + if (!PERMESSAGE_DEFLATE_EXTENSION.equals(extensionData.name())) { + return null; + } + + boolean deflateEnabled = true; + int clientWindowSize = MAX_WINDOW_SIZE; + int serverWindowSize = MAX_WINDOW_SIZE; + boolean serverNoContext = false; + boolean clientNoContext = false; + + Iterator> parametersIterator = + extensionData.parameters().entrySet().iterator(); + while (deflateEnabled && parametersIterator.hasNext()) { + Entry parameter = parametersIterator.next(); + + if (CLIENT_MAX_WINDOW.equalsIgnoreCase(parameter.getKey())) { + // use preferred clientWindowSize because client is compatible with customization + clientWindowSize = preferredClientWindowSize; + } else if (SERVER_MAX_WINDOW.equalsIgnoreCase(parameter.getKey())) { + // use provided windowSize if it is allowed + if (allowServerWindowSize) { + serverWindowSize = Integer.parseInt(parameter.getValue()); + if (serverWindowSize > MAX_WINDOW_SIZE || serverWindowSize < MIN_WINDOW_SIZE) { + deflateEnabled = false; + } + } else { + deflateEnabled = false; + } + } else if (CLIENT_NO_CONTEXT.equalsIgnoreCase(parameter.getKey())) { + // use preferred clientNoContext because client is compatible with customization + clientNoContext = preferredClientNoContext; + } else if (SERVER_NO_CONTEXT.equalsIgnoreCase(parameter.getKey())) { + // use server no context if allowed + if (allowServerNoContext) { + serverNoContext = true; + } else { + deflateEnabled = false; + } + } else { + // unknown parameter + deflateEnabled = false; + } + } + + if (deflateEnabled) { + return new PermessageDeflateExtension(compressionLevel, serverNoContext, + serverWindowSize, clientNoContext, clientWindowSize); + } else { + return null; + } + } + + private static class PermessageDeflateExtension implements WebSocketServerExtension { + + private final int compressionLevel; + private final boolean serverNoContext; + private final int serverWindowSize; + private final boolean clientNoContext; + private final int clientWindowSize; + + public PermessageDeflateExtension(int compressionLevel, boolean serverNoContext, + int serverWindowSize, boolean clientNoContext, int clientWindowSize) { + this.compressionLevel = compressionLevel; + this.serverNoContext = serverNoContext; + this.serverWindowSize = serverWindowSize; + this.clientNoContext = clientNoContext; + this.clientWindowSize = clientWindowSize; + } + + @Override + public int rsv() { + return RSV1; + } + + @Override + public WebSocketExtensionEncoder newExtensionEncoder() { + return new PerMessageDeflateEncoder(compressionLevel, clientWindowSize, clientNoContext); + } + + @Override + public WebSocketExtensionDecoder newExtensionDecoder() { + return new PerMessageDeflateDecoder(serverNoContext); + } + + @Override + public WebSocketExtensionData newReponseData() { + HashMap parameters = new HashMap(4); + if (serverNoContext) { + parameters.put(SERVER_NO_CONTEXT, null); + } + if (clientNoContext) { + parameters.put(CLIENT_NO_CONTEXT, null); + } + if (serverWindowSize != MAX_WINDOW_SIZE) { + parameters.put(SERVER_MAX_WINDOW, Integer.toString(serverWindowSize)); + } + if (clientWindowSize != MAX_WINDOW_SIZE) { + parameters.put(CLIENT_MAX_WINDOW, Integer.toString(clientWindowSize)); + } + return new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, parameters); + } + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/WebSocketClientCompressionHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/WebSocketClientCompressionHandler.java new file mode 100644 index 0000000000..88182074ba --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/WebSocketClientCompressionHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandler; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandshaker; + +import java.util.Arrays; + +/** + * Extends io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientExtensionHandler + * to handle the most common WebSocket Compression Extensions. + * + * See io.netty.example.http.websocketx.client.WebSocketClient for usage. + */ +public class WebSocketClientCompressionHandler extends WebSocketClientExtensionHandler { + + /** + * Constructor with default configuration. + */ + public WebSocketClientCompressionHandler() { + super(new PerMessageDeflateClientExtensionHandshaker(), + new DeflateFrameClientExtensionHandshaker(false), + new DeflateFrameClientExtensionHandshaker(true)); + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/WebSocketServerCompressionHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/WebSocketServerCompressionHandler.java new file mode 100644 index 0000000000..2db3fda2f9 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/WebSocketServerCompressionHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandler; + +/** + * Extends io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerExtensionHandler + * to handle the most common WebSocket Compression Extensions. + * + * See io.netty.example.http.websocketx.html5.WebSocketServer for usage. + */ +public class WebSocketServerCompressionHandler extends WebSocketServerExtensionHandler { + + /** + * Constructor with default configuration. + */ + public WebSocketServerCompressionHandler() { + super(new PerMessageDeflateServerExtensionHandshaker(), + new DeflateFrameServerExtensionHandshaker()); + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/package-info.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/package-info.java new file mode 100644 index 0000000000..7c7b6dff26 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/package-info.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014 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. + */ + +/** + * Encoder, decoder, handshakers to handle most common WebSocket Compression Extensions. + *

+ * This package supports different web socket extensions. + * The specification currently supported are: + *

+ *

+ *

+ * See io.netty.example.http.websocketx.client.WebSocketClient and + * io.netty.example.http.websocketx.html5.WebSocketServer for usage. + *

+ */ +package io.netty.handler.codec.http.websocketx.extensions.compression; diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/package-info.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/package-info.java new file mode 100644 index 0000000000..83f16b0edb --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2014 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. + */ + +/** + * Encoder, decoder, handshakers to handle + * WebSocket Extensions. + * + * See WebSocketServerExtensionHandler for more details. + */ +package io.netty.handler.codec.http.websocketx.extensions; diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandlerTest.java new file mode 100644 index 0000000000..acaa8e7be7 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandlerTest.java @@ -0,0 +1,253 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions; + +import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionTestUtil.*; +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.CodecException; +import io.netty.handler.codec.http.HttpHeaders.Names; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; + +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +public class WebSocketClientExtensionHandlerTest { + + WebSocketClientExtensionHandshaker mainHandshakerMock = + createMock("mainHandshaker", WebSocketClientExtensionHandshaker.class); + WebSocketClientExtensionHandshaker fallbackHandshakerMock = + createMock("fallbackHandshaker", WebSocketClientExtensionHandshaker.class); + WebSocketClientExtension mainExtensionMock = + createMock("mainExtension", WebSocketClientExtension.class); + WebSocketClientExtension fallbackExtensionMock = + createMock("fallbackExtension", WebSocketClientExtension.class); + + @Test + public void testMainSuccess() { + // initialize + expect(mainHandshakerMock.newRequestData()). + andReturn(new WebSocketExtensionData("main", Collections.emptyMap())).once(); + expect(mainHandshakerMock.handshakeExtension( + anyObject(WebSocketExtensionData.class))).andReturn(mainExtensionMock).once(); + replay(mainHandshakerMock); + + expect(fallbackHandshakerMock.newRequestData()). + andReturn(new WebSocketExtensionData("fallback", Collections.emptyMap())).once(); + replay(fallbackHandshakerMock); + + expect(mainExtensionMock.rsv()).andReturn(WebSocketExtension.RSV1).anyTimes(); + expect(mainExtensionMock.newExtensionEncoder()).andReturn(new DummyEncoder()).once(); + expect(mainExtensionMock.newExtensionDecoder()).andReturn(new DummyDecoder()).once(); + replay(mainExtensionMock); + + // execute + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketClientExtensionHandler( + mainHandshakerMock, fallbackHandshakerMock)); + + HttpRequest req = newUpgradeRequest(null); + ch.writeOutbound(req); + + HttpRequest req2 = ch.readOutbound(); + List reqExts = WebSocketExtensionUtil.extractExtensions( + req2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + HttpResponse res = newUpgradeResponse("main"); + ch.writeInbound(res); + + HttpResponse res2 = ch.readInbound(); + List resExts = WebSocketExtensionUtil.extractExtensions( + res2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + // test + assertEquals(2, reqExts.size()); + assertEquals("main", reqExts.get(0).name()); + assertEquals("fallback", reqExts.get(1).name()); + + assertEquals(1, resExts.size()); + assertEquals("main", resExts.get(0).name()); + assertTrue(resExts.get(0).parameters().isEmpty()); + assertTrue(ch.pipeline().get(DummyDecoder.class) != null); + assertTrue(ch.pipeline().get(DummyEncoder.class) != null); + } + + @Test + public void testFallbackSuccess() { + // initialize + expect(mainHandshakerMock.newRequestData()). + andReturn(new WebSocketExtensionData("main", Collections.emptyMap())).once(); + expect(mainHandshakerMock.handshakeExtension( + anyObject(WebSocketExtensionData.class))).andReturn(null).once(); + replay(mainHandshakerMock); + + expect(fallbackHandshakerMock.newRequestData()). + andReturn(new WebSocketExtensionData("fallback", Collections.emptyMap())).once(); + expect(fallbackHandshakerMock.handshakeExtension( + anyObject(WebSocketExtensionData.class))).andReturn(fallbackExtensionMock).once(); + replay(fallbackHandshakerMock); + + expect(fallbackExtensionMock.rsv()).andReturn(WebSocketExtension.RSV1).anyTimes(); + expect(fallbackExtensionMock.newExtensionEncoder()).andReturn(new DummyEncoder()).once(); + expect(fallbackExtensionMock.newExtensionDecoder()).andReturn(new DummyDecoder()).once(); + replay(fallbackExtensionMock); + + // execute + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketClientExtensionHandler( + mainHandshakerMock, fallbackHandshakerMock)); + + HttpRequest req = newUpgradeRequest(null); + ch.writeOutbound(req); + + HttpRequest req2 = ch.readOutbound(); + List reqExts = WebSocketExtensionUtil.extractExtensions( + req2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + HttpResponse res = newUpgradeResponse("fallback"); + ch.writeInbound(res); + + HttpResponse res2 = ch.readInbound(); + List resExts = WebSocketExtensionUtil.extractExtensions( + res2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + // test + assertEquals(2, reqExts.size()); + assertEquals("main", reqExts.get(0).name()); + assertEquals("fallback", reqExts.get(1).name()); + + assertEquals(1, resExts.size()); + assertEquals("fallback", resExts.get(0).name()); + assertTrue(resExts.get(0).parameters().isEmpty()); + assertTrue(ch.pipeline().get(DummyDecoder.class) != null); + assertTrue(ch.pipeline().get(DummyEncoder.class) != null); + } + + @Test + public void testAllSuccess() { + // initialize + expect(mainHandshakerMock.newRequestData()). + andReturn(new WebSocketExtensionData("main", Collections.emptyMap())).once(); + expect(mainHandshakerMock.handshakeExtension( + webSocketExtensionDataEqual("main"))).andReturn(mainExtensionMock).anyTimes(); + expect(mainHandshakerMock.handshakeExtension( + webSocketExtensionDataEqual("fallback"))).andReturn(null).anyTimes(); + replay(mainHandshakerMock); + + expect(fallbackHandshakerMock.newRequestData()). + andReturn(new WebSocketExtensionData("fallback", Collections.emptyMap())).once(); + expect(fallbackHandshakerMock.handshakeExtension( + webSocketExtensionDataEqual("main"))).andReturn(null).anyTimes(); + expect(fallbackHandshakerMock.handshakeExtension( + webSocketExtensionDataEqual("fallback"))).andReturn(fallbackExtensionMock).anyTimes(); + replay(fallbackHandshakerMock); + + DummyEncoder mainEncoder = new DummyEncoder(); + DummyDecoder mainDecoder = new DummyDecoder(); + expect(mainExtensionMock.rsv()).andReturn(WebSocketExtension.RSV1).anyTimes(); + expect(mainExtensionMock.newExtensionEncoder()).andReturn(mainEncoder).once(); + expect(mainExtensionMock.newExtensionDecoder()).andReturn(mainDecoder).once(); + replay(mainExtensionMock); + + Dummy2Encoder fallbackEncoder = new Dummy2Encoder(); + Dummy2Decoder fallbackDecoder = new Dummy2Decoder(); + expect(fallbackExtensionMock.rsv()).andReturn(WebSocketExtension.RSV2).anyTimes(); + expect(fallbackExtensionMock.newExtensionEncoder()).andReturn(fallbackEncoder).once(); + expect(fallbackExtensionMock.newExtensionDecoder()).andReturn(fallbackDecoder).once(); + replay(fallbackExtensionMock); + + // execute + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketClientExtensionHandler( + mainHandshakerMock, fallbackHandshakerMock)); + + HttpRequest req = newUpgradeRequest(null); + ch.writeOutbound(req); + + HttpRequest req2 = ch.readOutbound(); + List reqExts = WebSocketExtensionUtil.extractExtensions( + req2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + HttpResponse res = newUpgradeResponse("main, fallback"); + ch.writeInbound(res); + + HttpResponse res2 = ch.readInbound(); + List resExts = WebSocketExtensionUtil.extractExtensions( + res2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + // test + assertEquals(2, reqExts.size()); + assertEquals("main", reqExts.get(0).name()); + assertEquals("fallback", reqExts.get(1).name()); + + assertEquals(2, resExts.size()); + assertEquals("main", resExts.get(0).name()); + assertEquals("fallback", resExts.get(1).name()); + assertTrue(ch.pipeline().context(mainEncoder) != null); + assertTrue(ch.pipeline().context(mainDecoder) != null); + assertTrue(ch.pipeline().context(fallbackEncoder) != null); + assertTrue(ch.pipeline().context(fallbackDecoder) != null); + } + + @Test(expected = CodecException.class) + public void testIfMainAndFallbackUseRSV1WillFail() { + // initialize + expect(mainHandshakerMock.newRequestData()). + andReturn(new WebSocketExtensionData("main", Collections.emptyMap())).once(); + expect(mainHandshakerMock.handshakeExtension( + webSocketExtensionDataEqual("main"))).andReturn(mainExtensionMock).anyTimes(); + expect(mainHandshakerMock.handshakeExtension( + webSocketExtensionDataEqual("fallback"))).andReturn(null).anyTimes(); + replay(mainHandshakerMock); + + expect(fallbackHandshakerMock.newRequestData()). + andReturn(new WebSocketExtensionData("fallback", Collections.emptyMap())).once(); + expect(fallbackHandshakerMock.handshakeExtension( + webSocketExtensionDataEqual("main"))).andReturn(null).anyTimes(); + expect(fallbackHandshakerMock.handshakeExtension( + webSocketExtensionDataEqual("fallback"))).andReturn(fallbackExtensionMock).anyTimes(); + replay(fallbackHandshakerMock); + + expect(mainExtensionMock.rsv()).andReturn(WebSocketExtension.RSV1).anyTimes(); + replay(mainExtensionMock); + + expect(fallbackExtensionMock.rsv()).andReturn(WebSocketExtension.RSV1).anyTimes(); + replay(fallbackExtensionMock); + + // execute + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketClientExtensionHandler( + mainHandshakerMock, fallbackHandshakerMock)); + + HttpRequest req = newUpgradeRequest(null); + ch.writeOutbound(req); + + HttpRequest req2 = ch.readOutbound(); + List reqExts = WebSocketExtensionUtil.extractExtensions( + req2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + HttpResponse res = newUpgradeResponse("main, fallback"); + ch.writeInbound(res); + + // test + assertEquals(2, reqExts.size()); + assertEquals("main", reqExts.get(0).name()); + assertEquals("fallback", reqExts.get(1).name()); + } + +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionTestUtil.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionTestUtil.java new file mode 100644 index 0000000000..b802246a61 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionTestUtil.java @@ -0,0 +1,129 @@ +/* + * 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. + */ +package io.netty.handler.codec.http.websocketx.extensions; + +import java.util.List; + +import org.easymock.EasyMock; +import org.easymock.IArgumentMatcher; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpHeaders.Names; +import io.netty.handler.codec.http.HttpHeaders.Values; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.ReferenceCountUtil; + +public final class WebSocketExtensionTestUtil { + + public static HttpRequest newUpgradeRequest(String ext) { + HttpRequest req = ReferenceCountUtil.releaseLater(new DefaultHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.GET, "/chat")); + + req.headers().set(Names.HOST, "server.example.com"); + req.headers().set(Names.UPGRADE, Values.WEBSOCKET.toString().toLowerCase()); + req.headers().set(Names.CONNECTION, "Upgrade"); + req.headers().set(Names.ORIGIN, "http://example.com"); + if (ext != null) { + req.headers().set(Names.SEC_WEBSOCKET_EXTENSIONS, ext); + } + + return req; + } + + public static HttpResponse newUpgradeResponse(String ext) { + HttpResponse res = ReferenceCountUtil.releaseLater(new DefaultHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.SWITCHING_PROTOCOLS)); + + res.headers().set(Names.HOST, "server.example.com"); + res.headers().set(Names.UPGRADE, Values.WEBSOCKET.toString().toLowerCase()); + res.headers().set(Names.CONNECTION, "Upgrade"); + res.headers().set(Names.ORIGIN, "http://example.com"); + if (ext != null) { + res.headers().set(Names.SEC_WEBSOCKET_EXTENSIONS, ext); + } + + return res; + } + + public static WebSocketExtensionData webSocketExtensionDataEqual(String name) { + EasyMock.reportMatcher(new WebSocketExtensionDataMatcher(name)); + return null; + } + + public static class WebSocketExtensionDataMatcher implements IArgumentMatcher { + + private final String name; + + public WebSocketExtensionDataMatcher(String name) { + this.name = name; + } + + @Override + public void appendTo(StringBuffer buf) { + buf.append("WebSocketExtensionData with name=" + name); + } + + @Override + public boolean matches(Object o) { + return o instanceof WebSocketExtensionData && + name.equals(((WebSocketExtensionData) o).name()); + } + } + + private WebSocketExtensionTestUtil() { + // unused + } + + static class DummyEncoder extends WebSocketExtensionEncoder { + @Override + protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg, + List out) throws Exception { + // unused + } + } + + static class DummyDecoder extends WebSocketExtensionDecoder { + @Override + protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, + List out) throws Exception { + // unused + } + } + + static class Dummy2Encoder extends WebSocketExtensionEncoder { + @Override + protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg, + List out) throws Exception { + // unused + } + } + + static class Dummy2Decoder extends WebSocketExtensionDecoder { + @Override + protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, + List out) throws Exception { + // unused + } + } + +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandlerTest.java new file mode 100644 index 0000000000..cf1bd420ec --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandlerTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions; + +import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionTestUtil.*; +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.CodecException; +import io.netty.handler.codec.http.HttpHeaders.Names; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; + +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +public class WebSocketServerExtensionHandlerTest { + + WebSocketServerExtensionHandshaker mainHandshakerMock = + createMock("mainHandshaker", WebSocketServerExtensionHandshaker.class); + WebSocketServerExtensionHandshaker fallbackHandshakerMock = + createMock("fallbackHandshaker", WebSocketServerExtensionHandshaker.class); + WebSocketServerExtension mainExtensionMock = + createMock("mainExtension", WebSocketServerExtension.class); + WebSocketServerExtension fallbackExtensionMock = + createMock("fallbackExtension", WebSocketServerExtension.class); + + @Test + public void testMainSuccess() { + // initialize + expect(mainHandshakerMock.handshakeExtension(webSocketExtensionDataEqual("main"))). + andReturn(mainExtensionMock).anyTimes(); + expect(mainHandshakerMock.handshakeExtension(webSocketExtensionDataEqual("fallback"))). + andReturn(null).anyTimes(); + replay(mainHandshakerMock); + + expect(fallbackHandshakerMock.handshakeExtension(webSocketExtensionDataEqual("fallback"))). + andReturn(fallbackExtensionMock).anyTimes(); + expect(fallbackHandshakerMock.handshakeExtension(webSocketExtensionDataEqual("main"))). + andReturn(null).anyTimes(); + replay(fallbackHandshakerMock); + + expect(mainExtensionMock.rsv()).andReturn(WebSocketExtension.RSV1).anyTimes(); + expect(mainExtensionMock.newReponseData()).andReturn( + new WebSocketExtensionData("main", Collections.emptyMap())).once(); + expect(mainExtensionMock.newExtensionEncoder()).andReturn(new DummyEncoder()).once(); + expect(mainExtensionMock.newExtensionDecoder()).andReturn(new DummyDecoder()).once(); + replay(mainExtensionMock); + + expect(fallbackExtensionMock.rsv()).andReturn(WebSocketExtension.RSV1).anyTimes(); + replay(fallbackExtensionMock); + + // execute + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler( + mainHandshakerMock, fallbackHandshakerMock)); + + HttpRequest req = newUpgradeRequest("main, fallback"); + ch.writeInbound(req); + + HttpResponse res = newUpgradeResponse(null); + ch.writeOutbound(res); + + HttpResponse res2 = ch.readOutbound(); + List resExts = WebSocketExtensionUtil.extractExtensions( + res2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + // test + assertEquals(1, resExts.size()); + assertEquals("main", resExts.get(0).name()); + assertTrue(resExts.get(0).parameters().isEmpty()); + assertTrue(ch.pipeline().get(DummyDecoder.class) != null); + assertTrue(ch.pipeline().get(DummyEncoder.class) != null); + } + + @Test + public void testCompatibleExtensionTogetherSuccess() { + // initialize + expect(mainHandshakerMock.handshakeExtension(webSocketExtensionDataEqual("main"))). + andReturn(mainExtensionMock).anyTimes(); + expect(mainHandshakerMock.handshakeExtension(webSocketExtensionDataEqual("fallback"))). + andReturn(null).anyTimes(); + replay(mainHandshakerMock); + + expect(fallbackHandshakerMock.handshakeExtension(webSocketExtensionDataEqual("fallback"))). + andReturn(fallbackExtensionMock).anyTimes(); + expect(fallbackHandshakerMock.handshakeExtension(webSocketExtensionDataEqual("main"))). + andReturn(null).anyTimes(); + replay(fallbackHandshakerMock); + + expect(mainExtensionMock.rsv()).andReturn(WebSocketExtension.RSV1).anyTimes(); + expect(mainExtensionMock.newReponseData()).andReturn( + new WebSocketExtensionData("main", Collections.emptyMap())).once(); + expect(mainExtensionMock.newExtensionEncoder()).andReturn(new DummyEncoder()).once(); + expect(mainExtensionMock.newExtensionDecoder()).andReturn(new DummyDecoder()).once(); + replay(mainExtensionMock); + + expect(fallbackExtensionMock.rsv()).andReturn(WebSocketExtension.RSV2).anyTimes(); + expect(fallbackExtensionMock.newReponseData()).andReturn( + new WebSocketExtensionData("fallback", Collections.emptyMap())).once(); + expect(fallbackExtensionMock.newExtensionEncoder()).andReturn(new Dummy2Encoder()).once(); + expect(fallbackExtensionMock.newExtensionDecoder()).andReturn(new Dummy2Decoder()).once(); + replay(fallbackExtensionMock); + + // execute + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler( + mainHandshakerMock, fallbackHandshakerMock)); + + HttpRequest req = newUpgradeRequest("main, fallback"); + ch.writeInbound(req); + + HttpResponse res = newUpgradeResponse(null); + ch.writeOutbound(res); + + HttpResponse res2 = ch.readOutbound(); + List resExts = WebSocketExtensionUtil.extractExtensions( + res2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + // test + assertEquals(2, resExts.size()); + assertEquals("main", resExts.get(0).name()); + assertEquals("fallback", resExts.get(1).name()); + assertTrue(ch.pipeline().get(DummyDecoder.class) != null); + assertTrue(ch.pipeline().get(DummyEncoder.class) != null); + assertTrue(ch.pipeline().get(Dummy2Decoder.class) != null); + assertTrue(ch.pipeline().get(Dummy2Encoder.class) != null); + } + + @Test + public void testNoneExtensionMatchingSuccess() { + // initialize + expect(mainHandshakerMock.handshakeExtension(webSocketExtensionDataEqual("unknown"))). + andReturn(null).anyTimes(); + expect(mainHandshakerMock.handshakeExtension(webSocketExtensionDataEqual("unknown2"))). + andReturn(null).anyTimes(); + replay(mainHandshakerMock); + + expect(fallbackHandshakerMock.handshakeExtension(webSocketExtensionDataEqual("unknown"))). + andReturn(null).anyTimes(); + expect(fallbackHandshakerMock.handshakeExtension(webSocketExtensionDataEqual("unknown2"))). + andReturn(null).anyTimes(); + replay(fallbackHandshakerMock); + + // execute + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler( + mainHandshakerMock, fallbackHandshakerMock)); + + HttpRequest req = newUpgradeRequest("unknown, unknown2"); + ch.writeInbound(req); + + HttpResponse res = newUpgradeResponse(null); + ch.writeOutbound(res); + + HttpResponse res2 = ch.readOutbound(); + + // test + assertFalse(res2.headers().contains(Names.SEC_WEBSOCKET_EXTENSIONS)); + } + +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshakerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshakerTest.java new file mode 100644 index 0000000000..d3ee5a4c1f --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshakerTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import static io.netty.handler.codec.http.websocketx.extensions.compression. + DeflateFrameServerExtensionHandshaker.*; +import static org.junit.Assert.*; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +public class DeflateFrameClientExtensionHandshakerTest { + + @Test + public void testWebkitDeflateFrameData() { + DeflateFrameClientExtensionHandshaker handshaker = + new DeflateFrameClientExtensionHandshaker(true); + + WebSocketExtensionData data = handshaker.newRequestData(); + + assertEquals(X_WEBKIT_DEFLATE_FRAME_EXTENSION, data.name()); + assertTrue(data.parameters().isEmpty()); + } + + @Test + public void testDeflateFrameData() { + DeflateFrameClientExtensionHandshaker handshaker = + new DeflateFrameClientExtensionHandshaker(false); + + WebSocketExtensionData data = handshaker.newRequestData(); + + assertEquals(DEFLATE_FRAME_EXTENSION, data.name()); + assertTrue(data.parameters().isEmpty()); + } + + @Test + public void testNormalHandshake() { + DeflateFrameClientExtensionHandshaker handshaker = + new DeflateFrameClientExtensionHandshaker(false); + + WebSocketClientExtension extension = handshaker.handshakeExtension( + new WebSocketExtensionData(DEFLATE_FRAME_EXTENSION, Collections.emptyMap())); + + assertNotNull(extension); + assertEquals(WebSocketClientExtension.RSV1, extension.rsv()); + assertTrue(extension.newExtensionDecoder() instanceof PerFrameDeflateDecoder); + assertTrue(extension.newExtensionEncoder() instanceof PerFrameDeflateEncoder); + } + + @Test + public void testFailedHandshake() { + // initialize + DeflateFrameClientExtensionHandshaker handshaker = + new DeflateFrameClientExtensionHandshaker(false); + + Map parameters = new HashMap(); + parameters.put("invalid", "12"); + + // execute + WebSocketClientExtension extension = handshaker.handshakeExtension( + new WebSocketExtensionData(DEFLATE_FRAME_EXTENSION, parameters)); + + // test + assertNull(extension); + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshakerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshakerTest.java new file mode 100644 index 0000000000..1d6f8ba5af --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshakerTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import static io.netty.handler.codec.http.websocketx.extensions.compression. + DeflateFrameServerExtensionHandshaker.*; +import static org.junit.Assert.*; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +public class DeflateFrameServerExtensionHandshakerTest { + + @Test + public void testNormalHandshake() { + // initialize + DeflateFrameServerExtensionHandshaker handshaker = + new DeflateFrameServerExtensionHandshaker(); + + // execute + WebSocketServerExtension extension = handshaker.handshakeExtension( + new WebSocketExtensionData(DEFLATE_FRAME_EXTENSION, Collections.emptyMap())); + + // test + assertNotNull(extension); + assertEquals(WebSocketServerExtension.RSV1, extension.rsv()); + assertTrue(extension.newExtensionDecoder() instanceof PerFrameDeflateDecoder); + assertTrue(extension.newExtensionEncoder() instanceof PerFrameDeflateEncoder); + } + + @Test + public void testWebkitHandshake() { + // initialize + DeflateFrameServerExtensionHandshaker handshaker = + new DeflateFrameServerExtensionHandshaker(); + + // execute + WebSocketServerExtension extension = handshaker.handshakeExtension( + new WebSocketExtensionData(X_WEBKIT_DEFLATE_FRAME_EXTENSION, Collections.emptyMap())); + + // test + assertNotNull(extension); + assertEquals(WebSocketServerExtension.RSV1, extension.rsv()); + assertTrue(extension.newExtensionDecoder() instanceof PerFrameDeflateDecoder); + assertTrue(extension.newExtensionEncoder() instanceof PerFrameDeflateEncoder); + } + + @Test + public void testFailedHandshake() { + // initialize + DeflateFrameServerExtensionHandshaker handshaker = + new DeflateFrameServerExtensionHandshaker(); + + Map parameters; + parameters = new HashMap(); + parameters.put("unknown", "11"); + + // execute + WebSocketServerExtension extension = handshaker.handshakeExtension( + new WebSocketExtensionData(DEFLATE_FRAME_EXTENSION, parameters)); + + // test + assertNull(extension); + } + +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoderTest.java new file mode 100644 index 0000000000..e59d380287 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoderTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import static org.junit.Assert.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.compression.ZlibWrapper; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; + +import java.util.Arrays; +import java.util.Random; + +import org.junit.Test; + +public class PerFrameDeflateDecoderTest { + + private static final Random random = new Random(); + + @Test + public void testCompressedFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8)); + EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerFrameDeflateDecoder(false)); + + // initialize + byte[] payload = new byte[300]; + random.nextBytes(payload); + + encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload)); + ByteBuf compressedPayload = encoderChannel.readOutbound(); + + BinaryWebSocketFrame compressedFrame = new BinaryWebSocketFrame(true, + WebSocketExtension.RSV1 | WebSocketExtension.RSV3, + compressedPayload.slice(0, compressedPayload.readableBytes() - 4)); + + // execute + decoderChannel.writeInbound(compressedFrame); + BinaryWebSocketFrame uncompressedFrame = decoderChannel.readInbound(); + + // test + assertNotNull(uncompressedFrame); + assertNotNull(uncompressedFrame.content()); + assertTrue(uncompressedFrame instanceof BinaryWebSocketFrame); + assertEquals(WebSocketExtension.RSV3, uncompressedFrame.rsv()); + assertEquals(300, uncompressedFrame.content().readableBytes()); + + byte[] finalPayload = new byte[300]; + uncompressedFrame.content().readBytes(finalPayload); + assertTrue(Arrays.equals(finalPayload, payload)); + uncompressedFrame.release(); + } + + @Test + public void testNormalFrame() { + EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerFrameDeflateDecoder(false)); + + // initialize + byte[] payload = new byte[300]; + random.nextBytes(payload); + + BinaryWebSocketFrame frame = new BinaryWebSocketFrame(true, + WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload)); + + // execute + decoderChannel.writeInbound(frame); + BinaryWebSocketFrame newFrame = decoderChannel.readInbound(); + + // test + assertNotNull(newFrame); + assertNotNull(newFrame.content()); + assertTrue(newFrame instanceof BinaryWebSocketFrame); + assertEquals(WebSocketExtension.RSV3, newFrame.rsv()); + assertEquals(300, newFrame.content().readableBytes()); + + byte[] finalPayload = new byte[300]; + newFrame.content().readBytes(finalPayload); + assertTrue(Arrays.equals(finalPayload, payload)); + newFrame.release(); + } + + // See https://github.com/netty/netty/issues/4348 + @Test + public void testCompressedEmptyFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8)); + EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerFrameDeflateDecoder(false)); + + encoderChannel.writeOutbound(Unpooled.EMPTY_BUFFER); + ByteBuf compressedPayload = encoderChannel.readOutbound(); + BinaryWebSocketFrame compressedFrame = + new BinaryWebSocketFrame(true, WebSocketExtension.RSV1 | WebSocketExtension.RSV3, compressedPayload); + + // execute + decoderChannel.writeInbound(compressedFrame); + BinaryWebSocketFrame uncompressedFrame = decoderChannel.readInbound(); + + // test + assertNotNull(uncompressedFrame); + assertNotNull(uncompressedFrame.content()); + assertTrue(uncompressedFrame instanceof BinaryWebSocketFrame); + assertEquals(WebSocketExtension.RSV3, uncompressedFrame.rsv()); + assertEquals(0, uncompressedFrame.content().readableBytes()); + uncompressedFrame.release(); + } + +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoderTest.java new file mode 100644 index 0000000000..5c085e9cee --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoderTest.java @@ -0,0 +1,162 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import static org.junit.Assert.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.compression.ZlibWrapper; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; + +import java.util.Arrays; +import java.util.Random; + +import org.junit.Test; + +public class PerFrameDeflateEncoderTest { + + private static final Random random = new Random(); + + @Test + public void testCompressedFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerFrameDeflateEncoder(9, 15, false)); + EmbeddedChannel decoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibDecoder(ZlibWrapper.NONE)); + + // initialize + byte[] payload = new byte[300]; + random.nextBytes(payload); + BinaryWebSocketFrame frame = new BinaryWebSocketFrame(true, + WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload)); + + // execute + encoderChannel.writeOutbound(frame); + BinaryWebSocketFrame compressedFrame = encoderChannel.readOutbound(); + + // test + assertNotNull(compressedFrame); + assertNotNull(compressedFrame.content()); + assertTrue(compressedFrame instanceof BinaryWebSocketFrame); + assertEquals(WebSocketExtension.RSV1 | WebSocketExtension.RSV3, compressedFrame.rsv()); + + decoderChannel.writeInbound(compressedFrame.content()); + decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL); + ByteBuf uncompressedPayload = decoderChannel.readInbound(); + assertEquals(300, uncompressedPayload.readableBytes()); + + byte[] finalPayload = new byte[300]; + uncompressedPayload.readBytes(finalPayload); + assertTrue(Arrays.equals(finalPayload, payload)); + uncompressedPayload.release(); + } + + @Test + public void testAlreadyCompressedFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerFrameDeflateEncoder(9, 15, false)); + + // initialize + byte[] payload = new byte[300]; + random.nextBytes(payload); + + BinaryWebSocketFrame frame = new BinaryWebSocketFrame(true, + WebSocketExtension.RSV3 | WebSocketExtension.RSV1, Unpooled.wrappedBuffer(payload)); + + // execute + encoderChannel.writeOutbound(frame); + BinaryWebSocketFrame newFrame = encoderChannel.readOutbound(); + + // test + assertNotNull(newFrame); + assertNotNull(newFrame.content()); + assertTrue(newFrame instanceof BinaryWebSocketFrame); + assertEquals(WebSocketExtension.RSV3 | WebSocketExtension.RSV1, newFrame.rsv()); + assertEquals(300, newFrame.content().readableBytes()); + + byte[] finalPayload = new byte[300]; + newFrame.content().readBytes(finalPayload); + assertTrue(Arrays.equals(finalPayload, payload)); + newFrame.release(); + } + + @Test + public void testFramementedFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerFrameDeflateEncoder(9, 15, false)); + EmbeddedChannel decoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibDecoder(ZlibWrapper.NONE)); + + // initialize + byte[] payload1 = new byte[100]; + random.nextBytes(payload1); + byte[] payload2 = new byte[100]; + random.nextBytes(payload2); + byte[] payload3 = new byte[100]; + random.nextBytes(payload3); + + BinaryWebSocketFrame frame1 = new BinaryWebSocketFrame(false, + WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload1)); + ContinuationWebSocketFrame frame2 = new ContinuationWebSocketFrame(false, + WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload2)); + ContinuationWebSocketFrame frame3 = new ContinuationWebSocketFrame(true, + WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload3)); + + // execute + encoderChannel.writeOutbound(frame1); + encoderChannel.writeOutbound(frame2); + encoderChannel.writeOutbound(frame3); + BinaryWebSocketFrame compressedFrame1 = encoderChannel.readOutbound(); + ContinuationWebSocketFrame compressedFrame2 = encoderChannel.readOutbound(); + ContinuationWebSocketFrame compressedFrame3 = encoderChannel.readOutbound(); + + // test + assertNotNull(compressedFrame1); + assertNotNull(compressedFrame2); + assertNotNull(compressedFrame3); + assertEquals(WebSocketExtension.RSV1 | WebSocketExtension.RSV3, compressedFrame1.rsv()); + assertEquals(WebSocketExtension.RSV1 | WebSocketExtension.RSV3, compressedFrame2.rsv()); + assertEquals(WebSocketExtension.RSV1 | WebSocketExtension.RSV3, compressedFrame3.rsv()); + assertFalse(compressedFrame1.isFinalFragment()); + assertFalse(compressedFrame2.isFinalFragment()); + assertTrue(compressedFrame3.isFinalFragment()); + + decoderChannel.writeInbound(compressedFrame1.content()); + decoderChannel.writeInbound(Unpooled.wrappedBuffer(DeflateDecoder.FRAME_TAIL)); + ByteBuf uncompressedPayload1 = decoderChannel.readInbound(); + byte[] finalPayload1 = new byte[100]; + uncompressedPayload1.readBytes(finalPayload1); + assertTrue(Arrays.equals(finalPayload1, payload1)); + uncompressedPayload1.release(); + + decoderChannel.writeInbound(compressedFrame2.content()); + decoderChannel.writeInbound(Unpooled.wrappedBuffer(DeflateDecoder.FRAME_TAIL)); + ByteBuf uncompressedPayload2 = decoderChannel.readInbound(); + byte[] finalPayload2 = new byte[100]; + uncompressedPayload2.readBytes(finalPayload2); + assertTrue(Arrays.equals(finalPayload2, payload2)); + uncompressedPayload2.release(); + + decoderChannel.writeInbound(compressedFrame3.content()); + decoderChannel.writeInbound(Unpooled.wrappedBuffer(DeflateDecoder.FRAME_TAIL)); + ByteBuf uncompressedPayload3 = decoderChannel.readInbound(); + byte[] finalPayload3 = new byte[100]; + uncompressedPayload3.readBytes(finalPayload3); + assertTrue(Arrays.equals(finalPayload3, payload3)); + uncompressedPayload3.release(); + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshakerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshakerTest.java new file mode 100644 index 0000000000..f221100e48 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshakerTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import static io.netty.handler.codec.http.websocketx.extensions.compression. + PerMessageDeflateServerExtensionHandshaker.*; +import static org.junit.Assert.*; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +public class PerMessageDeflateClientExtensionHandshakerTest { + + @Test + public void testNormalData() { + PerMessageDeflateClientExtensionHandshaker handshaker = + new PerMessageDeflateClientExtensionHandshaker(); + + WebSocketExtensionData data = handshaker.newRequestData(); + + assertEquals(PERMESSAGE_DEFLATE_EXTENSION, data.name()); + assertTrue(data.parameters().isEmpty()); + } + + @Test + public void testCustomData() { + PerMessageDeflateClientExtensionHandshaker handshaker = + new PerMessageDeflateClientExtensionHandshaker(6, true, 10, true, true); + + WebSocketExtensionData data = handshaker.newRequestData(); + + assertEquals(PERMESSAGE_DEFLATE_EXTENSION, data.name()); + assertTrue(data.parameters().containsKey(CLIENT_MAX_WINDOW)); + assertTrue(data.parameters().containsKey(SERVER_MAX_WINDOW)); + assertTrue(data.parameters().get(SERVER_MAX_WINDOW).equals("10")); + assertTrue(data.parameters().containsKey(CLIENT_MAX_WINDOW)); + assertTrue(data.parameters().containsKey(SERVER_MAX_WINDOW)); + } + + @Test + public void testNormalHandshake() { + PerMessageDeflateClientExtensionHandshaker handshaker = + new PerMessageDeflateClientExtensionHandshaker(); + + WebSocketClientExtension extension = handshaker.handshakeExtension( + new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, Collections.emptyMap())); + + assertNotNull(extension); + assertEquals(WebSocketClientExtension.RSV1, extension.rsv()); + assertTrue(extension.newExtensionDecoder() instanceof PerMessageDeflateDecoder); + assertTrue(extension.newExtensionEncoder() instanceof PerMessageDeflateEncoder); + } + + @Test + public void testCustomHandshake() { + WebSocketClientExtension extension; + Map parameters; + + // initialize + PerMessageDeflateClientExtensionHandshaker handshaker = + new PerMessageDeflateClientExtensionHandshaker(6, true, 10, true, true); + + parameters = new HashMap(); + parameters.put(CLIENT_MAX_WINDOW, "12"); + parameters.put(SERVER_MAX_WINDOW, "10"); + parameters.put(CLIENT_NO_CONTEXT, null); + parameters.put(SERVER_NO_CONTEXT, null); + + // execute + extension = handshaker.handshakeExtension( + new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, parameters)); + + // test + assertNotNull(extension); + assertEquals(WebSocketClientExtension.RSV1, extension.rsv()); + assertTrue(extension.newExtensionDecoder() instanceof PerMessageDeflateDecoder); + assertTrue(extension.newExtensionEncoder() instanceof PerMessageDeflateEncoder); + + // initialize + parameters = new HashMap(); + parameters.put(SERVER_MAX_WINDOW, "10"); + parameters.put(SERVER_NO_CONTEXT, null); + + // execute + extension = handshaker.handshakeExtension( + new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, parameters)); + + // test + assertNotNull(extension); + assertEquals(WebSocketClientExtension.RSV1, extension.rsv()); + assertTrue(extension.newExtensionDecoder() instanceof PerMessageDeflateDecoder); + assertTrue(extension.newExtensionEncoder() instanceof PerMessageDeflateEncoder); + + // initialize + parameters = new HashMap(); + + // execute + extension = handshaker.handshakeExtension( + new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, parameters)); + + // test + assertNull(extension); + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java new file mode 100644 index 0000000000..5c02976076 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java @@ -0,0 +1,194 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import static org.junit.Assert.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.compression.ZlibWrapper; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; + +import java.util.Arrays; +import java.util.Random; + +import org.junit.Test; + +public class PerMessageDeflateDecoderTest { + + private static final Random random = new Random(); + + @Test + public void testCompressedFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8)); + EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false)); + + // initialize + byte[] payload = new byte[300]; + random.nextBytes(payload); + + encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload)); + ByteBuf compressedPayload = encoderChannel.readOutbound(); + + BinaryWebSocketFrame compressedFrame = new BinaryWebSocketFrame(true, + WebSocketExtension.RSV1 | WebSocketExtension.RSV3, + compressedPayload.slice(0, compressedPayload.readableBytes() - 4)); + + // execute + decoderChannel.writeInbound(compressedFrame); + BinaryWebSocketFrame uncompressedFrame = decoderChannel.readInbound(); + + // test + assertNotNull(uncompressedFrame); + assertNotNull(uncompressedFrame.content()); + assertTrue(uncompressedFrame instanceof BinaryWebSocketFrame); + assertEquals(WebSocketExtension.RSV3, uncompressedFrame.rsv()); + assertEquals(300, uncompressedFrame.content().readableBytes()); + + byte[] finalPayload = new byte[300]; + uncompressedFrame.content().readBytes(finalPayload); + assertTrue(Arrays.equals(finalPayload, payload)); + uncompressedFrame.release(); + } + + @Test + public void testNormalFrame() { + EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false)); + + // initialize + byte[] payload = new byte[300]; + random.nextBytes(payload); + + BinaryWebSocketFrame frame = new BinaryWebSocketFrame(true, + WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload)); + + // execute + decoderChannel.writeInbound(frame); + BinaryWebSocketFrame newFrame = decoderChannel.readInbound(); + + // test + assertNotNull(newFrame); + assertNotNull(newFrame.content()); + assertTrue(newFrame instanceof BinaryWebSocketFrame); + assertEquals(WebSocketExtension.RSV3, newFrame.rsv()); + assertEquals(300, newFrame.content().readableBytes()); + + byte[] finalPayload = new byte[300]; + newFrame.content().readBytes(finalPayload); + assertTrue(Arrays.equals(finalPayload, payload)); + newFrame.release(); + } + + @Test + public void testFramementedFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8)); + EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false)); + + // initialize + byte[] payload = new byte[300]; + random.nextBytes(payload); + + encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload)); + ByteBuf compressedPayload = encoderChannel.readOutbound(); + compressedPayload = compressedPayload.slice(0, compressedPayload.readableBytes() - 4); + + int oneThird = compressedPayload.readableBytes() / 3; + BinaryWebSocketFrame compressedFrame1 = new BinaryWebSocketFrame(false, + WebSocketExtension.RSV1 | WebSocketExtension.RSV3, + compressedPayload.slice(0, oneThird)); + ContinuationWebSocketFrame compressedFrame2 = new ContinuationWebSocketFrame(false, + WebSocketExtension.RSV3, compressedPayload.slice(oneThird, oneThird)); + ContinuationWebSocketFrame compressedFrame3 = new ContinuationWebSocketFrame(true, + WebSocketExtension.RSV3, compressedPayload.slice(oneThird * 2, + compressedPayload.readableBytes() - oneThird * 2)); + + // execute + decoderChannel.writeInbound(compressedFrame1.retain()); + decoderChannel.writeInbound(compressedFrame2.retain()); + decoderChannel.writeInbound(compressedFrame3); + BinaryWebSocketFrame uncompressedFrame1 = decoderChannel.readInbound(); + ContinuationWebSocketFrame uncompressedFrame2 = decoderChannel.readInbound(); + ContinuationWebSocketFrame uncompressedFrame3 = decoderChannel.readInbound(); + + // test + assertNotNull(uncompressedFrame1); + assertNotNull(uncompressedFrame2); + assertNotNull(uncompressedFrame3); + assertEquals(WebSocketExtension.RSV3, uncompressedFrame1.rsv()); + assertEquals(WebSocketExtension.RSV3, uncompressedFrame2.rsv()); + assertEquals(WebSocketExtension.RSV3, uncompressedFrame3.rsv()); + + ByteBuf finalPayloadWrapped = Unpooled.wrappedBuffer(uncompressedFrame1.content(), + uncompressedFrame2.content(), uncompressedFrame3.content()); + assertEquals(300, finalPayloadWrapped.readableBytes()); + + byte[] finalPayload = new byte[300]; + finalPayloadWrapped.readBytes(finalPayload); + assertTrue(Arrays.equals(finalPayload, payload)); + uncompressedFrame1.release(); + uncompressedFrame2.release(); + uncompressedFrame3.release(); + } + + @Test + public void testMultiCompressedPayloadWithinFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8)); + EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false)); + + // initialize + byte[] payload1 = new byte[100]; + random.nextBytes(payload1); + byte[] payload2 = new byte[100]; + random.nextBytes(payload2); + + encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload1)); + ByteBuf compressedPayload1 = encoderChannel.readOutbound(); + encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload2)); + ByteBuf compressedPayload2 = encoderChannel.readOutbound(); + + BinaryWebSocketFrame compressedFrame = new BinaryWebSocketFrame(true, + WebSocketExtension.RSV1 | WebSocketExtension.RSV3, + Unpooled.wrappedBuffer( + compressedPayload1, + compressedPayload2.slice(0, compressedPayload2.readableBytes() - 4))); + + // execute + decoderChannel.writeInbound(compressedFrame); + BinaryWebSocketFrame uncompressedFrame = decoderChannel.readInbound(); + + // test + assertNotNull(uncompressedFrame); + assertNotNull(uncompressedFrame.content()); + assertTrue(uncompressedFrame instanceof BinaryWebSocketFrame); + assertEquals(WebSocketExtension.RSV3, uncompressedFrame.rsv()); + assertEquals(200, uncompressedFrame.content().readableBytes()); + + byte[] finalPayload1 = new byte[100]; + uncompressedFrame.content().readBytes(finalPayload1); + assertTrue(Arrays.equals(finalPayload1, payload1)); + byte[] finalPayload2 = new byte[100]; + uncompressedFrame.content().readBytes(finalPayload2); + assertTrue(Arrays.equals(finalPayload2, payload2)); + uncompressedFrame.release(); + } + +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java new file mode 100644 index 0000000000..66ae9627e9 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import static org.junit.Assert.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.compression.ZlibWrapper; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension; + +import java.util.Arrays; +import java.util.Random; + +import org.junit.Test; + +public class PerMessageDeflateEncoderTest { + + private static final Random random = new Random(); + + @Test + public void testCompressedFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false)); + EmbeddedChannel decoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibDecoder(ZlibWrapper.NONE)); + + // initialize + byte[] payload = new byte[300]; + random.nextBytes(payload); + BinaryWebSocketFrame frame = new BinaryWebSocketFrame(true, + WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload)); + + // execute + encoderChannel.writeOutbound(frame); + BinaryWebSocketFrame compressedFrame = encoderChannel.readOutbound(); + + // test + assertNotNull(compressedFrame); + assertNotNull(compressedFrame.content()); + assertTrue(compressedFrame instanceof BinaryWebSocketFrame); + assertEquals(WebSocketExtension.RSV1 | WebSocketExtension.RSV3, compressedFrame.rsv()); + + decoderChannel.writeInbound(compressedFrame.content()); + decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL); + ByteBuf uncompressedPayload = decoderChannel.readInbound(); + assertEquals(300, uncompressedPayload.readableBytes()); + + byte[] finalPayload = new byte[300]; + uncompressedPayload.readBytes(finalPayload); + assertTrue(Arrays.equals(finalPayload, payload)); + uncompressedPayload.release(); + } + + @Test + public void testAlreadyCompressedFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false)); + + // initialize + byte[] payload = new byte[300]; + random.nextBytes(payload); + + BinaryWebSocketFrame frame = new BinaryWebSocketFrame(true, + WebSocketExtension.RSV3 | WebSocketExtension.RSV1, Unpooled.wrappedBuffer(payload)); + + // execute + encoderChannel.writeOutbound(frame); + BinaryWebSocketFrame newFrame = encoderChannel.readOutbound(); + + // test + assertNotNull(newFrame); + assertNotNull(newFrame.content()); + assertTrue(newFrame instanceof BinaryWebSocketFrame); + assertEquals(WebSocketExtension.RSV3 | WebSocketExtension.RSV1, newFrame.rsv()); + assertEquals(300, newFrame.content().readableBytes()); + + byte[] finalPayload = new byte[300]; + newFrame.content().readBytes(finalPayload); + assertTrue(Arrays.equals(finalPayload, payload)); + newFrame.release(); + } + + @Test + public void testFramementedFrame() { + EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false)); + EmbeddedChannel decoderChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibDecoder(ZlibWrapper.NONE)); + + // initialize + byte[] payload1 = new byte[100]; + random.nextBytes(payload1); + byte[] payload2 = new byte[100]; + random.nextBytes(payload2); + byte[] payload3 = new byte[100]; + random.nextBytes(payload3); + + BinaryWebSocketFrame frame1 = new BinaryWebSocketFrame(false, + WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload1)); + ContinuationWebSocketFrame frame2 = new ContinuationWebSocketFrame(false, + WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload2)); + ContinuationWebSocketFrame frame3 = new ContinuationWebSocketFrame(true, + WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload3)); + + // execute + encoderChannel.writeOutbound(frame1); + encoderChannel.writeOutbound(frame2); + encoderChannel.writeOutbound(frame3); + BinaryWebSocketFrame compressedFrame1 = encoderChannel.readOutbound(); + ContinuationWebSocketFrame compressedFrame2 = encoderChannel.readOutbound(); + ContinuationWebSocketFrame compressedFrame3 = encoderChannel.readOutbound(); + + // test + assertNotNull(compressedFrame1); + assertNotNull(compressedFrame2); + assertNotNull(compressedFrame3); + assertEquals(WebSocketExtension.RSV1 | WebSocketExtension.RSV3, compressedFrame1.rsv()); + assertEquals(WebSocketExtension.RSV3, compressedFrame2.rsv()); + assertEquals(WebSocketExtension.RSV3, compressedFrame3.rsv()); + assertFalse(compressedFrame1.isFinalFragment()); + assertFalse(compressedFrame2.isFinalFragment()); + assertTrue(compressedFrame3.isFinalFragment()); + + decoderChannel.writeInbound(compressedFrame1.content()); + ByteBuf uncompressedPayload1 = decoderChannel.readInbound(); + byte[] finalPayload1 = new byte[100]; + uncompressedPayload1.readBytes(finalPayload1); + assertTrue(Arrays.equals(finalPayload1, payload1)); + uncompressedPayload1.release(); + + decoderChannel.writeInbound(compressedFrame2.content()); + ByteBuf uncompressedPayload2 = decoderChannel.readInbound(); + byte[] finalPayload2 = new byte[100]; + uncompressedPayload2.readBytes(finalPayload2); + assertTrue(Arrays.equals(finalPayload2, payload2)); + uncompressedPayload2.release(); + + decoderChannel.writeInbound(compressedFrame3.content()); + decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL); + ByteBuf uncompressedPayload3 = decoderChannel.readInbound(); + byte[] finalPayload3 = new byte[100]; + uncompressedPayload3.readBytes(finalPayload3); + assertTrue(Arrays.equals(finalPayload3, payload3)); + uncompressedPayload3.release(); + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshakerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshakerTest.java new file mode 100644 index 0000000000..3b245d8164 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshakerTest.java @@ -0,0 +1,172 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import static io.netty.handler.codec.http.websocketx.extensions.compression. + PerMessageDeflateServerExtensionHandshaker.*; +import static org.junit.Assert.*; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtension; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +public class PerMessageDeflateServerExtensionHandshakerTest { + + @Test + public void testNormalHandshake() { + WebSocketServerExtension extension; + WebSocketExtensionData data; + Map parameters; + + // initialize + PerMessageDeflateServerExtensionHandshaker handshaker = + new PerMessageDeflateServerExtensionHandshaker(); + + // execute + extension = handshaker.handshakeExtension( + new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, Collections.emptyMap())); + + // test + assertNotNull(extension); + assertEquals(WebSocketServerExtension.RSV1, extension.rsv()); + assertTrue(extension.newExtensionDecoder() instanceof PerMessageDeflateDecoder); + assertTrue(extension.newExtensionEncoder() instanceof PerMessageDeflateEncoder); + + // execute + data = extension.newReponseData(); + + assertEquals(PERMESSAGE_DEFLATE_EXTENSION, data.name()); + assertTrue(data.parameters().isEmpty()); + + // initialize + parameters = new HashMap(); + parameters.put(CLIENT_MAX_WINDOW, null); + parameters.put(CLIENT_NO_CONTEXT, null); + + // execute + extension = handshaker.handshakeExtension( + new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, Collections.emptyMap())); + + // test + assertNotNull(extension); + assertEquals(WebSocketServerExtension.RSV1, extension.rsv()); + assertTrue(extension.newExtensionDecoder() instanceof PerMessageDeflateDecoder); + assertTrue(extension.newExtensionEncoder() instanceof PerMessageDeflateEncoder); + + // execute + data = extension.newReponseData(); + + // test + assertEquals(PERMESSAGE_DEFLATE_EXTENSION, data.name()); + assertTrue(data.parameters().isEmpty()); + + // initialize + parameters = new HashMap(); + parameters.put(SERVER_MAX_WINDOW, "12"); + parameters.put(SERVER_NO_CONTEXT, null); + + // execute + extension = handshaker.handshakeExtension( + new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, parameters)); + + // test + assertNull(extension); + } + + @Test + public void testCustomHandshake() { + WebSocketServerExtension extension; + Map parameters; + WebSocketExtensionData data; + + // initialize + PerMessageDeflateServerExtensionHandshaker handshaker = + new PerMessageDeflateServerExtensionHandshaker(6, true, 10, true, true); + + parameters = new HashMap(); + parameters.put(CLIENT_MAX_WINDOW, null); + parameters.put(SERVER_MAX_WINDOW, "12"); + parameters.put(CLIENT_NO_CONTEXT, null); + parameters.put(SERVER_NO_CONTEXT, null); + + // execute + extension = handshaker.handshakeExtension( + new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, parameters)); + + // test + assertNotNull(extension); + assertEquals(WebSocketServerExtension.RSV1, extension.rsv()); + assertTrue(extension.newExtensionDecoder() instanceof PerMessageDeflateDecoder); + assertTrue(extension.newExtensionEncoder() instanceof PerMessageDeflateEncoder); + + // execute + data = extension.newReponseData(); + + // test + assertEquals(PERMESSAGE_DEFLATE_EXTENSION, data.name()); + assertTrue(data.parameters().containsKey(CLIENT_MAX_WINDOW)); + assertTrue(data.parameters().get(CLIENT_MAX_WINDOW).equals("10")); + assertTrue(data.parameters().containsKey(SERVER_MAX_WINDOW)); + assertTrue(data.parameters().get(SERVER_MAX_WINDOW).equals("12")); + assertTrue(data.parameters().containsKey(CLIENT_MAX_WINDOW)); + assertTrue(data.parameters().containsKey(SERVER_MAX_WINDOW)); + + // initialize + parameters = new HashMap(); + parameters.put(SERVER_MAX_WINDOW, "12"); + parameters.put(SERVER_NO_CONTEXT, null); + + // execute + extension = handshaker.handshakeExtension( + new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, parameters)); + + // test + assertNotNull(extension); + assertEquals(WebSocketServerExtension.RSV1, extension.rsv()); + assertTrue(extension.newExtensionDecoder() instanceof PerMessageDeflateDecoder); + assertTrue(extension.newExtensionEncoder() instanceof PerMessageDeflateEncoder); + + // execute + data = extension.newReponseData(); + + // test + assertEquals(PERMESSAGE_DEFLATE_EXTENSION, data.name()); + assertEquals(2, data.parameters().size()); + assertTrue(data.parameters().containsKey(SERVER_MAX_WINDOW)); + assertTrue(data.parameters().get(SERVER_MAX_WINDOW).equals("12")); + assertTrue(data.parameters().containsKey(SERVER_NO_CONTEXT)); + + // initialize + parameters = new HashMap(); + + // execute + extension = handshaker.handshakeExtension( + new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, parameters)); + // test + assertNotNull(extension); + + // execute + data = extension.newReponseData(); + + // test + assertEquals(PERMESSAGE_DEFLATE_EXTENSION, data.name()); + assertTrue(data.parameters().isEmpty()); + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/WebSocketServerCompressionHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/WebSocketServerCompressionHandlerTest.java new file mode 100644 index 0000000000..0b9b48fcee --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/WebSocketServerCompressionHandlerTest.java @@ -0,0 +1,196 @@ +/* + * Copyright 2014 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 io.netty.handler.codec.http.websocketx.extensions.compression; + +import static io.netty.handler.codec.http.websocketx.extensions.compression. + PerMessageDeflateServerExtensionHandshaker.*; +import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionTestUtil.*; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionUtil; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandler; +import io.netty.handler.codec.http.HttpHeaders.Names; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class WebSocketServerCompressionHandlerTest { + + @Test + public void testNormalSuccess() { + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerCompressionHandler()); + + HttpRequest req = newUpgradeRequest(PERMESSAGE_DEFLATE_EXTENSION); + ch.writeInbound(req); + + HttpResponse res = newUpgradeResponse(null); + ch.writeOutbound(res); + + HttpResponse res2 = ch.readOutbound(); + List exts = WebSocketExtensionUtil.extractExtensions( + res2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + Assert.assertEquals(PERMESSAGE_DEFLATE_EXTENSION, exts.get(0).name()); + Assert.assertTrue(exts.get(0).parameters().isEmpty()); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateDecoder.class) != null); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateEncoder.class) != null); + } + + @Test + public void testClientWindowSizeSuccess() { + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler( + new PerMessageDeflateServerExtensionHandshaker(6, false, 10, false, false))); + + HttpRequest req = newUpgradeRequest(PERMESSAGE_DEFLATE_EXTENSION + "; " + CLIENT_MAX_WINDOW); + ch.writeInbound(req); + + HttpResponse res = newUpgradeResponse(null); + ch.writeOutbound(res); + + HttpResponse res2 = ch.readOutbound(); + List exts = WebSocketExtensionUtil.extractExtensions( + res2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + Assert.assertEquals(PERMESSAGE_DEFLATE_EXTENSION, exts.get(0).name()); + Assert.assertEquals("10", exts.get(0).parameters().get(CLIENT_MAX_WINDOW)); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateDecoder.class) != null); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateEncoder.class) != null); + } + + @Test + public void testClientWindowSizeUnavailable() { + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler( + new PerMessageDeflateServerExtensionHandshaker(6, false, 10, false, false))); + + HttpRequest req = newUpgradeRequest(PERMESSAGE_DEFLATE_EXTENSION); + ch.writeInbound(req); + + HttpResponse res = newUpgradeResponse(null); + ch.writeOutbound(res); + + HttpResponse res2 = ch.readOutbound(); + List exts = WebSocketExtensionUtil.extractExtensions( + res2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + Assert.assertEquals(PERMESSAGE_DEFLATE_EXTENSION, exts.get(0).name()); + Assert.assertTrue(exts.get(0).parameters().isEmpty()); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateDecoder.class) != null); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateEncoder.class) != null); + } + + @Test + public void testServerWindowSizeSuccess() { + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler( + new PerMessageDeflateServerExtensionHandshaker(6, true, 15, false, false))); + + HttpRequest req = newUpgradeRequest(PERMESSAGE_DEFLATE_EXTENSION + "; " + SERVER_MAX_WINDOW + "=10"); + ch.writeInbound(req); + + HttpResponse res = newUpgradeResponse(null); + ch.writeOutbound(res); + + HttpResponse res2 = ch.readOutbound(); + List exts = WebSocketExtensionUtil.extractExtensions( + res2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + Assert.assertEquals(PERMESSAGE_DEFLATE_EXTENSION, exts.get(0).name()); + Assert.assertEquals("10", exts.get(0).parameters().get(SERVER_MAX_WINDOW)); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateDecoder.class) != null); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateEncoder.class) != null); + } + + @Test + public void testServerWindowSizeDisable() { + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler( + new PerMessageDeflateServerExtensionHandshaker(6, false, 15, false, false))); + + HttpRequest req = newUpgradeRequest(PERMESSAGE_DEFLATE_EXTENSION + "; " + SERVER_MAX_WINDOW + "=10"); + ch.writeInbound(req); + + HttpResponse res = newUpgradeResponse(null); + ch.writeOutbound(res); + + HttpResponse res2 = ch.readOutbound(); + + Assert.assertFalse(res2.headers().contains(Names.SEC_WEBSOCKET_EXTENSIONS)); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateDecoder.class) == null); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateEncoder.class) == null); + } + + @Test + public void testServerNoContext() { + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerCompressionHandler()); + + HttpRequest req = newUpgradeRequest(PERMESSAGE_DEFLATE_EXTENSION + "; " + SERVER_NO_CONTEXT); + ch.writeInbound(req); + + HttpResponse res = newUpgradeResponse(null); + ch.writeOutbound(res); + + HttpResponse res2 = ch.readOutbound(); + + Assert.assertFalse(res2.headers().contains(Names.SEC_WEBSOCKET_EXTENSIONS)); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateDecoder.class) == null); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateEncoder.class) == null); + } + + @Test + public void testClientNoContext() { + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerCompressionHandler()); + + HttpRequest req = newUpgradeRequest(PERMESSAGE_DEFLATE_EXTENSION + "; " + CLIENT_NO_CONTEXT); + ch.writeInbound(req); + + HttpResponse res = newUpgradeResponse(null); + ch.writeOutbound(res); + + HttpResponse res2 = ch.readOutbound(); + List exts = WebSocketExtensionUtil.extractExtensions( + res2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + Assert.assertEquals(PERMESSAGE_DEFLATE_EXTENSION, exts.get(0).name()); + Assert.assertTrue(exts.get(0).parameters().isEmpty()); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateDecoder.class) != null); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateEncoder.class) != null); + } + + @Test + public void testServerWindowSizeDisableThenFallback() { + EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler( + new PerMessageDeflateServerExtensionHandshaker(6, false, 15, false, false))); + + HttpRequest req = newUpgradeRequest(PERMESSAGE_DEFLATE_EXTENSION + "; " + SERVER_MAX_WINDOW + "=10, " + + PERMESSAGE_DEFLATE_EXTENSION); + ch.writeInbound(req); + + HttpResponse res = newUpgradeResponse(null); + ch.writeOutbound(res); + + HttpResponse res2 = ch.readOutbound(); + List exts = WebSocketExtensionUtil.extractExtensions( + res2.headers().get(Names.SEC_WEBSOCKET_EXTENSIONS)); + + Assert.assertEquals(PERMESSAGE_DEFLATE_EXTENSION, exts.get(0).name()); + Assert.assertTrue(exts.get(0).parameters().isEmpty()); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateDecoder.class) != null); + Assert.assertTrue(ch.pipeline().get(PerMessageDeflateEncoder.class) != null); + } + +} diff --git a/codec/src/main/java/io/netty/handler/codec/compression/ZlibCodecFactory.java b/codec/src/main/java/io/netty/handler/codec/compression/ZlibCodecFactory.java index 4641cf1657..8d94b8cdda 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/ZlibCodecFactory.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/ZlibCodecFactory.java @@ -66,7 +66,7 @@ public final class ZlibCodecFactory { public static ZlibEncoder newZlibEncoder(ZlibWrapper wrapper, int compressionLevel, int windowBits, int memLevel) { if (PlatformDependent.javaVersion() < 7 || noJdkZlibEncoder || - windowBits != DEFAULT_JDK_WINDOW_SIZE || memLevel != DEFAULT_JDK_MEM_LEVEL) { + windowBits != DEFAULT_JDK_WINDOW_SIZE || memLevel != DEFAULT_JDK_MEM_LEVEL) { return new JZlibEncoder(wrapper, compressionLevel, windowBits, memLevel); } else { return new JdkZlibEncoder(wrapper, compressionLevel); @@ -91,7 +91,7 @@ public final class ZlibCodecFactory { public static ZlibEncoder newZlibEncoder(int compressionLevel, int windowBits, int memLevel, byte[] dictionary) { if (PlatformDependent.javaVersion() < 7 || noJdkZlibEncoder || - windowBits != DEFAULT_JDK_WINDOW_SIZE || memLevel != DEFAULT_JDK_MEM_LEVEL) { + windowBits != DEFAULT_JDK_WINDOW_SIZE || memLevel != DEFAULT_JDK_MEM_LEVEL) { return new JZlibEncoder(compressionLevel, windowBits, memLevel, dictionary); } else { return new JdkZlibEncoder(compressionLevel, dictionary); diff --git a/example/src/main/java/io/netty/example/http/websocketx/client/WebSocketClient.java b/example/src/main/java/io/netty/example/http/websocketx/client/WebSocketClient.java index 1c5f8af7eb..88a3bd840a 100644 --- a/example/src/main/java/io/netty/example/http/websocketx/client/WebSocketClient.java +++ b/example/src/main/java/io/netty/example/http/websocketx/client/WebSocketClient.java @@ -33,6 +33,7 @@ import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketVersion; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; @@ -113,6 +114,7 @@ public final class WebSocketClient { p.addLast( new HttpClientCodec(), new HttpObjectAggregator(8192), + new WebSocketClientCompressionHandler(), handler); } }); diff --git a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServerInitializer.java b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServerInitializer.java index f012004c2f..6c7ef4f85a 100644 --- a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServerInitializer.java +++ b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServerInitializer.java @@ -20,6 +20,7 @@ import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; import io.netty.handler.ssl.SslContext; /** @@ -40,6 +41,7 @@ public class WebSocketServerInitializer extends ChannelInitializer