Simplify WebSocket handlers constructor arguments hell #9698 (#9699)

### Motivation:

Introduction of `WebSocketDecoderConfig` made our server-side code more elegant and simpler for support.

However there is still some problem with maintenance and new features development for WebSocket codecs (`WebSocketServerProtocolHandler`, `WebSocketServerProtocolHandler`).

Particularly, it makes me ~~crying with blood~~ extremely sad to add new parameter and yet another one constructor into these handlers, when I want to contribute new feature.

### Modification:

I've extracted all parameters for client and server WebSocket handlers into config/builder structures, like it was made for decoders in PR #9116.

### Result:

* Fixes #9698: Simplify WebSocket handlers constructor arguments hell
* Unblock further development in this module (configurable close frame handling on server-side; automatic close-frame sending, when missed; memory leaks on protocol violations; etc...)

Bonuses:

* All defaults are gathered in one place and could be easily found/reused.
* New API greatly simplifies usage, but does NOT allow inheritance or modification.
* New API would simplify long-term maintenance of WebSockets module.

### Example

    WebSocketClientProtocolConfig config = WebSocketClientProtocolConfig.newBuilder()
        .webSocketUri("wss://localhost:8443/fx-spot")
        .subprotocol("trading")
        .handshakeTimeoutMillis(15000L)
        .build();
    ctx.pipeline().addLast(new WebSocketClientProtocolHandler(config));
This commit is contained in:
ursa 2019-10-29 19:48:18 +00:00 committed by Norman Maurer
parent 4f6c21fa97
commit 8d99aa1235
8 changed files with 675 additions and 81 deletions

View File

