Skip to content

Commit

Permalink
Merge branch 'release/1.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
mostroverkhov committed Sep 20, 2020
2 parents 7eb7ecf + 0b56492 commit abaae62
Show file tree
Hide file tree
Showing 13 changed files with 227 additions and 34 deletions.
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ interface Http2WebSocketAcceptor {
Http2WebSocketClientHandshaker handShaker = Http2WebSocketClientHandshaker.create(channel);
Http2Headers headers =
new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/0.0.3");
new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/0.1.0");
ChannelFuture handshakeFuture =
/*http1 websocket handler*/
handShaker.handshake("/echo", headers, new EchoWebSocketHandler());
Expand Down Expand Up @@ -103,8 +103,6 @@ Only verifies whether http2 stream is valid websocket, then passes it down the p

Works with both callbacks-style `Http2ConnectionHandler` and frames based `Http2FrameCodec`.

Websocket requests rejected due to protocol violation are reported to `RejectedWebSocketListener`

```
Http2WebSocketServerHandler.builder().handshakeOnly(rejectedWebSocketListener);
```
Expand All @@ -113,6 +111,24 @@ Runnable demo is available in `netty-websocket-http2-example` module -
[handshakeserver](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/handshakeserver/Main.java),
[channelclient](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/channelclient/Main.java).

### configuration
Initial settings of server http2 codecs (`Http2ConnectionHandler` or `Http2FrameCodec`) should contain [SETTINGS_ENABLE_CONNECT_PROTOCOL=1](https://tools.ietf.org/html/rfc8441#section-9.1)
parameter to advertise websocket-over-http2 support.

Also server http2 codecs must disable built-in headers validation because It is not compatible
with rfc8441 due to newly introduced `:protocol` pseudo-header. All websocket handlers provided by this library
do headers validation on their own - both for websocket and non-websocket requests.

Above configuration may be done with utility methods of `Http2WebSocketServerBuilder`

```
public static Http2FrameCodecBuilder configureHttp2Server(
Http2FrameCodecBuilder http2Builder);
public static Http2ConnectionHandlerBuilder configureHttp2Server(
Http2ConnectionHandlerBuilder http2Builder)
```

### compression & subprotocols
Client and server `permessage-deflate` compression configuration is shared by all streams
```groovy
Expand All @@ -135,7 +151,7 @@ ChannelFuture handshake =
```groovy
Http2WebSocketServerHandler.builder()
.handler("/echo", "subprotocol", http1WebSocketHandler)
.handler("/echo", "wamp", http1WebSocketWampHandler)
.handler("/echo", "wamp", http1WebSocketWampHandler);
```

### lifecycle
Expand Down Expand Up @@ -189,7 +205,8 @@ Currently blocked by [netty bug](https://github.com/netty/netty/issues/10416).
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.
Expand All @@ -207,7 +224,7 @@ repositories {
}
dependencies {
implementation 'com.jauntsdn.netty:netty-websocket-http2:0.0.3'
implementation 'com.jauntsdn.netty:netty-websocket-http2:0.1.0'
}
```

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,4 @@ def projectVersion(project) {
return project.version + versionSuffix
}

defaultTasks 'clean', 'build'
defaultTasks "clean", "build"
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
group=com.jauntsdn.netty
version=0.1.0
version=1.0.0

googleJavaFormatPluginVersion=0.9
dependencyManagementPluginVersion=1.0.10.RELEASE
Expand Down
2 changes: 1 addition & 1 deletion gradle/publishing.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ subprojects {
name = "sonatype"
def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/"
url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
url = version.endsWith("SNAPSHOT") ? snapshotsRepoUrl : releasesRepoUrl
credentials {
username = project.property("ossrhUsername")
password = project.property("ossrhPassword")
Expand Down
3 changes: 3 additions & 0 deletions lws_client.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

./gradlew netty-websocket-http2-example:runLwsClient
5 changes: 5 additions & 0 deletions netty-websocket-http2-example/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,8 @@ task runChannelClient(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
main = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.channelclient.Main"
}

task runLwsClient(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
main = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.lwsclient.Main"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* 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.example.lwsclient;

import static io.netty.channel.ChannelHandler.Sharable;

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.example.Security;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
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.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 io.netty.util.concurrent.ScheduledFuture;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
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", "libwebsockets.org");
int port = Integer.parseInt(System.getProperty("PORT", "443"));
InetSocketAddress address = new InetSocketAddress(host, port);

logger.info("\n==> libwebsockets.org websocket-over-http2 client\n");
logger.info("\n==> Remote address: {}:{}", host, port);
logger.info("\n==> Dumb increment demo of https://libwebsockets.org/testserver/");

final SslContext sslContext = Security.clientSslContext();
Channel channel =
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(
new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
SslHandler sslHandler = sslContext.newHandler(ch.alloc());
Http2FrameCodecBuilder http2FrameCodecBuilder =
Http2FrameCodecBuilder.forClient();
http2FrameCodecBuilder.initialSettings().initialWindowSize(10_000);
Http2FrameCodec http2FrameCodec = http2FrameCodecBuilder.build();
Http2WebSocketClientHandler http2WebSocketClientHandler =
Http2WebSocketClientHandler.builder()
.decoderConfig(
WebSocketDecoderConfig.newBuilder()
.allowExtensions(true)
.allowMaskMismatch(true)
.build())
.handshakeTimeoutMillis(15_000)
.compression(true)
.build();

ch.pipeline().addLast(sslHandler, http2FrameCodec, http2WebSocketClientHandler);
}
})
.connect(address)
.sync()
.channel();

Http2WebSocketClientHandshaker handShaker = Http2WebSocketClientHandshaker.create(channel);

Http2Headers headers =
new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/0.1.0");
ChannelFuture handshake =
handShaker.handshake(
"/", "dumb-increment-protocol", headers, new WebSocketDumbIncrementHandler());

handshake.addListener(new WebSocketFutureListener());

Channel echoWebSocketChannel = handshake.channel();

EventLoopGroup eventLoop = echoWebSocketChannel.eventLoop();

echoWebSocketChannel.closeFuture().sync();
logger.info("==> Websocket closed. Terminating client...");
eventLoop.shutdownGracefully();
logger.info("==> Client terminated");
}

@Sharable
private static class WebSocketDumbIncrementHandler
extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private ScheduledFuture<?> sendResetFuture;
private ScheduledFuture<?> receiveReportFuture;
private int receiveCounter;
private int receiveValue;

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
sendResetFuture =
ctx.executor()
.scheduleAtFixedRate(
() -> {
ctx.writeAndFlush(new TextWebSocketFrame("reset\n"));
logger.info("==> Sent reset counter websocket frame");
},
5_000,
5_000,
TimeUnit.MILLISECONDS);

receiveReportFuture =
ctx.executor()
.scheduleAtFixedRate(
() ->
logger.info(
"==> Received {} frames, latest value: {}", receiveCounter, receiveValue),
1_000,
1_000,
TimeUnit.MILLISECONDS);

super.channelActive(ctx);
}

@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
sendResetFuture.cancel(true);
receiveReportFuture.cancel(true);
super.channelInactive(ctx);
}

