HTTP/2 to HTTP/1 non-ascii headers reset stream

Motivation:
The HTTP/2 specification indicates that when converting from HTTP/2 to HTTP/1.x and non-ascii characters are detected that an error should be thrown.

Modifications:
- The ASCII validation is already done but the exception that is raised is not properly converted to a RST_STREAM error.

Result:
- If HTTP/2 to HTTP/1.x translation layer is in use and a non-ascii header is received then a RST_STREAM frame should be sent in response.
This commit is contained in:
Scott Mitchell 2014-11-01 16:42:49 -04:00
parent d5042baf58
commit 4a86dce2ca
2 changed files with 98 additions and 61 deletions

View File

@ -14,6 +14,7 @@
*/ */
package io.netty.handler.codec.http2; package io.netty.handler.codec.http2;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.handler.codec.AsciiString; import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.BinaryHeaders; import io.netty.handler.codec.BinaryHeaders;
import io.netty.handler.codec.TextHeaders.EntryVisitor; import io.netty.handler.codec.TextHeaders.EntryVisitor;
@ -210,8 +211,12 @@ public final class HttpUtil {
throws Http2Exception { throws Http2Exception {
// HTTP/2 does not define a way to carry the version identifier that is // HTTP/2 does not define a way to carry the version identifier that is
// included in the HTTP/1.1 request line. // included in the HTTP/1.1 request line.
FullHttpRequest msg = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(http2Headers.method() final AsciiString method = checkNotNull(http2Headers.method(),
.toString()), http2Headers.path().toString(), validateHttpHeaders); "method header cannot be null in conversion to HTTP/1.x");
final AsciiString path = checkNotNull(http2Headers.path(),
"path header cannot be null in conversion to HTTP/1.x");
FullHttpRequest msg = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method
.toString()), path.toString(), validateHttpHeaders);
addHttp2ToHttpHeaders(streamId, http2Headers, msg, false); addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
return msg; return msg;
} }
@ -235,7 +240,8 @@ public final class HttpUtil {
} catch (Http2Exception ex) { } catch (Http2Exception ex) {
throw ex; throw ex;
} catch (Exception ex) { } catch (Exception ex) {
PlatformDependent.throwException(ex); throw new Http2StreamException(streamId, Http2Error.PROTOCOL_ERROR,
"HTTP/2 to HTTP/1.x headers conversion error", ex);
} }
headers.remove(HttpHeaderNames.TRANSFER_ENCODING); headers.remove(HttpHeaderNames.TRANSFER_ENCODING);

View File

@ -14,12 +14,23 @@
*/ */
package io.netty.handler.codec.http2; package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.getEmbeddedHttp2Exception;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap; import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPipeline;
@ -28,6 +39,7 @@ import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.FullHttpMessage;
@ -39,8 +51,16 @@ import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.Http2TestUtil.FrameAdapter;
import io.netty.handler.codec.http2.Http2TestUtil.Http2Runnable;
import io.netty.util.CharsetUtil;
import io.netty.util.NetUtil; import io.netty.util.NetUtil;
import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Future;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -48,15 +68,6 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import static io.netty.handler.codec.http2.Http2TestUtil.*;
import static java.util.concurrent.TimeUnit.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/** /**
* Testing the {@link InboundHttp2ToHttpPriorityAdapter} and base class {@link InboundHttp2ToHttpAdapter} for HTTP/2 * Testing the {@link InboundHttp2ToHttpPriorityAdapter} and base class {@link InboundHttp2ToHttpAdapter} for HTTP/2
* frames into {@link HttpObject}s * frames into {@link HttpObject}s
@ -82,6 +93,7 @@ public class InboundHttp2ToHttpAdapterTest {
private int maxContentLength; private int maxContentLength;
private HttpResponseDelegator serverDelegator; private HttpResponseDelegator serverDelegator;
private HttpResponseDelegator clientDelegator; private HttpResponseDelegator clientDelegator;
private Http2Exception serverException;
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
@ -112,6 +124,18 @@ public class InboundHttp2ToHttpAdapterTest {
serverDelegator = new HttpResponseDelegator(serverListener, serverLatch); serverDelegator = new HttpResponseDelegator(serverListener, serverLatch);
p.addLast(serverDelegator); p.addLast(serverDelegator);
serverConnectedChannel = ch; serverConnectedChannel = ch;
p.addLast(new ChannelHandlerAdapter() {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Http2Exception e = getEmbeddedHttp2Exception(cause);
if (e != null) {
serverException = e;
serverLatch.countDown();
} else {
super.exceptionCaught(ctx, cause);
}
}
});
} }
}); });
@ -167,10 +191,8 @@ public class InboundHttp2ToHttpAdapterTest {
httpHeaders.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "example.org"); httpHeaders.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "example.org");
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
final Http2Headers http2Headers = final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
new DefaultHttp2Headers().method(as("GET")).scheme(as("https")) .authority(as("example.org")).path(as("/some/path/resource2"));
.authority(as("example.org"))
.path(as("/some/path/resource2"));
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
@ -184,10 +206,30 @@ public class InboundHttp2ToHttpAdapterTest {
capturedRequests = requestCaptor.getAllValues(); capturedRequests = requestCaptor.getAllValues();
assertEquals(request, capturedRequests.get(0)); assertEquals(request, capturedRequests.get(0));
} finally { } finally {
request.release(); request.release();
} }
} }
@Test
public void clientRequestSingleHeaderNonAsciiShouldThrow() throws Exception {
final Http2Headers http2Headers = new DefaultHttp2Headers()
.method(as("GET"))
.scheme(as("https"))
.authority(as("example.org"))
.path(as("/some/path/resource2"))
.add(new AsciiString("çã".getBytes(CharsetUtil.UTF_8)),
new AsciiString("Ãã".getBytes(CharsetUtil.UTF_8)));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
assertTrue(serverException instanceof Http2StreamException);
}
@Test @Test
public void clientRequestOneDataFrame() throws Exception { public void clientRequestOneDataFrame() throws Exception {
final String text = "hello world"; final String text = "hello world";
@ -198,8 +240,8 @@ public class InboundHttp2ToHttpAdapterTest {
HttpHeaders httpHeaders = request.headers(); HttpHeaders httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")) final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")).path(
.path(as("/some/path/resource2")); as("/some/path/resource2"));
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
@ -228,17 +270,17 @@ public class InboundHttp2ToHttpAdapterTest {
HttpHeaders httpHeaders = request.headers(); HttpHeaders httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")) final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")).path(
.path(as("/some/path/resource2")); as("/some/path/resource2"));
final int midPoint = text.length() / 2; final int midPoint = text.length() / 2;
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient()); frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter frameWriter.writeData(ctxClient(), 3, content.slice(0, midPoint).retain(), 0, false,
.writeData(ctxClient(), 3, content.slice(0, midPoint).retain(), 0, false, newPromiseClient()); newPromiseClient());
frameWriter.writeData(ctxClient(), 3, content.slice(midPoint, text.length() - midPoint).retain(), 0, frameWriter.writeData(ctxClient(), 3, content.slice(midPoint, text.length() - midPoint).retain(),
true, newPromiseClient()); 0, true, newPromiseClient());
ctxClient().flush(); ctxClient().flush();
} }
}); });
@ -262,8 +304,8 @@ public class InboundHttp2ToHttpAdapterTest {
HttpHeaders httpHeaders = request.headers(); HttpHeaders httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")) final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")).path(
.path(as("/some/path/resource2")); as("/some/path/resource2"));
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
@ -300,13 +342,10 @@ public class InboundHttp2ToHttpAdapterTest {
trailingHeaders.set("FoO", "goo"); trailingHeaders.set("FoO", "goo");
trailingHeaders.set("foO2", "goo2"); trailingHeaders.set("foO2", "goo2");
trailingHeaders.add("fOo2", "goo3"); trailingHeaders.add("fOo2", "goo3");
final Http2Headers http2Headers = final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")).path(
new DefaultHttp2Headers().method(as("GET")).path( as("/some/path/resource2"));
as("/some/path/resource2")); final Http2Headers http2Headers2 = new DefaultHttp2Headers().set(as("foo"), as("goo"))
final Http2Headers http2Headers2 = .set(as("foo2"), as("goo2")).add(as("foo2"), as("goo3"));
new DefaultHttp2Headers().set(as("foo"), as("goo"))
.set(as("foo2"), as("goo2"))
.add(as("foo2"), as("goo3"));
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
@ -340,13 +379,10 @@ public class InboundHttp2ToHttpAdapterTest {
trailingHeaders.set("Foo", "goo"); trailingHeaders.set("Foo", "goo");
trailingHeaders.set("fOo2", "goo2"); trailingHeaders.set("fOo2", "goo2");
trailingHeaders.add("foO2", "goo3"); trailingHeaders.add("foO2", "goo3");
final Http2Headers http2Headers = final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")).path(
new DefaultHttp2Headers().method(as("GET")).path( as("/some/path/resource2"));
as("/some/path/resource2")); final Http2Headers http2Headers2 = new DefaultHttp2Headers().set(as("foo"), as("goo"))
final Http2Headers http2Headers2 = .set(as("foo2"), as("goo2")).add(as("foo2"), as("goo3"));
new DefaultHttp2Headers().set(as("foo"), as("goo"))
.set(as("foo2"), as("goo2"))
.add(as("foo2"), as("goo3"));
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
@ -386,10 +422,10 @@ public class InboundHttp2ToHttpAdapterTest {
httpHeaders2.setInt(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3); httpHeaders2.setInt(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3);
httpHeaders2.setInt(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), 123); httpHeaders2.setInt(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), 123);
httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length()); httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length());
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("PUT")) final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("PUT")).path(
.path(as("/some/path/resource")); as("/some/path/resource"));
final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(as("PUT")) final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(as("PUT")).path(
.path(as("/some/path/resource2")); as("/some/path/resource2"));
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
@ -433,10 +469,10 @@ public class InboundHttp2ToHttpAdapterTest {
HttpHeaders httpHeaders2 = request2.headers(); HttpHeaders httpHeaders2 = request2.headers();
httpHeaders2.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); httpHeaders2.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length()); httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length());
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("PUT")) final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("PUT")).path(
.path(as("/some/path/resource")); as("/some/path/resource"));
final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(as("PUT")) final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(as("PUT")).path(
.path(as("/some/path/resource2")); as("/some/path/resource2"));
HttpHeaders httpHeaders3 = request3.headers(); HttpHeaders httpHeaders3 = request3.headers();
httpHeaders3.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); httpHeaders3.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
httpHeaders3.setInt(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3); httpHeaders3.setInt(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3);
@ -478,8 +514,8 @@ public class InboundHttp2ToHttpAdapterTest {
content, true); content, true);
final FullHttpMessage response2 = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CREATED, final FullHttpMessage response2 = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CREATED,
content2, true); content2, true);
final FullHttpMessage request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, final FullHttpMessage request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/push/test",
HttpMethod.GET, "/push/test", true); true);
try { try {
HttpHeaders httpHeaders = response.headers(); HttpHeaders httpHeaders = response.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
@ -494,8 +530,7 @@ public class InboundHttp2ToHttpAdapterTest {
httpHeaders = request.headers(); httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
final Http2Headers http2Headers3 = new DefaultHttp2Headers().method(as("GET")) final Http2Headers http2Headers3 = new DefaultHttp2Headers().method(as("GET")).path(as("/push/test"));
.path(as("/push/test"));
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
@ -510,9 +545,8 @@ public class InboundHttp2ToHttpAdapterTest {
assertEquals(request, capturedRequests.get(0)); assertEquals(request, capturedRequests.get(0));
final Http2Headers http2Headers = new DefaultHttp2Headers().status(as("200")); final Http2Headers http2Headers = new DefaultHttp2Headers().status(as("200"));
final Http2Headers http2Headers2 = final Http2Headers http2Headers2 = new DefaultHttp2Headers().status(as("201")).scheme(as("https"))
new DefaultHttp2Headers().status(as("201")).scheme(as("https")) .authority(as("example.org"));
.authority(as("example.org"));
runInChannel(serverConnectedChannel, new Http2Runnable() { runInChannel(serverConnectedChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
@ -544,11 +578,8 @@ public class InboundHttp2ToHttpAdapterTest {
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE); httpHeaders.set(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
final Http2Headers http2Headers = final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("PUT")).path(as("/info/test"))
new DefaultHttp2Headers() .set(as(HttpHeaderNames.EXPECT.toString()), as(HttpHeaderValues.CONTINUE.toString()));
.method(as("PUT"))
.path(as("/info/test"))
.set(as(HttpHeaderNames.EXPECT.toString()), as(HttpHeaderValues.CONTINUE.toString()));
final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE); final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
final String text = "a big payload"; final String text = "a big payload";
final ByteBuf payload = Unpooled.copiedBuffer(text.getBytes()); final ByteBuf payload = Unpooled.copiedBuffer(text.getBytes());