working on websocket

This commit is contained in:
Jörg Prante 2021-12-10 22:50:22 +01:00
parent 2f2050a6b3
commit 844b38f219
53 changed files with 5246 additions and 321 deletions

View file

@ -1,8 +1,10 @@
import org.xbib.netty.http.server.api.ServerCertificateProvider;
module org.xbib.netty.http.bouncycastle { module org.xbib.netty.http.bouncycastle {
exports org.xbib.netty.http.bouncycastle; exports org.xbib.netty.http.bouncycastle;
requires org.xbib.netty.http.server.api; requires org.xbib.netty.http.server.api;
requires org.bouncycastle.pkix; requires org.bouncycastle.pkix;
requires org.bouncycastle.provider; requires org.bouncycastle.provider;
provides org.xbib.netty.http.server.api.security.ServerCertificateProvider with provides ServerCertificateProvider with
org.xbib.netty.http.bouncycastle.BouncyCastleSelfSignedCertificateProvider; org.xbib.netty.http.bouncycastle.BouncyCastleSelfSignedCertificateProvider;
} }

View file

@ -1,7 +1,7 @@
package org.xbib.netty.http.bouncycastle; package org.xbib.netty.http.bouncycastle;
import org.bouncycastle.operator.OperatorCreationException; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;

View file