@ -0,0 +1,329 @@
/*
* 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.http.EmptyHttpHeaders;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler.ClientHandshakeStateEvent;
import io.netty.util.internal.ObjectUtil;
import java.net.URI;
import static io.netty.util.internal.ObjectUtil.checkPositive;
/**
* WebSocket server configuration.
*/
public final class WebSocketClientProtocolConfig {
static final WebSocketClientProtocolConfig DEFAULT = new WebSocketClientProtocolConfig(
URI.create("https://localhost/"), null, WebSocketVersion.V13, false,
EmptyHttpHeaders.INSTANCE, 65536, true, false, true, true, 10000L, -1, false);
private final URI webSocketUri;
private final String subprotocol;
private final WebSocketVersion version;
private final boolean allowExtensions;
private final HttpHeaders customHeaders;
private final int maxFramePayloadLength;
private final boolean performMasking;
private final boolean allowMaskMismatch;
private final boolean handleCloseFrames;
private final boolean dropPongFrames;
private final long handshakeTimeoutMillis;
private final long forceCloseTimeoutMillis;
private final boolean absoluteUpgradeUrl;
private WebSocketClientProtocolConfig(
URI webSocketUri,
String subprotocol,
WebSocketVersion version,
boolean allowExtensions,
HttpHeaders customHeaders,
int maxFramePayloadLength,
boolean performMasking,
boolean allowMaskMismatch,
boolean handleCloseFrames,
boolean dropPongFrames,
long handshakeTimeoutMillis,
long forceCloseTimeoutMillis,
boolean absoluteUpgradeUrl
) {
this.webSocketUri = webSocketUri;
this.subprotocol = subprotocol;
this.version = version;
this.allowExtensions = allowExtensions;
this.customHeaders = customHeaders;
this.maxFramePayloadLength = maxFramePayloadLength;
this.performMasking = performMasking;
this.allowMaskMismatch = allowMaskMismatch;
this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
this.handleCloseFrames = handleCloseFrames;
this.dropPongFrames = dropPongFrames;
this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis");
this.absoluteUpgradeUrl = absoluteUpgradeUrl;
}
public URI webSocketUri() {
return webSocketUri;
}
public String subprotocol() {
return subprotocol;
}
public WebSocketVersion version() {
return version;
}
public boolean allowExtensions() {
return allowExtensions;
}
public HttpHeaders customHeaders() {
return customHeaders;
}
public int maxFramePayloadLength() {
return maxFramePayloadLength;
}
public boolean performMasking() {
return performMasking;
}
public boolean allowMaskMismatch() {
return allowMaskMismatch;
}
public boolean handleCloseFrames() {
return handleCloseFrames;
}
public boolean dropPongFrames() {
return dropPongFrames;
}
public long handshakeTimeoutMillis() {
return handshakeTimeoutMillis;
}
public long forceCloseTimeoutMillis() {
return forceCloseTimeoutMillis;
}
public boolean absoluteUpgradeUrl() {
return absoluteUpgradeUrl;
}
@Override
public String toString() {
return "WebSocketClientProtocolConfig" +
" {webSocketUri=" + webSocketUri +
", subprotocol=" + subprotocol +
", version=" + version +
", allowExtensions=" + allowExtensions +
", customHeaders=" + customHeaders +
", maxFramePayloadLength=" + maxFramePayloadLength +
", performMasking=" + performMasking +
", allowMaskMismatch=" + allowMaskMismatch +
", handleCloseFrames=" + handleCloseFrames +
", dropPongFrames=" + dropPongFrames +
", handshakeTimeoutMillis=" + handshakeTimeoutMillis +
", forceCloseTimeoutMillis=" + forceCloseTimeoutMillis +
", absoluteUpgradeUrl=" + absoluteUpgradeUrl +
"}";
}
public Builder toBuilder() {
return new Builder(this);
}
public static Builder newBuilder() {
return new Builder(DEFAULT);
}
public static final class Builder {
private URI webSocketUri;
private String subprotocol;
private WebSocketVersion version;
private boolean allowExtensions;
private HttpHeaders customHeaders;
private int maxFramePayloadLength;
private boolean performMasking;
private boolean allowMaskMismatch;
private boolean handleCloseFrames;
private boolean dropPongFrames;
private long handshakeTimeoutMillis;
private long forceCloseTimeoutMillis;
private boolean absoluteUpgradeUrl;
private Builder(WebSocketClientProtocolConfig clientConfig) {
ObjectUtil.checkNotNull(clientConfig, "clientConfig");
this.webSocketUri = clientConfig.webSocketUri();
this.subprotocol = clientConfig.subprotocol();
this.version = clientConfig.version();
this.allowExtensions = clientConfig.allowExtensions();
this.customHeaders = clientConfig.customHeaders();
this.maxFramePayloadLength = clientConfig.maxFramePayloadLength();
this.performMasking = clientConfig.performMasking();
this.allowMaskMismatch = clientConfig.allowMaskMismatch();
this.handleCloseFrames = clientConfig.handleCloseFrames();
this.dropPongFrames = clientConfig.dropPongFrames();
this.handshakeTimeoutMillis = clientConfig.handshakeTimeoutMillis();
this.forceCloseTimeoutMillis = clientConfig.forceCloseTimeoutMillis();
this.absoluteUpgradeUrl = clientConfig.absoluteUpgradeUrl();
}
/**
* URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
* sent to this URL.
*/
public Builder webSocketUri(String webSocketUri) {
return webSocketUri(URI.create(webSocketUri));
}
/**
* URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
* sent to this URL.
*/
public Builder webSocketUri(URI webSocketUri) {
this.webSocketUri = webSocketUri;
return this;
}
/**
* Sub protocol request sent to the server.
*/
public Builder subprotocol(String subprotocol) {
this.subprotocol = subprotocol;
return this;
}
/**
* Version of web socket specification to use to connect to the server
*/
public Builder version(WebSocketVersion version) {
this.version = version;
return this;
}
/**
* Allow extensions to be used in the reserved bits of the web socket frame
*/
public Builder allowExtensions(boolean allowExtensions) {
this.allowExtensions = allowExtensions;
return this;
}
/**
* Map of custom headers to add to the client request
*/
public Builder customHeaders(HttpHeaders customHeaders) {
this.customHeaders = customHeaders;
return this;
}
/**
* Maximum length of a frame's payload
*/
public Builder maxFramePayloadLength(int maxFramePayloadLength) {
this.maxFramePayloadLength = maxFramePayloadLength;
return this;
}
/**
* 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.
*/
public Builder performMasking(boolean performMasking) {
this.performMasking = performMasking;
return this;
}
/**
* When set to true, frames which are not masked properly according to the standard will still be accepted.
*/
public Builder allowMaskMismatch(boolean allowMaskMismatch) {
this.allowMaskMismatch = allowMaskMismatch;
return this;
}
/**
* {@code true} if close frames should not be forwarded and just close the channel
*/
public Builder handleCloseFrames(boolean handleCloseFrames) {
this.handleCloseFrames = handleCloseFrames;
return this;
}
/**
* {@code true} if pong frames should not be forwarded
*/
public Builder dropPongFrames(boolean dropPongFrames) {
this.dropPongFrames = dropPongFrames;
return this;
}
/**
* Handshake timeout in mills, when handshake timeout, will trigger user
* event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
*/
public Builder handshakeTimeoutMillis(long handshakeTimeoutMillis) {
this.handshakeTimeoutMillis = handshakeTimeoutMillis;
return this;
}
/**
* Close the connection if it was not closed by the server after timeout specified
*/
public Builder forceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
return this;
}
/**
* Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over clear HTTP
*/
public Builder absoluteUpgradeUrl(boolean absoluteUpgradeUrl) {
this.absoluteUpgradeUrl = absoluteUpgradeUrl;
return this;
}
/**
* Build unmodifiable client protocol configuration.
*/
public WebSocketClientProtocolConfig build() {
return new WebSocketClientProtocolConfig(
webSocketUri,
subprotocol,
version,
allowExtensions,
customHeaders,
maxFramePayloadLength,
performMasking,
allowMaskMismatch,
handleCloseFrames,
dropPongFrames,
handshakeTimeoutMillis,
forceCloseTimeoutMillis,
absoluteUpgradeUrl
);
}
}
}

