From 0ad3b49453348c81564d4c79302fba05e85f8a0c Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Mon, 31 May 2021 22:34:36 +0300 Subject: [PATCH 1/2] next development iteration --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 520530a..5e2da7f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.jauntsdn.netty -version=1.1.1 +version=1.1.2 googleJavaFormatPluginVersion=0.9 dependencyManagementPluginVersion=1.0.10.RELEASE From 87250c224a36d22f8d645c1ad073b173e1f424ad Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Tue, 1 Jun 2021 15:50:14 +0300 Subject: [PATCH 2/2] Http2WebSocketServerBuilder: do not mask outbound frames payload; by default only allow masked inbound frames; force allowExtensions on WebSocketDecoderConfig if compression is enabled. Http2WebSocketClientBuilder: by default only allow non-masked inbound frames; force allowExtensions on WebSocketDecoderConfig if compression is enabled. --- README.md | 25 +++---- .../example/channelclient/Main.java | 7 +- .../example/channelserver/Main.java | 6 +- .../example/handshakeserver/Main.java | 68 ++----------------- .../websocketx/example/lwsclient/Main.java | 6 +- .../src/main/resources/web/index.html | 14 ++-- .../src/main/resources/web/index.js | 2 +- .../Http2WebSocketClientBuilder.java | 11 ++- .../Http2WebSocketServerBuilder.java | 24 +++---- 9 files changed, 58 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 1842734..19532eb 100644 --- a/README.md +++ b/README.md @@ -30,14 +30,14 @@ EchoWebSocketHandler http1WebSocketHandler = new EchoWebSocketHandler(); Http2WebSocketAcceptor.Subprotocol .accept("echo.jauntsdn.com", response); return ctx.executor() - .newSucceededFuture(echoWebSocketHandler); + .newSucceededFuture(http1WebSocketHandler); } break; case "/echo_all": if (subprotocols.isEmpty() && acceptUserAgent(request, response)) { return ctx.executor() - .newSucceededFuture(echoWebSocketHandler); + .newSucceededFuture(http1WebSocketHandler); } break; } @@ -82,7 +82,7 @@ EchoWebSocketHandler http1WebSocketHandler = new EchoWebSocketHandler(); Http2WebSocketClientHandshaker handShaker = Http2WebSocketClientHandshaker.create(channel); Http2Headers headers = - new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/1.0.1"); + new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/1.1.1"); ChannelFuture handshakeFuture = /*http1 websocket handler*/ handShaker.handshake("/echo", headers, new EchoWebSocketHandler()); @@ -98,7 +98,8 @@ Runnable demo is available in `netty-websocket-http2-example` module - ### websocket handshake only API Intended for intermediaries/proxies. -Only verifies whether http2 stream is valid websocket, then passes it down the pipeline. +Only verifies whether http2 stream is valid websocket, then passes it down the pipeline as `POST` request with `x-protocol=websocket` header. + ```groovy Http2WebSocketServerHandler http2webSocketHandler = Http2WebSocketServerBuilder.buildHandshakeOnly(); @@ -159,7 +160,7 @@ EchoWebSocketHandler http1WebsocketHandler = new EchoWebSocketHandler(); ChannelFuture handshake = handShaker.handshake("/echo", "subprotocol", headers, http1WebsocketHandler); ``` -On a server It is responsibility of `Http2WebSocketAcceptor` to select supported protocol with +On a server It is responsibility of `Http2WebSocketAcceptor` to select supported subprotocol with ```groovy Http2WebSocketAcceptor.Subprotocol.accept(subprotocol, response); ``` @@ -244,17 +245,17 @@ the results are as follows (measured over time spans of 5 seconds): ### examples -`netty-websocket-http2-example` module contains demos showcasing both API styles, -with this library/browser as clients. +`netty-websocket-http2-example` module contains demos showcasing both API styles, with this library/browser as clients. + * `channelserver, channelclient` packages for websocket subchannel API demos. * `handshakeserver, channelclient` packages for handshake only API demo. * `lwsclient` package for client demo that runs against [https://libwebsockets.org/testserver/](https://libwebsockets.org/testserver/) which hosts websocket-over-http2 server implemented with [libwebsockets](https://github.com/warmcat/libwebsockets) - popular C-based networking library. ### browser example -Both example servers have web page at `https://localhost:8099` that sends pings to -`/echo` endpoint. -The only browser with http2 websockets protocol support is `Mozilla Firefox`. +`Channelserver` example serves web page at `https://www.localhost:8099` that sends pings to `/echo` endpoint. + +Currently only `Mozilla Firefox` and latest `Google Chrome` support websockets-over-http2. ### build & binaries ``` @@ -268,7 +269,7 @@ repositories { } dependencies { - implementation 'com.jauntsdn.netty:netty-websocket-http2:1.1.0' + implementation 'com.jauntsdn.netty:netty-websocket-http2:1.1.1' } ``` @@ -286,4 +287,4 @@ 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. \ No newline at end of file +limitations under the License. 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 9e192b8..1ceb462 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 @@ -68,7 +68,10 @@ protected void initChannel(SocketChannel ch) { Http2WebSocketClientBuilder.create() .streamWeight(16) .decoderConfig( - WebSocketDecoderConfig.newBuilder().allowExtensions(true).build()) + WebSocketDecoderConfig.newBuilder() + .expectMaskedFrames(false) + .allowMaskMismatch(true) + .build()) .handshakeTimeoutMillis(15_000) .compression(true) .build(); @@ -90,7 +93,7 @@ protected void initChannel(SocketChannel ch) { Http2WebSocketClientHandshaker handShaker = Http2WebSocketClientHandshaker.create(channel); Http2Headers headers = - new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/1.0.1"); + new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/1.1.2"); ChannelFuture handshake = handShaker.handshake("/echo", "echo.jauntsdn.com", headers, new EchoWebSocketHandler()); 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 1c2da1b..4d17379 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 @@ -57,12 +57,14 @@ public class Main { public static void main(String[] args) throws Exception { String host = System.getProperty("HOST", "localhost"); int port = Integer.parseInt(System.getProperty("PORT", "8099")); + String advertiseAddress = System.getProperty("ADVERTISE_ADDRESS", "www.localhost:8099"); String echoPath = System.getProperty("PING", "echo"); String keyStoreFile = System.getProperty("KEYSTORE", "localhost.p12"); String keyStorePassword = System.getProperty("KEYSTORE_PASS", "localhost"); logger.info("\n==> Channel per websocket server\n"); logger.info("\n==> Bind address: {}:{}", host, port); + logger.info("\n==> Advertise address: {}", advertiseAddress); logger.info("\n==> Keystore file: {}", keyStoreFile); SslContext sslContext = Security.serverSslContext(keyStoreFile, keyStorePassword); @@ -79,14 +81,14 @@ public static void main(String[] args) throws Exception { logger.info("\n==> Server is listening on {}:{}", host, port); logger.info("\n==> Echo path: {}", echoPath); - logger.info("\n==> Modern browser (Mozilla Firefox) demo: https://{}:{}", host, port); + logger.info("\n==> Modern browser (Firefox, latest Chrome) demo: https://{}", advertiseAddress); server.closeFuture().sync(); } private static class ConnectionAcceptor extends ChannelInitializer { private static final List SUPPORTED_USER_AGENTS = - Arrays.asList("Firefox/", "jauntsdn-websocket-http2-client/"); + Arrays.asList("Firefox/", "Chrome/", "jauntsdn-websocket-http2-client/"); private final SslContext sslContext; ConnectionAcceptor(SslContext sslContext) { 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 9a00a02..ee732a0 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 @@ -20,23 +20,21 @@ import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketServerBuilder; import com.jauntsdn.netty.handler.codec.http2.websocketx.example.Security; import io.netty.bootstrap.ServerBootstrap; -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.ChannelHandlerContext; +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.QueryStringDecoder; import io.netty.handler.codec.http2.*; -import io.netty.handler.ssl.*; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; import io.netty.util.AsciiString; import io.netty.util.collection.IntObjectHashMap; import io.netty.util.collection.IntObjectMap; -import java.io.BufferedInputStream; import java.io.IOException; -import java.io.InputStream; -import java.util.concurrent.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,7 +66,6 @@ public static void main(String[] args) throws Exception { logger.info("\n==> Server is listening on {}:{}", host, port); logger.info("\n==> Echo path: {}", echoPath); - logger.info("\n==> Modern browser (Mozilla Firefox) demo: https://{}:{}", host, port); server.closeFuture().sync(); } @@ -112,7 +109,6 @@ private static class Http2StreamsHandler extends Http2ChannelDuplexHandler { AsciiString.of("echo.jauntsdn.com")); private final IntObjectMap echos = new IntObjectHashMap<>(); - private final ExecutorService executorService = Executors.newCachedThreadPool(); private int receiveBytes; private final int receiveWindowBytes; @@ -131,14 +127,6 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception String query = requestHeaders.get(":path").toString(); String path = new QueryStringDecoder(query).path(); - if ("GET".contentEquals(method)) { - if (path.isEmpty() || path.equals("/")) { - path = "index.html"; - } - serveResource(ctx, stream, path); - return; - } - if (!"POST".contentEquals(method)) { ctx.write(new DefaultHttp2HeadersFrame(HEADERS_405, true).stream(stream)); return; @@ -215,52 +203,6 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ctx.close(); } - private void serveResource( - ChannelHandlerContext ctx, Http2FrameStream frameStream, String resource) { - executorService.execute( - () -> { - String classPathResource = "web/" + resource; - try (InputStream resourceStream = - Main.class.getClassLoader().getResourceAsStream(classPathResource)) { - if (resourceStream == null) { - ctx.write(new DefaultHttp2HeadersFrame(HEADERS_404, true).stream(frameStream)); - return; - } - Http2Headers responseHeaders = new DefaultHttp2Headers(true).status("200"); - - String contentType; - if (resource.endsWith(".html")) { - contentType = "text/html"; - } else if (resource.endsWith(".js")) { - contentType = "text/javascript"; - } else { - contentType = "application/octet-stream"; - } - responseHeaders.set("content-type", contentType); - - ctx.write(new DefaultHttp2HeadersFrame(responseHeaders, false).stream(frameStream)); - - int bufferSize = 16384; - InputStream bufferedResourceStream = - new BufferedInputStream(resourceStream, bufferSize); - byte[] buffer = new byte[bufferSize]; - int bytesRead; - /*keep it simple for the demo*/ - while ((bytesRead = bufferedResourceStream.read(buffer)) != -1) { - ByteBuf response = ByteBufAllocator.DEFAULT.buffer(bytesRead); - response.writeBytes(buffer, 0, bytesRead); - ctx.write(new DefaultHttp2DataFrame(response, false).stream(frameStream)); - } - ctx.writeAndFlush( - new DefaultHttp2DataFrame(Unpooled.EMPTY_BUFFER, true).stream(frameStream)); - } catch (Exception e) { - ctx.fireExceptionCaught( - new RuntimeException( - "Error while reading classpath resource: " + classPathResource, e)); - } - }); - } - @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { echos.clear(); 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 8663942..5b8c490 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 @@ -73,8 +73,8 @@ protected void initChannel(SocketChannel ch) { Http2WebSocketClientBuilder.create() .decoderConfig( WebSocketDecoderConfig.newBuilder() - .allowExtensions(true) - .allowMaskMismatch(true) + .expectMaskedFrames(false) + .allowMaskMismatch(false) .build()) .handshakeTimeoutMillis(15_000) .compression(true) @@ -90,7 +90,7 @@ protected void initChannel(SocketChannel ch) { Http2WebSocketClientHandshaker handShaker = Http2WebSocketClientHandshaker.create(channel); Http2Headers headers = - new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/1.0.1"); + new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/1.1.2"); ChannelFuture handshake = handShaker.handshake( "/", "dumb-increment-protocol", headers, new WebSocketDumbIncrementHandler()); diff --git a/netty-websocket-http2-example/src/main/resources/web/index.html b/netty-websocket-http2-example/src/main/resources/web/index.html index 069a134..08e72de 100644 --- a/netty-websocket-http2-example/src/main/resources/web/index.html +++ b/netty-websocket-http2-example/src/main/resources/web/index.html @@ -5,20 +5,22 @@ diff --git a/netty-websocket-http2-example/src/main/resources/web/index.js b/netty-websocket-http2-example/src/main/resources/web/index.js index 4b85468..2ad4df3 100644 --- a/netty-websocket-http2-example/src/main/resources/web/index.js +++ b/netty-websocket-http2-example/src/main/resources/web/index.js @@ -1,4 +1,4 @@ -const address = 'wss://localhost:8099/echo'; +const address = 'wss://www.localhost:8099/echo'; const subprotocol = 'echo.jauntsdn.com'; const startMessage = 'start'; 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 bb91add..8fc4312 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 @@ -145,12 +145,17 @@ public Http2WebSocketClientHandler build() { boolean hasCompression = compressionHandshaker != null; WebSocketDecoderConfig config = webSocketDecoderConfig; if (config == null) { - config = WebSocketDecoderConfig.newBuilder().allowExtensions(hasCompression).build(); + config = + WebSocketDecoderConfig.newBuilder() + /*align with the spec and strictness of some browsers*/ + .expectMaskedFrames(false) + .allowMaskMismatch(false) + .allowExtensions(hasCompression) + .build(); } else { boolean isAllowExtensions = config.allowExtensions(); if (!isAllowExtensions && hasCompression) { - throw new IllegalStateException( - "websocket compression is enabled while extensions are disabled"); + config = config.toBuilder().allowExtensions(true).build(); } } short weight = streamWeight; 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 acb481e..9190b90 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 @@ -27,6 +27,7 @@ /** Builder for {@link Http2WebSocketServerHandler} */ public final class Http2WebSocketServerBuilder { private static final Logger logger = LoggerFactory.getLogger(Http2WebSocketServerBuilder.class); + private static final boolean MASK_PAYLOAD = false; private static final Http2WebSocketAcceptor REJECT_REQUESTS_ACCEPTOR = (context, path, subprotocols, request, response) -> @@ -40,7 +41,7 @@ public final class Http2WebSocketServerBuilder { + subprotocols)); private WebSocketDecoderConfig webSocketDecoderConfig; - private boolean isEncoderMaskPayload = true; + private PerMessageDeflateServerExtensionHandshaker perMessageDeflateServerExtensionHandshaker; private long closedWebSocketRemoveTimeoutMillis = 30_000; private boolean isSingleWebSocketPerConnection; @@ -98,14 +99,6 @@ public Http2WebSocketServerBuilder decoderConfig(WebSocketDecoderConfig webSocke Preconditions.requireNonNull(webSocketDecoderConfig, "webSocketDecoderConfig"); return this; } - /** - * @param isEncoderMaskPayload enables websocket frames encoder payload masking - * @return this {@link Http2WebSocketServerBuilder} instance - */ - public Http2WebSocketServerBuilder encoderMaskPayload(boolean isEncoderMaskPayload) { - this.isEncoderMaskPayload = isEncoderMaskPayload; - return this; - } /** * @param closedWebSocketRemoveTimeoutMillis delay until websockets handler forgets closed @@ -199,17 +192,22 @@ public Http2WebSocketServerHandler build() { boolean hasCompression = perMessageDeflateServerExtensionHandshaker != null; WebSocketDecoderConfig config = webSocketDecoderConfig; if (config == null) { - config = WebSocketDecoderConfig.newBuilder().allowExtensions(hasCompression).build(); + config = + WebSocketDecoderConfig.newBuilder() + /*align with the spec and strictness of some browsers*/ + .expectMaskedFrames(true) + .allowMaskMismatch(false) + .allowExtensions(hasCompression) + .build(); } else { boolean isAllowExtensions = config.allowExtensions(); if (!isAllowExtensions && hasCompression) { - throw new IllegalStateException( - "websocket compression is enabled while extensions are disabled"); + config = config.toBuilder().allowExtensions(true).build(); } } return new Http2WebSocketServerHandler( config, - isEncoderMaskPayload, + MASK_PAYLOAD, closedWebSocketRemoveTimeoutMillis, perMessageDeflateServerExtensionHandshaker, acceptor,