HTTP/2: Ensure newStream() is called only once per connection upgrade and the correct handler is used (#9396)

Motivation:

306299323c introduced some code change to move the responsibility of creating the stream for the upgrade to Http2FrameCodec. Unfortunaly this lead to the situation of having newStream().setStreamAndProperty(...) be called twice. Because of this we only ever saw the channelActive(...) on Http2StreamChannel but no other events as the mapping was replaced on the second newStream().setStreamAndProperty(...) call.

Beside this we also did not use the correct handler for the upgrade stream in some cases

Modifications:

- Just remove the Http2FrameCodec.onHttpClientUpgrade() method and so let the base class handle all of it. The stream is created correctly as part of the ConnectionListener implementation of Http2FrameCodec already.
- Consolidate logic of creating stream channels
- Adjust unit test to capture the bug

Result:

Fixes https://github.com/netty/netty/issues/9395
This commit is contained in:
Norman Maurer 2019-07-23 21:05:39 +02:00
parent 90b788bef7
commit d6c05e9b31
5 changed files with 52 additions and 46 deletions

View File

@ -232,13 +232,6 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
// sub-class can override this for extra steps that needs to be done when the handler is added. // sub-class can override this for extra steps that needs to be done when the handler is added.
} }
@Override
public void onHttpClientUpgrade() throws Http2Exception {
super.onHttpClientUpgrade();
// Now make a new Http2FrameStream, set it's underlying Http2Stream, and initialize it.
newStream().setStreamAndProperty(streamKey, connection().stream(HTTP_UPGRADE_STREAM_ID));
}
/** /**
* Handles the cleartext HTTP upgrade event. If an upgrade occurred, sends a simple response via * Handles the cleartext HTTP upgrade event. If an upgrade occurred, sends a simple response via
* HTTP/2 on stream 1 (the stream specifically reserved for cleartext HTTP upgrade). * HTTP/2 on stream 1 (the stream specifically reserved for cleartext HTTP upgrade).

View File

@ -149,39 +149,35 @@ public class Http2MultiplexCodec extends Http2FrameCodec {
ctx.fireChannelRead(frame); ctx.fireChannelRead(frame);
} }
private void onHttp2UpgradeStreamInitialized(ChannelHandlerContext ctx, DefaultHttp2FrameStream stream) {
assert stream.state() == Http2Stream.State.HALF_CLOSED_LOCAL;
AbstractHttp2StreamChannel ch = new Http2MultiplexCodecStreamChannel(stream, null);
ch.closeOutbound();
// Add our upgrade handler to the channel and then register the channel.
// The register call fires the channelActive, etc.
ch.pipeline().addLast(upgradeStreamHandler);
ChannelFuture future = ch.register();
if (future.isDone()) {
Http2MultiplexHandler.registerDone(future);
} else {
future.addListener(Http2MultiplexHandler.CHILD_CHANNEL_REGISTRATION_LISTENER);
}
}
@Override @Override
final void onHttp2StreamStateChanged(ChannelHandlerContext ctx, DefaultHttp2FrameStream stream) { final void onHttp2StreamStateChanged(ChannelHandlerContext ctx, DefaultHttp2FrameStream stream) {
switch (stream.state()) { switch (stream.state()) {
case HALF_CLOSED_LOCAL: case HALF_CLOSED_LOCAL:
if (stream.id() == HTTP_UPGRADE_STREAM_ID) { if (stream.id() != HTTP_UPGRADE_STREAM_ID) {
onHttp2UpgradeStreamInitialized(ctx, stream); // Ignore everything which was not caused by an upgrade
}
break; break;
}
// fall-through
case HALF_CLOSED_REMOTE: case HALF_CLOSED_REMOTE:
// fall-through
case OPEN: case OPEN:
if (stream.attachment != null) { if (stream.attachment != null) {
// ignore if child channel was already created. // ignore if child channel was already created.
break; break;
} }
// fall-trough final Http2MultiplexCodecStreamChannel streamChannel;
ChannelFuture future = new Http2MultiplexCodecStreamChannel(stream, inboundStreamHandler).register(); // We need to handle upgrades special when on the client side.
if (stream.id() == HTTP_UPGRADE_STREAM_ID && !connection().isServer()) {
// Add our upgrade handler to the channel and then register the channel.
// The register call fires the channelActive, etc.
assert upgradeStreamHandler != null;
streamChannel = new Http2MultiplexCodecStreamChannel(stream, upgradeStreamHandler);
streamChannel.closeOutbound();
} else {
streamChannel = new Http2MultiplexCodecStreamChannel(stream, inboundStreamHandler);
}
ChannelFuture future = streamChannel.register();
if (future.isDone()) { if (future.isDone()) {
Http2MultiplexHandler.registerDone(future); Http2MultiplexHandler.registerDone(future);
} else { } else {

View File

@ -209,26 +209,24 @@ public final class Http2MultiplexHandler extends Http2ChannelDuplexHandler {
// Ignore everything which was not caused by an upgrade // Ignore everything which was not caused by an upgrade
break; break;
} }
// We must have an upgrade handler or else we can't handle the stream // fall-through
if (upgradeStreamHandler == null) {
throw connectionError(INTERNAL_ERROR,
"Client is misconfigured for upgrade requests");
}
// fall-trough
case HALF_CLOSED_REMOTE: case HALF_CLOSED_REMOTE:
// fall-trough // fall-through
case OPEN: case OPEN:
if (stream.attachment != null) { if (stream.attachment != null) {
// ignore if child channel was already created. // ignore if child channel was already created.
break; break;
} }
final AbstractHttp2StreamChannel ch; final AbstractHttp2StreamChannel ch;
if (stream.state() == Http2Stream.State.HALF_CLOSED_LOCAL) { // We need to handle upgrades special when on the client side.
ch = new Http2MultiplexHandlerStreamChannel(stream, null); if (stream.id() == Http2CodecUtil.HTTP_UPGRADE_STREAM_ID && !isServer(ctx)) {
// We must have an upgrade handler or else we can't handle the stream
if (upgradeStreamHandler == null) {
throw connectionError(INTERNAL_ERROR,
"Client is misconfigured for upgrade requests");
}
ch = new Http2MultiplexHandlerStreamChannel(stream, upgradeStreamHandler);
ch.closeOutbound(); ch.closeOutbound();
// Add our upgrade handler to the channel and then register the channel.
// The register call fires the channelActive, etc.
ch.pipeline().addLast(upgradeStreamHandler);
} else { } else {
ch = new Http2MultiplexHandlerStreamChannel(stream, inboundStreamHandler); ch = new Http2MultiplexHandlerStreamChannel(stream, inboundStreamHandler);
} }
@ -277,9 +275,13 @@ public final class Http2MultiplexHandler extends Http2ChannelDuplexHandler {
ctx.fireExceptionCaught(cause); ctx.fireExceptionCaught(cause);
} }
private static boolean isServer(ChannelHandlerContext ctx) {
return ctx.channel().parent() instanceof ServerChannel;
}
private void onHttp2GoAwayFrame(ChannelHandlerContext ctx, final Http2GoAwayFrame goAwayFrame) { private void onHttp2GoAwayFrame(ChannelHandlerContext ctx, final Http2GoAwayFrame goAwayFrame) {
try { try {
final boolean server = ctx.channel().parent() instanceof ServerChannel; final boolean server = isServer(ctx);
forEachActiveStream(stream -> { forEachActiveStream(stream -> {
final int streamId = stream.id(); final int streamId = stream.id();
if (streamId > goAwayFrame.lastStreamId() && Http2CodecUtil.isStreamIdValid(streamId, server)) { if (streamId > goAwayFrame.lastStreamId() && Http2CodecUtil.isStreamIdValid(streamId, server)) {

View File

@ -22,21 +22,23 @@ import io.netty.channel.embedded.EmbeddedChannel;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public abstract class Http2MultiplexClientUpgradeTest<C extends Http2FrameCodec> { public abstract class Http2MultiplexClientUpgradeTest<C extends Http2FrameCodec> {
@ChannelHandler.Sharable @ChannelHandler.Sharable
final class NoopHandler implements ChannelHandler { static final class NoopHandler implements ChannelHandler {
@Override @Override
public void channelActive(ChannelHandlerContext ctx) { public void channelActive(ChannelHandlerContext ctx) {
ctx.channel().close(); ctx.channel().close();
} }
} }
private final class UpgradeHandler implements ChannelHandler { private static final class UpgradeHandler implements ChannelHandler {
Http2Stream.State stateOnActive; Http2Stream.State stateOnActive;
int streamId; int streamId;
boolean channelInactiveCalled;
@Override @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { public void channelActive(ChannelHandlerContext ctx) throws Exception {
@ -45,6 +47,12 @@ public abstract class Http2MultiplexClientUpgradeTest<C extends Http2FrameCodec>
streamId = ch.stream().id(); streamId = ch.stream().id();
ctx.fireChannelActive(); ctx.fireChannelActive();
} }
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
channelInactiveCalled = true;
ctx.fireChannelInactive();
}
} }
protected abstract C newCodec(ChannelHandler upgradeHandler); protected abstract C newCodec(ChannelHandler upgradeHandler);
@ -61,8 +69,10 @@ public abstract class Http2MultiplexClientUpgradeTest<C extends Http2FrameCodec>
assertFalse(upgradeHandler.stateOnActive.localSideOpen()); assertFalse(upgradeHandler.stateOnActive.localSideOpen());
assertTrue(upgradeHandler.stateOnActive.remoteSideOpen()); assertTrue(upgradeHandler.stateOnActive.remoteSideOpen());
assertEquals(1, upgradeHandler.streamId); assertNotNull(codec.connection().stream(Http2CodecUtil.HTTP_UPGRADE_STREAM_ID).getProperty(codec.streamKey));
assertEquals(Http2CodecUtil.HTTP_UPGRADE_STREAM_ID, upgradeHandler.streamId);
assertTrue(ch.finishAndReleaseAll()); assertTrue(ch.finishAndReleaseAll());
assertTrue(upgradeHandler.channelInactiveCalled);
} }
@Test(expected = Http2Exception.class) @Test(expected = Http2Exception.class)

View File

@ -17,6 +17,8 @@ package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.DefaultChannelId;
import io.netty.channel.ServerChannel;
import io.netty.channel.embedded.EmbeddedChannel; import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.DefaultHttpHeaders;
@ -31,6 +33,7 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito;
public class Http2ServerUpgradeCodecTest { public class Http2ServerUpgradeCodecTest {
@ -62,7 +65,9 @@ public class Http2ServerUpgradeCodecTest {
request.headers().set(HttpHeaderNames.UPGRADE, "h2c"); request.headers().set(HttpHeaderNames.UPGRADE, "h2c");
request.headers().set("HTTP2-Settings", "AAMAAABkAAQAAP__"); request.headers().set("HTTP2-Settings", "AAMAAABkAAQAAP__");
EmbeddedChannel channel = new EmbeddedChannel(new ChannelHandler() { }); ServerChannel parent = Mockito.mock(ServerChannel.class);
EmbeddedChannel channel = new EmbeddedChannel(parent, DefaultChannelId.newInstance(), true, false,
new ChannelHandler() { });
ChannelHandlerContext ctx = channel.pipeline().firstContext(); ChannelHandlerContext ctx = channel.pipeline().firstContext();
Http2ServerUpgradeCodec codec; Http2ServerUpgradeCodec codec;
if (multiplexer == null) { if (multiplexer == null) {