From ae9b82388e49cdc904e0ef4e81439486105d9d30 Mon Sep 17 00:00:00 2001 From: Andrey Mizurov Date: Thu, 10 Nov 2022 12:28:07 +0100 Subject: [PATCH] Fix generating the `Origin` header value for websocket handshake request (#12941) Motivation: We have the old erroneous behavior of generating the `Origin| Sec-WebSocket-Origin` for client websocket handshake request (https://github.com/netty/netty/issues/9673). In Netty5 this fixed and auto-generation has been deleted at all, only if the client passed the `Origin` header via custom headers. The same we can do for Netty4 but it could potentially break some clients (unlikely), or introduce an additional parameter to disable or enable this behavior. Modification: Introduce new `generateOriginHeader` parameter in client config and generate the `Origin|Sec-WebSocket-Origin` header value only if it enabled. Add additional check for webSocketURI if it contains host or passed through `customHeaders` to prevent NPE in `newHandshakeRequest()`. Result: Fixes https://github.com/netty/netty/issues/9673 https://github.com/netty/netty/issues/12933 Co-authored-by: Norman Maurer --- .../websocketx/WebSocketClientHandshaker.java | 55 ++++++++++++++++ .../WebSocketClientHandshaker00.java | 46 +++++++++++-- .../WebSocketClientHandshaker07.java | 46 ++++++++++++- .../WebSocketClientHandshaker08.java | 48 ++++++++++++-- .../WebSocketClientHandshaker13.java | 48 ++++++++++++-- .../WebSocketClientHandshakerFactory.java | 65 +++++++++++++++++++ .../WebSocketClientProtocolConfig.java | 65 +++++++++++++------ .../WebSocketClientProtocolHandler.java | 3 +- .../WebSocketClientHandshaker00Test.java | 4 +- .../WebSocketClientHandshaker07Test.java | 6 +- .../WebSocketClientHandshaker08Test.java | 4 +- .../WebSocketClientHandshaker13Test.java | 4 +- .../WebSocketClientHandshakerTest.java | 58 +++++++++++++++-- 13 files changed, 401 insertions(+), 51 deletions(-) diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java index e5ff31c6d94..2df036ac0a8 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java @@ -85,6 +85,8 @@ public abstract class WebSocketClientHandshaker { private final boolean absoluteUpgradeUrl; + protected final boolean generateOriginHeader; + /** * Base constructor * @@ -151,6 +153,36 @@ protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String su protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol, HttpHeaders customHeaders, int maxFramePayloadLength, long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) { + this(uri, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, + absoluteUpgradeUrl, true); + } + + /** + * Base constructor + * + * @param uri + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified + * @param absoluteUpgradeUrl + * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over + * clear HTTP + * @param generateOriginHeader + * Allows to generate the `Origin`|`Sec-WebSocket-Origin` header value for handshake request + * according to the given webSocketURL + */ + protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol, + HttpHeaders customHeaders, int maxFramePayloadLength, + long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl, boolean generateOriginHeader) { this.uri = uri; this.version = version; expectedSubprotocol = subprotocol; @@ -158,6 +190,7 @@ protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String su this.maxFramePayloadLength = maxFramePayloadLength; this.forceCloseTimeoutMillis = forceCloseTimeoutMillis; this.absoluteUpgradeUrl = absoluteUpgradeUrl; + this.generateOriginHeader = generateOriginHeader; } /** @@ -265,6 +298,28 @@ public final ChannelFuture handshake(Channel channel, final ChannelPromise promi } } + if (uri.getHost() == null) { + if (customHeaders == null || !customHeaders.contains(HttpHeaderNames.HOST)) { + promise.setFailure(new IllegalArgumentException("Cannot generate the 'host' header value," + + " webSocketURI should contain host or passed through customHeaders")); + return promise; + } + + if (generateOriginHeader && !customHeaders.contains(HttpHeaderNames.ORIGIN)) { + final String originName; + if (version == WebSocketVersion.V07 || version == WebSocketVersion.V08) { + originName = HttpHeaderNames.SEC_WEBSOCKET_ORIGIN.toString(); + } else { + originName = HttpHeaderNames.ORIGIN.toString(); + } + + promise.setFailure(new IllegalArgumentException("Cannot generate the '" + originName + "' header" + + " value, webSocketURI should contain host or disable generateOriginHeader or pass value" + + " through customHeaders")); + return promise; + } + } + FullHttpRequest request = newHandshakeRequest(); channel.writeAndFlush(request).addListener(new ChannelFutureListener() { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java index 9ccee77af1c..6b63e38f24f 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java @@ -109,11 +109,42 @@ public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, S * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over * clear HTTP */ + WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol, + HttpHeaders customHeaders, int maxFramePayloadLength, + long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) { + this(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, + absoluteUpgradeUrl, true); + } + + /** + * Creates a new instance with the specified destination WebSocket location and version to initiate. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified + * @param absoluteUpgradeUrl + * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over + * clear HTTP + * @param generateOriginHeader + * Allows to generate the `Origin` header value for handshake request + * according to the given webSocketURL + */ WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol, HttpHeaders customHeaders, int maxFramePayloadLength, - long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) { + long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl, + boolean generateOriginHeader) { super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, - absoluteUpgradeUrl); + absoluteUpgradeUrl, generateOriginHeader); } /** @@ -182,15 +213,22 @@ protected FullHttpRequest newHandshakeRequest() { if (customHeaders != null) { headers.add(customHeaders); + if (!headers.contains(HttpHeaderNames.HOST)) { + // Only add HOST header if customHeaders did not contain it. + // + // See https://github.com/netty/netty/issues/10101 + headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL)); + } + } else { + headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL)); } headers.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET) .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE) - .set(HttpHeaderNames.HOST, websocketHostValue(wsURL)) .set(HttpHeaderNames.SEC_WEBSOCKET_KEY1, key1) .set(HttpHeaderNames.SEC_WEBSOCKET_KEY2, key2); - if (!headers.contains(HttpHeaderNames.ORIGIN)) { + if (generateOriginHeader && !headers.contains(HttpHeaderNames.ORIGIN)) { headers.set(HttpHeaderNames.ORIGIN, websocketOriginValue(wsURL)); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java index 924978a256b..b0d1ace3628 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java @@ -164,12 +164,52 @@ public WebSocketClientHandshaker07(URI webSocketURL, WebSocketVersion version, S * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over * clear HTTP */ + WebSocketClientHandshaker07(URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, + boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis, + boolean absoluteUpgradeUrl) { + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking, + allowMaskMismatch, forceCloseTimeoutMillis, absoluteUpgradeUrl, true); + } + + /** + * Creates a new instance. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param performMasking + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + * @param allowMaskMismatch + * When set to true, frames which are not masked properly according to the standard will still be + * accepted + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified. + * @param absoluteUpgradeUrl + * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over + * clear HTTP + * @param generateOriginHeader + * Allows to generate a `Sec-WebSocket-Origin` header value for handshake request + * according to the given webSocketURL + */ WebSocketClientHandshaker07(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis, - boolean absoluteUpgradeUrl) { + boolean absoluteUpgradeUrl, boolean generateOriginHeader) { super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, - absoluteUpgradeUrl); + absoluteUpgradeUrl, generateOriginHeader); this.allowExtensions = allowExtensions; this.performMasking = performMasking; this.allowMaskMismatch = allowMaskMismatch; @@ -232,7 +272,7 @@ protected FullHttpRequest newHandshakeRequest() { .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE) .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key); - if (!headers.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN)) { + if (generateOriginHeader && !headers.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN)) { headers.set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL)); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java index f24e7c469ad..8f2f7b7c0cd 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java @@ -134,7 +134,7 @@ public WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, S boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis) { this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking, - allowMaskMismatch, forceCloseTimeoutMillis, false); + allowMaskMismatch, forceCloseTimeoutMillis, false, true); } /** @@ -166,12 +166,52 @@ public WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, S * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over * clear HTTP */ + WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, + boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis, + boolean absoluteUpgradeUrl) { + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking, + allowMaskMismatch, forceCloseTimeoutMillis, absoluteUpgradeUrl, true); + } + + /** + * Creates a new instance. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param performMasking + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + * @param allowMaskMismatch + * When set to true, frames which are not masked properly according to the standard will still be + * accepted + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified. + * @param absoluteUpgradeUrl + * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over + * clear HTTP + * @param generateOriginHeader + * Allows to generate a `Sec-WebSocket-Origin` header value for handshake request + * according to the given webSocketURL + */ WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis, - boolean absoluteUpgradeUrl) { + boolean absoluteUpgradeUrl, boolean generateOriginHeader) { super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, - absoluteUpgradeUrl); + absoluteUpgradeUrl, generateOriginHeader); this.allowExtensions = allowExtensions; this.performMasking = performMasking; this.allowMaskMismatch = allowMaskMismatch; @@ -234,7 +274,7 @@ protected FullHttpRequest newHandshakeRequest() { .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE) .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key); - if (!headers.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN)) { + if (generateOriginHeader && !headers.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN)) { headers.set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL)); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java index 70805611fa5..6e47607c02e 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java @@ -167,12 +167,53 @@ public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, S * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over * clear HTTP */ + WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, + boolean performMasking, boolean allowMaskMismatch, + long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) { + this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking, + allowMaskMismatch, forceCloseTimeoutMillis, absoluteUpgradeUrl, true); + } + + /** + * Creates a new instance. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be + * sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + * @param customHeaders + * Map of custom headers to add to the client request + * @param maxFramePayloadLength + * Maximum length of a frame's payload + * @param performMasking + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + * @param allowMaskMismatch + * When set to true, frames which are not masked properly according to the standard will still be + * accepted + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified. + * @param absoluteUpgradeUrl + * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over + * clear HTTP + * @param generateOriginHeader + * Allows to generate the `Origin` header value for handshake request + * according to the given webSocketURL + */ WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, boolean performMasking, boolean allowMaskMismatch, - long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) { + long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl, + boolean generateOriginHeader) { super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, - absoluteUpgradeUrl); + absoluteUpgradeUrl, generateOriginHeader); this.allowExtensions = allowExtensions; this.performMasking = performMasking; this.allowMaskMismatch = allowMaskMismatch; @@ -190,7 +231,6 @@ public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, S * Upgrade: websocket * Connection: Upgrade * Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== - * Origin: http://example.com * Sec-WebSocket-Protocol: chat, superchat * Sec-WebSocket-Version: 13 * @@ -235,7 +275,7 @@ protected FullHttpRequest newHandshakeRequest() { .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE) .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key); - if (!headers.contains(HttpHeaderNames.ORIGIN)) { + if (generateOriginHeader && !headers.contains(HttpHeaderNames.ORIGIN)) { headers.set(HttpHeaderNames.ORIGIN, websocketOriginValue(wsURL)); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java index a5dabdcbd55..ce915eb3986 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java @@ -222,4 +222,69 @@ public static WebSocketClientHandshaker newHandshaker( throw new WebSocketClientHandshakeException("Protocol version " + version + " not supported."); } + + /** + * Creates a new handshaker. + * + * @param webSocketURL + * URL for web socket communications. e.g "ws://myhost.com/mypath". + * Subsequent web socket frames will be sent to this URL. + * @param version + * Version of web socket specification to use to connect to the server + * @param subprotocol + * Sub protocol request sent to the server. Null if no sub-protocol support is required. + * @param allowExtensions + * Allow extensions to be used in the reserved bits of the web socket frame + * @param customHeaders + * Custom HTTP headers to send during the handshake + * @param maxFramePayloadLength + * Maximum allowable frame payload length. Setting this value to your application's + * requirement may reduce denial of service attacks using long data frames. + * @param performMasking + * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible + * with the websocket specifications. Client applications that communicate with a non-standard server + * which doesn't require masking might set this to false to achieve a higher performance. + * @param allowMaskMismatch + * When set to true, frames which are not masked properly according to the standard will still be + * accepted. + * @param forceCloseTimeoutMillis + * Close the connection if it was not closed by the server after timeout specified + * @param absoluteUpgradeUrl + * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over + * clear HTTP + * @param generateOriginHeader + * Allows to generate the `Origin`|`Sec-WebSocket-Origin` header value for handshake request + * according to the given webSocketURL + */ + public static WebSocketClientHandshaker newHandshaker( + URI webSocketURL, WebSocketVersion version, String subprotocol, + boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength, + boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis, + boolean absoluteUpgradeUrl, boolean generateOriginHeader) { + if (version == V13) { + return new WebSocketClientHandshaker13( + webSocketURL, V13, subprotocol, allowExtensions, customHeaders, + maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis, + absoluteUpgradeUrl, generateOriginHeader); + } + if (version == V08) { + return new WebSocketClientHandshaker08( + webSocketURL, V08, subprotocol, allowExtensions, customHeaders, + maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis, + absoluteUpgradeUrl, generateOriginHeader); + } + if (version == V07) { + return new WebSocketClientHandshaker07( + webSocketURL, V07, subprotocol, allowExtensions, customHeaders, + maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis, + absoluteUpgradeUrl, generateOriginHeader); + } + if (version == V00) { + return new WebSocketClientHandshaker00( + webSocketURL, V00, subprotocol, customHeaders, + maxFramePayloadLength, forceCloseTimeoutMillis, absoluteUpgradeUrl, generateOriginHeader); + } + + throw new WebSocketClientHandshakeException("Protocol version " + version + " not supported."); + } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolConfig.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolConfig.java index 0cfd6c39db8..6700c7818ad 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolConfig.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolConfig.java @@ -34,6 +34,7 @@ public final class WebSocketClientProtocolConfig { static final boolean DEFAULT_ALLOW_MASK_MISMATCH = false; static final boolean DEFAULT_HANDLE_CLOSE_FRAMES = true; static final boolean DEFAULT_DROP_PONG_FRAMES = true; + static final boolean DEFAULT_GENERATE_ORIGIN_HEADER = true; private final URI webSocketUri; private final String subprotocol; @@ -49,6 +50,7 @@ public final class WebSocketClientProtocolConfig { private final long handshakeTimeoutMillis; private final long forceCloseTimeoutMillis; private final boolean absoluteUpgradeUrl; + private final boolean generateOriginHeader; private WebSocketClientProtocolConfig( URI webSocketUri, @@ -64,7 +66,8 @@ private WebSocketClientProtocolConfig( boolean dropPongFrames, long handshakeTimeoutMillis, long forceCloseTimeoutMillis, - boolean absoluteUpgradeUrl + boolean absoluteUpgradeUrl, + boolean generateOriginHeader ) { this.webSocketUri = webSocketUri; this.subprotocol = subprotocol; @@ -80,6 +83,7 @@ private WebSocketClientProtocolConfig( this.dropPongFrames = dropPongFrames; this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis"); this.absoluteUpgradeUrl = absoluteUpgradeUrl; + this.generateOriginHeader = generateOriginHeader; } public URI webSocketUri() { @@ -138,24 +142,29 @@ public boolean absoluteUpgradeUrl() { return absoluteUpgradeUrl; } + public boolean generateOriginHeader() { + return generateOriginHeader; + } + @Override public String toString() { return "WebSocketClientProtocolConfig" + - " {webSocketUri=" + webSocketUri + - ", subprotocol=" + subprotocol + - ", version=" + version + - ", allowExtensions=" + allowExtensions + - ", customHeaders=" + customHeaders + - ", maxFramePayloadLength=" + maxFramePayloadLength + - ", performMasking=" + performMasking + - ", allowMaskMismatch=" + allowMaskMismatch + - ", handleCloseFrames=" + handleCloseFrames + - ", sendCloseFrame=" + sendCloseFrame + - ", dropPongFrames=" + dropPongFrames + - ", handshakeTimeoutMillis=" + handshakeTimeoutMillis + - ", forceCloseTimeoutMillis=" + forceCloseTimeoutMillis + - ", absoluteUpgradeUrl=" + absoluteUpgradeUrl + - "}"; + " {webSocketUri=" + webSocketUri + + ", subprotocol=" + subprotocol + + ", version=" + version + + ", allowExtensions=" + allowExtensions + + ", customHeaders=" + customHeaders + + ", maxFramePayloadLength=" + maxFramePayloadLength + + ", performMasking=" + performMasking + + ", allowMaskMismatch=" + allowMaskMismatch + + ", handleCloseFrames=" + handleCloseFrames + + ", sendCloseFrame=" + sendCloseFrame + + ", dropPongFrames=" + dropPongFrames + + ", handshakeTimeoutMillis=" + handshakeTimeoutMillis + + ", forceCloseTimeoutMillis=" + forceCloseTimeoutMillis + + ", absoluteUpgradeUrl=" + absoluteUpgradeUrl + + ", generateOriginHeader=" + generateOriginHeader + + "}"; } public Builder toBuilder() { @@ -177,7 +186,8 @@ public static Builder newBuilder() { DEFAULT_DROP_PONG_FRAMES, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS, -1, - false); + false, + DEFAULT_GENERATE_ORIGIN_HEADER); } public static final class Builder { @@ -195,6 +205,7 @@ public static final class Builder { private long handshakeTimeoutMillis; private long forceCloseTimeoutMillis; private boolean absoluteUpgradeUrl; + private boolean generateOriginHeader; private Builder(WebSocketClientProtocolConfig clientConfig) { this(ObjectUtil.checkNotNull(clientConfig, "clientConfig").webSocketUri(), @@ -210,7 +221,8 @@ private Builder(WebSocketClientProtocolConfig clientConfig) { clientConfig.dropPongFrames(), clientConfig.handshakeTimeoutMillis(), clientConfig.forceCloseTimeoutMillis(), - clientConfig.absoluteUpgradeUrl()); + clientConfig.absoluteUpgradeUrl(), + clientConfig.generateOriginHeader()); } private Builder(URI webSocketUri, @@ -226,7 +238,8 @@ private Builder(URI webSocketUri, boolean dropPongFrames, long handshakeTimeoutMillis, long forceCloseTimeoutMillis, - boolean absoluteUpgradeUrl) { + boolean absoluteUpgradeUrl, + boolean generateOriginHeader) { this.webSocketUri = webSocketUri; this.subprotocol = subprotocol; this.version = version; @@ -241,6 +254,7 @@ private Builder(URI webSocketUri, this.handshakeTimeoutMillis = handshakeTimeoutMillis; this.forceCloseTimeoutMillis = forceCloseTimeoutMillis; this.absoluteUpgradeUrl = absoluteUpgradeUrl; + this.generateOriginHeader = generateOriginHeader; } /** @@ -367,6 +381,16 @@ public Builder absoluteUpgradeUrl(boolean absoluteUpgradeUrl) { return this; } + /** + * Allows to generate the `Origin`|`Sec-WebSocket-Origin` header value for handshake request + * according the given webSocketURI. Usually it's not necessary and can be disabled, + * but for backward compatibility is set to {@code true} as default. + */ + public Builder generateOriginHeader(boolean generateOriginHeader) { + this.generateOriginHeader = generateOriginHeader; + return this; + } + /** * Build unmodifiable client protocol configuration. */ @@ -385,7 +409,8 @@ public WebSocketClientProtocolConfig build() { dropPongFrames, handshakeTimeoutMillis, forceCloseTimeoutMillis, - absoluteUpgradeUrl + absoluteUpgradeUrl, + generateOriginHeader ); } } 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 257d3f025c9..2f8e262ca0a 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 @@ -94,7 +94,8 @@ public WebSocketClientProtocolHandler(WebSocketClientProtocolConfig clientConfig clientConfig.performMasking(), clientConfig.allowMaskMismatch(), clientConfig.forceCloseTimeoutMillis(), - clientConfig.absoluteUpgradeUrl() + clientConfig.absoluteUpgradeUrl(), + clientConfig.generateOriginHeader() ); this.clientConfig = clientConfig; } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00Test.java index 9d1606f3707..d84c9032c2a 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00Test.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00Test.java @@ -23,9 +23,9 @@ public class WebSocketClientHandshaker00Test extends WebSocketClientHandshakerTest { @Override protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers, - boolean absoluteUpgradeUrl) { + boolean absoluteUpgradeUrl, boolean generateOriginHeader) { return new WebSocketClientHandshaker00(uri, WebSocketVersion.V00, subprotocol, headers, - 1024, 10000, absoluteUpgradeUrl); + 1024, 10000, absoluteUpgradeUrl, generateOriginHeader); } @Override diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07Test.java index 692bc3bbe64..e3f04da85f2 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07Test.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07Test.java @@ -31,7 +31,7 @@ public class WebSocketClientHandshaker07Test extends WebSocketClientHandshakerTe public void testHostHeaderPreserved() { URI uri = URI.create("ws://localhost:9999"); WebSocketClientHandshaker handshaker = newHandshaker(uri, null, - new DefaultHttpHeaders().set(HttpHeaderNames.HOST, "test.netty.io"), false); + new DefaultHttpHeaders().set(HttpHeaderNames.HOST, "test.netty.io"), false, true); FullHttpRequest request = handshaker.newHandshakeRequest(); try { @@ -44,10 +44,10 @@ public void testHostHeaderPreserved() { @Override protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers, - boolean absoluteUpgradeUrl) { + boolean absoluteUpgradeUrl, boolean generateOriginHeader) { return new WebSocketClientHandshaker07(uri, WebSocketVersion.V07, subprotocol, false, headers, 1024, true, false, 10000, - absoluteUpgradeUrl); + absoluteUpgradeUrl, generateOriginHeader); } @Override diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08Test.java index 34c5fb76d8b..aa8e11dd50a 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08Test.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08Test.java @@ -22,9 +22,9 @@ public class WebSocketClientHandshaker08Test extends WebSocketClientHandshaker07Test { @Override protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers, - boolean absoluteUpgradeUrl) { + boolean absoluteUpgradeUrl, boolean generateOriginHeader) { return new WebSocketClientHandshaker08(uri, WebSocketVersion.V08, subprotocol, false, headers, 1024, true, true, 10000, - absoluteUpgradeUrl); + absoluteUpgradeUrl, generateOriginHeader); } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13Test.java index 2371caed598..cd82f214546 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13Test.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13Test.java @@ -24,10 +24,10 @@ public class WebSocketClientHandshaker13Test extends WebSocketClientHandshaker07 @Override protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers, - boolean absoluteUpgradeUrl) { + boolean absoluteUpgradeUrl, boolean generateOriginHeader) { return new WebSocketClientHandshaker13(uri, WebSocketVersion.V13, subprotocol, false, headers, 1024, true, true, 10000, - absoluteUpgradeUrl); + absoluteUpgradeUrl, generateOriginHeader); } @Override diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java index 1be884e0b66..411e8ae9c2a 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java @@ -51,6 +51,7 @@ import static io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker13.WEBSOCKET_13_ACCEPT_GUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -59,10 +60,11 @@ public abstract class WebSocketClientHandshakerTest { protected abstract WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers, - boolean absoluteUpgradeUrl); + boolean absoluteUpgradeUrl, + boolean generateOriginHeader); protected WebSocketClientHandshaker newHandshaker(URI uri) { - return newHandshaker(uri, null, null, false); + return newHandshaker(uri, null, null, false, true); } protected abstract CharSequence getOriginHeaderName(); @@ -183,7 +185,7 @@ public void originHeaderWithoutScheme() { public void testSetOriginFromCustomHeaders() { HttpHeaders customHeaders = new DefaultHttpHeaders().set(getOriginHeaderName(), "http://example.com"); WebSocketClientHandshaker handshaker = newHandshaker(URI.create("ws://server.example.com/chat"), null, - customHeaders, false); + customHeaders, false, true); FullHttpRequest request = handshaker.newHandshakeRequest(); try { assertEquals("http://example.com", request.headers().get(getOriginHeaderName())); @@ -192,6 +194,50 @@ public void testSetOriginFromCustomHeaders() { } } + @Test + public void testOriginHeaderIsAbsentWhenGeneratingDisable() { + URI uri = URI.create("http://example.com/ws"); + WebSocketClientHandshaker handshaker = newHandshaker(uri, null, null, false, false); + FullHttpRequest request = handshaker.newHandshakeRequest(); + try { + assertFalse(request.headers().contains(getOriginHeaderName())); + assertEquals("/ws", request.uri()); + } finally { + request.release(); + } + } + + @Test + public void testInvalidHostWhenIncorrectWebSocketURI() { + URI uri = URI.create("/ws"); + EmbeddedChannel channel = new EmbeddedChannel(new HttpClientCodec()); + final WebSocketClientHandshaker handshaker = newHandshaker(uri, null, null, false, true); + final ChannelFuture handshakeFuture = handshaker.handshake(channel); + + assertFalse(handshakeFuture.isSuccess()); + assertInstanceOf(IllegalArgumentException.class, handshakeFuture.cause()); + assertEquals("Cannot generate the 'host' header value, webSocketURI should contain host" + + " or passed through customHeaders", handshakeFuture.cause().getMessage()); + assertFalse(channel.finish()); + } + + @Test + public void testInvalidOriginWhenIncorrectWebSocketURI() { + URI uri = URI.create("/ws"); + EmbeddedChannel channel = new EmbeddedChannel(new HttpClientCodec()); + HttpHeaders headers = new DefaultHttpHeaders(); + headers.set(HttpHeaderNames.HOST, "localhost:80"); + final WebSocketClientHandshaker handshaker = newHandshaker(uri, null, headers, false, true); + final ChannelFuture handshakeFuture = handshaker.handshake(channel); + + assertFalse(handshakeFuture.isSuccess()); + assertInstanceOf(IllegalArgumentException.class, handshakeFuture.cause()); + assertEquals("Cannot generate the '" + getOriginHeaderName() + "' header value," + + " webSocketURI should contain host or disable generateOriginHeader" + + " or pass value through customHeaders", handshakeFuture.cause().getMessage()); + assertFalse(channel.finish()); + } + private void testHostHeader(String uri, String expected) { testHeaderDefaultHttp(uri, HttpHeaderNames.HOST, expected); } @@ -262,7 +308,7 @@ public void testUpgradeUrlWithoutPathWithQuery() { @Test public void testAbsoluteUpgradeUrlWithQuery() { URI uri = URI.create("ws://localhost:9999/path%20with%20ws?a=b%20c"); - WebSocketClientHandshaker handshaker = newHandshaker(uri, null, null, true); + WebSocketClientHandshaker handshaker = newHandshaker(uri, null, null, true, true); FullHttpRequest request = handshaker.newHandshakeRequest(); try { assertEquals("ws://localhost:9999/path%20with%20ws?a=b%20c", request.uri()); @@ -392,7 +438,7 @@ public void testDuplicateWebsocketHandshakeHeaders() { inputHeaders.add(getProtocolHeaderName(), bogusSubProtocol); String realSubProtocol = "realSubProtocol"; - WebSocketClientHandshaker handshaker = newHandshaker(uri, realSubProtocol, inputHeaders, false); + WebSocketClientHandshaker handshaker = newHandshaker(uri, realSubProtocol, inputHeaders, false, true); FullHttpRequest request = handshaker.newHandshakeRequest(); HttpHeaders outputHeaders = request.headers(); @@ -412,7 +458,7 @@ public void testDuplicateWebsocketHandshakeHeaders() { @Test public void testWebSocketClientHandshakeException() { URI uri = URI.create("ws://localhost:9999/exception"); - WebSocketClientHandshaker handshaker = newHandshaker(uri, null, null, false); + WebSocketClientHandshaker handshaker = newHandshaker(uri, null, null, false, true); FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED); response.headers().set(HttpHeaderNames.WWW_AUTHENTICATE, "realm = access token required");