From 430a9dd4d54752c105d09ecca6d1dc77ad709487 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Tue, 30 Apr 2024 08:10:22 +0300 Subject: [PATCH 01/14] next development iteration --- README.md | 4 ++-- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 42117ce..7a45935 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ EchoWebSocketHandler http1WebSocketHandler = new EchoWebSocketHandler(); Http2WebSocketClientHandshaker handShaker = Http2WebSocketClientHandshaker.create(channel); Http2Headers headers = - new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/1.2.5"); + new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/1.2.6"); ChannelFuture handshakeFuture = /*http1 websocket handler*/ handShaker.handshake("/echo", headers, new EchoWebSocketHandler()); @@ -286,7 +286,7 @@ repositories { } dependencies { - implementation 'com.jauntsdn.netty:netty-websocket-http2:1.2.5' + implementation 'com.jauntsdn.netty:netty-websocket-http2:1.2.6' } ``` diff --git a/gradle.properties b/gradle.properties index ac082ca..b5771b7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.jauntsdn.netty -version=1.2.6 +version=1.2.7 googleJavaFormatPluginVersion=0.9 dependencyManagementPluginVersion=1.1.0 From 5b12f6cddaa57114ca622f087468272f571d7bfa Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 2 May 2024 09:00:06 +0300 Subject: [PATCH 02/14] cleanup: remove star imports remove unused code --- .../http2/websocketx/example/Security.java | 8 ++++- .../example/channelclient/Main.java | 24 +++++++++++--- .../example/channelserver/Main.java | 30 ++++++++++++++--- .../example/handshakeserver/Main.java | 15 ++++++++- .../websocketx/example/lwsclient/Main.java | 7 +++- .../example/multiprotocolclient/Main.java | 22 +++++++++++-- .../example/multiprotocolserver/Main.java | 6 +++- .../perftest/callbackscodec/server/Main.java | 14 +++++--- .../perftest/messagecodec/client/Main.java | 10 ++++-- .../perftest/messagecodec/server/Main.java | 14 +++++--- .../websocketx/Http2WebSocketChannel.java | 33 +++++++++++++++++-- .../Http2WebSocketChannelHandler.java | 13 ++++++-- .../Http2WebSocketClientHandler.java | 6 +++- .../Http2WebSocketClientHandshaker.java | 15 +++++++-- .../websocketx/Http2WebSocketHandler.java | 8 ++++- .../Http2WebSocketServerHandler.java | 2 +- .../Http2WebSocketServerHandshaker.java | 17 ++++++++-- .../codec/http2/websocketx/Preconditions.java | 7 ---- .../websocketx/ApplicationHandshakeTest.java | 17 +++++++--- .../codec/http2/websocketx/PingPongTest.java | 8 ++++- .../websocketx/ProtocolHandshakeTest.java | 21 +++++++++--- .../http2/websocketx/TerminationTest.java | 10 ++++-- .../codec/http2/websocketx/WebSocketTest.java | 7 +++- 23 files changed, 252 insertions(+), 62 deletions(-) diff --git a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/Security.java b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/Security.java index 0fa8a20..06e8342 100644 --- a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/Security.java +++ b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/Security.java @@ -17,7 +17,13 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx.example; import io.netty.handler.codec.http2.Http2SecurityUtil; -import io.netty.handler.ssl.*; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import java.io.InputStream; import java.security.KeyStore; diff --git a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/channelclient/Main.java b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/channelclient/Main.java index 500137f..984e2a8 100644 --- a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/channelclient/Main.java +++ b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/channelclient/Main.java @@ -16,21 +16,35 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx.example.channelclient; -import static com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.*; - import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientBuilder; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientHandler; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientHandshaker; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeErrorEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeStartEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeSuccessEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketLifecycleEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketLocalCloseEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketStreamWeightUpdateEvent; import com.jauntsdn.netty.handler.codec.http2.websocketx.example.Security; import io.netty.bootstrap.Bootstrap; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig; -import io.netty.handler.codec.http2.*; -import io.netty.handler.ssl.*; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import java.io.IOException; diff --git a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/channelserver/Main.java b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/channelserver/Main.java index 4d17379..c77e3ea 100644 --- a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/channelserver/Main.java +++ b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/channelserver/Main.java @@ -16,10 +16,12 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx.example.channelserver; -import static com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.*; -import static io.netty.channel.ChannelHandler.*; - import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketAcceptor; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeErrorEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeStartEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeSuccessEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketLifecycleEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketLocalCloseEvent; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketServerBuilder; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketServerHandler; import com.jauntsdn.netty.handler.codec.http2.websocketx.example.Security; @@ -27,14 +29,32 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; -import io.netty.handler.codec.http2.*; +import io.netty.handler.codec.http2.DefaultHttp2DataFrame; +import io.netty.handler.codec.http2.DefaultHttp2GoAwayFrame; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.Http2ChannelDuplexHandler; +import io.netty.handler.codec.http2.Http2DataFrame; +import io.netty.handler.codec.http2.Http2Error; +import io.netty.handler.codec.http2.Http2Frame; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2FrameStream; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2HeadersFrame; +import io.netty.handler.codec.http2.ReadOnlyHttp2Headers; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; import io.netty.util.AsciiString; diff --git a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/handshakeserver/Main.java b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/handshakeserver/Main.java index ee732a0..6406e1b 100644 --- a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/handshakeserver/Main.java +++ b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/handshakeserver/Main.java @@ -28,7 +28,20 @@ import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.QueryStringDecoder; -import io.netty.handler.codec.http2.*; +import io.netty.handler.codec.http2.DefaultHttp2GoAwayFrame; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.DefaultHttp2WindowUpdateFrame; +import io.netty.handler.codec.http2.Http2ChannelDuplexHandler; +import io.netty.handler.codec.http2.Http2DataFrame; +import io.netty.handler.codec.http2.Http2Error; +import io.netty.handler.codec.http2.Http2Frame; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2FrameStream; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2HeadersFrame; +import io.netty.handler.codec.http2.Http2ResetFrame; +import io.netty.handler.codec.http2.ReadOnlyHttp2Headers; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; import io.netty.util.AsciiString; diff --git a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/lwsclient/Main.java b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/lwsclient/Main.java index 3105f53..5d66303 100644 --- a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/lwsclient/Main.java +++ b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/lwsclient/Main.java @@ -23,7 +23,12 @@ import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientHandshaker; import com.jauntsdn.netty.handler.codec.http2.websocketx.example.Security; import io.netty.bootstrap.Bootstrap; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; diff --git a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolclient/Main.java b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolclient/Main.java index af2796f..3b262ef 100644 --- a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolclient/Main.java +++ b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolclient/Main.java @@ -21,12 +21,28 @@ import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientHandshaker; import com.jauntsdn.netty.handler.codec.http2.websocketx.example.Security; import io.netty.bootstrap.Bootstrap; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.codec.http.*; -import io.netty.handler.codec.http.websocketx.*; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker; +import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory; +import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig; +import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; +import io.netty.handler.codec.http.websocketx.WebSocketVersion; import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; import io.netty.handler.codec.http2.DefaultHttp2Headers; import io.netty.handler.codec.http2.Http2FrameCodec; diff --git a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolserver/Main.java b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolserver/Main.java index f6f6e20..6400ad1 100644 --- a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolserver/Main.java +++ b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolserver/Main.java @@ -23,7 +23,11 @@ import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketServerHandler; import com.jauntsdn.netty.handler.codec.http2.websocketx.example.Security; import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; diff --git a/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/callbackscodec/server/Main.java b/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/callbackscodec/server/Main.java index 5d35949..db12e92 100644 --- a/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/callbackscodec/server/Main.java +++ b/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/callbackscodec/server/Main.java @@ -16,8 +16,6 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.callbackscodec.server; -import static io.netty.channel.ChannelHandler.*; - import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketCallbacksHandler; import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketFrameFactory; import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketFrameListener; @@ -30,13 +28,21 @@ import com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.Transport; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; import io.netty.channel.epoll.Epoll; import io.netty.channel.kqueue.KQueue; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig; import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; -import io.netty.handler.codec.http2.*; +import io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.UniformStreamByteDistributor; import io.netty.handler.ssl.OpenSsl; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; diff --git a/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/messagecodec/client/Main.java b/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/messagecodec/client/Main.java index e496f2c..23ad204 100644 --- a/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/messagecodec/client/Main.java +++ b/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/messagecodec/client/Main.java @@ -16,18 +16,22 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.messagecodec.client; -import static com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.*; - import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientBuilder; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientHandler; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientHandshaker; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketLifecycleEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Type; import com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.Security; import com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.Transport; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; import io.netty.channel.epoll.Epoll; import io.netty.channel.kqueue.KQueue; import io.netty.channel.socket.SocketChannel; diff --git a/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/messagecodec/server/Main.java b/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/messagecodec/server/Main.java index 28320be..b00eb1c 100644 --- a/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/messagecodec/server/Main.java +++ b/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/messagecodec/server/Main.java @@ -16,20 +16,26 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.messagecodec.server; -import static io.netty.channel.ChannelHandler.*; - import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketServerBuilder; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketServerHandler; import com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.Security; import com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.Transport; import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; import io.netty.channel.epoll.Epoll; import io.netty.channel.kqueue.KQueue; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; -import io.netty.handler.codec.http2.*; +import io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.UniformStreamByteDistributor; import io.netty.handler.ssl.OpenSsl; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketChannel.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketChannel.java index 7efd438..8d8119a 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketChannel.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketChannel.java @@ -23,11 +23,38 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; -import io.netty.channel.*; -import io.netty.handler.codec.http.websocketx.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelId; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelProgressivePromise; +import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultChannelConfig; +import io.netty.channel.DefaultChannelPipeline; +import io.netty.channel.DefaultMessageSizeEstimator; +import io.netty.channel.EventLoop; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.VoidChannelPromise; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.handler.codec.http.websocketx.Utf8FrameValidator; +import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrameDecoder; +import io.netty.handler.codec.http.websocketx.WebSocketFrameEncoder; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder; import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder; -import io.netty.handler.codec.http2.*; +import io.netty.handler.codec.http2.Http2Error; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2Flags; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2Settings; import io.netty.util.AttributeKey; import io.netty.util.DefaultAttributeMap; import io.netty.util.ReferenceCountUtil; diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketChannelHandler.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketChannelHandler.java index d1f9636..a9329d6 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketChannelHandler.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketChannelHandler.java @@ -17,9 +17,18 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx; import io.netty.buffer.ByteBuf; -import io.netty.channel.*; +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.handler.codec.http2.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2ConnectionEncoder; +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.util.collection.IntCollections; import io.netty.util.collection.IntObjectHashMap; import io.netty.util.collection.IntObjectMap; diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandler.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandler.java index 44002c2..2963af7 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandler.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandler.java @@ -20,7 +20,11 @@ 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.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2LocalFlowController; +import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.ssl.SslHandler; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import javax.annotation.Nullable; diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java index e3204c4..12369c0 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java @@ -16,16 +16,25 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx; -import static com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketHandler.*; +import static com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketHandler.endOfStreamName; +import static com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketHandler.endOfStreamValue; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketChannelHandler.WebSocketsParent; -import io.netty.channel.*; +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.*; +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 java.net.InetSocketAddress; import java.nio.channels.ClosedChannelException; diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandler.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandler.java index b927210..b149ece 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandler.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandler.java @@ -19,7 +19,13 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http2.*; +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 */ diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandler.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandler.java index 2511c23..2057bd3 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandler.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandler.java @@ -16,7 +16,7 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx; -import io.netty.channel.*; +import io.netty.channel.ChannelHandlerContext; 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; diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java index 447f7a9..8b24c97 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java @@ -20,11 +20,22 @@ import static com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketHandler.endOfStreamValue; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketChannelHandler.WebSocketsParent; -import io.netty.channel.*; +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.*; -import io.netty.handler.codec.http2.*; +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; diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Preconditions.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Preconditions.java index fb3e3c6..6c61b5c 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Preconditions.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Preconditions.java @@ -50,13 +50,6 @@ static long requirePositive(long value, String message) { return value; } - static int requireNonNegative(int value, String message) { - if (value < 0) { - throw new IllegalArgumentException(message + " must be non-negative: " + value); - } - return value; - } - static short requireRange(int value, int from, int to, String message) { if (value >= from && value <= to) { return (short) value; diff --git a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ApplicationHandshakeTest.java b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ApplicationHandshakeTest.java index 90d6afe..f5c55d1 100644 --- a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ApplicationHandshakeTest.java +++ b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ApplicationHandshakeTest.java @@ -16,12 +16,21 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx; -import static com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.*; - -import io.netty.channel.*; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeErrorEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeStartEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeSuccessEvent; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig; import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; -import io.netty.handler.codec.http2.*; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; import io.netty.util.AsciiString; diff --git a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/PingPongTest.java b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/PingPongTest.java index 657b16b..36f75ed 100644 --- a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/PingPongTest.java +++ b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/PingPongTest.java @@ -18,7 +18,13 @@ import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPromise; +import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; diff --git a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ProtocolHandshakeTest.java b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ProtocolHandshakeTest.java index ceee1e6..2acaf04 100644 --- a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ProtocolHandshakeTest.java +++ b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ProtocolHandshakeTest.java @@ -16,12 +16,25 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx; -import static com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.*; - +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeErrorEvent; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeStartEvent; -import io.netty.channel.*; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeSuccessEvent; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; -import io.netty.handler.codec.http2.*; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.Http2ChannelDuplexHandler; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2FrameStream; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2HeadersFrame; +import io.netty.handler.codec.http2.Http2ResetFrame; +import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; import io.netty.util.ReferenceCountUtil; diff --git a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/TerminationTest.java b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/TerminationTest.java index 6e321cf..595db01 100644 --- a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/TerminationTest.java +++ b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/TerminationTest.java @@ -16,9 +16,13 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx; -import static com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.*; - -import io.netty.channel.*; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketLocalCloseEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketRemoteCloseEvent; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http2.Http2FrameCodec; import io.netty.handler.codec.http2.Http2FrameCodecBuilder; import io.netty.handler.codec.http2.Http2Settings; diff --git a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/WebSocketTest.java b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/WebSocketTest.java index 7750d2a..def1a35 100644 --- a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/WebSocketTest.java +++ b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/WebSocketTest.java @@ -16,7 +16,12 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http2.Http2FrameCodec; import io.netty.handler.codec.http2.Http2FrameCodecBuilder; From 527a58976b3f49681495ce6dcec0f80675b09050 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 2 May 2024 10:19:10 +0300 Subject: [PATCH 03/14] remove Http2WebSocketMessages; move its contents into Http2WebSocketProtocol --- .../Http2WebSocketClientHandshaker.java | 22 ++++++----- .../http2/websocketx/Http2WebSocketEvent.java | 4 +- .../websocketx/Http2WebSocketMessages.java | 39 ------------------- .../websocketx/Http2WebSocketProtocol.java | 21 ++++++++++ .../Http2WebSocketServerBuilder.java | 4 +- .../Http2WebSocketServerHandshaker.java | 6 +-- 6 files changed, 40 insertions(+), 56 deletions(-) delete mode 100644 netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketMessages.java diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java index 12369c0..e0bc026 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java @@ -229,7 +229,7 @@ void handshake(Http2WebSocket webSocket, Http2Headers responseHeaders, boolean e switch (status) { case "200": if (endOfStream) { - errorMessage = Http2WebSocketMessages.HANDSHAKE_UNEXPECTED_RESULT; + errorMessage = Http2WebSocketProtocol.MSG_HANDSHAKE_UNEXPECTED_RESULT; } else { /*subprotocol*/ String clientSubprotocol = webSocketChannel.subprotocol(); @@ -237,7 +237,7 @@ void handshake(Http2WebSocket webSocket, Http2Headers responseHeaders, boolean e responseHeaders.get(Http2WebSocketProtocol.HEADER_WEBSOCKET_SUBPROTOCOL_NAME); if (!isEqual(clientSubprotocol, serverSubprotocol)) { errorMessage = - Http2WebSocketMessages.HANDSHAKE_UNEXPECTED_SUBPROTOCOL + clientSubprotocol; + Http2WebSocketProtocol.MSG_HANDSHAKE_UNEXPECTED_SUBPROTOCOL + clientSubprotocol; } /*compression*/ if (errorMessage == null) { @@ -259,18 +259,18 @@ void handshake(Http2WebSocket webSocket, Http2Headers responseHeaders, boolean e responseHeaders.get(Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_NAME); errorMessage = webSocketVersion != null - ? Http2WebSocketMessages.HANDSHAKE_UNSUPPORTED_VERSION + webSocketVersion - : Http2WebSocketMessages.HANDSHAKE_BAD_REQUEST; + ? Http2WebSocketProtocol.MSG_HANDSHAKE_UNSUPPORTED_VERSION + webSocketVersion + : Http2WebSocketProtocol.MSG_HANDSHAKE_BAD_REQUEST; break; case "404": errorMessage = - Http2WebSocketMessages.HANDSHAKE_PATH_NOT_FOUND + Http2WebSocketProtocol.MSG_HANDSHAKE_PATH_NOT_FOUND + webSocketChannel.path() - + Http2WebSocketMessages.HANDSHAKE_PATH_NOT_FOUND_SUBPROTOCOLS + + Http2WebSocketProtocol.MSG_HANDSHAKE_PATH_NOT_FOUND_SUBPROTOCOLS + webSocketChannel.subprotocol(); break; default: - errorMessage = Http2WebSocketMessages.HANDSHAKE_GENERIC_ERROR + status; + errorMessage = Http2WebSocketProtocol.MSG_HANDSHAKE_GENERIC_ERROR + status; } if (errorMessage != null) { Exception cause = new WebSocketHandshakeException(errorMessage); @@ -305,7 +305,8 @@ void reject(int streamId, Http2WebSocket webSocket, Http2Headers headers, boolea return; } Exception cause = - new WebSocketHandshakeException(Http2WebSocketMessages.HANDSHAKE_INVALID_RESPONSE_HEADERS); + new WebSocketHandshakeException( + Http2WebSocketProtocol.MSG_HANDSHAKE_INVALID_RESPONSE_HEADERS); if (handshakePromise.tryFailure(cause)) { Http2WebSocketEvent.fireHandshakeError(webSocketChannel, headers, System.nanoTime(), cause); } @@ -313,7 +314,7 @@ void reject(int streamId, Http2WebSocket webSocket, Http2Headers headers, boolea void onSupportsWebSocket(boolean supportsWebSocket) { if (!supportsWebSocket) { - logger.error(Http2WebSocketMessages.HANDSHAKE_UNSUPPORTED_BOOTSTRAP); + logger.error(Http2WebSocketProtocol.MSG_HANDSHAKE_UNSUPPORTED_BOOTSTRAP); } this.supportsWebSocket = supportsWebSocket; handshakeDeferred(supportsWebSocket); @@ -387,7 +388,8 @@ private void handshakeImmediate(Handshake handshake, boolean supportsWebSocket) /*server does not support http2 websockets*/ if (!supportsWebSocket) { WebSocketHandshakeException e = - new WebSocketHandshakeException(Http2WebSocketMessages.HANDSHAKE_UNSUPPORTED_BOOTSTRAP); + new WebSocketHandshakeException( + Http2WebSocketProtocol.MSG_HANDSHAKE_UNSUPPORTED_BOOTSTRAP); Http2WebSocketEvent.fireHandshakeError(webSocketChannel, null, System.nanoTime(), e); handshake.complete(e); return; diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java index cf6c46c..9d657f4 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java @@ -39,7 +39,7 @@ static void fireFrameWriteError(Channel parentChannel, Throwable t) { } if (t instanceof Exception) { parentPipeline.fireUserEventTriggered( - new Http2WebSocketWriteErrorEvent(Http2WebSocketMessages.WRITE_ERROR, t)); + new Http2WebSocketWriteErrorEvent(Http2WebSocketProtocol.MSG_WRITE_ERROR, t)); return; } parentPipeline.fireExceptionCaught(t); @@ -57,7 +57,7 @@ static void fireHandshakeValidationStartAndError( timestamp, timestamp, WebSocketHandshakeException.class.getName(), - Http2WebSocketMessages.HANDSHAKE_INVALID_REQUEST_HEADERS); + Http2WebSocketProtocol.MSG_HANDSHAKE_INVALID_REQUEST_HEADERS); } static void fireHandshakeStartAndError( diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketMessages.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketMessages.java deleted file mode 100644 index 8ce2934..0000000 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketMessages.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 - present Maksym Ostroverkhov. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.jauntsdn.netty.handler.codec.http2.websocketx; - -final class Http2WebSocketMessages { - static final String HANDSHAKE_UNEXPECTED_RESULT = - "websocket handshake error: unexpected result - status=200, end_of_stream=true"; - static final String HANDSHAKE_UNSUPPORTED_VERSION = - "websocket handshake error: unsupported version; supported versions - "; - static final String HANDSHAKE_BAD_REQUEST = "websocket handshake error: bad request"; - static final String HANDSHAKE_PATH_NOT_FOUND = "websocket handshake error: path not found - "; - static final String HANDSHAKE_PATH_NOT_FOUND_SUBPROTOCOLS = ", subprotocols - "; - static final String HANDSHAKE_UNEXPECTED_SUBPROTOCOL = - "websocket handshake error: unexpected subprotocol - "; - static final String HANDSHAKE_GENERIC_ERROR = "websocket handshake error: "; - static final String HANDSHAKE_UNSUPPORTED_ACCEPTOR_TYPE = - "websocket handshake error: async acceptors are not supported"; - static final String HANDSHAKE_UNSUPPORTED_BOOTSTRAP = - "websocket handshake error: bootstrapping websockets with http2 is not supported by server"; - static final String HANDSHAKE_INVALID_REQUEST_HEADERS = - "websocket handshake error: invalid request headers"; - static final String HANDSHAKE_INVALID_RESPONSE_HEADERS = - "websocket handshake error: invalid response headers"; - static final String WRITE_ERROR = "websocket frame write error"; -} diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java index a9c1ddd..e09a046 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java @@ -37,6 +37,27 @@ final class Http2WebSocketProtocol { static final AsciiString HEADER_PROTOCOL_NAME_HANDSHAKED = AsciiString.of("x-protocol"); static final AsciiString HEADER_METHOD_CONNECT_HANDSHAKED = AsciiString.of("POST"); + /*messages*/ + static final String MSG_HANDSHAKE_UNEXPECTED_RESULT = + "websocket handshake error: unexpected result - status=200, end_of_stream=true"; + static final String MSG_HANDSHAKE_UNSUPPORTED_VERSION = + "websocket handshake error: unsupported version; supported versions - "; + static final String MSG_HANDSHAKE_BAD_REQUEST = "websocket handshake error: bad request"; + static final String MSG_HANDSHAKE_PATH_NOT_FOUND = "websocket handshake error: path not found - "; + static final String MSG_HANDSHAKE_PATH_NOT_FOUND_SUBPROTOCOLS = ", subprotocols - "; + static final String MSG_HANDSHAKE_UNEXPECTED_SUBPROTOCOL = + "websocket handshake error: unexpected subprotocol - "; + static final String MSG_HANDSHAKE_GENERIC_ERROR = "websocket handshake error: "; + static final String MSG_HANDSHAKE_UNSUPPORTED_ACCEPTOR_TYPE = + "websocket handshake error: async acceptors are not supported"; + static final String MSG_HANDSHAKE_UNSUPPORTED_BOOTSTRAP = + "websocket handshake error: bootstrapping websockets with http2 is not supported by server"; + static final String MSG_HANDSHAKE_INVALID_REQUEST_HEADERS = + "websocket handshake error: invalid request headers"; + static final String MSG_HANDSHAKE_INVALID_RESPONSE_HEADERS = + "websocket handshake error: invalid response headers"; + static final String MSG_WRITE_ERROR = "websocket frame write error"; + static Http2Headers extendedConnect(Http2Headers headers) { return headers .method(Http2WebSocketProtocol.HEADER_METHOD_CONNECT) diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerBuilder.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerBuilder.java index 8fdb99b..0e260f9 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerBuilder.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerBuilder.java @@ -35,9 +35,9 @@ public final class Http2WebSocketServerBuilder { .executor() .newFailedFuture( new Http2WebSocketPathNotFoundException( - Http2WebSocketMessages.HANDSHAKE_PATH_NOT_FOUND + Http2WebSocketProtocol.MSG_HANDSHAKE_PATH_NOT_FOUND + path - + Http2WebSocketMessages.HANDSHAKE_PATH_NOT_FOUND_SUBPROTOCOLS + + Http2WebSocketProtocol.MSG_HANDSHAKE_PATH_NOT_FOUND_SUBPROTOCOLS + subprotocols)); private Http1WebSocketCodec webSocketCodec = Http1WebSocketCodec.DEFAULT; diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java index 8b24c97..683a074 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java @@ -117,7 +117,7 @@ void handshake(final int streamId, final Http2Headers requestHeaders, boolean en startNanos, System.nanoTime(), WebSocketHandshakeException.class.getName(), - Http2WebSocketMessages.HANDSHAKE_UNSUPPORTED_VERSION + webSocketVersion); + Http2WebSocketProtocol.MSG_HANDSHAKE_UNSUPPORTED_VERSION + webSocketVersion); writeHeaders(ctx, streamId, HEADERS_UNSUPPORTED_VERSION, true).addListener(this); return; @@ -171,7 +171,7 @@ void handshake(final int streamId, final Http2Headers requestHeaders, boolean en startNanos, System.nanoTime(), WebSocketHandshakeException.class.getName(), - Http2WebSocketMessages.HANDSHAKE_UNSUPPORTED_ACCEPTOR_TYPE); + Http2WebSocketProtocol.MSG_HANDSHAKE_UNSUPPORTED_ACCEPTOR_TYPE); writeHeaders(ctx, streamId, HEADERS_INTERNAL_ERROR, true).addListener(this); return; @@ -213,7 +213,7 @@ void handshake(final int streamId, final Http2Headers requestHeaders, boolean en startNanos, System.nanoTime(), WebSocketHandshakeException.class.getName(), - Http2WebSocketMessages.HANDSHAKE_UNEXPECTED_SUBPROTOCOL + subprotocolOrBlank); + Http2WebSocketProtocol.MSG_HANDSHAKE_UNEXPECTED_SUBPROTOCOL + subprotocolOrBlank); writeHeaders(ctx, streamId, HEADERS_NOT_FOUND, true).addListener(this); return; From 24c74ba60409ebc78473ce3cd43954886340c681 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 2 May 2024 10:25:16 +0300 Subject: [PATCH 04/14] remove Http2WebSocketExtensions; move its contents into Http2WebSocketProtocol --- .../Http2WebSocketClientHandshaker.java | 4 +- .../websocketx/Http2WebSocketExtensions.java | 104 ------------------ .../websocketx/Http2WebSocketProtocol.java | 86 +++++++++++++++ .../Http2WebSocketServerHandshaker.java | 5 +- 4 files changed, 91 insertions(+), 108 deletions(-) delete mode 100644 netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketExtensions.java diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java index e0bc026..8f8859b 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java @@ -246,7 +246,7 @@ void handshake(Http2WebSocket webSocket, Http2Headers responseHeaders, boolean e CharSequence extensionsHeader = responseHeaders.get(Http2WebSocketProtocol.HEADER_WEBSOCKET_EXTENSIONS_NAME); WebSocketExtensionData compression = - Http2WebSocketExtensions.decode(extensionsHeader); + Http2WebSocketProtocol.decodeExtensions(extensionsHeader); if (compression != null) { compressionExtension = handshaker.handshakeExtension(compression); } @@ -453,7 +453,7 @@ private CharSequence compressionExtensionHeader( if (header == null) { header = compressionExtensionHeader = - AsciiString.of(Http2WebSocketExtensions.encode(handshaker.newRequestData())); + AsciiString.of(Http2WebSocketProtocol.encodeExtensions(handshaker.newRequestData())); } return header; } diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketExtensions.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketExtensions.java deleted file mode 100644 index 5522989..0000000 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketExtensions.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2020 - present Maksym Ostroverkhov. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.jauntsdn.netty.handler.codec.http2.websocketx; - -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; -import javax.annotation.Nullable; - -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("^([^=]+)(=[\\\"]?([^\\\"]+)[\\\"]?)?$"); - - @Nullable - static WebSocketExtensionData decode(@Nullable 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 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; - } - - static String encode(WebSocketExtensionData extensionData) { - String name = extensionData.name(); - Map params = extensionData.parameters(); - if (params.isEmpty()) { - return name; - } - /*at most 4 parameters*/ - StringBuilder sb = new StringBuilder(sizeOf(name, params)); - sb.append(name); - for (Map.Entry 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 extensionParameters) { - int size = extensionName.length(); - for (Map.Entry param : extensionParameters.entrySet()) { - /* key and ; */ - size += param.getKey().length() + 1; - String value = param.getValue(); - if (value != null) { - /* value and = */ size += value.length() + 1; - } - } - return size; - } -} diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java index e09a046..4b2b16f 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java @@ -16,8 +16,15 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData; import io.netty.handler.codec.http2.Http2Headers; 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; +import javax.annotation.Nullable; final class Http2WebSocketProtocol { static final char SETTINGS_ENABLE_CONNECT_PROTOCOL = 8; @@ -37,6 +44,13 @@ final class Http2WebSocketProtocol { static final AsciiString HEADER_PROTOCOL_NAME_HANDSHAKED = AsciiString.of("x-protocol"); static final AsciiString HEADER_METHOD_CONNECT_HANDSHAKED = AsciiString.of("POST"); + /*extensions*/ + 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("^([^=]+)(=[\\\"]?([^\\\"]+)[\\\"]?)?$"); + /*messages*/ static final String MSG_HANDSHAKE_UNEXPECTED_RESULT = "websocket handshake error: unexpected result - status=200, end_of_stream=true"; @@ -70,4 +84,76 @@ static boolean isExtendedConnect(Http2Headers headers) { return HEADER_METHOD_CONNECT.equals(headers.method()) && HEADER_PROTOCOL_VALUE.equals(headers.get(HEADER_PROTOCOL_NAME)); } + + /*extensions*/ + + @Nullable + static WebSocketExtensionData decodeExtensions(@Nullable 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 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; + } + + static String encodeExtensions(WebSocketExtensionData extensionData) { + String name = extensionData.name(); + Map params = extensionData.parameters(); + if (params.isEmpty()) { + return name; + } + /*at most 4 parameters*/ + StringBuilder sb = new StringBuilder(sizeOf(name, params)); + sb.append(name); + for (Map.Entry 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 extensionParameters) { + int size = extensionName.length(); + for (Map.Entry param : extensionParameters.entrySet()) { + /* key and ; */ + size += param.getKey().length() + 1; + String value = param.getValue(); + if (value != null) { + /* value and = */ size += value.length() + 1; + } + } + return size; + } } diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java index 683a074..55a23dd 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java @@ -131,7 +131,8 @@ void handshake(final int streamId, final Http2Headers requestHeaders, boolean en if (compressionHandshaker != null) { CharSequence extensionsHeader = requestHeaders.get(Http2WebSocketProtocol.HEADER_WEBSOCKET_EXTENSIONS_NAME); - WebSocketExtensionData compression = Http2WebSocketExtensions.decode(extensionsHeader); + WebSocketExtensionData compression = + Http2WebSocketProtocol.decodeExtensions(extensionsHeader); if (compression != null) { compressionExtension = compressionHandshaker.handshakeExtension(compression); } @@ -145,7 +146,7 @@ void handshake(final int streamId, final Http2Headers requestHeaders, boolean en if (hasCompression) { responseHeaders.set( Http2WebSocketProtocol.HEADER_WEBSOCKET_EXTENSIONS_NAME, - Http2WebSocketExtensions.encode(compressionExtension.newReponseData())); + Http2WebSocketProtocol.encodeExtensions(compressionExtension.newReponseData())); compressionEncoder = compressionExtension.newExtensionEncoder(); compressionDecoder = compressionExtension.newExtensionDecoder(); } From cc2d3f3a3eec3f19c7025373d75dd7df3a027364 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 2 May 2024 10:39:31 +0300 Subject: [PATCH 05/14] remove Http2WebSocketClosedChannel; use anonymous Http2WebSocket impl instead --- .../http2/websocketx/Http2WebSocket.java | 137 ++++++++++++------ 1 file changed, 96 insertions(+), 41 deletions(-) diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocket.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocket.java index 9fa3d06..c5c6e90 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocket.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocket.java @@ -18,10 +18,10 @@ 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; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2Settings; interface Http2WebSocket extends Http2FrameListener { @@ -37,43 +37,98 @@ interface Http2WebSocket extends Http2FrameListener { 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(); - } - } + Http2WebSocket CLOSED = + new Http2WebSocket() { + @Override + public void onGoAwayRead( + ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData) {} + + @Override + public void onWindowUpdateRead( + ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) {} + + @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) { + int processed = data.readableBytes() + padding; + data.release(); + return processed; + } + + @Override + public void onUnknownFrame( + ChannelHandlerContext ctx, + byte frameType, + int streamId, + Http2Flags flags, + ByteBuf payload) { + payload.release(); + } + + @Override + public void onHeadersRead( + ChannelHandlerContext ctx, + int streamId, + Http2Headers headers, + int padding, + boolean endOfStream) {} + + @Override + public void onHeadersRead( + ChannelHandlerContext ctx, + int streamId, + Http2Headers headers, + int streamDependency, + short weight, + boolean exclusive, + int padding, + boolean endOfStream) {} + + @Override + public void onPriorityRead( + ChannelHandlerContext ctx, + int streamId, + int streamDependency, + short weight, + boolean exclusive) {} + + @Override + public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) {} + + @Override + public void onSettingsAckRead(ChannelHandlerContext ctx) {} + + @Override + public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) {} + + @Override + public void onPingRead(ChannelHandlerContext ctx, long data) {} + + @Override + public void onPingAckRead(ChannelHandlerContext ctx, long data) {} + + @Override + public void onPushPromiseRead( + ChannelHandlerContext ctx, + int streamId, + int promisedStreamId, + Http2Headers headers, + int padding) {} + }; } From a1ae3af4c0224fdfa1a6d92bb090693febaa086e Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 2 May 2024 11:08:35 +0300 Subject: [PATCH 06/14] move Http2WebSocketValidator into Http2WebSocketProtocol --- .../Http2WebSocketClientHandler.java | 2 +- ...p2WebSocketHandshakeOnlyServerHandler.java | 4 +- .../websocketx/Http2WebSocketProtocol.java | 157 +++++++++++++++ .../Http2WebSocketServerHandler.java | 4 +- .../websocketx/Http2WebSocketValidator.java | 179 ------------------ .../websocketx/HeadersValidatorTest.java | 66 ++++--- 6 files changed, 198 insertions(+), 214 deletions(-) delete mode 100644 netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketValidator.java diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandler.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandler.java index 2963af7..9ae5cdf 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandler.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandler.java @@ -182,7 +182,7 @@ private boolean handshakeWebSocket( int streamId, Http2Headers responseHeaders, boolean endOfStream) { Http2WebSocket webSocket = webSocketRegistry.get(streamId); if (webSocket != null) { - if (!Http2WebSocketValidator.isValid(responseHeaders)) { + if (!Http2WebSocketProtocol.Validator.isValid(responseHeaders)) { handShaker().reject(streamId, webSocket, responseHeaders, endOfStream); } else { handShaker().handshake(webSocket, responseHeaders, endOfStream); diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandshakeOnlyServerHandler.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandshakeOnlyServerHandler.java index 2505b37..ec522be 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandshakeOnlyServerHandler.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandshakeOnlyServerHandler.java @@ -86,13 +86,13 @@ public void operationComplete(ChannelFuture future) { private boolean handshake(Http2Headers headers, boolean endOfStream) { if (Http2WebSocketProtocol.isExtendedConnect(headers)) { - boolean isValid = Http2WebSocketValidator.WebSocket.isValid(headers, endOfStream); + boolean isValid = Http2WebSocketProtocol.Validator.WebSocket.isValid(headers, endOfStream); if (isValid) { Http2WebSocketServerHandshaker.handshakeOnlyWebSocket(headers); } return isValid; } - return Http2WebSocketValidator.Http.isValid(headers, endOfStream); + return Http2WebSocketProtocol.Validator.Http.isValid(headers, endOfStream); } private void reject( diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java index 4b2b16f..54a5140 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java @@ -21,7 +21,9 @@ import io.netty.util.AsciiString; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -156,4 +158,159 @@ static int sizeOf(String extensionName, Map extensionParameters) } return size; } + + static final class Validator { + 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 INVALID_HEADERS = invalidHeaders(); + + public static boolean isValid(final Http2Headers responseHeaders) { + boolean isFirst = true; + for (Map.Entry 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 validPseudoHeaders) { + for (Map.Entry 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 validPseudoHeaders() { + Set 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 invalidHeaders() { + Set 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 SCHEME_HTTPS.equals(scheme) || SCHEME_HTTP.equals(scheme); + } + + static class Http { + private static final Set 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; + } + /*must be non-empty for http/https requests*/ + if (isEmpty(path) && isHttp(scheme)) { + return false; + } + } + + return containsValidPseudoHeaders(requestHeaders, VALID_PSEUDO_HEADERS) + && containsValidHeaders(requestHeaders); + } + } + + static class WebSocket { + private static final Set VALID_PSEUDO_HEADERS; + + static { + Set 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()); + /*must be non-empty, not include userinfo subcomponent*/ + if (isEmpty(authority) || authority.contains("@")) { + return false; + } + + if (isEmpty(requestHeaders.path())) { + return false; + } + /*:method is known to be "connect"*/ + + return containsValidPseudoHeaders(requestHeaders, VALID_PSEUDO_HEADERS) + && containsValidHeaders(requestHeaders); + } + } + } } diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandler.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandler.java index 2057bd3..381404e 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandler.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandler.java @@ -99,14 +99,14 @@ public void onHeadersRead( private boolean handshakeWebSocket(int streamId, Http2Headers headers, boolean endOfStream) { if (Http2WebSocketProtocol.isExtendedConnect(headers)) { - if (!Http2WebSocketValidator.WebSocket.isValid(headers, endOfStream)) { + if (!Http2WebSocketProtocol.Validator.WebSocket.isValid(headers, endOfStream)) { handshaker.reject(streamId, headers, endOfStream); } else { handshaker.handshake(streamId, headers, endOfStream); } return false; } - if (!Http2WebSocketValidator.Http.isValid(headers, endOfStream)) { + if (!Http2WebSocketProtocol.Validator.Http.isValid(headers, endOfStream)) { handshaker.reject(streamId, headers, endOfStream); return false; } diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketValidator.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketValidator.java deleted file mode 100644 index 067b1b7..0000000 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketValidator.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2020 - present Maksym Ostroverkhov. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.jauntsdn.netty.handler.codec.http2.websocketx; - -import io.netty.handler.codec.http2.Http2Headers; -import io.netty.util.AsciiString; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -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 INVALID_HEADERS = invalidHeaders(); - - public static boolean isValid(final Http2Headers responseHeaders) { - boolean isFirst = true; - for (Map.Entry 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 validPseudoHeaders) { - for (Map.Entry 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 validPseudoHeaders() { - Set 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 invalidHeaders() { - Set 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); - } - - static class Http { - private static final Set 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; - } - /*must be non-empty for http/https requests*/ - if (isEmpty(path) && isHttp(scheme)) { - return false; - } - } - - return containsValidPseudoHeaders(requestHeaders, VALID_PSEUDO_HEADERS) - && containsValidHeaders(requestHeaders); - } - } - - static class WebSocket { - private static final Set VALID_PSEUDO_HEADERS; - - static { - Set 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()); - /*must be non-empty, not include userinfo subcomponent*/ - if (isEmpty(authority) || authority.contains("@")) { - return false; - } - - if (isEmpty(requestHeaders.path())) { - return false; - } - /*:method is known to be "connect"*/ - - return containsValidPseudoHeaders(requestHeaders, VALID_PSEUDO_HEADERS) - && containsValidHeaders(requestHeaders); - } - } -} diff --git a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/HeadersValidatorTest.java b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/HeadersValidatorTest.java index c688c79..5e13c8c 100644 --- a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/HeadersValidatorTest.java +++ b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/HeadersValidatorTest.java @@ -27,21 +27,23 @@ public class HeadersValidatorTest { @Test void webSocketRequest() { Assertions.assertThat( - Http2WebSocketValidator.WebSocket.isValid(validWebSocketRequestHeaders(), false)) + Http2WebSocketProtocol.Validator.WebSocket.isValid( + validWebSocketRequestHeaders(), false)) .isTrue(); } @Test void webSocketRequestWithEndStream() { Assertions.assertThat( - Http2WebSocketValidator.WebSocket.isValid(validWebSocketRequestHeaders(), true)) + Http2WebSocketProtocol.Validator.WebSocket.isValid( + validWebSocketRequestHeaders(), true)) .isFalse(); } @Test void webSocketRequestWithEmptyScheme() { Assertions.assertThat( - Http2WebSocketValidator.WebSocket.isValid( + Http2WebSocketProtocol.Validator.WebSocket.isValid( validWebSocketRequestHeaders().scheme(asciiString("")), false)) .isFalse(); } @@ -49,7 +51,7 @@ void webSocketRequestWithEmptyScheme() { @Test void webSocketRequestWithNonHttpScheme() { Assertions.assertThat( - Http2WebSocketValidator.WebSocket.isValid( + Http2WebSocketProtocol.Validator.WebSocket.isValid( validWebSocketRequestHeaders().scheme(asciiString("ftp")), false)) .isFalse(); } @@ -57,7 +59,7 @@ void webSocketRequestWithNonHttpScheme() { @Test void webSocketRequestWithEmptyAuthority() { Assertions.assertThat( - Http2WebSocketValidator.WebSocket.isValid( + Http2WebSocketProtocol.Validator.WebSocket.isValid( validWebSocketRequestHeaders().authority(asciiString("")), false)) .isFalse(); } @@ -65,7 +67,7 @@ void webSocketRequestWithEmptyAuthority() { @Test void webSocketRequestWithSubcomponentAuthority() { Assertions.assertThat( - Http2WebSocketValidator.WebSocket.isValid( + Http2WebSocketProtocol.Validator.WebSocket.isValid( validWebSocketRequestHeaders().authority(asciiString("test@localhost")), false)) .isFalse(); } @@ -73,7 +75,7 @@ void webSocketRequestWithSubcomponentAuthority() { @Test void webSocketRequestWithEmptyPath() { Assertions.assertThat( - Http2WebSocketValidator.WebSocket.isValid( + Http2WebSocketProtocol.Validator.WebSocket.isValid( validWebSocketRequestHeaders().path(asciiString("")), false)) .isFalse(); } @@ -81,7 +83,7 @@ void webSocketRequestWithEmptyPath() { @Test void webSocketRequestWithInvalidPseudoHeader() { Assertions.assertThat( - Http2WebSocketValidator.WebSocket.isValid( + Http2WebSocketProtocol.Validator.WebSocket.isValid( validWebSocketRequestHeaders().add(asciiString(":status"), asciiString("200")), false)) .isFalse(); @@ -90,7 +92,7 @@ void webSocketRequestWithInvalidPseudoHeader() { @Test void webSocketRequestWithInvalidHeader() { Assertions.assertThat( - Http2WebSocketValidator.WebSocket.isValid( + Http2WebSocketProtocol.Validator.WebSocket.isValid( validWebSocketRequestHeaders() .add(asciiString("connection"), asciiString("keep-alive")), false)) @@ -100,7 +102,7 @@ void webSocketRequestWithInvalidHeader() { @Test void webSocketRequestWithInvalidTeHeader() { Assertions.assertThat( - Http2WebSocketValidator.WebSocket.isValid( + Http2WebSocketProtocol.Validator.WebSocket.isValid( validWebSocketRequestHeaders().add(asciiString("te"), asciiString("gzip")), false)) .isFalse(); } @@ -108,7 +110,7 @@ void webSocketRequestWithInvalidTeHeader() { @Test void webSocketRequestWithValidTeHeader() { Assertions.assertThat( - Http2WebSocketValidator.WebSocket.isValid( + Http2WebSocketProtocol.Validator.WebSocket.isValid( validWebSocketRequestHeaders().add(asciiString("te"), asciiString("trailers")), false)) .isTrue(); @@ -116,13 +118,15 @@ void webSocketRequestWithValidTeHeader() { @Test void webSocketResponse() { - Assertions.assertThat(Http2WebSocketValidator.isValid(validResponseHeaders())).isTrue(); + Assertions.assertThat(Http2WebSocketProtocol.Validator.isValid(validResponseHeaders())) + .isTrue(); } @Test void webSocketResponseWithEmptyStatus() { Assertions.assertThat( - Http2WebSocketValidator.isValid(validResponseHeaders().status(asciiString("")))) + Http2WebSocketProtocol.Validator.isValid( + validResponseHeaders().status(asciiString("")))) .isFalse(); } @@ -131,13 +135,13 @@ void webSocketResponseWithoutStatus() { Http2Headers headers = validResponseHeaders(); headers.remove(asciiString(":status")); - Assertions.assertThat(Http2WebSocketValidator.isValid(headers)).isFalse(); + Assertions.assertThat(Http2WebSocketProtocol.Validator.isValid(headers)).isFalse(); } @Test void webSocketResponseAdditionalPseudoHeader() { Assertions.assertThat( - Http2WebSocketValidator.isValid(validResponseHeaders().path(asciiString("/")))) + Http2WebSocketProtocol.Validator.isValid(validResponseHeaders().path(asciiString("/")))) .isFalse(); } @@ -146,21 +150,23 @@ void webSocketResponseUnexpectedPseudoHeader() { Http2Headers headers = validResponseHeaders(); headers.remove(asciiString(":status")); headers.path(asciiString("/")); - Assertions.assertThat(Http2WebSocketValidator.isValid(headers)).isFalse(); + Assertions.assertThat(Http2WebSocketProtocol.Validator.isValid(headers)).isFalse(); } @Test void httpRequest() { - Assertions.assertThat(Http2WebSocketValidator.Http.isValid(validHttpRequestHeaders(), false)) + Assertions.assertThat( + Http2WebSocketProtocol.Validator.Http.isValid(validHttpRequestHeaders(), false)) .isTrue(); - Assertions.assertThat(Http2WebSocketValidator.Http.isValid(validHttpRequestHeaders(), true)) + Assertions.assertThat( + Http2WebSocketProtocol.Validator.Http.isValid(validHttpRequestHeaders(), true)) .isTrue(); } @Test void httpRequestWithEmptyAuthority() { Assertions.assertThat( - Http2WebSocketValidator.Http.isValid( + Http2WebSocketProtocol.Validator.Http.isValid( validHttpRequestHeaders().authority(asciiString("")), false)) .isFalse(); } @@ -168,7 +174,7 @@ void httpRequestWithEmptyAuthority() { @Test void httpRequestWithSubcomponentAuthority() { Assertions.assertThat( - Http2WebSocketValidator.Http.isValid( + Http2WebSocketProtocol.Validator.Http.isValid( validHttpRequestHeaders().authority(asciiString("test@localhost")), false)) .isFalse(); } @@ -176,7 +182,7 @@ void httpRequestWithSubcomponentAuthority() { @Test void httpRequestWithEmptyMethod() { Assertions.assertThat( - Http2WebSocketValidator.Http.isValid( + Http2WebSocketProtocol.Validator.Http.isValid( validHttpRequestHeaders().method(asciiString("")), false)) .isFalse(); } @@ -184,7 +190,7 @@ void httpRequestWithEmptyMethod() { @Test void httpRequestWithEmptyScheme() { Assertions.assertThat( - Http2WebSocketValidator.Http.isValid( + Http2WebSocketProtocol.Validator.Http.isValid( validHttpRequestHeaders().scheme(asciiString("")), false)) .isFalse(); } @@ -192,12 +198,12 @@ void httpRequestWithEmptyScheme() { @Test void httpRequestWithConnectMethod() { Assertions.assertThat( - Http2WebSocketValidator.Http.isValid( + Http2WebSocketProtocol.Validator.Http.isValid( validHttpRequestHeaders().method(asciiString("connect")), false)) .isFalse(); Assertions.assertThat( - Http2WebSocketValidator.Http.isValid( + Http2WebSocketProtocol.Validator.Http.isValid( validHttpRequestHeaders() .method(asciiString("connect")) .scheme(asciiString("")) @@ -209,7 +215,7 @@ void httpRequestWithConnectMethod() { @Test void httpRequestWithEmptyPath() { Assertions.assertThat( - Http2WebSocketValidator.Http.isValid( + Http2WebSocketProtocol.Validator.Http.isValid( validHttpRequestHeaders().path(asciiString("")), false)) .isFalse(); } @@ -217,7 +223,7 @@ void httpRequestWithEmptyPath() { @Test void nonHttpRequestWithEmptyPath() { Assertions.assertThat( - Http2WebSocketValidator.Http.isValid( + Http2WebSocketProtocol.Validator.Http.isValid( validHttpRequestHeaders().scheme(asciiString("ftp")).path(asciiString("")), false)) .isTrue(); } @@ -225,7 +231,7 @@ void nonHttpRequestWithEmptyPath() { @Test void httpRequestWithInvalidPseudoHeader() { Assertions.assertThat( - Http2WebSocketValidator.Http.isValid( + Http2WebSocketProtocol.Validator.Http.isValid( validHttpRequestHeaders().add(asciiString(":status"), asciiString("200")), false)) .isFalse(); } @@ -233,7 +239,7 @@ void httpRequestWithInvalidPseudoHeader() { @Test void httpRequestWithInvalidHeader() { Assertions.assertThat( - Http2WebSocketValidator.Http.isValid( + Http2WebSocketProtocol.Validator.Http.isValid( validHttpRequestHeaders().set(asciiString("connection"), asciiString("keep-alive")), false)) .isFalse(); @@ -242,7 +248,7 @@ void httpRequestWithInvalidHeader() { @Test void httpRequestWithInvalidTeHeader() { Assertions.assertThat( - Http2WebSocketValidator.Http.isValid( + Http2WebSocketProtocol.Validator.Http.isValid( validHttpRequestHeaders().set(asciiString("te"), asciiString("gzip")), false)) .isFalse(); } @@ -250,7 +256,7 @@ void httpRequestWithInvalidTeHeader() { @Test void httpRequestWithValidTeHeader() { Assertions.assertThat( - Http2WebSocketValidator.Http.isValid( + Http2WebSocketProtocol.Validator.Http.isValid( validHttpRequestHeaders().set(asciiString("te"), asciiString("trailers")), false)) .isTrue(); } From 6d9aba64a27105835f61f98022b368a38f525875 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 2 May 2024 11:32:35 +0300 Subject: [PATCH 07/14] remove Preconditions --- .../Http2WebSocketClientBuilder.java | 11 ++-- .../Http2WebSocketClientHandshaker.java | 18 ++++-- .../http2/websocketx/Http2WebSocketEvent.java | 2 +- .../websocketx/Http2WebSocketHandler.java | 14 ++++- .../websocketx/Http2WebSocketProtocol.java | 17 ++++++ .../Http2WebSocketServerBuilder.java | 6 +- .../codec/http2/websocketx/Preconditions.java | 60 ------------------- 7 files changed, 52 insertions(+), 76 deletions(-) delete mode 100644 netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Preconditions.java diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientBuilder.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientBuilder.java index 8127f88..54a8c1c 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientBuilder.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientBuilder.java @@ -18,6 +18,7 @@ import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig; import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateClientExtensionHandshaker; +import java.util.Objects; /** Builder for {@link Http2WebSocketClientHandler} */ public final class Http2WebSocketClientBuilder { @@ -45,7 +46,7 @@ public static Http2WebSocketClientBuilder create() { * @return this {@link Http2WebSocketClientBuilder} instance */ public Http2WebSocketClientBuilder codec(Http1WebSocketCodec webSocketCodec) { - this.webSocketCodec = Preconditions.requireNonNull(webSocketCodec, "webSocketCodec"); + this.webSocketCodec = Objects.requireNonNull(webSocketCodec, "webSocketCodec"); return this; } @@ -55,7 +56,7 @@ public Http2WebSocketClientBuilder codec(Http1WebSocketCodec webSocketCodec) { */ public Http2WebSocketClientBuilder decoderConfig(WebSocketDecoderConfig webSocketDecoderConfig) { this.webSocketDecoderConfig = - Preconditions.requireNonNull(webSocketDecoderConfig, "webSocketDecoderConfig"); + Objects.requireNonNull(webSocketDecoderConfig, "webSocketDecoderConfig"); return this; } @@ -74,7 +75,7 @@ public Http2WebSocketClientBuilder maskPayload(boolean maskPayload) { */ public Http2WebSocketClientBuilder handshakeTimeoutMillis(long handshakeTimeoutMillis) { this.handshakeTimeoutMillis = - Preconditions.requirePositive(handshakeTimeoutMillis, "handshakeTimeoutMillis"); + Http2WebSocketProtocol.requirePositive(handshakeTimeoutMillis, "handshakeTimeoutMillis"); return this; } @@ -87,7 +88,7 @@ public Http2WebSocketClientBuilder handshakeTimeoutMillis(long handshakeTimeoutM public Http2WebSocketClientBuilder closedWebSocketRemoveTimeoutMillis( long closedWebSocketRemoveTimeoutMillis) { this.closedWebSocketRemoveTimeoutMillis = - Preconditions.requirePositive( + Http2WebSocketProtocol.requirePositive( closedWebSocketRemoveTimeoutMillis, "closedWebSocketRemoveTimeoutMillis"); return this; } @@ -144,7 +145,7 @@ public Http2WebSocketClientBuilder compression( * @return this {@link Http2WebSocketClientBuilder} instance */ public Http2WebSocketClientBuilder streamWeight(int weight) { - this.streamWeight = Preconditions.requireRange(weight, 1, 256, "streamWeight"); + this.streamWeight = Http2WebSocketProtocol.requireRange(weight, 1, 256, "streamWeight"); return this; } diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java index 8f8859b..e9aeca2 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketClientHandshaker.java @@ -101,7 +101,8 @@ public final class Http2WebSocketClientHandshaker { */ public static Http2WebSocketClientHandshaker create(Channel channel) { Objects.requireNonNull(channel, "channel"); - return Preconditions.requireHandler(channel, Http2WebSocketClientHandler.class).handShaker(); + return Http2WebSocketHandler.requireChannelHandler(channel, Http2WebSocketClientHandler.class) + .handShaker(); } /** @@ -162,10 +163,10 @@ public ChannelFuture handshake( String subprotocol, Http2Headers requestHeaders, ChannelHandler webSocketHandler) { - Preconditions.requireNonEmpty(path, "path"); - Preconditions.requireNonNull(subprotocol, "subprotocol"); - Preconditions.requireNonNull(requestHeaders, "requestHeaders"); - Preconditions.requireNonNull(webSocketHandler, "webSocketHandler"); + requireNonEmpty(path, "path"); + Objects.requireNonNull(subprotocol, "subprotocol"); + Objects.requireNonNull(requestHeaders, "requestHeaders"); + Objects.requireNonNull(webSocketHandler, "webSocketHandler"); long startNanos = System.nanoTime(); ChannelHandlerContext ctx = webSocketsParent.context(); @@ -469,6 +470,13 @@ private static boolean isEqual(String str, @Nullable CharSequence seq) { return str.contentEquals(seq); } + private static String requireNonEmpty(String string, String message) { + if (string == null || string.isEmpty()) { + throw new IllegalArgumentException(message + " must be non empty"); + } + return string; + } + static class Handshake extends Http2WebSocketServerHandshaker.Handshake { private final Http2WebSocketChannel webSocketChannel; private final Http2Headers requestHeaders; diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java index 9d657f4..65ea5f0 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java @@ -449,7 +449,7 @@ public static final class Http2WebSocketStreamWeightUpdateEvent extends Http2Web Http2WebSocketStreamWeightUpdateEvent(short streamWeight) { super(Type.WEIGHT_UPDATE); - this.streamWeight = Preconditions.requireRange(streamWeight, 1, 256, "streamWeight"); + this.streamWeight = Http2WebSocketProtocol.requireRange(streamWeight, 1, 256, "streamWeight"); } public short streamWeight() { diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandler.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandler.java index b149ece..d8bc13b 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandler.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandler.java @@ -17,7 +17,9 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx; import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http2.Http2ConnectionDecoder; import io.netty.handler.codec.http2.Http2ConnectionHandler; @@ -44,8 +46,7 @@ public abstract class Http2WebSocketHandler extends ChannelDuplexHandler @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { Http2ConnectionHandler http2Handler = - this.http2Handler = - Preconditions.requireHandler(ctx.channel(), Http2ConnectionHandler.class); + this.http2Handler = requireChannelHandler(ctx.channel(), Http2ConnectionHandler.class); HandlerListener listener = handlerListener; if (listener == null) { Http2ConnectionDecoder decoder = http2Handler.decoder(); @@ -183,6 +184,15 @@ static AsciiString endOfStreamValue(boolean endOfStream) { : HEADER_WEBSOCKET_ENDOFSTREAM_VALUE_FALSE; } + static T requireChannelHandler(Channel channel, Class handler) { + T h = channel.pipeline().get(handler); + if (h == null) { + throw new IllegalArgumentException( + handler.getSimpleName() + " is absent in the channel pipeline"); + } + return h; + } + static final class HandlerListener implements Http2FrameListener { Http2FrameListener cur; Http2FrameListener next; diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java index 54a5140..39c8cd1 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketProtocol.java @@ -313,4 +313,21 @@ public static boolean isValid(final Http2Headers requestHeaders, boolean endOfSt } } } + + /*preconditions*/ + + static long requirePositive(long value, String message) { + if (value <= 0) { + throw new IllegalArgumentException(message + " must be positive: " + value); + } + return value; + } + + 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)); + } } diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerBuilder.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerBuilder.java index 0e260f9..fa280cb 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerBuilder.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerBuilder.java @@ -97,7 +97,7 @@ public static Http2ConnectionHandlerBuilder configureHttp2Server( * @return this {@link Http2WebSocketClientBuilder} instance */ public Http2WebSocketServerBuilder codec(Http1WebSocketCodec webSocketCodec) { - this.webSocketCodec = Preconditions.requireNonNull(webSocketCodec, "webSocketCodec"); + this.webSocketCodec = Objects.requireNonNull(webSocketCodec, "webSocketCodec"); return this; } @@ -107,7 +107,7 @@ public Http2WebSocketServerBuilder codec(Http1WebSocketCodec webSocketCodec) { */ public Http2WebSocketServerBuilder decoderConfig(WebSocketDecoderConfig webSocketDecoderConfig) { this.webSocketDecoderConfig = - Preconditions.requireNonNull(webSocketDecoderConfig, "webSocketDecoderConfig"); + Objects.requireNonNull(webSocketDecoderConfig, "webSocketDecoderConfig"); return this; } @@ -120,7 +120,7 @@ public Http2WebSocketServerBuilder decoderConfig(WebSocketDecoderConfig webSocke public Http2WebSocketServerBuilder closedWebSocketRemoveTimeout( long closedWebSocketRemoveTimeoutMillis) { this.closedWebSocketRemoveTimeoutMillis = - Preconditions.requirePositive( + Http2WebSocketProtocol.requirePositive( closedWebSocketRemoveTimeoutMillis, "closedWebSocketRemoveTimeoutMillis"); return this; } diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Preconditions.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Preconditions.java deleted file mode 100644 index 6c61b5c..0000000 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Preconditions.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020 - present Maksym Ostroverkhov. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.jauntsdn.netty.handler.codec.http2.websocketx; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandler; - -final class Preconditions { - static T requireNonNull(T t, String message) { - if (t == null) { - throw new IllegalArgumentException(message + " must be non null"); - } - return t; - } - - static String requireNonEmpty(String string, String message) { - if (string == null || string.isEmpty()) { - throw new IllegalArgumentException(message + " must be non empty"); - } - return string; - } - - static T requireHandler(Channel channel, Class handler) { - T h = channel.pipeline().get(handler); - if (h == null) { - throw new IllegalArgumentException( - handler.getSimpleName() + " is absent in the channel pipeline"); - } - return h; - } - - static long requirePositive(long value, String message) { - if (value <= 0) { - throw new IllegalArgumentException(message + " must be positive: " + value); - } - return value; - } - - 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)); - } -} From 4e69b5b63127286a52d9067ebe5185d573f34f58 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 23 May 2024 15:08:08 +0300 Subject: [PATCH 08/14] update dependencies --- gradle.properties | 2 +- .../gradle.lockfile | 18 +++++++++--------- netty-websocket-http2/gradle.lockfile | 18 +++++++++--------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/gradle.properties b/gradle.properties index b5771b7..6817a2c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ gitPluginVersion=0.13.0 osDetectorPluginVersion=1.7.3 versionsPluginVersion=0.45.0 -nettyVersion=4.1.109.Final +nettyVersion=4.1.110.Final jauntNettyWebsocketHttp1=1.1.4 nettyTcnativeVersion=2.0.65.Final hdrHistogramVersion=2.1.12 diff --git a/netty-websocket-http2-callbacks-codec/gradle.lockfile b/netty-websocket-http2-callbacks-codec/gradle.lockfile index fbadd96..1baec01 100644 --- a/netty-websocket-http2-callbacks-codec/gradle.lockfile +++ b/netty-websocket-http2-callbacks-codec/gradle.lockfile @@ -8,14 +8,14 @@ com.google.googlejavaformat:google-java-format:1.6=googleJavaFormat1.6 com.google.guava:guava:22.0=googleJavaFormat1.6 com.google.j2objc:j2objc-annotations:1.1=googleJavaFormat1.6 com.jauntsdn.netty:netty-websocket-http1:1.1.4=compileClasspath -io.netty:netty-buffer:4.1.109.Final=compileClasspath -io.netty:netty-codec-http2:4.1.109.Final=compileClasspath -io.netty:netty-codec-http:4.1.109.Final=compileClasspath -io.netty:netty-codec:4.1.109.Final=compileClasspath -io.netty:netty-common:4.1.109.Final=compileClasspath -io.netty:netty-handler:4.1.109.Final=compileClasspath -io.netty:netty-resolver:4.1.109.Final=compileClasspath -io.netty:netty-transport-native-unix-common:4.1.109.Final=compileClasspath -io.netty:netty-transport:4.1.109.Final=compileClasspath +io.netty:netty-buffer:4.1.110.Final=compileClasspath +io.netty:netty-codec-http2:4.1.110.Final=compileClasspath +io.netty:netty-codec-http:4.1.110.Final=compileClasspath +io.netty:netty-codec:4.1.110.Final=compileClasspath +io.netty:netty-common:4.1.110.Final=compileClasspath +io.netty:netty-handler:4.1.110.Final=compileClasspath +io.netty:netty-resolver:4.1.110.Final=compileClasspath +io.netty:netty-transport-native-unix-common:4.1.110.Final=compileClasspath +io.netty:netty-transport:4.1.110.Final=compileClasspath org.codehaus.mojo:animal-sniffer-annotations:1.14=googleJavaFormat1.6 empty=annotationProcessor diff --git a/netty-websocket-http2/gradle.lockfile b/netty-websocket-http2/gradle.lockfile index 6f83340..a5000e0 100644 --- a/netty-websocket-http2/gradle.lockfile +++ b/netty-websocket-http2/gradle.lockfile @@ -9,17 +9,17 @@ com.google.errorprone:javac-shaded:9+181-r4173-1=googleJavaFormat1.6 com.google.googlejavaformat:google-java-format:1.6=googleJavaFormat1.6 com.google.guava:guava:22.0=googleJavaFormat1.6 com.google.j2objc:j2objc-annotations:1.1=googleJavaFormat1.6 -io.netty:netty-buffer:4.1.109.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-codec-http2:4.1.109.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-codec-http:4.1.109.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-codec:4.1.109.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-common:4.1.109.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-handler:4.1.109.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-resolver:4.1.109.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-buffer:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-http2:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-http:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-common:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-handler:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-resolver:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.netty:netty-tcnative-boringssl-static:2.0.65.Final=testRuntimeClasspath io.netty:netty-tcnative-classes:2.0.65.Final=testRuntimeClasspath -io.netty:netty-transport-native-unix-common:4.1.109.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-transport:4.1.109.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport-native-unix-common:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.11=testCompileClasspath,testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath org.assertj:assertj-core:3.25.3=testCompileClasspath,testRuntimeClasspath From eda9681a6a60b6dcdf5b24d404e16fd5fd4b5915 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Sun, 21 Jul 2024 12:26:23 +0300 Subject: [PATCH 09/14] websocket handshake success event: add selected subprotocol --- .../http2/websocketx/Http2WebSocketEvent.java | 25 +++++++++++++------ .../Http2WebSocketServerHandshaker.java | 1 + 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java index 65ea5f0..cc3df98 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java @@ -108,6 +108,7 @@ static void fireHandshakeStartAndSuccess( int serial, String path, String subprotocols, + String subprotocol, Http2Headers requestHeaders, Http2Headers responseHeaders, long startNanos, @@ -120,7 +121,7 @@ static void fireHandshakeStartAndSuccess( serial, path, subprotocols, startNanos, requestHeaders); Http2WebSocketHandshakeSuccessEvent successEvent = new Http2WebSocketHandshakeSuccessEvent( - serial, path, subprotocols, successNanos, responseHeaders); + serial, path, subprotocols, subprotocol, successNanos, responseHeaders); parentPipeline.fireUserEventTriggered(startEvent); parentPipeline.fireUserEventTriggered(successEvent); @@ -176,6 +177,7 @@ static void fireHandshakeError( static void fireHandshakeSuccess( Http2WebSocketChannel webSocketChannel, Http2Headers responseHeaders, long timestampNanos) { String path = webSocketChannel.path(); + String subprotocol = webSocketChannel.subprotocol(); ChannelPipeline parentPipeline = webSocketChannel.parent().pipeline(); ChannelPipeline webSocketPipeline = webSocketChannel.pipeline(); @@ -183,7 +185,8 @@ static void fireHandshakeSuccess( new Http2WebSocketHandshakeSuccessEvent( webSocketChannel.serial(), path, - webSocketChannel.subprotocol(), + subprotocol, + subprotocol, timestampNanos, responseHeaders); @@ -242,15 +245,15 @@ public Throwable error() { public static class Http2WebSocketLifecycleEvent extends Http2WebSocketEvent { private final int id; private final String path; - private final String subprotocol; + private final String subprotocols; private final long timestampNanos; Http2WebSocketLifecycleEvent( - Type type, int id, String path, String subprotocol, long timestampNanos) { + Type type, int id, String path, String subprotocols, long timestampNanos) { super(type); this.id = id; this.path = path; - this.subprotocol = subprotocol; + this.subprotocols = subprotocols; this.timestampNanos = timestampNanos; } @@ -264,9 +267,9 @@ public String path() { return path; } - /** @return websocket subprotocol */ + /** @return websocket subprotocols */ public String subprotocols() { - return subprotocol; + return subprotocols; } /** @return event timestamp */ @@ -368,18 +371,26 @@ public String errorMessage() { /** websocket-over-http2 handshake success event */ public static class Http2WebSocketHandshakeSuccessEvent extends Http2WebSocketLifecycleEvent { + private final String subprotocol; private final Http2Headers responseHeaders; Http2WebSocketHandshakeSuccessEvent( int id, String path, String subprotocols, + String subprotocol, long timestampNanos, Http2Headers responseHeaders) { super(Type.HANDSHAKE_SUCCESS, id, path, subprotocols, timestampNanos); + this.subprotocol = subprotocol; this.responseHeaders = responseHeaders; } + /** @return selected subprotocol of succeeded websocket handshake */ + public String subprotocol() { + return subprotocol; + } + /** @return response headers of succeeded websocket handshake */ public Http2Headers responseHeaders() { return responseHeaders; diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java index 55a23dd..20dd199 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketServerHandshaker.java @@ -299,6 +299,7 @@ void handshake(final int streamId, final Http2Headers requestHeaders, boolean en streamId, path, subprotocols, + acceptedSubprotocol, requestHeaders, successHeaders, startNanos, From a16a39d680c77e635d8511800f5caac5fcea2a03 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Mon, 22 Jul 2024 12:11:59 +0300 Subject: [PATCH 10/14] websocket lifecycle events: add transport agnostic variants --- .../http2/websocketx/Http2WebSocketEvent.java | 80 ++-- .../http2/websocketx/WebSocketEvent.java | 155 ++++++++ .../websocketx/ApplicationHandshakeTest.java | 364 +++++++++++++----- .../websocketx/ProtocolHandshakeTest.java | 58 ++- 4 files changed, 516 insertions(+), 141 deletions(-) create mode 100644 netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/WebSocketEvent.java diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java index cc3df98..67345d2 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketEvent.java @@ -16,6 +16,9 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx; +import com.jauntsdn.netty.handler.codec.http2.websocketx.WebSocketEvent.WebSocketHandshakeErrorEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.WebSocketEvent.WebSocketHandshakeStartEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.WebSocketEvent.WebSocketHandshakeSuccessEvent; import io.netty.channel.Channel; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; @@ -74,9 +77,13 @@ static void fireHandshakeStartAndError( parentPipeline.fireUserEventTriggered( new Http2WebSocketHandshakeStartEvent( serial, path, subprotocols, startNanos, requestHeaders)); + parentPipeline.fireUserEventTriggered( + new WebSocketHandshakeStartEvent(serial, path, subprotocols, startNanos, requestHeaders)); parentPipeline.fireUserEventTriggered( new Http2WebSocketHandshakeErrorEvent(serial, path, subprotocols, errorNanos, null, t)); + parentPipeline.fireUserEventTriggered( + new WebSocketHandshakeErrorEvent(serial, path, subprotocols, errorNanos, null, t)); return; } parentPipeline.fireExceptionCaught(t); @@ -97,10 +104,15 @@ static void fireHandshakeStartAndError( parentPipeline.fireUserEventTriggered( new Http2WebSocketHandshakeStartEvent( serial, path, subprotocols, startNanos, requestHeaders)); + parentPipeline.fireUserEventTriggered( + new WebSocketHandshakeStartEvent(serial, path, subprotocols, startNanos, requestHeaders)); parentPipeline.fireUserEventTriggered( new Http2WebSocketHandshakeErrorEvent( serial, path, subprotocols, errorNanos, null, errorName, errorMessage)); + parentPipeline.fireUserEventTriggered( + new WebSocketHandshakeErrorEvent( + serial, path, subprotocols, errorNanos, null, errorName, errorMessage)); } static void fireHandshakeStartAndSuccess( @@ -116,17 +128,26 @@ static void fireHandshakeStartAndSuccess( ChannelPipeline parentPipeline = webSocketChannel.parent().pipeline(); ChannelPipeline webSocketPipeline = webSocketChannel.pipeline(); - Http2WebSocketHandshakeStartEvent startEvent = + Http2WebSocketHandshakeStartEvent http2StartEvent = new Http2WebSocketHandshakeStartEvent( serial, path, subprotocols, startNanos, requestHeaders); - Http2WebSocketHandshakeSuccessEvent successEvent = + WebSocketHandshakeStartEvent startEvent = + new WebSocketHandshakeStartEvent(serial, path, subprotocols, startNanos, requestHeaders); + Http2WebSocketHandshakeSuccessEvent http2SuccessEvent = new Http2WebSocketHandshakeSuccessEvent( serial, path, subprotocols, subprotocol, successNanos, responseHeaders); + WebSocketHandshakeSuccessEvent successEvent = + new WebSocketHandshakeSuccessEvent( + serial, path, subprotocols, subprotocol, successNanos, responseHeaders); + parentPipeline.fireUserEventTriggered(http2StartEvent); parentPipeline.fireUserEventTriggered(startEvent); + parentPipeline.fireUserEventTriggered(http2SuccessEvent); parentPipeline.fireUserEventTriggered(successEvent); + webSocketPipeline.fireUserEventTriggered(http2StartEvent); webSocketPipeline.fireUserEventTriggered(startEvent); + webSocketPipeline.fireUserEventTriggered(http2SuccessEvent); webSocketPipeline.fireUserEventTriggered(successEvent); } @@ -134,16 +155,20 @@ static void fireHandshakeStart( Http2WebSocketChannel webSocketChannel, Http2Headers requestHeaders, long timestampNanos) { ChannelPipeline parentPipeline = webSocketChannel.parent().pipeline(); ChannelPipeline webSocketPipeline = webSocketChannel.pipeline(); + int serial = webSocketChannel.serial(); + String path = webSocketChannel.path(); + String subprotocol = webSocketChannel.subprotocol(); - Http2WebSocketHandshakeStartEvent startEvent = + Http2WebSocketHandshakeStartEvent http2StartEvent = new Http2WebSocketHandshakeStartEvent( - webSocketChannel.serial(), - webSocketChannel.path(), - webSocketChannel.subprotocol(), - timestampNanos, - requestHeaders); + serial, path, subprotocol, timestampNanos, requestHeaders); + + WebSocketHandshakeStartEvent startEvent = + new WebSocketHandshakeStartEvent(serial, path, subprotocol, timestampNanos, requestHeaders); + parentPipeline.fireUserEventTriggered(http2StartEvent); parentPipeline.fireUserEventTriggered(startEvent); + webSocketPipeline.fireUserEventTriggered(http2StartEvent); webSocketPipeline.fireUserEventTriggered(startEvent); } @@ -155,19 +180,22 @@ static void fireHandshakeError( ChannelPipeline parentPipeline = webSocketChannel.parent().pipeline(); if (t instanceof Exception) { - String path = webSocketChannel.path(); ChannelPipeline webSocketPipeline = webSocketChannel.pipeline(); + String path = webSocketChannel.path(); + int serial = webSocketChannel.serial(); + String subprotocol = webSocketChannel.subprotocol(); - Http2WebSocketHandshakeErrorEvent errorEvent = + Http2WebSocketHandshakeErrorEvent http2ErrorEvent = new Http2WebSocketHandshakeErrorEvent( - webSocketChannel.serial(), - path, - webSocketChannel.subprotocol(), - timestampNanos, - responseHeaders, - t); + serial, path, subprotocol, timestampNanos, responseHeaders, t); + WebSocketHandshakeErrorEvent errorEvent = + new WebSocketHandshakeErrorEvent( + serial, path, subprotocol, timestampNanos, responseHeaders, t); + + parentPipeline.fireUserEventTriggered(http2ErrorEvent); parentPipeline.fireUserEventTriggered(errorEvent); + webSocketPipeline.fireUserEventTriggered(http2ErrorEvent); webSocketPipeline.fireUserEventTriggered(errorEvent); return; } @@ -176,21 +204,23 @@ static void fireHandshakeError( static void fireHandshakeSuccess( Http2WebSocketChannel webSocketChannel, Http2Headers responseHeaders, long timestampNanos) { - String path = webSocketChannel.path(); - String subprotocol = webSocketChannel.subprotocol(); ChannelPipeline parentPipeline = webSocketChannel.parent().pipeline(); ChannelPipeline webSocketPipeline = webSocketChannel.pipeline(); + String path = webSocketChannel.path(); + String subprotocol = webSocketChannel.subprotocol(); + int serial = webSocketChannel.serial(); - Http2WebSocketHandshakeSuccessEvent successEvent = + Http2WebSocketHandshakeSuccessEvent http2SuccessEvent = new Http2WebSocketHandshakeSuccessEvent( - webSocketChannel.serial(), - path, - subprotocol, - subprotocol, - timestampNanos, - responseHeaders); + serial, path, subprotocol, subprotocol, timestampNanos, responseHeaders); + + WebSocketHandshakeSuccessEvent successEvent = + new WebSocketHandshakeSuccessEvent( + serial, path, subprotocol, subprotocol, timestampNanos, responseHeaders); + parentPipeline.fireUserEventTriggered(http2SuccessEvent); parentPipeline.fireUserEventTriggered(successEvent); + webSocketPipeline.fireUserEventTriggered(http2SuccessEvent); webSocketPipeline.fireUserEventTriggered(successEvent); } diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/WebSocketEvent.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/WebSocketEvent.java new file mode 100644 index 0000000..9bcc398 --- /dev/null +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/WebSocketEvent.java @@ -0,0 +1,155 @@ +/* + * Copyright 2024 - present Maksym Ostroverkhov. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jauntsdn.netty.handler.codec.http2.websocketx; + +import io.netty.handler.codec.Headers; +import javax.annotation.Nullable; + +/** Base type for transport agnostic websocket lifecycle events */ +public abstract class WebSocketEvent extends Http2WebSocketEvent.Http2WebSocketLifecycleEvent { + + WebSocketEvent( + Http2WebSocketEvent.Type type, + int id, + String path, + String subprotocols, + long timestampNanos) { + super(type, id, path, subprotocols, timestampNanos); + } + + /** websocket handshake start event */ + public static class WebSocketHandshakeStartEvent extends WebSocketEvent { + private final Headers requestHeaders; + + WebSocketHandshakeStartEvent( + int id, + String path, + String subprotocol, + long timestampNanos, + Headers requestHeaders) { + super(Type.HANDSHAKE_START, id, path, subprotocol, timestampNanos); + this.requestHeaders = requestHeaders; + } + + /** @return websocket request headers */ + public Headers requestHeaders() { + return requestHeaders; + } + } + + /** websocket handshake error event */ + public static class WebSocketHandshakeErrorEvent extends WebSocketEvent { + private final Headers responseHeaders; + private final String errorName; + private final String errorMessage; + private final Throwable error; + + WebSocketHandshakeErrorEvent( + int id, + String path, + String subprotocols, + long timestampNanos, + Headers responseHeaders, + Throwable error) { + this(id, path, subprotocols, timestampNanos, responseHeaders, error, null, null); + } + + WebSocketHandshakeErrorEvent( + int id, + String path, + String subprotocols, + long timestampNanos, + Headers responseHeaders, + String errorName, + String errorMessage) { + this(id, path, subprotocols, timestampNanos, responseHeaders, null, errorName, errorMessage); + } + + private WebSocketHandshakeErrorEvent( + int id, + String path, + String subprotocols, + long timestampNanos, + Headers 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 Headers responseHeaders() { + return responseHeaders; + } + + /** + * @return exception associated with failed websocket handshake. May be null, in this case + * {@link #errorName()} and {@link #errorMessage()} contain error details. + */ + @Nullable + 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 handshake success event */ + public static class WebSocketHandshakeSuccessEvent extends WebSocketEvent { + private final String subprotocol; + private final Headers responseHeaders; + + WebSocketHandshakeSuccessEvent( + int id, + String path, + String subprotocols, + String subprotocol, + long timestampNanos, + Headers responseHeaders) { + super(Type.HANDSHAKE_SUCCESS, id, path, subprotocols, timestampNanos); + this.subprotocol = subprotocol; + this.responseHeaders = responseHeaders; + } + + public String subprotocol() { + return subprotocol; + } + + /** @return response headers of succeeded websocket handshake */ + public Headers responseHeaders() { + return responseHeaders; + } + } +} diff --git a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ApplicationHandshakeTest.java b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ApplicationHandshakeTest.java index f5c55d1..0f3012c 100644 --- a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ApplicationHandshakeTest.java +++ b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ApplicationHandshakeTest.java @@ -19,11 +19,15 @@ import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeErrorEvent; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeStartEvent; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeSuccessEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.WebSocketEvent.WebSocketHandshakeErrorEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.WebSocketEvent.WebSocketHandshakeStartEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.WebSocketEvent.WebSocketHandshakeSuccessEvent; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.Headers; import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig; import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; import io.netty.handler.codec.http2.DefaultHttp2Headers; @@ -72,7 +76,7 @@ void knownPathAccepted() throws Exception { .sync() .channel(); - WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(2); + WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(4); SocketAddress address = server.localAddress(); client = createClient( @@ -96,17 +100,31 @@ void knownPathAccepted() throws Exception { Assertions.assertThat(handshake.isSuccess()).isTrue(); eventsRecorder.eventsReceived().await(5, TimeUnit.SECONDS); + List events = eventsRecorder.events(); - Assertions.assertThat(events).hasSize(2); - Http2WebSocketEvent startEvent = events.get(0); - Http2WebSocketEvent successEvent = events.get(1); - Assertions.assertThat(startEvent).isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); - Assertions.assertThat(startEvent.cast().path()) + Assertions.assertThat(events).hasSize(4); + Http2WebSocketEvent http2startEvent = events.get(0); + Http2WebSocketEvent startEvent = events.get(1); + Http2WebSocketEvent http2SuccessEvent = events.get(2); + Http2WebSocketEvent successEvent = events.get(3); + + Assertions.assertThat(http2startEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); + Assertions.assertThat(http2startEvent.cast().path()) + .isEqualTo("/test"); + + Assertions.assertThat(startEvent).isExactlyInstanceOf(WebSocketHandshakeStartEvent.class); + Assertions.assertThat(startEvent.cast().path()) .isEqualTo("/test"); - Assertions.assertThat(successEvent) + + Assertions.assertThat(http2SuccessEvent) .isExactlyInstanceOf(Http2WebSocketHandshakeSuccessEvent.class); - Assertions.assertThat(startEvent.cast().id()) - .isEqualTo(successEvent.cast().id()); + Assertions.assertThat(http2startEvent.cast().id()) + .isEqualTo(http2SuccessEvent.cast().id()); + + Assertions.assertThat(successEvent).isExactlyInstanceOf(WebSocketHandshakeSuccessEvent.class); + Assertions.assertThat(startEvent.cast().id()) + .isEqualTo(successEvent.cast().id()); } @Test @@ -127,7 +145,7 @@ void unknownPathRejected() throws Exception { .sync() .channel(); - WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(2); + WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(4); SocketAddress address = server.localAddress(); client = createClient( @@ -156,15 +174,30 @@ void unknownPathRejected() throws Exception { Assertions.assertThat(webSocketChannel.isOpen()).isFalse(); eventsRecorder.eventsReceived().await(5, TimeUnit.SECONDS); + List events = eventsRecorder.events(); - Assertions.assertThat(events).hasSize(2); - Http2WebSocketEvent startEvent = events.get(0); - Http2WebSocketEvent errorEvent = events.get(1); - Assertions.assertThat(startEvent).isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); - Assertions.assertThat(startEvent.cast().path()) + Assertions.assertThat(events).hasSize(4); + Http2WebSocketEvent http2StartEvent = events.get(0); + Http2WebSocketEvent startEvent = events.get(1); + Http2WebSocketEvent http2ErrorEvent = events.get(2); + Http2WebSocketEvent errorEvent = events.get(3); + + Assertions.assertThat(http2StartEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); + Assertions.assertThat(http2StartEvent.cast().path()) .isEqualTo("/test"); - Assertions.assertThat(errorEvent).isExactlyInstanceOf(Http2WebSocketHandshakeErrorEvent.class); - Assertions.assertThat(errorEvent.cast().error()) + + Assertions.assertThat(startEvent).isExactlyInstanceOf(WebSocketHandshakeStartEvent.class); + Assertions.assertThat(startEvent.cast().path()) + .isEqualTo("/test"); + + Assertions.assertThat(http2ErrorEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeErrorEvent.class); + Assertions.assertThat(http2ErrorEvent.cast().error()) + .isExactlyInstanceOf(WebSocketHandshakeException.class); + + Assertions.assertThat(errorEvent).isExactlyInstanceOf(WebSocketHandshakeErrorEvent.class); + Assertions.assertThat(errorEvent.cast().error()) .isExactlyInstanceOf(WebSocketHandshakeException.class); } @@ -186,7 +219,7 @@ void handshakeTimeout() throws Exception { .sync() .channel(); - WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(2); + WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(4); SocketAddress address = server.localAddress(); client = createClient( @@ -217,14 +250,28 @@ void handshakeTimeout() throws Exception { eventsRecorder.eventsReceived().await(5, TimeUnit.SECONDS); List events = eventsRecorder.events(); - Assertions.assertThat(events).hasSize(2); - Http2WebSocketEvent startEvent = events.get(0); - Http2WebSocketEvent errorEvent = events.get(1); - Assertions.assertThat(startEvent).isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); - Assertions.assertThat(startEvent.cast().path()) + Assertions.assertThat(events).hasSize(4); + Http2WebSocketEvent http2StartEvent = events.get(0); + Http2WebSocketEvent startEvent = events.get(1); + Http2WebSocketEvent http2ErrorEvent = events.get(2); + Http2WebSocketEvent errorEvent = events.get(3); + + Assertions.assertThat(http2StartEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); + Assertions.assertThat(http2StartEvent.cast().path()) .isEqualTo("/test"); - Assertions.assertThat(errorEvent).isExactlyInstanceOf(Http2WebSocketHandshakeErrorEvent.class); - Assertions.assertThat(errorEvent.cast().error()) + + Assertions.assertThat(startEvent).isExactlyInstanceOf(WebSocketHandshakeStartEvent.class); + Assertions.assertThat(startEvent.cast().path()) + .isEqualTo("/test"); + + Assertions.assertThat(http2ErrorEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeErrorEvent.class); + Assertions.assertThat(http2ErrorEvent.cast().error()) + .isExactlyInstanceOf(TimeoutException.class); + + Assertions.assertThat(errorEvent).isExactlyInstanceOf(WebSocketHandshakeErrorEvent.class); + Assertions.assertThat(errorEvent.cast().error()) .isExactlyInstanceOf(TimeoutException.class); } @@ -249,7 +296,7 @@ void serverAcceptorAccept() throws Exception { .sync() .channel(); - WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(2); + WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(4); SocketAddress address = server.localAddress(); client = createClient( @@ -277,17 +324,31 @@ void serverAcceptorAccept() throws Exception { eventsRecorder.eventsReceived().await(5, TimeUnit.SECONDS); List events = eventsRecorder.events(); - Assertions.assertThat(events).hasSize(2); - Http2WebSocketEvent startEvent = events.get(0); - Http2WebSocketEvent successEvent = events.get(1); - Assertions.assertThat(startEvent).isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); - Assertions.assertThat(startEvent.cast().path()) + Assertions.assertThat(events).hasSize(4); + Http2WebSocketEvent http2StartEvent = events.get(0); + Http2WebSocketEvent startEvent = events.get(1); + Http2WebSocketEvent http2SuccessEvent = events.get(2); + Http2WebSocketEvent successEvent = events.get(3); + + Assertions.assertThat(http2StartEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); + Assertions.assertThat(http2StartEvent.cast().path()) .isEqualTo("/test"); - Assertions.assertThat(successEvent) - .isExactlyInstanceOf(Http2WebSocketHandshakeSuccessEvent.class); - Http2Headers responseHeaders = - successEvent.cast().responseHeaders(); + Assertions.assertThat(startEvent).isExactlyInstanceOf(WebSocketHandshakeStartEvent.class); + Assertions.assertThat(startEvent.cast().path()) + .isEqualTo("/test"); + + Assertions.assertThat(http2SuccessEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeSuccessEvent.class); + Http2Headers http2ResponseHeaders = + http2SuccessEvent.cast().responseHeaders(); + Assertions.assertThat(http2ResponseHeaders.contains("x-request-id")).isTrue(); + Assertions.assertThat(http2ResponseHeaders.get(":status")).isEqualTo(AsciiString.of("200")); + + Assertions.assertThat(successEvent).isExactlyInstanceOf(WebSocketHandshakeSuccessEvent.class); + Headers responseHeaders = + successEvent.cast().responseHeaders(); Assertions.assertThat(responseHeaders.contains("x-request-id")).isTrue(); Assertions.assertThat(responseHeaders.get(":status")).isEqualTo(AsciiString.of("200")); } @@ -313,7 +374,7 @@ void serverAcceptorReject() throws Exception { .sync() .channel(); - WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(2); + WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(4); SocketAddress address = server.localAddress(); client = createClient( @@ -342,16 +403,30 @@ void serverAcceptorReject() throws Exception { eventsRecorder.eventsReceived().await(5, TimeUnit.SECONDS); List events = eventsRecorder.events(); - Assertions.assertThat(events).hasSize(2); - Http2WebSocketEvent startEvent = events.get(0); - Http2WebSocketEvent errorEvent = events.get(1); - Assertions.assertThat(startEvent).isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); - Assertions.assertThat(startEvent.cast().path()) + Assertions.assertThat(events).hasSize(4); + Http2WebSocketEvent http2startEvent = events.get(0); + Http2WebSocketEvent startEvent = events.get(1); + Http2WebSocketEvent http2ErrorEvent = events.get(2); + Http2WebSocketEvent errorEvent = events.get(3); + + Assertions.assertThat(http2startEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); + Assertions.assertThat(http2startEvent.cast().path()) .isEqualTo("/test"); - Assertions.assertThat(errorEvent).isExactlyInstanceOf(Http2WebSocketHandshakeErrorEvent.class); - Http2Headers responseHeaders = - errorEvent.cast().responseHeaders(); + Assertions.assertThat(startEvent).isExactlyInstanceOf(WebSocketHandshakeStartEvent.class); + Assertions.assertThat(startEvent.cast().path()) + .isEqualTo("/test"); + + Assertions.assertThat(http2ErrorEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeErrorEvent.class); + Http2Headers http2ResponseHeaders = + http2ErrorEvent.cast().responseHeaders(); + Assertions.assertThat(http2ResponseHeaders.get(":status")).isEqualTo(AsciiString.of("400")); + + Assertions.assertThat(errorEvent).isExactlyInstanceOf(WebSocketHandshakeErrorEvent.class); + Headers responseHeaders = + errorEvent.cast().responseHeaders(); Assertions.assertThat(responseHeaders.get(":status")).isEqualTo(AsciiString.of("400")); } @@ -377,7 +452,7 @@ void knownSubprotocolAccepted() throws Exception { .sync() .channel(); - WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(2); + WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(4); SocketAddress address = server.localAddress(); client = createClient( @@ -402,17 +477,30 @@ void knownSubprotocolAccepted() throws Exception { eventsRecorder.eventsReceived().await(5, TimeUnit.SECONDS); List events = eventsRecorder.events(); - Assertions.assertThat(events).hasSize(2); - Http2WebSocketEvent startEvent = events.get(0); - Http2WebSocketEvent successEvent = events.get(1); - Assertions.assertThat(startEvent).isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); - Assertions.assertThat(startEvent.cast().path()) + Assertions.assertThat(events).hasSize(4); + Http2WebSocketEvent http2StartEvent = events.get(0); + Http2WebSocketEvent startEvent = events.get(1); + Http2WebSocketEvent http2SuccessEvent = events.get(2); + Http2WebSocketEvent successEvent = events.get(3); + + Assertions.assertThat(http2StartEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); + Assertions.assertThat(http2StartEvent.cast().path()) .isEqualTo("/test"); - Assertions.assertThat(successEvent) + + Assertions.assertThat(startEvent).isExactlyInstanceOf(WebSocketHandshakeStartEvent.class); + Assertions.assertThat(startEvent.cast().path()) + .isEqualTo("/test"); + + Assertions.assertThat(http2SuccessEvent) .isExactlyInstanceOf(Http2WebSocketHandshakeSuccessEvent.class); + Http2Headers http2responseHeaders = + http2SuccessEvent.cast().responseHeaders(); + Assertions.assertThat(http2responseHeaders.get(":status")).isEqualTo(AsciiString.of("200")); - Http2Headers responseHeaders = - successEvent.cast().responseHeaders(); + Assertions.assertThat(successEvent).isExactlyInstanceOf(WebSocketHandshakeSuccessEvent.class); + Headers responseHeaders = + successEvent.cast().responseHeaders(); Assertions.assertThat(responseHeaders.get(":status")).isEqualTo(AsciiString.of("200")); } @@ -438,7 +526,7 @@ void unknownSubprotocolRejected() throws Exception { .sync() .channel(); - WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(2); + WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(4); SocketAddress address = server.localAddress(); client = createClient( @@ -466,18 +554,34 @@ void unknownSubprotocolRejected() throws Exception { eventsRecorder.eventsReceived().await(5, TimeUnit.SECONDS); List events = eventsRecorder.events(); - Assertions.assertThat(events).hasSize(2); - Http2WebSocketEvent startEvent = events.get(0); - Http2WebSocketEvent errorEvent = events.get(1); - Assertions.assertThat(startEvent).isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); - Assertions.assertThat(startEvent.cast().path()) + Assertions.assertThat(events).hasSize(4); + Http2WebSocketEvent http2StartEvent = events.get(0); + Http2WebSocketEvent startEvent = events.get(1); + Http2WebSocketEvent http2ErrorEvent = events.get(2); + Http2WebSocketEvent errorEvent = events.get(3); + + Assertions.assertThat(http2StartEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); + Assertions.assertThat(http2StartEvent.cast().path()) .isEqualTo("/test"); - Assertions.assertThat(errorEvent).isExactlyInstanceOf(Http2WebSocketHandshakeErrorEvent.class); - Assertions.assertThat(errorEvent.cast().error()) + + Assertions.assertThat(startEvent).isExactlyInstanceOf(WebSocketHandshakeStartEvent.class); + Assertions.assertThat(startEvent.cast().path()) + .isEqualTo("/test"); + + Assertions.assertThat(http2ErrorEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeErrorEvent.class); + Assertions.assertThat(http2ErrorEvent.cast().error()) .isExactlyInstanceOf(WebSocketHandshakeException.class); + Http2Headers http2responseHeaders = + http2ErrorEvent.cast().responseHeaders(); + Assertions.assertThat(http2responseHeaders.get(":status")).isEqualTo(AsciiString.of("404")); - Http2Headers responseHeaders = - errorEvent.cast().responseHeaders(); + Assertions.assertThat(errorEvent).isExactlyInstanceOf(WebSocketHandshakeErrorEvent.class); + Assertions.assertThat(errorEvent.cast().error()) + .isExactlyInstanceOf(WebSocketHandshakeException.class); + Headers responseHeaders = + http2ErrorEvent.cast().responseHeaders(); Assertions.assertThat(responseHeaders.get(":status")).isEqualTo(AsciiString.of("404")); } @@ -506,7 +610,7 @@ void nonHandshakedSubprotocolRejected() throws Exception { .sync() .channel(); - WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(2); + WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(4); SocketAddress address = server.localAddress(); client = createClient( @@ -534,18 +638,34 @@ void nonHandshakedSubprotocolRejected() throws Exception { eventsRecorder.eventsReceived().await(5, TimeUnit.SECONDS); List events = eventsRecorder.events(); - Assertions.assertThat(events).hasSize(2); - Http2WebSocketEvent startEvent = events.get(0); - Http2WebSocketEvent errorEvent = events.get(1); - Assertions.assertThat(startEvent).isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); - Assertions.assertThat(startEvent.cast().path()) + Assertions.assertThat(events).hasSize(4); + Http2WebSocketEvent http2StartEvent = events.get(0); + Http2WebSocketEvent startEvent = events.get(1); + Http2WebSocketEvent http2ErrorEvent = events.get(2); + Http2WebSocketEvent errorEvent = events.get(3); + + Assertions.assertThat(http2StartEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); + Assertions.assertThat(http2StartEvent.cast().path()) + .isEqualTo("/test"); + + Assertions.assertThat(startEvent).isExactlyInstanceOf(WebSocketHandshakeStartEvent.class); + Assertions.assertThat(startEvent.cast().path()) .isEqualTo("/test"); - Assertions.assertThat(errorEvent).isExactlyInstanceOf(Http2WebSocketHandshakeErrorEvent.class); - Assertions.assertThat(errorEvent.cast().error()) + + Assertions.assertThat(http2ErrorEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeErrorEvent.class); + Assertions.assertThat(http2ErrorEvent.cast().error()) .isExactlyInstanceOf(WebSocketHandshakeException.class); + Http2Headers http2responseHeaders = + http2ErrorEvent.cast().responseHeaders(); + Assertions.assertThat(http2responseHeaders.get(":status")).isEqualTo(AsciiString.of("404")); - Http2Headers responseHeaders = - errorEvent.cast().responseHeaders(); + Assertions.assertThat(errorEvent).isExactlyInstanceOf(WebSocketHandshakeErrorEvent.class); + Assertions.assertThat(errorEvent.cast().error()) + .isExactlyInstanceOf(WebSocketHandshakeException.class); + Headers responseHeaders = + errorEvent.cast().responseHeaders(); Assertions.assertThat(responseHeaders.get(":status")).isEqualTo(AsciiString.of("404")); } @@ -572,7 +692,7 @@ void compressionAccepted() throws Exception { .sync() .channel(); - WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(2); + WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(4); SocketAddress address = server.localAddress(); client = createClient( @@ -602,17 +722,32 @@ void compressionAccepted() throws Exception { eventsRecorder.eventsReceived().await(5, TimeUnit.SECONDS); List events = eventsRecorder.events(); - Assertions.assertThat(events).hasSize(2); - Http2WebSocketEvent startEvent = events.get(0); - Http2WebSocketEvent successEvent = events.get(1); - Assertions.assertThat(startEvent).isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); - Assertions.assertThat(startEvent.cast().path()) + Assertions.assertThat(events).hasSize(4); + Http2WebSocketEvent http2StartEvent = events.get(0); + Http2WebSocketEvent startEvent = events.get(1); + Http2WebSocketEvent http2SuccessEvent = events.get(2); + Http2WebSocketEvent successEvent = events.get(3); + + Assertions.assertThat(http2StartEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); + Assertions.assertThat(http2StartEvent.cast().path()) .isEqualTo("/test"); - Assertions.assertThat(successEvent) + + Assertions.assertThat(startEvent).isExactlyInstanceOf(WebSocketHandshakeStartEvent.class); + Assertions.assertThat(startEvent.cast().path()) + .isEqualTo("/test"); + + Assertions.assertThat(http2SuccessEvent) .isExactlyInstanceOf(Http2WebSocketHandshakeSuccessEvent.class); + Http2Headers http2responseHeaders = + http2SuccessEvent.cast().responseHeaders(); + Assertions.assertThat(http2responseHeaders.get(":status")).isEqualTo(AsciiString.of("200")); + Assertions.assertThat(http2responseHeaders.get("sec-websocket-extensions")) + .isEqualTo(AsciiString.of("permessage-deflate")); - Http2Headers responseHeaders = - successEvent.cast().responseHeaders(); + Assertions.assertThat(successEvent).isExactlyInstanceOf(WebSocketHandshakeSuccessEvent.class); + Headers responseHeaders = + successEvent.cast().responseHeaders(); Assertions.assertThat(responseHeaders.get(":status")).isEqualTo(AsciiString.of("200")); Assertions.assertThat(responseHeaders.get("sec-websocket-extensions")) .isEqualTo(AsciiString.of("permessage-deflate")); @@ -638,7 +773,7 @@ void compressionRejected() throws Exception { .sync() .channel(); - WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(2); + WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(4); SocketAddress address = server.localAddress(); client = createClient( @@ -668,17 +803,31 @@ void compressionRejected() throws Exception { eventsRecorder.eventsReceived().await(5, TimeUnit.SECONDS); List events = eventsRecorder.events(); - Assertions.assertThat(events).hasSize(2); - Http2WebSocketEvent startEvent = events.get(0); - Http2WebSocketEvent successEvent = events.get(1); - Assertions.assertThat(startEvent).isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); - Assertions.assertThat(startEvent.cast().path()) + Assertions.assertThat(events).hasSize(4); + Http2WebSocketEvent http2StartEvent = events.get(0); + Http2WebSocketEvent startEvent = events.get(1); + Http2WebSocketEvent http2SuccessEvent = events.get(2); + Http2WebSocketEvent successEvent = events.get(3); + + Assertions.assertThat(http2StartEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); + Assertions.assertThat(http2StartEvent.cast().path()) .isEqualTo("/test"); - Assertions.assertThat(successEvent) - .isExactlyInstanceOf(Http2WebSocketHandshakeSuccessEvent.class); - Http2Headers responseHeaders = - successEvent.cast().responseHeaders(); + Assertions.assertThat(startEvent).isExactlyInstanceOf(WebSocketHandshakeStartEvent.class); + Assertions.assertThat(startEvent.cast().path()) + .isEqualTo("/test"); + + Assertions.assertThat(http2SuccessEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeSuccessEvent.class); + Http2Headers http2responseHeaders = + http2SuccessEvent.cast().responseHeaders(); + Assertions.assertThat(http2responseHeaders.get("sec-websocket-extensions")).isNullOrEmpty(); + Assertions.assertThat(http2responseHeaders.get(":status")).isEqualTo(AsciiString.of("200")); + + Assertions.assertThat(successEvent).isExactlyInstanceOf(WebSocketHandshakeSuccessEvent.class); + Headers responseHeaders = + successEvent.cast().responseHeaders(); Assertions.assertThat(responseHeaders.get("sec-websocket-extensions")).isNullOrEmpty(); Assertions.assertThat(responseHeaders.get(":status")).isEqualTo(AsciiString.of("200")); } @@ -705,7 +854,7 @@ void priorKnowledgeAccepted() throws Exception { .sync() .channel(); - WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(2); + WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(4); SocketAddress address = server.localAddress(); client = createClient( @@ -730,17 +879,30 @@ void priorKnowledgeAccepted() throws Exception { eventsRecorder.eventsReceived().await(5, TimeUnit.SECONDS); List events = eventsRecorder.events(); - Assertions.assertThat(events).hasSize(2); - Http2WebSocketEvent startEvent = events.get(0); - Http2WebSocketEvent successEvent = events.get(1); - Assertions.assertThat(startEvent).isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); - Assertions.assertThat(startEvent.cast().path()) + Assertions.assertThat(events).hasSize(4); + Http2WebSocketEvent http2startEvent = events.get(0); + Http2WebSocketEvent startEvent = events.get(1); + Http2WebSocketEvent http2successEvent = events.get(2); + Http2WebSocketEvent successEvent = events.get(3); + + Assertions.assertThat(http2startEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); + Assertions.assertThat(http2startEvent.cast().path()) .isEqualTo("/test"); - Assertions.assertThat(successEvent) + + Assertions.assertThat(startEvent).isExactlyInstanceOf(WebSocketHandshakeStartEvent.class); + Assertions.assertThat(startEvent.cast().path()) + .isEqualTo("/test"); + + Assertions.assertThat(http2successEvent) .isExactlyInstanceOf(Http2WebSocketHandshakeSuccessEvent.class); + Http2Headers http2responseHeaders = + http2successEvent.cast().responseHeaders(); + Assertions.assertThat(http2responseHeaders.get(":status")).isEqualTo(AsciiString.of("200")); - Http2Headers responseHeaders = - successEvent.cast().responseHeaders(); + Assertions.assertThat(successEvent).isExactlyInstanceOf(WebSocketHandshakeSuccessEvent.class); + Headers responseHeaders = + successEvent.cast().responseHeaders(); Assertions.assertThat(responseHeaders.get(":status")).isEqualTo(AsciiString.of("200")); } diff --git a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ProtocolHandshakeTest.java b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ProtocolHandshakeTest.java index 2acaf04..f0c70b1 100644 --- a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ProtocolHandshakeTest.java +++ b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/ProtocolHandshakeTest.java @@ -19,6 +19,9 @@ import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeErrorEvent; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeStartEvent; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeSuccessEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.WebSocketEvent.WebSocketHandshakeErrorEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.WebSocketEvent.WebSocketHandshakeStartEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.WebSocketEvent.WebSocketHandshakeSuccessEvent; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; @@ -75,7 +78,7 @@ void settingsEnableConnectAccepted() throws Exception { .sync() .channel(); - WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(2); + WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(4); SocketAddress address = server.localAddress(); client = createClient( @@ -100,14 +103,25 @@ void settingsEnableConnectAccepted() throws Exception { eventsRecorder.eventsReceived().await(5, TimeUnit.SECONDS); List events = eventsRecorder.events(); - Assertions.assertThat(events).hasSize(2); - Http2WebSocketEvent startEvent = events.get(0); - Http2WebSocketEvent successEvent = events.get(1); - Assertions.assertThat(startEvent).isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); - Assertions.assertThat(startEvent.cast().path()) + Assertions.assertThat(events).hasSize(4); + Http2WebSocketEvent http2startEvent = events.get(0); + Http2WebSocketEvent startEvent = events.get(1); + Http2WebSocketEvent http2successEvent = events.get(2); + Http2WebSocketEvent successEvent = events.get(3); + + Assertions.assertThat(http2startEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); + Assertions.assertThat(http2startEvent.cast().path()) + .isEqualTo("/test"); + + Assertions.assertThat(startEvent).isExactlyInstanceOf(WebSocketHandshakeStartEvent.class); + Assertions.assertThat(startEvent.cast().path()) .isEqualTo("/test"); - Assertions.assertThat(successEvent) + + Assertions.assertThat(http2successEvent) .isExactlyInstanceOf(Http2WebSocketHandshakeSuccessEvent.class); + + Assertions.assertThat(successEvent).isExactlyInstanceOf(WebSocketHandshakeSuccessEvent.class); } @Test @@ -125,7 +139,7 @@ void settingsNoEnableConnectRejected() throws Exception { SocketAddress address = server.localAddress(); SslContext clientSslContext = clientSslContext(); - WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(2); + WebsocketEventsHandler eventsRecorder = new WebsocketEventsHandler(4); client = createClient( address, @@ -153,14 +167,28 @@ void settingsNoEnableConnectRejected() throws Exception { eventsRecorder.eventsReceived().await(5, TimeUnit.SECONDS); List events = eventsRecorder.events(); - Assertions.assertThat(events).hasSize(2); - Http2WebSocketEvent startEvent = events.get(0); - Http2WebSocketEvent errorEvent = events.get(1); - Assertions.assertThat(startEvent).isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); - Assertions.assertThat(startEvent.cast().path()) + Assertions.assertThat(events).hasSize(4); + Http2WebSocketEvent http2startEvent = events.get(0); + Http2WebSocketEvent startEvent = events.get(1); + Http2WebSocketEvent http2errorEvent = events.get(2); + Http2WebSocketEvent errorEvent = events.get(3); + + Assertions.assertThat(http2startEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeStartEvent.class); + Assertions.assertThat(http2startEvent.cast().path()) .isEqualTo("/test"); - Assertions.assertThat(errorEvent).isExactlyInstanceOf(Http2WebSocketHandshakeErrorEvent.class); - Assertions.assertThat(errorEvent.cast().error()) + + Assertions.assertThat(startEvent).isExactlyInstanceOf(WebSocketHandshakeStartEvent.class); + Assertions.assertThat(startEvent.cast().path()) + .isEqualTo("/test"); + + Assertions.assertThat(http2errorEvent) + .isExactlyInstanceOf(Http2WebSocketHandshakeErrorEvent.class); + Assertions.assertThat(http2errorEvent.cast().error()) + .isExactlyInstanceOf(WebSocketHandshakeException.class); + + Assertions.assertThat(errorEvent).isExactlyInstanceOf(WebSocketHandshakeErrorEvent.class); + Assertions.assertThat(errorEvent.cast().error()) .isExactlyInstanceOf(WebSocketHandshakeException.class); } From abfb8c0ffcc4fff4cc336d66b97e6cb2f145b750 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Wed, 24 Jul 2024 10:43:08 +0300 Subject: [PATCH 11/14] ci: update os --- .github/workflows/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 369e507..111bd66 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - os: [ ubuntu-20.04, macos-11, windows-2019 ] + os: [ ubuntu-20.04, macos-12, windows-2019 ] jdk: [ 8, 11, 17, 21 ] fail-fast: false From 70998e992fde3a6caf9f9c5e3b81ddc0eb041620 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Wed, 24 Jul 2024 10:43:46 +0300 Subject: [PATCH 12/14] update dependencies --- gradle.properties | 4 ++-- .../gradle.lockfile | 20 +++++++++---------- netty-websocket-http2/gradle.lockfile | 18 ++++++++--------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/gradle.properties b/gradle.properties index 6817a2c..4029e2a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,8 +7,8 @@ gitPluginVersion=0.13.0 osDetectorPluginVersion=1.7.3 versionsPluginVersion=0.45.0 -nettyVersion=4.1.110.Final -jauntNettyWebsocketHttp1=1.1.4 +nettyVersion=4.1.112.Final +jauntNettyWebsocketHttp1=1.2.0 nettyTcnativeVersion=2.0.65.Final hdrHistogramVersion=2.1.12 slf4jVersion=1.7.36 diff --git a/netty-websocket-http2-callbacks-codec/gradle.lockfile b/netty-websocket-http2-callbacks-codec/gradle.lockfile index 1baec01..96720b0 100644 --- a/netty-websocket-http2-callbacks-codec/gradle.lockfile +++ b/netty-websocket-http2-callbacks-codec/gradle.lockfile @@ -7,15 +7,15 @@ com.google.errorprone:javac-shaded:9+181-r4173-1=googleJavaFormat1.6 com.google.googlejavaformat:google-java-format:1.6=googleJavaFormat1.6 com.google.guava:guava:22.0=googleJavaFormat1.6 com.google.j2objc:j2objc-annotations:1.1=googleJavaFormat1.6 -com.jauntsdn.netty:netty-websocket-http1:1.1.4=compileClasspath -io.netty:netty-buffer:4.1.110.Final=compileClasspath -io.netty:netty-codec-http2:4.1.110.Final=compileClasspath -io.netty:netty-codec-http:4.1.110.Final=compileClasspath -io.netty:netty-codec:4.1.110.Final=compileClasspath -io.netty:netty-common:4.1.110.Final=compileClasspath -io.netty:netty-handler:4.1.110.Final=compileClasspath -io.netty:netty-resolver:4.1.110.Final=compileClasspath -io.netty:netty-transport-native-unix-common:4.1.110.Final=compileClasspath -io.netty:netty-transport:4.1.110.Final=compileClasspath +com.jauntsdn.netty:netty-websocket-http1:1.2.0=compileClasspath +io.netty:netty-buffer:4.1.112.Final=compileClasspath +io.netty:netty-codec-http2:4.1.112.Final=compileClasspath +io.netty:netty-codec-http:4.1.112.Final=compileClasspath +io.netty:netty-codec:4.1.112.Final=compileClasspath +io.netty:netty-common:4.1.112.Final=compileClasspath +io.netty:netty-handler:4.1.112.Final=compileClasspath +io.netty:netty-resolver:4.1.112.Final=compileClasspath +io.netty:netty-transport-native-unix-common:4.1.112.Final=compileClasspath +io.netty:netty-transport:4.1.112.Final=compileClasspath org.codehaus.mojo:animal-sniffer-annotations:1.14=googleJavaFormat1.6 empty=annotationProcessor diff --git a/netty-websocket-http2/gradle.lockfile b/netty-websocket-http2/gradle.lockfile index a5000e0..20f08a8 100644 --- a/netty-websocket-http2/gradle.lockfile +++ b/netty-websocket-http2/gradle.lockfile @@ -9,17 +9,17 @@ com.google.errorprone:javac-shaded:9+181-r4173-1=googleJavaFormat1.6 com.google.googlejavaformat:google-java-format:1.6=googleJavaFormat1.6 com.google.guava:guava:22.0=googleJavaFormat1.6 com.google.j2objc:j2objc-annotations:1.1=googleJavaFormat1.6 -io.netty:netty-buffer:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-codec-http2:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-codec-http:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-codec:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-common:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-handler:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-resolver:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-buffer:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-http2:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-http:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-common:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-handler:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-resolver:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.netty:netty-tcnative-boringssl-static:2.0.65.Final=testRuntimeClasspath io.netty:netty-tcnative-classes:2.0.65.Final=testRuntimeClasspath -io.netty:netty-transport-native-unix-common:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-transport:4.1.110.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport-native-unix-common:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.11=testCompileClasspath,testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath org.assertj:assertj-core:3.25.3=testCompileClasspath,testRuntimeClasspath From f1dce3d94a4d8cc48f6995d7a3794c47e02a48d1 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 25 Jul 2024 06:51:01 +0300 Subject: [PATCH 13/14] add websocket multiprotocol support (http1, http2) (#17) --- README.md | 54 +- build.gradle | 1 + multiprotocol_callbacks_server.sh | 3 + multiprotocol_default_server.sh | 3 + multiprotocol_server.sh | 3 - netty-websocket-http2-example/build.gradle | 13 +- .../client}/Main.java | 2 +- .../server/callbackscodec/Main.java | 115 ++++ .../server/defaultcodec/Main.java | 81 +++ .../example/multiprotocolserver/Main.java | 212 ------- netty-websocket-multiprotocol/build.gradle | 50 ++ netty-websocket-multiprotocol/gradle.lockfile | 36 ++ .../MultiProtocolWebSocketServerHandler.java | 400 +++++++++++++ .../MultiprotocolWebSocketServerBuilder.java | 240 ++++++++ .../websocketx/multiprotocol/Security.java | 103 ++++ .../WebSocketMultiprotocolTest.java | 525 ++++++++++++++++++ .../src/test/resources/localhost.p12 | Bin 0 -> 4077 bytes .../src/test/resources/logback.xml | 13 + settings.gradle | 1 + 19 files changed, 1629 insertions(+), 226 deletions(-) create mode 100755 multiprotocol_callbacks_server.sh create mode 100755 multiprotocol_default_server.sh delete mode 100755 multiprotocol_server.sh rename netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/{multiprotocolclient => multiprotocol/client}/Main.java (99%) create mode 100644 netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/server/callbackscodec/Main.java create mode 100644 netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/server/defaultcodec/Main.java delete mode 100644 netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolserver/Main.java create mode 100644 netty-websocket-multiprotocol/build.gradle create mode 100644 netty-websocket-multiprotocol/gradle.lockfile create mode 100644 netty-websocket-multiprotocol/src/main/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/MultiProtocolWebSocketServerHandler.java create mode 100644 netty-websocket-multiprotocol/src/main/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/MultiprotocolWebSocketServerBuilder.java create mode 100644 netty-websocket-multiprotocol/src/test/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/Security.java create mode 100644 netty-websocket-multiprotocol/src/test/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/WebSocketMultiprotocolTest.java create mode 100644 netty-websocket-multiprotocol/src/test/resources/localhost.p12 create mode 100644 netty-websocket-multiprotocol/src/test/resources/logback.xml diff --git a/README.md b/README.md index 7a45935..7f36207 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,30 @@ [![Build](https://github.com/jauntsdn/netty-websocket-http2/actions/workflows/ci-build.yml/badge.svg)](https://github.com/jauntsdn/netty-websocket-http2/actions/workflows/ci-build.yml) # netty-websocket-http2 -Netty based implementation of [rfc8441](https://tools.ietf.org/html/rfc8441) - bootstrapping websockets with http/2 +Netty based implementation of [rfc8441](https://tools.ietf.org/html/rfc8441) - bootstrapping websockets with http/2, and multiprotocol support (websocket-over-http1, websocket-over-http2). -Library addresses 2 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. +### use cases + +* Websocket channel API + +for application servers and clients, It provides transparent use of existing http1 websocket handlers on top of http2 streams. Compatible with +callbacks codec (described below). + +* Websocket handshake only API + +for gateways/proxies, It provides websockets-over-http2 support with no http1 dependencies and minimal overhead. + +* Websocket multiprotocol + +for application servers, It provides transparent use of existing http1 websocket handlers to process both http1 and http2 websockets. +Compatible with callbacks codec (described below). [https://jauntsdn.com/post/netty-websocket-http2/](https://jauntsdn.com/post/netty-websocket-http2/) ### much faster http1 codec -Integration with [jauntsdn/netty-websocket-http1](https://github.com/jauntsdn/netty-websocket-http2/tree/develop/netty-websocket-http2-callbacks-codec) codec for websocket-http1 +Integration with [jauntsdn/netty-websocket-http1](https://github.com/jauntsdn/netty-websocket-http2/tree/develop/netty-websocket-http2-callbacks-codec) codec (callbacks codec) for websocket-http1 frames processing [improves](https://github.com/jauntsdn/netty-websocket-http2/tree/develop/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/callbackscodec) -throughput 1.4x - 1.7x for small messages. +throughput 1.4x - 1.7x for small messages compared to one provided by netty (default codec). ### websocket channel API Intended for application servers and clients. @@ -25,6 +37,7 @@ EchoWebSocketHandler http1WebSocketHandler = new EchoWebSocketHandler(); Http2WebSocketServerHandler http2webSocketHandler = Http2WebSocketServerBuilder.create() + .codec(Http1WebSocketCodec.DEFAULT) .acceptor( (ctx, path, subprotocols, request, response) -> { switch (path) { @@ -70,6 +83,7 @@ EchoWebSocketHandler http1WebSocketHandler = new EchoWebSocketHandler(); Http2WebSocketClientHandler http2WebSocketClientHandler = Http2WebSocketClientBuilder.create() + .codec(Http1WebSocketCodec.DEFAULT) .handshakeTimeoutMillis(15_000) .build(); @@ -127,6 +141,26 @@ 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). +### websocket multiprotocol +Provides transparent use of existing http1 websocket handlers to process both http1 and http2 websockets. + +* Server +```groovy + MultiProtocolWebSocketServerHandler multiprotocolHandler = + MultiprotocolWebSocketServerBuilder.create() + .path("/echo") + .subprotocols("echo.jauntsdn.com") + .defaultCodec() + .handler(new DefaultEchoWebSocketHandler()) + .build(); + ch.pipeline().addLast(sslHandler, multiprotocolHandler); +``` + +Runnable demo is available in `netty-websocket-http2-example` module - +[multiprotocol.server.defaultcodec](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/server/defaultcodec/Main.java), +[multiprotocol.server.callbackscodec](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/server/callbackscodec/Main.java), +[multiprotocol.client.defaultcodec](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/client/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. @@ -181,6 +215,12 @@ Events are fired on parent channel, also on websocket channel if one gets create * `Http2WebSocketHandshakeErrorEvent(webSocketId, path, subprotocols, timestampNanos, responseHeaders, error)` * `Http2WebSocketHandshakeSuccessEvent(webSocketId, path, subprotocols, timestampNanos, responseHeaders)` +These events are accompanied by transport agnostic variants + +* `WebSocketHandshakeStartEvent(websocketId, path, subprotocols, timestampNanos, requestHeaders)` +* `WebSocketHandshakeErrorEvent(webSocketId, path, subprotocols, timestampNanos, responseHeaders, error)` +* `WebSocketHandshakeSuccessEvent(webSocketId, path, subprotocols, timestampNanos, responseHeaders)` + #### close events Outbound `Http2WebSocketLocalCloseEvent` on websocket channel pipeline closes @@ -265,7 +305,7 @@ the results are as follows (measured over time spans of 5 seconds): * `channelserver, channelclient` packages for websocket subchannel API demos. * `handshakeserver, channelclient` packages for handshake only API demo. -* `multiprotocolserver, multiprotocolclient` packages for demo of server handling htt1/http2 websockets on the same port. +* `multiprotocol` packages for demo of server handling htt1/http2 websockets on the same port. * `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. diff --git a/build.gradle b/build.gradle index 301b079..e21b231 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,7 @@ subprojects { repositories { mavenCentral() + mavenLocal() } plugins.withType(JavaPlugin) { diff --git a/multiprotocol_callbacks_server.sh b/multiprotocol_callbacks_server.sh new file mode 100755 index 0000000..0b29659 --- /dev/null +++ b/multiprotocol_callbacks_server.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +./gradlew netty-websocket-http2-example:runMultiProtocolCallbacksServer \ No newline at end of file diff --git a/multiprotocol_default_server.sh b/multiprotocol_default_server.sh new file mode 100755 index 0000000..e98979c --- /dev/null +++ b/multiprotocol_default_server.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +./gradlew netty-websocket-http2-example:runMultiProtocolDefaultServer \ No newline at end of file diff --git a/multiprotocol_server.sh b/multiprotocol_server.sh deleted file mode 100755 index 5a30c39..0000000 --- a/multiprotocol_server.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -./gradlew netty-websocket-http2-example:runMultiProtocolServer \ No newline at end of file diff --git a/netty-websocket-http2-example/build.gradle b/netty-websocket-http2-example/build.gradle index 21afaca..718a311 100644 --- a/netty-websocket-http2-example/build.gradle +++ b/netty-websocket-http2-example/build.gradle @@ -22,6 +22,8 @@ description = "Netty based implementation of rfc8441 - bootstrapping websockets dependencies { implementation project(":netty-websocket-http2") + implementation project(":netty-websocket-multiprotocol") + implementation project(":netty-websocket-http2-callbacks-codec") implementation "org.slf4j:slf4j-api" runtimeOnly "io.netty:netty-tcnative-boringssl-static::${osdetector.classifier}" runtimeOnly "ch.qos.logback:logback-classic" @@ -37,9 +39,14 @@ task runChannelServer(type: JavaExec) { mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.channelserver.Main" } -task runMultiProtocolServer(type: JavaExec) { +task runMultiProtocolDefaultServer(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath - mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocolserver.Main" + mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocol.server.defaultcodec.Main" +} + +task runMultiProtocolCallbacksServer(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocol.server.callbackscodec.Main" } task runChannelClient(type: JavaExec) { @@ -54,6 +61,6 @@ task runLwsClient(type: JavaExec) { task runMultiProtocolClient(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath - mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocolclient.Main" + mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocol.client.Main" } diff --git a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolclient/Main.java b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/client/Main.java similarity index 99% rename from netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolclient/Main.java rename to netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/client/Main.java index 3b262ef..1dd612b 100644 --- a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolclient/Main.java +++ b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/client/Main.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocolclient; +package com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocol.client; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientBuilder; import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientHandler; diff --git a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/server/callbackscodec/Main.java b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/server/callbackscodec/Main.java new file mode 100644 index 0000000..ac93c3f --- /dev/null +++ b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/server/callbackscodec/Main.java @@ -0,0 +1,115 @@ +package com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocol.server.callbackscodec; + +import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketCallbacksHandler; +import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketFrameListener; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeSuccessEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.example.Security; +import com.jauntsdn.netty.handler.codec.websocketx.multiprotocol.MultiProtocolWebSocketServerHandler; +import com.jauntsdn.netty.handler.codec.websocketx.multiprotocol.MultiprotocolWebSocketServerBuilder; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler.HandshakeComplete; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.ReferenceCountUtil; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Main { + static final Logger logger = LoggerFactory.getLogger(Main.class); + + public static void main(String[] args) throws Exception { + String host = "localhost"; + int port = 8099; + SslContext sslContext = Security.serverSslContext("localhost.p12", "localhost"); + + Channel server = + new ServerBootstrap() + .group(new NioEventLoopGroup()) + .channel(NioServerSocketChannel.class) + .childHandler( + new ChannelInitializer() { + + @Override + protected void initChannel(SocketChannel ch) { + SslHandler sslHandler = sslContext.newHandler(ch.alloc()); + MultiProtocolWebSocketServerHandler multiprotocolHandler = + MultiprotocolWebSocketServerBuilder.create() + .path("/echo") + .subprotocols("echo.jauntsdn.com") + .callbacksCodec() + .handler( + new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast(new CallbacksServerHandler()); + } + }) + .build(); + ch.pipeline().addLast(sslHandler, multiprotocolHandler); + } + }) + .bind(host, port) + .sync() + .channel(); + logger.info("\n==> Websocket server (callbacks codec) is listening on {}:{}", host, port); + server.closeFuture().await(); + } + + private static class CallbacksServerHandler extends ChannelInboundHandlerAdapter { + + @Override + public void userEventTriggered(ChannelHandlerContext c, Object evt) throws Exception { + if (evt instanceof HandshakeComplete || evt instanceof Http2WebSocketHandshakeSuccessEvent) { + WebSocketCallbacksHandler.exchange( + c, + (ctx, webSocketFrameFactory) -> + new WebSocketFrameListener() { + @Override + public void onChannelRead( + ChannelHandlerContext context, + boolean finalFragment, + int rsv, + int opcode, + ByteBuf payload) { + ByteBuf textFrame = + webSocketFrameFactory.mask( + webSocketFrameFactory.createTextFrame( + ctx.alloc(), payload.readableBytes())); + textFrame.writeBytes(payload); + payload.release(); + ctx.write(textFrame); + } + + @Override + public void onChannelReadComplete(ChannelHandlerContext ctx1) { + ctx1.flush(); + } + + @Override + public void onExceptionCaught(ChannelHandlerContext ctx1, Throwable cause) { + if (cause instanceof IOException) { + return; + } + logger.error("Unexpected websocket error", cause); + ctx1.close(); + } + }); + } + super.userEventTriggered(c, evt); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ReferenceCountUtil.safeRelease(msg); + } + } +} diff --git a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/server/defaultcodec/Main.java b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/server/defaultcodec/Main.java new file mode 100644 index 0000000..ac3d078 --- /dev/null +++ b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/server/defaultcodec/Main.java @@ -0,0 +1,81 @@ +package com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocol.server.defaultcodec; + +import com.jauntsdn.netty.handler.codec.http2.websocketx.example.Security; +import com.jauntsdn.netty.handler.codec.websocketx.multiprotocol.MultiProtocolWebSocketServerHandler; +import com.jauntsdn.netty.handler.codec.websocketx.multiprotocol.MultiprotocolWebSocketServerBuilder; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Main { + static final Logger logger = LoggerFactory.getLogger(Main.class); + + public static void main(String[] args) throws Exception { + String host = "localhost"; + int port = 8099; + SslContext sslContext = Security.serverSslContext("localhost.p12", "localhost"); + + Channel server = + new ServerBootstrap() + .group(new NioEventLoopGroup()) + .channel(NioServerSocketChannel.class) + .childHandler( + new ChannelInitializer() { + + @Override + protected void initChannel(SocketChannel ch) { + SslHandler sslHandler = sslContext.newHandler(ch.alloc()); + MultiProtocolWebSocketServerHandler multiprotocolHandler = + MultiprotocolWebSocketServerBuilder.create() + .path("/echo") + .subprotocols("echo.jauntsdn.com") + .defaultCodec() + .handler(new DefaultEchoWebSocketHandler()) + .build(); + ch.pipeline().addLast(sslHandler, multiprotocolHandler); + } + }) + .bind(host, port) + .sync() + .channel(); + logger.info("\n==> Websocket server (default codec) is listening on {}:{}", host, port); + server.closeFuture().await(); + } + + @ChannelHandler.Sharable + private static class DefaultEchoWebSocketHandler + extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame webSocketFrame) { + ctx.write(webSocketFrame.retain()); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + super.channelReadComplete(ctx); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (cause instanceof IOException) { + return; + } + logger.error("Unexpected websocket error", cause); + ctx.close(); + } + } +} diff --git a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolserver/Main.java b/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolserver/Main.java deleted file mode 100644 index 6400ad1..0000000 --- a/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocolserver/Main.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright 2022 - present Maksym Ostroverkhov. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocolserver; - -import static io.netty.channel.ChannelHandler.Sharable; - -import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketAcceptor; -import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketServerBuilder; -import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketServerHandler; -import com.jauntsdn.netty.handler.codec.http2.websocketx.example.Security; -import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.handler.codec.http.HttpObjectAggregator; -import io.netty.handler.codec.http.HttpServerCodec; -import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; -import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; -import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; -import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; -import io.netty.handler.codec.http2.Http2FrameCodec; -import io.netty.handler.codec.http2.Http2FrameCodecBuilder; -import io.netty.handler.codec.http2.Http2Headers; -import io.netty.handler.ssl.ApplicationProtocolNames; -import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslHandler; -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class Main { - private static final Logger logger = LoggerFactory.getLogger(Main.class); - - public static void main(String[] args) throws Exception { - String host = System.getProperty("HOST", "localhost"); - int port = Integer.parseInt(System.getProperty("PORT", "8099")); - String echoPath = System.getProperty("PING", "echo"); - String keyStoreFile = System.getProperty("KEYSTORE", "localhost.p12"); - String keyStorePassword = System.getProperty("KEYSTORE_PASS", "localhost"); - - logger.info("\n==> http1/http2 websocket server\n"); - logger.info("\n==> Bind address: {}:{}", host, port); - logger.info("\n==> Keystore file: {}", keyStoreFile); - - SslContext sslContext = Security.serverSslContext(keyStoreFile, keyStorePassword); - - ServerBootstrap bootstrap = new ServerBootstrap(); - Channel server = - bootstrap - .group(new NioEventLoopGroup()) - .channel(NioServerSocketChannel.class) - .childHandler(new ConnectionAcceptor(sslContext)) - .bind(host, port) - .sync() - .channel(); - logger.info("\n==> Server is listening on {}:{}", host, port); - - logger.info("\n==> Echo path: {}", echoPath); - server.closeFuture().sync(); - } - - private static class ConnectionAcceptor extends ChannelInitializer { - private static final List SUPPORTED_USER_AGENTS = - Collections.singletonList("jauntsdn-websocket-http2-client/"); - private final SslContext sslContext; - - ConnectionAcceptor(SslContext sslContext) { - this.sslContext = sslContext; - } - - @Override - protected void initChannel(SocketChannel ch) { - SslHandler sslHandler = sslContext.newHandler(ch.alloc()); - ChannelHandler http1webSocketHandler = new EchoWebSocketHandler(); - ApplicationProtocolNegotiationHandler webSocketProtocolVersionHandler = - new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_1_1) { - @Override - protected void configurePipeline(ChannelHandlerContext c, String protocol) { - - switch (protocol) { - case ApplicationProtocolNames.HTTP_2: - logger.info("Server accepted TLS connection for websockets-over-http2"); - - Http2FrameCodecBuilder http2Builder = - Http2WebSocketServerBuilder.configureHttp2Server( - Http2FrameCodecBuilder.forServer()); - http2Builder.initialSettings().initialWindowSize(1_000); - Http2FrameCodec http2frameCodec = http2Builder.build(); - - Http2WebSocketServerHandler http2webSocketHandler = - Http2WebSocketServerBuilder.create() - .compression(true) - .acceptor( - (ctx, path, subprotocols, request, response) -> { - if ("/echo".equals(path)) { - if (subprotocols.contains("echo.jauntsdn.com") - && acceptUserAgent(request, response)) { - Http2WebSocketAcceptor.Subprotocol.accept( - "echo.jauntsdn.com", response); - return ctx.executor().newSucceededFuture(http1webSocketHandler); - } - } - return ctx.executor() - .newFailedFuture( - new WebSocketHandshakeException( - String.format( - "websocket rejected, path: %s, subprotocols: %s, user-agent: %s", - path, subprotocols, request.get("user-agent")))); - }) - .build(); - - ch.pipeline().addLast(http2frameCodec, http2webSocketHandler); - break; - - case ApplicationProtocolNames.HTTP_1_1: - logger.info("Server accepted TLS connection for websockets-over-http1"); - - HttpServerCodec http1Codec = new HttpServerCodec(); - HttpObjectAggregator http1Aggregator = new HttpObjectAggregator(65536); - WebSocketServerCompressionHandler http1WebSocketCompressor = - new WebSocketServerCompressionHandler(); - WebSocketServerProtocolHandler http1WebSocketProtocolHandler = - new WebSocketServerProtocolHandler("/echo", "echo.jauntsdn.com", true); - - ch.pipeline() - .addLast(http1Codec) - .addLast(http1Aggregator) - .addLast(http1WebSocketCompressor) - .addLast(http1WebSocketProtocolHandler) - .addLast(http1webSocketHandler); - break; - - default: - logger.info("Unsupported protocol for TLS connection: {}", protocol); - c.close(); - } - } - }; - - ch.pipeline().addLast(sslHandler, webSocketProtocolVersionHandler); - } - - private boolean acceptUserAgent(Http2Headers request, Http2Headers response) { - CharSequence userAgentSeq = request.get("user-agent"); - if (userAgentSeq == null || userAgentSeq.length() == 0) { - return false; - } - String userAgent = userAgentSeq.toString(); - for (String supportedUserAgent : SUPPORTED_USER_AGENTS) { - int index = userAgent.indexOf(supportedUserAgent); - if (index >= 0) { - int length = supportedUserAgent.length(); - String version = userAgent.substring(index + length); - String clientId = supportedUserAgent.substring(0, length - 1); - request.set("x-client-id", clientId); - request.set("x-client-version", version); - response.set("x-request-id", UUID.randomUUID().toString()); - return true; - } - } - return false; - } - } - - @Sharable - private static class EchoWebSocketHandler - extends SimpleChannelInboundHandler { - - @Override - protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame webSocketFrame) { - ctx.write(webSocketFrame.retain()); - } - - @Override - public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - ctx.flush(); - super.channelReadComplete(ctx); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - if (cause instanceof IOException) { - return; - } - logger.error("Unexpected websocket error", cause); - ctx.close(); - } - } -} diff --git a/netty-websocket-multiprotocol/build.gradle b/netty-websocket-multiprotocol/build.gradle new file mode 100644 index 0000000..bbfe5d4 --- /dev/null +++ b/netty-websocket-multiprotocol/build.gradle @@ -0,0 +1,50 @@ +/* + * Copyright 2020 - present Maksym Ostroverkhov. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id "java-library" + id "maven-publish" + id "signing" +} + +description = "Netty support for multiprotocol websockets: http1, http2" + +dependencies { + api project(":netty-websocket-http2") + implementation "org.slf4j:slf4j-api" + compileOnly project(":netty-websocket-http2-callbacks-codec") + compileOnly "com.google.code.findbugs:jsr305" + + testImplementation "org.assertj:assertj-core" + testImplementation "org.junit.jupiter:junit-jupiter-api" + testImplementation "org.junit.jupiter:junit-jupiter-params" + testImplementation project(":netty-websocket-http2-callbacks-codec") + + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" + testRuntimeOnly "ch.qos.logback:logback-classic" + testRuntimeOnly "io.netty:netty-tcnative-classes" + testRuntimeOnly "io.netty:netty-tcnative-boringssl-static::${osdetector.classifier}" +} + +dependencyLocking { + lockAllConfigurations() +} + +tasks.named("jar") { + manifest { + attributes("Automatic-Module-Name": "com.jauntsdn.netty.websocket.multiprotocol") + } +} diff --git a/netty-websocket-multiprotocol/gradle.lockfile b/netty-websocket-multiprotocol/gradle.lockfile new file mode 100644 index 0000000..6651cac --- /dev/null +++ b/netty-websocket-multiprotocol/gradle.lockfile @@ -0,0 +1,36 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +ch.qos.logback:logback-classic:1.2.13=testRuntimeClasspath +ch.qos.logback:logback-core:1.2.13=testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,googleJavaFormat1.6 +com.google.errorprone:error_prone_annotations:2.0.18=googleJavaFormat1.6 +com.google.errorprone:javac-shaded:9+181-r4173-1=googleJavaFormat1.6 +com.google.googlejavaformat:google-java-format:1.6=googleJavaFormat1.6 +com.google.guava:guava:22.0=googleJavaFormat1.6 +com.google.j2objc:j2objc-annotations:1.1=googleJavaFormat1.6 +com.jauntsdn.netty:netty-websocket-http1:1.2.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-buffer:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-http2:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-http:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-common:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-handler:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-resolver:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-tcnative-boringssl-static:2.0.65.Final=testRuntimeClasspath +io.netty:netty-tcnative-classes:2.0.65.Final=testRuntimeClasspath +io.netty:netty-transport-native-unix-common:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport:4.1.112.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.11=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.assertj:assertj-core:3.25.3=testCompileClasspath,testRuntimeClasspath +org.codehaus.mojo:animal-sniffer-annotations:1.14=googleJavaFormat1.6 +org.junit.jupiter:junit-jupiter-api:5.10.2=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.2=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.2=testRuntimeClasspath +org.junit:junit-bom:5.10.2=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:1.7.36=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor,testAnnotationProcessor diff --git a/netty-websocket-multiprotocol/src/main/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/MultiProtocolWebSocketServerHandler.java b/netty-websocket-multiprotocol/src/main/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/MultiProtocolWebSocketServerHandler.java new file mode 100644 index 0000000..db49b37 --- /dev/null +++ b/netty-websocket-multiprotocol/src/main/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/MultiProtocolWebSocketServerHandler.java @@ -0,0 +1,400 @@ +/* + * Copyright 2024 - present Maksym Ostroverkhov. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jauntsdn.netty.handler.codec.websocketx.multiprotocol; + +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http1WebSocketCodec; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketAcceptor; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketServerBuilder; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketServerHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.http.HttpDecoderConfig; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig; +import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; +import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandler; +import io.netty.handler.codec.http.websocketx.extensions.compression.DeflateFrameServerExtensionHandshaker; +import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Handler to process both websocket-over-http1 and websocket-over-http2 protocols */ +public final class MultiProtocolWebSocketServerHandler extends ChannelInitializer { + private static final Logger logger = + LoggerFactory.getLogger(MultiProtocolWebSocketServerHandler.class); + + private final Http1WebSocketCodec webSocketCodec; + private final WebSocketDecoderConfig webSocketDecoderConfig; + private final HttpDecoderConfig http1Config; + private final Http2Config http2Config; + private final CompressionConfig compression; + private final String path; + private final String subprotocols; + private final Set subprotocolSet; + private final long handshakeTimeoutMillis; + private final ChannelHandler webSocketHandler; + + MultiProtocolWebSocketServerHandler( + Http1WebSocketCodec webSocketCodec, + WebSocketDecoderConfig webSocketDecoderConfig, + HttpDecoderConfig http1Config, + Http2Config http2Config, + CompressionConfig compression, + String path, + @Nullable String subprotocols, + long handshakeTimeoutMillis, + ChannelHandler webSocketHandler) { + this.webSocketCodec = webSocketCodec; + this.webSocketDecoderConfig = webSocketDecoderConfig; + this.http1Config = http1Config; + this.http2Config = http2Config; + this.compression = compression; + this.path = path; + this.subprotocols = subprotocols; + this.subprotocolSet = parseSubprotocols(subprotocols); + this.handshakeTimeoutMillis = handshakeTimeoutMillis; + this.webSocketHandler = webSocketHandler; + } + + @Override + protected void initChannel(SocketChannel ch) { + ChannelHandler webSocketHandler = this.webSocketHandler; + ApplicationProtocolNegotiationHandler alpnHandler = + new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_1_1) { + @Override + protected void configurePipeline(ChannelHandlerContext c, String protocol) { + + switch (protocol) { + case ApplicationProtocolNames.HTTP_2: + { + Http2Config h2Config = http2Config; + Http2FrameCodecBuilder http2FrameCodecBuilder = + applyConfig(Http2FrameCodecBuilder.forServer(), h2Config); + Http2FrameCodec http2frameCodec = + Http2WebSocketServerBuilder.configureHttp2Server(http2FrameCodecBuilder) + .build(); + + Http2WebSocketServerBuilder http2webSocketBuilder = + applyConfig( + Http2WebSocketServerBuilder.create() + .codec(webSocketCodec) + .decoderConfig(webSocketDecoderConfig), + h2Config); + + CompressionConfig compr = compression; + if (compr != null) { + http2webSocketBuilder.compression( + compr.compressionLevel, + compr.allowServerWindowSize, + compr.preferredClientWindowSize, + compr.allowServerNoContext, + compr.preferredClientNoContext); + } + + String webSocketPath = path; + Http2WebSocketServerHandler http2WebSocketHandler = + http2webSocketBuilder + .acceptor( + (ctx, path, requestedSubprotocols, request, response) -> { + if (webSocketPath.equals(path)) { + String subprotocol = + selectSubprotocol(requestedSubprotocols, subprotocolSet); + if (subprotocol != null) { + if (!subprotocol.isEmpty()) { + Http2WebSocketAcceptor.Subprotocol.accept( + subprotocol, response); + } + return ctx.executor().newSucceededFuture(webSocketHandler); + } + } + return ctx.executor() + .newFailedFuture( + new WebSocketHandshakeException( + String.format( + "websocket rejected, path: %s, subprotocols: %s", + path, requestedSubprotocols))); + }) + .build(); + + ch.pipeline().addLast(http2frameCodec, http2WebSocketHandler); + break; + } + + case ApplicationProtocolNames.HTTP_1_1: + { + logger.debug("Server accepted TLS connection for websockets-over-http1"); + + ChannelPipeline pipeline = ch.pipeline(); + + HttpServerCodec http1Codec = createWithConfig(http1Config); + HttpObjectAggregator http1Aggregator = + new HttpObjectAggregator(/*maxContentLength*/ 65536); + pipeline.addLast(http1Codec, http1Aggregator); + + if (webSocketCodec == Http1WebSocketCodec.DEFAULT) { + CompressionConfig compr = compression; + if (compr != null) { + WebSocketServerExtensionHandler http1WebSocketCompressor = + new ConfigurableWebSocketCompressionHandler( + compr.compressionLevel, + compr.allowServerWindowSize, + compr.preferredClientWindowSize, + compr.allowServerNoContext, + compr.preferredClientNoContext); + + pipeline.addLast(http1WebSocketCompressor); + } + io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler + defaultWebSocketProtocolHandler = + new io.netty.handler.codec.http.websocketx + .WebSocketServerProtocolHandler( + path, + subprotocols, + false, + false, + handshakeTimeoutMillis, + webSocketDecoderConfig); + pipeline.addLast(defaultWebSocketProtocolHandler); + } else { + com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler + callbacksWebSocketProtocolHandler = + com.jauntsdn.netty.handler.codec.http.websocketx + .WebSocketServerProtocolHandler.create() + .path(path) + .subprotocols(subprotocols) + .handshakeTimeoutMillis(handshakeTimeoutMillis) + .decoderConfig(webSocketDecoderConfig) + .build(); + pipeline.addLast(callbacksWebSocketProtocolHandler); + } + pipeline.addLast(webSocketHandler); + break; + } + default: + logger.info("Unsupported protocol for TLS connection: {}", protocol); + c.close(); + } + } + }; + + ch.pipeline().addLast(alpnHandler); + } + + static Http2FrameCodecBuilder applyConfig( + Http2FrameCodecBuilder http2frameCodecBuilder, Http2Config http2Config) { + if (http2Config != null) { + http2frameCodecBuilder + .autoAckPingFrame(http2Config.autoAckPingFrame) + .autoAckSettingsFrame(http2Config.autoAckSettingsFrame) + .validateHeaders(http2Config.validateHeaders) + .gracefulShutdownTimeoutMillis(http2Config.gracefulShutdownTimeoutMillis); + try { + http2Config.http2Settings.accept(http2frameCodecBuilder.initialSettings()); + } catch (Exception e) { + logger.error("Error while applying http2 settings to Http2FrameCodecBuilder", e); + } + } + return http2frameCodecBuilder; + } + + static Http2WebSocketServerBuilder applyConfig( + Http2WebSocketServerBuilder http2WebSocketBuilder, Http2Config http2Config) { + if (http2Config != null) { + http2WebSocketBuilder.closedWebSocketRemoveTimeout( + http2Config.closedWebSocketRemoveTimeoutMillis); + } + return http2WebSocketBuilder; + } + + static HttpServerCodec createWithConfig(HttpDecoderConfig http1Config) { + if (http1Config != null) { + return new HttpServerCodec(http1Config); + } + return new HttpServerCodec(); + } + + static final class CompressionConfig { + final int compressionLevel; + final boolean allowServerWindowSize; + final int preferredClientWindowSize; + final boolean allowServerNoContext; + final boolean preferredClientNoContext; + + CompressionConfig( + int compressionLevel, + boolean allowServerWindowSize, + int preferredClientWindowSize, + boolean allowServerNoContext, + boolean preferredClientNoContext) { + this.compressionLevel = compressionLevel; + this.allowServerWindowSize = allowServerWindowSize; + this.preferredClientWindowSize = preferredClientWindowSize; + this.allowServerNoContext = allowServerNoContext; + this.preferredClientNoContext = preferredClientNoContext; + } + + CompressionConfig() { + this(6, ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(), 15, false, false); + } + } + + public static final class Http2Config { + final Consumer http2Settings; + final long closedWebSocketRemoveTimeoutMillis; + final long gracefulShutdownTimeoutMillis; + final boolean validateHeaders; + final boolean autoAckSettingsFrame; + final boolean autoAckPingFrame; + + Http2Config( + Consumer http2Settings, + long closedWebSocketRemoveTimeoutMillis, + long gracefulShutdownTimeoutMillis, + boolean validateHeaders, + boolean autoAckSettingsFrame, + boolean autoAckPingFrame) { + + this.http2Settings = http2Settings; + this.closedWebSocketRemoveTimeoutMillis = closedWebSocketRemoveTimeoutMillis; + this.gracefulShutdownTimeoutMillis = gracefulShutdownTimeoutMillis; + this.validateHeaders = validateHeaders; + this.autoAckSettingsFrame = autoAckSettingsFrame; + this.autoAckPingFrame = autoAckPingFrame; + } + + public static class Builder { + private Consumer http2Settings = settings -> {}; + private long closedWebSocketRemoveTimeoutMillis = 30_000; + private long gracefulShutdownTimeoutMillis = 30_000; + private boolean valdiateHeaders = true; + private boolean autoAckSettingsFrame = true; + private boolean autoAckPingFrame = true; + + public Builder http2Settings(Consumer http2Settings) { + this.http2Settings = Objects.requireNonNull(http2Settings, "http2Settings"); + return this; + } + + public Builder closedWebSocketRemoveTimeoutMillis(long closedWebSocketRemoveTimeoutMillis) { + this.closedWebSocketRemoveTimeoutMillis = + requirePositive( + closedWebSocketRemoveTimeoutMillis, "closedWebSocketRemoveTimeoutMillis"); + return this; + } + + public Builder gracefulShutdownTimeoutMillis(long gracefulShutdownTimeoutMillis) { + this.gracefulShutdownTimeoutMillis = + requirePositive(gracefulShutdownTimeoutMillis, "gracefulShutdownTimeoutMillis"); + return this; + } + + public Builder valdiateHeaders(boolean valdiateHeaders) { + this.valdiateHeaders = valdiateHeaders; + return this; + } + + public Builder autoAckSettingsFrame(boolean autoAckSettingsFrame) { + this.autoAckSettingsFrame = autoAckSettingsFrame; + return this; + } + + public Builder autoAckPingFrame(boolean autoAckPingFrame) { + this.autoAckPingFrame = autoAckPingFrame; + return this; + } + + public Http2Config build() { + return new Http2Config( + http2Settings, + closedWebSocketRemoveTimeoutMillis, + gracefulShutdownTimeoutMillis, + valdiateHeaders, + autoAckSettingsFrame, + autoAckPingFrame); + } + } + } + + static final class ConfigurableWebSocketCompressionHandler + extends WebSocketServerExtensionHandler { + + ConfigurableWebSocketCompressionHandler( + int compressionLevel, + boolean allowServerWindowSize, + int preferredClientWindowSize, + boolean allowServerNoContext, + boolean preferredClientNoContext) { + super( + new PerMessageDeflateServerExtensionHandshaker( + compressionLevel, + allowServerWindowSize, + preferredClientWindowSize, + allowServerNoContext, + preferredClientNoContext), + new DeflateFrameServerExtensionHandshaker(compressionLevel)); + } + } + + static Set parseSubprotocols(String subprotocols) { + if (subprotocols == null || subprotocols.isEmpty()) { + return Collections.emptySet(); + } + String[] arr = subprotocols.split(","); + Set subprotocolSet = new HashSet<>(arr.length); + for (String subprotocol : arr) { + subprotocolSet.add(subprotocol.trim()); + } + return subprotocolSet; + } + + static String selectSubprotocol( + List requestedSubprotocols, Set supportedSubprotocols) { + if (requestedSubprotocols.isEmpty() && supportedSubprotocols.isEmpty()) { + return ""; + } + for (String requested : requestedSubprotocols) { + if (supportedSubprotocols.contains(requested)) { + return requested; + } + } + return null; + } + + static long requirePositive(long value, String message) { + if (value <= 0) { + throw new IllegalArgumentException(message + " must positive, provided: " + value); + } + return value; + } +} diff --git a/netty-websocket-multiprotocol/src/main/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/MultiprotocolWebSocketServerBuilder.java b/netty-websocket-multiprotocol/src/main/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/MultiprotocolWebSocketServerBuilder.java new file mode 100644 index 0000000..8f35684 --- /dev/null +++ b/netty-websocket-multiprotocol/src/main/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/MultiprotocolWebSocketServerBuilder.java @@ -0,0 +1,240 @@ +/* + * Copyright 2024 - present Maksym Ostroverkhov. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jauntsdn.netty.handler.codec.websocketx.multiprotocol; + +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http1WebSocketCodec; +import com.jauntsdn.netty.handler.codec.http2.websocketx.WebSocketCallbacksCodec; +import io.netty.channel.ChannelHandler; +import io.netty.handler.codec.http.HttpDecoderConfig; +import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Builder for {@link MultiProtocolWebSocketServerHandler} */ +public final class MultiprotocolWebSocketServerBuilder { + private static final Logger logger = + LoggerFactory.getLogger(MultiprotocolWebSocketServerBuilder.class); + private static final boolean MASK_PAYLOAD = false; + + private String path = "/"; + private String subprotocols; + private WebSocketDecoderConfig webSocketDecoderConfig; + private Http1WebSocketCodec webSocketCodec = Http1WebSocketCodec.DEFAULT; + + private MultiProtocolWebSocketServerHandler.CompressionConfig compression; + private ChannelHandler handler; + private HttpDecoderConfig http1Config; + private MultiProtocolWebSocketServerHandler.Http2Config http2Config; + private long handshakeTimeoutMillis = 10_000; + + MultiprotocolWebSocketServerBuilder() {} + + /** @return new {@link MultiprotocolWebSocketServerBuilder} instance */ + public static MultiprotocolWebSocketServerBuilder create() { + return new MultiprotocolWebSocketServerBuilder(); + } + + /** + * Configures this handler with netty's default codec for websockets processing + * + * @return this {@link MultiProtocolWebSocketServerHandler} instance + */ + public MultiprotocolWebSocketServerBuilder defaultCodec() { + webSocketCodec = Http1WebSocketCodec.DEFAULT; + return this; + } + + /** + * Configures this handler with jauntsdn/netty-websocket-http1 websocket codec offering + * significantly higher throughput and lower per-frame heap allocation rate. + * + * @return this {@link MultiProtocolWebSocketServerHandler} instance + */ + public MultiprotocolWebSocketServerBuilder callbacksCodec() { + try { + webSocketCodec = WebSocketCallbacksCodec.instance(); + } catch (NoClassDefFoundError e) { + throw new IllegalArgumentException("websocket-http1 callbacks codec is not available", e); + } + return this; + } + + /** + * @param path websocket path, must start with / + * @return this {@link MultiprotocolWebSocketServerBuilder} instance + */ + public MultiprotocolWebSocketServerBuilder path(String path) { + Objects.requireNonNull(path, "path"); + if (!path.startsWith("/")) { + throw new IllegalArgumentException("path must be started with /"); + } + this.path = path; + return this; + } + + /** + * @param subprotocols comma separated list of supported subprotocols + * @return this {@link MultiprotocolWebSocketServerBuilder} instance + */ + public MultiprotocolWebSocketServerBuilder subprotocols(String subprotocols) { + this.subprotocols = Objects.requireNonNull(subprotocols, "subprotocols"); + return this; + } + + /** + * @param handshakeTimeoutMillis websocket handshake timeout, millis + * @return this {@link MultiprotocolWebSocketServerBuilder} instance + */ + public MultiprotocolWebSocketServerBuilder handshakeTimeout(long handshakeTimeoutMillis) { + this.handshakeTimeoutMillis = + MultiProtocolWebSocketServerHandler.requirePositive( + handshakeTimeoutMillis, "handshakeTimeoutMillis"); + return this; + } + + /** + * @param webSocketDecoderConfig websocket decoder configuration. Must be non-null + * @return this {@link MultiprotocolWebSocketServerBuilder} instance + */ + public MultiprotocolWebSocketServerBuilder decoderConfig( + WebSocketDecoderConfig webSocketDecoderConfig) { + this.webSocketDecoderConfig = + Objects.requireNonNull(webSocketDecoderConfig, "webSocketDecoderConfig"); + return this; + } + + /** + * @param http1Config http1 codec configuration options + * @return this {@link MultiprotocolWebSocketServerBuilder} instance + */ + public MultiprotocolWebSocketServerBuilder http1Config(HttpDecoderConfig http1Config) { + this.http1Config = Objects.requireNonNull(http1Config, "http1Config"); + return this; + } + + /** + * @param http2Config http2 codec configuration options + * @return this {@link MultiprotocolWebSocketServerBuilder} instance + */ + public MultiprotocolWebSocketServerBuilder http2Config( + MultiProtocolWebSocketServerHandler.Http2Config http2Config) { + this.http2Config = Objects.requireNonNull(http2Config, "http2Config"); + return this; + } + + /** + * @param isCompressionEnabled enables permessage-deflate compression with default configuration + * @return this {@link MultiprotocolWebSocketServerBuilder} instance + */ + public MultiprotocolWebSocketServerBuilder compression(boolean isCompressionEnabled) { + if (isCompressionEnabled) { + compression = new MultiProtocolWebSocketServerHandler.CompressionConfig(); + } else { + compression = 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 MultiprotocolWebSocketServerBuilder} instance + */ + public MultiprotocolWebSocketServerBuilder compression( + int compressionLevel, + boolean allowServerWindowSize, + int preferredClientWindowSize, + boolean allowServerNoContext, + boolean preferredClientNoContext) { + compression = + new MultiProtocolWebSocketServerHandler.CompressionConfig( + compressionLevel, + allowServerWindowSize, + preferredClientWindowSize, + allowServerNoContext, + preferredClientNoContext); + return this; + } + + /** + * @param channelHandler websocket channel handler. Must be non-null. + * @return this {@link MultiprotocolWebSocketServerBuilder} instance + */ + public MultiprotocolWebSocketServerBuilder handler(ChannelHandler channelHandler) { + this.handler = Objects.requireNonNull(channelHandler, "channelHandler"); + return this; + } + + /** @return new {@link MultiProtocolWebSocketServerHandler} instance */ + public MultiProtocolWebSocketServerHandler build() { + ChannelHandler webSocketHandler = handler; + boolean hasCompression = compression != null; + WebSocketDecoderConfig wsConfig = webSocketDecoderConfig; + HttpDecoderConfig h1Config = http1Config; + MultiProtocolWebSocketServerHandler.Http2Config h2Config = http2Config; + Http1WebSocketCodec codec = webSocketCodec; + + if (wsConfig == null) { + if (codec == WebSocketCallbacksCodec.DEFAULT) { + wsConfig = + WebSocketDecoderConfig.newBuilder() + /*align with the spec and strictness of some browsers*/ + .expectMaskedFrames(true) + .allowMaskMismatch(false) + .allowExtensions(hasCompression) + .build(); + } else { + wsConfig = + WebSocketDecoderConfig.newBuilder() + .expectMaskedFrames(true) + .allowMaskMismatch(true) + .withUTF8Validator(false) + .maxFramePayloadLength(65535) + .allowExtensions(hasCompression) + .build(); + } + } else { + boolean isAllowExtensions = wsConfig.allowExtensions(); + if (!isAllowExtensions && hasCompression) { + wsConfig = wsConfig.toBuilder().allowExtensions(true).build(); + } + } + codec.validate(MASK_PAYLOAD, wsConfig); + + return new MultiProtocolWebSocketServerHandler( + codec, + wsConfig, + h1Config, + h2Config, + compression, + path, + subprotocols, + handshakeTimeoutMillis, + webSocketHandler); + } +} diff --git a/netty-websocket-multiprotocol/src/test/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/Security.java b/netty-websocket-multiprotocol/src/test/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/Security.java new file mode 100644 index 0000000..eb0c6b0 --- /dev/null +++ b/netty-websocket-multiprotocol/src/test/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/Security.java @@ -0,0 +1,103 @@ +/* + * Copyright 2024 - present Maksym Ostroverkhov. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jauntsdn.netty.handler.codec.websocketx.multiprotocol; + +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import java.io.InputStream; +import java.security.KeyStore; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLException; + +public final class Security { + + public static SslContext serverSslContext(String keystoreFile, String keystorePassword) + throws Exception { + + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509"); + SslProvider sslProvider = sslProvider(); + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + + InputStream keystoreStream = Security.class.getClassLoader().getResourceAsStream(keystoreFile); + char[] keystorePasswordArray = keystorePassword.toCharArray(); + keyStore.load(keystoreStream, keystorePasswordArray); + keyManagerFactory.init(keyStore, keystorePasswordArray); + + return SslContextBuilder.forServer(keyManagerFactory) + .protocols("TLSv1.3") + .sslProvider(sslProvider) + .applicationProtocolConfig(alpnConfigHttp2()) + .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) + .build(); + } + + public static SslContext clientLocalSslContextHttp2() throws SSLException { + return clientSslContextBuilder().trustManager(InsecureTrustManagerFactory.INSTANCE).build(); + } + + public static SslContext clientLocalSslContextHttp1() throws SSLException { + return clientSslContextBuilder() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocolConfig(alpnConfigHttp1()) + .build(); + } + + public static SslContext clientSslContextHttp2() throws SSLException { + return clientSslContextBuilder().build(); + } + + private static SslContextBuilder clientSslContextBuilder() throws SSLException { + return SslContextBuilder.forClient() + .protocols("TLSv1.3") + .sslProvider(sslProvider()) + .applicationProtocolConfig(alpnConfigHttp2()) + .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE); + } + + private static ApplicationProtocolConfig alpnConfigHttp2() { + return new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2); + } + + private static ApplicationProtocolConfig alpnConfigHttp1() { + return new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_1_1); + } + + private static SslProvider sslProvider() { + final SslProvider sslProvider; + if (OpenSsl.isAvailable()) { + sslProvider = SslProvider.OPENSSL_REFCNT; + } else { + sslProvider = SslProvider.JDK; + } + return sslProvider; + } +} diff --git a/netty-websocket-multiprotocol/src/test/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/WebSocketMultiprotocolTest.java b/netty-websocket-multiprotocol/src/test/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/WebSocketMultiprotocolTest.java new file mode 100644 index 0000000..989c4b5 --- /dev/null +++ b/netty-websocket-multiprotocol/src/test/java/com/jauntsdn/netty/handler/codec/websocketx/multiprotocol/WebSocketMultiprotocolTest.java @@ -0,0 +1,525 @@ +/* + * Copyright 2024 - present Maksym Ostroverkhov. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jauntsdn.netty.handler.codec.websocketx.multiprotocol; + +import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketCallbacksHandler; +import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketFrameListener; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientBuilder; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientHandler; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientHandshaker; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeSuccessEvent; +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker; +import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory; +import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig; +import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler.HandshakeComplete; +import io.netty.handler.codec.http.websocketx.WebSocketVersion; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; +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; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; +import io.netty.util.concurrent.DefaultPromise; +import io.netty.util.concurrent.Promise; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.channels.ClosedChannelException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +public class WebSocketMultiprotocolTest { + + private volatile Channel server; + + @AfterEach + void tearDown() throws Exception { + Channel s = server; + if (s != null) { + s.close(); + s.closeFuture().await(15_000); + } + } + + @Test + void parseSubprotocols() { + Set subprotocols = MultiProtocolWebSocketServerHandler.parseSubprotocols(null); + Assertions.assertThat(subprotocols).hasSize(0); + subprotocols = MultiProtocolWebSocketServerHandler.parseSubprotocols(""); + Assertions.assertThat(subprotocols).hasSize(0); + subprotocols = MultiProtocolWebSocketServerHandler.parseSubprotocols("foo"); + Assertions.assertThat(subprotocols).hasSize(1); + Assertions.assertThat(subprotocols.contains("foo")).isTrue(); + + subprotocols = MultiProtocolWebSocketServerHandler.parseSubprotocols("foo, bar"); + Assertions.assertThat(subprotocols).hasSize(2); + Assertions.assertThat(subprotocols.contains("foo")).isTrue(); + Assertions.assertThat(subprotocols.contains("bar")).isTrue(); + } + + @Test + void selectSubprotocol() { + String subprotocol = + MultiProtocolWebSocketServerHandler.selectSubprotocol( + Collections.emptyList(), Collections.emptySet()); + Assertions.assertThat(subprotocol).isEmpty(); + subprotocol = + MultiProtocolWebSocketServerHandler.selectSubprotocol( + Collections.emptyList(), Collections.singleton("foo")); + Assertions.assertThat(subprotocol).isNull(); + subprotocol = + MultiProtocolWebSocketServerHandler.selectSubprotocol( + Collections.singletonList("foo"), Collections.emptySet()); + Assertions.assertThat(subprotocol).isNull(); + subprotocol = + MultiProtocolWebSocketServerHandler.selectSubprotocol( + Collections.singletonList("baz"), setOf("foo", "bar")); + Assertions.assertThat(subprotocol).isNull(); + subprotocol = + MultiProtocolWebSocketServerHandler.selectSubprotocol( + Collections.singletonList("foo"), Collections.singleton("foo")); + Assertions.assertThat(subprotocol).isEqualTo("foo"); + subprotocol = + MultiProtocolWebSocketServerHandler.selectSubprotocol( + Collections.singletonList("foo"), setOf("foo", "bar")); + Assertions.assertThat(subprotocol).isEqualTo("foo"); + subprotocol = + MultiProtocolWebSocketServerHandler.selectSubprotocol( + Arrays.asList("bar", "foo"), Collections.singleton("foo")); + Assertions.assertThat(subprotocol).isEqualTo("foo"); + subprotocol = + MultiProtocolWebSocketServerHandler.selectSubprotocol( + Arrays.asList("bar", "foo"), setOf("baz", "foo")); + Assertions.assertThat(subprotocol).isEqualTo("foo"); + } + + @Test + void http1webSocketDefaultCodec() throws Exception { + String host = "localhost"; + int port = 8099; + server = server(host, port, true, new DefaultServerHandler()); + ClientHandler clientHandler = new ClientHandler(); + Channel client = http1WebSocketClient(host, port, true, clientHandler); + client.writeAndFlush(new TextWebSocketFrame("test")); + TextWebSocketFrame receivedFrame = clientHandler.exchangeCompleted().get(5, TimeUnit.SECONDS); + try { + Assertions.assertThat(receivedFrame.text()).isEqualTo("test"); + } finally { + receivedFrame.release(); + } + } + + @Test + void http2webSocketDefaultCodec() throws Exception { + String host = "localhost"; + int port = 8099; + server = server(host, port, true, new DefaultServerHandler()); + ClientHandler clientHandler = new ClientHandler(); + Channel client = http2WebSocketClient(host, port, true, clientHandler); + client.writeAndFlush(new TextWebSocketFrame("test")); + TextWebSocketFrame receivedFrame = clientHandler.exchangeCompleted().get(5, TimeUnit.SECONDS); + try { + Assertions.assertThat(receivedFrame.text()).isEqualTo("test"); + } finally { + receivedFrame.release(); + } + } + + @Test + void http1webSocketCallbacksCodec() throws Exception { + String host = "localhost"; + int port = 8099; + server = server(host, port, false, new CallbacksServerHandler()); + ClientHandler clientHandler = new ClientHandler(); + Channel client = http1WebSocketClient(host, port, false, clientHandler); + client.writeAndFlush(new TextWebSocketFrame("test")); + TextWebSocketFrame receivedFrame = clientHandler.exchangeCompleted().get(5, TimeUnit.SECONDS); + try { + Assertions.assertThat(receivedFrame.text()).isEqualTo("test"); + } finally { + receivedFrame.release(); + } + } + + @Test + void http2webSocketCallbacksCodec() throws Exception { + String host = "localhost"; + int port = 8099; + server = server(host, port, false, new CallbacksServerHandler()); + ClientHandler clientHandler = new ClientHandler(); + Channel client = http2WebSocketClient(host, port, false, clientHandler); + client.writeAndFlush(new TextWebSocketFrame("test")); + TextWebSocketFrame receivedFrame = clientHandler.exchangeCompleted().get(5, TimeUnit.SECONDS); + try { + Assertions.assertThat(receivedFrame.text()).isEqualTo("test"); + } finally { + receivedFrame.release(); + } + } + + static Channel server(String host, int port, boolean defaultCodec, ChannelHandler handler) + throws Exception { + SslContext sslContext = Security.serverSslContext("localhost.p12", "localhost"); + + return new ServerBootstrap() + .group(new NioEventLoopGroup()) + .channel(NioServerSocketChannel.class) + .childHandler( + new ChannelInitializer() { + + @Override + protected void initChannel(SocketChannel ch) { + SslHandler sslHandler = sslContext.newHandler(ch.alloc()); + + MultiprotocolWebSocketServerBuilder builder = + MultiprotocolWebSocketServerBuilder.create() + .path("/test") + .compression(defaultCodec) + .handler(handler); + if (defaultCodec) { + builder.defaultCodec(); + } else { + builder.callbacksCodec(); + } + MultiProtocolWebSocketServerHandler multiprotocolHandler = builder.build(); + ch.pipeline().addLast(sslHandler, multiprotocolHandler); + } + }) + .bind(host, port) + .sync() + .channel(); + } + + static Channel http2WebSocketClient( + String host, int port, boolean compression, ChannelHandler handler) throws Exception { + SslContext http2SslContext = Security.clientLocalSslContextHttp2(); + + WebSocketDecoderConfig decoderConfig = + WebSocketDecoderConfig.newBuilder() + .expectMaskedFrames(false) + .allowMaskMismatch(false) + .allowExtensions(compression) + .build(); + + Channel http2Channel = + new Bootstrap() + .group(new NioEventLoopGroup(1)) + .channel(NioSocketChannel.class) + .handler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + SslHandler sslHandler = http2SslContext.newHandler(ch.alloc()); + Http2FrameCodec http2FrameCodec = Http2FrameCodecBuilder.forClient().build(); + + Http2WebSocketClientHandler http2WebSocketClientHandler = + Http2WebSocketClientBuilder.create() + .decoderConfig(decoderConfig) + .handshakeTimeoutMillis(15_000) + .compression(compression) + .build(); + + ch.pipeline().addLast(sslHandler, http2FrameCodec, http2WebSocketClientHandler); + } + }) + .connect(new InetSocketAddress(host, port)) + .sync() + .channel(); + + /*wait until channel is ready (future completes)*/ + Http2WebSocketClientHandshaker http2WebSocketHandShaker = + Http2WebSocketClientHandshaker.create(http2Channel); + + ChannelFuture http2WebSocketHandshake = http2WebSocketHandShaker.handshake("/test", handler); + + return http2WebSocketHandshake.sync().channel(); + } + + static Channel http1WebSocketClient( + String host, int port, boolean compression, ChannelHandler handler) throws Exception { + SslContext clientSslContext = Security.clientLocalSslContextHttp1(); + WebSocketDecoderConfig decoderConfig = + WebSocketDecoderConfig.newBuilder() + .expectMaskedFrames(false) + .allowMaskMismatch(false) + .allowExtensions(true) + .build(); + + Http1WebSocketHandshaker http1WebSocketHandshaker = + new Http1WebSocketHandshaker("/test", decoderConfig, host, port); + + Channel client = + new Bootstrap() + .group(new NioEventLoopGroup(1)) + .channel(NioSocketChannel.class) + .handler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + SslHandler sslHandler = clientSslContext.newHandler(ch.alloc()); + HttpClientCodec http1Codec = new HttpClientCodec(); + HttpObjectAggregator http1Aggregator = new HttpObjectAggregator(65536); + + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(sslHandler, http1Codec, http1Aggregator); + if (compression) { + WebSocketClientCompressionHandler http1WebSocketCompressionHandler = + WebSocketClientCompressionHandler.INSTANCE; + pipeline.addLast(http1WebSocketCompressionHandler); + } + pipeline.addLast(http1WebSocketHandshaker, handler); + } + }) + .connect(new InetSocketAddress(host, port)) + .sync() + .channel(); + + http1WebSocketHandshaker.handshakeComplete().sync(); + return client; + } + + private static class Http1WebSocketHandshaker extends SimpleChannelInboundHandler { + private final WebSocketClientHandshaker handshaker; + private ChannelPromise handshakeComplete; + + public Http1WebSocketHandshaker( + String path, WebSocketDecoderConfig webSocketDecoderConfig, String host, int port) { + handshaker = + WebSocketClientHandshakerFactory.newHandshaker( + uri("wss://" + host + ":" + port + path), + WebSocketVersion.V13, + null, + webSocketDecoderConfig.allowExtensions(), + null, + webSocketDecoderConfig.maxFramePayloadLength(), + true, + webSocketDecoderConfig.allowMaskMismatch()); + } + + public ChannelFuture handshakeComplete() { + return handshakeComplete; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + handshakeComplete = ctx.newPromise(); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + handshaker.handshake(ctx.channel()); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + ChannelPromise f = handshakeComplete; + if (!f.isDone()) { + f.setFailure(new ClosedChannelException()); + } + super.channelInactive(ctx); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof FullHttpResponse) { + WebSocketClientHandshaker h = handshaker; + if (h.isHandshakeComplete()) { + throw new IllegalStateException( + "Unexpected http response after http1 websocket handshake completion"); + } + ChannelPromise hc = handshakeComplete; + try { + h.finishHandshake(ctx.channel(), (FullHttpResponse) msg); + hc.setSuccess(); + } catch (WebSocketHandshakeException e) { + hc.setFailure(e); + } + return; + } + if (msg instanceof ReferenceCounted) { + ((ReferenceCounted) msg).retain(); + } + ctx.fireChannelRead(msg); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + ChannelPromise f = handshakeComplete; + if (!f.isDone()) { + f.setFailure(cause); + } + ctx.close(); + super.exceptionCaught(ctx, cause); + } + + private static URI uri(String uri) { + try { + return new URI(uri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("uri syntax error: " + uri, e); + } + } + } + + private static class ClientHandler extends SimpleChannelInboundHandler { + private volatile Promise exchangeCompleted; + + public Promise exchangeCompleted() { + return exchangeCompleted; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + this.exchangeCompleted = new DefaultPromise<>(ctx.executor()); + super.handlerAdded(ctx); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame webSocketFrame) { + exchangeCompleted.trySuccess(webSocketFrame.retain()); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + exchangeCompleted.tryFailure(cause); + if (cause instanceof IOException) { + return; + } + throw new RuntimeException("Unexpected websocket error", cause); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + exchangeCompleted.tryFailure(new ClosedChannelException()); + super.channelInactive(ctx); + } + } + + @ChannelHandler.Sharable + private static class DefaultServerHandler + extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame webSocketFrame) { + ctx.write(webSocketFrame.retain()); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + super.channelReadComplete(ctx); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (cause instanceof IOException) { + return; + } + throw new RuntimeException("Unexpected websocket error", cause); + } + } + + private static class CallbacksServerHandler extends ChannelInboundHandlerAdapter { + + @Override + public void userEventTriggered(ChannelHandlerContext c, Object evt) throws Exception { + if (evt instanceof HandshakeComplete || evt instanceof Http2WebSocketHandshakeSuccessEvent) { + WebSocketCallbacksHandler.exchange( + c, + (ctx, webSocketFrameFactory) -> + new WebSocketFrameListener() { + @Override + public void onChannelRead( + ChannelHandlerContext context, + boolean finalFragment, + int rsv, + int opcode, + ByteBuf payload) { + ByteBuf textFrame = + webSocketFrameFactory.mask( + webSocketFrameFactory.createTextFrame( + ctx.alloc(), payload.readableBytes())); + textFrame.writeBytes(payload); + payload.release(); + ctx.write(textFrame); + } + + @Override + public void onChannelReadComplete(ChannelHandlerContext ctx1) { + ctx1.flush(); + } + + @Override + public void onExceptionCaught(ChannelHandlerContext ctx1, Throwable cause) { + if (cause instanceof IOException) { + return; + } + throw new RuntimeException("Unexpected websocket error", cause); + } + }); + } + super.userEventTriggered(c, evt); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ReferenceCountUtil.safeRelease(msg); + } + } + + static Set setOf(T... elems) { + Set set = new HashSet<>(elems.length); + for (T elem : elems) { + set.add(elem); + } + return set; + } +} diff --git a/netty-websocket-multiprotocol/src/test/resources/localhost.p12 b/netty-websocket-multiprotocol/src/test/resources/localhost.p12 new file mode 100644 index 0000000000000000000000000000000000000000..3d07e307ed4b5addc2833d8c8dc1f5f86ba399c8 GIT binary patch literal 4077 zcmVLj{#%>?ss5W{0EWqT)Yp30^TG z8S=>I9#DH4t51d7o)TZI4}|Eiy?25Y@5S3=JS~k(xL>u*(XG!zx&V1-DM7q_m@+~% zHfD}1*`ac;AB?)~J<_kt;1>Us)53ai`ZrOYtalY|5-riu?mo;bNC$3-uw#}e_sB&2;4l2i(ra9Yl^?u^ z8w&(Go8F*h?Ej8A%l#HcGKDxYjvQK`W$NskvlWT@@|AQpC(+> zdVGu2b^giC&A|XGM_~#W)^YLz242c+VvJ-iE!)y3Od%FIi9Sb;3pzEKzwc;hCWIVi zo=047s%C@(^+)2c)F9~(D=u)1a|UlUr@Tzfmh{v7bu{`agK|2hb4Zt{n_9QfLq!j0 zLW}B#vSEZV>yvv;W%&_YmdB~QdQtk6<{z}bsXxMf&0N=LX;3Hk5$z$rCfa2=+!STTgBU zX`}A5m+@5&ea`vMm<3>Y*g=ReYYk2@$DKw+b%16H#sIyHIlpb|Q*fhDq!jbv6n^zW zeDkDCo<1u4x_|G!W@&PEZ>34|3-{+{#1j-S_^*|gu46Sv$RL&>PL@if*x0ZfkLW;^ zMovfHlQq)_-X&e0D}g$-nRSHYPHE1p140JYvzRWOS`c+Lw`u~(k`$j(u--PNXZoYN zXY`QFVK4w8Yrd(eLVVf6+7;t`(Jr6IBx7-j_p6sz2nzD7Q0s3K%7zBgL8>Axv=$M* zxl?b-pH3k^y=;cp4QQcAAtQ^R$UBzKE0U9|k1RFUrn)MpLxc&QJWVb7RV|ON3i8NQ znZi5|;C)8f*bx!}P4lYjjKUb}_}odz{{@K^a1^!dCcXZc8;jE9_V1}UZ%VmWiG1b% zIuwp6bZ$1c;3Kf6Sq2@r49tG{v(r^?}BH9U){rZjUzKvDzjUJxT0qd9WWA zBRgpC@X1^D$#4OuGhNq3hZ%iNLAeqMNQf3Q;9VkVG#ZvpKA))5f91!s;xZmK(|r7* zf=YGW2nUgN9hTEzAJ+AF5GkcA90>z;>Nny!8zABB~mq zk{dQ6>G_}hG!Eh$lcGH!pK)x)uQ|NhaLko`NR)BO7bpRRqOiva^`#q|!jB8ui zKBOq`FO?QN`S4|Bj$~$8I}?|B)r_Iq@l+_amahO+>8L?i_-iswvDWtD7%yiNDyl6` zeSVA;1ii)3_56I^(}XigC^vRC;P^qo;eB{_-H@_fv}z&_D7XX2Iryiojcn@mZigve zwSsIq6^a!s5bF)i!thyW7Lr)^o6H+v$#Lp%rYTh{mX|9y}2fbxfTL9H7NhPDQcFYJo6#LHM6gC8;c;9-M1hkn$_lxGb zGgdY(|3JsY6u-8ICg-YgOc){lVdD-Ca;y+hQjzj1!E9KS7}$J2TIfPG6D^;I$xN#9 zdwLmnyuN-a|G57WDCp>jeW)IO$B@HZFoFre1_>&LNQUrK0##Jn#4jnLD!u7$}!X<0eRk7OLlw}iwlJ&&efe;=uojNK#cQU#v!@1ZrPDv4SV}X19|yu ztEu63tB`mf9;!`~N7AmiA1#1wU3Wt>r!|;ir^>DleK?w7!M00aT;Z&$0q)ukW*HX9 zjQwdAKFpl`=jl7jhSsZ~#r~Skis30~R9bzl!6<<9LPQ5$EMXWH8 zE={{(iq&6Vg)1HAYXOyAwXfs&3ZQ65|JNNkx7uLEE(ypknkH5m8kPZ=fW?vElp z5Hn_a_DU6*FHf*_y`VfGmc+RSA1x0_;T96qA?x5>3xDtRNi9nCFUZ*W6B$ZYlxfUo z`+ELQ#-4g(umIpS>iPs4vmt8ndxzjIZe(RHPiwy`yOMFY<%PtURdrF&7q0kP;)@d5-e~5kMw-zCU+T}x%7|5j$_OSNawrvZ6vof7KqP?5f`xlUTMoZqu^r0?OKy>% zOQ;mc`Wzi<22jV!xdW6w49aH!TIR^;6N#?TrVq~6scv<(X{~H?U-g^W3}Y>-Nk-EG z4@q;IOhXKZRc4#J5>jOhEha>~cvPh7f7!^Rs|4iA3(k4A#isX&1Re{n9Xe;@-Yo9! zwinJZ8ga!=u_`#IRu@lW8L?m@vctqb`YDGkvb}Za(n_P&YG2{QQO@9{F_THbZKiFK z5{WtFT`zfPfV#Dy9o5JrGS&B|;Q67Z=dw>&3oBOTwPM-H!F?&0w6rK~t;DF9m^E|4 z+eT|!b&tijehmo1(-14Bzx#4jeJU9_4YW~SJsd930Qp%6xTm#vl;F1w^6fO~u&{ROZI>!FU%78U@bFJiLne7|xd_as z)UngE5w?1AzU!6(k^(T#mbDy>wKN0>zAu5&%vn9xn-H^4E%zRX6+T1ADd5TLrZT7mvA;;`2-=k@Xm8K9J0^5tfcriJnuv!r-zwxf1 zn6^=TTL#`WC?TpO7dRBXX}WJjZ@@s+;0OTFRBU{% z88EYU_o|1@xfIzRwn5qD9Ys8a^9wInP26<34Es)5wXdv?$K6Z&pmEp3NGSGS z|5oi64iY^bX!|iW#GBv*pB)N~%1~g41-KYphgc%>XSVMSgzov}NhffG=i}wIbHUz3 zL_wcf>_yuWUEz;L>|U} zx+J3SRi@BFp1}l zv=ZOE-Mv!a47Rw>OXSXqh)Y9Xww|2t&)6%fucji#h}D1YKToR1q4uW7^nqW?o?AAEjfEQ!j*6qvdH8Qg5|KB;S!bkNG$ZW@M=yvB;@Hl3(c4Dajk$1OZBFxV9Y9?Ui6!}|0!o%)VY#IoOmPz} zks(RvNRkP%&|E{{20UYI(d+XJ!=HwDOD1Gv_p{#GKj8pE@A9 z<;o!##aKRhE0)Rm_eWF+yHUP^s&l4pP=s~lqHkPc*jIk$AA&=@BruHgMhjo+Qm}=% zb!w~Wp2)Cz(Orq{H#vTcF)9XBt6cLaIFMyM0!8amZ#+*7(+ybXUrjpC-UgR3?20AR zIhbHSg|Ub158iflXKi2*LR{L_WDx`xrd8AVl-ydtl;36-DH=lr)$1`OFe3&DDuzgg z_YDCF6)_eB6cZ4_6Sxa6(7ZTkxBXPd{Ej?>HZU + + + + + %date{HH:mm:ss.SSS} %-10thread %-42logger %msg%n + + + + + + + diff --git a/settings.gradle b/settings.gradle index 1e49b4e..965e6b5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -27,6 +27,7 @@ pluginManagement { rootProject.name = "netty-websocket-http2-parent" include "netty-websocket-http2" include "netty-websocket-http2-callbacks-codec" +include "netty-websocket-multiprotocol" include "netty-websocket-http2-example" include "netty-websocket-http2-perftest" From 90542884bf65b387b65eca532ae3299c80f9310c Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 25 Jul 2024 07:00:10 +0300 Subject: [PATCH 14/14] update version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4029e2a..cd0f527 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.jauntsdn.netty -version=1.2.7 +version=1.3.0 googleJavaFormatPluginVersion=0.9 dependencyManagementPluginVersion=1.1.0