netty: Support Host header on server-side
We want to know the single, unambiguous authority for the request. If
there is no authority, we use host instead. While authority would be
most typical for HTTP/2, requests proxied from HTTP/1 may use host
instead of authority.
This is generally useful, but the impetus is RBAC. See gRFC A41.
diff --git a/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersUtils.java b/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersUtils.java
index 04cf7f4..df7875f 100644
--- a/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersUtils.java
+++ b/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersUtils.java
@@ -365,12 +365,28 @@
AsciiString value = requireAsciiString(csValue);
if (equals(PATH_HEADER, name)) {
+ if (path != null) {
+ PlatformDependent.throwException(
+ connectionError(PROTOCOL_ERROR, "Duplicate :path header"));
+ }
path = value;
} else if (equals(AUTHORITY_HEADER, name)) {
+ if (authority != null) {
+ PlatformDependent.throwException(
+ connectionError(PROTOCOL_ERROR, "Duplicate :authority header"));
+ }
authority = value;
} else if (equals(METHOD_HEADER, name)) {
+ if (method != null) {
+ PlatformDependent.throwException(
+ connectionError(PROTOCOL_ERROR, "Duplicate :method header"));
+ }
method = value;
} else if (equals(SCHEME_HEADER, name)) {
+ if (scheme != null) {
+ PlatformDependent.throwException(
+ connectionError(PROTOCOL_ERROR, "Duplicate :scheme header"));
+ }
scheme = value;
} else {
PlatformDependent.throwException(
diff --git a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java
index ee21129..91f8f55 100644
--- a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java
+++ b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java
@@ -27,7 +27,9 @@
import static io.grpc.netty.Utils.TE_HEADER;
import static io.grpc.netty.Utils.TE_TRAILERS;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
+import static io.netty.handler.codec.http.HttpHeaderNames.HOST;
import static io.netty.handler.codec.http2.DefaultHttp2LocalFlowController.DEFAULT_WINDOW_UPDATE_RATIO;
+import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.AUTHORITY;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
@@ -383,6 +385,20 @@
return;
}
+ if (headers.authority() == null) {
+ List<CharSequence> hosts = headers.getAll(HOST);
+ if (hosts.size() > 1) {
+ // RFC 7230 section 5.4
+ respondWithHttpError(ctx, streamId, 400, Status.Code.INTERNAL,
+ "Multiple host headers");
+ return;
+ }
+ if (!hosts.isEmpty()) {
+ headers.add(AUTHORITY.value(), hosts.get(0));
+ }
+ }
+ headers.remove(HOST);
+
// Remove the leading slash of the path and get the fully qualified method name
CharSequence path = headers.path();
diff --git a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java
index efae5c0..2f01ed9 100644
--- a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java
+++ b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java
@@ -557,6 +557,77 @@
}
@Test
+ public void headersWithMultipleHostsShouldFail() throws Exception {
+ manualSetUp();
+ Http2Headers headers = new DefaultHttp2Headers()
+ .method(HTTP_METHOD)
+ .set(CONTENT_TYPE_HEADER, CONTENT_TYPE_GRPC)
+ .add(AsciiString.of("host"), AsciiString.of("example.com"))
+ .add(AsciiString.of("host"), AsciiString.of("bad.com"))
+ .path(new AsciiString("/foo/bar"));
+ ByteBuf headersFrame = headersFrame(STREAM_ID, headers);
+ channelRead(headersFrame);
+ Http2Headers responseHeaders = new DefaultHttp2Headers()
+ .set(InternalStatus.CODE_KEY.name(), String.valueOf(Code.INTERNAL.value()))
+ .set(InternalStatus.MESSAGE_KEY.name(), "Multiple host headers")
+ .status("" + 400)
+ .set(CONTENT_TYPE_HEADER, "text/plain; charset=utf-8");
+
+ verifyWrite()
+ .writeHeaders(
+ eq(ctx()),
+ eq(STREAM_ID),
+ eq(responseHeaders),
+ eq(0),
+ eq(false),
+ any(ChannelPromise.class));
+ }
+
+ @Test
+ public void headersWithAuthorityAndHostUsesAuthority() throws Exception {
+ manualSetUp();
+ Http2Headers headers = new DefaultHttp2Headers()
+ .method(HTTP_METHOD)
+ .authority("example.com")
+ .set(CONTENT_TYPE_HEADER, CONTENT_TYPE_GRPC)
+ .add(AsciiString.of("host"), AsciiString.of("bad.com"))
+ .path(new AsciiString("/foo/bar"));
+ ByteBuf headersFrame = headersFrame(STREAM_ID, headers);
+ channelRead(headersFrame);
+ Metadata.Key<String> hostKey = Metadata.Key.of("host", Metadata.ASCII_STRING_MARSHALLER);
+
+ ArgumentCaptor<NettyServerStream> streamCaptor =
+ ArgumentCaptor.forClass(NettyServerStream.class);
+ ArgumentCaptor<Metadata> metadataCaptor = ArgumentCaptor.forClass(Metadata.class);
+ verify(transportListener).streamCreated(streamCaptor.capture(), eq("foo/bar"),
+ metadataCaptor.capture());
+ Truth.assertThat(streamCaptor.getValue().getAuthority()).isEqualTo("example.com");
+ Truth.assertThat(metadataCaptor.getValue().get(hostKey)).isNull();
+ }
+
+ @Test
+ public void headersWithOnlyHostBecomesAuthority() throws Exception {
+ manualSetUp();
+ // No authority header
+ Http2Headers headers = new DefaultHttp2Headers()
+ .method(HTTP_METHOD)
+ .set(CONTENT_TYPE_HEADER, CONTENT_TYPE_GRPC)
+ .add(AsciiString.of("host"), AsciiString.of("example.com"))
+ .path(new AsciiString("/foo/bar"));
+ ByteBuf headersFrame = headersFrame(STREAM_ID, headers);
+ channelRead(headersFrame);
+ Metadata.Key<String> hostKey = Metadata.Key.of("host", Metadata.ASCII_STRING_MARSHALLER);
+
+ ArgumentCaptor<NettyServerStream> streamCaptor =
+ ArgumentCaptor.forClass(NettyServerStream.class);
+ ArgumentCaptor<Metadata> metadataCaptor = ArgumentCaptor.forClass(Metadata.class);
+ verify(transportListener).streamCreated(streamCaptor.capture(), eq("foo/bar"),
+ metadataCaptor.capture());
+ Truth.assertThat(streamCaptor.getValue().getAuthority()).isEqualTo("example.com");
+ Truth.assertThat(metadataCaptor.getValue().get(hostKey)).isNull();
+ }
+
+ @Test
public void keepAliveManagerOnDataReceived_headersRead() throws Exception {
manualSetUp();
ByteBuf headersFrame = headersFrame(STREAM_ID, new DefaultHttp2Headers());