@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame webSocketFrame) {
receiveValue = Integer.parseInt(webSocketFrame.text());
receiveCounter++;
}
}

private static class WebSocketFutureListener
implements GenericFutureListener<Future<? super Void>> {
@Override
public void operationComplete(Future<? super Void> future) {
if (future.isSuccess()) {
logger.info("==> Websocket channel future success");
} else {
logger.info("==> Websocket channel future error: {}", future.cause().toString());
}
}
}
}
2 changes: 1 addition & 1 deletion netty-websocket-http2-perftest/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

description = "Netty based implementation of rfc8441 - bootstrapping websockets with http/2. Performance test project"

def nettyNativeTransportVersion = "4.1.51.Final"
def nettyNativeTransportVersion = "4.1.52.Final"

dependencies {
implementation project(":netty-websocket-http2")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ io.netty:netty-common:4.1.52.Final
io.netty:netty-handler:4.1.52.Final
io.netty:netty-resolver:4.1.52.Final
io.netty:netty-tcnative-boringssl-static:2.0.34.Final
io.netty:netty-transport-native-epoll:4.1.51.Final
io.netty:netty-transport-native-epoll:4.1.52.Final
io.netty:netty-transport-native-unix-common:4.1.52.Final
io.netty:netty-transport:4.1.52.Final
org.hdrhistogram:HdrHistogram:2.1.12
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,17 +382,18 @@ private void handshakeImmediate(Handshake handshake, boolean supportsWebSocket)
int streamId = streamIdFactory.incrementAndGetNextStreamId();
webSocketsParent.register(streamId, webSocketChannel.setStreamId(streamId));

String path = webSocketChannel.path();
String authority = authority();
String path = webSocketChannel.path();
Http2Headers headers =
Http2WebSocketProtocol.extendedConnect()
.scheme(scheme)
.authority(authority)
.path(path)
/* sec-websocket-version=13 only */
.set(
Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_NAME,
Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_VALUE);
Http2WebSocketProtocol.extendedConnect(
new DefaultHttp2Headers()
.scheme(scheme)
.authority(authority)
.path(path)
/* sec-websocket-version=13 only */
.set(
Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_NAME,
Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_VALUE));

/*compression*/
PerMessageDeflateClientExtensionHandshaker handshaker = compressionHandshaker;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.jauntsdn.netty.handler.codec.http2.websocketx;

import io.netty.handler.codec.http2.DefaultHttp2Headers;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.util.AsciiString;

Expand All @@ -38,8 +37,8 @@ final class Http2WebSocketProtocol {
static final AsciiString HEADER_PROTOCOL_NAME_HANDSHAKED = AsciiString.of("x-protocol");
static final AsciiString HEADER_METHOD_CONNECT_HANDSHAKED = AsciiString.of("POST");

static Http2Headers extendedConnect() {
return new DefaultHttp2Headers()
static Http2Headers extendedConnect(Http2Headers headers) {
return headers
.method(Http2WebSocketProtocol.HEADER_METHOD_CONNECT)
.set(
Http2WebSocketProtocol.HEADER_PROTOCOL_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ void invalidWebSocketRequestRejected(Http2Headers invalidHttp2RequestHeaders) th

static Stream<Http2Headers> invalidWebSocketRequests() {
Http2Headers emptyPath =
Http2WebSocketProtocol.extendedConnect()
Http2WebSocketProtocol.extendedConnect(new DefaultHttp2Headers())
.scheme("https")
.authority("localhost")
.path("")
Expand All @@ -206,7 +206,7 @@ static Stream<Http2Headers> invalidWebSocketRequests() {
Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_VALUE);

Http2Headers emptyAuthority =
Http2WebSocketProtocol.extendedConnect()
Http2WebSocketProtocol.extendedConnect(new DefaultHttp2Headers())
.scheme("https")
.authority("")
.path("path")
Expand All @@ -216,7 +216,7 @@ static Stream<Http2Headers> invalidWebSocketRequests() {
Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_VALUE);

Http2Headers emptyScheme =
Http2WebSocketProtocol.extendedConnect()
Http2WebSocketProtocol.extendedConnect(new DefaultHttp2Headers())
.scheme("")
.authority("localhost")
.path("path")
Expand All @@ -226,7 +226,7 @@ static Stream<Http2Headers> invalidWebSocketRequests() {
Http2WebSocketProtocol.HEADER_WEBSOCKET_VERSION_VALUE);

Http2Headers nonHttpScheme =
Http2WebSocketProtocol.extendedConnect()
Http2WebSocketProtocol.extendedConnect(new DefaultHttp2Headers())
.scheme("ftp")
.authority("localhost")
.path("path")
Expand Down
Loading

0 comments on commit abaae62

Please sign in to comment.