View File

@ -23,6 +23,7 @@ import io.netty.handler.codec.http.HttpHeaders;
import java.net.URI;
import java.util.List;
import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT;
import static io.netty.util.internal.ObjectUtil.*;
/**
@ -40,8 +41,6 @@ import static io.netty.util.internal.ObjectUtil.*;
* {@link ClientHandshakeStateEvent#HANDSHAKE_ISSUED} or {@link ClientHandshakeStateEvent#HANDSHAKE_COMPLETE}.
*/
public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
private static final long DEFAULT_HANDSHAKE_TIMEOUT_MS = 10000L;
private final WebSocketClientHandshaker handshaker;
private final boolean handleCloseFrames;
private final long handshakeTimeoutMillis;
@ -73,6 +72,30 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
HANDSHAKE_COMPLETE
}
/**
* Base constructor
*
* @param clientConfig
* Client protocol configuration.
*/
public WebSocketClientProtocolHandler(WebSocketClientProtocolConfig clientConfig) {
super(checkNotNull(clientConfig, "clientConfig").dropPongFrames());
this.handshaker = WebSocketClientHandshakerFactory.newHandshaker(
clientConfig.webSocketUri(),
clientConfig.version(),
clientConfig.subprotocol(),
clientConfig.allowExtensions(),
clientConfig.customHeaders(),
clientConfig.maxFramePayloadLength(),
clientConfig.performMasking(),
clientConfig.allowMaskMismatch(),
clientConfig.forceCloseTimeoutMillis(),
clientConfig.absoluteUpgradeUrl()
);
this.handleCloseFrames = clientConfig.handleCloseFrames();
this.handshakeTimeoutMillis = clientConfig.handshakeTimeoutMillis();
}
/**
* Base constructor
*
@ -101,8 +124,8 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
boolean allowExtensions, HttpHeaders customHeaders,
int maxFramePayloadLength, boolean handleCloseFrames,
boolean performMasking, boolean allowMaskMismatch) {
this(webSocketURL, version, subprotocol, allowExtensions, customHeaders,
maxFramePayloadLength, handleCloseFrames, performMasking, allowMaskMismatch, DEFAULT_HANDSHAKE_TIMEOUT_MS);
this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
handleCloseFrames, performMasking, allowMaskMismatch, DEFAULT.handshakeTimeoutMillis());
}
/**
@ -163,7 +186,7 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
boolean allowExtensions, HttpHeaders customHeaders,
int maxFramePayloadLength, boolean handleCloseFrames) {
this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
handleCloseFrames, DEFAULT_HANDSHAKE_TIMEOUT_MS);
handleCloseFrames, DEFAULT.handshakeTimeoutMillis());
}
/**
@ -190,7 +213,7 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
boolean handleCloseFrames, long handshakeTimeoutMillis) {
this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
handleCloseFrames, true, false, handshakeTimeoutMillis);
handleCloseFrames, DEFAULT.performMasking(), DEFAULT.allowMaskMismatch(), handshakeTimeoutMillis);
}
/**
@ -211,8 +234,8 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
boolean allowExtensions, HttpHeaders customHeaders,
int maxFramePayloadLength) {
this(webSocketURL, version, subprotocol,
allowExtensions, customHeaders, maxFramePayloadLength, DEFAULT_HANDSHAKE_TIMEOUT_MS);
this(webSocketURL, version, subprotocol, allowExtensions,
customHeaders, maxFramePayloadLength, DEFAULT.handshakeTimeoutMillis());
}
/**
@ -236,8 +259,8 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
boolean allowExtensions, HttpHeaders customHeaders,
int maxFramePayloadLength, long handshakeTimeoutMillis) {
this(webSocketURL, version, subprotocol,
allowExtensions, customHeaders, maxFramePayloadLength, true, handshakeTimeoutMillis);
this(webSocketURL, version, subprotocol, allowExtensions, customHeaders,
maxFramePayloadLength, DEFAULT.handleCloseFrames(), handshakeTimeoutMillis);
}
/**
@ -250,7 +273,7 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
* {@code true} if close frames should not be forwarded and just close the channel
*/
public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames) {
this(handshaker, handleCloseFrames, DEFAULT_HANDSHAKE_TIMEOUT_MS);
this(handshaker, handleCloseFrames, DEFAULT.handshakeTimeoutMillis());
}
/**
@ -267,7 +290,7 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
*/
public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames,
long handshakeTimeoutMillis) {
this(handshaker, handleCloseFrames, true, handshakeTimeoutMillis);
this(handshaker, handleCloseFrames, DEFAULT.dropPongFrames(), handshakeTimeoutMillis);
}
/**
@ -283,7 +306,7 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
*/
public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames,
boolean dropPongFrames) {
this(handshaker, handleCloseFrames, dropPongFrames, DEFAULT_HANDSHAKE_TIMEOUT_MS);
this(handshaker, handleCloseFrames, dropPongFrames, DEFAULT.handshakeTimeoutMillis());
}
/**
@ -316,7 +339,7 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
* was established to the remote peer.
*/
public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker) {
this(handshaker, DEFAULT_HANDSHAKE_TIMEOUT_MS);
this(handshaker, DEFAULT.handshakeTimeoutMillis());
}
/**
@ -330,7 +353,7 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
* event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
*/
public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, long handshakeTimeoutMillis) {
this(handshaker, true, handshakeTimeoutMillis);
this(handshaker, DEFAULT.handleCloseFrames(), handshakeTimeoutMillis);
}
@Override