@ -12,6 +12,7 @@ import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.multipart.InterfaceHttpData; 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.handler.codec.http2.HttpConversionUtil;
import io.netty.util.AsciiString; import io.netty.util.AsciiString;
import org.xbib.net.URL; import org.xbib.net.URL;
@ -76,12 +77,28 @@ public final class Request implements AutoCloseable {
private TimeoutListener timeoutListener; private TimeoutListener timeoutListener;
private Request(URL url, HttpVersion httpVersion, HttpMethod httpMethod, private final WebSocketFrame webSocketFrame;
HttpHeaders headers, Collection<Cookie> cookies, ByteBuf content, List<InterfaceHttpData> bodyData,
long timeoutInMillis, boolean followRedirect, int maxRedirect, int redirectCount, private final WebSocketResponseListener<WebSocketFrame> webSocketResponseListener;
boolean isBackOff, BackOff backOff,
ResponseListener<HttpResponse> responseListener, ExceptionListener exceptionListener, private Request(URL url,
TimeoutListener timeoutListener) { 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.url = url;
this.httpVersion = httpVersion; this.httpVersion = httpVersion;
this.httpMethod = httpMethod; this.httpMethod = httpMethod;
@ -98,6 +115,8 @@ public final class Request implements AutoCloseable {
this.responseListener = responseListener; this.responseListener = responseListener;
this.exceptionListener = exceptionListener; this.exceptionListener = exceptionListener;
this.timeoutListener = timeoutListener; this.timeoutListener = timeoutListener;
this.webSocketFrame = webSocketFrame;
this.webSocketResponseListener = webSocketResponseListener;
} }
public URL url() { public URL url() {
@ -157,6 +176,14 @@ public final class Request implements AutoCloseable {
return backOff; return backOff;
} }
public WebSocketFrame getWebSocketFrame() {
return webSocketFrame;
}
public WebSocketResponseListener<WebSocketFrame> getWebSocketResponseListener() {
return webSocketResponseListener;
}
public boolean canRedirect() { public boolean canRedirect() {
if (!followRedirect) { if (!followRedirect) {
return false; return false;
@ -356,6 +383,10 @@ public final class Request implements AutoCloseable {
private TimeoutListener timeoutListener; private TimeoutListener timeoutListener;
private WebSocketFrame webSocketFrame;
private WebSocketResponseListener<WebSocketFrame> webSocketResponseListener;
Builder(ByteBufAllocator allocator) { Builder(ByteBufAllocator allocator) {
this.allocator = allocator; this.allocator = allocator;
this.httpMethod = DEFAULT_METHOD; this.httpMethod = DEFAULT_METHOD;
@ -622,6 +653,16 @@ public final class Request implements AutoCloseable {
return this; 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() { public Request build() {
DefaultHttpHeaders validatedHeaders = new DefaultHttpHeaders(true); DefaultHttpHeaders validatedHeaders = new DefaultHttpHeaders(true);
validatedHeaders.set(headers); validatedHeaders.set(headers);
@ -670,7 +711,7 @@ public final class Request implements AutoCloseable {
} }
return new Request(url, httpVersion, httpMethod, validatedHeaders, cookies, content, bodyData, return new Request(url, httpVersion, httpMethod, validatedHeaders, cookies, content, bodyData,
timeoutInMillis, followRedirect, maxRedirects, 0, enableBackOff, backOff, timeoutInMillis, followRedirect, maxRedirects, 0, enableBackOff, backOff,
responseListener, exceptionListener, timeoutListener); responseListener, exceptionListener, timeoutListener, webSocketFrame, webSocketResponseListener);
} }
private void addHeader(AsciiString name, Object value) { private void addHeader(AsciiString name, Object value) {

View file

@ -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);
}

View 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

View file

@ -245,7 +245,7 @@ public class ClientConfig {
private Boolean poolSecure = Defaults.POOL_SECURE; 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; private Http2Settings http2Settings = Defaults.HTTP2_SETTINGS;

View file

@ -4,9 +4,12 @@ import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline; 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.HttpClientCodec;
import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpObjectAggregator; 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.logging.LogLevel;
import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
@ -14,9 +17,12 @@ import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.handler.stream.ChunkedWriteHandler;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.ClientConfig; 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.common.HttpChannelInitializer;
import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer; import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import java.net.URI;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -96,17 +102,23 @@ public class Http1ChannelInitializer extends ChannelInitializer<Channel> impleme
private void configureCleartext(Channel channel) { private void configureCleartext(Channel channel) {
ChannelPipeline pipeline = channel.pipeline(); ChannelPipeline pipeline = channel.pipeline();
//pipeline.addLast("client-chunk-compressor", new HttpChunkContentCompressor(6)); pipeline.addLast("http-client-chunk-writer",
pipeline.addLast("http-client-chunk-writer", new ChunkedWriteHandler()); new ChunkedWriteHandler());
pipeline.addLast("http-client-codec", new HttpClientCodec(clientConfig.getMaxInitialLineLength(), pipeline.addLast("http-client-codec",
new HttpClientCodec(clientConfig.getMaxInitialLineLength(),
clientConfig.getMaxHeadersSize(), clientConfig.getMaxChunkSize())); clientConfig.getMaxHeadersSize(), clientConfig.getMaxChunkSize()));
if (clientConfig.isEnableGzip()) { if (clientConfig.isEnableGzip()) {
pipeline.addLast("http-client-decompressor", new HttpContentDecompressor()); pipeline.addLast("http-client-decompressor", new HttpContentDecompressor());
} }
HttpObjectAggregator httpObjectAggregator = new HttpObjectAggregator(clientConfig.getMaxContentLength(), HttpObjectAggregator httpObjectAggregator =
false); new HttpObjectAggregator(clientConfig.getMaxContentLength(), false);
httpObjectAggregator.setMaxCumulationBufferComponents(clientConfig.getMaxCompositeBufferComponents()); httpObjectAggregator.setMaxCumulationBufferComponents(clientConfig.getMaxCompositeBufferComponents());
pipeline.addLast("http-client-aggregator", httpObjectAggregator); pipeline.addLast("http-client-aggregator",
pipeline.addLast("http-client-handler", httpResponseHandler); 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);
} }
} }

View file

@ -11,10 +11,6 @@ import io.netty.handler.codec.http.HttpContentCompressor;
*/ */
public class HttpChunkContentCompressor extends HttpContentCompressor { public class HttpChunkContentCompressor extends HttpContentCompressor {
HttpChunkContentCompressor(int compressionLevel) {
super(compressionLevel);
}
@Override @Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof ByteBuf) { if (msg instanceof ByteBuf) {

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -23,7 +23,7 @@ class GoogleTest {
void testHttp1WithTlsV13() throws Exception { void testHttp1WithTlsV13() throws Exception {
AtomicBoolean success = new AtomicBoolean(); AtomicBoolean success = new AtomicBoolean();
Client client = Client.builder() Client client = Client.builder()
.setTransportLayerSecurityProtocols(new String[] { "TLSv1.3" }) .setTransportLayerSecurityProtocols("TLSv1.3")
.build(); .build();
try { try {
Request request = Request.get().url("https://www.google.com/") Request request = Request.get().url("https://www.google.com/")

View file

@ -24,7 +24,8 @@ class Http1Test {
Client client = Client.builder() Client client = Client.builder()
.build(); .build();
try { try {
Request request = Request.get().url("https://xbib.org") Request request = Request.get()
.url("https://xbib.org")
.setResponseListener(resp -> logger.log(Level.FINE, .setResponseListener(resp -> logger.log(Level.FINE,
"got response: " + resp.getHeaders() + "got response: " + resp.getHeaders() +
resp.getBodyAsString(StandardCharsets.UTF_8) + resp.getBodyAsString(StandardCharsets.UTF_8) +

View 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'
}
```

View file

@ -4,6 +4,7 @@ module org.xbib.netty.http.common {
exports org.xbib.netty.http.common.mime; exports org.xbib.netty.http.common.mime;
exports org.xbib.netty.http.common.security; exports org.xbib.netty.http.common.security;
exports org.xbib.netty.http.common.util; exports org.xbib.netty.http.common.util;
exports org.xbib.netty.http.common.ws;
requires org.xbib.net.url; requires org.xbib.net.url;
requires io.netty.buffer; requires io.netty.buffer;
requires io.netty.common; requires io.netty.common;

View file

@ -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();
}
}
}

View file

@ -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;
}
}
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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");
}
}
}

View file

@ -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";
}

View file

@ -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));
}
}

View file

@ -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);
}
}
}

View file

@ -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));
}
}

View file

@ -1,7 +1,5 @@
module org.xbib.netty.http.server.api { module org.xbib.netty.http.server.api {
exports 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.netty.http.common;
requires org.xbib.net.url; requires org.xbib.net.url;
requires io.netty.buffer; requires io.netty.buffer;

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.server.api.security; package org.xbib.netty.http.server.api;
import java.io.InputStream; import java.io.InputStream;

View file

@ -1,11 +1,16 @@
package org.xbib.netty.http.server.api; package org.xbib.netty.http.server.api;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.WriteBufferWaterMark;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.CipherSuiteFilter; import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.SslProvider;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.security.SecurityUtil;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.Provider; import java.security.Provider;
import java.util.Collection; import java.util.Collection;
@ -61,8 +66,6 @@ public interface ServerConfig {
boolean isCompressionEnabled(); boolean isCompressionEnabled();
int getCompressionThreshold();
boolean isDecompressionEnabled(); boolean isDecompressionEnabled();
boolean isInstallHttp2Upgrade(); boolean isInstallHttp2Upgrade();
@ -92,4 +95,174 @@ public interface ServerConfig {
Domain<? extends EndpointResolver<?>> getDomain(String name); Domain<? extends EndpointResolver<?>> getDomain(String name);
Domain<? extends EndpointResolver<?>> getDefaultDomain(); 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;
}
} }

View file

@ -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();
}

View file

@ -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.http1.Http1;
import org.xbib.netty.http.server.protocol.http2.Http2; import org.xbib.netty.http.server.protocol.http2.Http2;
module org.xbib.netty.http.server { 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.server.api.ServerProtocolProvider;
uses org.xbib.netty.http.common.TransportProvider; uses org.xbib.netty.http.common.TransportProvider;
exports org.xbib.netty.http.server; exports org.xbib.netty.http.server;

View file

@ -34,8 +34,7 @@ public abstract class BaseTransport implements ServerTransport {
* @param reqHeaders the request headers * @param reqHeaders the request headers
* @return whether further processing should be performed * @return whether further processing should be performed
*/ */
protected static AcceptState acceptRequest(HttpVersion httpVersion, protected static AcceptState acceptRequest(HttpVersion httpVersion, HttpHeaders reqHeaders) {
HttpHeaders reqHeaders) {
if (httpVersion.majorVersion() == 1 || httpVersion.majorVersion() == 2) { if (httpVersion.majorVersion() == 1 || httpVersion.majorVersion() == 2) {
if (!reqHeaders.contains(HttpHeaderNames.HOST)) { if (!reqHeaders.contains(HttpHeaderNames.HOST)) {
// RFC2616#14.23: missing Host header gets 400 // RFC2616#14.23: missing Host header gets 400

View file

@ -1,10 +1,11 @@
package org.xbib.netty.http.server; package org.xbib.netty.http.server;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.WriteBufferWaterMark;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.CipherSuiteFilter; import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.SslProvider;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.security.SecurityUtil; import org.xbib.netty.http.common.security.SecurityUtil;
@ -21,180 +22,6 @@ import javax.net.ssl.TrustManagerFactory;
public class DefaultServerConfig implements ServerConfig { 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 HttpAddress httpAddress = Defaults.ADDRESS;
private boolean debug = Defaults.DEBUG; private boolean debug = Defaults.DEBUG;
@ -243,8 +70,6 @@ public class DefaultServerConfig implements ServerConfig {
private boolean enableCompression = Defaults.ENABLE_COMPRESSION; private boolean enableCompression = Defaults.ENABLE_COMPRESSION;
private int compressionThreshold = Defaults.COMPRESSION_THRESHOLD;
private boolean enableDecompression = Defaults.ENABLE_DECOMPRESSION; private boolean enableDecompression = Defaults.ENABLE_DECOMPRESSION;
private Http2Settings http2Settings = Defaults.HTTP_2_SETTINGS; private Http2Settings http2Settings = Defaults.HTTP_2_SETTINGS;
@ -271,6 +96,8 @@ public class DefaultServerConfig implements ServerConfig {
private boolean acceptInvalidCertificates = false; private boolean acceptInvalidCertificates = false;
private SimpleChannelInboundHandler<WebSocketFrame> webSocketFrameHandler;
public DefaultServerConfig() { public DefaultServerConfig() {
this.domains = new LinkedList<>(); this.domains = new LinkedList<>();
} }
@ -492,15 +319,6 @@ public class DefaultServerConfig implements ServerConfig {
return enableCompression; return enableCompression;
} }
public ServerConfig setCompressionThreshold(int compressionThreshold) {
this.compressionThreshold = compressionThreshold;
return this;
}
public int getCompressionThreshold() {
return compressionThreshold;
}
public ServerConfig setDecompression(boolean enabled) { public ServerConfig setDecompression(boolean enabled) {
this.enableDecompression = enabled; this.enableDecompression = enabled;
return this; return this;
@ -642,4 +460,14 @@ public class DefaultServerConfig implements ServerConfig {
domains.stream().filter(d -> d.getName().equals("*")).findFirst(); domains.stream().filter(d -> d.getName().equals("*")).findFirst();
return domainOptional.orElse(domains.getFirst()); return domainOptional.orElse(domains.getFirst());
} }
public ServerConfig setWebSocketFrameHandler(SimpleChannelInboundHandler<WebSocketFrame> webSocketFrameHandler) {
this.webSocketFrameHandler = webSocketFrameHandler;
return this;
}
@Override
public SimpleChannelInboundHandler<WebSocketFrame> getWebSocketFrameHandler() {
return webSocketFrameHandler;
}
} }

View file

@ -12,7 +12,7 @@ import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.HttpMethod; import org.xbib.netty.http.common.HttpMethod;
import org.xbib.netty.http.server.api.Domain; import org.xbib.netty.http.server.api.Domain;
import org.xbib.netty.http.server.api.EndpointResolver; 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.common.security.SecurityUtil;
import org.xbib.netty.http.server.api.ServerRequest; import org.xbib.netty.http.server.api.ServerRequest;
import org.xbib.netty.http.server.api.ServerResponse; import org.xbib.netty.http.server.api.ServerResponse;

View file

@ -5,12 +5,14 @@ import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption; import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup; import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.WriteBufferWaterMark;
import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpVersion; 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.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContext;
import io.netty.util.DomainWildcardMappingBuilder; 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.common.TransportProvider;
import org.xbib.netty.http.server.api.Domain; import org.xbib.netty.http.server.api.Domain;
import org.xbib.netty.http.server.api.EndpointResolver; 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.ServerProtocolProvider;
import org.xbib.netty.http.server.api.ServerRequest; import org.xbib.netty.http.server.api.ServerRequest;
import org.xbib.netty.http.server.api.ServerResponse; 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; private final EventLoopGroup parentEventLoopGroup;
@ -99,7 +102,7 @@ public final class Server implements AutoCloseable {
* @param executor an extra blocking thread pool executor or null * @param executor an extra blocking thread pool executor or null
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Server(DefaultServerConfig serverConfig, private Server(ServerConfig serverConfig,
ByteBufAllocator byteBufAllocator, ByteBufAllocator byteBufAllocator,
EventLoopGroup parentEventLoopGroup, EventLoopGroup parentEventLoopGroup,
EventLoopGroup childEventLoopGroup, EventLoopGroup childEventLoopGroup,
@ -177,7 +180,7 @@ public final class Server implements AutoCloseable {
return new Builder(httpServerDomain); return new Builder(httpServerDomain);
} }
public DefaultServerConfig getServerConfig() { public ServerConfig getServerConfig() {
return serverConfig; return serverConfig;
} }
@ -373,7 +376,7 @@ public final class Server implements AutoCloseable {
throw new IllegalStateException("no channel initializer found for major version " + majorVersion); 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 parentEventLoopGroup ) {
EventLoopGroup eventLoopGroup = parentEventLoopGroup; EventLoopGroup eventLoopGroup = parentEventLoopGroup;
if (eventLoopGroup == null) { if (eventLoopGroup == null) {
@ -391,7 +394,7 @@ public final class Server implements AutoCloseable {
return eventLoopGroup; return eventLoopGroup;
} }
private static EventLoopGroup createChildEventLoopGroup(DefaultServerConfig serverConfig, private static EventLoopGroup createChildEventLoopGroup(ServerConfig serverConfig,
EventLoopGroup childEventLoopGroup ) { EventLoopGroup childEventLoopGroup ) {
EventLoopGroup eventLoopGroup = childEventLoopGroup; EventLoopGroup eventLoopGroup = childEventLoopGroup;
if (eventLoopGroup == null) { if (eventLoopGroup == null) {
@ -409,7 +412,7 @@ public final class Server implements AutoCloseable {
return eventLoopGroup; 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> socketChannelClass) {
Class<? extends ServerSocketChannel> channelClass = socketChannelClass; Class<? extends ServerSocketChannel> channelClass = socketChannelClass;
if (channelClass == null) { if (channelClass == null) {
@ -684,6 +687,11 @@ public final class Server implements AutoCloseable {
return this; return this;
} }
public Builder setWebSocketFrameHandler(SimpleChannelInboundHandler<WebSocketFrame> webSocketFrameHandler) {
this.serverConfig.setWebSocketFrameHandler(webSocketFrameHandler);
return this;
}
public Server build() { public Server build() {
int maxThreads = serverConfig.getBlockingThreadCount(); int maxThreads = serverConfig.getBlockingThreadCount();
int maxQueue = serverConfig.getBlockingQueueCount(); int maxQueue = serverConfig.getBlockingQueueCount();
@ -732,9 +740,8 @@ public final class Server implements AutoCloseable {
} }
} }
logger.log(Level.INFO, "configured domains: " + serverConfig.getDomains()); logger.log(Level.INFO, "configured domains: " + serverConfig.getDomains());
return new Server(serverConfig, byteBufAllocator, return new Server(serverConfig, byteBufAllocator, parentEventLoopGroup, childEventLoopGroup,
parentEventLoopGroup, childEventLoopGroup, socketChannelClass, socketChannelClass, executor);
executor);
} }
} }
} }

View file

@ -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.Filter;
import org.xbib.netty.http.server.api.ServerRequest; import org.xbib.netty.http.server.api.ServerRequest;
import org.xbib.netty.http.server.api.ServerResponse; 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.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -108,32 +103,6 @@ public class HttpEndpointResolver implements EndpointResolver<HttpEndpoint> {
return this; 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) { public Builder setDispatcher(Filter dispatcher) {
Objects.requireNonNull(dispatcher); Objects.requireNonNull(dispatcher);
this.dispatcher = dispatcher; this.dispatcher = dispatcher;

View file

@ -6,7 +6,7 @@ import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandler;
import io.netty.util.Mapping; import io.netty.util.Mapping;
import org.xbib.netty.http.common.HttpAddress; 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.net.InetSocketAddress;
import java.util.Arrays; import java.util.Arrays;
import java.util.logging.Level; 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 static final Logger logger = Logger.getLogger(ExtendedSNIHandler.class.getName());
private final DefaultServerConfig serverConfig; private final ServerConfig serverConfig;
private final HttpAddress httpAddress; private final HttpAddress httpAddress;
public ExtendedSNIHandler(Mapping<? super String, ? extends SslContext> mapping, public ExtendedSNIHandler(Mapping<? super String, ? extends SslContext> mapping,
DefaultServerConfig serverConfig, HttpAddress httpAddress) { ServerConfig serverConfig, HttpAddress httpAddress) {
super(mapping); super(mapping);
this.serverConfig = serverConfig; this.serverConfig = serverConfig;
this.httpAddress = httpAddress; this.httpAddress = httpAddress;
@ -35,7 +35,7 @@ public class ExtendedSNIHandler extends SniHandler {
} }
private static SslHandler newSslHandler(SslContext sslContext, private static SslHandler newSslHandler(SslContext sslContext,
DefaultServerConfig serverConfig, ServerConfig serverConfig,
ByteBufAllocator allocator, ByteBufAllocator allocator,
HttpAddress httpAddress) { HttpAddress httpAddress) {
InetSocketAddress peer = httpAddress.getInetSocketAddress(); InetSocketAddress peer = httpAddress.getInetSocketAddress();

View file

@ -15,6 +15,8 @@ import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpVersion; 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.logging.LogLevel;
import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContext;
import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.handler.stream.ChunkedWriteHandler;
@ -22,8 +24,8 @@ import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.util.Mapping; import io.netty.util.Mapping;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.Server; 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.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.ExtendedSNIHandler;
import org.xbib.netty.http.server.handler.IdleTimeoutHandler; import org.xbib.netty.http.server.handler.IdleTimeoutHandler;
import org.xbib.netty.http.server.handler.TrafficLoggingHandler; import org.xbib.netty.http.server.handler.TrafficLoggingHandler;
@ -39,7 +41,7 @@ public class Http1ChannelInitializer extends ChannelInitializer<Channel>
private final Server server; private final Server server;
private final DefaultServerConfig serverConfig; private final ServerConfig serverConfig;
private final HttpAddress httpAddress; private final HttpAddress httpAddress;
@ -89,8 +91,7 @@ public class Http1ChannelInitializer extends ChannelInitializer<Channel>
serverConfig.getMaxHeadersSize(), serverConfig.getMaxChunkSize())); serverConfig.getMaxHeadersSize(), serverConfig.getMaxChunkSize()));
if (serverConfig.isCompressionEnabled()) { if (serverConfig.isCompressionEnabled()) {
pipeline.addLast("http-server-compressor", pipeline.addLast("http-server-compressor",
new HttpContentCompressor(6, 15, 8, new HttpContentCompressor());
serverConfig.getCompressionThreshold()));
} }
if (serverConfig.isDecompressionEnabled()) { if (serverConfig.isDecompressionEnabled()) {
pipeline.addLast("http-server-decompressor", pipeline.addLast("http-server-decompressor",
@ -99,10 +100,20 @@ public class Http1ChannelInitializer extends ChannelInitializer<Channel>
HttpObjectAggregator httpObjectAggregator = HttpObjectAggregator httpObjectAggregator =
new HttpObjectAggregator(serverConfig.getMaxContentLength()); new HttpObjectAggregator(serverConfig.getMaxContentLength());
httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents()); httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents());
pipeline.addLast("http-server-aggregator", httpObjectAggregator); pipeline.addLast("http-server-aggregator",
pipeline.addLast("http-server-pipelining", new HttpPipeliningHandler(serverConfig.getPipeliningCapacity())); httpObjectAggregator);
pipeline.addLast("http-server-handler", new ServerMessages(server)); if (serverConfig.getWebSocketFrameHandler() != null) {
pipeline.addLast("http-idle-timeout-handler", new IdleTimeoutHandler(serverConfig.getIdleTimeoutMillis())); 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 @Sharable
@ -118,6 +129,13 @@ public class Http1ChannelInitializer extends ChannelInitializer<Channel>
@Override @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 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) { if (msg instanceof HttpPipelinedRequest) {
HttpPipelinedRequest httpPipelinedRequest = (HttpPipelinedRequest) msg; HttpPipelinedRequest httpPipelinedRequest = (HttpPipelinedRequest) msg;
if (httpPipelinedRequest.getRequest() instanceof FullHttpRequest) { if (httpPipelinedRequest.getRequest() instanceof FullHttpRequest) {

View file

@ -34,8 +34,8 @@ import io.netty.util.AsciiString;
import io.netty.util.Mapping; import io.netty.util.Mapping;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.Server; 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.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.ExtendedSNIHandler;
import org.xbib.netty.http.server.handler.IdleTimeoutHandler; import org.xbib.netty.http.server.handler.IdleTimeoutHandler;
import org.xbib.netty.http.server.handler.TrafficLoggingHandler; import org.xbib.netty.http.server.handler.TrafficLoggingHandler;
@ -52,7 +52,7 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel>
private final Server server; private final Server server;
private final DefaultServerConfig serverConfig; private final ServerConfig serverConfig;
private final HttpAddress httpAddress; private final HttpAddress httpAddress;
@ -80,10 +80,12 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel>
configureCleartext(channel); configureCleartext(channel);
} }
if (serverConfig.isDebug()) { 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()); " address=" + httpAddress + " pipeline=" + channel.pipeline().names());
} }
} }
}
private void configureEncrypted(Channel channel) { private void configureEncrypted(Channel channel) {
channel.pipeline().addLast("sni-handler", channel.pipeline().addLast("sni-handler",

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}
}

View file

@ -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)));
}
}

View file

@ -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)));
}
}

View file

@ -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.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.xbib.net.URL;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.api.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.client.api.ResponseListener; 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.common.HttpResponse;
import org.xbib.netty.http.server.HttpServerDomain;
import org.xbib.netty.http.server.Server;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class BasicAuthTest { public class BasicAuthTest {
private static final Logger logger = Logger.getLogger(PostTest.class.getName()); private static final Logger logger = Logger.getLogger(PostTest.class.getName());
@Disabled @Test
void testBasicAuth() throws Exception { 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() Client client = Client.builder()
.build(); .build();
try { try {
ResponseListener<HttpResponse> responseListener = (resp) -> { server.accept();
if (resp.getStatus().getCode() == HttpResponseStatus.OK.code()) { ResponseListener<HttpResponse> responseListener = (resp) ->
logger.log(Level.INFO, "got response " + resp.getBodyAsString(StandardCharsets.UTF_8)); assertEquals("Basic aGVsbG86d29ybGQ=", resp.getBodyAsString(StandardCharsets.UTF_8));
} Request postRequest = Request.get()
}; .setVersion(HttpVersion.HTTP_1_1)
URL serverUrl = URL.from(""); .url(server.getServerConfig().getAddress().base())
Request postRequest = Request.post().setVersion(HttpVersion.HTTP_1_1) .addBasicAuthorization("hello", "world")
.url(serverUrl)
.addBasicAuthorization("", "")
.setResponseListener(responseListener) .setResponseListener(responseListener)
.build(); .build();
client.execute(postRequest).get(); client.execute(postRequest).get();
} finally { } finally {
server.shutdownGracefully();
client.shutdownGracefully(); client.shutdownGracefully();
logger.log(Level.INFO, "server and client shut down"); logger.log(Level.INFO, "server and client shut down");
} }

View file

@ -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");
}
}
}