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());