diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java index 2aef9409e2..e45996c8e4 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java @@ -15,78 +15,76 @@ */ package io.netty.handler.codec.http; -import io.netty.buffer.ByteBuf; -import io.netty.util.CharsetUtil; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import io.netty.util.AsciiString; import java.util.HashMap; import java.util.Map; -import static io.netty.util.internal.ObjectUtil.checkNotNull; - /** - * The request getMethod of HTTP or its derived protocols, such as + * The request method of HTTP or its derived protocols, such as * RTSP and * ICAP. */ public class HttpMethod implements Comparable { /** - * The OPTIONS getMethod represents a request for information about the communication options - * available on the request/response chain identified by the Request-URI. This getMethod allows + * The OPTIONS method represents a request for information about the communication options + * available on the request/response chain identified by the Request-URI. This method allows * the client to determine the options and/or requirements associated with a resource, or the * capabilities of a server, without implying a resource action or initiating a resource * retrieval. */ - public static final HttpMethod OPTIONS = new HttpMethod("OPTIONS", true); + public static final HttpMethod OPTIONS = new HttpMethod("OPTIONS"); /** - * The GET getMethod means retrieve whatever information (in the form of an entity) is identified + * The GET method means retrieve whatever information (in the form of an entity) is identified * by the Request-URI. If the Request-URI refers to a data-producing process, it is the * produced data which shall be returned as the entity in the response and not the source text * of the process, unless that text happens to be the output of the process. */ - public static final HttpMethod GET = new HttpMethod("GET", true); + public static final HttpMethod GET = new HttpMethod("GET"); /** - * The HEAD getMethod is identical to GET except that the server MUST NOT return a message-body + * The HEAD method is identical to GET except that the server MUST NOT return a message-body * in the response. */ - public static final HttpMethod HEAD = new HttpMethod("HEAD", true); + public static final HttpMethod HEAD = new HttpMethod("HEAD"); /** - * The POST getMethod is used to request that the origin server accept the entity enclosed in the + * The POST method is used to request that the origin server accept the entity enclosed in the * request as a new subordinate of the resource identified by the Request-URI in the * Request-Line. */ - public static final HttpMethod POST = new HttpMethod("POST", true); + public static final HttpMethod POST = new HttpMethod("POST"); /** - * The PUT getMethod requests that the enclosed entity be stored under the supplied Request-URI. + * The PUT method requests that the enclosed entity be stored under the supplied Request-URI. */ - public static final HttpMethod PUT = new HttpMethod("PUT", true); + public static final HttpMethod PUT = new HttpMethod("PUT"); /** - * The PATCH getMethod requests that a set of changes described in the + * The PATCH method requests that a set of changes described in the * request entity be applied to the resource identified by the Request-URI. */ - public static final HttpMethod PATCH = new HttpMethod("PATCH", true); + public static final HttpMethod PATCH = new HttpMethod("PATCH"); /** - * The DELETE getMethod requests that the origin server delete the resource identified by the + * The DELETE method requests that the origin server delete the resource identified by the * Request-URI. */ - public static final HttpMethod DELETE = new HttpMethod("DELETE", true); + public static final HttpMethod DELETE = new HttpMethod("DELETE"); /** - * The TRACE getMethod is used to invoke a remote, application-layer loop- back of the request + * The TRACE method is used to invoke a remote, application-layer loop- back of the request * message. */ - public static final HttpMethod TRACE = new HttpMethod("TRACE", true); + public static final HttpMethod TRACE = new HttpMethod("TRACE"); /** - * This specification reserves the getMethod name CONNECT for use with a proxy that can dynamically + * This specification reserves the method name CONNECT for use with a proxy that can dynamically * switch to being a tunnel */ - public static final HttpMethod CONNECT = new HttpMethod("CONNECT", true); + public static final HttpMethod CONNECT = new HttpMethod("CONNECT"); private static final Map methodMap = new HashMap(); @@ -104,7 +102,7 @@ public class HttpMethod implements Comparable { /** * Returns the {@link HttpMethod} represented by the specified name. - * If the specified name is a standard HTTP getMethod name, a cached instance + * If the specified name is a standard HTTP method name, a cached instance * will be returned. Otherwise, a new instance will be returned. */ public static HttpMethod valueOf(String name) { @@ -112,25 +110,16 @@ public class HttpMethod implements Comparable { return result != null ? result : new HttpMethod(name); } - private final String name; - private final byte[] bytes; + private final AsciiString name; /** - * Creates a new HTTP getMethod with the specified name. You will not need to - * create a new getMethod unless you are implementing a protocol derived from + * Creates a new HTTP method with the specified name. You will not need to + * create a new method unless you are implementing a protocol derived from * HTTP, such as * RTSP and * ICAP */ public HttpMethod(String name) { - this(name, false); - } - - private HttpMethod(String name, boolean bytes) { - if (name == null) { - throw new NullPointerException("name"); - } - name = checkNotNull(name, "name").trim(); if (name.isEmpty()) { throw new IllegalArgumentException("empty name"); @@ -143,18 +132,20 @@ public class HttpMethod implements Comparable { } } - this.name = name; - if (bytes) { - this.bytes = name.getBytes(CharsetUtil.US_ASCII); - } else { - this.bytes = null; - } + this.name = new AsciiString(name); } /** - * Returns the name of this getMethod. + * Returns the name of this method. */ public String name() { + return name.toString(); + } + + /** + * Returns the name of this method. + */ + public AsciiString asciiName() { return name; } @@ -175,19 +166,11 @@ public class HttpMethod implements Comparable { @Override public String toString() { - return name(); + return name.toString(); } @Override public int compareTo(HttpMethod o) { return name().compareTo(o.name()); } - - void encode(ByteBuf buf) { - if (bytes == null) { - HttpHeaders.encodeAscii0(name, buf); - } else { - buf.writeBytes(bytes); - } - } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequestEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequestEncoder.java index 3d8fdabc82..ffe9ce64fd 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequestEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequestEncoder.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.http; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import io.netty.util.CharsetUtil; import static io.netty.handler.codec.http.HttpConstants.*; @@ -36,7 +37,7 @@ public class HttpRequestEncoder extends HttpObjectEncoder { @Override protected void encodeInitialLine(ByteBuf buf, HttpRequest request) throws Exception { - request.method().encode(buf); + ByteBufUtil.writeAscii(buf, request.method().name()); buf.writeByte(SP); // Add / as absolute path if no is present. diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpUtil.java index a8d60465d8..304ef8f779 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpUtil.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpUtil.java @@ -14,10 +14,6 @@ */ package io.netty.handler.codec.http2; -import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; -import static io.netty.handler.codec.http2.Http2Exception.connectionError; -import static io.netty.handler.codec.http2.Http2Exception.streamError; -import static io.netty.util.internal.ObjectUtil.checkNotNull; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpMessage; @@ -45,6 +41,17 @@ import java.util.Map.Entry; import java.util.Set; import java.util.regex.Pattern; +import static io.netty.handler.codec.http.HttpScheme.HTTP; +import static io.netty.handler.codec.http.HttpScheme.HTTPS; +import static io.netty.handler.codec.http.HttpUtil.isAsteriskForm; +import static io.netty.handler.codec.http.HttpUtil.isOriginForm; +import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; +import static io.netty.handler.codec.http2.Http2Exception.connectionError; +import static io.netty.handler.codec.http2.Http2Exception.streamError; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.StringUtil.isNullOrEmpty; +import static io.netty.util.internal.StringUtil.length; + /** * Provides utility methods and constants for the HTTP/2 to HTTP conversion */ @@ -88,10 +95,10 @@ public final class HttpUtil { public static final HttpResponseStatus OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE = HttpResponseStatus.OK; /** - * This pattern will use to avoid compile it each time it is used - * when we need to replace some part of authority. + * rfc7540, 8.1.2.3 states the path must not + * be empty, and instead should be {@code /}. */ - private static final Pattern AUTHORITY_REPLACEMENT_PATTERN = Pattern.compile("^.*@"); + private static final AsciiString EMPTY_REQUEST_PATH = new AsciiString("/"); private HttpUtil() { } @@ -266,39 +273,41 @@ public final class HttpUtil { /** * Converts the given HTTP/1.x headers into HTTP/2 headers. + * The following headers are only used if they can not be found in from the {@code HOST} header or the + * {@code Request-Line} as defined by rfc7230 + *
    + *
  • {@link ExtensionHeaderNames#AUTHORITY}
  • + *
  • {@link ExtensionHeaderNames#SCHEME}
  • + *
+ * {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}. */ public static Http2Headers toHttp2Headers(HttpMessage in) throws Exception { final Http2Headers out = new DefaultHttp2Headers(); HttpHeaders inHeaders = in.headers(); if (in instanceof HttpRequest) { HttpRequest request = (HttpRequest) in; - out.path(new AsciiString(request.uri())); - out.method(new AsciiString(request.method().toString())); + URI requestTargetUri = URI.create(request.uri()); + out.path(toHttp2Path(requestTargetUri)); + out.method(request.method().asciiName()); - String value = inHeaders.getAsString(HttpHeaderNames.HOST); - if (value != null) { - URI hostUri = URI.create(value); - // The authority MUST NOT include the deprecated "userinfo" subcomponent - value = hostUri.getAuthority(); - if (value != null) { - out.authority(new AsciiString(AUTHORITY_REPLACEMENT_PATTERN.matcher(value).replaceFirst(""))); + // Attempt to take from HOST header before taking from the request-line + String host = inHeaders.getAsString(HttpHeaderNames.HOST); + boolean shouldSetAuthroity = !isOriginForm(requestTargetUri) && !isAsteriskForm(requestTargetUri); + if (host == null) { + if (shouldSetAuthroity) { + setHttp2Authority(inHeaders, requestTargetUri, out); } - value = hostUri.getScheme(); - if (value != null) { - out.scheme(new AsciiString(value)); + setHttp2Scheme(inHeaders, requestTargetUri, true, out); + } else { + URI hostUri = URI.create(host); + if (shouldSetAuthroity) { + setHttp2Authority(inHeaders, hostUri, out); + } + if (!setHttp2Scheme(inHeaders, hostUri, false, out)) { + /** :scheme must be present as defined by + rfc7540, 8.1.2.3. */ + setHttp2Scheme(inHeaders, requestTargetUri, true, out); } - } - - // Consume the Authority extension header if present - CharSequence cValue = inHeaders.get(ExtensionHeaderNames.AUTHORITY.text()); - if (cValue != null) { - out.authority(AsciiString.of(cValue)); - } - - // Consume the Scheme extension header if present - cValue = inHeaders.get(ExtensionHeaderNames.SCHEME.text()); - if (cValue != null) { - out.scheme(AsciiString.of(cValue)); } } else if (in instanceof HttpResponse) { HttpResponse response = (HttpResponse) in; @@ -333,6 +342,67 @@ public final class HttpUtil { return out; } + /** + * Generate a HTTP/2 {code :path} from a URI in accordance with + * rfc7230, 5.3. + */ + private static AsciiString toHttp2Path(URI uri) { + StringBuilder pathBuilder = new StringBuilder(length(uri.getPath()) + + length(uri.getQuery()) + length(uri.getFragment()) + 2); + if (!isNullOrEmpty(uri.getPath())) { + pathBuilder.append(uri.getPath()); + } + if (!isNullOrEmpty(uri.getQuery())) { + pathBuilder.append('?'); + pathBuilder.append(uri.getQuery()); + } + if (!isNullOrEmpty(uri.getFragment())) { + pathBuilder.append('#'); + pathBuilder.append(uri.getFragment()); + } + String path = pathBuilder.toString(); + return path.isEmpty() ? EMPTY_REQUEST_PATH : new AsciiString(path); + } + + private static void setHttp2Authority(HttpHeaders in, URI uri, Http2Headers out) { + // The authority MUST NOT include the deprecated "userinfo" subcomponent + String value = uri.getAuthority(); + if (value != null) { + int endOfUserInfo = value.indexOf('@'); + if (endOfUserInfo < 0) { + out.authority(new AsciiString(value)); + } else if (endOfUserInfo + 1 < value.length()) { + out.authority(new AsciiString(value.substring(endOfUserInfo + 1))); + } + } else { + // Consume the Authority extension header if present + CharSequence cValue = in.get(ExtensionHeaderNames.AUTHORITY.text()); + if (cValue != null) { + // Assume this is sanitized of all "userinfo" + out.authority(AsciiString.of(cValue)); + } + } + } + + private static boolean setHttp2Scheme(HttpHeaders in, URI uri, boolean mustSet, Http2Headers out) { + String value = uri.getScheme(); + if (value != null) { + out.scheme(new AsciiString(value)); + return true; + } + // Consume the Scheme extension header if present + CharSequence cValue = in.get(ExtensionHeaderNames.SCHEME.text()); + if (cValue != null) { + out.scheme(AsciiString.of(cValue)); + return true; + } + if (uri.getPort() >= 0 || mustSet) { + out.scheme(uri.getPort() == HTTPS.port() ? HTTPS.name() : HTTP.name()); + return true; + } + return false; + } + /** * Utility which translates HTTP/2 headers to HTTP/1 headers. */ diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerTest.java index 442c4a658e..82fd3c0398 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerTest.java @@ -53,6 +53,8 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; +import static io.netty.handler.codec.http.HttpMethod.CONNECT; +import static io.netty.handler.codec.http.HttpMethod.OPTIONS; import static io.netty.handler.codec.http.HttpMethod.GET; import static io.netty.handler.codec.http.HttpMethod.POST; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; @@ -60,6 +62,7 @@ import static io.netty.util.CharsetUtil.UTF_8; 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.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; @@ -147,6 +150,247 @@ public class HttpToHttp2ConnectionHandlerTest { any(ByteBuf.class), anyInt(), anyBoolean()); } + @Test + public void testOriginFormRequestTargetHandled() throws Exception { + bootstrapEnv(2, 1, 0); + final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/where?q=now&f=then#section1"); + final HttpHeaders httpHeaders = request.headers(); + httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + final Http2Headers http2Headers = + new DefaultHttp2Headers().method(new AsciiString("GET")) + .path(new AsciiString("/where?q=now&f=then#section1")) + .scheme(new AsciiString("http")); + ChannelPromise writePromise = newPromise(); + ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise); + + assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writePromise.isSuccess()); + assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writeFuture.isSuccess()); + awaitRequests(); + verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5), + eq(http2Headers), eq(0), anyShort(), anyBoolean(), eq(0), eq(true)); + verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class), anyInt(), + any(ByteBuf.class), anyInt(), anyBoolean()); + } + + @Test + public void testAbsoluteFormRequestTargetHandledFromHeaders() throws Exception { + bootstrapEnv(2, 1, 0); + final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/pub/WWW/TheProject.html"); + final HttpHeaders httpHeaders = request.headers(); + httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + httpHeaders.set(HttpHeaderNames.HOST, + "https://foouser@www.example.org:5555/ignored_host"); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.PATH.text(), "ignored_path"); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "ignored_authority"); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.SCHEME.text(), "ignored_scheme"); + final Http2Headers http2Headers = + new DefaultHttp2Headers().method(new AsciiString("GET")) + .path(new AsciiString("/pub/WWW/TheProject.html")) + .authority(new AsciiString("www.example.org:5555")).scheme(new AsciiString("https")); + ChannelPromise writePromise = newPromise(); + ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise); + + assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writePromise.isSuccess()); + assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writeFuture.isSuccess()); + awaitRequests(); + verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5), + eq(http2Headers), eq(0), anyShort(), anyBoolean(), eq(0), eq(true)); + verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class), anyInt(), + any(ByteBuf.class), anyInt(), anyBoolean()); + } + + @Test + public void testAbsoluteFormRequestTargetHandledFromHeadersNoHost() throws Exception { + bootstrapEnv(2, 1, 0); + final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/pub/WWW/TheProject.html"); + final HttpHeaders httpHeaders = request.headers(); + httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.PATH.text(), "ignored_path"); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "www.example.org:5555"); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.SCHEME.text(), "https"); + final Http2Headers http2Headers = + new DefaultHttp2Headers().method(new AsciiString("GET")) + .path(new AsciiString("/pub/WWW/TheProject.html")) + .authority(new AsciiString("www.example.org:5555")).scheme(new AsciiString("https")); + ChannelPromise writePromise = newPromise(); + ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise); + + assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writePromise.isSuccess()); + assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writeFuture.isSuccess()); + awaitRequests(); + verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5), + eq(http2Headers), eq(0), anyShort(), anyBoolean(), eq(0), eq(true)); + verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class), anyInt(), + any(ByteBuf.class), anyInt(), anyBoolean()); + } + + @Test + public void testAbsoluteFormRequestTargetHandledFromRequestTargetUri() throws Exception { + bootstrapEnv(2, 1, 0); + final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, + "http://foouser@www.example.org:5555/pub/WWW/TheProject.html"); + final HttpHeaders httpHeaders = request.headers(); + httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + final Http2Headers http2Headers = + new DefaultHttp2Headers().method(new AsciiString("GET")) + .path(new AsciiString("/pub/WWW/TheProject.html")) + .authority(new AsciiString("www.example.org:5555")).scheme(new AsciiString("http")); + ChannelPromise writePromise = newPromise(); + ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise); + + assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writePromise.isSuccess()); + assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writeFuture.isSuccess()); + awaitRequests(); + verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5), + eq(http2Headers), eq(0), anyShort(), anyBoolean(), eq(0), eq(true)); + verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class), anyInt(), + any(ByteBuf.class), anyInt(), anyBoolean()); + } + + @Test + public void testAuthorityFormRequestTargetHandled() throws Exception { + bootstrapEnv(2, 1, 0); + final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, CONNECT, "http://www.example.com:80"); + final HttpHeaders httpHeaders = request.headers(); + httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + final Http2Headers http2Headers = + new DefaultHttp2Headers().method(new AsciiString("CONNECT")).path(new AsciiString("/")) + .scheme(new AsciiString("http")).authority(new AsciiString("www.example.com:80")); + ChannelPromise writePromise = newPromise(); + ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise); + + assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writePromise.isSuccess()); + assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writeFuture.isSuccess()); + awaitRequests(); + verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5), + eq(http2Headers), eq(0), anyShort(), anyBoolean(), eq(0), eq(true)); + verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class), anyInt(), + any(ByteBuf.class), anyInt(), anyBoolean()); + } + + @Test + public void testAsterikFormRequestTargetHandled() throws Exception { + bootstrapEnv(2, 1, 0); + final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, OPTIONS, "*"); + final HttpHeaders httpHeaders = request.headers(); + httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + httpHeaders.set(HttpHeaderNames.HOST, "http://www.example.com:80"); + final Http2Headers http2Headers = + new DefaultHttp2Headers().method(new AsciiString("OPTIONS")).path(new AsciiString("*")) + .scheme(new AsciiString("http")).authority(new AsciiString("www.example.com:80")); + ChannelPromise writePromise = newPromise(); + ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise); + + assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writePromise.isSuccess()); + assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writeFuture.isSuccess()); + awaitRequests(); + verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5), + eq(http2Headers), eq(0), anyShort(), anyBoolean(), eq(0), eq(true)); + verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class), anyInt(), + any(ByteBuf.class), anyInt(), anyBoolean()); + } + + @Test + public void testHostIPv6FormRequestTargetHandled() throws Exception { + // Valid according to + // https://tools.ietf.org/html/rfc7230#section-2.7.1 -> https://tools.ietf.org/html/rfc3986#section-3.2.2 + bootstrapEnv(2, 1, 0); + final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/"); + final HttpHeaders httpHeaders = request.headers(); + httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + httpHeaders.set(HttpHeaderNames.HOST, "http://[::1]:80"); + final Http2Headers http2Headers = + new DefaultHttp2Headers().method(new AsciiString("GET")).path(new AsciiString("/")) + .scheme(new AsciiString("http")).authority(new AsciiString("[::1]:80")); + ChannelPromise writePromise = newPromise(); + ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise); + + assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writePromise.isSuccess()); + assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writeFuture.isSuccess()); + awaitRequests(); + verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5), + eq(http2Headers), eq(0), anyShort(), anyBoolean(), eq(0), eq(true)); + verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class), anyInt(), + any(ByteBuf.class), anyInt(), anyBoolean()); + } + + @Test + public void testHostNoSchemeFormRequestTargetHandled() throws Exception { + bootstrapEnv(2, 1, 0); + final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/"); + final HttpHeaders httpHeaders = request.headers(); + httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + // This is an "irregular" host in that the scheme is "localhost" + httpHeaders.set(HttpHeaderNames.HOST, "localhost:80"); + final Http2Headers http2Headers = + new DefaultHttp2Headers().method(new AsciiString("GET")).path(new AsciiString("/")) + .scheme(new AsciiString("localhost")); + ChannelPromise writePromise = newPromise(); + ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise); + + assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writePromise.isSuccess()); + assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writeFuture.isSuccess()); + awaitRequests(); + verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5), + eq(http2Headers), eq(0), anyShort(), anyBoolean(), eq(0), eq(true)); + verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class), anyInt(), + any(ByteBuf.class), anyInt(), anyBoolean()); + } + + @Test + public void testBadHostIPv4FormRequestTargetHandled() throws Exception { + // Invalid according to + // https://tools.ietf.org/html/rfc7230#section-2.7.1 -> https://tools.ietf.org/html/rfc3986#section-3 + bootstrapEnv(2, 1, 0); + final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/"); + final HttpHeaders httpHeaders = request.headers(); + httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + httpHeaders.set(HttpHeaderNames.HOST, "1.2.3.4:80"); + ChannelPromise writePromise = newPromise(); + ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise); + + assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writePromise.isDone()); + assertFalse(writePromise.isSuccess()); + assertTrue(writeFuture.isDone()); + assertFalse(writeFuture.isSuccess()); + } + + @Test + public void testBadHostIPv6FormRequestTargetHandled() throws Exception { + // Invalid according to + // https://tools.ietf.org/html/rfc7230#section-2.7.1 -> https://tools.ietf.org/html/rfc3986#section-3 + bootstrapEnv(2, 1, 0); + final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/"); + final HttpHeaders httpHeaders = request.headers(); + httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + httpHeaders.set(HttpHeaderNames.HOST, "[::1]:80"); + ChannelPromise writePromise = newPromise(); + ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise); + + assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS)); + assertTrue(writePromise.isDone()); + assertFalse(writePromise.isSuccess()); + assertTrue(writeFuture.isDone()); + assertFalse(writeFuture.isSuccess()); + } + @Test public void testRequestWithBody() throws Exception { final String text = "foooooogoooo";