working on websocket
This commit is contained in:
parent
2f2050a6b3
commit
844b38f219
53 changed files with 5246 additions and 321 deletions
|
@ -1,8 +1,10 @@
|
|||
import org.xbib.netty.http.server.api.ServerCertificateProvider;
|
||||
|
||||
module org.xbib.netty.http.bouncycastle {
|
||||
exports org.xbib.netty.http.bouncycastle;
|
||||
requires org.xbib.netty.http.server.api;
|
||||
requires org.bouncycastle.pkix;
|
||||
requires org.bouncycastle.provider;
|
||||
provides org.xbib.netty.http.server.api.security.ServerCertificateProvider with
|
||||
provides ServerCertificateProvider with
|
||||
org.xbib.netty.http.bouncycastle.BouncyCastleSelfSignedCertificateProvider;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package org.xbib.netty.http.bouncycastle;
|
||||
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.xbib.netty.http.server.api.security.ServerCertificateProvider;
|
||||
import org.xbib.netty.http.server.api.ServerCertificateProvider;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
|
|
|
@ -12,6 +12,7 @@ import io.netty.handler.codec.http.HttpMethod;
|
|||
import io.netty.handler.codec.http.HttpUtil;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
|
||||
import io.netty.handler.codec.http2.HttpConversionUtil;
|
||||
import io.netty.util.AsciiString;
|
||||
import org.xbib.net.URL;
|
||||
|
@ -76,12 +77,28 @@ public final class Request implements AutoCloseable {
|
|||
|
||||
private TimeoutListener timeoutListener;
|
||||
|
||||
private Request(URL url, HttpVersion httpVersion, HttpMethod httpMethod,
|
||||
HttpHeaders headers, Collection<Cookie> cookies, ByteBuf content, List<InterfaceHttpData> bodyData,
|
||||
long timeoutInMillis, boolean followRedirect, int maxRedirect, int redirectCount,
|
||||
boolean isBackOff, BackOff backOff,
|
||||
ResponseListener<HttpResponse> responseListener, ExceptionListener exceptionListener,
|
||||
TimeoutListener timeoutListener) {
|
||||
private final WebSocketFrame webSocketFrame;
|
||||
|
||||
private final WebSocketResponseListener<WebSocketFrame> webSocketResponseListener;
|
||||
|
||||
private Request(URL url,
|
||||
HttpVersion httpVersion,
|
||||
HttpMethod httpMethod,
|
||||
HttpHeaders headers,
|
||||
Collection<Cookie> cookies,
|
||||
ByteBuf content,
|
||||
List<InterfaceHttpData> bodyData,
|
||||
long timeoutInMillis,
|
||||
boolean followRedirect,
|
||||
int maxRedirect,
|
||||
int redirectCount,
|
||||
boolean isBackOff,
|
||||
BackOff backOff,
|
||||
ResponseListener<HttpResponse> responseListener,
|
||||
ExceptionListener exceptionListener,
|
||||
TimeoutListener timeoutListener,
|
||||
WebSocketFrame webSocketFrame,
|
||||
WebSocketResponseListener<WebSocketFrame> webSocketResponseListener) {
|
||||
this.url = url;
|
||||
this.httpVersion = httpVersion;
|
||||
this.httpMethod = httpMethod;
|
||||
|
@ -98,6 +115,8 @@ public final class Request implements AutoCloseable {
|
|||
this.responseListener = responseListener;
|
||||
this.exceptionListener = exceptionListener;
|
||||
this.timeoutListener = timeoutListener;
|
||||
this.webSocketFrame = webSocketFrame;
|
||||
this.webSocketResponseListener = webSocketResponseListener;
|
||||
}
|
||||
|
||||
public URL url() {
|
||||
|
@ -157,6 +176,14 @@ public final class Request implements AutoCloseable {
|
|||
return backOff;
|
||||
}
|
||||
|
||||
public WebSocketFrame getWebSocketFrame() {
|
||||
return webSocketFrame;
|
||||
}
|
||||
|
||||
public WebSocketResponseListener<WebSocketFrame> getWebSocketResponseListener() {
|
||||
return webSocketResponseListener;
|
||||
}
|
||||
|
||||
public boolean canRedirect() {
|
||||
if (!followRedirect) {
|
||||
return false;
|
||||
|
@ -356,6 +383,10 @@ public final class Request implements AutoCloseable {
|
|||
|
||||
private TimeoutListener timeoutListener;
|
||||
|
||||
private WebSocketFrame webSocketFrame;
|
||||
|
||||
private WebSocketResponseListener<WebSocketFrame> webSocketResponseListener;
|
||||
|
||||
Builder(ByteBufAllocator allocator) {
|
||||
this.allocator = allocator;
|
||||
this.httpMethod = DEFAULT_METHOD;
|
||||
|
@ -622,6 +653,16 @@ public final class Request implements AutoCloseable {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder setWebSocketFrame(WebSocketFrame webSocketFrame) {
|
||||
this.webSocketFrame = webSocketFrame;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setWebSocketResponseListener(WebSocketResponseListener<WebSocketFrame> webSocketResponseListener) {
|
||||
this.webSocketResponseListener = webSocketResponseListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Request build() {
|
||||
DefaultHttpHeaders validatedHeaders = new DefaultHttpHeaders(true);
|
||||
validatedHeaders.set(headers);
|
||||
|
@ -670,7 +711,7 @@ public final class Request implements AutoCloseable {
|
|||
}
|
||||
return new Request(url, httpVersion, httpMethod, validatedHeaders, cookies, content, bodyData,
|
||||
timeoutInMillis, followRedirect, maxRedirects, 0, enableBackOff, backOff,
|
||||
responseListener, exceptionListener, timeoutListener);
|
||||
responseListener, exceptionListener, timeoutListener, webSocketFrame, webSocketResponseListener);
|
||||
}
|
||||
|
||||
private void addHeader(AsciiString name, Object value) {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package org.xbib.netty.http.client.api;
|
||||
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface WebSocketResponseListener<F extends WebSocketFrame> {
|
||||
|
||||
void onResponse(F frame);
|
||||
}
|
8
netty-http-client/NOTICE.txt
Normal file
8
netty-http-client/NOTICE.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
http2 web socket implementation based on the work of Maksym Ostroverkhov
|
||||
|
||||
https://github.com/jauntsdn/netty-websocket-http2/
|
||||
|
||||
Apache License 2.0
|
||||
|
||||
forked at 20 October 2021
|
|
@ -245,7 +245,7 @@ public class ClientConfig {
|
|||
|
||||
private Boolean poolSecure = Defaults.POOL_SECURE;
|
||||
|
||||
private List<String> serverNamesForIdentification = new ArrayList<>();
|
||||
private final List<String> serverNamesForIdentification = new ArrayList<>();
|
||||
|
||||
private Http2Settings http2Settings = Defaults.HTTP2_SETTINGS;
|
||||
|
||||
|
|
|
@ -4,9 +4,12 @@ import io.netty.channel.Channel;
|
|||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.codec.http.DefaultHttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpClientCodec;
|
||||
import io.netty.handler.codec.http.HttpContentDecompressor;
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
|
||||
import io.netty.handler.logging.LogLevel;
|
||||
import io.netty.handler.ssl.ApplicationProtocolNames;
|
||||
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
|
||||
|
@ -14,9 +17,12 @@ import io.netty.handler.ssl.SslHandler;
|
|||
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||
import org.xbib.netty.http.client.Client;
|
||||
import org.xbib.netty.http.client.ClientConfig;
|
||||
import org.xbib.netty.http.client.handler.ws1.Http1WebSocketClientHandler;
|
||||
import org.xbib.netty.http.common.HttpChannelInitializer;
|
||||
import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
@ -96,17 +102,23 @@ public class Http1ChannelInitializer extends ChannelInitializer<Channel> impleme
|
|||
|
||||
private void configureCleartext(Channel channel) {
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
//pipeline.addLast("client-chunk-compressor", new HttpChunkContentCompressor(6));
|
||||
pipeline.addLast("http-client-chunk-writer", new ChunkedWriteHandler());
|
||||
pipeline.addLast("http-client-codec", new HttpClientCodec(clientConfig.getMaxInitialLineLength(),
|
||||
pipeline.addLast("http-client-chunk-writer",
|
||||
new ChunkedWriteHandler());
|
||||
pipeline.addLast("http-client-codec",
|
||||
new HttpClientCodec(clientConfig.getMaxInitialLineLength(),
|
||||
clientConfig.getMaxHeadersSize(), clientConfig.getMaxChunkSize()));
|
||||
if (clientConfig.isEnableGzip()) {
|
||||
pipeline.addLast("http-client-decompressor", new HttpContentDecompressor());
|
||||
}
|
||||
HttpObjectAggregator httpObjectAggregator = new HttpObjectAggregator(clientConfig.getMaxContentLength(),
|
||||
false);
|
||||
HttpObjectAggregator httpObjectAggregator =
|
||||
new HttpObjectAggregator(clientConfig.getMaxContentLength(), false);
|
||||
httpObjectAggregator.setMaxCumulationBufferComponents(clientConfig.getMaxCompositeBufferComponents());
|
||||
pipeline.addLast("http-client-aggregator", httpObjectAggregator);
|
||||
pipeline.addLast("http-client-handler", httpResponseHandler);
|
||||
pipeline.addLast("http-client-aggregator",
|
||||
httpObjectAggregator);
|
||||
//pipeline.addLast( "http-client-ws-protocol-handler",
|
||||
// new Http1WebSocketClientHandler(WebSocketClientHandshakerFactory.newHandshaker(URI.create("/websocket"),
|
||||
// WebSocketVersion.V13, null, false, new DefaultHttpHeaders())));
|
||||
pipeline.addLast("http-client-handler",
|
||||
httpResponseHandler);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,6 @@ import io.netty.handler.codec.http.HttpContentCompressor;
|
|||
*/
|
||||
public class HttpChunkContentCompressor extends HttpContentCompressor {
|
||||
|
||||
HttpChunkContentCompressor(int compressionLevel) {
|
||||
super(compressionLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
|
||||
if (msg instanceof ByteBuf) {
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package org.xbib.netty.http.client.handler.ws1;
|
||||
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class Http1WebSocketClientHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
final WebSocketClientHandshaker handshaker;
|
||||
|
||||
public Http1WebSocketClientHandler(WebSocketClientHandshaker handshaker) {
|
||||
this.handshaker = handshaker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||
ctx.fireChannelActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
ctx.fireChannelInactive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (msg instanceof CloseWebSocketFrame) {
|
||||
handshaker.close(ctx.channel(), (CloseWebSocketFrame) msg)
|
||||
.addListener(ChannelFutureListener.CLOSE);
|
||||
} else {
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||
if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) {
|
||||
String actualProtocol = handshaker.actualSubprotocol();
|
||||
if (actualProtocol.equals("")) {
|
||||
}
|
||||
else {
|
||||
throw new IOException("Invalid Websocket Protocol");
|
||||
}
|
||||
} else {
|
||||
ctx.fireUserEventTriggered(evt);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
package org.xbib.netty.http.client.handler.ws2;
|
||||
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateClientExtensionHandshaker;
|
||||
import org.xbib.netty.http.common.ws.Preconditions;
|
||||
|
||||
/**
|
||||
* Builder for {@link Http2WebSocketClientHandler}
|
||||
*/
|
||||
public final class Http2WebSocketClientBuilder {
|
||||
|
||||
private static final short DEFAULT_STREAM_WEIGHT = 16;
|
||||
|
||||
private static final boolean MASK_PAYLOAD = true;
|
||||
|
||||
private WebSocketDecoderConfig webSocketDecoderConfig;
|
||||
|
||||
private PerMessageDeflateClientExtensionHandshaker perMessageDeflateClientExtensionHandshaker;
|
||||
|
||||
private long handshakeTimeoutMillis = 15_000;
|
||||
|
||||
private short streamWeight;
|
||||
|
||||
private long closedWebSocketRemoveTimeoutMillis = 30_000;
|
||||
|
||||
private boolean isSingleWebSocketPerConnection;
|
||||
|
||||
Http2WebSocketClientBuilder() {}
|
||||
|
||||
/** @return new {@link Http2WebSocketClientBuilder} instance */
|
||||
public static Http2WebSocketClientBuilder create() {
|
||||
return new Http2WebSocketClientBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param webSocketDecoderConfig websocket decoder configuration. Must be non-null
|
||||
* @return this {@link Http2WebSocketClientBuilder} instance
|
||||
*/
|
||||
public Http2WebSocketClientBuilder decoderConfig(WebSocketDecoderConfig webSocketDecoderConfig) {
|
||||
this.webSocketDecoderConfig = Preconditions.requireNonNull(webSocketDecoderConfig, "webSocketDecoderConfig");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param handshakeTimeoutMillis websocket handshake timeout. Must be positive
|
||||
* @return this {@link Http2WebSocketClientBuilder} instance
|
||||
*/
|
||||
public Http2WebSocketClientBuilder handshakeTimeoutMillis(long handshakeTimeoutMillis) {
|
||||
this.handshakeTimeoutMillis =
|
||||
Preconditions.requirePositive(handshakeTimeoutMillis, "handshakeTimeoutMillis");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param closedWebSocketRemoveTimeoutMillis delay until websockets handler forgets closed
|
||||
* websocket. Necessary to gracefully handle incoming http2 frames racing with outgoing stream
|
||||
* termination frame.
|
||||
* @return this {@link Http2WebSocketClientBuilder} instance
|
||||
*/
|
||||
public Http2WebSocketClientBuilder closedWebSocketRemoveTimeoutMillis(long closedWebSocketRemoveTimeoutMillis) {
|
||||
this.closedWebSocketRemoveTimeoutMillis =
|
||||
Preconditions.requirePositive(closedWebSocketRemoveTimeoutMillis, "closedWebSocketRemoveTimeoutMillis");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isCompressionEnabled enables permessage-deflate compression with default configuration
|
||||
* @return this {@link Http2WebSocketClientBuilder} instance
|
||||
*/
|
||||
public Http2WebSocketClientBuilder compression(boolean isCompressionEnabled) {
|
||||
if (isCompressionEnabled) {
|
||||
if (perMessageDeflateClientExtensionHandshaker == null) {
|
||||
perMessageDeflateClientExtensionHandshaker =
|
||||
new PerMessageDeflateClientExtensionHandshaker();
|
||||
}
|
||||
} else {
|
||||
perMessageDeflateClientExtensionHandshaker = null;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables permessage-deflate compression with extended configuration. Parameters are described in
|
||||
* netty's PerMessageDeflateClientExtensionHandshaker
|
||||
*
|
||||
* @param compressionLevel sets compression level. Range is [0; 9], default is 6
|
||||
* @param allowClientWindowSize allows server to customize the client's inflater window size,
|
||||
* default is false
|
||||
* @param requestedServerWindowSize requested server window size if server inflater is
|
||||
* customizable
|
||||
* @param allowClientNoContext allows server to activate client_no_context_takeover, default is
|
||||
* false
|
||||
* @param requestedServerNoContext whether client needs to activate server_no_context_takeover if
|
||||
* server is compatible, default is false.
|
||||
* @return this {@link Http2WebSocketClientBuilder} instance
|
||||
*/
|
||||
public Http2WebSocketClientBuilder compression(int compressionLevel, boolean allowClientWindowSize,
|
||||
int requestedServerWindowSize, boolean allowClientNoContext, boolean requestedServerNoContext) {
|
||||
perMessageDeflateClientExtensionHandshaker =
|
||||
new PerMessageDeflateClientExtensionHandshaker(compressionLevel, allowClientWindowSize,
|
||||
requestedServerWindowSize, allowClientNoContext, requestedServerNoContext);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param weight sets websocket http2 stream weight. Must belong to [1; 256] range
|
||||
* @return this {@link Http2WebSocketClientBuilder} instance
|
||||
*/
|
||||
public Http2WebSocketClientBuilder streamWeight(int weight) {
|
||||
this.streamWeight = Preconditions.requireRange(weight, 1, 256, "streamWeight");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isSingleWebSocketPerConnection optimize for at most 1 websocket per connection
|
||||
* @return this {@link Http2WebSocketClientBuilder} instance
|
||||
*/
|
||||
public Http2WebSocketClientBuilder assumeSingleWebSocketPerConnection(boolean isSingleWebSocketPerConnection) {
|
||||
this.isSingleWebSocketPerConnection = isSingleWebSocketPerConnection;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** @return new {@link Http2WebSocketClientHandler} instance */
|
||||
public Http2WebSocketClientHandler build() {
|
||||
PerMessageDeflateClientExtensionHandshaker compressionHandshaker = perMessageDeflateClientExtensionHandshaker;
|
||||
boolean hasCompression = compressionHandshaker != null;
|
||||
WebSocketDecoderConfig config = webSocketDecoderConfig;
|
||||
if (config == null) {
|
||||
config = WebSocketDecoderConfig.newBuilder()
|
||||
.expectMaskedFrames(false)
|
||||
.allowMaskMismatch(false)
|
||||
.allowExtensions(hasCompression)
|
||||
.build();
|
||||
} else {
|
||||
boolean isAllowExtensions = config.allowExtensions();
|
||||
if (!isAllowExtensions && hasCompression) {
|
||||
config = config.toBuilder().allowExtensions(true).build();
|
||||
}
|
||||
}
|
||||
short weight = streamWeight;
|
||||
if (weight == 0) {
|
||||
weight = DEFAULT_STREAM_WEIGHT;
|
||||
}
|
||||
return new Http2WebSocketClientHandler(config, MASK_PAYLOAD, weight, handshakeTimeoutMillis,
|
||||
closedWebSocketRemoveTimeoutMillis, compressionHandshaker, isSingleWebSocketPerConnection);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
package org.xbib.netty.http.client.handler.ws2;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.EventLoop;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateClientExtensionHandshaker;
|
||||
import io.netty.handler.codec.http2.*;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocket;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketChannelHandler;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketProtocol;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketValidator;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
|
||||
|
||||
/**
|
||||
* Provides client-side support for websocket-over-http2. Creates sub channel for http2 stream of
|
||||
* successfully handshaked websocket. Subchannel is compatible with http1 websocket handlers. Should
|
||||
* be used in tandem with {@link Http2WebSocketClientHandshaker}
|
||||
*/
|
||||
public final class Http2WebSocketClientHandler extends Http2WebSocketChannelHandler {
|
||||
|
||||
private static final AtomicReferenceFieldUpdater<Http2WebSocketClientHandler, Http2WebSocketClientHandshaker> HANDSHAKER =
|
||||
AtomicReferenceFieldUpdater.newUpdater(Http2WebSocketClientHandler.class, Http2WebSocketClientHandshaker.class, "handshaker");
|
||||
|
||||
private final long handshakeTimeoutMillis;
|
||||
|
||||
private final PerMessageDeflateClientExtensionHandshaker compressionHandshaker;
|
||||
|
||||
private final short streamWeight;
|
||||
|
||||
private CharSequence scheme;
|
||||
|
||||
private Boolean supportsWebSocket;
|
||||
|
||||
private boolean supportsWebSocketCalled;
|
||||
|
||||
private volatile Http2Connection.Endpoint<Http2LocalFlowController> streamIdFactory;
|
||||
|
||||
private volatile Http2WebSocketClientHandshaker handshaker;
|
||||
|
||||
Http2WebSocketClientHandler(
|
||||
WebSocketDecoderConfig webSocketDecoderConfig,
|
||||
boolean isEncoderMaskPayload,
|
||||
short streamWeight,
|
||||
long handshakeTimeoutMillis,
|
||||
long closedWebSocketRemoveTimeoutMillis,
|
||||
PerMessageDeflateClientExtensionHandshaker compressionHandshaker,
|
||||
boolean isSingleWebSocketPerConnection) {
|
||||
super(
|
||||
webSocketDecoderConfig,
|
||||
isEncoderMaskPayload,
|
||||
closedWebSocketRemoveTimeoutMillis,
|
||||
isSingleWebSocketPerConnection);
|
||||
this.streamWeight = streamWeight;
|
||||
this.handshakeTimeoutMillis = handshakeTimeoutMillis;
|
||||
this.compressionHandshaker = compressionHandshaker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
|
||||
super.handlerAdded(ctx);
|
||||
this.scheme =
|
||||
ctx.pipeline().get(SslHandler.class) != null
|
||||
? Http2WebSocketProtocol.SCHEME_HTTPS
|
||||
: Http2WebSocketProtocol.SCHEME_HTTP;
|
||||
this.streamIdFactory = http2Handler.connection().local();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings)
|
||||
throws Http2Exception {
|
||||
if (supportsWebSocket != null) {
|
||||
super.onSettingsRead(ctx, settings);
|
||||
return;
|
||||
}
|
||||
Long extendedConnectEnabled =
|
||||
settings.get(Http2WebSocketProtocol.SETTINGS_ENABLE_CONNECT_PROTOCOL);
|
||||
boolean supports =
|
||||
supportsWebSocket = extendedConnectEnabled != null && extendedConnectEnabled == 1;
|
||||
Http2WebSocketClientHandshaker listener = HANDSHAKER.get(this);
|
||||
if (listener != null) {
|
||||
supportsWebSocketCalled = true;
|
||||
listener.onSupportsWebSocket(supports);
|
||||
}
|
||||
super.onSettingsRead(ctx, settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeadersRead(
|
||||
ChannelHandlerContext ctx,
|
||||
int streamId,
|
||||
Http2Headers headers,
|
||||
int padding,
|
||||
boolean endOfStream)
|
||||
throws Http2Exception {
|
||||
boolean proceed = handshakeWebSocket(streamId, headers, endOfStream);
|
||||
if (proceed) {
|
||||
next().onHeadersRead(ctx, streamId, headers, padding, endOfStream);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeadersRead(
|
||||
ChannelHandlerContext ctx,
|
||||
int streamId,
|
||||
Http2Headers headers,
|
||||
int streamDependency,
|
||||
short weight,
|
||||
boolean exclusive,
|
||||
int padding,
|
||||
boolean endOfStream)
|
||||
throws Http2Exception {
|
||||
boolean proceed = handshakeWebSocket(streamId, headers, endOfStream);
|
||||
if (proceed) {
|
||||
next().onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endOfStream);
|
||||
}
|
||||
}
|
||||
|
||||
Http2WebSocketClientHandshaker handShaker() {
|
||||
Http2WebSocketClientHandshaker h = HANDSHAKER.get(this);
|
||||
if (h != null) {
|
||||
return h;
|
||||
}
|
||||
Http2Connection.Endpoint<Http2LocalFlowController> streamIdFactory = this.streamIdFactory;
|
||||
if (streamIdFactory == null) {
|
||||
throw new IllegalStateException(
|
||||
"webSocket handshaker cant be created before channel is registered");
|
||||
}
|
||||
Http2WebSocketClientHandshaker handShaker =
|
||||
new Http2WebSocketClientHandshaker(
|
||||
webSocketsParent,
|
||||
streamIdFactory,
|
||||
config,
|
||||
isEncoderMaskPayload,
|
||||
streamWeight,
|
||||
scheme,
|
||||
handshakeTimeoutMillis,
|
||||
compressionHandshaker);
|
||||
|
||||
if (HANDSHAKER.compareAndSet(this, null, handShaker)) {
|
||||
EventLoop el = ctx.channel().eventLoop();
|
||||
if (el.inEventLoop()) {
|
||||
onSupportsWebSocket(handShaker);
|
||||
} else {
|
||||
el.execute(() -> onSupportsWebSocket(handShaker));
|
||||
}
|
||||
return handShaker;
|
||||
}
|
||||
return HANDSHAKER.get(this);
|
||||
}
|
||||
|
||||
private boolean handshakeWebSocket(int streamId, Http2Headers responseHeaders, boolean endOfStream) {
|
||||
Http2WebSocket webSocket = webSocketRegistry.get(streamId);
|
||||
if (webSocket != null) {
|
||||
if (!Http2WebSocketValidator.isValid(responseHeaders)) {
|
||||
handShaker().reject(streamId, webSocket, responseHeaders, endOfStream);
|
||||
} else {
|
||||
handShaker().handshake(webSocket, responseHeaders, endOfStream);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onSupportsWebSocket(Http2WebSocketClientHandshaker handshaker) {
|
||||
if (supportsWebSocketCalled) {
|
||||
return;
|
||||
}
|
||||
Boolean supports = supportsWebSocket;
|
||||
if (supports != null) {
|
||||
handshaker.onSupportsWebSocket(supports);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,502 @@
|
|||
package org.xbib.netty.http.client.handler.ws2;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.channel.EventLoop;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtension;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateClientExtensionHandshaker;
|
||||
import io.netty.handler.codec.http2.DefaultHttp2Headers;
|
||||
import io.netty.handler.codec.http2.Http2Connection;
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
import io.netty.handler.codec.http2.Http2LocalFlowController;
|
||||
import io.netty.util.AsciiString;
|
||||
import io.netty.util.concurrent.Future;
|
||||
import io.netty.util.concurrent.GenericFutureListener;
|
||||
import io.netty.util.concurrent.ScheduledFuture;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocket;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketChannel;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketChannelHandler;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketEvent;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketExtensions;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketMessages;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketProtocol;
|
||||
import org.xbib.netty.http.common.ws.Preconditions;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Objects;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Establishes websocket-over-http2 on provided connection channel
|
||||
*/
|
||||
public final class Http2WebSocketClientHandshaker {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(Http2WebSocketClientHandshaker.class.getName());
|
||||
|
||||
private static final int ESTIMATED_DEFERRED_HANDSHAKES = 4;
|
||||
|
||||
private static final AtomicIntegerFieldUpdater<Http2WebSocketClientHandshaker> WEBSOCKET_CHANNEL_SERIAL =
|
||||
AtomicIntegerFieldUpdater.newUpdater(Http2WebSocketClientHandshaker.class, "webSocketChannelSerial");
|
||||
|
||||
private static final Http2Headers EMPTY_HEADERS = new DefaultHttp2Headers(false);
|
||||
|
||||
private final Http2Connection.Endpoint<Http2LocalFlowController> streamIdFactory;
|
||||
|
||||
private final WebSocketDecoderConfig webSocketDecoderConfig;
|
||||
|
||||
private final Http2WebSocketChannelHandler.WebSocketsParent webSocketsParent;
|
||||
|
||||
private final short streamWeight;
|
||||
|
||||
private final CharSequence scheme;
|
||||
|
||||
private final PerMessageDeflateClientExtensionHandshaker compressionHandshaker;
|
||||
|
||||
private final boolean isEncoderMaskPayload;
|
||||
|
||||
private final long timeoutMillis;
|
||||
|
||||
private Queue<Handshake> deferred;
|
||||
|
||||
private Boolean supportsWebSocket;
|
||||
|
||||
private volatile int webSocketChannelSerial;
|
||||
|
||||
private CharSequence compressionExtensionHeader;
|
||||
|
||||
Http2WebSocketClientHandshaker(Http2WebSocketChannelHandler.WebSocketsParent webSocketsParent,
|
||||
Http2Connection.Endpoint<Http2LocalFlowController> streamIdFactory,
|
||||
WebSocketDecoderConfig webSocketDecoderConfig,
|
||||
boolean isEncoderMaskPayload,
|
||||
short streamWeight,
|
||||
CharSequence scheme,
|
||||
long handshakeTimeoutMillis, PerMessageDeflateClientExtensionHandshaker compressionHandshaker) {
|
||||
this.webSocketsParent = webSocketsParent;
|
||||
this.streamIdFactory = streamIdFactory;
|
||||
this.webSocketDecoderConfig = webSocketDecoderConfig;
|
||||
this.isEncoderMaskPayload = isEncoderMaskPayload;
|
||||
this.timeoutMillis = handshakeTimeoutMillis;
|
||||
this.streamWeight = streamWeight;
|
||||
this.scheme = scheme;
|
||||
this.compressionHandshaker = compressionHandshaker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link Http2WebSocketClientHandshaker} for given connection channel
|
||||
*
|
||||
* @param channel connection channel. Pipeline must contain {@link Http2WebSocketClientHandler}
|
||||
* and netty http2 codec (e.g. Http2ConnectionHandler or Http2FrameCodec)
|
||||
* @return new {@link Http2WebSocketClientHandshaker} instance
|
||||
*/
|
||||
public static Http2WebSocketClientHandshaker create(Channel channel) {
|
||||
Objects.requireNonNull(channel, "channel");
|
||||
return Preconditions.requireHandler(channel, Http2WebSocketClientHandler.class).handShaker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts websocket-over-http2 handshake using given path
|
||||
*
|
||||
* @param path websocket path, must be non-empty
|
||||
* @param webSocketHandler http1 websocket handler added to pipeline of subchannel created for
|
||||
* successfully handshaked http2 websocket
|
||||
* @return ChannelFuture with result of handshake. Its channel accepts http1 WebSocketFrames as
|
||||
* soon as this method returns.
|
||||
*/
|
||||
public ChannelFuture handshake(String path, ChannelHandler webSocketHandler) {
|
||||
return handshake(path, "", EMPTY_HEADERS, webSocketHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts websocket-over-http2 handshake using given path and request headers
|
||||
*
|
||||
* @param path websocket path, must be non-empty
|
||||
* @param requestHeaders request headers, must be non-null
|
||||
* @param webSocketHandler http1 websocket handler added to pipeline of subchannel created for
|
||||
* successfully handshaked http2 websocket
|
||||
* @return ChannelFuture with result of handshake. Its channel accepts http1 WebSocketFrames as
|
||||
* soon as this method returns.
|
||||
*/
|
||||
public ChannelFuture handshake(
|
||||
String path, Http2Headers requestHeaders, ChannelHandler webSocketHandler) {
|
||||
return handshake(path, "", requestHeaders, webSocketHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts websocket-over-http2 handshake using given path and subprotocol
|
||||
*
|
||||
* @param path websocket path, must be non-empty
|
||||
* @param subprotocol websocket subprotocol, must be non-null
|
||||
* @param webSocketHandler http1 websocket handler added to pipeline of subchannel created for
|
||||
* successfully handshaked http2 websocket
|
||||
* @return ChannelFuture with result of handshake. Its channel accepts http1 WebSocketFrames as
|
||||
* soon as this method returns.
|
||||
*/
|
||||
public ChannelFuture handshake(String path, String subprotocol, ChannelHandler webSocketHandler) {
|
||||
return handshake(path, subprotocol, EMPTY_HEADERS, webSocketHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts websocket-over-http2 handshake using given path, subprotocol and request headers
|
||||
*
|
||||
* @param path websocket path, must be non-empty
|
||||
* @param subprotocol websocket subprotocol, must be non-null
|
||||
* @param requestHeaders request headers, must be non-null
|
||||
* @param webSocketHandler http1 websocket handler added to pipeline of subchannel created for
|
||||
* successfully handshaked http2 websocket
|
||||
* @return ChannelFuture with result of handshake. Its channel accepts http1 WebSocketFrames as
|
||||
* soon as this method returns.
|
||||
*/
|
||||
public ChannelFuture handshake(String path, String subprotocol,
|
||||
Http2Headers requestHeaders, ChannelHandler webSocketHandler) {
|
||||
Preconditions.requireNonEmpty(path, "path");
|
||||
Preconditions.requireNonNull(subprotocol, "subprotocol");
|
||||
Preconditions.requireNonNull(requestHeaders, "requestHeaders");
|
||||
Preconditions.requireNonNull(webSocketHandler, "webSocketHandler");
|
||||
long startNanos = System.nanoTime();
|
||||
ChannelHandlerContext ctx = webSocketsParent.context();
|
||||
if (!ctx.channel().isOpen()) {
|
||||
return ctx.newFailedFuture(new ClosedChannelException());
|
||||
}
|
||||
int serial = WEBSOCKET_CHANNEL_SERIAL.getAndIncrement(this);
|
||||
Http2WebSocketChannel webSocketChannel = new Http2WebSocketChannel(webSocketsParent, serial, path,
|
||||
subprotocol, webSocketDecoderConfig, isEncoderMaskPayload, webSocketHandler).initialize();
|
||||
Handshake handshake = new Handshake(webSocketChannel, requestHeaders, timeoutMillis, startNanos);
|
||||
handshake.future().addListener(future -> {
|
||||
Throwable cause = future.cause();
|
||||
if (cause != null && !(cause instanceof WebSocketHandshakeException)) {
|
||||
Http2WebSocketEvent.fireHandshakeError(webSocketChannel, null, System.nanoTime(), cause);
|
||||
}
|
||||
});
|
||||
EventLoop el = ctx.channel().eventLoop();
|
||||
if (el.inEventLoop()) {
|
||||
handshakeOrDefer(handshake, el);
|
||||
} else {
|
||||
el.execute(() -> handshakeOrDefer(handshake, el));
|
||||
}
|
||||
return webSocketChannel.handshakePromise();
|
||||
}
|
||||
|
||||
void handshake(Http2WebSocket webSocket, Http2Headers responseHeaders, boolean endOfStream) {
|
||||
if (webSocket == Http2WebSocket.CLOSED) {
|
||||
return;
|
||||
}
|
||||
Http2WebSocketChannel webSocketChannel = (Http2WebSocketChannel) webSocket;
|
||||
ChannelPromise handshakePromise = webSocketChannel.handshakePromise();
|
||||
if (handshakePromise.isDone()) {
|
||||
return;
|
||||
}
|
||||
String errorMessage = null;
|
||||
WebSocketClientExtension compressionExtension = null;
|
||||
String status = responseHeaders.status().toString();
|
||||
switch (status) {
|
||||
case "200":
|
||||
if (endOfStream) {
|
||||
errorMessage = Http2WebSocketMessages.HANDSHAKE_UNEXPECTED_RESULT;
|
||||
} else {
|
||||
String clientSubprotocol = webSocketChannel.subprotocol();
|
||||
CharSequence serverSubprotocol =
|
||||
responseHeaders.get(Http2WebSocketProtocol.HEADER_WEBSOCKET_SUBPROTOCOL_NAME);
|
||||
if (!isEqual(clientSubprotocol, serverSubprotocol)) {
|
||||
errorMessage =
|
||||
Http2WebSocketMessages.HANDSHAKE_UNEXPECTED_SUBPROTOCOL + clientSubprotocol;
|
||||
}
|
||||
if (errorMessage == null) {
|
||||
PerMessageDeflateClientExtensionHandshaker handshaker = compressionHandshaker;
|
||||
if (handshaker != null) {
|
||||
CharSequence extensionsHeader = responseHeaders.get(Http2WebSocketProtocol.HEADER_WEBSOCKET_EXTENSIONS_NAME);
|
||||
WebSocketExtensionData compression = Http2WebSocketExtensions.decode(extensionsHeader);
|
||||
if (compression != null) {
|
||||
compressionExtension = handshaker.handshakeExtension(compression);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "400":
|
||||
CharSequence webSocketVersion =
|
||||
responseHeaders.get(Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_NAME);
|
||||
errorMessage = webSocketVersion != null
|
||||
? Http2WebSocketMessages.HANDSHAKE_UNSUPPORTED_VERSION + webSocketVersion
|
||||
: Http2WebSocketMessages.HANDSHAKE_BAD_REQUEST;
|
||||
break;
|
||||
case "404":
|
||||
errorMessage = Http2WebSocketMessages.HANDSHAKE_PATH_NOT_FOUND
|
||||
+ webSocketChannel.path()
|
||||
+ Http2WebSocketMessages.HANDSHAKE_PATH_NOT_FOUND_SUBPROTOCOLS
|
||||
+ webSocketChannel.subprotocol();
|
||||
break;
|
||||
default:
|
||||
errorMessage = Http2WebSocketMessages.HANDSHAKE_GENERIC_ERROR + status;
|
||||
}
|
||||
if (errorMessage != null) {
|
||||
Exception cause = new WebSocketHandshakeException(errorMessage);
|
||||
if (handshakePromise.tryFailure(cause)) {
|
||||
Http2WebSocketEvent.fireHandshakeError(webSocketChannel, responseHeaders, System.nanoTime(), cause);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (compressionExtension != null) {
|
||||
webSocketChannel.compression(compressionExtension.newExtensionEncoder(), compressionExtension.newExtensionDecoder());
|
||||
}
|
||||
if (handshakePromise.trySuccess()) {
|
||||
Http2WebSocketEvent.fireHandshakeSuccess(webSocketChannel, responseHeaders, System.nanoTime());
|
||||
}
|
||||
}
|
||||
|
||||
void reject(int streamId, Http2WebSocket webSocket, Http2Headers headers, boolean endOfStream) {
|
||||
Http2WebSocketEvent.fireHandshakeValidationStartAndError(webSocketsParent.context().channel(),
|
||||
streamId, headers.set(AsciiString.of("x-websocket-endofstream"), AsciiString.of(endOfStream ? "true" : "false")));
|
||||
if (webSocket == Http2WebSocket.CLOSED) {
|
||||
return;
|
||||
}
|
||||
Http2WebSocketChannel webSocketChannel = (Http2WebSocketChannel) webSocket;
|
||||
ChannelPromise handshakePromise = webSocketChannel.handshakePromise();
|
||||
if (handshakePromise.isDone()) {
|
||||
return;
|
||||
}
|
||||
Exception cause = new WebSocketHandshakeException(Http2WebSocketMessages.HANDSHAKE_INVALID_RESPONSE_HEADERS);
|
||||
if (handshakePromise.tryFailure(cause)) {
|
||||
Http2WebSocketEvent.fireHandshakeError(webSocketChannel, headers, System.nanoTime(), cause);
|
||||
}
|
||||
}
|
||||
|
||||
void onSupportsWebSocket(boolean supportsWebSocket) {
|
||||
if (!supportsWebSocket) {
|
||||
logger.log(Level.SEVERE, Http2WebSocketMessages.HANDSHAKE_UNSUPPORTED_BOOTSTRAP);
|
||||
}
|
||||
this.supportsWebSocket = supportsWebSocket;
|
||||
handshakeDeferred(supportsWebSocket);
|
||||
}
|
||||
|
||||
private void handshakeOrDefer(Handshake handshake, EventLoop eventLoop) {
|
||||
if (handshake.isDone()) {
|
||||
return;
|
||||
}
|
||||
Http2WebSocketChannel webSocketChannel = handshake.webSocketChannel();
|
||||
Http2Headers requestHeaders = handshake.requestHeaders();
|
||||
long startNanos = handshake.startNanos();
|
||||
ChannelFuture registered = eventLoop.register(webSocketChannel);
|
||||
if (!registered.isSuccess()) {
|
||||
Throwable cause = registered.cause();
|
||||
Exception e = new WebSocketHandshakeException("websocket handshake channel registration error", cause);
|
||||
Http2WebSocketEvent.fireHandshakeStartAndError(webSocketChannel.parent(),
|
||||
webSocketChannel.serial(), webSocketChannel.path(), webSocketChannel.subprotocol(),
|
||||
requestHeaders, startNanos, System.nanoTime(), e);
|
||||
handshake.complete(e);
|
||||
return;
|
||||
}
|
||||
Http2WebSocketEvent.fireHandshakeStart(webSocketChannel, requestHeaders, startNanos);
|
||||
Boolean supports = supportsWebSocket;
|
||||
if (supports == null) {
|
||||
Queue<Handshake> d = deferred;
|
||||
if (d == null) {
|
||||
d = deferred = new ArrayDeque<>(ESTIMATED_DEFERRED_HANDSHAKES);
|
||||
}
|
||||
handshake.startTimeout();
|
||||
d.add(handshake);
|
||||
return;
|
||||
}
|
||||
if (supports) {
|
||||
handshake.startTimeout();
|
||||
}
|
||||
handshakeImmediate(handshake, supports);
|
||||
}
|
||||
|
||||
private void handshakeDeferred(boolean supportsWebSocket) {
|
||||
Queue<Handshake> d = deferred;
|
||||
if (d == null) {
|
||||
return;
|
||||
}
|
||||
deferred = null;
|
||||
Handshake handshake = d.poll();
|
||||
while (handshake != null) {
|
||||
handshakeImmediate(handshake, supportsWebSocket);
|
||||
handshake = d.poll();
|
||||
}
|
||||
}
|
||||
|
||||
private void handshakeImmediate(Handshake handshake, boolean supportsWebSocket) {
|
||||
Http2WebSocketChannel webSocketChannel = handshake.webSocketChannel();
|
||||
Http2Headers customHeaders = handshake.requestHeaders();
|
||||
if (handshake.isDone()) {
|
||||
return;
|
||||
}
|
||||
if (!supportsWebSocket) {
|
||||
WebSocketHandshakeException e = new WebSocketHandshakeException(Http2WebSocketMessages.HANDSHAKE_UNSUPPORTED_BOOTSTRAP);
|
||||
Http2WebSocketEvent.fireHandshakeError(webSocketChannel, null, System.nanoTime(), e);
|
||||
handshake.complete(e);
|
||||
return;
|
||||
}
|
||||
int streamId = streamIdFactory.incrementAndGetNextStreamId();
|
||||
webSocketsParent.register(streamId, webSocketChannel.setStreamId(streamId));
|
||||
String authority = authority();
|
||||
String path = webSocketChannel.path();
|
||||
Http2Headers headers = Http2WebSocketProtocol.extendedConnect(new DefaultHttp2Headers()
|
||||
.scheme(scheme)
|
||||
.authority(authority)
|
||||
.path(path)
|
||||
.set(Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_NAME,
|
||||
Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_VALUE));
|
||||
PerMessageDeflateClientExtensionHandshaker handshaker = compressionHandshaker;
|
||||
if (handshaker != null) {
|
||||
headers.set(Http2WebSocketProtocol.HEADER_WEBSOCKET_EXTENSIONS_NAME,
|
||||
compressionExtensionHeader(handshaker));
|
||||
}
|
||||
String subprotocol = webSocketChannel.subprotocol();
|
||||
if (!subprotocol.isEmpty()) {
|
||||
headers.set(Http2WebSocketProtocol.HEADER_WEBSOCKET_SUBPROTOCOL_NAME, subprotocol);
|
||||
}
|
||||
if (!customHeaders.isEmpty()) {
|
||||
headers.setAll(customHeaders);
|
||||
}
|
||||
short pendingStreamWeight = webSocketChannel.pendingStreamWeight();
|
||||
short weight = pendingStreamWeight > 0 ? pendingStreamWeight : streamWeight;
|
||||
webSocketsParent.writeHeaders(webSocketChannel.streamId(), headers, false, weight)
|
||||
.addListener(future -> {
|
||||
if (!future.isSuccess()) {
|
||||
handshake.complete(future.cause());
|
||||
return;
|
||||
}
|
||||
webSocketChannel.setStreamWeightAttribute(weight);
|
||||
});
|
||||
}
|
||||
|
||||
private String authority() {
|
||||
return ((InetSocketAddress) webSocketsParent.context().channel().remoteAddress()).getHostString();
|
||||
}
|
||||
|
||||
private CharSequence compressionExtensionHeader(PerMessageDeflateClientExtensionHandshaker handshaker) {
|
||||
CharSequence header = compressionExtensionHeader;
|
||||
if (header == null) {
|
||||
header = compressionExtensionHeader = AsciiString.of(Http2WebSocketExtensions.encode(handshaker.newRequestData()));
|
||||
}
|
||||
return header;
|
||||
}
|
||||
|
||||
private static boolean isEqual(String str, CharSequence seq) {
|
||||
if ((seq == null || seq.length() == 0) && str.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (seq == null) {
|
||||
return false;
|
||||
}
|
||||
return str.contentEquals(seq);
|
||||
}
|
||||
|
||||
static class Handshake {
|
||||
private final Future<Void> channelClose;
|
||||
private final ChannelPromise handshake;
|
||||
private final long timeoutMillis;
|
||||
private boolean done;
|
||||
private ScheduledFuture<?> timeoutFuture;
|
||||
private Future<?> handshakeCompleteFuture;
|
||||
private GenericFutureListener<ChannelFuture> channelCloseListener;
|
||||
private final Http2WebSocketChannel webSocketChannel;
|
||||
private final Http2Headers requestHeaders;
|
||||
private final long handshakeStartNanos;
|
||||
|
||||
public Handshake(Http2WebSocketChannel webSocketChannel, Http2Headers requestHeaders,
|
||||
long timeoutMillis, long handshakeStartNanos) {
|
||||
this.channelClose = webSocketChannel.closeFuture();
|
||||
this.handshake = webSocketChannel.handshakePromise();
|
||||
this.timeoutMillis = timeoutMillis;
|
||||
this.webSocketChannel = webSocketChannel;
|
||||
this.requestHeaders = requestHeaders;
|
||||
this.handshakeStartNanos = handshakeStartNanos;
|
||||
}
|
||||
|
||||
public void startTimeout() {
|
||||
ChannelPromise h = handshake;
|
||||
Channel channel = h.channel();
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
GenericFutureListener<ChannelFuture> l = channelCloseListener = future -> onConnectionClose();
|
||||
channelClose.addListener(l);
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
handshakeCompleteFuture = h.addListener(future -> onHandshakeComplete(future.cause()));
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
timeoutFuture = channel.eventLoop().schedule(this::onTimeout, timeoutMillis, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public void complete(Throwable e) {
|
||||
onHandshakeComplete(e);
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public ChannelFuture future() {
|
||||
return handshake;
|
||||
}
|
||||
|
||||
public Http2WebSocketChannel webSocketChannel() {
|
||||
return webSocketChannel;
|
||||
}
|
||||
|
||||
public Http2Headers requestHeaders() {
|
||||
return requestHeaders;
|
||||
}
|
||||
|
||||
public long startNanos() {
|
||||
return handshakeStartNanos;
|
||||
}
|
||||
private void onConnectionClose() {
|
||||
if (!done) {
|
||||
handshake.tryFailure(new ClosedChannelException());
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
private void onHandshakeComplete(Throwable cause) {
|
||||
if (!done) {
|
||||
if (cause != null) {
|
||||
handshake.tryFailure(cause);
|
||||
} else {
|
||||
handshake.trySuccess();
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
private void onTimeout() {
|
||||
if (!done) {
|
||||
handshake.tryFailure(new TimeoutException());
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
private void done() {
|
||||
done = true;
|
||||
GenericFutureListener<ChannelFuture> closeListener = channelCloseListener;
|
||||
if (closeListener != null) {
|
||||
channelClose.removeListener(closeListener);
|
||||
}
|
||||
cancel(handshakeCompleteFuture);
|
||||
cancel(timeoutFuture);
|
||||
}
|
||||
|
||||
private void cancel(Future<?> future) {
|
||||
if (future != null) {
|
||||
future.cancel(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ class GoogleTest {
|
|||
void testHttp1WithTlsV13() throws Exception {
|
||||
AtomicBoolean success = new AtomicBoolean();
|
||||
Client client = Client.builder()
|
||||
.setTransportLayerSecurityProtocols(new String[] { "TLSv1.3" })
|
||||
.setTransportLayerSecurityProtocols("TLSv1.3")
|
||||
.build();
|
||||
try {
|
||||
Request request = Request.get().url("https://www.google.com/")
|
||||
|
|
|
@ -24,7 +24,8 @@ class Http1Test {
|
|||
Client client = Client.builder()
|
||||
.build();
|
||||
try {
|
||||
Request request = Request.get().url("https://xbib.org")
|
||||
Request request = Request.get()
|
||||
.url("https://xbib.org")
|
||||
.setResponseListener(resp -> logger.log(Level.FINE,
|
||||
"got response: " + resp.getHeaders() +
|
||||
resp.getBodyAsString(StandardCharsets.UTF_8) +
|
||||
|
|
258
netty-http-client/websocket.md
Normal file
258
netty-http-client/websocket.md
Normal file
|
@ -0,0 +1,258 @@
|
|||
|
||||
# netty-websocket-http2
|
||||
|
||||
Netty based implementation of [rfc8441](https://tools.ietf.org/html/rfc8441) - bootstrapping websockets with http/2
|
||||
|
||||
Library addresses two use cases: for application servers and clients,
|
||||
It is transparent use of existing http1 websocket handlers on top of http2 streams; for gateways/proxies,
|
||||
It is websockets-over-http2 support with no http1 dependencies and minimal overhead.
|
||||
|
||||
[https://jauntsdn.com/post/netty-websocket-http2/](https://jauntsdn.com/post/netty-websocket-http2/)
|
||||
|
||||
### websocket channel API
|
||||
Intended for application servers and clients.
|
||||
Allows transparent application of existing http1 websocket handlers on top of http2 stream.
|
||||
|
||||
* Server
|
||||
```groovy
|
||||
EchoWebSocketHandler http1WebSocketHandler = new EchoWebSocketHandler();
|
||||
|
||||
Http2WebSocketServerHandler http2webSocketHandler =
|
||||
Http2WebSocketServerBuilder.create()
|
||||
.acceptor(
|
||||
(ctx, path, subprotocols, request, response) -> {
|
||||
switch (path) {
|
||||
case "/echo":
|
||||
if (subprotocols.contains("echo.jauntsdn.com")
|
||||
&& acceptUserAgent(request, response)) {
|
||||
/*selecting subprotocol for accepted requests is mandatory*/
|
||||
Http2WebSocketAcceptor.Subprotocol
|
||||
.accept("echo.jauntsdn.com", response);
|
||||
return ctx.executor()
|
||||
.newSucceededFuture(http1WebSocketHandler);
|
||||
}
|
||||
break;
|
||||
case "/echo_all":
|
||||
if (subprotocols.isEmpty()
|
||||
&& acceptUserAgent(request, response)) {
|
||||
return ctx.executor()
|
||||
.newSucceededFuture(http1WebSocketHandler);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return ctx.executor()
|
||||
.newFailedFuture(
|
||||
new WebSocketHandshakeException(
|
||||
"websocket rejected, path: " + path));
|
||||
})
|
||||
.build();
|
||||
|
||||
ch.pipeline()
|
||||
.addLast(sslHandler,
|
||||
http2frameCodec,
|
||||
http2webSocketHandler);
|
||||
```
|
||||
|
||||
* Client
|
||||
```groovy
|
||||
Channel channel =
|
||||
new Bootstrap()
|
||||
.handler(
|
||||
new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) {
|
||||
|
||||
Http2WebSocketClientHandler http2WebSocketClientHandler =
|
||||
Http2WebSocketClientBuilder.create()
|
||||
.handshakeTimeoutMillis(15_000)
|
||||
.build();
|
||||
|
||||
ch.pipeline()
|
||||
.addLast(
|
||||
sslHandler,
|
||||
http2FrameCodec,
|
||||
http2WebSocketClientHandler);
|
||||
}
|
||||
})
|
||||
.connect(address)
|
||||
.sync()
|
||||
.channel();
|
||||
|
||||
Http2WebSocketClientHandshaker handShaker = Http2WebSocketClientHandshaker.create(channel);
|
||||
|
||||
Http2Headers headers =
|
||||
new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/1.1.2");
|
||||
ChannelFuture handshakeFuture =
|
||||
/*http1 websocket handler*/
|
||||
handShaker.handshake("/echo", headers, new EchoWebSocketHandler());
|
||||
|
||||
handshakeFuture.channel().writeAndFlush(new TextWebSocketFrame("hello http2 websocket"));
|
||||
```
|
||||
Successfully handshaked http2 stream spawns websocket subchannel, with provided
|
||||
http1 websocket handlers on its pipeline.
|
||||
|
||||
Runnable demo is available in `netty-websocket-http2-example` module -
|
||||
[channelserver](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/channelserver/Main.java),
|
||||
[channelclient](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/channelclient/Main.java).
|
||||
|
||||
### websocket handshake only API
|
||||
Intended for intermediaries/proxies.
|
||||
Only verifies whether http2 stream is valid websocket, then passes it down the pipeline as `POST` request with `x-protocol=websocket` header.
|
||||
|
||||
```groovy
|
||||
Http2WebSocketServerHandler http2webSocketHandler =
|
||||
Http2WebSocketServerBuilder.buildHandshakeOnly();
|
||||
|
||||
Http2StreamsHandler http2StreamsHandler = new Http2StreamsHandler();
|
||||
ch.pipeline()
|
||||
.addLast(sslHandler,
|
||||
frameCodec,
|
||||
http2webSocketHandler,
|
||||
http2StreamsHandler);
|
||||
```
|
||||
|
||||
Works with both callbacks-style `Http2ConnectionHandler` and frames based `Http2FrameCodec`.
|
||||
|
||||
```
|
||||
Http2WebSocketServerBuilder.buildHandshakeOnly();
|
||||
```
|
||||
|
||||
Runnable demo is available in `netty-websocket-http2-example` module -
|
||||
[handshakeserver](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/handshakeserver/Main.java),
|
||||
[channelclient](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/channelclient/Main.java).
|
||||
|
||||
### configuration
|
||||
Initial settings of server http2 codecs (`Http2ConnectionHandler` or `Http2FrameCodec`) should contain [SETTINGS_ENABLE_CONNECT_PROTOCOL=1](https://tools.ietf.org/html/rfc8441#section-9.1)
|
||||
parameter to advertise websocket-over-http2 support.
|
||||
|
||||
Also server http2 codecs must disable built-in headers validation because It is not compatible
|
||||
with rfc8441 due to newly introduced `:protocol` pseudo-header. All websocket handlers provided by this library
|
||||
do headers validation on their own - both for websocket and non-websocket requests.
|
||||
|
||||
Above configuration may be done with utility methods of `Http2WebSocketServerBuilder`
|
||||
|
||||
```
|
||||
public static Http2FrameCodecBuilder configureHttp2Server(
|
||||
Http2FrameCodecBuilder http2Builder);
|
||||
|
||||
public static Http2ConnectionHandlerBuilder configureHttp2Server(
|
||||
Http2ConnectionHandlerBuilder http2Builder)
|
||||
```
|
||||
|
||||
### compression & subprotocols
|
||||
Client and server `permessage-deflate` compression configuration is shared by all streams
|
||||
```groovy
|
||||
Http2WebSocketServerBuilder.compression(enabled);
|
||||
```
|
||||
or
|
||||
```groovy
|
||||
Http2WebSocketServerBuilder.compression(
|
||||
compressionLevel,
|
||||
allowServerWindowSize,
|
||||
preferredClientWindowSize,
|
||||
allowServerNoContext,
|
||||
preferredClientNoContext);
|
||||
```
|
||||
Client subprotocols are configured on per-path basis
|
||||
```groovy
|
||||
EchoWebSocketHandler http1WebsocketHandler = new EchoWebSocketHandler();
|
||||
ChannelFuture handshake =
|
||||
handShaker.handshake("/echo", "subprotocol", headers, http1WebsocketHandler);
|
||||
```
|
||||
On a server It is responsibility of `Http2WebSocketAcceptor` to select supported subprotocol with
|
||||
```groovy
|
||||
Http2WebSocketAcceptor.Subprotocol.accept(subprotocol, response);
|
||||
```
|
||||
### lifecycle
|
||||
|
||||
Handshake events and several shutdown options are available when
|
||||
using `Websocket channel` style APIs.
|
||||
|
||||
#### handshake events
|
||||
|
||||
Events are fired on parent channel, also on websocket channel if one gets created
|
||||
* `Http2WebSocketHandshakeStartEvent(websocketId, path, subprotocols, timestampNanos, requestHeaders)`
|
||||
* `Http2WebSocketHandshakeErrorEvent(webSocketId, path, subprotocols, timestampNanos, responseHeaders, error)`
|
||||
* `Http2WebSocketHandshakeSuccessEvent(webSocketId, path, subprotocols, timestampNanos, responseHeaders)`
|
||||
|
||||
#### close events
|
||||
|
||||
Outbound `Http2WebSocketLocalCloseEvent` on websocket channel pipeline closes
|
||||
http2 stream by sending empty `DATA` frame with `END_STREAM` flag set.
|
||||
|
||||
Graceful and `RST` stream shutdown by remote endpoint is represented with inbound `Http2WebSocketRemoteCloseEvent`
|
||||
(with type `CLOSE_REMOTE_ENDSTREAM` and `CLOSE_REMOTE_RESET` respectively) on websocket channel pipeline.
|
||||
|
||||
Graceful connection shutdown by remote with `GO_AWAY` frame is represented by inbound `Http2WebSocketRemoteGoAwayEvent`
|
||||
on websocket channel pipeline.
|
||||
|
||||
#### shutdown
|
||||
|
||||
Closing websocket channel terminates its http2 stream by sending `RST` frame.
|
||||
|
||||
#### validation & write error events
|
||||
|
||||
Both API style handlers send `Http2WebSocketHandshakeErrorEvent` for invalid websocket-over-http2 and http requests.
|
||||
For http2 frame write errors `Http2WebSocketWriteErrorEvent` is sent on parent channel if auto-close is not enabled;
|
||||
otherwise exception is delivered with `ChannelPipeline.fireExceptionCaught` followed by immediate close.
|
||||
|
||||
### flow control
|
||||
|
||||
Inbound flow control is done automatically as soon as `DATA` frames are received.
|
||||
Library relies on netty's `DefaultHttp2LocalFlowController` for refilling receive window.
|
||||
|
||||
Outbound flow control is expressed as websocket channels writability change on send window
|
||||
exhaust/refill, provided by `DefaultHttp2RemoteFlowController`.
|
||||
|
||||
### websocket stream weight
|
||||
|
||||
Initial stream weight is configured with
|
||||
|
||||
```groovy
|
||||
Http2WebSocketClientBuilder.streamWeight(weight);
|
||||
```
|
||||
it can be updated by firing `Http2WebSocketStreamWeightUpdateEvent` on websocket channel pipeline.
|
||||
|
||||
### performance
|
||||
|
||||
Library relies on capabilities provided by netty's `Http2ConnectionHandler` so performance characteristics should be similar.
|
||||
[netty-websocket-http2-perftest](https://github.com/jauntsdn/netty-websocket-http2/tree/develop/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest)
|
||||
module contains application that gives rough throughput/latency estimate. The application is started with `perf_server.sh`, `perf_client.sh`.
|
||||
|
||||
On modern box one can expect following results for single websocket:
|
||||
|
||||
To evaluate performance with multiple connections we compose an application comprised with simple echo server, and client
|
||||
sending batches of messages periodically over single websocket per connection (approximately models chat application)
|
||||
|
||||
With 25k active connections each sending batches of 5-10 messages of 0.2-0.5 KBytes over single websocket every 15-30seconds,
|
||||
the results are as follows (measured over time spans of 5 seconds):
|
||||
|
||||
### examples
|
||||
|
||||
`netty-websocket-http2-example` module contains demos showcasing both API styles, with this library/browser as clients.
|
||||
|
||||
* `channelserver, channelclient` packages for websocket subchannel API demos.
|
||||
* `handshakeserver, channelclient` packages for handshake only API demo.
|
||||
* `lwsclient` package for client demo that runs against [https://libwebsockets.org/testserver/](https://libwebsockets.org/testserver/) which hosts websocket-over-http2
|
||||
server implemented with [libwebsockets](https://github.com/warmcat/libwebsockets) - popular C-based networking library.
|
||||
|
||||
### browser example
|
||||
`Channelserver` example serves web page at `https://www.localhost:8099` that sends pings to `/echo` endpoint.
|
||||
|
||||
Currently only `Mozilla Firefox` and latest `Google Chrome` support websockets-over-http2.
|
||||
|
||||
### build & binaries
|
||||
```
|
||||
./gradlew
|
||||
```
|
||||
|
||||
Releases are published on MavenCentral
|
||||
```groovy
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.jauntsdn.netty:netty-websocket-http2:1.1.2'
|
||||
}
|
||||
```
|
|
@ -4,6 +4,7 @@ module org.xbib.netty.http.common {
|
|||
exports org.xbib.netty.http.common.mime;
|
||||
exports org.xbib.netty.http.common.security;
|
||||
exports org.xbib.netty.http.common.util;
|
||||
exports org.xbib.netty.http.common.ws;
|
||||
requires org.xbib.net.url;
|
||||
requires io.netty.buffer;
|
||||
requires io.netty.common;
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package org.xbib.netty.http.common.ws;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http2.Http2Exception;
|
||||
import io.netty.handler.codec.http2.Http2Flags;
|
||||
import io.netty.handler.codec.http2.Http2FrameAdapter;
|
||||
import io.netty.handler.codec.http2.Http2FrameListener;
|
||||
|
||||
public interface Http2WebSocket extends Http2FrameListener {
|
||||
|
||||
@Override
|
||||
void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData);
|
||||
|
||||
void trySetWritable();
|
||||
|
||||
void fireExceptionCaught(Throwable t);
|
||||
|
||||
void streamClosed();
|
||||
|
||||
void closeForcibly();
|
||||
|
||||
Http2WebSocket CLOSED = new Http2WebSocketClosedChannel();
|
||||
|
||||
class Http2WebSocketClosedChannel extends Http2FrameAdapter implements Http2WebSocket {
|
||||
|
||||
@Override
|
||||
public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData) {}
|
||||
|
||||
@Override
|
||||
public void streamClosed() {}
|
||||
|
||||
@Override
|
||||
public void trySetWritable() {}
|
||||
|
||||
@Override
|
||||
public void fireExceptionCaught(Throwable t) {}
|
||||
|
||||
@Override
|
||||
public void closeForcibly() {}
|
||||
|
||||
@Override
|
||||
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)
|
||||
throws Http2Exception {
|
||||
int processed = super.onDataRead(ctx, streamId, data, padding, endOfStream);
|
||||
data.release();
|
||||
return processed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags, ByteBuf payload) {
|
||||
payload.release();
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,392 @@
|
|||
package org.xbib.netty.http.common.ws;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.channel.EventLoop;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig;
|
||||
import io.netty.handler.codec.http2.*;
|
||||
import io.netty.util.collection.IntCollections;
|
||||
import io.netty.util.collection.IntObjectHashMap;
|
||||
import io.netty.util.collection.IntObjectMap;
|
||||
import io.netty.util.concurrent.GenericFutureListener;
|
||||
import io.netty.util.concurrent.ScheduledFuture;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public abstract class Http2WebSocketChannelHandler extends Http2WebSocketHandler {
|
||||
|
||||
protected final WebSocketDecoderConfig config;
|
||||
|
||||
protected final boolean isEncoderMaskPayload;
|
||||
|
||||
protected final long closedWebSocketRemoveTimeoutMillis;
|
||||
|
||||
protected final Supplier<IntObjectMap<Http2WebSocket>> webSocketRegistryFactory;
|
||||
|
||||
protected IntObjectMap<Http2WebSocket> webSocketRegistry = IntCollections.emptyMap();
|
||||
|
||||
protected ChannelHandlerContext ctx;
|
||||
|
||||
protected WebSocketsParent webSocketsParent;
|
||||
|
||||
protected boolean isAutoRead;
|
||||
|
||||
public Http2WebSocketChannelHandler(WebSocketDecoderConfig webSocketDecoderConfig,
|
||||
boolean isEncoderMaskPayload,
|
||||
long closedWebSocketRemoveTimeoutMillis,
|
||||
boolean isSingleWebSocketPerConnection) {
|
||||
this.config = webSocketDecoderConfig;
|
||||
this.isEncoderMaskPayload = isEncoderMaskPayload;
|
||||
this.closedWebSocketRemoveTimeoutMillis = closedWebSocketRemoveTimeoutMillis;
|
||||
this.webSocketRegistryFactory = webSocketRegistryFactory(isSingleWebSocketPerConnection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
|
||||
super.handlerAdded(ctx);
|
||||
this.ctx = ctx;
|
||||
this.isAutoRead = ctx.channel().config().isAutoRead();
|
||||
Http2ConnectionEncoder encoder = http2Handler.encoder();
|
||||
this.webSocketsParent = new WebSocketsParent(encoder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
webSocketRegistry.clear();
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
|
||||
if (ctx.channel().isWritable()) {
|
||||
IntObjectMap<Http2WebSocket> webSockets = this.webSocketRegistry;
|
||||
if (!webSockets.isEmpty()) {
|
||||
webSockets.forEach((key, webSocket) -> webSocket.trySetWritable());
|
||||
}
|
||||
}
|
||||
super.channelWritabilityChanged(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
webSocketsParent.setReadInProgress();
|
||||
super.channelRead(ctx, msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
|
||||
webSocketsParent.processPendingReadCompleteQueue();
|
||||
super.channelReadComplete(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
if (!(cause instanceof Http2Exception.StreamException)) {
|
||||
super.exceptionCaught(ctx, cause);
|
||||
return;
|
||||
}
|
||||
IntObjectMap<Http2WebSocket> webSockets = this.webSocketRegistry;
|
||||
if (!webSockets.isEmpty()) {
|
||||
Http2Exception.StreamException streamException = (Http2Exception.StreamException) cause;
|
||||
Http2WebSocket webSocket = webSockets.get(streamException.streamId());
|
||||
if (webSocket == null) {
|
||||
super.exceptionCaught(ctx, cause);
|
||||
return;
|
||||
}
|
||||
if (webSocket != Http2WebSocket.CLOSED) {
|
||||
try {
|
||||
ClosedChannelException e = new ClosedChannelException();
|
||||
e.initCause(streamException);
|
||||
webSocket.fireExceptionCaught(e);
|
||||
} finally {
|
||||
webSocket.closeForcibly();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
super.exceptionCaught(ctx, cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
|
||||
IntObjectMap<Http2WebSocket> webSockets = this.webSocketRegistry;
|
||||
if (!webSockets.isEmpty()) {
|
||||
webSockets.forEach((key, webSocket) -> webSocket.streamClosed());
|
||||
}
|
||||
super.close(ctx, promise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGoAwayRead(
|
||||
ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData)
|
||||
throws Http2Exception {
|
||||
IntObjectMap<Http2WebSocket> webSockets = this.webSocketRegistry;
|
||||
if (!webSockets.isEmpty()) {
|
||||
webSockets.forEach(
|
||||
(key, webSocket) -> webSocket.onGoAwayRead(ctx, lastStreamId, errorCode, debugData));
|
||||
}
|
||||
next().onGoAwayRead(ctx, lastStreamId, errorCode, debugData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode)
|
||||
throws Http2Exception {
|
||||
webSocketOrNext(streamId).onRstStreamRead(ctx, streamId, errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onDataRead(
|
||||
ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)
|
||||
throws Http2Exception {
|
||||
return webSocketOrNext(streamId).onDataRead(ctx, streamId, data.retain(), padding, endOfStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeadersRead(
|
||||
ChannelHandlerContext ctx,
|
||||
int streamId,
|
||||
Http2Headers headers,
|
||||
int padding,
|
||||
boolean endOfStream)
|
||||
throws Http2Exception {
|
||||
webSocketOrNext(streamId).onHeadersRead(ctx, streamId, headers, padding, endOfStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeadersRead(
|
||||
ChannelHandlerContext ctx,
|
||||
int streamId,
|
||||
Http2Headers headers,
|
||||
int streamDependency,
|
||||
short weight,
|
||||
boolean exclusive,
|
||||
int padding,
|
||||
boolean endOfStream)
|
||||
throws Http2Exception {
|
||||
webSocketOrNext(streamId)
|
||||
.onHeadersRead(
|
||||
ctx, streamId, headers, streamDependency, weight, exclusive, padding, endOfStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPriorityRead(
|
||||
ChannelHandlerContext ctx,
|
||||
int streamId,
|
||||
int streamDependency,
|
||||
short weight,
|
||||
boolean exclusive)
|
||||
throws Http2Exception {
|
||||
webSocketOrNext(streamId).onPriorityRead(ctx, streamId, streamDependency, weight, exclusive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
|
||||
throws Http2Exception {
|
||||
webSocketOrNext(streamId).onWindowUpdateRead(ctx, streamId, windowSizeIncrement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnknownFrame(
|
||||
ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags, ByteBuf payload)
|
||||
throws Http2Exception {
|
||||
webSocketOrNext(streamId).onUnknownFrame(ctx, frameType, streamId, flags, payload);
|
||||
}
|
||||
|
||||
Http2FrameListener webSocketOrNext(int streamId) {
|
||||
Http2WebSocket webSocket = webSocketRegistry.get(streamId);
|
||||
if (webSocket != null) {
|
||||
ChannelHandlerContext c = ctx;
|
||||
if (!isAutoRead) {
|
||||
c.read();
|
||||
}
|
||||
return webSocket;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
void registerWebSocket(int streamId, Http2WebSocketChannel webSocket) {
|
||||
IntObjectMap<Http2WebSocket> registry = webSocketRegistry;
|
||||
if (registry == IntCollections.<Http2WebSocket>emptyMap()) {
|
||||
webSocketRegistry = registry = webSocketRegistryFactory.get();
|
||||
}
|
||||
registry.put(streamId, webSocket);
|
||||
IntObjectMap<Http2WebSocket> finalRegistry = registry;
|
||||
webSocket
|
||||
.closeFuture()
|
||||
.addListener(
|
||||
future -> {
|
||||
Channel channel = ctx.channel();
|
||||
ChannelFuture connectionCloseFuture = channel.closeFuture();
|
||||
if (connectionCloseFuture.isDone()) {
|
||||
return;
|
||||
}
|
||||
/*stream is remotely closed already so there will be no frames stream received*/
|
||||
if (!webSocket.isCloseInitiator()) {
|
||||
finalRegistry.remove(streamId);
|
||||
return;
|
||||
}
|
||||
finalRegistry.put(streamId, Http2WebSocket.CLOSED);
|
||||
removeAfterTimeout(
|
||||
streamId, finalRegistry, connectionCloseFuture, channel.eventLoop());
|
||||
});
|
||||
}
|
||||
|
||||
void removeAfterTimeout(int streamId, IntObjectMap<Http2WebSocket> webSockets, ChannelFuture connectionCloseFuture,
|
||||
EventLoop eventLoop) {
|
||||
RemoveWebSocket removeWebSocket =
|
||||
new RemoveWebSocket(streamId, webSockets, connectionCloseFuture);
|
||||
ScheduledFuture<?> removeWebSocketFuture =
|
||||
eventLoop.schedule(
|
||||
removeWebSocket, closedWebSocketRemoveTimeoutMillis, TimeUnit.MILLISECONDS);
|
||||
removeWebSocket.removeWebSocketFuture(removeWebSocketFuture);
|
||||
}
|
||||
|
||||
private static class RemoveWebSocket implements Runnable, GenericFutureListener<ChannelFuture> {
|
||||
|
||||
private final IntObjectMap<Http2WebSocket> webSockets;
|
||||
|
||||
private final int streamId;
|
||||
|
||||
private final ChannelFuture connectionCloseFuture;
|
||||
|
||||
private ScheduledFuture<?> removeWebSocketFuture;
|
||||
|
||||
RemoveWebSocket(int streamId, IntObjectMap<Http2WebSocket> webSockets, ChannelFuture connectionCloseFuture) {
|
||||
this.streamId = streamId;
|
||||
this.webSockets = webSockets;
|
||||
this.connectionCloseFuture = connectionCloseFuture;
|
||||
}
|
||||
|
||||
void removeWebSocketFuture(ScheduledFuture<?> removeWebSocketFuture) {
|
||||
this.removeWebSocketFuture = removeWebSocketFuture;
|
||||
connectionCloseFuture.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) {
|
||||
removeWebSocketFuture.cancel(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
webSockets.remove(streamId);
|
||||
connectionCloseFuture.removeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("Convert2MethodRef")
|
||||
static Supplier<IntObjectMap<Http2WebSocket>> webSocketRegistryFactory(
|
||||
boolean isSingleWebSocketPerConnection) {
|
||||
if (isSingleWebSocketPerConnection) {
|
||||
return () -> new Http2WebSocketHandlerContainers.SingleElementOptimizedMap<>();
|
||||
} else {
|
||||
return () -> new IntObjectHashMap<>(4);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides DATA, RST, WINDOW_UPDATE frame write operations to websocket channel. Also hosts code
|
||||
* derived from netty so It can be attributed properly
|
||||
*/
|
||||
public class WebSocketsParent {
|
||||
static final int READ_COMPLETE_PENDING_QUEUE_MAX_SIZE = Http2CodecUtil.SMALLEST_MAX_CONCURRENT_STREAMS;
|
||||
|
||||
final Queue<Http2WebSocketChannel> readCompletePendingQueue = new ArrayDeque<>(8);
|
||||
|
||||
boolean parentReadInProgress;
|
||||
|
||||
final Http2ConnectionEncoder connectionEncoder;
|
||||
|
||||
public WebSocketsParent(Http2ConnectionEncoder connectionEncoder) {
|
||||
this.connectionEncoder = connectionEncoder;
|
||||
}
|
||||
|
||||
public ChannelFuture writeHeaders(int streamId, Http2Headers headers, boolean endStream) {
|
||||
ChannelHandlerContext c = ctx;
|
||||
ChannelPromise p = c.newPromise();
|
||||
return connectionEncoder.writeHeaders(c, streamId, headers, 0, endStream, p);
|
||||
}
|
||||
|
||||
public ChannelFuture writeHeaders(
|
||||
int streamId, Http2Headers headers, boolean endStream, short weight) {
|
||||
ChannelHandlerContext c = ctx;
|
||||
ChannelPromise p = c.newPromise();
|
||||
ChannelFuture channelFuture =
|
||||
connectionEncoder.writeHeaders(c, streamId, headers, 0, weight, false, 0, endStream, p);
|
||||
c.flush();
|
||||
return channelFuture;
|
||||
}
|
||||
|
||||
public ChannelFuture writeData(int streamId, ByteBuf data, boolean endStream, ChannelPromise promise) {
|
||||
ChannelHandlerContext c = ctx;
|
||||
return connectionEncoder.writeData(c, streamId, data, 0, endStream, promise);
|
||||
}
|
||||
|
||||
public ChannelFuture writeRstStream(int streamId, long errorCode) {
|
||||
ChannelHandlerContext c = ctx;
|
||||
ChannelPromise p = c.newPromise();
|
||||
ChannelFuture channelFuture = connectionEncoder.writeRstStream(c, streamId, errorCode, p);
|
||||
c.flush();
|
||||
return channelFuture;
|
||||
}
|
||||
|
||||
public ChannelFuture writePriority(int streamId, short weight) {
|
||||
ChannelHandlerContext c = ctx;
|
||||
ChannelPromise p = c.newPromise();
|
||||
ChannelFuture channelFuture =
|
||||
connectionEncoder.writePriority(c, streamId, 0, weight, false, p);
|
||||
c.flush();
|
||||
return channelFuture;
|
||||
}
|
||||
|
||||
public boolean isParentReadInProgress() {
|
||||
return parentReadInProgress;
|
||||
}
|
||||
|
||||
public void addChannelToReadCompletePendingQueue(Http2WebSocketChannel webSocketChannel) {
|
||||
Queue<Http2WebSocketChannel> q = readCompletePendingQueue;
|
||||
while (q.size() >= READ_COMPLETE_PENDING_QUEUE_MAX_SIZE) {
|
||||
processPendingReadCompleteQueue();
|
||||
}
|
||||
q.offer(webSocketChannel);
|
||||
}
|
||||
|
||||
public ChannelHandlerContext context() {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
public void register(final int streamId, Http2WebSocketChannel webSocket) {
|
||||
registerWebSocket(streamId, webSocket);
|
||||
}
|
||||
|
||||
void setReadInProgress() {
|
||||
parentReadInProgress = true;
|
||||
}
|
||||
|
||||
void processPendingReadCompleteQueue() {
|
||||
parentReadInProgress = true;
|
||||
Queue<Http2WebSocketChannel> q = readCompletePendingQueue;
|
||||
Http2WebSocketChannel childChannel = q.poll();
|
||||
if (childChannel != null) {
|
||||
try {
|
||||
do {
|
||||
childChannel.fireChildReadComplete();
|
||||
childChannel = q.poll();
|
||||
} while (childChannel != null);
|
||||
} finally {
|
||||
parentReadInProgress = false;
|
||||
q.clear();
|
||||
ctx.flush();
|
||||
}
|
||||
} else {
|
||||
parentReadInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,379 @@
|
|||
package org.xbib.netty.http.common.ws;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
|
||||
/**
|
||||
* Base type for websocket-over-http2 events
|
||||
*/
|
||||
public abstract class Http2WebSocketEvent {
|
||||
|
||||
private final Type type;
|
||||
|
||||
public Http2WebSocketEvent(Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public static void fireFrameWriteError(Channel parentChannel, Throwable t) {
|
||||
ChannelPipeline parentPipeline = parentChannel.pipeline();
|
||||
if (parentChannel.config().isAutoClose()) {
|
||||
parentPipeline.fireExceptionCaught(t);
|
||||
parentChannel.close();
|
||||
return;
|
||||
}
|
||||
if (t instanceof Exception) {
|
||||
parentPipeline.fireUserEventTriggered(new Http2WebSocketWriteErrorEvent(Http2WebSocketMessages.WRITE_ERROR, t));
|
||||
return;
|
||||
}
|
||||
parentPipeline.fireExceptionCaught(t);
|
||||
}
|
||||
|
||||
public static void fireHandshakeValidationStartAndError(Channel parentChannel, int streamId, Http2Headers headers) {
|
||||
long timestamp = System.nanoTime();
|
||||
Http2WebSocketEvent.fireHandshakeStartAndError(parentChannel, streamId,
|
||||
nonNullString(headers.path()),
|
||||
nonNullString(headers.get(Http2WebSocketProtocol.HEADER_WEBSOCKET_SUBPROTOCOL_NAME)),
|
||||
headers, timestamp, timestamp, WebSocketHandshakeException.class.getName(),
|
||||
Http2WebSocketMessages.HANDSHAKE_INVALID_REQUEST_HEADERS);
|
||||
}
|
||||
|
||||
public static void fireHandshakeStartAndError(Channel parentChannel,
|
||||
int serial, String path, String subprotocols,
|
||||
Http2Headers requestHeaders, long startNanos, long errorNanos, Throwable t) {
|
||||
ChannelPipeline parentPipeline = parentChannel.pipeline();
|
||||
if (t instanceof Exception) {
|
||||
parentPipeline.fireUserEventTriggered(new Http2WebSocketHandshakeStartEvent(serial, path, subprotocols, startNanos, requestHeaders));
|
||||
parentPipeline.fireUserEventTriggered(
|
||||
new Http2WebSocketHandshakeErrorEvent(serial, path, subprotocols, errorNanos, null, t));
|
||||
return;
|
||||
}
|
||||
parentPipeline.fireExceptionCaught(t);
|
||||
}
|
||||
|
||||
public static void fireHandshakeStartAndError(Channel parentChannel, int serial, String path, String subprotocols,
|
||||
Http2Headers requestHeaders, long startNanos, long errorNanos, String errorName, String errorMessage) {
|
||||
ChannelPipeline parentPipeline = parentChannel.pipeline();
|
||||
parentPipeline.fireUserEventTriggered(new Http2WebSocketHandshakeStartEvent(serial, path, subprotocols,
|
||||
startNanos, requestHeaders));
|
||||
parentPipeline.fireUserEventTriggered(new Http2WebSocketHandshakeErrorEvent(serial, path, subprotocols,
|
||||
errorNanos, null, errorName, errorMessage));
|
||||
}
|
||||
|
||||
public static void fireHandshakeStartAndSuccess(Http2WebSocketChannel webSocketChannel, int serial, String path, String subprotocols,
|
||||
Http2Headers requestHeaders, Http2Headers responseHeaders, long startNanos, long successNanos) {
|
||||
ChannelPipeline parentPipeline = webSocketChannel.parent().pipeline();
|
||||
ChannelPipeline webSocketPipeline = webSocketChannel.pipeline();
|
||||
Http2WebSocketHandshakeStartEvent startEvent = new Http2WebSocketHandshakeStartEvent(serial, path, subprotocols,
|
||||
startNanos, requestHeaders);
|
||||
Http2WebSocketHandshakeSuccessEvent successEvent = new Http2WebSocketHandshakeSuccessEvent(serial, path, subprotocols,
|
||||
successNanos, responseHeaders);
|
||||
parentPipeline.fireUserEventTriggered(startEvent);
|
||||
parentPipeline.fireUserEventTriggered(successEvent);
|
||||
webSocketPipeline.fireUserEventTriggered(startEvent);
|
||||
webSocketPipeline.fireUserEventTriggered(successEvent);
|
||||
}
|
||||
|
||||
public static void fireHandshakeStart(Http2WebSocketChannel webSocketChannel, Http2Headers requestHeaders, long timestampNanos) {
|
||||
ChannelPipeline parentPipeline = webSocketChannel.parent().pipeline();
|
||||
ChannelPipeline webSocketPipeline = webSocketChannel.pipeline();
|
||||
Http2WebSocketHandshakeStartEvent startEvent = new Http2WebSocketHandshakeStartEvent(
|
||||
webSocketChannel.serial(),
|
||||
webSocketChannel.path(),
|
||||
webSocketChannel.subprotocol(),
|
||||
timestampNanos,
|
||||
requestHeaders);
|
||||
parentPipeline.fireUserEventTriggered(startEvent);
|
||||
webSocketPipeline.fireUserEventTriggered(startEvent);
|
||||
}
|
||||
|
||||
public static void fireHandshakeError(Http2WebSocketChannel webSocketChannel, Http2Headers responseHeaders,
|
||||
long timestampNanos, Throwable t) {
|
||||
ChannelPipeline parentPipeline = webSocketChannel.parent().pipeline();
|
||||
if (t instanceof Exception) {
|
||||
String path = webSocketChannel.path();
|
||||
ChannelPipeline webSocketPipeline = webSocketChannel.pipeline();
|
||||
Http2WebSocketHandshakeErrorEvent errorEvent = new Http2WebSocketHandshakeErrorEvent(webSocketChannel.serial(),
|
||||
path, webSocketChannel.subprotocol(), timestampNanos, responseHeaders, t);
|
||||
parentPipeline.fireUserEventTriggered(errorEvent);
|
||||
webSocketPipeline.fireUserEventTriggered(errorEvent);
|
||||
return;
|
||||
}
|
||||
parentPipeline.fireExceptionCaught(t);
|
||||
}
|
||||
|
||||
public static void fireHandshakeSuccess(Http2WebSocketChannel webSocketChannel, Http2Headers responseHeaders, long timestampNanos) {
|
||||
String path = webSocketChannel.path();
|
||||
ChannelPipeline parentPipeline = webSocketChannel.parent().pipeline();
|
||||
ChannelPipeline webSocketPipeline = webSocketChannel.pipeline();
|
||||
Http2WebSocketHandshakeSuccessEvent successEvent = new Http2WebSocketHandshakeSuccessEvent(webSocketChannel.serial(),
|
||||
path, webSocketChannel.subprotocol(), timestampNanos, responseHeaders);
|
||||
parentPipeline.fireUserEventTriggered(successEvent);
|
||||
webSocketPipeline.fireUserEventTriggered(successEvent);
|
||||
}
|
||||
|
||||
public Type type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Http2WebSocketEvent> T cast() {
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
HANDSHAKE_START,
|
||||
HANDSHAKE_SUCCESS,
|
||||
HANDSHAKE_ERROR,
|
||||
CLOSE_LOCAL_ENDSTREAM,
|
||||
CLOSE_REMOTE_ENDSTREAM,
|
||||
CLOSE_REMOTE_RESET,
|
||||
CLOSE_REMOTE_GOAWAY,
|
||||
WEIGHT_UPDATE,
|
||||
WRITE_ERROR
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents write error of frames that are not exposed to user code: HEADERS and RST_STREAM
|
||||
* frames sent by server on handshake, DATA frames with END_STREAM flag for graceful shutdown,
|
||||
* PRIORITY frames etc.
|
||||
*/
|
||||
public static class Http2WebSocketWriteErrorEvent extends Http2WebSocketEvent {
|
||||
private final String message;
|
||||
private final Throwable cause;
|
||||
|
||||
Http2WebSocketWriteErrorEvent(String message, Throwable cause) {
|
||||
super(Type.WRITE_ERROR);
|
||||
this.message = message;
|
||||
this.cause = cause;
|
||||
}
|
||||
|
||||
/** @return frame write error message */
|
||||
public String errorMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/** @return frame write error */
|
||||
public Throwable error() {
|
||||
return cause;
|
||||
}
|
||||
}
|
||||
|
||||
/** Base type for websocket-over-http2 lifecycle events */
|
||||
public static class Http2WebSocketLifecycleEvent extends Http2WebSocketEvent {
|
||||
private final int id;
|
||||
private final String path;
|
||||
private final String subprotocol;
|
||||
private final long timestampNanos;
|
||||
|
||||
Http2WebSocketLifecycleEvent(Type type, int id, String path, String subprotocol, long timestampNanos) {
|
||||
super(type);
|
||||
this.id = id;
|
||||
this.path = path;
|
||||
this.subprotocol = subprotocol;
|
||||
this.timestampNanos = timestampNanos;
|
||||
}
|
||||
|
||||
/** @return id to correlate events of particular websocket */
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/** @return websocket path */
|
||||
public String path() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/** @return websocket subprotocol */
|
||||
public String subprotocols() {
|
||||
return subprotocol;
|
||||
}
|
||||
|
||||
/** @return event timestamp */
|
||||
public long timestampNanos() {
|
||||
return timestampNanos;
|
||||
}
|
||||
}
|
||||
|
||||
/** websocket-over-http2 handshake start event */
|
||||
public static class Http2WebSocketHandshakeStartEvent extends Http2WebSocketLifecycleEvent {
|
||||
private final Http2Headers requestHeaders;
|
||||
|
||||
Http2WebSocketHandshakeStartEvent(int id, String path, String subprotocol, long timestampNanos, Http2Headers requestHeaders) {
|
||||
super(Type.HANDSHAKE_START, id, path, subprotocol, timestampNanos);
|
||||
this.requestHeaders = requestHeaders;
|
||||
}
|
||||
|
||||
/** @return websocket request headers */
|
||||
public Http2Headers requestHeaders() {
|
||||
return requestHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* websocket-over-http2 handshake error event
|
||||
*/
|
||||
public static class Http2WebSocketHandshakeErrorEvent extends Http2WebSocketLifecycleEvent {
|
||||
private final Http2Headers responseHeaders;
|
||||
private final String errorName;
|
||||
private final String errorMessage;
|
||||
private final Throwable error;
|
||||
|
||||
Http2WebSocketHandshakeErrorEvent(int id, String path, String subprotocols,
|
||||
long timestampNanos, Http2Headers responseHeaders, Throwable error) {
|
||||
this(id, path, subprotocols, timestampNanos, responseHeaders, error, null, null);
|
||||
}
|
||||
|
||||
Http2WebSocketHandshakeErrorEvent(int id, String path, String subprotocols,
|
||||
long timestampNanos, Http2Headers responseHeaders, String errorName, String errorMessage) {
|
||||
this(id, path, subprotocols, timestampNanos, responseHeaders, null, errorName, errorMessage);
|
||||
}
|
||||
|
||||
private Http2WebSocketHandshakeErrorEvent(int id, String path, String subprotocols, long timestampNanos,
|
||||
Http2Headers responseHeaders, Throwable error, String errorName, String errorMessage) {
|
||||
super(Type.HANDSHAKE_ERROR, id, path, subprotocols, timestampNanos);
|
||||
this.responseHeaders = responseHeaders;
|
||||
this.errorName = errorName;
|
||||
this.errorMessage = errorMessage;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
/** @return response headers of failed websocket handshake */
|
||||
public Http2Headers responseHeaders() {
|
||||
return responseHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return exception associated with failed websocket handshake. May be null, in this case
|
||||
* {@link #errorName()} and {@link #errorMessage()} contain error details.
|
||||
*/
|
||||
public Throwable error() {
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return name of error associated with failed websocket handshake. May be null, in this case
|
||||
* {@link #error()} contains respective exception
|
||||
*/
|
||||
public String errorName() {
|
||||
return errorName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return message of error associated with failed websocket handshake. May be null, in this
|
||||
* case {@link #error()} contains respective exception
|
||||
*/
|
||||
public String errorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* websocket-over-http2 handshake success event
|
||||
*/
|
||||
public static class Http2WebSocketHandshakeSuccessEvent extends Http2WebSocketLifecycleEvent {
|
||||
private final Http2Headers responseHeaders;
|
||||
|
||||
Http2WebSocketHandshakeSuccessEvent(int id, String path, String subprotocols,
|
||||
long timestampNanos, Http2Headers responseHeaders) {
|
||||
super(Type.HANDSHAKE_SUCCESS, id, path, subprotocols, timestampNanos);
|
||||
this.responseHeaders = responseHeaders;
|
||||
}
|
||||
|
||||
/** @return response headers of succeeded websocket handshake */
|
||||
public Http2Headers responseHeaders() {
|
||||
return responseHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* websocket-over-http2 close by remote event. Graceful close is denoted by {@link
|
||||
* Type#CLOSE_REMOTE_ENDSTREAM}, forced close is denoted by {@link Type#CLOSE_REMOTE_RESET}
|
||||
*/
|
||||
public static class Http2WebSocketRemoteCloseEvent extends Http2WebSocketLifecycleEvent {
|
||||
private Http2WebSocketRemoteCloseEvent(Type type, int id, String path, String subprotocols, long timestampNanos) {
|
||||
super(type, id, path, subprotocols, timestampNanos);
|
||||
}
|
||||
|
||||
static Http2WebSocketRemoteCloseEvent endStream(int id, String path, String subprotocols, long timestampNanos) {
|
||||
return new Http2WebSocketRemoteCloseEvent(Type.CLOSE_REMOTE_ENDSTREAM, id, path, subprotocols, timestampNanos);
|
||||
}
|
||||
|
||||
static Http2WebSocketRemoteCloseEvent reset(int id, String path, String subprotocols, long timestampNanos) {
|
||||
return new Http2WebSocketRemoteCloseEvent(Type.CLOSE_REMOTE_RESET, id, path, subprotocols, timestampNanos);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* graceful connection close by remote (GO_AWAY) event.
|
||||
*/
|
||||
public static class Http2WebSocketRemoteGoAwayEvent extends Http2WebSocketLifecycleEvent {
|
||||
private final long errorCode;
|
||||
|
||||
Http2WebSocketRemoteGoAwayEvent(int id, String path, String subprotocol, long timestampNanos, long errorCode) {
|
||||
super(Type.CLOSE_REMOTE_GOAWAY, id, path, subprotocol, timestampNanos);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
/** @return received GO_AWAY frame error code */
|
||||
public long errorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* websocket-over-http2 local graceful close event. Firing {@link
|
||||
* Http2WebSocketLocalCloseEvent#INSTANCE} on channel pipeline will close associated http2 stream
|
||||
* locally by sending empty DATA frame with END_STREAN flag set
|
||||
*/
|
||||
public static final class Http2WebSocketLocalCloseEvent extends Http2WebSocketEvent {
|
||||
|
||||
public static final Http2WebSocketLocalCloseEvent INSTANCE = new Http2WebSocketLocalCloseEvent();
|
||||
|
||||
Http2WebSocketLocalCloseEvent() {
|
||||
super(Type.CLOSE_LOCAL_ENDSTREAM);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* websocket-over-http2 stream weight update event. Firing {@link
|
||||
* Http2WebSocketLocalCloseEvent#INSTANCE} on channel pipeline will send PRIORITY frame for
|
||||
* associated http2 stream
|
||||
*/
|
||||
public static final class Http2WebSocketStreamWeightUpdateEvent extends Http2WebSocketEvent {
|
||||
|
||||
private final short streamWeight;
|
||||
|
||||
Http2WebSocketStreamWeightUpdateEvent(short streamWeight) {
|
||||
super(Type.WEIGHT_UPDATE);
|
||||
this.streamWeight = Preconditions.requireRange(streamWeight, 1, 256, "streamWeight");
|
||||
}
|
||||
|
||||
public short streamWeight() {
|
||||
return streamWeight;
|
||||
}
|
||||
|
||||
public static Http2WebSocketStreamWeightUpdateEvent create(short streamWeight) {
|
||||
return new Http2WebSocketStreamWeightUpdateEvent(streamWeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param webSocketChannel websocket-over-http2 channel
|
||||
* @return weight of http2 stream associated with websocket channel
|
||||
*/
|
||||
public static Short streamWeight(Channel webSocketChannel) {
|
||||
if (webSocketChannel instanceof Http2WebSocketChannel) {
|
||||
return ((Http2WebSocketChannel) webSocketChannel).streamWeightAttribute();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String nonNullString(CharSequence seq) {
|
||||
if (seq == null) {
|
||||
return "";
|
||||
}
|
||||
return seq.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package org.xbib.netty.http.common.ws;
|
||||
|
||||
import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData;
|
||||
import io.netty.util.AsciiString;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class Http2WebSocketExtensions {
|
||||
|
||||
static final String HEADER_WEBSOCKET_EXTENSIONS_VALUE_PERMESSAGE_DEFLATE = "permessage-deflate";
|
||||
|
||||
static final AsciiString HEADER_WEBSOCKET_EXTENSIONS_VALUE_PERMESSAGE_DEFLATE_ASCII =
|
||||
AsciiString.of(HEADER_WEBSOCKET_EXTENSIONS_VALUE_PERMESSAGE_DEFLATE);
|
||||
|
||||
static final Pattern HEADER_WEBSOCKET_EXTENSIONS_PARAMETER_PATTERN =
|
||||
Pattern.compile("^([^=]+)(=[\\\"]?([^\\\"]+)[\\\"]?)?$");
|
||||
|
||||
public static WebSocketExtensionData decode(CharSequence extensionHeader) {
|
||||
if (extensionHeader == null || extensionHeader.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
AsciiString asciiExtensionHeader = (AsciiString) extensionHeader;
|
||||
for (AsciiString extension : asciiExtensionHeader.split(',')) {
|
||||
AsciiString[] extensionParameters = extension.split(';');
|
||||
AsciiString name = extensionParameters[0].trim();
|
||||
if (HEADER_WEBSOCKET_EXTENSIONS_VALUE_PERMESSAGE_DEFLATE_ASCII.equals(name)) {
|
||||
Map<String, String> parameters;
|
||||
if (extensionParameters.length > 1) {
|
||||
parameters = new HashMap<>(extensionParameters.length - 1);
|
||||
for (int i = 1; i < extensionParameters.length; i++) {
|
||||
AsciiString parameter = extensionParameters[i].trim();
|
||||
Matcher parameterMatcher =
|
||||
HEADER_WEBSOCKET_EXTENSIONS_PARAMETER_PATTERN.matcher(parameter);
|
||||
if (parameterMatcher.matches()) {
|
||||
String key = parameterMatcher.group(1);
|
||||
if (key != null) {
|
||||
String value = parameterMatcher.group(3);
|
||||
parameters.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parameters = Collections.emptyMap();
|
||||
}
|
||||
return new WebSocketExtensionData(
|
||||
HEADER_WEBSOCKET_EXTENSIONS_VALUE_PERMESSAGE_DEFLATE, parameters);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String encode(WebSocketExtensionData extensionData) {
|
||||
String name = extensionData.name();
|
||||
Map<String, String> params = extensionData.parameters();
|
||||
if (params.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(sizeOf(name, params));
|
||||
sb.append(name);
|
||||
for (Map.Entry<String, String> param : params.entrySet()) {
|
||||
sb.append(";");
|
||||
sb.append(param.getKey());
|
||||
String value = param.getValue();
|
||||
if (value != null) {
|
||||
sb.append("=");
|
||||
sb.append(value);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static int sizeOf(String extensionName, Map<String, String> extensionParameters) {
|
||||
int size = extensionName.length();
|
||||
for (Map.Entry<String, String> param : extensionParameters.entrySet()) {
|
||||
size += param.getKey().length() + 1;
|
||||
String value = param.getValue();
|
||||
if (value != null) {
|
||||
size += value.length() + 1;
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
package org.xbib.netty.http.common.ws;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http2.Http2ConnectionDecoder;
|
||||
import io.netty.handler.codec.http2.Http2ConnectionHandler;
|
||||
import io.netty.handler.codec.http2.Http2Exception;
|
||||
import io.netty.handler.codec.http2.Http2Flags;
|
||||
import io.netty.handler.codec.http2.Http2FrameListener;
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
import io.netty.handler.codec.http2.Http2Settings;
|
||||
import io.netty.util.AsciiString;
|
||||
|
||||
/**
|
||||
* Base type for client and server websocket-over-http2 handlers
|
||||
*/
|
||||
public abstract class Http2WebSocketHandler extends ChannelDuplexHandler implements Http2FrameListener {
|
||||
|
||||
static final AsciiString HEADER_WEBSOCKET_ENDOFSTREAM_NAME = AsciiString.of("x-websocket-endofstream");
|
||||
|
||||
static final AsciiString HEADER_WEBSOCKET_ENDOFSTREAM_VALUE_TRUE = AsciiString.of("true");
|
||||
|
||||
static final AsciiString HEADER_WEBSOCKET_ENDOFSTREAM_VALUE_FALSE = AsciiString.of("false");
|
||||
|
||||
protected Http2ConnectionHandler http2Handler;
|
||||
|
||||
protected Http2FrameListener next;
|
||||
|
||||
public Http2WebSocketHandler() {}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
|
||||
Http2ConnectionHandler http2Handler = this.http2Handler = Preconditions.requireHandler(ctx.channel(), Http2ConnectionHandler.class);
|
||||
Http2ConnectionDecoder decoder = http2Handler.decoder();
|
||||
Http2FrameListener next = decoder.frameListener();
|
||||
decoder.frameListener(this);
|
||||
this.next = next;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData) throws Http2Exception {
|
||||
next().onGoAwayRead(ctx, lastStreamId, errorCode, debugData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception {
|
||||
next().onRstStreamRead(ctx, streamId, errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception {
|
||||
return next().onDataRead(ctx, streamId, data.retain(), padding, endOfStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
|
||||
boolean endOfStream) throws Http2Exception {
|
||||
next().onHeadersRead(ctx, streamId, headers, padding, endOfStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
|
||||
int streamDependency, short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception {
|
||||
next().onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endOfStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight,
|
||||
boolean exclusive) throws Http2Exception {
|
||||
next().onPriorityRead(ctx, streamId, streamDependency, weight, exclusive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception {
|
||||
next().onSettingsAckRead(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings)
|
||||
throws Http2Exception {
|
||||
next().onSettingsRead(ctx, settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPingRead(ChannelHandlerContext ctx, long data) throws Http2Exception {
|
||||
next().onPingRead(ctx, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPingAckRead(ChannelHandlerContext ctx, long data) throws Http2Exception {
|
||||
next().onPingAckRead(ctx, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
|
||||
Http2Headers headers, int padding) throws Http2Exception {
|
||||
next().onPushPromiseRead(ctx, streamId, promisedStreamId, headers, padding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
|
||||
throws Http2Exception {
|
||||
next().onWindowUpdateRead(ctx, streamId, windowSizeIncrement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags, ByteBuf payload)
|
||||
throws Http2Exception {
|
||||
next().onUnknownFrame(ctx, frameType, streamId, flags, payload);
|
||||
}
|
||||
|
||||
protected final Http2FrameListener next() {
|
||||
return next;
|
||||
}
|
||||
|
||||
static AsciiString endOfStreamName() {
|
||||
return HEADER_WEBSOCKET_ENDOFSTREAM_NAME;
|
||||
}
|
||||
|
||||
static AsciiString endOfStreamValue(boolean endOfStream) {
|
||||
return endOfStream
|
||||
? HEADER_WEBSOCKET_ENDOFSTREAM_VALUE_TRUE
|
||||
: HEADER_WEBSOCKET_ENDOFSTREAM_VALUE_FALSE;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
package org.xbib.netty.http.common.ws;
|
||||
|
||||
import io.netty.util.collection.IntCollections;
|
||||
import io.netty.util.collection.IntObjectHashMap;
|
||||
import io.netty.util.collection.IntObjectMap;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
final class Http2WebSocketHandlerContainers {
|
||||
|
||||
static final class SingleElementOptimizedMap<T> implements IntObjectMap<T> {
|
||||
int singleKey;
|
||||
T singleValue;
|
||||
IntObjectMap<T> delegate = IntCollections.emptyMap();
|
||||
|
||||
@Override
|
||||
public T get(int key) {
|
||||
int sk = singleKey;
|
||||
if (key == sk) {
|
||||
return singleValue;
|
||||
}
|
||||
if (sk == -1) {
|
||||
return delegate.get(key);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T put(int key, T value) {
|
||||
int sk = singleKey;
|
||||
if (sk == 0 || key == sk) {
|
||||
T sv = singleValue;
|
||||
singleKey = key;
|
||||
singleValue = value;
|
||||
return sv;
|
||||
}
|
||||
IntObjectMap<T> d = delegate;
|
||||
if (d.isEmpty()) {
|
||||
d = delegate = new IntObjectHashMap<>(4);
|
||||
d.put(sk, singleValue);
|
||||
singleKey = -1;
|
||||
singleValue = null;
|
||||
}
|
||||
return d.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T remove(int key) {
|
||||
int sk = singleKey;
|
||||
if (key == sk) {
|
||||
T sv = singleValue;
|
||||
singleKey = 0;
|
||||
singleValue = null;
|
||||
return sv;
|
||||
}
|
||||
if (sk == -1) {
|
||||
IntObjectMap<T> d = delegate;
|
||||
T removed = d.remove(key);
|
||||
if (d.isEmpty()) {
|
||||
singleKey = 0;
|
||||
delegate = IntCollections.emptyMap();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(int key) {
|
||||
int sk = singleKey;
|
||||
return sk == key || sk == -1 && delegate.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
int sk = singleKey;
|
||||
switch (sk) {
|
||||
case 0:
|
||||
return 0;
|
||||
case -1:
|
||||
return delegate.size();
|
||||
/*sk > 0*/
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return singleKey == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
singleKey = 0;
|
||||
singleValue = null;
|
||||
delegate = IntCollections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(BiConsumer<? super Integer, ? super T> action) {
|
||||
int sk = singleKey;
|
||||
if (sk > 0) {
|
||||
action.accept(sk, singleValue);
|
||||
} else if (sk == -1) {
|
||||
delegate.forEach(action);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<PrimitiveEntry<T>> entries() {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public T get(Object key) {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public T put(Integer key, T value) {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public T remove(Object key) {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends Integer, ? extends T> m) {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Integer> keySet() {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<T> values() {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<Integer, T>> entrySet() {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.xbib.netty.http.common.ws;
|
||||
|
||||
public interface Http2WebSocketMessages {
|
||||
String HANDSHAKE_UNEXPECTED_RESULT =
|
||||
"websocket handshake error: unexpected result - status=200, end_of_stream=true";
|
||||
String HANDSHAKE_UNSUPPORTED_VERSION =
|
||||
"websocket handshake error: unsupported version; supported versions - ";
|
||||
String HANDSHAKE_BAD_REQUEST =
|
||||
"websocket handshake error: bad request";
|
||||
String HANDSHAKE_PATH_NOT_FOUND =
|
||||
"websocket handshake error: path not found - ";
|
||||
String HANDSHAKE_PATH_NOT_FOUND_SUBPROTOCOLS =
|
||||
", subprotocols - ";
|
||||
String HANDSHAKE_UNEXPECTED_SUBPROTOCOL =
|
||||
"websocket handshake error: unexpected subprotocol - ";
|
||||
String HANDSHAKE_GENERIC_ERROR =
|
||||
"websocket handshake error: ";
|
||||
String HANDSHAKE_UNSUPPORTED_ACCEPTOR_TYPE =
|
||||
"websocket handshake error: async acceptors are not supported";
|
||||
String HANDSHAKE_UNSUPPORTED_BOOTSTRAP =
|
||||
"websocket handshake error: bootstrapping websockets with http2 is not supported by server";
|
||||
String HANDSHAKE_INVALID_REQUEST_HEADERS =
|
||||
"websocket handshake error: invalid request headers";
|
||||
String HANDSHAKE_INVALID_RESPONSE_HEADERS =
|
||||
"websocket handshake error: invalid response headers";
|
||||
String WRITE_ERROR = "websocket frame write error";
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package org.xbib.netty.http.common.ws;
|
||||
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
import io.netty.util.AsciiString;
|
||||
|
||||
public final class Http2WebSocketProtocol {
|
||||
|
||||
public static final char SETTINGS_ENABLE_CONNECT_PROTOCOL = 8;
|
||||
|
||||
public static final AsciiString HEADER_METHOD_CONNECT = AsciiString.of("CONNECT");
|
||||
|
||||
public static final AsciiString HEADER_PROTOCOL_NAME = AsciiString.of(":protocol");
|
||||
|
||||
public static final AsciiString HEADER_PROTOCOL_VALUE = AsciiString.of("websocket");
|
||||
|
||||
public static final AsciiString SCHEME_HTTP = AsciiString.of("http");
|
||||
|
||||
public static final AsciiString SCHEME_HTTPS = AsciiString.of("https");
|
||||
|
||||
public static final AsciiString HEADER_WEBSOCKET_VERSION_NAME = AsciiString.of("sec-websocket-version");
|
||||
|
||||
public static final AsciiString HEADER_WEBSOCKET_VERSION_VALUE = AsciiString.of("13");
|
||||
|
||||
public static final AsciiString HEADER_WEBSOCKET_SUBPROTOCOL_NAME = AsciiString.of("sec-websocket-protocol");
|
||||
|
||||
public static final AsciiString HEADER_WEBSOCKET_EXTENSIONS_NAME = AsciiString.of("sec-websocket-extensions");
|
||||
|
||||
public static final AsciiString HEADER_PROTOCOL_NAME_HANDSHAKED = AsciiString.of("x-protocol");
|
||||
|
||||
public static final AsciiString HEADER_METHOD_CONNECT_HANDSHAKED = AsciiString.of("POST");
|
||||
|
||||
public static Http2Headers extendedConnect(Http2Headers headers) {
|
||||
return headers.method(Http2WebSocketProtocol.HEADER_METHOD_CONNECT)
|
||||
.set(Http2WebSocketProtocol.HEADER_PROTOCOL_NAME, Http2WebSocketProtocol.HEADER_PROTOCOL_VALUE);
|
||||
}
|
||||
|
||||
public static boolean isExtendedConnect(Http2Headers headers) {
|
||||
return HEADER_METHOD_CONNECT.equals(headers.method())
|
||||
&& HEADER_PROTOCOL_VALUE.equals(headers.get(HEADER_PROTOCOL_NAME));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
package org.xbib.netty.http.common.ws;
|
||||
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
import io.netty.util.AsciiString;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class Http2WebSocketValidator {
|
||||
static final AsciiString PSEUDO_HEADER_METHOD = AsciiString.of(":method");
|
||||
static final AsciiString PSEUDO_HEADER_SCHEME = AsciiString.of(":scheme");
|
||||
static final AsciiString PSEUDO_HEADER_AUTHORITY = AsciiString.of(":authority");
|
||||
static final AsciiString PSEUDO_HEADER_PATH = AsciiString.of(":path");
|
||||
static final AsciiString PSEUDO_HEADER_PROTOCOL = AsciiString.of(":protocol");
|
||||
static final AsciiString PSEUDO_HEADER_STATUS = AsciiString.of(":status");
|
||||
static final AsciiString PSEUDO_HEADER_METHOD_CONNECT = AsciiString.of("connect");
|
||||
|
||||
static final AsciiString HEADER_CONNECTION = AsciiString.of("connection");
|
||||
static final AsciiString HEADER_KEEPALIVE = AsciiString.of("keep-alive");
|
||||
static final AsciiString HEADER_PROXY_CONNECTION = AsciiString.of("proxy-connection");
|
||||
static final AsciiString HEADER_TRANSFER_ENCODING = AsciiString.of("transfer-encoding");
|
||||
static final AsciiString HEADER_UPGRADE = AsciiString.of("upgrade");
|
||||
static final AsciiString HEADER_TE = AsciiString.of("te");
|
||||
static final AsciiString HEADER_TE_TRAILERS = AsciiString.of("trailers");
|
||||
|
||||
static final Set<CharSequence> INVALID_HEADERS = invalidHeaders();
|
||||
|
||||
public static boolean isValid(final Http2Headers responseHeaders) {
|
||||
boolean isFirst = true;
|
||||
for (Map.Entry<CharSequence, CharSequence> header : responseHeaders) {
|
||||
CharSequence name = header.getKey();
|
||||
if (isFirst) {
|
||||
if (!PSEUDO_HEADER_STATUS.equals(name) || isEmpty(header.getValue())) {
|
||||
return false;
|
||||
}
|
||||
isFirst = false;
|
||||
} else if (Http2Headers.PseudoHeaderName.hasPseudoHeaderFormat(name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return containsValidHeaders(responseHeaders);
|
||||
}
|
||||
|
||||
static boolean containsValidPseudoHeaders(
|
||||
Http2Headers requestHeaders, Set<CharSequence> validPseudoHeaders) {
|
||||
for (Map.Entry<CharSequence, CharSequence> header : requestHeaders) {
|
||||
CharSequence name = header.getKey();
|
||||
if (!Http2Headers.PseudoHeaderName.hasPseudoHeaderFormat(name)) {
|
||||
break;
|
||||
}
|
||||
if (!validPseudoHeaders.contains(name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean containsValidHeaders(Http2Headers headers) {
|
||||
for (CharSequence invalidHeader : INVALID_HEADERS) {
|
||||
if (headers.contains(invalidHeader)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
CharSequence te = headers.get(HEADER_TE);
|
||||
return te == null || HEADER_TE_TRAILERS.equals(te);
|
||||
}
|
||||
|
||||
static Set<CharSequence> validPseudoHeaders() {
|
||||
Set<CharSequence> result = new HashSet<>();
|
||||
result.add(PSEUDO_HEADER_SCHEME);
|
||||
result.add(PSEUDO_HEADER_AUTHORITY);
|
||||
result.add(PSEUDO_HEADER_PATH);
|
||||
result.add(PSEUDO_HEADER_METHOD);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Set<CharSequence> invalidHeaders() {
|
||||
Set<CharSequence> result = new HashSet<>();
|
||||
result.add(HEADER_CONNECTION);
|
||||
result.add(HEADER_KEEPALIVE);
|
||||
result.add(HEADER_PROXY_CONNECTION);
|
||||
result.add(HEADER_TRANSFER_ENCODING);
|
||||
result.add(HEADER_UPGRADE);
|
||||
return result;
|
||||
}
|
||||
|
||||
static boolean isEmpty(CharSequence seq) {
|
||||
return seq == null || seq.length() == 0;
|
||||
}
|
||||
|
||||
static boolean isHttp(CharSequence scheme) {
|
||||
return Http2WebSocketProtocol.SCHEME_HTTPS.equals(scheme)
|
||||
|| Http2WebSocketProtocol.SCHEME_HTTP.equals(scheme);
|
||||
}
|
||||
|
||||
public static class Http {
|
||||
|
||||
private static final Set<CharSequence> VALID_PSEUDO_HEADERS = validPseudoHeaders();
|
||||
|
||||
public static boolean isValid(final Http2Headers requestHeaders, boolean endOfStream) {
|
||||
AsciiString authority = AsciiString.of(requestHeaders.authority());
|
||||
/*must be non-empty, not include userinfo subcomponent*/
|
||||
if (isEmpty(authority) || authority.contains("@")) {
|
||||
return false;
|
||||
}
|
||||
AsciiString method = AsciiString.of(requestHeaders.method());
|
||||
if (isEmpty(method)) {
|
||||
return false;
|
||||
}
|
||||
AsciiString scheme = AsciiString.of(requestHeaders.scheme());
|
||||
AsciiString path = AsciiString.of(requestHeaders.path());
|
||||
if (method.equals(PSEUDO_HEADER_METHOD_CONNECT)) {
|
||||
if (!isEmpty(scheme) || !isEmpty(path)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (isEmpty(scheme)) {
|
||||
return false;
|
||||
}
|
||||
if (isEmpty(path) && isHttp(scheme)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return containsValidPseudoHeaders(requestHeaders, VALID_PSEUDO_HEADERS)
|
||||
&& containsValidHeaders(requestHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
public static class WebSocket {
|
||||
|
||||
private static final Set<CharSequence> VALID_PSEUDO_HEADERS;
|
||||
|
||||
static {
|
||||
Set<CharSequence> headers = VALID_PSEUDO_HEADERS = validPseudoHeaders();
|
||||
headers.add(PSEUDO_HEADER_PROTOCOL);
|
||||
}
|
||||
|
||||
public static boolean isValid(final Http2Headers requestHeaders, boolean endOfStream) {
|
||||
if (endOfStream) {
|
||||
return false;
|
||||
}
|
||||
if (!isHttp(requestHeaders.scheme())) {
|
||||
return false;
|
||||
}
|
||||
AsciiString authority = AsciiString.of(requestHeaders.authority());
|
||||
if (isEmpty(authority) || authority.contains("@")) {
|
||||
return false;
|
||||
}
|
||||
if (isEmpty(requestHeaders.path())) {
|
||||
return false;
|
||||
}
|
||||
return containsValidPseudoHeaders(requestHeaders, VALID_PSEUDO_HEADERS)
|
||||
&& containsValidHeaders(requestHeaders);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package org.xbib.netty.http.common.ws;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
|
||||
public final class Preconditions {
|
||||
|
||||
public static <T> T requireNonNull(T t, String message) {
|
||||
if (t == null) {
|
||||
throw new IllegalArgumentException(message + " must be non null");
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
public static String requireNonEmpty(String string, String message) {
|
||||
if (string == null || string.isEmpty()) {
|
||||
throw new IllegalArgumentException(message + " must be non empty");
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
public static <T extends ChannelHandler> T requireHandler(Channel channel, Class<T> handler) {
|
||||
T h = channel.pipeline().get(handler);
|
||||
if (h == null) {
|
||||
throw new IllegalArgumentException(
|
||||
handler.getSimpleName() + " is absent in the channel pipeline");
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
public static long requirePositive(long value, String message) {
|
||||
if (value <= 0) {
|
||||
throw new IllegalArgumentException(message + " must be positive: " + value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static int requireNonNegative(int value, String message) {
|
||||
if (value < 0) {
|
||||
throw new IllegalArgumentException(message + " must be non-negative: " + value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static short requireRange(int value, int from, int to, String message) {
|
||||
if (value >= from && value <= to) {
|
||||
return (short) value;
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
String.format("%s must belong to range [%d, %d]: ", message, from, to));
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
module org.xbib.netty.http.server.api {
|
||||
exports org.xbib.netty.http.server.api;
|
||||
exports org.xbib.netty.http.server.api.annotation;
|
||||
exports org.xbib.netty.http.server.api.security;
|
||||
requires org.xbib.netty.http.common;
|
||||
requires org.xbib.net.url;
|
||||
requires io.netty.buffer;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package org.xbib.netty.http.server.api.security;
|
||||
package org.xbib.netty.http.server.api;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
package org.xbib.netty.http.server.api;
|
||||
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.channel.WriteBufferWaterMark;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
|
||||
import io.netty.handler.codec.http2.Http2Settings;
|
||||
import io.netty.handler.logging.LogLevel;
|
||||
import io.netty.handler.ssl.CipherSuiteFilter;
|
||||
import io.netty.handler.ssl.OpenSsl;
|
||||
import io.netty.handler.ssl.SslProvider;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
import org.xbib.netty.http.common.security.SecurityUtil;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.security.Provider;
|
||||
import java.util.Collection;
|
||||
|
@ -61,8 +66,6 @@ public interface ServerConfig {
|
|||
|
||||
boolean isCompressionEnabled();
|
||||
|
||||
int getCompressionThreshold();
|
||||
|
||||
boolean isDecompressionEnabled();
|
||||
|
||||
boolean isInstallHttp2Upgrade();
|
||||
|
@ -92,4 +95,174 @@ public interface ServerConfig {
|
|||
Domain<? extends EndpointResolver<?>> getDomain(String name);
|
||||
|
||||
Domain<? extends EndpointResolver<?>> getDefaultDomain();
|
||||
|
||||
SimpleChannelInboundHandler<WebSocketFrame> getWebSocketFrameHandler();
|
||||
|
||||
interface Defaults {
|
||||
|
||||
/**
|
||||
* Default bind address. We do not want to use port 80 or 8080.
|
||||
*/
|
||||
HttpAddress ADDRESS = HttpAddress.http1("localhost", 8008);
|
||||
|
||||
/**
|
||||
* If frame logging/traffic logging is enabled or not.
|
||||
*/
|
||||
boolean DEBUG = false;
|
||||
|
||||
/**
|
||||
* Default debug log level.
|
||||
*/
|
||||
LogLevel DEBUG_LOG_LEVEL = LogLevel.DEBUG;
|
||||
|
||||
String TRANSPORT_PROVIDER_NAME = null;
|
||||
|
||||
/**
|
||||
* Let Netty decide about parent thread count.
|
||||
*/
|
||||
int PARENT_THREAD_COUNT = 0;
|
||||
|
||||
/**
|
||||
* Let Netty decide about child thread count.
|
||||
*/
|
||||
int CHILD_THREAD_COUNT = 0;
|
||||
|
||||
/**
|
||||
* Blocking thread pool count. Disabled by default, use Netty threads.
|
||||
*/
|
||||
int BLOCKING_THREAD_COUNT = 0;
|
||||
|
||||
/**
|
||||
* Blocking thread pool queue count. Disabled by default, use Netty threads.
|
||||
*/
|
||||
int BLOCKING_QUEUE_COUNT = 0;
|
||||
|
||||
/**
|
||||
* Default for SO_REUSEADDR.
|
||||
*/
|
||||
boolean SO_REUSEADDR = true;
|
||||
|
||||
/**
|
||||
* Default for TCP_NODELAY.
|
||||
*/
|
||||
boolean TCP_NODELAY = true;
|
||||
|
||||
/**
|
||||
* Set TCP send buffer to 64k per socket.
|
||||
*/
|
||||
int TCP_SEND_BUFFER_SIZE = 64 * 1024;
|
||||
|
||||
/**
|
||||
* Set TCP receive buffer to 64k per socket.
|
||||
*/
|
||||
int TCP_RECEIVE_BUFFER_SIZE = 64 * 1024;
|
||||
|
||||
/**
|
||||
* Default for socket back log.
|
||||
*/
|
||||
int SO_BACKLOG = 10 * 1024;
|
||||
|
||||
/**
|
||||
* Default connect timeout in milliseconds.
|
||||
*/
|
||||
int CONNECT_TIMEOUT_MILLIS = 5000;
|
||||
|
||||
/**
|
||||
* Default connect timeout in milliseconds.
|
||||
*/
|
||||
int READ_TIMEOUT_MILLIS = 15000;
|
||||
|
||||
/**
|
||||
* Default idle timeout in milliseconds.
|
||||
*/
|
||||
int IDLE_TIMEOUT_MILLIS = 60000;
|
||||
|
||||
/**
|
||||
* Set HTTP chunk maximum size to 8k.
|
||||
* See {@link io.netty.handler.codec.http.HttpClientCodec}.
|
||||
*/
|
||||
int MAX_CHUNK_SIZE = 8 * 1024;
|
||||
|
||||
/**
|
||||
* Set HTTP initial line length to 4k.
|
||||
* See {@link io.netty.handler.codec.http.HttpClientCodec}.
|
||||
*/
|
||||
int MAX_INITIAL_LINE_LENGTH = 4 * 1024;
|
||||
|
||||
/**
|
||||
* Set HTTP maximum headers size to 8k.
|
||||
* See {@link io.netty.handler.codec.http.HttpClientCodec}.
|
||||
*/
|
||||
int MAX_HEADERS_SIZE = 8 * 1024;
|
||||
|
||||
/**
|
||||
* Set maximum content length to 256 MB.
|
||||
*/
|
||||
int MAX_CONTENT_LENGTH = 256 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* HTTP/1 pipelining capacity. 1024 is very high, it means
|
||||
* 1024 requests can be present for a single client.
|
||||
*/
|
||||
int PIPELINING_CAPACITY = 1024;
|
||||
|
||||
/**
|
||||
* This is Netty's default.
|
||||
*/
|
||||
int MAX_COMPOSITE_BUFFER_COMPONENTS = 1024;
|
||||
|
||||
/**
|
||||
* Default write buffer water mark.
|
||||
*/
|
||||
WriteBufferWaterMark WRITE_BUFFER_WATER_MARK = WriteBufferWaterMark.DEFAULT;
|
||||
|
||||
/**
|
||||
* Default for compression.
|
||||
*/
|
||||
boolean ENABLE_COMPRESSION = true;
|
||||
|
||||
/**
|
||||
* Default for decompression.
|
||||
*/
|
||||
boolean ENABLE_DECOMPRESSION = true;
|
||||
|
||||
/**
|
||||
* Default HTTP/2 settings.
|
||||
*/
|
||||
Http2Settings HTTP_2_SETTINGS = Http2Settings.defaultSettings();
|
||||
|
||||
/**
|
||||
* Default for HTTP/2 upgrade under HTTP 1.
|
||||
*/
|
||||
boolean INSTALL_HTTP_UPGRADE2 = false;
|
||||
|
||||
/**
|
||||
* Default SSL provider.
|
||||
*/
|
||||
SslProvider SSL_PROVIDER = SecurityUtil.Defaults.DEFAULT_SSL_PROVIDER;
|
||||
|
||||
/**
|
||||
* Default SSL context provider (for JDK SSL only).
|
||||
*/
|
||||
Provider SSL_CONTEXT_PROVIDER = null;
|
||||
|
||||
/**
|
||||
* Transport layer security protocol versions.
|
||||
* Do not use SSLv2, SSLv3, TLS 1.0, TLS 1.1.
|
||||
*/
|
||||
String[] PROTOCOLS = OpenSsl.isAvailable() && OpenSsl.version() <= 0x10101009L ?
|
||||
new String[] { "TLSv1.2" } :
|
||||
new String[] { "TLSv1.3", "TLSv1.2" };
|
||||
|
||||
/**
|
||||
* Default ciphers. We care about HTTP/2.
|
||||
*/
|
||||
Iterable<String> CIPHERS = SecurityUtil.Defaults.DEFAULT_CIPHERS;
|
||||
|
||||
/**
|
||||
* Default cipher suite filter.
|
||||
*/
|
||||
CipherSuiteFilter CIPHER_SUITE_FILTER = SecurityUtil.Defaults.DEFAULT_CIPHER_SUITE_FILTER;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
package org.xbib.netty.http.server.api.annotation;
|
||||
|
||||
import org.xbib.netty.http.server.api.Filter;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* The {@code Endpoint} annotation decorates methods which are mapped
|
||||
* to a HTTP endpoint within the server, and provide its contents.
|
||||
* The annotated methods must have the same signature and contract
|
||||
* as {@link Filter#handle}, but can have arbitrary names.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface Endpoint {
|
||||
|
||||
/**
|
||||
* The path that this field maps to (must begin with '/').
|
||||
*
|
||||
* @return the path that this field maps to
|
||||
*/
|
||||
String path();
|
||||
|
||||
/**
|
||||
* The HTTP methods supported by this endpoint (default is "GET" and "HEAD").
|
||||
*
|
||||
* @return the HTTP methods supported by this endpoint
|
||||
*/
|
||||
String[] methods() default {"GET", "HEAD"};
|
||||
|
||||
String[] contentTypes();
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import org.xbib.netty.http.server.api.ServerCertificateProvider;
|
||||
import org.xbib.netty.http.server.protocol.http1.Http1;
|
||||
import org.xbib.netty.http.server.protocol.http2.Http2;
|
||||
|
||||
module org.xbib.netty.http.server {
|
||||
uses org.xbib.netty.http.server.api.security.ServerCertificateProvider;
|
||||
uses ServerCertificateProvider;
|
||||
uses org.xbib.netty.http.server.api.ServerProtocolProvider;
|
||||
uses org.xbib.netty.http.common.TransportProvider;
|
||||
exports org.xbib.netty.http.server;
|
||||
|
|
|
@ -34,8 +34,7 @@ public abstract class BaseTransport implements ServerTransport {
|
|||
* @param reqHeaders the request headers
|
||||
* @return whether further processing should be performed
|
||||
*/
|
||||
protected static AcceptState acceptRequest(HttpVersion httpVersion,
|
||||
HttpHeaders reqHeaders) {
|
||||
protected static AcceptState acceptRequest(HttpVersion httpVersion, HttpHeaders reqHeaders) {
|
||||
if (httpVersion.majorVersion() == 1 || httpVersion.majorVersion() == 2) {
|
||||
if (!reqHeaders.contains(HttpHeaderNames.HOST)) {
|
||||
// RFC2616#14.23: missing Host header gets 400
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package org.xbib.netty.http.server;
|
||||
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.channel.WriteBufferWaterMark;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
|
||||
import io.netty.handler.codec.http2.Http2Settings;
|
||||
import io.netty.handler.logging.LogLevel;
|
||||
import io.netty.handler.ssl.CipherSuiteFilter;
|
||||
import io.netty.handler.ssl.OpenSsl;
|
||||
import io.netty.handler.ssl.SslProvider;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
import org.xbib.netty.http.common.security.SecurityUtil;
|
||||
|
@ -21,180 +22,6 @@ import javax.net.ssl.TrustManagerFactory;
|
|||
|
||||
public class DefaultServerConfig implements ServerConfig {
|
||||
|
||||
interface Defaults {
|
||||
|
||||
/**
|
||||
* Default bind address. We do not want to use port 80 or 8080.
|
||||
*/
|
||||
HttpAddress ADDRESS = HttpAddress.http1("localhost", 8008);
|
||||
|
||||
/**
|
||||
* If frame logging/traffic logging is enabled or not.
|
||||
*/
|
||||
boolean DEBUG = false;
|
||||
|
||||
/**
|
||||
* Default debug log level.
|
||||
*/
|
||||
LogLevel DEBUG_LOG_LEVEL = LogLevel.DEBUG;
|
||||
|
||||
String TRANSPORT_PROVIDER_NAME = null;
|
||||
|
||||
/**
|
||||
* Let Netty decide about parent thread count.
|
||||
*/
|
||||
int PARENT_THREAD_COUNT = 0;
|
||||
|
||||
/**
|
||||
* Let Netty decide about child thread count.
|
||||
*/
|
||||
int CHILD_THREAD_COUNT = 0;
|
||||
|
||||
/**
|
||||
* Blocking thread pool count. Disabled by default, use Netty threads.
|
||||
*/
|
||||
int BLOCKING_THREAD_COUNT = 0;
|
||||
|
||||
/**
|
||||
* Blocking thread pool queue count. Disabled by default, use Netty threads.
|
||||
*/
|
||||
int BLOCKING_QUEUE_COUNT = 0;
|
||||
|
||||
/**
|
||||
* Default for SO_REUSEADDR.
|
||||
*/
|
||||
boolean SO_REUSEADDR = true;
|
||||
|
||||
/**
|
||||
* Default for TCP_NODELAY.
|
||||
*/
|
||||
boolean TCP_NODELAY = true;
|
||||
|
||||
/**
|
||||
* Set TCP send buffer to 64k per socket.
|
||||
*/
|
||||
int TCP_SEND_BUFFER_SIZE = 64 * 1024;
|
||||
|
||||
/**
|
||||
* Set TCP receive buffer to 64k per socket.
|
||||
*/
|
||||
int TCP_RECEIVE_BUFFER_SIZE = 64 * 1024;
|
||||
|
||||
/**
|
||||
* Default for socket back log.
|
||||
*/
|
||||
int SO_BACKLOG = 10 * 1024;
|
||||
|
||||
/**
|
||||
* Default connect timeout in milliseconds.
|
||||
*/
|
||||
int CONNECT_TIMEOUT_MILLIS = 5000;
|
||||
|
||||
/**
|
||||
* Default connect timeout in milliseconds.
|
||||
*/
|
||||
int READ_TIMEOUT_MILLIS = 15000;
|
||||
|
||||
/**
|
||||
* Default idle timeout in milliseconds.
|
||||
*/
|
||||
int IDLE_TIMEOUT_MILLIS = 60000;
|
||||
|
||||
/**
|
||||
* Set HTTP chunk maximum size to 8k.
|
||||
* See {@link io.netty.handler.codec.http.HttpClientCodec}.
|
||||
*/
|
||||
int MAX_CHUNK_SIZE = 8 * 1024;
|
||||
|
||||
/**
|
||||
* Set HTTP initial line length to 4k.
|
||||
* See {@link io.netty.handler.codec.http.HttpClientCodec}.
|
||||
*/
|
||||
int MAX_INITIAL_LINE_LENGTH = 4 * 1024;
|
||||
|
||||
/**
|
||||
* Set HTTP maximum headers size to 8k.
|
||||
* See {@link io.netty.handler.codec.http.HttpClientCodec}.
|
||||
*/
|
||||
int MAX_HEADERS_SIZE = 8 * 1024;
|
||||
|
||||
/**
|
||||
* Set maximum content length to 256 MB.
|
||||
*/
|
||||
int MAX_CONTENT_LENGTH = 256 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* HTTP/1 pipelining capacity. 1024 is very high, it means
|
||||
* 1024 requests can be present for a single client.
|
||||
*/
|
||||
int PIPELINING_CAPACITY = 1024;
|
||||
|
||||
/**
|
||||
* This is Netty's default.
|
||||
*/
|
||||
int MAX_COMPOSITE_BUFFER_COMPONENTS = 1024;
|
||||
|
||||
/**
|
||||
* Default write buffer water mark.
|
||||
*/
|
||||
WriteBufferWaterMark WRITE_BUFFER_WATER_MARK = WriteBufferWaterMark.DEFAULT;
|
||||
|
||||
/**
|
||||
* Default for compression.
|
||||
*/
|
||||
boolean ENABLE_COMPRESSION = true;
|
||||
|
||||
/**
|
||||
* Default compression threshold. If a response size is over this value,
|
||||
* it will be compressed, otherwise not.
|
||||
*/
|
||||
int COMPRESSION_THRESHOLD = 8192;
|
||||
|
||||
/**
|
||||
* Default for decompression.
|
||||
*/
|
||||
boolean ENABLE_DECOMPRESSION = true;
|
||||
|
||||
/**
|
||||
* Default HTTP/2 settings.
|
||||
*/
|
||||
Http2Settings HTTP_2_SETTINGS = Http2Settings.defaultSettings();
|
||||
|
||||
/**
|
||||
* Default for HTTP/2 upgrade under HTTP 1.
|
||||
*/
|
||||
boolean INSTALL_HTTP_UPGRADE2 = false;
|
||||
|
||||
/**
|
||||
* Default SSL provider.
|
||||
*/
|
||||
SslProvider SSL_PROVIDER = SecurityUtil.Defaults.DEFAULT_SSL_PROVIDER;
|
||||
|
||||
/**
|
||||
* Default SSL context provider (for JDK SSL only).
|
||||
*/
|
||||
Provider SSL_CONTEXT_PROVIDER = null;
|
||||
|
||||
/**
|
||||
* Transport layer security protocol versions.
|
||||
* Do not use SSLv2, SSLv3, TLS 1.0, TLS 1.1.
|
||||
*/
|
||||
String[] PROTOCOLS = OpenSsl.isAvailable() && OpenSsl.version() <= 0x10101009L ?
|
||||
new String[] { "TLSv1.2" } :
|
||||
new String[] { "TLSv1.3", "TLSv1.2" };
|
||||
|
||||
/**
|
||||
* Default ciphers. We care about HTTP/2.
|
||||
*/
|
||||
Iterable<String> CIPHERS = SecurityUtil.Defaults.DEFAULT_CIPHERS;
|
||||
|
||||
/**
|
||||
* Default cipher suite filter.
|
||||
*/
|
||||
CipherSuiteFilter CIPHER_SUITE_FILTER = SecurityUtil.Defaults.DEFAULT_CIPHER_SUITE_FILTER;
|
||||
|
||||
}
|
||||
|
||||
private HttpAddress httpAddress = Defaults.ADDRESS;
|
||||
|
||||
private boolean debug = Defaults.DEBUG;
|
||||
|
@ -243,8 +70,6 @@ public class DefaultServerConfig implements ServerConfig {
|
|||
|
||||
private boolean enableCompression = Defaults.ENABLE_COMPRESSION;
|
||||
|
||||
private int compressionThreshold = Defaults.COMPRESSION_THRESHOLD;
|
||||
|
||||
private boolean enableDecompression = Defaults.ENABLE_DECOMPRESSION;
|
||||
|
||||
private Http2Settings http2Settings = Defaults.HTTP_2_SETTINGS;
|
||||
|
@ -271,6 +96,8 @@ public class DefaultServerConfig implements ServerConfig {
|
|||
|
||||
private boolean acceptInvalidCertificates = false;
|
||||
|
||||
private SimpleChannelInboundHandler<WebSocketFrame> webSocketFrameHandler;
|
||||
|
||||
public DefaultServerConfig() {
|
||||
this.domains = new LinkedList<>();
|
||||
}
|
||||
|
@ -492,15 +319,6 @@ public class DefaultServerConfig implements ServerConfig {
|
|||
return enableCompression;
|
||||
}
|
||||
|
||||
public ServerConfig setCompressionThreshold(int compressionThreshold) {
|
||||
this.compressionThreshold = compressionThreshold;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getCompressionThreshold() {
|
||||
return compressionThreshold;
|
||||
}
|
||||
|
||||
public ServerConfig setDecompression(boolean enabled) {
|
||||
this.enableDecompression = enabled;
|
||||
return this;
|
||||
|
@ -642,4 +460,14 @@ public class DefaultServerConfig implements ServerConfig {
|
|||
domains.stream().filter(d -> d.getName().equals("*")).findFirst();
|
||||
return domainOptional.orElse(domains.getFirst());
|
||||
}
|
||||
|
||||
public ServerConfig setWebSocketFrameHandler(SimpleChannelInboundHandler<WebSocketFrame> webSocketFrameHandler) {
|
||||
this.webSocketFrameHandler = webSocketFrameHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SimpleChannelInboundHandler<WebSocketFrame> getWebSocketFrameHandler() {
|
||||
return webSocketFrameHandler;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import org.xbib.netty.http.common.HttpAddress;
|
|||
import org.xbib.netty.http.common.HttpMethod;
|
||||
import org.xbib.netty.http.server.api.Domain;
|
||||
import org.xbib.netty.http.server.api.EndpointResolver;
|
||||
import org.xbib.netty.http.server.api.security.ServerCertificateProvider;
|
||||
import org.xbib.netty.http.server.api.ServerCertificateProvider;
|
||||
import org.xbib.netty.http.common.security.SecurityUtil;
|
||||
import org.xbib.netty.http.server.api.ServerRequest;
|
||||
import org.xbib.netty.http.server.api.ServerResponse;
|
||||
|
|
|
@ -5,12 +5,14 @@ import io.netty.buffer.ByteBufAllocator;
|
|||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.channel.WriteBufferWaterMark;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.ServerSocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
|
||||
import io.netty.handler.logging.LoggingHandler;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.util.DomainWildcardMappingBuilder;
|
||||
|
@ -21,6 +23,7 @@ import org.xbib.netty.http.common.HttpChannelInitializer;
|
|||
import org.xbib.netty.http.common.TransportProvider;
|
||||
import org.xbib.netty.http.server.api.Domain;
|
||||
import org.xbib.netty.http.server.api.EndpointResolver;
|
||||
import org.xbib.netty.http.server.api.ServerConfig;
|
||||
import org.xbib.netty.http.server.api.ServerProtocolProvider;
|
||||
import org.xbib.netty.http.server.api.ServerRequest;
|
||||
import org.xbib.netty.http.server.api.ServerResponse;
|
||||
|
@ -66,7 +69,7 @@ public final class Server implements AutoCloseable {
|
|||
}
|
||||
}
|
||||
|
||||
private final DefaultServerConfig serverConfig;
|
||||
private final ServerConfig serverConfig;
|
||||
|
||||
private final EventLoopGroup parentEventLoopGroup;
|
||||
|
||||
|
@ -99,7 +102,7 @@ public final class Server implements AutoCloseable {
|
|||
* @param executor an extra blocking thread pool executor or null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Server(DefaultServerConfig serverConfig,
|
||||
private Server(ServerConfig serverConfig,
|
||||
ByteBufAllocator byteBufAllocator,
|
||||
EventLoopGroup parentEventLoopGroup,
|
||||
EventLoopGroup childEventLoopGroup,
|
||||
|
@ -177,7 +180,7 @@ public final class Server implements AutoCloseable {
|
|||
return new Builder(httpServerDomain);
|
||||
}
|
||||
|
||||
public DefaultServerConfig getServerConfig() {
|
||||
public ServerConfig getServerConfig() {
|
||||
return serverConfig;
|
||||
}
|
||||
|
||||
|
@ -373,7 +376,7 @@ public final class Server implements AutoCloseable {
|
|||
throw new IllegalStateException("no channel initializer found for major version " + majorVersion);
|
||||
}
|
||||
|
||||
private static EventLoopGroup createParentEventLoopGroup(DefaultServerConfig serverConfig,
|
||||
private static EventLoopGroup createParentEventLoopGroup(ServerConfig serverConfig,
|
||||
EventLoopGroup parentEventLoopGroup ) {
|
||||
EventLoopGroup eventLoopGroup = parentEventLoopGroup;
|
||||
if (eventLoopGroup == null) {
|
||||
|
@ -391,7 +394,7 @@ public final class Server implements AutoCloseable {
|
|||
return eventLoopGroup;
|
||||
}
|
||||
|
||||
private static EventLoopGroup createChildEventLoopGroup(DefaultServerConfig serverConfig,
|
||||
private static EventLoopGroup createChildEventLoopGroup(ServerConfig serverConfig,
|
||||
EventLoopGroup childEventLoopGroup ) {
|
||||
EventLoopGroup eventLoopGroup = childEventLoopGroup;
|
||||
if (eventLoopGroup == null) {
|
||||
|
@ -409,7 +412,7 @@ public final class Server implements AutoCloseable {
|
|||
return eventLoopGroup;
|
||||
}
|
||||
|
||||
private static Class<? extends ServerSocketChannel> createSocketChannelClass(DefaultServerConfig serverConfig,
|
||||
private static Class<? extends ServerSocketChannel> createSocketChannelClass(ServerConfig serverConfig,
|
||||
Class<? extends ServerSocketChannel> socketChannelClass) {
|
||||
Class<? extends ServerSocketChannel> channelClass = socketChannelClass;
|
||||
if (channelClass == null) {
|
||||
|
@ -684,6 +687,11 @@ public final class Server implements AutoCloseable {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder setWebSocketFrameHandler(SimpleChannelInboundHandler<WebSocketFrame> webSocketFrameHandler) {
|
||||
this.serverConfig.setWebSocketFrameHandler(webSocketFrameHandler);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Server build() {
|
||||
int maxThreads = serverConfig.getBlockingThreadCount();
|
||||
int maxQueue = serverConfig.getBlockingQueueCount();
|
||||
|
@ -732,9 +740,8 @@ public final class Server implements AutoCloseable {
|
|||
}
|
||||
}
|
||||
logger.log(Level.INFO, "configured domains: " + serverConfig.getDomains());
|
||||
return new Server(serverConfig, byteBufAllocator,
|
||||
parentEventLoopGroup, childEventLoopGroup, socketChannelClass,
|
||||
executor);
|
||||
return new Server(serverConfig, byteBufAllocator, parentEventLoopGroup, childEventLoopGroup,
|
||||
socketChannelClass, executor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,8 @@ import org.xbib.netty.http.server.api.EndpointResolver;
|
|||
import org.xbib.netty.http.server.api.Filter;
|
||||
import org.xbib.netty.http.server.api.ServerRequest;
|
||||
import org.xbib.netty.http.server.api.ServerResponse;
|
||||
import org.xbib.netty.http.server.api.annotation.Endpoint;
|
||||
import org.xbib.netty.http.server.endpoint.service.MethodService;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
@ -108,32 +103,6 @@ public class HttpEndpointResolver implements EndpointResolver<HttpEndpoint> {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a service for the methods of the given object that
|
||||
* are annotated with the {@link Endpoint} annotation.
|
||||
* @param classWithAnnotatedMethods class with annotated methods
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder addEndpoint(Object classWithAnnotatedMethods) {
|
||||
Objects.requireNonNull(classWithAnnotatedMethods);
|
||||
for (Class<?> clazz = classWithAnnotatedMethods.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
Endpoint endpoint = method.getAnnotation(Endpoint.class);
|
||||
if (endpoint != null) {
|
||||
MethodService methodService = new MethodService(method, classWithAnnotatedMethods);
|
||||
addEndpoint(HttpEndpoint.builder()
|
||||
.setPrefix(prefix)
|
||||
.setPath(endpoint.path())
|
||||
.setMethods(Arrays.asList(endpoint.methods()))
|
||||
.setContentTypes(Arrays.asList(endpoint.contentTypes()))
|
||||
.setBefore(Collections.singletonList(methodService))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDispatcher(Filter dispatcher) {
|
||||
Objects.requireNonNull(dispatcher);
|
||||
this.dispatcher = dispatcher;
|
||||
|
|
|
@ -6,7 +6,7 @@ import io.netty.handler.ssl.SslContext;
|
|||
import io.netty.handler.ssl.SslHandler;
|
||||
import io.netty.util.Mapping;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
import org.xbib.netty.http.server.DefaultServerConfig;
|
||||
import org.xbib.netty.http.server.api.ServerConfig;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.logging.Level;
|
||||
|
@ -18,12 +18,12 @@ public class ExtendedSNIHandler extends SniHandler {
|
|||
|
||||
private static final Logger logger = Logger.getLogger(ExtendedSNIHandler.class.getName());
|
||||
|
||||
private final DefaultServerConfig serverConfig;
|
||||
private final ServerConfig serverConfig;
|
||||
|
||||
private final HttpAddress httpAddress;
|
||||
|
||||
public ExtendedSNIHandler(Mapping<? super String, ? extends SslContext> mapping,
|
||||
DefaultServerConfig serverConfig, HttpAddress httpAddress) {
|
||||
ServerConfig serverConfig, HttpAddress httpAddress) {
|
||||
super(mapping);
|
||||
this.serverConfig = serverConfig;
|
||||
this.httpAddress = httpAddress;
|
||||
|
@ -35,7 +35,7 @@ public class ExtendedSNIHandler extends SniHandler {
|
|||
}
|
||||
|
||||
private static SslHandler newSslHandler(SslContext sslContext,
|
||||
DefaultServerConfig serverConfig,
|
||||
ServerConfig serverConfig,
|
||||
ByteBufAllocator allocator,
|
||||
HttpAddress httpAddress) {
|
||||
InetSocketAddress peer = httpAddress.getInetSocketAddress();
|
||||
|
|
|
@ -15,6 +15,8 @@ import io.netty.handler.codec.http.HttpObjectAggregator;
|
|||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
||||
import io.netty.handler.logging.LogLevel;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||
|
@ -22,8 +24,8 @@ import io.netty.handler.timeout.ReadTimeoutHandler;
|
|||
import io.netty.util.Mapping;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
import org.xbib.netty.http.server.Server;
|
||||
import org.xbib.netty.http.server.DefaultServerConfig;
|
||||
import org.xbib.netty.http.common.HttpChannelInitializer;
|
||||
import org.xbib.netty.http.server.api.ServerConfig;
|
||||
import org.xbib.netty.http.server.handler.ExtendedSNIHandler;
|
||||
import org.xbib.netty.http.server.handler.IdleTimeoutHandler;
|
||||
import org.xbib.netty.http.server.handler.TrafficLoggingHandler;
|
||||
|
@ -39,7 +41,7 @@ public class Http1ChannelInitializer extends ChannelInitializer<Channel>
|
|||
|
||||
private final Server server;
|
||||
|
||||
private final DefaultServerConfig serverConfig;
|
||||
private final ServerConfig serverConfig;
|
||||
|
||||
private final HttpAddress httpAddress;
|
||||
|
||||
|
@ -89,8 +91,7 @@ public class Http1ChannelInitializer extends ChannelInitializer<Channel>
|
|||
serverConfig.getMaxHeadersSize(), serverConfig.getMaxChunkSize()));
|
||||
if (serverConfig.isCompressionEnabled()) {
|
||||
pipeline.addLast("http-server-compressor",
|
||||
new HttpContentCompressor(6, 15, 8,
|
||||
serverConfig.getCompressionThreshold()));
|
||||
new HttpContentCompressor());
|
||||
}
|
||||
if (serverConfig.isDecompressionEnabled()) {
|
||||
pipeline.addLast("http-server-decompressor",
|
||||
|
@ -99,10 +100,20 @@ public class Http1ChannelInitializer extends ChannelInitializer<Channel>
|
|||
HttpObjectAggregator httpObjectAggregator =
|
||||
new HttpObjectAggregator(serverConfig.getMaxContentLength());
|
||||
httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents());
|
||||
pipeline.addLast("http-server-aggregator", httpObjectAggregator);
|
||||
pipeline.addLast("http-server-pipelining", new HttpPipeliningHandler(serverConfig.getPipeliningCapacity()));
|
||||
pipeline.addLast("http-server-handler", new ServerMessages(server));
|
||||
pipeline.addLast("http-idle-timeout-handler", new IdleTimeoutHandler(serverConfig.getIdleTimeoutMillis()));
|
||||
pipeline.addLast("http-server-aggregator",
|
||||
httpObjectAggregator);
|
||||
if (serverConfig.getWebSocketFrameHandler() != null) {
|
||||
pipeline.addLast("http-server-ws-protocol-handler",
|
||||
new WebSocketServerProtocolHandler("/websocket"));
|
||||
pipeline.addLast("http-server-ws-handler",
|
||||
serverConfig.getWebSocketFrameHandler());
|
||||
}
|
||||
pipeline.addLast("http-server-pipelining",
|
||||
new HttpPipeliningHandler(serverConfig.getPipeliningCapacity()));
|
||||
pipeline.addLast("http-server-handler",
|
||||
new ServerMessages(server));
|
||||
pipeline.addLast("http-idle-timeout-handler",
|
||||
new IdleTimeoutHandler(serverConfig.getIdleTimeoutMillis()));
|
||||
}
|
||||
|
||||
@Sharable
|
||||
|
@ -118,6 +129,13 @@ public class Http1ChannelInitializer extends ChannelInitializer<Channel>
|
|||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (msg instanceof WebSocketFrame) {
|
||||
WebSocketFrame webSocketFrame = (WebSocketFrame) msg;
|
||||
if (serverConfig.getWebSocketFrameHandler() != null) {
|
||||
serverConfig.getWebSocketFrameHandler().channelRead(ctx, webSocketFrame);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (msg instanceof HttpPipelinedRequest) {
|
||||
HttpPipelinedRequest httpPipelinedRequest = (HttpPipelinedRequest) msg;
|
||||
if (httpPipelinedRequest.getRequest() instanceof FullHttpRequest) {
|
||||
|
|
|
@ -34,8 +34,8 @@ import io.netty.util.AsciiString;
|
|||
import io.netty.util.Mapping;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
import org.xbib.netty.http.server.Server;
|
||||
import org.xbib.netty.http.server.DefaultServerConfig;
|
||||
import org.xbib.netty.http.common.HttpChannelInitializer;
|
||||
import org.xbib.netty.http.server.api.ServerConfig;
|
||||
import org.xbib.netty.http.server.handler.ExtendedSNIHandler;
|
||||
import org.xbib.netty.http.server.handler.IdleTimeoutHandler;
|
||||
import org.xbib.netty.http.server.handler.TrafficLoggingHandler;
|
||||
|
@ -52,7 +52,7 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel>
|
|||
|
||||
private final Server server;
|
||||
|
||||
private final DefaultServerConfig serverConfig;
|
||||
private final ServerConfig serverConfig;
|
||||
|
||||
private final HttpAddress httpAddress;
|
||||
|
||||
|
@ -80,10 +80,12 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel>
|
|||
configureCleartext(channel);
|
||||
}
|
||||
if (serverConfig.isDebug()) {
|
||||
logger.log(Level.FINE, "HTTP/2 server channel initialized: " +
|
||||
if (logger.isLoggable(Level.FINEST)) {
|
||||
logger.log(Level.FINEST, "HTTP/2 server channel initialized: " +
|
||||
" address=" + httpAddress + " pipeline=" + channel.pipeline().names());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void configureEncrypted(Channel channel) {
|
||||
channel.pipeline().addLast("sni-handler",
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package org.xbib.netty.http.server.protocol.ws2;
|
||||
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
import io.netty.util.concurrent.Future;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketProtocol;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Accepts valid websocket-over-http2 request, optionally modifies request and response headers.
|
||||
*/
|
||||
public interface Http2WebSocketAcceptor {
|
||||
|
||||
/**
|
||||
* @param ctx ChannelHandlerContext of connection channel. Intended for creating acceptor result
|
||||
* with context.executor().newFailedFuture(Throwable),
|
||||
* context.executor().newSucceededFuture(ChannelHandler)
|
||||
* @param path websocket path
|
||||
* @param subprotocols requested websocket subprotocols. Accepted subprotocol must be set on
|
||||
* response headers, e.g. with {@link Subprotocol#accept(String, Http2Headers)}
|
||||
* @param request request headers
|
||||
* @param response response headers
|
||||
* @return Succeeded future for accepted request, failed for rejected request. It is an error to
|
||||
* return non completed future
|
||||
*/
|
||||
Future<ChannelHandler> accept(ChannelHandlerContext ctx, String path, List<String> subprotocols,
|
||||
Http2Headers request, Http2Headers response);
|
||||
|
||||
final class Subprotocol {
|
||||
private Subprotocol() {}
|
||||
|
||||
public static void accept(String subprotocol, Http2Headers response) {
|
||||
Objects.requireNonNull(subprotocol, "subprotocol");
|
||||
Objects.requireNonNull(response, "response");
|
||||
if (subprotocol.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
response.set(Http2WebSocketProtocol.HEADER_WEBSOCKET_SUBPROTOCOL_NAME, subprotocol);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.xbib.netty.http.server.protocol.ws2;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.util.concurrent.GenericFutureListener;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketEvent;
|
||||
|
||||
/**
|
||||
* ChannelFuture listener that gracefully closes websocket by sending empty DATA frame with
|
||||
* END_STREAM flag set.
|
||||
*/
|
||||
public final class Http2WebSocketChannelFutureListener implements GenericFutureListener<ChannelFuture> {
|
||||
|
||||
public static final Http2WebSocketChannelFutureListener CLOSE = new Http2WebSocketChannelFutureListener();
|
||||
|
||||
private Http2WebSocketChannelFutureListener() {}
|
||||
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) {
|
||||
Channel channel = future.channel();
|
||||
Throwable cause = future.cause();
|
||||
if (cause != null) {
|
||||
Http2WebSocketEvent.fireFrameWriteError(channel, cause);
|
||||
}
|
||||
channel.pipeline().fireUserEventTriggered(Http2WebSocketEvent.Http2WebSocketLocalCloseEvent.INSTANCE);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package org.xbib.netty.http.server.protocol.ws2;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig;
|
||||
import io.netty.handler.codec.http2.Http2FrameCodec;
|
||||
import io.netty.handler.codec.http2.Http2FrameCodecBuilder;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
|
||||
public class Http2WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||
private final SslContext sslContext;
|
||||
|
||||
Http2WebSocketChannelInitializer(SslContext sslContext) {
|
||||
this.sslContext = sslContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) {
|
||||
SslHandler sslHandler = sslContext.newHandler(ch.alloc());
|
||||
Http2FrameCodec http2frameCodec = Http2WebSocketServerBuilder
|
||||
.configureHttp2Server(Http2FrameCodecBuilder.forServer())
|
||||
.build();
|
||||
ServerWebSocketHandler serverWebSocketHandler = new ServerWebSocketHandler();
|
||||
Http2WebSocketServerHandler http2webSocketHandler =
|
||||
Http2WebSocketServerBuilder.create()
|
||||
.decoderConfig(WebSocketDecoderConfig.newBuilder().allowExtensions(true).build())
|
||||
.compression(true)
|
||||
.acceptor(new PathAcceptor("/test", serverWebSocketHandler))
|
||||
.build();
|
||||
ch.pipeline().addLast(sslHandler, http2frameCodec, http2webSocketHandler);
|
||||
}
|
||||
|
||||
@Sharable
|
||||
private static class ServerWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame webSocketFrame) {
|
||||
// echo
|
||||
ctx.writeAndFlush(webSocketFrame.retain());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package org.xbib.netty.http.server.protocol.ws2;
|
||||
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http2.Http2Error;
|
||||
import io.netty.handler.codec.http2.Http2Exception;
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
import io.netty.util.AsciiString;
|
||||
import io.netty.util.concurrent.GenericFutureListener;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketEvent;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketHandler;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketProtocol;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketValidator;
|
||||
|
||||
/**
|
||||
* Provides server-side support for websocket-over-http2. Verifies websocket-over-http2 request
|
||||
* validity. Invalid websocket requests are rejected by sending RST frame, valid websocket http2
|
||||
* stream frames are passed down the pipeline. Valid websocket stream request headers are modified
|
||||
* as follows: :method=POST, x-protocol=websocket. Intended for proxies/intermidiaries that do not
|
||||
* process websocket byte streams, but only route respective http2 streams - hence is not compatible
|
||||
* with http1 websocket handlers. http1 websocket handlers support is provided by complementary
|
||||
* {@link Http2WebSocketServerHandler}
|
||||
*/
|
||||
public final class Http2WebSocketHandshakeOnlyServerHandler extends Http2WebSocketHandler
|
||||
implements GenericFutureListener<ChannelFuture> {
|
||||
|
||||
public Http2WebSocketHandshakeOnlyServerHandler() {}
|
||||
|
||||
@Override
|
||||
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
|
||||
boolean endOfStream) throws Http2Exception {
|
||||
if (handshake(headers, endOfStream)) {
|
||||
super.onHeadersRead(ctx, streamId, headers, padding, endOfStream);
|
||||
} else {
|
||||
reject(ctx, streamId, headers, endOfStream);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
|
||||
short weight, boolean exclusive, int padding, boolean endOfStream)
|
||||
throws Http2Exception {
|
||||
if (handshake(headers, endOfStream)) {
|
||||
super.onHeadersRead(
|
||||
ctx, streamId, headers, streamDependency, weight, exclusive, padding, endOfStream);
|
||||
} else {
|
||||
reject(ctx, streamId, headers, endOfStream);
|
||||
}
|
||||
}
|
||||
|
||||
/*RST_STREAM frame write*/
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) {
|
||||
Throwable cause = future.cause();
|
||||
if (cause != null) {
|
||||
Http2WebSocketEvent.fireFrameWriteError(future.channel(), cause);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handshake(Http2Headers headers, boolean endOfStream) {
|
||||
if (Http2WebSocketProtocol.isExtendedConnect(headers)) {
|
||||
boolean isValid = Http2WebSocketValidator.WebSocket.isValid(headers, endOfStream);
|
||||
if (isValid) {
|
||||
Http2WebSocketServerHandshaker.handshakeOnlyWebSocket(headers);
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
return Http2WebSocketValidator.Http.isValid(headers, endOfStream);
|
||||
}
|
||||
|
||||
private void reject(ChannelHandlerContext ctx, int streamId, Http2Headers headers, boolean endOfStream) {
|
||||
Http2WebSocketEvent.fireHandshakeValidationStartAndError(ctx.channel(), streamId,
|
||||
headers.set( AsciiString.of("x-websocket-endofstream"), AsciiString.of(endOfStream ? "true" : "false")));
|
||||
http2Handler.encoder()
|
||||
.writeRstStream(ctx, streamId, Http2Error.PROTOCOL_ERROR.code(), ctx.newPromise())
|
||||
.addListener(this);
|
||||
ctx.flush();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.xbib.netty.http.server.protocol.ws2;
|
||||
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
|
||||
|
||||
public final class Http2WebSocketPathNotFoundException extends WebSocketHandshakeException {
|
||||
|
||||
public Http2WebSocketPathNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
package org.xbib.netty.http.server.protocol.ws2;
|
||||
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker;
|
||||
import io.netty.handler.codec.http2.Http2ConnectionHandlerBuilder;
|
||||
import io.netty.handler.codec.http2.Http2FrameCodecBuilder;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketMessages;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketProtocol;
|
||||
import org.xbib.netty.http.common.ws.Preconditions;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Builder for {@link Http2WebSocketServerHandler}
|
||||
*/
|
||||
public final class Http2WebSocketServerBuilder {
|
||||
|
||||
private static final boolean MASK_PAYLOAD = false;
|
||||
|
||||
private static final Http2WebSocketAcceptor REJECT_REQUESTS_ACCEPTOR =
|
||||
(context, path, subprotocols, request, response) -> context.executor()
|
||||
.newFailedFuture(new Http2WebSocketPathNotFoundException(Http2WebSocketMessages.HANDSHAKE_PATH_NOT_FOUND + path +
|
||||
Http2WebSocketMessages.HANDSHAKE_PATH_NOT_FOUND_SUBPROTOCOLS +
|
||||
subprotocols));
|
||||
|
||||
private WebSocketDecoderConfig webSocketDecoderConfig;
|
||||
|
||||
private PerMessageDeflateServerExtensionHandshaker perMessageDeflateServerExtensionHandshaker;
|
||||
|
||||
private long closedWebSocketRemoveTimeoutMillis = 30_000;
|
||||
|
||||
private boolean isSingleWebSocketPerConnection;
|
||||
|
||||
private Http2WebSocketAcceptor acceptor = REJECT_REQUESTS_ACCEPTOR;
|
||||
|
||||
Http2WebSocketServerBuilder() {}
|
||||
|
||||
/**
|
||||
* Builds handshake-only {@link Http2WebSocketHandshakeOnlyServerHandler}.
|
||||
*
|
||||
* @return new {@link Http2WebSocketHandshakeOnlyServerHandler} instance
|
||||
*/
|
||||
public static Http2WebSocketHandshakeOnlyServerHandler buildHandshakeOnly() {
|
||||
return new Http2WebSocketHandshakeOnlyServerHandler();
|
||||
}
|
||||
|
||||
/** @return new {@link Http2WebSocketServerBuilder} instance */
|
||||
public static Http2WebSocketServerBuilder create() {
|
||||
return new Http2WebSocketServerBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method for configuring Http2FrameCodecBuilder with websocket-over-http2 support
|
||||
*
|
||||
* @param http2Builder {@link Http2FrameCodecBuilder} instance
|
||||
* @return same {@link Http2FrameCodecBuilder} instance
|
||||
*/
|
||||
public static Http2FrameCodecBuilder configureHttp2Server(Http2FrameCodecBuilder http2Builder) {
|
||||
Objects.requireNonNull(http2Builder, "http2Builder")
|
||||
.initialSettings()
|
||||
.put(Http2WebSocketProtocol.SETTINGS_ENABLE_CONNECT_PROTOCOL, (Long) 1L);
|
||||
return http2Builder.validateHeaders(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method for configuring Http2ConnectionHandlerBuilder with websocket-over-http2 support
|
||||
*
|
||||
* @param http2Builder {@link Http2ConnectionHandlerBuilder} instance
|
||||
* @return same {@link Http2ConnectionHandlerBuilder} instance
|
||||
*/
|
||||
public static Http2ConnectionHandlerBuilder configureHttp2Server(Http2ConnectionHandlerBuilder http2Builder) {
|
||||
Objects.requireNonNull(http2Builder, "http2Builder")
|
||||
.initialSettings()
|
||||
.put(Http2WebSocketProtocol.SETTINGS_ENABLE_CONNECT_PROTOCOL, (Long) 1L);
|
||||
return http2Builder.validateHeaders(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param webSocketDecoderConfig websocket decoder configuration. Must be non-null
|
||||
* @return this {@link Http2WebSocketServerBuilder} instance
|
||||
*/
|
||||
public Http2WebSocketServerBuilder decoderConfig(WebSocketDecoderConfig webSocketDecoderConfig) {
|
||||
this.webSocketDecoderConfig =
|
||||
Preconditions.requireNonNull(webSocketDecoderConfig, "webSocketDecoderConfig");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param closedWebSocketRemoveTimeoutMillis delay until websockets handler forgets closed
|
||||
* websocket. Necessary to gracefully handle incoming http2 frames racing with outgoing stream
|
||||
* termination frame.
|
||||
* @return this {@link Http2WebSocketServerBuilder} instance
|
||||
*/
|
||||
public Http2WebSocketServerBuilder closedWebSocketRemoveTimeout(long closedWebSocketRemoveTimeoutMillis) {
|
||||
this.closedWebSocketRemoveTimeoutMillis =
|
||||
Preconditions.requirePositive(closedWebSocketRemoveTimeoutMillis, "closedWebSocketRemoveTimeoutMillis");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isCompressionEnabled enables permessage-deflate compression with default configuration
|
||||
* @return this {@link Http2WebSocketServerBuilder} instance
|
||||
*/
|
||||
public Http2WebSocketServerBuilder compression(boolean isCompressionEnabled) {
|
||||
if (isCompressionEnabled) {
|
||||
if (perMessageDeflateServerExtensionHandshaker == null) {
|
||||
perMessageDeflateServerExtensionHandshaker =
|
||||
new PerMessageDeflateServerExtensionHandshaker();
|
||||
}
|
||||
} else {
|
||||
perMessageDeflateServerExtensionHandshaker = null;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables permessage-deflate compression with extended configuration. Parameters are described in
|
||||
* netty's PerMessageDeflateServerExtensionHandshaker
|
||||
*
|
||||
* @param compressionLevel sets compression level. Range is [0; 9], default is 6
|
||||
* @param allowServerWindowSize allows client to customize the server's inflater window size,
|
||||
* default is false
|
||||
* @param preferredClientWindowSize preferred client window size if client inflater is
|
||||
* customizable
|
||||
* @param allowServerNoContext allows client to activate server_no_context_takeover, default is
|
||||
* false
|
||||
* @param preferredClientNoContext whether server prefers to activate client_no_context_takeover
|
||||
* if client is compatible, default is false
|
||||
* @return this {@link Http2WebSocketServerBuilder} instance
|
||||
*/
|
||||
public Http2WebSocketServerBuilder compression(int compressionLevel,
|
||||
boolean allowServerWindowSize,
|
||||
int preferredClientWindowSize,
|
||||
boolean allowServerNoContext,
|
||||
boolean preferredClientNoContext) {
|
||||
perMessageDeflateServerExtensionHandshaker =
|
||||
new PerMessageDeflateServerExtensionHandshaker(compressionLevel, allowServerWindowSize,
|
||||
preferredClientWindowSize, allowServerNoContext, preferredClientNoContext);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets http1 websocket request acceptor
|
||||
*
|
||||
* @param acceptor websocket request acceptor. Must be non-null.
|
||||
* @return this {@link Http2WebSocketServerBuilder} instance
|
||||
*/
|
||||
public Http2WebSocketServerBuilder acceptor(Http2WebSocketAcceptor acceptor) {
|
||||
this.acceptor = Objects.requireNonNull(acceptor, "acceptor");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isSingleWebSocketPerConnection optimize for at most 1 websocket per connection
|
||||
* @return this {@link Http2WebSocketServerBuilder} instance
|
||||
*/
|
||||
public Http2WebSocketServerBuilder assumeSingleWebSocketPerConnection(boolean isSingleWebSocketPerConnection) {
|
||||
this.isSingleWebSocketPerConnection = isSingleWebSocketPerConnection;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds subchannel based {@link Http2WebSocketServerHandler} compatible with http1 websocket
|
||||
* handlers.
|
||||
*
|
||||
* @return new {@link Http2WebSocketServerHandler} instance
|
||||
*/
|
||||
public Http2WebSocketServerHandler build() {
|
||||
boolean hasCompression = perMessageDeflateServerExtensionHandshaker != null;
|
||||
WebSocketDecoderConfig config = webSocketDecoderConfig;
|
||||
if (config == null) {
|
||||
config = WebSocketDecoderConfig.newBuilder()
|
||||
.expectMaskedFrames(true)
|
||||
.allowMaskMismatch(false)
|
||||
.allowExtensions(hasCompression)
|
||||
.build();
|
||||
} else {
|
||||
boolean isAllowExtensions = config.allowExtensions();
|
||||
if (!isAllowExtensions && hasCompression) {
|
||||
config = config.toBuilder().allowExtensions(true).build();
|
||||
}
|
||||
}
|
||||
return new Http2WebSocketServerHandler(config,
|
||||
MASK_PAYLOAD,
|
||||
closedWebSocketRemoveTimeoutMillis,
|
||||
perMessageDeflateServerExtensionHandshaker,
|
||||
acceptor,
|
||||
isSingleWebSocketPerConnection);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package org.xbib.netty.http.server.protocol.ws2;
|
||||
|
||||
import io.netty.channel.*;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker;
|
||||
import io.netty.handler.codec.http2.Http2Exception;
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketChannelHandler;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketProtocol;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketValidator;
|
||||
|
||||
/**
|
||||
* Provides server-side support for websocket-over-http2. Creates sub channel for http2 stream of
|
||||
* successfully handshaked websocket. Subchannel is compatible with http1 websocket handlers.
|
||||
*/
|
||||
public final class Http2WebSocketServerHandler extends Http2WebSocketChannelHandler {
|
||||
|
||||
private final PerMessageDeflateServerExtensionHandshaker compressionHandshaker;
|
||||
|
||||
private final Http2WebSocketAcceptor http2WebSocketAcceptor;
|
||||
|
||||
private Http2WebSocketServerHandshaker handshaker;
|
||||
|
||||
Http2WebSocketServerHandler(WebSocketDecoderConfig webSocketDecoderConfig, boolean isEncoderMaskPayload,
|
||||
long closedWebSocketRemoveTimeoutMillis,
|
||||
PerMessageDeflateServerExtensionHandshaker compressionHandshaker,
|
||||
Http2WebSocketAcceptor http2WebSocketAcceptor,
|
||||
boolean isSingleWebSocketPerConnection) {
|
||||
super(webSocketDecoderConfig, isEncoderMaskPayload, closedWebSocketRemoveTimeoutMillis, isSingleWebSocketPerConnection);
|
||||
this.compressionHandshaker = compressionHandshaker;
|
||||
this.http2WebSocketAcceptor = http2WebSocketAcceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
|
||||
super.handlerAdded(ctx);
|
||||
this.handshaker = new Http2WebSocketServerHandshaker(webSocketsParent,
|
||||
config, isEncoderMaskPayload, http2WebSocketAcceptor, compressionHandshaker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeadersRead(ChannelHandlerContext ctx, final int streamId, Http2Headers headers,
|
||||
int padding, boolean endOfStream) throws Http2Exception {
|
||||
boolean proceed = handshakeWebSocket(streamId, headers, endOfStream);
|
||||
if (proceed) {
|
||||
next().onHeadersRead(ctx, streamId, headers, padding, endOfStream);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
|
||||
short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception {
|
||||
boolean proceed = handshakeWebSocket(streamId, headers, endOfStream);
|
||||
if (proceed) {
|
||||
next().onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endOfStream);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handshakeWebSocket(int streamId, Http2Headers headers, boolean endOfStream) {
|
||||
if (Http2WebSocketProtocol.isExtendedConnect(headers)) {
|
||||
if (!Http2WebSocketValidator.WebSocket.isValid(headers, endOfStream)) {
|
||||
handshaker.reject(streamId, headers, endOfStream);
|
||||
} else {
|
||||
handshaker.handshake(streamId, headers, endOfStream);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!Http2WebSocketValidator.Http.isValid(headers, endOfStream)) {
|
||||
handshaker.reject(streamId, headers, endOfStream);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,345 @@
|
|||
package org.xbib.netty.http.server.protocol.ws2;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtension;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandshaker;
|
||||
import io.netty.handler.codec.http2.DefaultHttp2Headers;
|
||||
import io.netty.handler.codec.http2.Http2Error;
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
import io.netty.handler.codec.http2.ReadOnlyHttp2Headers;
|
||||
import io.netty.util.AsciiString;
|
||||
import io.netty.util.concurrent.Future;
|
||||
import io.netty.util.concurrent.GenericFutureListener;
|
||||
import io.netty.util.concurrent.ScheduledFuture;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketChannel;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketChannelHandler;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketEvent;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketExtensions;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketMessages;
|
||||
import org.xbib.netty.http.common.ws.Http2WebSocketProtocol;
|
||||
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
final class Http2WebSocketServerHandshaker implements GenericFutureListener<ChannelFuture> {
|
||||
|
||||
private static final AsciiString HEADERS_STATUS_200 = AsciiString.of("200");
|
||||
|
||||
private static final ReadOnlyHttp2Headers HEADERS_OK =
|
||||
ReadOnlyHttp2Headers.serverHeaders(false, HEADERS_STATUS_200);
|
||||
|
||||
private static final ReadOnlyHttp2Headers HEADERS_UNSUPPORTED_VERSION =
|
||||
ReadOnlyHttp2Headers.serverHeaders(false, AsciiString.of("400"),
|
||||
AsciiString.of(Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_NAME),
|
||||
AsciiString.of(Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_VALUE));
|
||||
|
||||
private static final ReadOnlyHttp2Headers HEADERS_REJECTED =
|
||||
ReadOnlyHttp2Headers.serverHeaders(false, AsciiString.of("400"));
|
||||
|
||||
private static final ReadOnlyHttp2Headers HEADERS_NOT_FOUND =
|
||||
ReadOnlyHttp2Headers.serverHeaders(false, AsciiString.of("404"));
|
||||
|
||||
private static final ReadOnlyHttp2Headers HEADERS_INTERNAL_ERROR =
|
||||
ReadOnlyHttp2Headers.serverHeaders(false, AsciiString.of("500"));
|
||||
|
||||
private final Http2WebSocketChannelHandler.WebSocketsParent webSocketsParent;
|
||||
|
||||
private final WebSocketDecoderConfig webSocketDecoderConfig;
|
||||
|
||||
private final boolean isEncoderMaskPayload;
|
||||
|
||||
private final Http2WebSocketAcceptor http2WebSocketAcceptor;
|
||||
|
||||
private final WebSocketServerExtensionHandshaker compressionHandshaker;
|
||||
|
||||
Http2WebSocketServerHandshaker(Http2WebSocketChannelHandler.WebSocketsParent webSocketsParent,
|
||||
WebSocketDecoderConfig webSocketDecoderConfig,
|
||||
boolean isEncoderMaskPayload,
|
||||
Http2WebSocketAcceptor http2WebSocketAcceptor,
|
||||
WebSocketServerExtensionHandshaker compressionHandshaker) {
|
||||
this.webSocketsParent = webSocketsParent;
|
||||
this.webSocketDecoderConfig = webSocketDecoderConfig;
|
||||
this.isEncoderMaskPayload = isEncoderMaskPayload;
|
||||
this.http2WebSocketAcceptor = http2WebSocketAcceptor;
|
||||
this.compressionHandshaker = compressionHandshaker;
|
||||
}
|
||||
|
||||
void reject(final int streamId, final Http2Headers requestHeaders, boolean endOfStream) {
|
||||
Http2WebSocketEvent.fireHandshakeValidationStartAndError(
|
||||
webSocketsParent.context().channel(),
|
||||
streamId,
|
||||
requestHeaders.set(AsciiString.of("x-websocket-endofstream"), AsciiString.of(endOfStream ? "true" : "false")));
|
||||
writeRstStream(streamId).addListener(this);
|
||||
}
|
||||
|
||||
void handshake(final int streamId, final Http2Headers requestHeaders, boolean endOfStream) {
|
||||
long startNanos = System.nanoTime();
|
||||
ChannelHandlerContext ctx = webSocketsParent.context();
|
||||
String path = requestHeaders.path().toString();
|
||||
CharSequence webSocketVersion = requestHeaders.get(Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_NAME);
|
||||
CharSequence subprotocolsSeq = requestHeaders.get(Http2WebSocketProtocol.HEADER_WEBSOCKET_SUBPROTOCOL_NAME);
|
||||
String subprotocols = nonNullString(subprotocolsSeq);
|
||||
if (isUnsupportedWebSocketVersion(webSocketVersion)) {
|
||||
Http2WebSocketEvent.fireHandshakeStartAndError(ctx.channel(), streamId, path, subprotocols,
|
||||
requestHeaders, startNanos, System.nanoTime(), WebSocketHandshakeException.class.getName(),
|
||||
Http2WebSocketMessages.HANDSHAKE_UNSUPPORTED_VERSION + webSocketVersion);
|
||||
writeHeaders(ctx, streamId, HEADERS_UNSUPPORTED_VERSION, true).addListener(this);
|
||||
return;
|
||||
}
|
||||
List<String> requestedSubprotocols = parseSubprotocols(subprotocols);
|
||||
WebSocketServerExtension compressionExtension = null;
|
||||
WebSocketServerExtensionHandshaker compressionHandshaker = this.compressionHandshaker;
|
||||
if (compressionHandshaker != null) {
|
||||
CharSequence extensionsHeader = requestHeaders.get(Http2WebSocketProtocol.HEADER_WEBSOCKET_EXTENSIONS_NAME);
|
||||
WebSocketExtensionData compression = Http2WebSocketExtensions.decode(extensionsHeader);
|
||||
if (compression != null) {
|
||||
compressionExtension = compressionHandshaker.handshakeExtension(compression);
|
||||
}
|
||||
}
|
||||
boolean hasCompression = compressionExtension != null;
|
||||
WebSocketExtensionEncoder compressionEncoder = null;
|
||||
WebSocketExtensionDecoder compressionDecoder = null;
|
||||
Http2Headers responseHeaders = new DefaultHttp2Headers();
|
||||
if (hasCompression) {
|
||||
responseHeaders.set(Http2WebSocketProtocol.HEADER_WEBSOCKET_EXTENSIONS_NAME,
|
||||
Http2WebSocketExtensions.encode(compressionExtension.newReponseData()));
|
||||
compressionEncoder = compressionExtension.newExtensionEncoder();
|
||||
compressionDecoder = compressionExtension.newExtensionDecoder();
|
||||
}
|
||||
Future<ChannelHandler> acceptorResult;
|
||||
try {
|
||||
acceptorResult = http2WebSocketAcceptor.accept(ctx, path, requestedSubprotocols, requestHeaders, responseHeaders);
|
||||
} catch (Exception e) {
|
||||
acceptorResult = ctx.executor().newFailedFuture(e);
|
||||
}
|
||||
if (!acceptorResult.isDone()) {
|
||||
acceptorResult.cancel(true);
|
||||
Http2WebSocketEvent.fireHandshakeStartAndError(ctx.channel(), streamId, path, subprotocols,
|
||||
requestHeaders, startNanos, System.nanoTime(), WebSocketHandshakeException.class.getName(),
|
||||
Http2WebSocketMessages.HANDSHAKE_UNSUPPORTED_ACCEPTOR_TYPE);
|
||||
writeHeaders(ctx, streamId, HEADERS_INTERNAL_ERROR, true).addListener(this);
|
||||
return;
|
||||
}
|
||||
Throwable rejected = acceptorResult.cause();
|
||||
if (rejected != null) {
|
||||
Http2WebSocketEvent.fireHandshakeStartAndError(ctx.channel(), streamId, path, subprotocols,
|
||||
requestHeaders, startNanos, System.nanoTime(), rejected);
|
||||
Http2Headers response = rejected instanceof Http2WebSocketPathNotFoundException ?
|
||||
HEADERS_NOT_FOUND : HEADERS_REJECTED;
|
||||
writeHeaders(ctx, streamId, response, true).addListener(this);
|
||||
return;
|
||||
}
|
||||
CharSequence acceptedSubprotocolSeq = responseHeaders.get(Http2WebSocketProtocol.HEADER_WEBSOCKET_SUBPROTOCOL_NAME);
|
||||
String acceptedSubprotocol = nonNullString(acceptedSubprotocolSeq);
|
||||
if (!isExpectedSubprotocol(acceptedSubprotocol, requestedSubprotocols)) {
|
||||
String subprotocolOrBlank = acceptedSubprotocol.isEmpty() ? "''" : acceptedSubprotocol;
|
||||
Http2WebSocketEvent.fireHandshakeStartAndError(ctx.channel(), streamId, path, subprotocols,
|
||||
requestHeaders, startNanos, System.nanoTime(), WebSocketHandshakeException.class.getName(),
|
||||
Http2WebSocketMessages.HANDSHAKE_UNEXPECTED_SUBPROTOCOL + subprotocolOrBlank);
|
||||
writeHeaders(ctx, streamId, HEADERS_NOT_FOUND, true).addListener(this);
|
||||
return;
|
||||
}
|
||||
ChannelHandler webSocketHandler = acceptorResult.getNow();
|
||||
WebSocketExtensionEncoder finalCompressionEncoder = compressionEncoder;
|
||||
WebSocketExtensionDecoder finalCompressionDecoder = compressionDecoder;
|
||||
Http2Headers successHeaders = successHeaders(responseHeaders);
|
||||
writeHeaders(ctx, streamId, successHeaders, false).addListener(future -> {
|
||||
Throwable cause = future.cause();
|
||||
if (cause != null) {
|
||||
Channel ch = ctx.channel();
|
||||
Http2WebSocketEvent.fireFrameWriteError(ch, future.cause());
|
||||
Http2WebSocketEvent.fireHandshakeStartAndError(ch, streamId, path, subprotocols,
|
||||
requestHeaders, startNanos, System.nanoTime(), cause);
|
||||
return;
|
||||
}
|
||||
Http2WebSocketChannel webSocket = new Http2WebSocketChannel(webSocketsParent, streamId, path, acceptedSubprotocol,
|
||||
webSocketDecoderConfig, isEncoderMaskPayload, finalCompressionEncoder,
|
||||
finalCompressionDecoder, webSocketHandler).setStreamId(streamId);
|
||||
ChannelFuture registered = ctx.channel().eventLoop().register(webSocket);
|
||||
if (!registered.isSuccess()) {
|
||||
Http2WebSocketEvent.fireHandshakeStartAndError(ctx.channel(), streamId, path, subprotocols,
|
||||
requestHeaders, startNanos, System.nanoTime(), registered.cause());
|
||||
writeRstStream(streamId).addListener(this);
|
||||
webSocket.streamClosed();
|
||||
return;
|
||||
}
|
||||
if (!webSocket.isOpen()) {
|
||||
Http2WebSocketEvent.fireHandshakeStartAndError(ctx.channel(), streamId, path, subprotocols,
|
||||
requestHeaders, startNanos, System.nanoTime(), ClosedChannelException.class.getName(),
|
||||
"websocket channel closed immediately after eventloop registration");
|
||||
return;
|
||||
}
|
||||
webSocketsParent.register(streamId, webSocket);
|
||||
Http2WebSocketEvent.fireHandshakeStartAndSuccess(webSocket, streamId, path, subprotocols,
|
||||
requestHeaders, successHeaders, startNanos, System.nanoTime());
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isExpectedSubprotocol(String subprotocol, List<String> requestedSubprotocols) {
|
||||
int requestedLength = requestedSubprotocols.size();
|
||||
if (subprotocol.isEmpty()) {
|
||||
return requestedLength == 0;
|
||||
}
|
||||
for (int i = 0; i < requestedLength; i++) {
|
||||
if (requestedSubprotocols.get(i).equals(subprotocol)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) {
|
||||
Throwable cause = future.cause();
|
||||
if (cause != null) {
|
||||
Http2WebSocketEvent.fireFrameWriteError(future.channel(), cause);
|
||||
}
|
||||
}
|
||||
|
||||
private ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId, Http2Headers headers, boolean endStream) {
|
||||
ChannelFuture channelFuture = webSocketsParent.writeHeaders(streamId, headers, endStream);
|
||||
ctx.flush();
|
||||
return channelFuture;
|
||||
}
|
||||
|
||||
private ChannelFuture writeRstStream(int streamId) {
|
||||
return webSocketsParent.writeRstStream(streamId, Http2Error.PROTOCOL_ERROR.code());
|
||||
}
|
||||
|
||||
static Http2Headers handshakeOnlyWebSocket(Http2Headers headers) {
|
||||
headers.remove(Http2WebSocketProtocol.HEADER_PROTOCOL_NAME);
|
||||
headers.method(Http2WebSocketProtocol.HEADER_METHOD_CONNECT_HANDSHAKED);
|
||||
return headers.set(
|
||||
Http2WebSocketProtocol.HEADER_PROTOCOL_NAME_HANDSHAKED,
|
||||
Http2WebSocketProtocol.HEADER_PROTOCOL_VALUE);
|
||||
}
|
||||
|
||||
static List<String> parseSubprotocols(String subprotocols) {
|
||||
if (subprotocols.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (subprotocols.indexOf(',') == -1) {
|
||||
return Collections.singletonList(subprotocols);
|
||||
}
|
||||
return Arrays.asList(subprotocols.split(","));
|
||||
}
|
||||
|
||||
private static String nonNullString(CharSequence seq) {
|
||||
if (seq == null) {
|
||||
return "";
|
||||
}
|
||||
return seq.toString();
|
||||
}
|
||||
|
||||
private static Http2Headers successHeaders(Http2Headers responseHeaders) {
|
||||
if (responseHeaders.isEmpty()) {
|
||||
return HEADERS_OK;
|
||||
}
|
||||
return responseHeaders.status(HEADERS_STATUS_200);
|
||||
}
|
||||
|
||||
private static boolean isUnsupportedWebSocketVersion(CharSequence webSocketVersion) {
|
||||
return webSocketVersion == null
|
||||
|| !Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_VALUE.contentEquals(webSocketVersion);
|
||||
}
|
||||
|
||||
static class Handshake {
|
||||
private final Future<Void> channelClose;
|
||||
private final ChannelPromise handshake;
|
||||
private final long timeoutMillis;
|
||||
private boolean done;
|
||||
private ScheduledFuture<?> timeoutFuture;
|
||||
private Future<?> handshakeCompleteFuture;
|
||||
private GenericFutureListener<ChannelFuture> channelCloseListener;
|
||||
|
||||
public Handshake(Future<Void> channelClose, ChannelPromise handshake, long timeoutMillis) {
|
||||
this.channelClose = channelClose;
|
||||
this.handshake = handshake;
|
||||
this.timeoutMillis = timeoutMillis;
|
||||
}
|
||||
|
||||
public void startTimeout() {
|
||||
ChannelPromise h = handshake;
|
||||
Channel channel = h.channel();
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
GenericFutureListener<ChannelFuture> l = channelCloseListener = future -> onConnectionClose();
|
||||
channelClose.addListener(l);
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
handshakeCompleteFuture = h.addListener(future -> onHandshakeComplete(future.cause()));
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
timeoutFuture = channel.eventLoop().schedule(this::onTimeout, timeoutMillis, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public void complete(Throwable e) {
|
||||
onHandshakeComplete(e);
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public ChannelFuture future() {
|
||||
return handshake;
|
||||
}
|
||||
|
||||
private void onConnectionClose() {
|
||||
if (!done) {
|
||||
handshake.tryFailure(new ClosedChannelException());
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
private void onHandshakeComplete(Throwable cause) {
|
||||
if (!done) {
|
||||
if (cause != null) {
|
||||
handshake.tryFailure(cause);
|
||||
} else {
|
||||
handshake.trySuccess();
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
private void onTimeout() {
|
||||
if (!done) {
|
||||
handshake.tryFailure(new TimeoutException());
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
private void done() {
|
||||
done = true;
|
||||
GenericFutureListener<ChannelFuture> closeListener = channelCloseListener;
|
||||
if (closeListener != null) {
|
||||
channelClose.removeListener(closeListener);
|
||||
}
|
||||
cancel(handshakeCompleteFuture);
|
||||
cancel(timeoutFuture);
|
||||
}
|
||||
|
||||
private void cancel(Future<?> future) {
|
||||
if (future != null) {
|
||||
future.cancel(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package org.xbib.netty.http.server.protocol.ws2;
|
||||
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
import io.netty.util.concurrent.Future;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PathAcceptor implements Http2WebSocketAcceptor {
|
||||
|
||||
private final String path;
|
||||
|
||||
private final ChannelHandler webSocketHandler;
|
||||
|
||||
public PathAcceptor(String path, ChannelHandler webSocketHandler) {
|
||||
this.path = path;
|
||||
this.webSocketHandler = webSocketHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ChannelHandler> accept(ChannelHandlerContext ctx, String path, List<String> subprotocols,
|
||||
Http2Headers request, Http2Headers response) {
|
||||
if (subprotocols.isEmpty() && path.equals(this.path)) {
|
||||
return ctx.executor().newSucceededFuture(webSocketHandler);
|
||||
}
|
||||
return ctx.executor().newFailedFuture(new WebSocketHandshakeException(String.format("Path not found: %s , subprotocols: %s", path, subprotocols)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package org.xbib.netty.http.server.protocol.ws2;
|
||||
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http2.Http2Headers;
|
||||
import io.netty.util.concurrent.Future;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PathSubprotocolAcceptor implements Http2WebSocketAcceptor {
|
||||
|
||||
private final ChannelHandler webSocketHandler;
|
||||
|
||||
private final String path;
|
||||
|
||||
private final String subprotocol;
|
||||
|
||||
private final boolean acceptSubprotocol;
|
||||
|
||||
public PathSubprotocolAcceptor(String path, String subprotocol, ChannelHandler webSocketHandler) {
|
||||
this(path, subprotocol, webSocketHandler, true);
|
||||
}
|
||||
|
||||
public PathSubprotocolAcceptor(String path, String subprotocol, ChannelHandler webSocketHandler, boolean acceptSubprotocol) {
|
||||
this.path = path;
|
||||
this.subprotocol = subprotocol;
|
||||
this.webSocketHandler = webSocketHandler;
|
||||
this.acceptSubprotocol = acceptSubprotocol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ChannelHandler> accept(ChannelHandlerContext ctx,
|
||||
String path, List<String> subprotocols, Http2Headers request, Http2Headers response) {
|
||||
String subprotocol = this.subprotocol;
|
||||
if (path.equals(this.path) && subprotocols.contains(subprotocol)) {
|
||||
if (acceptSubprotocol) {
|
||||
Subprotocol.accept(subprotocol, response);
|
||||
}
|
||||
return ctx.executor().newSucceededFuture(webSocketHandler);
|
||||
}
|
||||
return ctx.executor().newFailedFuture(new Http2WebSocketPathNotFoundException(
|
||||
String.format("Path not found: %s , subprotocols: %s", path, subprotocols)));
|
||||
}
|
||||
}
|
|
@ -2,39 +2,52 @@ package org.xbib.netty.http.server.test.http1;
|
|||
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.xbib.net.URL;
|
||||
import org.xbib.netty.http.client.Client;
|
||||
import org.xbib.netty.http.client.api.Request;
|
||||
import org.xbib.netty.http.client.api.ResponseListener;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
import org.xbib.netty.http.common.HttpResponse;
|
||||
import org.xbib.netty.http.server.HttpServerDomain;
|
||||
import org.xbib.netty.http.server.Server;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class BasicAuthTest {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(PostTest.class.getName());
|
||||
|
||||
@Disabled
|
||||
@Test
|
||||
void testBasicAuth() throws Exception {
|
||||
HttpAddress httpAddress = HttpAddress.http1("localhost", 8008);
|
||||
HttpServerDomain domain = HttpServerDomain.builder(httpAddress)
|
||||
.singleEndpoint("/**", (request, response) -> {
|
||||
String authorization = request.getHeader("Authorization");
|
||||
response.getBuilder().setStatus(HttpResponseStatus.OK.code())
|
||||
.setContentType("text/plain").build().write(authorization);
|
||||
})
|
||||
.build();
|
||||
Server server = Server.builder(domain)
|
||||
.build();
|
||||
Client client = Client.builder()
|
||||
.build();
|
||||
try {
|
||||
ResponseListener<HttpResponse> responseListener = (resp) -> {
|
||||
if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) {
|
||||
logger.log(Level.INFO, "got response " + resp.getBodyAsString(StandardCharsets.UTF_8));
|
||||
}
|
||||
};
|
||||
URL serverUrl = URL.from("");
|
||||
Request postRequest = Request.post().setVersion(HttpVersion.HTTP_1_1)
|
||||
.url(serverUrl)
|
||||
.addBasicAuthorization("", "")
|
||||
server.accept();
|
||||
ResponseListener<HttpResponse> responseListener = (resp) ->
|
||||
assertEquals("Basic aGVsbG86d29ybGQ=", resp.getBodyAsString(StandardCharsets.UTF_8));
|
||||
Request postRequest = Request.get()
|
||||
.setVersion(HttpVersion.HTTP_1_1)
|
||||
.url(server.getServerConfig().getAddress().base())
|
||||
.addBasicAuthorization("hello", "world")
|
||||
.setResponseListener(responseListener)
|
||||
.build();
|
||||
client.execute(postRequest).get();
|
||||
} finally {
|
||||
server.shutdownGracefully();
|
||||
client.shutdownGracefully();
|
||||
logger.log(Level.INFO, "server and client shut down");
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package org.xbib.netty.http.server.test.ws1;
|
||||
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.xbib.netty.http.client.Client;
|
||||
import org.xbib.netty.http.client.api.Request;
|
||||
import org.xbib.netty.http.client.api.ResponseListener;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
import org.xbib.netty.http.common.HttpResponse;
|
||||
import org.xbib.netty.http.server.HttpServerDomain;
|
||||
import org.xbib.netty.http.server.Server;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class EchoTest {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(EchoTest.class.getName());
|
||||
|
||||
@Test
|
||||
void testBasicAuth() throws Exception {
|
||||
HttpAddress httpAddress = HttpAddress.http1("localhost", 8008);
|
||||
HttpServerDomain domain = HttpServerDomain.builder(httpAddress)
|
||||
.singleEndpoint("/**", (request, response) -> {
|
||||
String authorization = request.getHeader("Authorization");
|
||||
response.getBuilder().setStatus(HttpResponseStatus.OK.code())
|
||||
.setContentType("text/plain").build().write(authorization);
|
||||
})
|
||||
.build();
|
||||
Server server = Server.builder(domain)
|
||||
.build();
|
||||
Client client = Client.builder()
|
||||
.build();
|
||||
try {
|
||||
server.accept();
|
||||
ResponseListener<HttpResponse> responseListener = (resp) ->
|
||||
assertEquals("Basic aGVsbG86d29ybGQ=", resp.getBodyAsString(StandardCharsets.UTF_8));
|
||||
Request postRequest = Request.get()
|
||||
.setVersion(HttpVersion.HTTP_1_1)
|
||||
.url(server.getServerConfig().getAddress().base())
|
||||
.addBasicAuthorization("hello", "world")
|
||||
.setResponseListener(responseListener)
|
||||
.build();
|
||||
client.execute(postRequest).get();
|
||||
} finally {
|
||||
server.shutdownGracefully();
|
||||
client.shutdownGracefully();
|
||||
logger.log(Level.INFO, "server and client shut down");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue