Http2 to Http1.1 converter on new Http2 server API

Motivation:

http/2 and http/1.1 have similar protocols, and it's useful to be able
to implement a single server against a single interface. There's an
injection from http/1.1 messages to http/2 ones, so it makes sense to
make folks program against http/1.1 and upgrade them under the hood.

Modifications:

added a MessageToMessageCodec<Http2StreamFrame, HttpObject> which turns
every kind of Http2StreamFrame domain object into an HttpObject domain
object, and then back again on the way out.  This one is specialized for
servers, but it should be straightforward to make a symmetric one for
clients, or else extend this one.

Result:

fixes #5199, and it's now simple to make your Http2MultiplexCodec speak
Http1.1
This commit is contained in:
Moses Nakamura 2016-05-03 23:51:45 -07:00 committed by Scott Mitchell
parent f2ed3e6ce8
commit 9ce84dcb21
4 changed files with 477 additions and 2 deletions

View File

@ -0,0 +1,126 @@
/*
* Copyright 2016 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.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.ReferenceCountUtil;
import java.util.List;
/**
* This is a server-side adapter so that an http2 codec can be downgraded to
* appear as if it's speaking http/1.1.
*
* In particular, this handler converts from {@link Http2StreamFrame} to {@link
* HttpObject}, and back. For simplicity, it converts to chunked encoding
* unless the entire stream is a single header.
*/
public class Http2ServerDowngrader extends MessageToMessageCodec<Http2StreamFrame, HttpObject> {
private final boolean validateHeaders;
public Http2ServerDowngrader(boolean validateHeaders) {
this.validateHeaders = validateHeaders;
}
public Http2ServerDowngrader() {
this(true);
}
@Override
public boolean acceptInboundMessage(Object msg) throws Exception {
return (msg instanceof Http2HeadersFrame) || (msg instanceof Http2DataFrame);
}
@Override
protected void decode(ChannelHandlerContext ctx, Http2StreamFrame frame, List<Object> out) throws Exception {
if (frame instanceof Http2HeadersFrame) {
int id = 0; // not really the id
Http2HeadersFrame headersFrame = (Http2HeadersFrame) frame;
Http2Headers headers = headersFrame.headers();
if (headersFrame.isEndStream()) {
if (headers.method() == null) {
LastHttpContent last = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHeaders);
HttpConversionUtil.addHttp2ToHttpHeaders(id, headers, last.trailingHeaders(),
HttpVersion.HTTP_1_1, true, true);
out.add(last);
} else {
FullHttpRequest full = HttpConversionUtil.toFullHttpRequest(id, headers, ctx.alloc(),
validateHeaders);
out.add(full);
}
} else {
out.add(HttpConversionUtil.toHttpRequest(id, headersFrame.headers(), validateHeaders));
}
} else if (frame instanceof Http2DataFrame) {
Http2DataFrame dataFrame = (Http2DataFrame) frame;
if (dataFrame.isEndStream()) {
out.add(new DefaultLastHttpContent(dataFrame.content(), validateHeaders));
} else {
out.add(new DefaultHttpContent(dataFrame.content()));
}
}
ReferenceCountUtil.retain(frame);
}
private void encodeLastContent(LastHttpContent last, List<Object> out) {
boolean needFiller = !(last instanceof FullHttpResponse) && last.trailingHeaders().isEmpty();
if (last.content().isReadable() || needFiller) {
out.add(new DefaultHttp2DataFrame(last.content(), last.trailingHeaders().isEmpty()));
}
if (!last.trailingHeaders().isEmpty()) {
Http2Headers headers = HttpConversionUtil.toHttp2Headers(last.trailingHeaders(), validateHeaders);
out.add(new DefaultHttp2HeadersFrame(headers, true));
}
}
@Override
protected void encode(ChannelHandlerContext ctx, HttpObject obj, List<Object> out) throws Exception {
if (obj instanceof HttpResponse) {
Http2Headers headers = HttpConversionUtil.toHttp2Headers((HttpResponse) obj, validateHeaders);
boolean noMoreFrames = false;
if (obj instanceof FullHttpResponse) {
FullHttpResponse full = (FullHttpResponse) obj;
noMoreFrames = !full.content().isReadable() && full.trailingHeaders().isEmpty();
}
out.add(new DefaultHttp2HeadersFrame(headers, noMoreFrames));
}
if (obj instanceof LastHttpContent) {
LastHttpContent last = (LastHttpContent) obj;
encodeLastContent(last, out);
} else if (obj instanceof HttpContent) {
HttpContent cont = (HttpContent) obj;
out.add(new DefaultHttp2DataFrame(cont.content(), false));
}
ReferenceCountUtil.retain(obj);
}
}

View File

@ -17,6 +17,7 @@ package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
@ -230,7 +231,7 @@ public final class HttpConversionUtil {
* @return A new request object which represents headers/data
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
*/
public static FullHttpRequest toHttpRequest(int streamId, Http2Headers http2Headers, ByteBufAllocator alloc,
public static FullHttpRequest toFullHttpRequest(int streamId, Http2Headers http2Headers, ByteBufAllocator alloc,
boolean validateHttpHeaders)
throws Http2Exception {
// HTTP/2 does not define a way to carry the version identifier that is included in the HTTP/1.1 request line.
@ -252,6 +253,37 @@ public final class HttpConversionUtil {
return msg;
}
/**
* Create a new object to contain the request data.
*
* @param streamId The stream associated with the request
* @param http2Headers The initial set of HTTP/2 headers to create the request with
* @param validateHttpHeaders <ul>
* <li>{@code true} to validate HTTP headers in the http-codec</li>
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
* </ul>
* @return A new request object which represents headers for a chunked request
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
*/
public static HttpRequest toHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
throws Http2Exception {
// HTTP/2 does not define a way to carry the version identifier that is included in the HTTP/1.1 request line.
final CharSequence method = checkNotNull(http2Headers.method(),
"method header cannot be null in conversion to HTTP/1.x");
final CharSequence path = checkNotNull(http2Headers.path(),
"path header cannot be null in conversion to HTTP/1.x");
HttpRequest msg = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method.toString()),
path.toString(), validateHttpHeaders);
try {
addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, true);
} catch (Http2Exception e) {
throw e;
} catch (Throwable t) {
throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
}
return msg;
}
/**
* Translate and add HTTP/2 headers to HTTP/1.x headers.
*

View File

@ -152,7 +152,7 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter {
protected FullHttpMessage newMessage(Http2Stream stream, Http2Headers headers, boolean validateHttpHeaders,
ByteBufAllocator alloc)
throws Http2Exception {
return connection.isServer() ? HttpConversionUtil.toHttpRequest(stream.id(), headers, alloc,
return connection.isServer() ? HttpConversionUtil.toFullHttpRequest(stream.id(), headers, alloc,
validateHttpHeaders) : HttpConversionUtil.toHttpResponse(stream.id(), headers, alloc,
validateHttpHeaders);
}

View File

@ -0,0 +1,317 @@
/*
* Copyright 2016 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.embedded.EmbeddedChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.CharsetUtil;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
public class Http2ServerDowngraderTest {
@Test
public void testUpgradeEmptyFullResponse() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
assertTrue(ch.writeOutbound(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK)));
Http2HeadersFrame headersFrame = ch.readOutbound();
assertThat(headersFrame.headers().status().toString(), is("200"));
assertTrue(headersFrame.isEndStream());
assertThat(ch.readOutbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testUpgradeNonEmptyFullResponse() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
assertTrue(ch.writeOutbound(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, hello)));
Http2HeadersFrame headersFrame = ch.readOutbound();
assertThat(headersFrame.headers().status().toString(), is("200"));
assertFalse(headersFrame.isEndStream());
Http2DataFrame dataFrame = ch.readOutbound();
assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world"));
assertTrue(dataFrame.isEndStream());
assertThat(ch.readOutbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testUpgradeEmptyFullResponseWithTrailers() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
HttpHeaders trailers = response.trailingHeaders();
trailers.set("key", "value");
assertTrue(ch.writeOutbound(response));
Http2HeadersFrame headersFrame = ch.readOutbound();
assertThat(headersFrame.headers().status().toString(), is("200"));
assertFalse(headersFrame.isEndStream());
Http2HeadersFrame trailersFrame = ch.readOutbound();
assertThat(trailersFrame.headers().get("key").toString(), is("value"));
assertTrue(trailersFrame.isEndStream());
assertThat(ch.readOutbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testUpgradeNonEmptyFullResponseWithTrailers() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, hello);
HttpHeaders trailers = response.trailingHeaders();
trailers.set("key", "value");
assertTrue(ch.writeOutbound(response));
Http2HeadersFrame headersFrame = ch.readOutbound();
assertThat(headersFrame.headers().status().toString(), is("200"));
assertFalse(headersFrame.isEndStream());
Http2DataFrame dataFrame = ch.readOutbound();
assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world"));
assertFalse(dataFrame.isEndStream());
Http2HeadersFrame trailersFrame = ch.readOutbound();
assertThat(trailersFrame.headers().get("key").toString(), is("value"));
assertTrue(trailersFrame.isEndStream());
assertThat(ch.readOutbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testUpgradeHeaders() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
assertTrue(ch.writeOutbound(response));
Http2HeadersFrame headersFrame = ch.readOutbound();
assertThat(headersFrame.headers().status().toString(), is("200"));
assertFalse(headersFrame.isEndStream());
assertThat(ch.readOutbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testUpgradeChunk() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
HttpContent content = new DefaultHttpContent(hello);
assertTrue(ch.writeOutbound(content));
Http2DataFrame dataFrame = ch.readOutbound();
assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world"));
assertFalse(dataFrame.isEndStream());
assertThat(ch.readOutbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testUpgradeEmptyEnd() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
LastHttpContent end = LastHttpContent.EMPTY_LAST_CONTENT;
assertTrue(ch.writeOutbound(end));
Http2DataFrame emptyFrame = ch.readOutbound();
assertThat(emptyFrame.content().readableBytes(), is(0));
assertTrue(emptyFrame.isEndStream());
assertThat(ch.readOutbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testUpgradeDataEnd() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
LastHttpContent end = new DefaultLastHttpContent(hello, true);
assertTrue(ch.writeOutbound(end));
Http2DataFrame dataFrame = ch.readOutbound();
assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world"));
assertTrue(dataFrame.isEndStream());
assertThat(ch.readOutbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testUpgradeTrailers() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
LastHttpContent trailers = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, true);
HttpHeaders headers = trailers.trailingHeaders();
headers.set("key", "value");
assertTrue(ch.writeOutbound(trailers));
Http2HeadersFrame headerFrame = ch.readOutbound();
assertThat(headerFrame.headers().get("key").toString(), is("value"));
assertTrue(headerFrame.isEndStream());
assertThat(ch.readOutbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testUpgradeDataEndWithTrailers() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
LastHttpContent trailers = new DefaultLastHttpContent(hello, true);
HttpHeaders headers = trailers.trailingHeaders();
headers.set("key", "value");
assertTrue(ch.writeOutbound(trailers));
Http2DataFrame dataFrame = ch.readOutbound();
assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world"));
assertFalse(dataFrame.isEndStream());
Http2HeadersFrame headerFrame = ch.readOutbound();
assertThat(headerFrame.headers().get("key").toString(), is("value"));
assertTrue(headerFrame.isEndStream());
assertThat(ch.readOutbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testDowngradeHeaders() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
Http2Headers headers = new DefaultHttp2Headers();
headers.path("/");
headers.method("GET");
assertTrue(ch.writeInbound(new DefaultHttp2HeadersFrame(headers)));
HttpRequest request = ch.readInbound();
assertThat(request.uri(), is("/"));
assertThat(request.method(), is(HttpMethod.GET));
assertThat(request.protocolVersion(), is(HttpVersion.HTTP_1_1));
assertFalse(request instanceof FullHttpRequest);
assertThat(ch.readInbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testDowngradeFullHeaders() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
Http2Headers headers = new DefaultHttp2Headers();
headers.path("/");
headers.method("GET");
assertTrue(ch.writeInbound(new DefaultHttp2HeadersFrame(headers, true)));
FullHttpRequest request = ch.readInbound();
assertThat(request.uri(), is("/"));
assertThat(request.method(), is(HttpMethod.GET));
assertThat(request.protocolVersion(), is(HttpVersion.HTTP_1_1));
assertThat(request.content().readableBytes(), is(0));
assertTrue(request.trailingHeaders().isEmpty());
assertThat(ch.readInbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testDowngradeTrailers() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
Http2Headers headers = new DefaultHttp2Headers();
headers.set("key", "value");
assertTrue(ch.writeInbound(new DefaultHttp2HeadersFrame(headers, true)));
LastHttpContent trailers = ch.readInbound();
assertThat(trailers.content().readableBytes(), is(0));
assertThat(trailers.trailingHeaders().get("key").toString(), is("value"));
assertFalse(trailers instanceof FullHttpRequest);
assertThat(ch.readInbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testDowngradeData() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
assertTrue(ch.writeInbound(new DefaultHttp2DataFrame(hello)));
HttpContent content = ch.readInbound();
assertThat(content.content().toString(CharsetUtil.UTF_8), is("hello world"));
assertFalse(content instanceof LastHttpContent);
assertThat(ch.readInbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testDowngradeEndData() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
assertTrue(ch.writeInbound(new DefaultHttp2DataFrame(hello, true)));
LastHttpContent content = ch.readInbound();
assertThat(content.content().toString(CharsetUtil.UTF_8), is("hello world"));
assertTrue(content.trailingHeaders().isEmpty());
assertThat(ch.readInbound(), is(nullValue()));
assertFalse(ch.finish());
}
@Test
public void testPassThroughOther() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader());
Http2ResetFrame reset = new DefaultHttp2ResetFrame(0);
Http2GoAwayFrame goaway = new DefaultHttp2GoAwayFrame(0);
assertTrue(ch.writeInbound(reset));
assertTrue(ch.writeInbound(goaway));
assertEquals(ch.readInbound(), reset);
assertEquals(ch.readInbound(), goaway);
assertThat(ch.readInbound(), is(nullValue()));
assertFalse(ch.finish());
}
}