From 7fc718db3cd52e82901dfc3fe8b437ae1ff75d4e Mon Sep 17 00:00:00 2001 From: ursa Date: Tue, 18 Jun 2019 09:05:58 +0100 Subject: [PATCH] WebSocket is closed without an error on protocol violations (#9116) Motivation: Incorrect WebSockets closure affects our production system. Enforced 'close socket on any protocol violation' prevents our custom termination sequence from execution. Huge number of parameters is a nightmare both in usage and in support (decoders configuration). Modification: Fix violations handling - send proper response codes. Fix for messages leak. Introduce decoder's option to disable default behavior (send close frame) on protocol violations. Encapsulate WebSocket response codes - WebSocketCloseStatus. Encapsulate decoder's configuration into a separate class - WebSocketDecoderConfig. Result: Fixes #8295. --- .../http/websocketx/CloseWebSocketFrame.java | 32 +- .../CorruptedWebSocketFrameException.java | 64 ++++ .../codec/http/websocketx/Utf8Validator.java | 9 +- .../websocketx/WebSocket00FrameDecoder.java | 14 +- .../websocketx/WebSocket07FrameDecoder.java | 25 +- .../websocketx/WebSocket08FrameDecoder.java | 96 ++++-- .../websocketx/WebSocket13FrameDecoder.java | 19 +- .../http/websocketx/WebSocketCloseStatus.java | 314 ++++++++++++++++++ .../websocketx/WebSocketDecoderConfig.java | 147 ++++++++ .../websocketx/WebSocketServerHandshaker.java | 39 ++- .../WebSocketServerHandshaker00.java | 23 +- .../WebSocketServerHandshaker07.java | 25 +- .../WebSocketServerHandshaker08.java | 31 +- .../WebSocketServerHandshaker13.java | 31 +- .../WebSocketServerHandshakerFactory.java | 41 ++- .../WebSocketServerProtocolHandler.java | 28 +- ...bSocketServerProtocolHandshakeHandler.java | 35 +- .../WebSocket08EncoderDecoderTest.java | 97 ++++-- .../websocketx/WebSocketCloseStatusTest.java | 127 +++++++ 19 files changed, 1039 insertions(+), 158 deletions(-) create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CorruptedWebSocketFrameException.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatus.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketDecoderConfig.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatusTest.java diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java index 4e3e9c7e93..d61af92632 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -33,7 +33,31 @@ public class CloseWebSocketFrame extends WebSocketFrame { } /** - * Creates a new empty close frame with closing getStatus code and reason text + * Creates a new empty close frame with closing status code and reason text + * + * @param status + * Status code as per RFC 6455. For + * example, 1000 indicates normal closure. + */ + public CloseWebSocketFrame(WebSocketCloseStatus status) { + this(status.code(), status.reasonText()); + } + + /** + * Creates a new empty close frame with closing status code and reason text + * + * @param status + * Status code as per RFC 6455. For + * example, 1000 indicates normal closure. + * @param reasonText + * Reason text. Set to null if no text. + */ + public CloseWebSocketFrame(WebSocketCloseStatus status, String reasonText) { + this(status.code(), reasonText); + } + + /** + * Creates a new empty close frame with closing status code and reason text * * @param statusCode * Integer status code as per RFC 6455. For @@ -46,7 +70,7 @@ public class CloseWebSocketFrame extends WebSocketFrame { } /** - * Creates a new close frame with no losing getStatus code and no reason text + * Creates a new close frame with no losing status code and no reason text * * @param finalFragment * flag indicating if this frame is the final fragment @@ -105,7 +129,7 @@ public class CloseWebSocketFrame extends WebSocketFrame { /** * Returns the closing status code as per RFC 6455. If - * a getStatus code is set, -1 is returned. + * a status code is set, -1 is returned. */ public int statusCode() { ByteBuf binaryData = content(); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CorruptedWebSocketFrameException.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CorruptedWebSocketFrameException.java new file mode 100644 index 0000000000..ceeec83b87 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CorruptedWebSocketFrameException.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019 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.handler.codec.CorruptedFrameException; +import io.netty.handler.codec.DecoderException; + +/** + * An {@link DecoderException} which is thrown when the received {@link WebSocketFrame} data could not be decoded by + * an inbound handler. + */ +public final class CorruptedWebSocketFrameException extends CorruptedFrameException { + + private static final long serialVersionUID = 3918055132492988338L; + + private final WebSocketCloseStatus closeStatus; + + /** + * Creates a new instance. + */ + public CorruptedWebSocketFrameException() { + this(WebSocketCloseStatus.PROTOCOL_ERROR, null, null); + } + + /** + * Creates a new instance. + */ + public CorruptedWebSocketFrameException(WebSocketCloseStatus status, String message, Throwable cause) { + super(message == null ? status.reasonText() : message, cause); + closeStatus = status; + } + + /** + * Creates a new instance. + */ + public CorruptedWebSocketFrameException(WebSocketCloseStatus status, String message) { + this(status, message, null); + } + + /** + * Creates a new instance. + */ + public CorruptedWebSocketFrameException(WebSocketCloseStatus status, Throwable cause) { + this(status, null, cause); + } + + public WebSocketCloseStatus closeStatus() { + return closeStatus; + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8Validator.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8Validator.java index 3a377e7c5b..be85dc2bac 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8Validator.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8Validator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -36,7 +36,6 @@ package io.netty.handler.codec.http.websocketx; import io.netty.buffer.ByteBuf; -import io.netty.handler.codec.CorruptedFrameException; import io.netty.util.ByteProcessor; /** @@ -79,7 +78,8 @@ final class Utf8Validator implements ByteProcessor { codep = 0; if (state != UTF8_ACCEPT) { state = UTF8_ACCEPT; - throw new CorruptedFrameException("bytes are not UTF-8"); + throw new CorruptedWebSocketFrameException( + WebSocketCloseStatus.INVALID_PAYLOAD_DATA, "bytes are not UTF-8"); } } @@ -93,7 +93,8 @@ final class Utf8Validator implements ByteProcessor { if (state == UTF8_REJECT) { checking = false; - throw new CorruptedFrameException("bytes are not UTF-8"); + throw new CorruptedWebSocketFrameException( + WebSocketCloseStatus.INVALID_PAYLOAD_DATA, "bytes are not UTF-8"); } return true; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java index 1f6bad5e5a..8a871b85f6 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ReplayingDecoder; import io.netty.handler.codec.TooLongFrameException; +import io.netty.util.internal.ObjectUtil; import java.util.List; @@ -52,6 +53,17 @@ public class WebSocket00FrameDecoder extends ReplayingDecoder implements W this.maxFrameSize = maxFrameSize; } + /** + * Creates a new instance of {@code WebSocketFrameDecoder} with the specified {@code maxFrameSize}. If the client + * sends a frame size larger than {@code maxFrameSize}, the channel will be closed. + * + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocket00FrameDecoder(WebSocketDecoderConfig decoderConfig) { + this.maxFrameSize = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig").maxFramePayloadLength(); + } + @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { // Discard all data received if closing handshake was received before. diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket07FrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket07FrameDecoder.java index 0ecb57128a..6aa2776467 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket07FrameDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket07FrameDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -71,7 +71,11 @@ public class WebSocket07FrameDecoder extends WebSocket08FrameDecoder { * helps check for denial of services attacks. */ public WebSocket07FrameDecoder(boolean expectMaskedFrames, boolean allowExtensions, int maxFramePayloadLength) { - this(expectMaskedFrames, allowExtensions, maxFramePayloadLength, false); + this(WebSocketDecoderConfig.newBuilder() + .expectMaskedFrames(expectMaskedFrames) + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .build()); } /** @@ -91,6 +95,21 @@ public class WebSocket07FrameDecoder extends WebSocket08FrameDecoder { */ public WebSocket07FrameDecoder(boolean expectMaskedFrames, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { - super(expectMaskedFrames, allowExtensions, maxFramePayloadLength, allowMaskMismatch); + this(WebSocketDecoderConfig.newBuilder() + .expectMaskedFrames(expectMaskedFrames) + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor + * + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocket07FrameDecoder(WebSocketDecoderConfig decoderConfig) { + super(decoderConfig); } } 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 05f7780f14..0bfcb48ba3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -58,8 +58,8 @@ import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; -import io.netty.handler.codec.CorruptedFrameException; import io.netty.handler.codec.TooLongFrameException; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -93,10 +93,7 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder private static final byte OPCODE_PING = 0x9; private static final byte OPCODE_PONG = 0xA; - private final long maxFramePayloadLength; - private final boolean allowExtensions; - private final boolean expectMaskedFrames; - private final boolean allowMaskMismatch; + private final WebSocketDecoderConfig config; private int fragmentedFramesCount; private boolean frameFinalFlag; @@ -142,10 +139,22 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder */ public WebSocket08FrameDecoder(boolean expectMaskedFrames, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { - this.expectMaskedFrames = expectMaskedFrames; - this.allowMaskMismatch = allowMaskMismatch; - this.allowExtensions = allowExtensions; - this.maxFramePayloadLength = maxFramePayloadLength; + this(WebSocketDecoderConfig.newBuilder() + .expectMaskedFrames(expectMaskedFrames) + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor + * + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocket08FrameDecoder(WebSocketDecoderConfig decoderConfig) { + this.config = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig"); } @Override @@ -184,13 +193,13 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder frameMasked = (b & 0x80) != 0; framePayloadLen1 = b & 0x7F; - if (frameRsv != 0 && !allowExtensions) { - protocolViolation(ctx, "RSV != 0 and no extension negotiated, RSV:" + frameRsv); + if (frameRsv != 0 && !config.allowExtensions()) { + protocolViolation(ctx, in, "RSV != 0 and no extension negotiated, RSV:" + frameRsv); return; } - if (!allowMaskMismatch && expectMaskedFrames != frameMasked) { - protocolViolation(ctx, "received a frame that is not masked as expected"); + if (!config.allowMaskMismatch() && config.expectMaskedFrames() != frameMasked) { + protocolViolation(ctx, in, "received a frame that is not masked as expected"); return; } @@ -198,20 +207,20 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder // control frames MUST NOT be fragmented if (!frameFinalFlag) { - protocolViolation(ctx, "fragmented control frame"); + protocolViolation(ctx, in, "fragmented control frame"); return; } // control frames MUST have payload 125 octets or less if (framePayloadLen1 > 125) { - protocolViolation(ctx, "control frame with payload length > 125 octets"); + protocolViolation(ctx, in, "control frame with payload length > 125 octets"); return; } // check for reserved control frame opcodes if (!(frameOpcode == OPCODE_CLOSE || frameOpcode == OPCODE_PING || frameOpcode == OPCODE_PONG)) { - protocolViolation(ctx, "control frame using reserved opcode " + frameOpcode); + protocolViolation(ctx, in, "control frame using reserved opcode " + frameOpcode); return; } @@ -219,26 +228,26 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder // body MUST be a 2-byte unsigned integer (in network byte // order) representing a getStatus code if (frameOpcode == 8 && framePayloadLen1 == 1) { - protocolViolation(ctx, "received close control frame with payload len 1"); + protocolViolation(ctx, in, "received close control frame with payload len 1"); return; } } else { // data frame // check for reserved data frame opcodes if (!(frameOpcode == OPCODE_CONT || frameOpcode == OPCODE_TEXT || frameOpcode == OPCODE_BINARY)) { - protocolViolation(ctx, "data frame using reserved opcode " + frameOpcode); + protocolViolation(ctx, in, "data frame using reserved opcode " + frameOpcode); return; } // check opcode vs message fragmentation state 1/2 if (fragmentedFramesCount == 0 && frameOpcode == OPCODE_CONT) { - protocolViolation(ctx, "received continuation data frame outside fragmented message"); + protocolViolation(ctx, in, "received continuation data frame outside fragmented message"); return; } // check opcode vs message fragmentation state 2/2 if (fragmentedFramesCount != 0 && frameOpcode != OPCODE_CONT && frameOpcode != OPCODE_PING) { - protocolViolation(ctx, + protocolViolation(ctx, in, "received non-continuation data frame while inside fragmented message"); return; } @@ -254,7 +263,7 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder } framePayloadLength = in.readUnsignedShort(); if (framePayloadLength < 126) { - protocolViolation(ctx, "invalid data frame length (not using minimal length encoding)"); + protocolViolation(ctx, in, "invalid data frame length (not using minimal length encoding)"); return; } } else if (framePayloadLen1 == 127) { @@ -266,15 +275,16 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder // just check if it's negative? if (framePayloadLength < 65536) { - protocolViolation(ctx, "invalid data frame length (not using minimal length encoding)"); + protocolViolation(ctx, in, "invalid data frame length (not using minimal length encoding)"); return; } } else { framePayloadLength = framePayloadLen1; } - if (framePayloadLength > maxFramePayloadLength) { - protocolViolation(ctx, "Max frame length of " + maxFramePayloadLength + " has been exceeded."); + if (framePayloadLength > config.maxFramePayloadLength()) { + protocolViolation(ctx, in, WebSocketCloseStatus.MESSAGE_TOO_BIG, + "Max frame length of " + config.maxFramePayloadLength() + " has been exceeded."); return; } @@ -408,18 +418,33 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder } } - private void protocolViolation(ChannelHandlerContext ctx, String reason) { - protocolViolation(ctx, new CorruptedFrameException(reason)); + private void protocolViolation(ChannelHandlerContext ctx, ByteBuf in, String reason) { + protocolViolation(ctx, in, WebSocketCloseStatus.PROTOCOL_ERROR, reason); } - private void protocolViolation(ChannelHandlerContext ctx, CorruptedFrameException ex) { + private void protocolViolation(ChannelHandlerContext ctx, ByteBuf in, WebSocketCloseStatus status, String reason) { + protocolViolation(ctx, in, new CorruptedWebSocketFrameException(status, reason)); + } + + private void protocolViolation(ChannelHandlerContext ctx, ByteBuf in, CorruptedWebSocketFrameException ex) { state = State.CORRUPT; - if (ctx.channel().isActive()) { + int readableBytes = in.readableBytes(); + if (readableBytes > 0) { + // Fix for memory leak, caused by ByteToMessageDecoder#channelRead: + // buffer 'cumulation' is released ONLY when no more readable bytes available. + in.skipBytes(readableBytes); + } + if (ctx.channel().isActive() && config.closeOnProtocolViolation()) { Object closeMessage; if (receivedClosingHandshake) { closeMessage = Unpooled.EMPTY_BUFFER; } else { - closeMessage = new CloseWebSocketFrame(1002, null); + WebSocketCloseStatus closeStatus = ex.closeStatus(); + String reasonText = ex.getMessage(); + if (reasonText == null) { + reasonText = closeStatus.reasonText(); + } + closeMessage = new CloseWebSocketFrame(closeStatus, reasonText); } ctx.writeAndFlush(closeMessage).addListener(ChannelFutureListener.CLOSE); } @@ -441,7 +466,7 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder return; } if (buffer.readableBytes() == 1) { - protocolViolation(ctx, "Invalid close frame body"); + protocolViolation(ctx, buffer, WebSocketCloseStatus.INVALID_PAYLOAD_DATA, "Invalid close frame body"); } // Save reader index @@ -450,17 +475,16 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder // Must have 2 byte integer within the valid range int statusCode = buffer.readShort(); - if (statusCode >= 0 && statusCode <= 999 || statusCode >= 1004 && statusCode <= 1006 - || statusCode >= 1015 && statusCode <= 2999) { - protocolViolation(ctx, "Invalid close frame getStatus code: " + statusCode); + if (!WebSocketCloseStatus.isValidStatusCode(statusCode)) { + protocolViolation(ctx, buffer, "Invalid close frame getStatus code: " + statusCode); } // May have UTF-8 message if (buffer.isReadable()) { try { new Utf8Validator().check(buffer); - } catch (CorruptedFrameException ex) { - protocolViolation(ctx, ex); + } catch (CorruptedWebSocketFrameException ex) { + protocolViolation(ctx, buffer, ex); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java index 04fd68295c..0c4a9b14e2 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -91,6 +91,21 @@ public class WebSocket13FrameDecoder extends WebSocket08FrameDecoder { */ public WebSocket13FrameDecoder(boolean expectMaskedFrames, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { - super(expectMaskedFrames, allowExtensions, maxFramePayloadLength, allowMaskMismatch); + this(WebSocketDecoderConfig.newBuilder() + .expectMaskedFrames(expectMaskedFrames) + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor + * + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocket13FrameDecoder(WebSocketDecoderConfig decoderConfig) { + super(decoderConfig); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatus.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatus.java new file mode 100644 index 0000000000..3069613fda --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatus.java @@ -0,0 +1,314 @@ +/* + * Copyright 2019 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 static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * WebSocket status codes specified in RFC-6455. + *
+ *
+ * RFC-6455 The WebSocket Protocol, December 2011:
+ * https://tools.ietf.org/html/rfc6455#section-7.4.1
+ *
+ * WebSocket Protocol Registries, April 2019:
+ * https://www.iana.org/assignments/websocket/websocket.xhtml
+ *
+ * 7.4.1.  Defined Status Codes
+ *
+ * Endpoints MAY use the following pre-defined status codes when sending
+ * a Close frame.
+ *
+ * 1000
+ *
+ *    1000 indicates a normal closure, meaning that the purpose for
+ *    which the connection was established has been fulfilled.
+ *
+ * 1001
+ *
+ *    1001 indicates that an endpoint is "going away", such as a server
+ *    going down or a browser having navigated away from a page.
+ *
+ * 1002
+ *
+ *    1002 indicates that an endpoint is terminating the connection due
+ *    to a protocol error.
+ *
+ * 1003
+ *
+ *    1003 indicates that an endpoint is terminating the connection
+ *    because it has received a type of data it cannot accept (e.g., an
+ *    endpoint that understands only text data MAY send this if it
+ *    receives a binary message).
+ *
+ * 1004
+ *
+ *    Reserved. The specific meaning might be defined in the future.
+ *
+ * 1005
+ *
+ *    1005 is a reserved value and MUST NOT be set as a status code in a
+ *    Close control frame by an endpoint. It is designated for use in
+ *    applications expecting a status code to indicate that no status
+ *    code was actually present.
+ *
+ * 1006
+ *
+ *    1006 is a reserved value and MUST NOT be set as a status code in a
+ *    Close control frame by an endpoint. It is designated for use in
+ *    applications expecting a status code to indicate that the
+ *    connection was closed abnormally, e.g., without sending or
+ *    receiving a Close control frame.
+ *
+ * 1007
+ *
+ *    1007 indicates that an endpoint is terminating the connection
+ *    because it has received data within a message that was not
+ *    consistent with the type of the message (e.g., non-UTF-8 [RFC3629]
+ *    data within a text message).
+ *
+ * 1008
+ *
+ *    1008 indicates that an endpoint is terminating the connection
+ *    because it has received a message that violates its policy. This
+ *    is a generic status code that can be returned when there is no
+ *    other more suitable status code (e.g., 1003 or 1009) or if there
+ *    is a need to hide specific details about the policy.
+ *
+ * 1009
+ *
+ *    1009 indicates that an endpoint is terminating the connection
+ *    because it has received a message that is too big for it to
+ *    process.
+ *
+ * 1010
+ *
+ *    1010 indicates that an endpoint (client) is terminating the
+ *    connection because it has expected the server to negotiate one or
+ *    more extension, but the server didn't return them in the response
+ *    message of the WebSocket handshake. The list of extensions that
+ *    are needed SHOULD appear in the /reason/ part of the Close frame.
+ *    Note that this status code is not used by the server, because it
+ *    can fail the WebSocket handshake instead.
+ *
+ * 1011
+ *
+ *    1011 indicates that a server is terminating the connection because
+ *    it encountered an unexpected condition that prevented it from
+ *    fulfilling the request.
+ *
+ * 1012 (IANA Registry, Non RFC-6455)
+ *
+ *    1012 indicates that the service is restarted. a client may reconnect,
+ *    and if it choses to do, should reconnect using a randomized delay
+ *    of 5 - 30 seconds.
+ *
+ * 1013 (IANA Registry, Non RFC-6455)
+ *
+ *    1013 indicates that the service is experiencing overload. a client
+ *    should only connect to a different IP (when there are multiple for the
+ *    target) or reconnect to the same IP upon user action.
+ *
+ * 1014 (IANA Registry, Non RFC-6455)
+ *
+ *    The server was acting as a gateway or proxy and received an invalid
+ *    response from the upstream server. This is similar to 502 HTTP Status Code.
+ *
+ * 1015
+ *
+ *    1015 is a reserved value and MUST NOT be set as a status code in a
+ *    Close control frame by an endpoint. It is designated for use in
+ *    applications expecting a status code to indicate that the
+ *    connection was closed due to a failure to perform a TLS handshake
+ *    (e.g., the server certificate can't be verified).
+ *
+ *
+ * 7.4.2. Reserved Status Code Ranges
+ *
+ * 0-999
+ *
+ *    Status codes in the range 0-999 are not used.
+ *
+ * 1000-2999
+ *
+ *    Status codes in the range 1000-2999 are reserved for definition by
+ *    this protocol, its future revisions, and extensions specified in a
+ *    permanent and readily available public specification.
+ *
+ * 3000-3999
+ *
+ *    Status codes in the range 3000-3999 are reserved for use by
+ *    libraries, frameworks, and applications. These status codes are
+ *    registered directly with IANA. The interpretation of these codes
+ *    is undefined by this protocol.
+ *
+ * 4000-4999
+ *
+ *    Status codes in the range 4000-4999 are reserved for private use
+ *    and thus can't be registered. Such codes can be used by prior
+ *    agreements between WebSocket applications. The interpretation of
+ *    these codes is undefined by this protocol.
+ * 
+ *

+ * While {@link WebSocketCloseStatus} is enum-like structure, its instances should NOT be compared by reference. + * Instead, either {@link #equals(Object)} should be used or direct comparison of {@link #code()} value. + */ +public final class WebSocketCloseStatus implements Comparable { + + public static final WebSocketCloseStatus NORMAL_CLOSURE = + new WebSocketCloseStatus(1000, "Bye"); + + public static final WebSocketCloseStatus ENDPOINT_UNAVAILABLE = + new WebSocketCloseStatus(1001, "Endpoint unavailable"); + + public static final WebSocketCloseStatus PROTOCOL_ERROR = + new WebSocketCloseStatus(1002, "Protocol error"); + + public static final WebSocketCloseStatus INVALID_MESSAGE_TYPE = + new WebSocketCloseStatus(1003, "Invalid message type"); + + public static final WebSocketCloseStatus INVALID_PAYLOAD_DATA = + new WebSocketCloseStatus(1007, "Invalid payload data"); + + public static final WebSocketCloseStatus POLICY_VIOLATION = + new WebSocketCloseStatus(1008, "Policy violation"); + + public static final WebSocketCloseStatus MESSAGE_TOO_BIG = + new WebSocketCloseStatus(1009, "Message too big"); + + public static final WebSocketCloseStatus MANDATORY_EXTENSION = + new WebSocketCloseStatus(1010, "Mandatory extension"); + + public static final WebSocketCloseStatus INTERNAL_SERVER_ERROR = + new WebSocketCloseStatus(1011, "Internal server error"); + + public static final WebSocketCloseStatus SERVICE_RESTART = + new WebSocketCloseStatus(1012, "Service Restart"); + + public static final WebSocketCloseStatus TRY_AGAIN_LATER = + new WebSocketCloseStatus(1013, "Try Again Later"); + + public static final WebSocketCloseStatus BAD_GATEWAY = + new WebSocketCloseStatus(1014, "Bad Gateway"); + + // 1004, 1005, 1006, 1015 are reserved and should never be used by user + //public static final WebSocketCloseStatus SPECIFIC_MEANING = register(1004, "..."); + //public static final WebSocketCloseStatus EMPTY = register(1005, "Empty"); + //public static final WebSocketCloseStatus ABNORMAL_CLOSURE = register(1006, "Abnormal closure"); + //public static final WebSocketCloseStatus TLS_HANDSHAKE_FAILED(1015, "TLS handshake failed"); + + private final int statusCode; + private final String reasonText; + private String text; + + public WebSocketCloseStatus(int statusCode, String reasonText) { + if (!isValidStatusCode(statusCode)) { + throw new IllegalArgumentException( + "WebSocket close status code does NOT comply with RFC-6455: " + statusCode); + } + this.statusCode = statusCode; + this.reasonText = checkNotNull(reasonText, "reasonText"); + } + + public int code() { + return statusCode; + } + + public String reasonText() { + return reasonText; + } + + /** + * Order of {@link WebSocketCloseStatus} only depends on {@link #code()}. + */ + @Override + public int compareTo(WebSocketCloseStatus o) { + return code() - o.code(); + } + + /** + * Equality of {@link WebSocketCloseStatus} only depends on {@link #code()}. + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (null == o || getClass() != o.getClass()) { + return false; + } + + WebSocketCloseStatus that = (WebSocketCloseStatus) o; + + return statusCode == that.statusCode; + } + + @Override + public int hashCode() { + return statusCode; + } + + @Override + public String toString() { + String text = this.text; + if (text == null) { + // E.g.: "1000 Bye", "1009 Message too big" + this.text = text = code() + " " + reasonText(); + } + return text; + } + + public static boolean isValidStatusCode(int code) { + return code < 0 || + 1000 <= code && code <= 1003 || + 1007 <= code && code <= 1014 || + 3000 <= code; + } + + public static WebSocketCloseStatus valueOf(int code) { + switch (code) { + case 1000: + return NORMAL_CLOSURE; + case 1001: + return ENDPOINT_UNAVAILABLE; + case 1002: + return PROTOCOL_ERROR; + case 1003: + return INVALID_MESSAGE_TYPE; + case 1007: + return INVALID_PAYLOAD_DATA; + case 1008: + return POLICY_VIOLATION; + case 1009: + return MESSAGE_TOO_BIG; + case 1010: + return MANDATORY_EXTENSION; + case 1011: + return INTERNAL_SERVER_ERROR; + case 1012: + return SERVICE_RESTART; + case 1013: + return TRY_AGAIN_LATER; + case 1014: + return BAD_GATEWAY; + default: + return new WebSocketCloseStatus(code, "Close status #" + code); + } + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketDecoderConfig.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketDecoderConfig.java new file mode 100644 index 0000000000..d539458890 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketDecoderConfig.java @@ -0,0 +1,147 @@ +/* + * Copyright 2019 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.util.internal.ObjectUtil; + +/** + * Frames decoder configuration. + */ +public final class WebSocketDecoderConfig { + + private final int maxFramePayloadLength; + private final boolean expectMaskedFrames; + private final boolean allowMaskMismatch; + private final boolean allowExtensions; + private final boolean closeOnProtocolViolation; + + /** + * Constructor + * + * @param maxFramePayloadLength + * Maximum length of a frame's payload. Setting this to an appropriate value for you application + * helps check for denial of services attacks. + * @param expectMaskedFrames + * Web socket servers must set this to true processed incoming masked payload. Client implementations + * must set this to false. + * @param allowMaskMismatch + * Allows to loosen the masking requirement on received frames. When this is set to false then also + * frames which are not masked properly according to the standard will still be accepted. + * @param allowExtensions + * Flag to allow reserved extension bits to be used or not + * @param closeOnProtocolViolation + * Flag to send close frame immediately on any protocol violation.ion. + */ + private WebSocketDecoderConfig(int maxFramePayloadLength, boolean expectMaskedFrames, boolean allowMaskMismatch, + boolean allowExtensions, boolean closeOnProtocolViolation) { + this.maxFramePayloadLength = maxFramePayloadLength; + this.expectMaskedFrames = expectMaskedFrames; + this.allowMaskMismatch = allowMaskMismatch; + this.allowExtensions = allowExtensions; + this.closeOnProtocolViolation = closeOnProtocolViolation; + } + + public int maxFramePayloadLength() { + return maxFramePayloadLength; + } + + public boolean expectMaskedFrames() { + return expectMaskedFrames; + } + + public boolean allowMaskMismatch() { + return allowMaskMismatch; + } + + public boolean allowExtensions() { + return allowExtensions; + } + + public boolean closeOnProtocolViolation() { + return closeOnProtocolViolation; + } + + @Override + public String toString() { + return "WebSocketDecoderConfig" + + " [maxFramePayloadLength=" + maxFramePayloadLength + + ", expectMaskedFrames=" + expectMaskedFrames + + ", allowMaskMismatch=" + allowMaskMismatch + + ", allowExtensions=" + allowExtensions + + ", closeOnProtocolViolation=" + closeOnProtocolViolation + + "]"; + } + + public Builder toBuilder() { + return new Builder(this); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder { + private int maxFramePayloadLength = 65536; + private boolean expectMaskedFrames = true; + private boolean allowMaskMismatch; + private boolean allowExtensions; + private boolean closeOnProtocolViolation = true; + + private Builder() { + /* No-op */ + } + + private Builder(WebSocketDecoderConfig decoderConfig) { + ObjectUtil.checkNotNull(decoderConfig, "decoderConfig"); + maxFramePayloadLength = decoderConfig.maxFramePayloadLength(); + expectMaskedFrames = decoderConfig.expectMaskedFrames(); + allowMaskMismatch = decoderConfig.allowMaskMismatch(); + allowExtensions = decoderConfig.allowExtensions(); + closeOnProtocolViolation = decoderConfig.closeOnProtocolViolation(); + } + + public Builder maxFramePayloadLength(int maxFramePayloadLength) { + this.maxFramePayloadLength = maxFramePayloadLength; + return this; + } + + public Builder expectMaskedFrames(boolean expectMaskedFrames) { + this.expectMaskedFrames = expectMaskedFrames; + return this; + } + + public Builder allowMaskMismatch(boolean allowMaskMismatch) { + this.allowMaskMismatch = allowMaskMismatch; + return this; + } + + public Builder allowExtensions(boolean allowExtensions) { + this.allowExtensions = allowExtensions; + return this; + } + + public Builder closeOnProtocolViolation(boolean closeOnProtocolViolation) { + this.closeOnProtocolViolation = closeOnProtocolViolation; + return this; + } + + public WebSocketDecoderConfig build() { + return new WebSocketDecoderConfig( + maxFramePayloadLength, expectMaskedFrames, allowMaskMismatch, + allowExtensions, closeOnProtocolViolation); + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java index ce2afa311c..10e8a0872d 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -33,7 +33,7 @@ import io.netty.handler.codec.http.HttpResponseEncoder; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.util.ReferenceCountUtil; import io.netty.util.internal.EmptyArrays; -import io.netty.util.internal.ThrowableUtil; +import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -54,7 +54,7 @@ public abstract class WebSocketServerHandshaker { private final WebSocketVersion version; - private final int maxFramePayloadLength; + private final WebSocketDecoderConfig decoderConfig; private String selectedSubprotocol; @@ -79,6 +79,26 @@ public abstract class WebSocketServerHandshaker { protected WebSocketServerHandshaker( WebSocketVersion version, String uri, String subprotocols, int maxFramePayloadLength) { + this(version, uri, subprotocols, WebSocketDecoderConfig.newBuilder() + .maxFramePayloadLength(maxFramePayloadLength) + .build()); + } + + /** + * Constructor specifying the destination web socket location + * + * @param version + * the protocol version + * @param uri + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param subprotocols + * CSV of supported protocols. Null if sub protocols not supported. + * @param decoderConfig + * Frames decoder configuration. + */ + protected WebSocketServerHandshaker( + WebSocketVersion version, String uri, String subprotocols, WebSocketDecoderConfig decoderConfig) { this.version = version; this.uri = uri; if (subprotocols != null) { @@ -90,7 +110,7 @@ public abstract class WebSocketServerHandshaker { } else { this.subprotocols = EmptyArrays.EMPTY_STRINGS; } - this.maxFramePayloadLength = maxFramePayloadLength; + this.decoderConfig = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig"); } /** @@ -122,7 +142,16 @@ public abstract class WebSocketServerHandshaker { * @return The maximum length for a frame's payload */ public int maxFramePayloadLength() { - return maxFramePayloadLength; + return decoderConfig.maxFramePayloadLength(); + } + + /** + * Gets this decoder configuration. + * + * @return This decoder configuration. + */ + public WebSocketDecoderConfig decoderConfig() { + return decoderConfig; } /** diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java index 994cd09103..c3e50ee152 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -60,7 +60,24 @@ public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker { * reduce denial of service attacks using long data frames. */ public WebSocketServerHandshaker00(String webSocketURL, String subprotocols, int maxFramePayloadLength) { - super(WebSocketVersion.V00, webSocketURL, subprotocols, maxFramePayloadLength); + this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder() + .maxFramePayloadLength(maxFramePayloadLength) + .build()); + } + + /** + * Constructor specifying the destination web socket location + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param subprotocols + * CSV of supported protocols + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocketServerHandshaker00(String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) { + super(WebSocketVersion.V00, webSocketURL, subprotocols, decoderConfig); } /** @@ -189,7 +206,7 @@ public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker { @Override protected WebSocketFrameDecoder newWebsocketDecoder() { - return new WebSocket00FrameDecoder(maxFramePayloadLength()); + return new WebSocket00FrameDecoder(decoderConfig()); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker07.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker07.java index 6529109c7f..1ab627d74d 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker07.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker07.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -37,9 +37,6 @@ public class WebSocketServerHandshaker07 extends WebSocketServerHandshaker { public static final String WEBSOCKET_07_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - private final boolean allowExtensions; - private final boolean allowMaskMismatch; - /** * Constructor specifying the destination web socket location * @@ -79,9 +76,21 @@ public class WebSocketServerHandshaker07 extends WebSocketServerHandshaker { public WebSocketServerHandshaker07( String webSocketURL, String subprotocols, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { - super(WebSocketVersion.V07, webSocketURL, subprotocols, maxFramePayloadLength); - this.allowExtensions = allowExtensions; - this.allowMaskMismatch = allowMaskMismatch; + this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder() + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor specifying the destination web socket location + * + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocketServerHandshaker07(String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) { + super(WebSocketVersion.V07, webSocketURL, subprotocols, decoderConfig); } /** @@ -159,7 +168,7 @@ public class WebSocketServerHandshaker07 extends WebSocketServerHandshaker { @Override protected WebSocketFrameDecoder newWebsocketDecoder() { - return new WebSocket07FrameDecoder(true, allowExtensions, maxFramePayloadLength(), allowMaskMismatch); + return new WebSocket07FrameDecoder(decoderConfig()); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker08.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker08.java index f0b58f8bca..e81dcc9fa4 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker08.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker08.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -37,9 +37,6 @@ public class WebSocketServerHandshaker08 extends WebSocketServerHandshaker { public static final String WEBSOCKET_08_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - private final boolean allowExtensions; - private final boolean allowMaskMismatch; - /** * Constructor specifying the destination web socket location * @@ -79,9 +76,27 @@ public class WebSocketServerHandshaker08 extends WebSocketServerHandshaker { public WebSocketServerHandshaker08( String webSocketURL, String subprotocols, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { - super(WebSocketVersion.V08, webSocketURL, subprotocols, maxFramePayloadLength); - this.allowExtensions = allowExtensions; - this.allowMaskMismatch = allowMaskMismatch; + this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder() + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor specifying the destination web socket location + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". + * Subsequent web socket frames will be sent to this URL. + * @param subprotocols + * CSV of supported protocols + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocketServerHandshaker08( + String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) { + super(WebSocketVersion.V08, webSocketURL, subprotocols, decoderConfig); } /** @@ -158,7 +173,7 @@ public class WebSocketServerHandshaker08 extends WebSocketServerHandshaker { @Override protected WebSocketFrameDecoder newWebsocketDecoder() { - return new WebSocket08FrameDecoder(true, allowExtensions, maxFramePayloadLength(), allowMaskMismatch); + return new WebSocket08FrameDecoder(decoderConfig()); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13.java index f36d06ca99..af139c7616 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -36,9 +36,6 @@ public class WebSocketServerHandshaker13 extends WebSocketServerHandshaker { public static final String WEBSOCKET_13_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - private final boolean allowExtensions; - private final boolean allowMaskMismatch; - /** * Constructor specifying the destination web socket location * @@ -78,9 +75,27 @@ public class WebSocketServerHandshaker13 extends WebSocketServerHandshaker { public WebSocketServerHandshaker13( String webSocketURL, String subprotocols, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { - super(WebSocketVersion.V13, webSocketURL, subprotocols, maxFramePayloadLength); - this.allowExtensions = allowExtensions; - this.allowMaskMismatch = allowMaskMismatch; + this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder() + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor specifying the destination web socket location + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web + * socket frames will be sent to this URL. + * @param subprotocols + * CSV of supported protocols + * @param decoderConfig + * Frames decoder configuration. + */ + public WebSocketServerHandshaker13( + String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) { + super(WebSocketVersion.V13, webSocketURL, subprotocols, decoderConfig); } /** @@ -156,7 +171,7 @@ public class WebSocketServerHandshaker13 extends WebSocketServerHandshaker { @Override protected WebSocketFrameDecoder newWebsocketDecoder() { - return new WebSocket13FrameDecoder(true, allowExtensions, maxFramePayloadLength(), allowMaskMismatch); + return new WebSocket13FrameDecoder(decoderConfig()); } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java index 27fdfa0259..02c4edb95f 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -25,6 +25,7 @@ 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.internal.ObjectUtil; /** * Auto-detects the version of the Web Socket protocol in use and creates a new proper @@ -36,11 +37,7 @@ public class WebSocketServerHandshakerFactory { private final String subprotocols; - private final boolean allowExtensions; - - private final int maxFramePayloadLength; - - private final boolean allowMaskMismatch; + private final WebSocketDecoderConfig decoderConfig; /** * Constructor specifying the destination web socket location @@ -98,11 +95,29 @@ public class WebSocketServerHandshakerFactory { public WebSocketServerHandshakerFactory( String webSocketURL, String subprotocols, boolean allowExtensions, int maxFramePayloadLength, boolean allowMaskMismatch) { + this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder() + .allowExtensions(allowExtensions) + .maxFramePayloadLength(maxFramePayloadLength) + .allowMaskMismatch(allowMaskMismatch) + .build()); + } + + /** + * Constructor specifying the destination web socket location + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". + * Subsequent web socket frames will be sent to this URL. + * @param subprotocols + * CSV of supported protocols. Null if sub protocols not supported. + * @param decoderConfig + * Frames decoder options. + */ + public WebSocketServerHandshakerFactory( + String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) { this.webSocketURL = webSocketURL; this.subprotocols = subprotocols; - this.allowExtensions = allowExtensions; - this.maxFramePayloadLength = maxFramePayloadLength; - this.allowMaskMismatch = allowMaskMismatch; + this.decoderConfig = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig"); } /** @@ -118,21 +133,21 @@ public class WebSocketServerHandshakerFactory { if (version.equals(WebSocketVersion.V13.toHttpHeaderValue())) { // Version 13 of the wire protocol - RFC 6455 (version 17 of the draft hybi specification). return new WebSocketServerHandshaker13( - webSocketURL, subprotocols, allowExtensions, maxFramePayloadLength, allowMaskMismatch); + webSocketURL, subprotocols, decoderConfig); } else if (version.equals(WebSocketVersion.V08.toHttpHeaderValue())) { // Version 8 of the wire protocol - version 10 of the draft hybi specification. return new WebSocketServerHandshaker08( - webSocketURL, subprotocols, allowExtensions, maxFramePayloadLength, allowMaskMismatch); + webSocketURL, subprotocols, decoderConfig); } else if (version.equals(WebSocketVersion.V07.toHttpHeaderValue())) { // Version 8 of the wire protocol - version 07 of the draft hybi specification. return new WebSocketServerHandshaker07( - webSocketURL, subprotocols, allowExtensions, maxFramePayloadLength, allowMaskMismatch); + webSocketURL, subprotocols, decoderConfig); } else { return null; } } else { // Assume version 00 where version header was not specified - return new WebSocketServerHandshaker00(webSocketURL, subprotocols, maxFramePayloadLength); + return new WebSocketServerHandshaker00(webSocketURL, subprotocols, decoderConfig); } } 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 cffcffd74c..9e66b04c06 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -107,11 +107,9 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler { private final String websocketPath; private final String subprotocols; - private final boolean allowExtensions; - private final int maxFramePayloadLength; - private final boolean allowMaskMismatch; private final boolean checkStartsWith; private final long handshakeTimeoutMillis; + private final WebSocketDecoderConfig decoderConfig; public WebSocketServerProtocolHandler(String websocketPath) { this(websocketPath, DEFAULT_HANDSHAKE_TIMEOUT_MS); @@ -191,14 +189,23 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler { public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith, boolean dropPongFrames, long handshakeTimeoutMillis) { + this(websocketPath, subprotocols, checkStartsWith, dropPongFrames, handshakeTimeoutMillis, + WebSocketDecoderConfig.newBuilder() + .maxFramePayloadLength(maxFrameSize) + .allowMaskMismatch(allowMaskMismatch) + .allowExtensions(allowExtensions) + .build()); + } + + public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean checkStartsWith, + boolean dropPongFrames, long handshakeTimeoutMillis, + WebSocketDecoderConfig decoderConfig) { super(dropPongFrames); this.websocketPath = websocketPath; this.subprotocols = subprotocols; - this.allowExtensions = allowExtensions; - maxFramePayloadLength = maxFrameSize; - this.allowMaskMismatch = allowMaskMismatch; this.checkStartsWith = checkStartsWith; this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis"); + this.decoderConfig = checkNotNull(decoderConfig, "decoderConfig"); } @Override @@ -207,11 +214,8 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler { if (cp.get(WebSocketServerProtocolHandshakeHandler.class) == null) { // Add the WebSocketHandshakeHandler before this one. ctx.pipeline().addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(), - new WebSocketServerProtocolHandshakeHandler(websocketPath, subprotocols, - allowExtensions, maxFramePayloadLength, - allowMaskMismatch, - checkStartsWith, - handshakeTimeoutMillis)); + new WebSocketServerProtocolHandshakeHandler( + websocketPath, subprotocols, checkStartsWith, handshakeTimeoutMillis, decoderConfig)); } if (cp.get(Utf8FrameValidator.class) == null) { // Add the UFT8 checking before this one. diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandshakeHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandshakeHandler.java index 08a82ce3d8..6479975626 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandshakeHandler.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandshakeHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2019 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 @@ -48,43 +48,19 @@ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapt private final String websocketPath; private final String subprotocols; - private final boolean allowExtensions; - private final int maxFramePayloadSize; - private final boolean allowMaskMismatch; private final boolean checkStartsWith; private final long handshakeTimeoutMillis; + private final WebSocketDecoderConfig decoderConfig; private ChannelHandlerContext ctx; private ChannelPromise handshakePromise; WebSocketServerProtocolHandshakeHandler(String websocketPath, String subprotocols, - boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch) { - this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, - DEFAULT_HANDSHAKE_TIMEOUT_MS); - } - - WebSocketServerProtocolHandshakeHandler(String websocketPath, String subprotocols, - boolean allowExtensions, int maxFrameSize, - boolean allowMaskMismatch, long handshakeTimeoutMillis) { - this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, - false, handshakeTimeoutMillis); - } - - WebSocketServerProtocolHandshakeHandler(String websocketPath, String subprotocols, - boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith) { - this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, - checkStartsWith, DEFAULT_HANDSHAKE_TIMEOUT_MS); - } - - WebSocketServerProtocolHandshakeHandler(String websocketPath, String subprotocols, - boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, - boolean checkStartsWith, long handshakeTimeoutMillis) { + boolean checkStartsWith, long handshakeTimeoutMillis, WebSocketDecoderConfig decoderConfig) { this.websocketPath = websocketPath; this.subprotocols = subprotocols; - this.allowExtensions = allowExtensions; - maxFramePayloadSize = maxFrameSize; - this.allowMaskMismatch = allowMaskMismatch; this.checkStartsWith = checkStartsWith; this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis"); + this.decoderConfig = checkNotNull(decoderConfig, "decoderConfig"); } @Override @@ -108,8 +84,7 @@ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapt } final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( - getWebSocketLocation(ctx.pipeline(), req, websocketPath), subprotocols, - allowExtensions, maxFramePayloadSize, allowMaskMismatch); + getWebSocketLocation(ctx.pipeline(), req, websocketPath), subprotocols, decoderConfig); final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req); final ChannelPromise localHandshakePromise = handshakePromise; if (handshaker == null) { diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocket08EncoderDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocket08EncoderDecoderTest.java index a84a81a2f7..ab032f64d8 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocket08EncoderDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocket08EncoderDecoderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 The Netty Project + * Copyright 2019 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 @@ -53,6 +53,67 @@ public class WebSocket08EncoderDecoderTest { strTestData = s.toString(); } + @Test + public void testWebSocketProtocolViolation() { + // Given + initTestData(); + + int maxPayloadLength = 255; + String errorMessage = "Max frame length of " + maxPayloadLength + " has been exceeded."; + WebSocketCloseStatus expectedStatus = WebSocketCloseStatus.MESSAGE_TOO_BIG; + + // With auto-close + WebSocketDecoderConfig config = WebSocketDecoderConfig.newBuilder() + .maxFramePayloadLength(maxPayloadLength) + .closeOnProtocolViolation(true) + .build(); + EmbeddedChannel inChannel = new EmbeddedChannel(new WebSocket08FrameDecoder(config)); + EmbeddedChannel outChannel = new EmbeddedChannel(new WebSocket08FrameEncoder(true)); + + executeProtocolViolationTest(outChannel, inChannel, maxPayloadLength + 1, expectedStatus, errorMessage); + + CloseWebSocketFrame response = inChannel.readOutbound(); + Assert.assertNotNull(response); + Assert.assertEquals(expectedStatus.code(), response.statusCode()); + Assert.assertEquals(errorMessage, response.reasonText()); + + // Without auto-close + config = WebSocketDecoderConfig.newBuilder() + .maxFramePayloadLength(maxPayloadLength) + .closeOnProtocolViolation(false) + .build(); + inChannel = new EmbeddedChannel(new WebSocket08FrameDecoder(config)); + outChannel = new EmbeddedChannel(new WebSocket08FrameEncoder(true)); + + executeProtocolViolationTest(outChannel, inChannel, maxPayloadLength + 1, expectedStatus, errorMessage); + + response = inChannel.readOutbound(); + Assert.assertNull(response); + + // Release test data + binTestData.release(); + Assert.assertFalse(inChannel.finish()); + Assert.assertFalse(outChannel.finish()); + } + + private void executeProtocolViolationTest(EmbeddedChannel outChannel, EmbeddedChannel inChannel, + int testDataLength, WebSocketCloseStatus expectedStatus, String errorMessage) { + CorruptedWebSocketFrameException corrupted = null; + + try { + testBinaryWithLen(outChannel, inChannel, testDataLength); + } catch (CorruptedWebSocketFrameException e) { + corrupted = e; + } + + BinaryWebSocketFrame exceedingFrame = inChannel.readInbound(); + Assert.assertNull(exceedingFrame); + + Assert.assertNotNull(corrupted); + Assert.assertEquals(expectedStatus, corrupted.closeStatus()); + Assert.assertEquals(errorMessage, corrupted.getMessage()); + } + @Test public void testWebSocketEncodingAndDecoding() { initTestData(); @@ -108,16 +169,7 @@ public class WebSocket08EncoderDecoderTest { String testStr = strTestData.substring(0, testDataLength); outChannel.writeOutbound(new TextWebSocketFrame(testStr)); - // Transfer encoded data into decoder - // Loop because there might be multiple frames (gathering write) - while (true) { - ByteBuf encoded = outChannel.readOutbound(); - if (encoded != null) { - inChannel.writeInbound(encoded); - } else { - break; - } - } + transfer(outChannel, inChannel); Object decoded = inChannel.readInbound(); Assert.assertNotNull(decoded); @@ -132,16 +184,7 @@ public class WebSocket08EncoderDecoderTest { binTestData.setIndex(0, testDataLength); // Send only len bytes outChannel.writeOutbound(new BinaryWebSocketFrame(binTestData)); - // Transfer encoded data into decoder - // Loop because there might be multiple frames (gathering write) - while (true) { - ByteBuf encoded = outChannel.readOutbound(); - if (encoded != null) { - inChannel.writeInbound(encoded); - } else { - break; - } - } + transfer(outChannel, inChannel); Object decoded = inChannel.readInbound(); Assert.assertNotNull(decoded); @@ -154,4 +197,16 @@ public class WebSocket08EncoderDecoderTest { } binFrame.release(); } + + private void transfer(EmbeddedChannel outChannel, EmbeddedChannel inChannel) { + // Transfer encoded data into decoder + // Loop because there might be multiple frames (gathering write) + for (;;) { + ByteBuf encoded = outChannel.readOutbound(); + if (encoded == null) { + return; + } + inChannel.writeInbound(encoded); + } + } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatusTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatusTest.java new file mode 100644 index 0000000000..05cb07d62f --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatusTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2019 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 java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Test; + +import static io.netty.handler.codec.http.websocketx.WebSocketCloseStatus.*; + +public class WebSocketCloseStatusTest { + + private final List validCodes = Arrays.asList( + NORMAL_CLOSURE, + ENDPOINT_UNAVAILABLE, + PROTOCOL_ERROR, + INVALID_MESSAGE_TYPE, + INVALID_PAYLOAD_DATA, + POLICY_VIOLATION, + MESSAGE_TOO_BIG, + MANDATORY_EXTENSION, + INTERNAL_SERVER_ERROR, + SERVICE_RESTART, + TRY_AGAIN_LATER, + BAD_GATEWAY + ); + + @Test + public void testToString() { + Assert.assertEquals("1000 Bye", NORMAL_CLOSURE.toString()); + } + + @Test + public void testKnownStatuses() { + Assert.assertSame(NORMAL_CLOSURE, valueOf(1000)); + Assert.assertSame(ENDPOINT_UNAVAILABLE, valueOf(1001)); + Assert.assertSame(PROTOCOL_ERROR, valueOf(1002)); + Assert.assertSame(INVALID_MESSAGE_TYPE, valueOf(1003)); + Assert.assertSame(INVALID_PAYLOAD_DATA, valueOf(1007)); + Assert.assertSame(POLICY_VIOLATION, valueOf(1008)); + Assert.assertSame(MESSAGE_TOO_BIG, valueOf(1009)); + Assert.assertSame(MANDATORY_EXTENSION, valueOf(1010)); + Assert.assertSame(INTERNAL_SERVER_ERROR, valueOf(1011)); + Assert.assertSame(SERVICE_RESTART, valueOf(1012)); + Assert.assertSame(TRY_AGAIN_LATER, valueOf(1013)); + Assert.assertSame(BAD_GATEWAY, valueOf(1014)); + } + + @Test + public void testNaturalOrder() { + Assert.assertThat(PROTOCOL_ERROR, Matchers.greaterThan(NORMAL_CLOSURE)); + Assert.assertThat(PROTOCOL_ERROR, Matchers.greaterThan(valueOf(1001))); + Assert.assertThat(PROTOCOL_ERROR, Matchers.comparesEqualTo(PROTOCOL_ERROR)); + Assert.assertThat(PROTOCOL_ERROR, Matchers.comparesEqualTo(valueOf(1002))); + Assert.assertThat(PROTOCOL_ERROR, Matchers.lessThan(INVALID_MESSAGE_TYPE)); + Assert.assertThat(PROTOCOL_ERROR, Matchers.lessThan(valueOf(1007))); + } + + @Test + public void testUserDefinedStatuses() { + // Given, when + WebSocketCloseStatus feedTimeot = new WebSocketCloseStatus(6033, "Feed timed out"); + WebSocketCloseStatus untradablePrice = new WebSocketCloseStatus(6034, "Untradable price"); + + // Then + Assert.assertNotSame(feedTimeot, valueOf(6033)); + Assert.assertEquals(feedTimeot.code(), 6033); + Assert.assertEquals(feedTimeot.reasonText(), "Feed timed out"); + + Assert.assertNotSame(untradablePrice, valueOf(6034)); + Assert.assertEquals(untradablePrice.code(), 6034); + Assert.assertEquals(untradablePrice.reasonText(), "Untradable price"); + } + + @Test + public void testRfc6455CodeValidation() { + // Given + List knownCodes = Arrays.asList( + NORMAL_CLOSURE.code(), + ENDPOINT_UNAVAILABLE.code(), + PROTOCOL_ERROR.code(), + INVALID_MESSAGE_TYPE.code(), + INVALID_PAYLOAD_DATA.code(), + POLICY_VIOLATION.code(), + MESSAGE_TOO_BIG.code(), + MANDATORY_EXTENSION.code(), + INTERNAL_SERVER_ERROR.code(), + SERVICE_RESTART.code(), + TRY_AGAIN_LATER.code(), + BAD_GATEWAY.code() + ); + + SortedSet invalidCodes = new TreeSet(); + + // When + for (int statusCode = Short.MIN_VALUE; statusCode < Short.MAX_VALUE; statusCode++) { + if (!isValidStatusCode(statusCode)) { + invalidCodes.add(statusCode); + } + } + + // Then + Assert.assertEquals(0, invalidCodes.first().intValue()); + Assert.assertEquals(2999, invalidCodes.last().intValue()); + Assert.assertEquals(3000 - validCodes.size(), invalidCodes.size()); + + invalidCodes.retainAll(knownCodes); + Assert.assertEquals(invalidCodes, Collections.emptySet()); + } + +}