netty5/codec-http2/src/test/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandlerTest.java
Rukshani Athapathu c68e85b749 Fix h2c upgrade failure when multiple connection headers are present in upgrade request (#8848)
Motivation:

When more than one connection header is present in h2c upgrade request, upgrade fails. This is to fix that.

Modification:
In HttpServerUpgradeHandler's upgrade() method, check whether any of the connection header value is upgrade, not just the first header value which might return a different value other than upgrade.

Result:
Fixes #8846.

With this PR, now when multiple connection headers are sent with the upgrade request, upgrade will not fail.
2019-02-12 08:05:30 -08:00

292 lines
11 KiB
Java

/*
* Copyright 2017 The Netty Project
*
* The Netty Project licenses this file to you 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 io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerUpgradeHandler;
import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodec;
import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodecFactory;
import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeEvent;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler.PriorKnowledgeUpgradeEvent;
import io.netty.handler.codec.http2.Http2Stream.State;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import org.junit.After;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link CleartextHttp2ServerUpgradeHandler}
*/
public class CleartextHttp2ServerUpgradeHandlerTest {
private EmbeddedChannel channel;
private Http2FrameListener frameListener;
private Http2ConnectionHandler http2ConnectionHandler;
private List<Object> userEvents;
private void setUpServerChannel() {
frameListener = mock(Http2FrameListener.class);
http2ConnectionHandler = new Http2ConnectionHandlerBuilder()
.frameListener(frameListener).build();
UpgradeCodecFactory upgradeCodecFactory = new UpgradeCodecFactory() {
@Override
public UpgradeCodec newUpgradeCodec(CharSequence protocol) {
return new Http2ServerUpgradeCodec(http2ConnectionHandler);
}
};
userEvents = new ArrayList<Object>();
HttpServerCodec httpServerCodec = new HttpServerCodec();
HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(httpServerCodec, upgradeCodecFactory);
CleartextHttp2ServerUpgradeHandler handler = new CleartextHttp2ServerUpgradeHandler(
httpServerCodec, upgradeHandler, http2ConnectionHandler);
channel = new EmbeddedChannel(handler, new ChannelInboundHandlerAdapter() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
userEvents.add(evt);
}
});
}
@After
public void tearDown() throws Exception {
channel.finishAndReleaseAll();
}
@Test
public void priorKnowledge() throws Exception {
setUpServerChannel();
channel.writeInbound(Http2CodecUtil.connectionPrefaceBuf());
ByteBuf settingsFrame = settingsFrameBuf();
assertFalse(channel.writeInbound(settingsFrame));
assertEquals(1, userEvents.size());
assertTrue(userEvents.get(0) instanceof PriorKnowledgeUpgradeEvent);
assertEquals(100, http2ConnectionHandler.connection().local().maxActiveStreams());
assertEquals(65535, http2ConnectionHandler.connection().local().flowController().initialWindowSize());
verify(frameListener).onSettingsRead(
any(ChannelHandlerContext.class), eq(expectedSettings()));
}
@Test
public void upgrade() throws Exception {
String upgradeString = "GET / HTTP/1.1\r\n" +
"Host: example.com\r\n" +
"Connection: Upgrade, HTTP2-Settings\r\n" +
"Upgrade: h2c\r\n" +
"HTTP2-Settings: AAMAAABkAAQAAP__\r\n\r\n";
validateClearTextUpgrade(upgradeString);
}
@Test
public void upgradeWithMultipleConnectionHeaders() {
String upgradeString = "GET / HTTP/1.1\r\n" +
"Host: example.com\r\n" +
"Connection: keep-alive\r\n" +
"Connection: Upgrade, HTTP2-Settings\r\n" +
"Upgrade: h2c\r\n" +
"HTTP2-Settings: AAMAAABkAAQAAP__\r\n\r\n";
validateClearTextUpgrade(upgradeString);
}
@Test
public void requiredHeadersInSeparateConnectionHeaders() {
String upgradeString = "GET / HTTP/1.1\r\n" +
"Host: example.com\r\n" +
"Connection: keep-alive\r\n" +
"Connection: HTTP2-Settings\r\n" +
"Connection: Upgrade\r\n" +
"Upgrade: h2c\r\n" +
"HTTP2-Settings: AAMAAABkAAQAAP__\r\n\r\n";
validateClearTextUpgrade(upgradeString);
}
@Test
public void priorKnowledgeInFragments() throws Exception {
setUpServerChannel();
ByteBuf connectionPreface = Http2CodecUtil.connectionPrefaceBuf();
assertFalse(channel.writeInbound(connectionPreface.readBytes(5), connectionPreface));
ByteBuf settingsFrame = settingsFrameBuf();
assertFalse(channel.writeInbound(settingsFrame));
assertEquals(1, userEvents.size());
assertTrue(userEvents.get(0) instanceof PriorKnowledgeUpgradeEvent);
assertEquals(100, http2ConnectionHandler.connection().local().maxActiveStreams());
assertEquals(65535, http2ConnectionHandler.connection().local().flowController().initialWindowSize());
verify(frameListener).onSettingsRead(
any(ChannelHandlerContext.class), eq(expectedSettings()));
}
@Test
public void downgrade() throws Exception {
setUpServerChannel();
String requestString = "GET / HTTP/1.1\r\n" +
"Host: example.com\r\n\r\n";
ByteBuf inbound = Unpooled.buffer().writeBytes(requestString.getBytes(CharsetUtil.US_ASCII));
assertTrue(channel.writeInbound(inbound));
Object firstInbound = channel.readInbound();
assertTrue(firstInbound instanceof HttpRequest);
HttpRequest request = (HttpRequest) firstInbound;
assertEquals(HttpMethod.GET, request.method());
assertEquals("/", request.uri());
assertEquals(HttpVersion.HTTP_1_1, request.protocolVersion());
assertEquals(new DefaultHttpHeaders().add("Host", "example.com"), request.headers());
((LastHttpContent) channel.readInbound()).release();
assertNull(channel.readInbound());
}
@Test
public void usedHttp2MultiplexCodec() throws Exception {
final Http2MultiplexCodec http2Codec = new Http2MultiplexCodecBuilder(true, new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
}
}).build();
UpgradeCodecFactory upgradeCodecFactory = new UpgradeCodecFactory() {
@Override
public UpgradeCodec newUpgradeCodec(CharSequence protocol) {
return new Http2ServerUpgradeCodec(http2Codec);
}
};
http2ConnectionHandler = http2Codec;
userEvents = new ArrayList<Object>();
HttpServerCodec httpServerCodec = new HttpServerCodec();
HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(httpServerCodec, upgradeCodecFactory);
CleartextHttp2ServerUpgradeHandler handler = new CleartextHttp2ServerUpgradeHandler(
httpServerCodec, upgradeHandler, http2Codec);
channel = new EmbeddedChannel(handler, new ChannelInboundHandlerAdapter() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
userEvents.add(evt);
}
});
assertFalse(channel.writeInbound(Http2CodecUtil.connectionPrefaceBuf()));
ByteBuf settingsFrame = settingsFrameBuf();
assertTrue(channel.writeInbound(settingsFrame));
assertEquals(1, userEvents.size());
assertTrue(userEvents.get(0) instanceof PriorKnowledgeUpgradeEvent);
}
private static ByteBuf settingsFrameBuf() {
ByteBuf settingsFrame = Unpooled.buffer();
settingsFrame.writeMedium(12); // Payload length
settingsFrame.writeByte(0x4); // Frame type
settingsFrame.writeByte(0x0); // Flags
settingsFrame.writeInt(0x0); // StreamId
settingsFrame.writeShort(0x3);
settingsFrame.writeInt(100);
settingsFrame.writeShort(0x4);
settingsFrame.writeInt(65535);
return settingsFrame;
}
private static Http2Settings expectedSettings() {
return new Http2Settings().maxConcurrentStreams(100).initialWindowSize(65535);
}
private void validateClearTextUpgrade(String upgradeString) {
setUpServerChannel();
ByteBuf upgrade = Unpooled.copiedBuffer(upgradeString, CharsetUtil.US_ASCII);
assertFalse(channel.writeInbound(upgrade));
assertEquals(1, userEvents.size());
Object userEvent = userEvents.get(0);
assertTrue(userEvent instanceof UpgradeEvent);
assertEquals("h2c", ((UpgradeEvent) userEvent).protocol());
ReferenceCountUtil.release(userEvent);
assertEquals(100, http2ConnectionHandler.connection().local().maxActiveStreams());
assertEquals(65535, http2ConnectionHandler.connection().local().flowController().initialWindowSize());
assertEquals(1, http2ConnectionHandler.connection().numActiveStreams());
assertNotNull(http2ConnectionHandler.connection().stream(1));
Http2Stream stream = http2ConnectionHandler.connection().stream(1);
assertEquals(State.HALF_CLOSED_REMOTE, stream.state());
assertFalse(stream.isHeadersSent());
String expectedHttpResponse = "HTTP/1.1 101 Switching Protocols\r\n" +
"connection: upgrade\r\n" +
"upgrade: h2c\r\n\r\n";
ByteBuf responseBuffer = channel.readOutbound();
assertEquals(expectedHttpResponse, responseBuffer.toString(CharsetUtil.UTF_8));
responseBuffer.release();
// Check that the preface was send (a.k.a the settings frame)
ByteBuf settingsBuffer = channel.readOutbound();
assertNotNull(settingsBuffer);
settingsBuffer.release();
assertNull(channel.readOutbound());
}
}