diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java index f262b114f0..557341c189 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java @@ -535,8 +535,8 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder connectionSpecificHeaders = Collections.unmodifiableList( + Arrays.asList(CONNECTION, TRANSFER_ENCODING, KEEP_ALIVE, UPGRADE)); + + private Http2HeadersValidator() { + } + + /** + * Validates connection-specific headers according to + * RFC7540, section-8.1.2.2 + */ + static void validateConnectionSpecificHeaders(Http2Headers headers, int streamId) throws Http2Exception { + for (int i = 0; i < connectionSpecificHeaders.size(); i++) { + final AsciiString header = connectionSpecificHeaders.get(i); + if (headers.contains(header)) { + throw streamError(streamId, PROTOCOL_ERROR, + "Connection-specific headers like [%s] must not be used with HTTP/2.", header); + } + } + + final CharSequence teHeader = headers.get(TE); + if (teHeader != null && !AsciiString.contentEqualsIgnoreCase(teHeader, TRAILERS)) { + throw streamError(streamId, PROTOCOL_ERROR, + "TE header must not contain any value other than \"%s\"", TRAILERS); + } + } + + /** + * Validates response pseudo-header fields + */ + static void validateResponsePseudoHeaders(Http2Headers headers, int streamId) throws Http2Exception { + for (Entry entry : headers) { + final CharSequence key = entry.getKey(); + if (!hasPseudoHeaderFormat(key)) { + // We know that pseudo header appears first so we can stop + // looking once we get to the first non pseudo headers. + break; + } + + final PseudoHeaderName pseudoHeader = PseudoHeaderName.getPseudoHeader(key); + if (pseudoHeader.isRequestOnly()) { + throw streamError(streamId, PROTOCOL_ERROR, + "Request pseudo-header [%s] is not allowed in a response.", key); + } + } + } + + /** + * Validates request pseudo-header fields according to + * RFC7540, section-8.1.2.3 + */ + static void validateRequestPseudoHeaders(Http2Headers headers, int streamId) throws Http2Exception { + final CharSequence method = headers.get(METHOD.value()); + if (method == null) { + throw streamError(streamId, PROTOCOL_ERROR, + "Mandatory header [:method] is missing."); + } + + if (HttpMethod.CONNECT.asciiName().contentEqualsIgnoreCase(method)) { + if (headers.contains(SCHEME.value())) { + throw streamError(streamId, PROTOCOL_ERROR, + "Header [:scheme] must be omitted when using CONNECT method."); + } + + if (headers.contains(PATH.value())) { + throw streamError(streamId, PROTOCOL_ERROR, + "Header [:path] must be omitted when using CONNECT method."); + } + + if (headers.getAll(METHOD.value()).size() > 1) { + throw streamError(streamId, PROTOCOL_ERROR, + "Header [:method] should have a unique value."); + } + } else { + final CharSequence path = headers.get(PATH.value()); + if (path != null && path.length() == 0) { + throw streamError(streamId, PROTOCOL_ERROR, "[:path] header cannot be empty."); + } + + int methodHeadersCount = 0; + int pathHeadersCount = 0; + int schemeHeadersCount = 0; + for (Entry entry : headers) { + final CharSequence key = entry.getKey(); + if (!hasPseudoHeaderFormat(key)) { + // We know that pseudo header appears first so we can stop + // looking once we get to the first non pseudo headers. + break; + } + + final PseudoHeaderName pseudoHeader = PseudoHeaderName.getPseudoHeader(key); + if (METHOD.value().contentEquals(key)) { + methodHeadersCount++; + } else if (PATH.value().contentEquals(key)) { + pathHeadersCount++; + } else if (SCHEME.value().contentEquals(key)) { + schemeHeadersCount++; + } else if (!pseudoHeader.isRequestOnly()) { + throw streamError(streamId, PROTOCOL_ERROR, + "Response pseudo-header [%s] is not allowed in a request.", key); + } + } + + validatePseudoHeaderCount(streamId, methodHeadersCount, METHOD); + validatePseudoHeaderCount(streamId, pathHeadersCount, PATH); + validatePseudoHeaderCount(streamId, schemeHeadersCount, SCHEME); + } + } + + private static void validatePseudoHeaderCount(int streamId, int valueCount, PseudoHeaderName headerName) + throws Http2Exception { + if (valueCount == 0) { + throw streamError(streamId, PROTOCOL_ERROR, + "Mandatory header [%s] is missing.", headerName.value()); + } else if (valueCount > 1) { + throw streamError(streamId, PROTOCOL_ERROR, + "Header [%s] should have a unique value.", headerName.value()); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java index 8ffbd28533..3aadce28ce 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java @@ -391,9 +391,14 @@ public final class HttpConversionUtil { if (in instanceof HttpRequest) { HttpRequest request = (HttpRequest) in; URI requestTargetUri = URI.create(request.uri()); - out.path(toHttp2Path(requestTargetUri)); out.method(request.method().asciiName()); - setHttp2Scheme(inHeaders, requestTargetUri, out); + + // According to the spec https://tools.ietf.org/html/rfc7540#section-8.3 scheme and path + // should be omitted for CONNECT method + if (request.method() != HttpMethod.CONNECT) { + setHttp2Scheme(inHeaders, requestTargetUri, out); + out.path(toHttp2Path(requestTargetUri)); + } if (!isOriginForm(requestTargetUri) && !isAsteriskForm(requestTargetUri)) { // Attempt to take from HOST header before taking from the request-line diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java index 1d815a330c..f3b5edffb9 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java @@ -70,6 +70,7 @@ public class DataCompressionHttp2Test { private static final AsciiString GET = new AsciiString("GET"); private static final AsciiString POST = new AsciiString("POST"); private static final AsciiString PATH = new AsciiString("/some/path"); + private static final AsciiString SCHEME = new AsciiString("http"); @Mock private Http2FrameListener serverListener; @@ -144,7 +145,7 @@ public class DataCompressionHttp2Test { @Test public void justHeadersNoData() throws Exception { bootstrapEnv(0); - final Http2Headers headers = new DefaultHttp2Headers().method(GET).path(PATH) + final Http2Headers headers = new DefaultHttp2Headers().method(GET).path(PATH).scheme(SCHEME) .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP); runInChannel(clientChannel, new Http2Runnable() { @@ -165,7 +166,7 @@ public class DataCompressionHttp2Test { final ByteBuf data = Unpooled.copiedBuffer(text.getBytes()); bootstrapEnv(data.readableBytes()); try { - final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH) + final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH).scheme(SCHEME) .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP); runInChannel(clientChannel, new Http2Runnable() { @@ -189,7 +190,7 @@ public class DataCompressionHttp2Test { final ByteBuf data = Unpooled.copiedBuffer(text.getBytes()); bootstrapEnv(data.readableBytes()); try { - final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH) + final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH).scheme(SCHEME) .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP); runInChannel(clientChannel, new Http2Runnable() { @@ -215,7 +216,7 @@ public class DataCompressionHttp2Test { final ByteBuf data2 = Unpooled.copiedBuffer(text2.getBytes()); bootstrapEnv(data1.readableBytes() + data2.readableBytes()); try { - final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH) + final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH).scheme(SCHEME) .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP); runInChannel(clientChannel, new Http2Runnable() { @@ -243,7 +244,7 @@ public class DataCompressionHttp2Test { bootstrapEnv(BUFFER_SIZE); final ByteBuf data = Unpooled.wrappedBuffer(bytes); try { - final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH) + final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH).scheme(SCHEME) .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.DEFLATE); runInChannel(clientChannel, new Http2Runnable() { diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java index 7e87d52893..75cfbf43ea 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java @@ -22,6 +22,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.DefaultChannelPromise; import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName; import junit.framework.AssertionFailedError; import org.junit.Before; import org.junit.Test; @@ -38,9 +39,11 @@ import static io.netty.buffer.Unpooled.EMPTY_BUFFER; import static io.netty.buffer.Unpooled.wrappedBuffer; import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT; import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; +import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_REMOTE; import static io.netty.handler.codec.http2.Http2Stream.State.IDLE; import static io.netty.handler.codec.http2.Http2Stream.State.OPEN; import static io.netty.handler.codec.http2.Http2Stream.State.RESERVED_REMOTE; +import static io.netty.handler.codec.http2.Http2TestUtil.newHttp2HeadersWithRequestPseudoHeaders; import static io.netty.util.CharsetUtil.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -192,7 +195,8 @@ public class DefaultHttp2ConnectionDecoderTest { when(ctx.newPromise()).thenReturn(promise); when(ctx.write(any())).thenReturn(future); - decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader); + decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader, + Http2PromisedRequestVerifier.ALWAYS_VERIFY, true, true, true); decoder.lifecycleManager(lifecycleManager); decoder.frameListener(listener); @@ -492,6 +496,64 @@ public class DefaultHttp2ConnectionDecoderTest { eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false)); } + @Test(expected = Http2Exception.class) + public void requestPseudoHeadersInResponseThrows() throws Exception { + when(connection.isServer()).thenReturn(false); + when(connection.stream(STREAM_ID)).thenReturn(null); + when(connection.streamMayHaveExisted(STREAM_ID)).thenReturn(false); + when(remote.createStream(eq(STREAM_ID), anyBoolean())).thenReturn(stream); + when(stream.state()).thenReturn(HALF_CLOSED_REMOTE); + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + decode().onHeadersRead(ctx, STREAM_ID, headers, 0, false); + } + + @Test(expected = Http2Exception.class) + public void missingPseudoHeadersInLeadingHeaderThrows() throws Exception { + when(connection.isServer()).thenReturn(true); + when(connection.stream(STREAM_ID)).thenReturn(null); + when(connection.streamMayHaveExisted(STREAM_ID)).thenReturn(false); + when(remote.createStream(eq(STREAM_ID), anyBoolean())).thenReturn(stream); + when(stream.state()).thenReturn(HALF_CLOSED_REMOTE); + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.remove(PseudoHeaderName.METHOD.value()); + decode().onHeadersRead(ctx, STREAM_ID, headers, 0, false); + } + + @Test + public void missingPseudoHeadersInLeadingHeaderShouldNotThrowsIfValidationDisabled() throws Exception { + when(connection.isServer()).thenReturn(true); + when(connection.stream(STREAM_ID)).thenReturn(null); + when(connection.streamMayHaveExisted(STREAM_ID)).thenReturn(false); + when(remote.createStream(eq(STREAM_ID), anyBoolean())).thenReturn(stream); + when(stream.state()).thenReturn(HALF_CLOSED_REMOTE); + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.remove(PseudoHeaderName.METHOD.value()); + + decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader, + Http2PromisedRequestVerifier.ALWAYS_VERIFY, true, true, false); + decoder.lifecycleManager(lifecycleManager); + decoder.frameListener(listener); + + // Simulate receiving the initial settings from the remote endpoint. + decode().onSettingsRead(ctx, new Http2Settings()); + // Simulate receiving the SETTINGS ACK for the initial settings. + decode().onSettingsAckRead(ctx); + + decode().onHeadersRead(ctx, STREAM_ID, headers, 0, false); + } + + @Test + public void missingPseudoHeadersInTrailerHeaderDoesNotThrow() throws Exception { + when(connection.isServer()).thenReturn(true); + when(connection.stream(STREAM_ID)).thenReturn(stream); + + decode().onHeadersRead(ctx, STREAM_ID, newHttp2HeadersWithRequestPseudoHeaders(), 0, false); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.remove(PseudoHeaderName.METHOD.value()); + decode().onHeadersRead(ctx, STREAM_ID, headers, 0, true); + } + @Test(expected = Http2Exception.class) public void trailersDoNotEndStreamThrows() throws Exception { decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false); diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackDecoderTest.java index 7b26d90d17..7abbbe4598 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackDecoderTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackDecoderTest.java @@ -575,46 +575,6 @@ public class HpackDecoderTest { } } - @Test - public void requestPseudoHeaderInResponse() throws Exception { - ByteBuf in = Unpooled.buffer(200); - try { - HpackEncoder hpackEncoder = new HpackEncoder(true); - - Http2Headers toEncode = new DefaultHttp2Headers(); - toEncode.add(":status", "200"); - toEncode.add(":method", "GET"); - hpackEncoder.encodeHeaders(1, in, toEncode, NEVER_SENSITIVE); - - Http2Headers decoded = new DefaultHttp2Headers(); - - expectedException.expect(Http2Exception.StreamException.class); - hpackDecoder.decode(1, in, decoded, true); - } finally { - in.release(); - } - } - - @Test - public void responsePseudoHeaderInRequest() throws Exception { - ByteBuf in = Unpooled.buffer(200); - try { - HpackEncoder hpackEncoder = new HpackEncoder(true); - - Http2Headers toEncode = new DefaultHttp2Headers(); - toEncode.add(":method", "GET"); - toEncode.add(":status", "200"); - hpackEncoder.encodeHeaders(1, in, toEncode, NEVER_SENSITIVE); - - Http2Headers decoded = new DefaultHttp2Headers(); - - expectedException.expect(Http2Exception.StreamException.class); - hpackDecoder.decode(1, in, decoded, true); - } finally { - in.release(); - } - } - @Test public void pseudoHeaderAfterRegularHeader() throws Exception { ByteBuf in = Unpooled.buffer(200); @@ -644,7 +604,7 @@ public class HpackDecoderTest { Http2Headers toEncode = new DefaultHttp2Headers(); toEncode.add(":method", "GET"); - toEncode.add(":status", "200"); + toEncode.add(":unknownpseudoheader", "200"); toEncode.add("foo", "bar"); hpackEncoder.encodeHeaders(1, in1, toEncode, NEVER_SENSITIVE); @@ -664,7 +624,7 @@ public class HpackDecoderTest { assertEquals(3, decoded.size()); assertEquals("GET", decoded.method().toString()); - assertEquals("200", decoded.status().toString()); + assertEquals("200", decoded.get(":unknownpseudoheader").toString()); assertEquals("bar", decoded.get("foo").toString()); } finally { in1.release(); diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeadersValidatorTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeadersValidatorTest.java new file mode 100644 index 0000000000..3f323a6ef0 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeadersValidatorTest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2018 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.handler.codec.http2.Http2Exception.StreamException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderNames.TE; +import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.METHOD; +import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.PATH; +import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.SCHEME; +import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.STATUS; +import static io.netty.handler.codec.http2.Http2HeadersValidator.validateConnectionSpecificHeaders; +import static io.netty.handler.codec.http2.Http2HeadersValidator.validateRequestPseudoHeaders; +import static io.netty.handler.codec.http2.Http2TestUtil.newHttp2HeadersWithRequestPseudoHeaders; + +public class Http2HeadersValidatorTest { + + private static final int STREAM_ID = 3; + + @Rule + public final ExpectedException expectedException = ExpectedException.none(); + + @Test + public void validateConnectionSpecificHeadersShouldThrowIfConnectionHeaderPresent() throws Http2Exception { + expectedException.expect(StreamException.class); + expectedException.expectMessage("Connection-speficic headers like [connection] must not be used with HTTP"); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.add(CONNECTION, "keep-alive"); + validateConnectionSpecificHeaders(headers, STREAM_ID); + } + + @Test + public void validateConnectionSpecificHeadersShouldThrowIfTeHeaderValueIsNotTrailers() throws Http2Exception { + expectedException.expect(StreamException.class); + expectedException.expectMessage("TE header must not contain any value other than \"trailers\""); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.add(TE, "trailers, deflate"); + validateConnectionSpecificHeaders(headers, STREAM_ID); + } + + @Test + public void validatePseudoHeadersShouldThrowWhenMethodHeaderIsMissing() throws Http2Exception { + expectedException.expect(StreamException.class); + expectedException.expectMessage("Mandatory header [:method] is missing."); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.remove(METHOD.value()); + validateRequestPseudoHeaders(headers, STREAM_ID); + } + + @Test + public void validatePseudoHeadersShouldThrowWhenPathHeaderIsMissing() throws Http2Exception { + expectedException.expect(StreamException.class); + expectedException.expectMessage("Mandatory header [:path] is missing."); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.remove(PATH.value()); + validateRequestPseudoHeaders(headers, STREAM_ID); + } + + @Test + public void validatePseudoHeadersShouldThrowWhenPathHeaderIsEmpty() throws Http2Exception { + expectedException.expect(StreamException.class); + expectedException.expectMessage("[:path] header cannot be empty."); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.set(PATH.value(), ""); + validateRequestPseudoHeaders(headers, STREAM_ID); + } + + @Test + public void validatePseudoHeadersShouldThrowWhenSchemeHeaderIsMissing() throws Http2Exception { + expectedException.expect(StreamException.class); + expectedException.expectMessage("Mandatory header [:scheme] is missing."); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.remove(SCHEME.value()); + validateRequestPseudoHeaders(headers, STREAM_ID); + } + + @Test + public void validatePseudoHeadersShouldThrowIfMethodHeaderIsNotUnique() throws Http2Exception { + expectedException.expect(StreamException.class); + expectedException.expectMessage("Header [:method] should have a unique value."); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.add(METHOD.value(), "GET"); + validateRequestPseudoHeaders(headers, STREAM_ID); + } + + @Test + public void validatePseudoHeadersShouldThrowIfPathHeaderIsNotUnique() throws Http2Exception { + expectedException.expect(StreamException.class); + expectedException.expectMessage("Header [:path] should have a unique value."); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.add(PATH.value(), "/"); + validateRequestPseudoHeaders(headers, STREAM_ID); + } + + @Test + public void validatePseudoHeadersShouldThrowIfSchemeHeaderIsNotUnique() throws Http2Exception { + expectedException.expect(StreamException.class); + expectedException.expectMessage("Header [:scheme] should have a unique value."); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.add(SCHEME.value(), "/"); + validateRequestPseudoHeaders(headers, STREAM_ID); + } + + @Test + public void validatePseudoHeadersShouldThrowIfMethodHeaderIsNotUniqueWhenMethodIsConnect() throws Http2Exception { + expectedException.expect(StreamException.class); + expectedException.expectMessage("Header [:method] should have a unique value."); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.remove(SCHEME.value()); + headers.remove(PATH.value()); + headers.set(METHOD.value(), "CONNECT"); + headers.add(METHOD.value(), "CONNECT"); + validateRequestPseudoHeaders(headers, STREAM_ID); + } + + @Test + public void validatePseudoHeadersShouldThrowIfPathHeaderIsPresentWhenMethodIsConnect() throws Http2Exception { + expectedException.expect(StreamException.class); + expectedException.expectMessage("Header [:path] must be omitted when using CONNECT method."); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.set(METHOD.value(), "CONNECT"); + headers.remove(SCHEME.value()); + validateRequestPseudoHeaders(headers, STREAM_ID); + } + + @Test + public void validatePseudoHeadersShouldThrowIfSchemeHeaderIsPresentWhenMethodIsConnect() throws Http2Exception { + expectedException.expect(StreamException.class); + expectedException.expectMessage("Header [:scheme] must be omitted when using CONNECT method."); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.set(METHOD.value(), "CONNECT"); + headers.remove(PATH.value()); + validateRequestPseudoHeaders(headers, STREAM_ID); + } + + @Test + public void validatePseudoHeadersShouldThrowIfResponseHeaderInRequest() throws Http2Exception { + expectedException.expect(StreamException.class); + expectedException.expectMessage("Response pseudo-header [:status] is not allowed in a request."); + + final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); + headers.add(STATUS.value(), "200"); + validateRequestPseudoHeaders(headers, STREAM_ID); + } + +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilderTest.java index c12f5f8b7c..17a548f5c3 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilderTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilderTest.java @@ -42,6 +42,7 @@ import org.junit.Test; import java.util.concurrent.CountDownLatch; import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid; +import static io.netty.handler.codec.http2.Http2TestUtil.newHttp2HeadersWithRequestPseudoHeaders; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -157,8 +158,8 @@ public class Http2MultiplexCodecBuilderTest { assertTrue(childChannel2.isActive()); assertFalse(isStreamIdValid(childChannel2.stream().id())); - Http2Headers headers1 = new DefaultHttp2Headers(); - Http2Headers headers2 = new DefaultHttp2Headers(); + Http2Headers headers1 = newHttp2HeadersWithRequestPseudoHeaders(); + Http2Headers headers2 = newHttp2HeadersWithRequestPseudoHeaders(); // Test that streams can be made active (headers sent) in different order than the corresponding channels // have been created. childChannel2.writeAndFlush(new DefaultHttp2HeadersFrame(headers2)); @@ -187,7 +188,7 @@ public class Http2MultiplexCodecBuilderTest { assertTrue(childChannel.isRegistered()); assertTrue(childChannel.isActive()); - Http2Headers headers = new DefaultHttp2Headers(); + Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders(); childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers)); ByteBuf data = Unpooled.buffer(100).writeZero(100); childChannel.writeAndFlush(new DefaultHttp2DataFrame(data, true)); diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java index 6fa3449709..a8ba26ba7f 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java @@ -98,6 +98,13 @@ public final class Http2TestUtil { return data; } + public static Http2Headers newHttp2HeadersWithRequestPseudoHeaders() { + return new DefaultHttp2Headers(true) + .method("GET") + .path("/") + .scheme("https"); + } + /** * Returns an {@link AsciiString} that wraps a randomly-filled byte array. */ 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 0e4f3e9520..a66ed56f3a 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 @@ -242,8 +242,8 @@ public class HttpToHttp2ConnectionHandlerTest { final HttpHeaders httpHeaders = request.headers(); httpHeaders.setInt(HttpConversionUtil.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")); + new DefaultHttp2Headers().method(new AsciiString("CONNECT")) + .authority(new AsciiString("www.example.com:80")); ChannelPromise writePromise = newPromise(); verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise)); diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java index 8fb4ebfc67..6e53d0e489 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java @@ -43,6 +43,7 @@ import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http2.Http2TestUtil.Http2Runnable; +import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames; import io.netty.util.AsciiString; import io.netty.util.CharsetUtil; import io.netty.util.concurrent.Future; @@ -268,8 +269,11 @@ public class InboundHttp2ToHttpAdapterTest { httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); - final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path( - new AsciiString("/some/path/resource2")); + httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http"); + final Http2Headers http2Headers = new DefaultHttp2Headers() + .method(new AsciiString("GET")) + .scheme(new AsciiString("http")) + .path(new AsciiString("/some/path/resource2")); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() throws Http2Exception { @@ -301,8 +305,11 @@ public class InboundHttp2ToHttpAdapterTest { httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); - final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path( - new AsciiString("/some/path/resource2")); + httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http"); + final Http2Headers http2Headers = new DefaultHttp2Headers() + .method(new AsciiString("GET")) + .scheme(new AsciiString("http")) + .path(new AsciiString("/some/path/resource2")); final int midPoint = text.length() / 2; runInChannel(clientChannel, new Http2Runnable() { @Override @@ -338,8 +345,11 @@ public class InboundHttp2ToHttpAdapterTest { httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); - final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path( - new AsciiString("/some/path/resource2")); + httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http"); + final Http2Headers http2Headers = new DefaultHttp2Headers() + .method(new AsciiString("GET")) + .scheme("http") + .path(new AsciiString("/some/path/resource2")); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() throws Http2Exception { @@ -372,12 +382,15 @@ public class InboundHttp2ToHttpAdapterTest { httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); + httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http"); HttpHeaders trailingHeaders = request.trailingHeaders(); trailingHeaders.set(of("Foo"), of("goo")); trailingHeaders.set(of("fOo2"), of("goo2")); trailingHeaders.add(of("foO2"), of("goo3")); - final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path( - new AsciiString("/some/path/resource2")); + final Http2Headers http2Headers = new DefaultHttp2Headers() + .method(new AsciiString("GET")) + .scheme(new AsciiString("http")) + .path(new AsciiString("/some/path/resource2")); final Http2Headers http2Headers2 = new DefaultHttp2Headers() .set(new AsciiString("foo"), new AsciiString("goo")) .set(new AsciiString("foo2"), new AsciiString("goo2")) @@ -418,15 +431,21 @@ public class InboundHttp2ToHttpAdapterTest { httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); + httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http"); HttpHeaders httpHeaders2 = request2.headers(); httpHeaders2.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); httpHeaders2.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3); httpHeaders2.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 123); httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length()); - final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("PUT")).path( - new AsciiString("/some/path/resource")); - final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(new AsciiString("PUT")).path( - new AsciiString("/some/path/resource2")); + httpHeaders2.set(ExtensionHeaderNames.SCHEME.text(), "http"); + final Http2Headers http2Headers = new DefaultHttp2Headers() + .method(new AsciiString("PUT")) + .scheme(new AsciiString("http")) + .path(new AsciiString("/some/path/resource")); + final Http2Headers http2Headers2 = new DefaultHttp2Headers() + .method(new AsciiString("PUT")) + .scheme(new AsciiString("http")) + .path(new AsciiString("/some/path/resource2")); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() throws Http2Exception { @@ -482,7 +501,10 @@ public class InboundHttp2ToHttpAdapterTest { httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0); httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); - final Http2Headers http2Headers3 = new DefaultHttp2Headers().method(new AsciiString("GET")) + httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http"); + final Http2Headers http2Headers3 = new DefaultHttp2Headers() + .method(new AsciiString("GET")) + .scheme("http") .path(new AsciiString("/push/test")); runInChannel(clientChannel, new Http2Runnable() { @Override @@ -540,9 +562,11 @@ public class InboundHttp2ToHttpAdapterTest { httpHeaders.set(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0); httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); + httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http"); final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("PUT")) .path(new AsciiString("/info/test")) + .scheme(new AsciiString("http")) .set(new AsciiString(HttpHeaderNames.EXPECT.toString()), new AsciiString(HttpHeaderValues.CONTINUE.toString())); final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE); diff --git a/testsuite-http2/pom.xml b/testsuite-http2/pom.xml index 71c40af60d..1c405150d6 100644 --- a/testsuite-http2/pom.xml +++ b/testsuite-http2/pom.xml @@ -94,15 +94,6 @@ 5.1 - half closed (remote): Sends a HEADERS frame 5.1 - closed: Sends a HEADERS frame 5.1.1 - Sends stream identifier that is numerically smaller than previous - 8.1.2.2 - Sends a HEADERS frame that contains the connection-specific header field - 8.1.2.2 - Sends a HEADERS frame that contains the TE header field with any value other than "trailers" - 8.1.2.3 - Sends a HEADERS frame with empty ":path" pseudo-header field - 8.1.2.3 - Sends a HEADERS frame that omits ":method" pseudo-header field - 8.1.2.3 - Sends a HEADERS frame that omits ":scheme" pseudo-header field - 8.1.2.3 - Sends a HEADERS frame that omits ":path" pseudo-header field - 8.1.2.3 - Sends a HEADERS frame with duplicated ":method" pseudo-header field - 8.1.2.3 - Sends a HEADERS frame with duplicated ":method" pseudo-header field - 8.1.2.3 - Sends a HEADERS frame with duplicated ":scheme" pseudo-header field 8.1.2.6 - Sends a HEADERS frame with the "content-length" header field which does not equal the DATA frame payload length 8.1.2.6 - Sends a HEADERS frame with the "content-length" header field which does not equal the sum of the multiple DATA frames payload length