View File

@ -22,6 +22,9 @@ import io.netty.util.internal.ObjectUtil;
*/
public final class WebSocketDecoderConfig {
static final WebSocketDecoderConfig DEFAULT =
new WebSocketDecoderConfig(65536, true, false, false, true, true);
private final int maxFramePayloadLength;
private final boolean expectMaskedFrames;
private final boolean allowMaskMismatch;
@ -102,20 +105,16 @@ public final class WebSocketDecoderConfig {
}
public static Builder newBuilder() {
return new Builder();
return new Builder(DEFAULT);
}
public static final class Builder {
private int maxFramePayloadLength = 65536;
private boolean expectMaskedFrames = true;
private int maxFramePayloadLength;
private boolean expectMaskedFrames;
private boolean allowMaskMismatch;
private boolean allowExtensions;
private boolean closeOnProtocolViolation = true;
private boolean withUTF8Validator = true;
private Builder() {
/* No-op */
}
private boolean closeOnProtocolViolation;
private boolean withUTF8Validator;
private Builder(WebSocketDecoderConfig decoderConfig) {
ObjectUtil.checkNotNull(decoderConfig, "decoderConfig");

View File

@ -0,0 +1,238 @@
/*
* 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.http.websocketx.WebSocketClientProtocolHandler.ClientHandshakeStateEvent;
import io.netty.util.internal.ObjectUtil;
import static io.netty.util.internal.ObjectUtil.checkPositive;
/**
* WebSocket server configuration.
*/
public final class WebSocketServerProtocolConfig {
static final WebSocketServerProtocolConfig DEFAULT =
new WebSocketServerProtocolConfig("/", null, false, 10000L, true, true, WebSocketDecoderConfig.DEFAULT);
private final String websocketPath;
private final String subprotocols;
private final boolean checkStartsWith;
private final long handshakeTimeoutMillis;
private final boolean handleCloseFrames;
private final boolean dropPongFrames;
private final WebSocketDecoderConfig decoderConfig;
private WebSocketServerProtocolConfig(
String websocketPath,
String subprotocols,
boolean checkStartsWith,
long handshakeTimeoutMillis,
boolean handleCloseFrames,
boolean dropPongFrames,
WebSocketDecoderConfig decoderConfig
) {
this.websocketPath = websocketPath;
this.subprotocols = subprotocols;
this.checkStartsWith = checkStartsWith;
this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis");
this.handleCloseFrames = handleCloseFrames;
this.dropPongFrames = dropPongFrames;
this.decoderConfig = decoderConfig == null ? WebSocketDecoderConfig.DEFAULT : decoderConfig;
}
public String websocketPath() {
return websocketPath;
}
public String subprotocols() {
return subprotocols;
}
public boolean checkStartsWith() {
return checkStartsWith;
}
public long handshakeTimeoutMillis() {
return handshakeTimeoutMillis;
}
public boolean handleCloseFrames() {
return handleCloseFrames;
}
public boolean dropPongFrames() {
return dropPongFrames;
}
public WebSocketDecoderConfig decoderConfig() {
return decoderConfig;
}
@Override
public String toString() {
return "WebSocketServerProtocolConfig" +
" {websocketPath=" + websocketPath +
", subprotocols=" + subprotocols +
", checkStartsWith=" + checkStartsWith +
", handshakeTimeoutMillis=" + handshakeTimeoutMillis +
", handleCloseFrames=" + handleCloseFrames +
", dropPongFrames=" + dropPongFrames +
", decoderConfig=" + decoderConfig +
"}";
}
public Builder toBuilder() {
return new Builder(this);
}
public static Builder newBuilder() {
return new Builder(DEFAULT);
}
public static final class Builder {
private String websocketPath;
private String subprotocols;
private boolean checkStartsWith;
private long handshakeTimeoutMillis;
private boolean handleCloseFrames;
private boolean dropPongFrames;
private WebSocketDecoderConfig decoderConfig;
private WebSocketDecoderConfig.Builder decoderConfigBuilder;
private Builder(WebSocketServerProtocolConfig serverConfig) {
ObjectUtil.checkNotNull(serverConfig, "serverConfig");
websocketPath = serverConfig.websocketPath();
subprotocols = serverConfig.subprotocols();
checkStartsWith = serverConfig.checkStartsWith();
handshakeTimeoutMillis = serverConfig.handshakeTimeoutMillis();
handleCloseFrames = serverConfig.handleCloseFrames();
dropPongFrames = serverConfig.dropPongFrames();
decoderConfig = serverConfig.decoderConfig();
}
/**
* URI path component to handle websocket upgrade requests on.
*/
public Builder websocketPath(String websocketPath) {
this.websocketPath = websocketPath;
return this;
}
/**
* CSV of supported protocols
*/
public Builder subprotocols(String subprotocols) {
this.subprotocols = subprotocols;
return this;
}
/**
* {@code true} to handle all requests, where URI path component starts from
* {@link WebSocketServerProtocolConfig#websocketPath()}, {@code false} for exact match (default).
*/
public Builder checkStartsWith(boolean checkStartsWith) {
this.checkStartsWith = checkStartsWith;
return this;
}
/**
* Handshake timeout in mills, when handshake timeout, will trigger user
* event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
*/
public Builder handshakeTimeoutMillis(long handshakeTimeoutMillis) {
this.handshakeTimeoutMillis = handshakeTimeoutMillis;
return this;
}
/**
* {@code true} if close frames should not be forwarded and just close the channel
*/
public Builder handleCloseFrames(boolean handleCloseFrames) {
this.handleCloseFrames = handleCloseFrames;
return this;
}
/**
* {@code true} if pong frames should not be forwarded
*/
public Builder dropPongFrames(boolean dropPongFrames) {
this.dropPongFrames = dropPongFrames;
return this;
}
/**
* Frames decoder configuration.
*/
public Builder decoderConfig(WebSocketDecoderConfig decoderConfig) {
this.decoderConfig = decoderConfig == null ? WebSocketDecoderConfig.DEFAULT : decoderConfig;
this.decoderConfigBuilder = null;
return this;
}
private WebSocketDecoderConfig.Builder decoderConfigBuilder() {
if (decoderConfigBuilder == null) {
decoderConfigBuilder = decoderConfig.toBuilder();
}
return decoderConfigBuilder;
}
public Builder maxFramePayloadLength(int maxFramePayloadLength) {
decoderConfigBuilder().maxFramePayloadLength(maxFramePayloadLength);
return this;
}
public Builder expectMaskedFrames(boolean expectMaskedFrames) {
decoderConfigBuilder().expectMaskedFrames(expectMaskedFrames);
return this;
}
public Builder allowMaskMismatch(boolean allowMaskMismatch) {
decoderConfigBuilder().allowMaskMismatch(allowMaskMismatch);
return this;
}
public Builder allowExtensions(boolean allowExtensions) {
decoderConfigBuilder().allowExtensions(allowExtensions);
return this;
}
public Builder closeOnProtocolViolation(boolean closeOnProtocolViolation) {
decoderConfigBuilder().closeOnProtocolViolation(closeOnProtocolViolation);
return this;
}
public Builder withUTF8Validator(boolean withUTF8Validator) {
decoderConfigBuilder().withUTF8Validator(withUTF8Validator);
return this;
}
/**
* Build unmodifiable server protocol configuration.
*/
public WebSocketServerProtocolConfig build() {
return new WebSocketServerProtocolConfig(
websocketPath,
subprotocols,
checkStartsWith,
handshakeTimeoutMillis,
handleCloseFrames,
dropPongFrames,
decoderConfigBuilder == null ? decoderConfig : decoderConfigBuilder.build()
);
}
}
}

View File

@ -33,6 +33,7 @@ import io.netty.util.AttributeKey;
import java.util.List;
import static io.netty.handler.codec.http.HttpVersion.*;
import static io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig.DEFAULT;
import static io.netty.util.internal.ObjectUtil.*;
/**
@ -103,16 +104,21 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
private static final AttributeKey<WebSocketServerHandshaker> HANDSHAKER_ATTR_KEY =
AttributeKey.valueOf(WebSocketServerHandshaker.class, "HANDSHAKER");
private static final long DEFAULT_HANDSHAKE_TIMEOUT_MS = 10000L;
private final WebSocketServerProtocolConfig serverConfig;
private final String websocketPath;
private final String subprotocols;
private final boolean checkStartsWith;
private final long handshakeTimeoutMillis;
private final WebSocketDecoderConfig decoderConfig;
/**
* Base constructor
*
* @param serverConfig
* Server protocol configuration.
*/
public WebSocketServerProtocolHandler(WebSocketServerProtocolConfig serverConfig) {
super(checkNotNull(serverConfig, "serverConfig").dropPongFrames());
this.serverConfig = serverConfig;
}
public WebSocketServerProtocolHandler(String websocketPath) {
this(websocketPath, DEFAULT_HANDSHAKE_TIMEOUT_MS);
this(websocketPath, DEFAULT.handshakeTimeoutMillis());
}
public WebSocketServerProtocolHandler(String websocketPath, long handshakeTimeoutMillis) {
@ -120,7 +126,7 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
}
public WebSocketServerProtocolHandler(String websocketPath, boolean checkStartsWith) {
this(websocketPath, checkStartsWith, DEFAULT_HANDSHAKE_TIMEOUT_MS);
this(websocketPath, checkStartsWith, DEFAULT.handshakeTimeoutMillis());
}
public WebSocketServerProtocolHandler(String websocketPath, boolean checkStartsWith, long handshakeTimeoutMillis) {
@ -128,7 +134,7 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
}
public WebSocketServerProtocolHandler(String websocketPath, String subprotocols) {
this(websocketPath, subprotocols, DEFAULT_HANDSHAKE_TIMEOUT_MS);
this(websocketPath, subprotocols, DEFAULT.handshakeTimeoutMillis());
}
public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, long handshakeTimeoutMillis) {
@ -136,7 +142,7 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
}
public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions) {
this(websocketPath, subprotocols, allowExtensions, DEFAULT_HANDSHAKE_TIMEOUT_MS);
this(websocketPath, subprotocols, allowExtensions, DEFAULT.handshakeTimeoutMillis());
}
public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions,
@ -146,7 +152,7 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
public WebSocketServerProtocolHandler(String websocketPath, String subprotocols,
boolean allowExtensions, int maxFrameSize) {
this(websocketPath, subprotocols, allowExtensions, maxFrameSize, DEFAULT_HANDSHAKE_TIMEOUT_MS);
this(websocketPath, subprotocols, allowExtensions, maxFrameSize, DEFAULT.handshakeTimeoutMillis());
}
public WebSocketServerProtocolHandler(String websocketPath, String subprotocols,
@ -157,7 +163,7 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
public WebSocketServerProtocolHandler(String websocketPath, String subprotocols,
boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch) {
this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch,
DEFAULT_HANDSHAKE_TIMEOUT_MS);
DEFAULT.handshakeTimeoutMillis());
}
public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions,
@ -169,7 +175,7 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
public WebSocketServerProtocolHandler(String websocketPath, String subprotocols,
boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith) {
this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, checkStartsWith,
DEFAULT_HANDSHAKE_TIMEOUT_MS);
DEFAULT.handshakeTimeoutMillis());
}
public WebSocketServerProtocolHandler(String websocketPath, String subprotocols,
@ -183,7 +189,7 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch,
boolean checkStartsWith, boolean dropPongFrames) {
this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, checkStartsWith,
dropPongFrames, DEFAULT_HANDSHAKE_TIMEOUT_MS);
dropPongFrames, DEFAULT.handshakeTimeoutMillis());
}
public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions,
@ -200,12 +206,14 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean checkStartsWith,
boolean dropPongFrames, long handshakeTimeoutMillis,
WebSocketDecoderConfig decoderConfig) {
super(dropPongFrames);
this.websocketPath = websocketPath;
this.subprotocols = subprotocols;
this.checkStartsWith = checkStartsWith;
this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis");
this.decoderConfig = checkNotNull(decoderConfig, "decoderConfig");
this(WebSocketServerProtocolConfig.newBuilder()
.websocketPath(websocketPath)
.subprotocols(subprotocols)
.checkStartsWith(checkStartsWith)
.handshakeTimeoutMillis(handshakeTimeoutMillis)
.dropPongFrames(dropPongFrames)
.decoderConfig(decoderConfig)
.build());
}
@Override
@ -214,10 +222,9 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
if (cp.get(WebSocketServerProtocolHandshakeHandler.class) == null) {
// Add the WebSocketHandshakeHandler before this one.
cp.addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(),
new WebSocketServerProtocolHandshakeHandler(
websocketPath, subprotocols, checkStartsWith, handshakeTimeoutMillis, decoderConfig));
new WebSocketServerProtocolHandshakeHandler(serverConfig));
}
if (decoderConfig.withUTF8Validator() && cp.get(Utf8FrameValidator.class) == null) {
if (serverConfig.decoderConfig().withUTF8Validator() && cp.get(Utf8FrameValidator.class) == null) {
// Add the UFT8 checking before this one.
cp.addBefore(ctx.name(), Utf8FrameValidator.class.getName(),
new Utf8FrameValidator());
@ -226,7 +233,7 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
@Override
protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception {
if (frame instanceof CloseWebSocketFrame) {
if (serverConfig.handleCloseFrames() && frame instanceof CloseWebSocketFrame) {
WebSocketServerHandshaker handshaker = getHandshaker(ctx.channel());
if (handshaker != null) {
frame.retain();

View File

@ -44,21 +44,12 @@ import static io.netty.util.internal.ObjectUtil.*;
*/
class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapter {
private final String websocketPath;
private final String subprotocols;
private final boolean checkStartsWith;
private final long handshakeTimeoutMillis;
private final WebSocketDecoderConfig decoderConfig;
private final WebSocketServerProtocolConfig serverConfig;
private ChannelHandlerContext ctx;
private ChannelPromise handshakePromise;
WebSocketServerProtocolHandshakeHandler(String websocketPath, String subprotocols,
boolean checkStartsWith, long handshakeTimeoutMillis, WebSocketDecoderConfig decoderConfig) {
this.websocketPath = websocketPath;
this.subprotocols = subprotocols;
this.checkStartsWith = checkStartsWith;
this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis");
this.decoderConfig = checkNotNull(decoderConfig, "decoderConfig");
WebSocketServerProtocolHandshakeHandler(WebSocketServerProtocolConfig serverConfig) {
this.serverConfig = checkNotNull(serverConfig, "serverConfig");
}
@Override
@ -82,7 +73,8 @@ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapt
}
final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
getWebSocketLocation(ctx.pipeline(), req, websocketPath), subprotocols, decoderConfig);
getWebSocketLocation(ctx.pipeline(), req, serverConfig.websocketPath()),
serverConfig.subprotocols(), serverConfig.decoderConfig());
final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
final ChannelPromise localHandshakePromise = handshakePromise;
if (handshaker == null) {
@ -123,7 +115,8 @@ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapt
}
private boolean isNotWebSocketPath(FullHttpRequest req) {
return checkStartsWith ? !req.uri().startsWith(websocketPath) : !req.uri().equals(websocketPath);
String websocketPath = serverConfig.websocketPath();
return serverConfig.checkStartsWith() ? !req.uri().startsWith(websocketPath) : !req.uri().equals(websocketPath);
}
private static void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) {
@ -145,7 +138,7 @@ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapt
private void applyHandshakeTimeout() {
final ChannelPromise localHandshakePromise = handshakePromise;
final long handshakeTimeoutMillis = this.handshakeTimeoutMillis;
final long handshakeTimeoutMillis = serverConfig.handshakeTimeoutMillis();
if (handshakeTimeoutMillis <= 0 || localHandshakePromise.isDone()) {
return;
}

View File

@ -271,12 +271,25 @@ public class WebSocketHandshakeHandOverTest {
}
private static EmbeddedChannel createClientChannel(ChannelHandler handler) throws Exception {
return createClientChannel(handler, WebSocketClientProtocolConfig.newBuilder()
.webSocketUri("ws://localhost:1234/test")
.subprotocol("test-proto-2")
.build());
}
private static EmbeddedChannel createClientChannel(ChannelHandler handler, long timeoutMillis) throws Exception {
return createClientChannel(handler, WebSocketClientProtocolConfig.newBuilder()
.webSocketUri("ws://localhost:1234/test")
.subprotocol("test-proto-2")
.handshakeTimeoutMillis(timeoutMillis)
.build());
}
private static EmbeddedChannel createClientChannel(ChannelHandler handler, WebSocketClientProtocolConfig config) {
return new EmbeddedChannel(
new HttpClientCodec(),
new HttpObjectAggregator(8192),
new WebSocketClientProtocolHandler(new URI("ws://localhost:1234/test"),
WebSocketVersion.V13, "test-proto-2",
false, null, 65536),
new WebSocketClientProtocolHandler(config),
handler);
}
@ -306,14 +319,4 @@ public class WebSocketHandshakeHandOverTest {
webSocketHandler,
handler);
}
private static EmbeddedChannel createClientChannel(ChannelHandler handler, long timeoutMillis) throws Exception {
return new EmbeddedChannel(
new HttpClientCodec(),
new HttpObjectAggregator(8192),
new WebSocketClientProtocolHandler(new URI("ws://localhost:1234/test"),
WebSocketVersion.V13, "test-proto-2",
false, null, 65536, timeoutMillis),
handler);
}
}

View File

@ -145,12 +145,13 @@ public class WebSocketServerProtocolHandlerTest {
@Test
public void testCreateUTF8Validator() {
WebSocketDecoderConfig config = WebSocketDecoderConfig.newBuilder()
WebSocketServerProtocolConfig config = WebSocketServerProtocolConfig.newBuilder()
.websocketPath("/test")
.withUTF8Validator(true)
.build();
EmbeddedChannel ch = new EmbeddedChannel(
new WebSocketServerProtocolHandler("/test", null, false, false, 1000L, config),
new WebSocketServerProtocolHandler(config),
new HttpRequestDecoder(),
new HttpResponseEncoder(),
new MockOutboundHandler());
@ -165,12 +166,13 @@ public class WebSocketServerProtocolHandlerTest {
@Test
public void testDoNotCreateUTF8Validator() {
WebSocketDecoderConfig config = WebSocketDecoderConfig.newBuilder()
WebSocketServerProtocolConfig config = WebSocketServerProtocolConfig.newBuilder()
.websocketPath("/test")
.withUTF8Validator(false)
.build();
EmbeddedChannel ch = new EmbeddedChannel(
new WebSocketServerProtocolHandler("/test", null, false, false, 1000L, config),
new WebSocketServerProtocolHandler(config),
new HttpRequestDecoder(),
new HttpResponseEncoder(),
new MockOutboundHandler());
@ -211,7 +213,7 @@ public class WebSocketServerProtocolHandlerTest {
private EmbeddedChannel createChannel(ChannelHandler handler) {
return new EmbeddedChannel(
new WebSocketServerProtocolHandler("/test", null, false),
new WebSocketServerProtocolHandler("/test"),
new HttpRequestDecoder(),
new HttpResponseEncoder(),
new MockOutboundHandler(),