Validate pseudo and conditional HTTP/2 headers (#8619)

Motivation:

Netty HTTP/2 implementation is not 100% compliant to the spec. This
commit improves the compliance regarding headers validation,
in particular pseudo-headers and connection ones.

According to the spec:
   All HTTP/2 requests MUST include exactly one valid value for the
   ":method", ":scheme", and ":path" pseudo-header fields, unless it is
   a CONNECT request (Section 8.3).  An HTTP request that omits
   mandatory pseudo-header fields is malformed (Section 8.1.2.6).

Modifications:

- Introduce Http2HeadersValidator class capable of validating HTTP/2
headers
- Invoke validation from DefaultHttp2ConnectionDecoder#onHeadersRead
- Modify tests to use valid headers when required
- Modify HttpConversionUtil#toHttp2Headers to not add :scheme and
:path header on CONNECT method in order to conform to the spec

Result:

- Initial requests without :method, :path, :scheme will fail
- Initial requests with multiple values for :method, :path, :scheme
will fail
- Initial requests with an empty :path fail
- Requests with connection-specific header field will fail
- Requests with TE header different than "trailers" will fail
-
- Fixes 8.1.2.2 tests from h2spec #5761
- Fixes 8.1.2.3 tests from h2spec #5761
This commit is contained in:
Julien Hoarau 2019-10-27 08:13:01 -07:00 committed by Norman Maurer
parent 4be554a21f
commit dd5d4887ed
14 changed files with 500 additions and 90 deletions

View File

@ -535,8 +535,8 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
encoder = new StreamBufferingEncoder(encoder); encoder = new StreamBufferingEncoder(encoder);
} }
DefaultHttp2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader, Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader,
promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame()); promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame(), isValidateHeaders());
return buildFromCodec(decoder, encoder); return buildFromCodec(decoder, encoder);
} }

View File

@ -31,6 +31,9 @@ import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.STREAM_CLOSED; import static io.netty.handler.codec.http2.Http2Error.STREAM_CLOSED;
import static io.netty.handler.codec.http2.Http2Exception.connectionError; import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import static io.netty.handler.codec.http2.Http2Exception.streamError; import static io.netty.handler.codec.http2.Http2Exception.streamError;
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.Http2HeadersValidator.validateResponsePseudoHeaders;
import static io.netty.handler.codec.http2.Http2PromisedRequestVerifier.ALWAYS_VERIFY; import static io.netty.handler.codec.http2.Http2PromisedRequestVerifier.ALWAYS_VERIFY;
import static io.netty.handler.codec.http2.Http2Stream.State.CLOSED; import static io.netty.handler.codec.http2.Http2Stream.State.CLOSED;
import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_REMOTE; import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_REMOTE;
@ -59,6 +62,7 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
private final Http2PromisedRequestVerifier requestVerifier; private final Http2PromisedRequestVerifier requestVerifier;
private final Http2SettingsReceivedConsumer settingsReceivedConsumer; private final Http2SettingsReceivedConsumer settingsReceivedConsumer;
private final boolean autoAckPing; private final boolean autoAckPing;
private final boolean validateHeaders;
public DefaultHttp2ConnectionDecoder(Http2Connection connection, public DefaultHttp2ConnectionDecoder(Http2Connection connection,
Http2ConnectionEncoder encoder, Http2ConnectionEncoder encoder,
@ -93,6 +97,15 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
this(connection, encoder, frameReader, requestVerifier, autoAckSettings, true); this(connection, encoder, frameReader, requestVerifier, autoAckSettings, true);
} }
public DefaultHttp2ConnectionDecoder(Http2Connection connection,
Http2ConnectionEncoder encoder,
Http2FrameReader frameReader,
Http2PromisedRequestVerifier requestVerifier,
boolean autoAckSettings,
boolean autoAckPing) {
this(connection, encoder, frameReader, requestVerifier, autoAckSettings, autoAckPing, false);
}
/** /**
* Create a new instance. * Create a new instance.
* @param connection The {@link Http2Connection} associated with this decoder. * @param connection The {@link Http2Connection} associated with this decoder.
@ -113,7 +126,8 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
Http2FrameReader frameReader, Http2FrameReader frameReader,
Http2PromisedRequestVerifier requestVerifier, Http2PromisedRequestVerifier requestVerifier,
boolean autoAckSettings, boolean autoAckSettings,
boolean autoAckPing) { boolean autoAckPing,
boolean validateHeaders) {
this.autoAckPing = autoAckPing; this.autoAckPing = autoAckPing;
if (autoAckSettings) { if (autoAckSettings) {
settingsReceivedConsumer = null; settingsReceivedConsumer = null;
@ -128,6 +142,7 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
this.frameReader = requireNonNull(frameReader, "frameReader"); this.frameReader = requireNonNull(frameReader, "frameReader");
this.encoder = requireNonNull(encoder, "encoder"); this.encoder = requireNonNull(encoder, "encoder");
this.requestVerifier = requireNonNull(requestVerifier, "requestVerifier"); this.requestVerifier = requireNonNull(requestVerifier, "requestVerifier");
this.validateHeaders = validateHeaders;
if (connection.local().flowController() == null) { if (connection.local().flowController() == null) {
connection.local().flowController(new DefaultHttp2LocalFlowController(connection)); connection.local().flowController(new DefaultHttp2LocalFlowController(connection));
} }
@ -337,6 +352,10 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
streamId, endOfStream, stream.state()); streamId, endOfStream, stream.state());
} }
if (validateHeaders) {
validateHeaders(streamId, headers, stream);
}
switch (stream.state()) { switch (stream.state()) {
case RESERVED_REMOTE: case RESERVED_REMOTE:
stream.open(endOfStream); stream.open(endOfStream);
@ -624,6 +643,18 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
throw connectionError(PROTOCOL_ERROR, "Stream %d does not exist", streamId); throw connectionError(PROTOCOL_ERROR, "Stream %d does not exist", streamId);
} }
} }
private void validateHeaders(int streamId, Http2Headers headers, Http2Stream stream) throws Http2Exception {
if (connection.isServer()) {
if (!stream.isHeadersReceived() || stream.state() == HALF_CLOSED_REMOTE) {
validateRequestPseudoHeaders(headers, streamId);
}
} else {
validateResponsePseudoHeaders(headers, streamId);
}
validateConnectionSpecificHeaders(headers, streamId);
}
} }
private final class PrefaceFrameListener implements Http2FrameListener { private final class PrefaceFrameListener implements Http2FrameListener {

View File

@ -393,13 +393,8 @@ final class HpackDecoder {
throw streamError(streamId, PROTOCOL_ERROR, "Invalid HTTP/2 pseudo-header '%s' encountered.", name); throw streamError(streamId, PROTOCOL_ERROR, "Invalid HTTP/2 pseudo-header '%s' encountered.", name);
} }
final HeaderType currentHeaderType = pseudoHeader.isRequestOnly() ? return pseudoHeader.isRequestOnly() ?
HeaderType.REQUEST_PSEUDO_HEADER : HeaderType.RESPONSE_PSEUDO_HEADER; HeaderType.REQUEST_PSEUDO_HEADER : HeaderType.RESPONSE_PSEUDO_HEADER;
if (previousHeaderType != null && currentHeaderType != previousHeaderType) {
throw streamError(streamId, PROTOCOL_ERROR, "Mix of request and response pseudo-headers.");
}
return currentHeaderType;
} }
return HeaderType.REGULAR_HEADER; return HeaderType.REGULAR_HEADER;

View File

@ -0,0 +1,159 @@
/*
* 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.http.HttpMethod;
import io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName;
import io.netty.util.AsciiString;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderNames.KEEP_ALIVE;
import static io.netty.handler.codec.http.HttpHeaderNames.TE;
import static io.netty.handler.codec.http.HttpHeaderNames.TRANSFER_ENCODING;
import static io.netty.handler.codec.http.HttpHeaderNames.UPGRADE;
import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.streamError;
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.hasPseudoHeaderFormat;
final class Http2HeadersValidator {
private static final List<AsciiString> connectionSpecificHeaders = Collections.unmodifiableList(
Arrays.asList(CONNECTION, TRANSFER_ENCODING, KEEP_ALIVE, UPGRADE));
private Http2HeadersValidator() {
}
/**
* Validates connection-specific headers according to
* <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.2">RFC7540, section-8.1.2.2</a>
*/
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<CharSequence, CharSequence> 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
* <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.3">RFC7540, section-8.1.2.3</a>
*/
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<CharSequence, CharSequence> 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());
}
}
}

View File

@ -391,9 +391,14 @@ public final class HttpConversionUtil {
if (in instanceof HttpRequest) { if (in instanceof HttpRequest) {
HttpRequest request = (HttpRequest) in; HttpRequest request = (HttpRequest) in;
URI requestTargetUri = URI.create(request.uri()); URI requestTargetUri = URI.create(request.uri());
out.path(toHttp2Path(requestTargetUri));
out.method(request.method().asciiName()); 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)) { if (!isOriginForm(requestTargetUri) && !isAsteriskForm(requestTargetUri)) {
// Attempt to take from HOST header before taking from the request-line // Attempt to take from HOST header before taking from the request-line

View File

@ -68,6 +68,7 @@ public class DataCompressionHttp2Test {
private static final AsciiString GET = new AsciiString("GET"); private static final AsciiString GET = new AsciiString("GET");
private static final AsciiString POST = new AsciiString("POST"); private static final AsciiString POST = new AsciiString("POST");
private static final AsciiString PATH = new AsciiString("/some/path"); private static final AsciiString PATH = new AsciiString("/some/path");
private static final AsciiString SCHEME = new AsciiString("http");
@Mock @Mock
private Http2FrameListener serverListener; private Http2FrameListener serverListener;
@ -136,7 +137,7 @@ public class DataCompressionHttp2Test {
@Test @Test
public void justHeadersNoData() throws Exception { public void justHeadersNoData() throws Exception {
bootstrapEnv(0); 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); .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
runInChannel(clientChannel, () -> { runInChannel(clientChannel, () -> {
@ -154,7 +155,7 @@ public class DataCompressionHttp2Test {
final ByteBuf data = Unpooled.copiedBuffer(text.getBytes()); final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
bootstrapEnv(data.readableBytes()); bootstrapEnv(data.readableBytes());
try { 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); .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
runInChannel(clientChannel, () -> { runInChannel(clientChannel, () -> {
@ -175,7 +176,7 @@ public class DataCompressionHttp2Test {
final ByteBuf data = Unpooled.copiedBuffer(text.getBytes()); final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
bootstrapEnv(data.readableBytes()); bootstrapEnv(data.readableBytes());
try { 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); .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
runInChannel(clientChannel, () -> { runInChannel(clientChannel, () -> {
@ -198,7 +199,7 @@ public class DataCompressionHttp2Test {
final ByteBuf data2 = Unpooled.copiedBuffer(text2.getBytes()); final ByteBuf data2 = Unpooled.copiedBuffer(text2.getBytes());
bootstrapEnv(data1.readableBytes() + data2.readableBytes()); bootstrapEnv(data1.readableBytes() + data2.readableBytes());
try { 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); .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
runInChannel(clientChannel, () -> { runInChannel(clientChannel, () -> {
@ -223,7 +224,7 @@ public class DataCompressionHttp2Test {
bootstrapEnv(BUFFER_SIZE); bootstrapEnv(BUFFER_SIZE);
final ByteBuf data = Unpooled.wrappedBuffer(bytes); final ByteBuf data = Unpooled.wrappedBuffer(bytes);
try { 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); .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.DEFLATE);
runInChannel(clientChannel, () -> { runInChannel(clientChannel, () -> {

View File

@ -23,13 +23,13 @@ import io.netty.channel.ChannelPromise;
import io.netty.channel.DefaultChannelPromise; import io.netty.channel.DefaultChannelPromise;
import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.util.concurrent.ImmediateEventExecutor; import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName;
import junit.framework.AssertionFailedError; import junit.framework.AssertionFailedError;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import java.util.Collections; import java.util.Collections;
@ -39,9 +39,11 @@ import static io.netty.buffer.Unpooled.EMPTY_BUFFER;
import static io.netty.buffer.Unpooled.wrappedBuffer; 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.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; 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.IDLE;
import static io.netty.handler.codec.http2.Http2Stream.State.OPEN; 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.Http2Stream.State.RESERVED_REMOTE;
import static io.netty.handler.codec.http2.Http2TestUtil.newHttp2HeadersWithRequestPseudoHeaders;
import static io.netty.util.CharsetUtil.UTF_8; import static io.netty.util.CharsetUtil.UTF_8;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -179,7 +181,8 @@ public class DefaultHttp2ConnectionDecoderTest {
when(ctx.newPromise()).thenReturn(promise); when(ctx.newPromise()).thenReturn(promise);
when(ctx.write(any())).thenReturn(future); 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.lifecycleManager(lifecycleManager);
decoder.frameListener(listener); decoder.frameListener(listener);
@ -468,6 +471,64 @@ public class DefaultHttp2ConnectionDecoderTest {
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false)); 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) @Test(expected = Http2Exception.class)
public void trailersDoNotEndStreamThrows() throws Exception { public void trailersDoNotEndStreamThrows() throws Exception {
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false); decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false);

View File

@ -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 @Test
public void pseudoHeaderAfterRegularHeader() throws Exception { public void pseudoHeaderAfterRegularHeader() throws Exception {
ByteBuf in = Unpooled.buffer(200); ByteBuf in = Unpooled.buffer(200);
@ -644,7 +604,7 @@ public class HpackDecoderTest {
Http2Headers toEncode = new DefaultHttp2Headers(); Http2Headers toEncode = new DefaultHttp2Headers();
toEncode.add(":method", "GET"); toEncode.add(":method", "GET");
toEncode.add(":status", "200"); toEncode.add(":unknownpseudoheader", "200");
toEncode.add("foo", "bar"); toEncode.add("foo", "bar");
hpackEncoder.encodeHeaders(1, in1, toEncode, NEVER_SENSITIVE); hpackEncoder.encodeHeaders(1, in1, toEncode, NEVER_SENSITIVE);
@ -664,7 +624,7 @@ public class HpackDecoderTest {
assertEquals(3, decoded.size()); assertEquals(3, decoded.size());
assertEquals("GET", decoded.method().toString()); assertEquals("GET", decoded.method().toString());
assertEquals("200", decoded.status().toString()); assertEquals("200", decoded.get(":unknownpseudoheader").toString());
assertEquals("bar", decoded.get("foo").toString()); assertEquals("bar", decoded.get("foo").toString());
} finally { } finally {
in1.release(); in1.release();

View File

@ -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);
}
}

View File

@ -42,6 +42,7 @@ import org.junit.Test;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid; 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 java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@ -157,8 +158,8 @@ public class Http2MultiplexCodecBuilderTest {
assertTrue(childChannel2.isActive()); assertTrue(childChannel2.isActive());
assertFalse(isStreamIdValid(childChannel2.stream().id())); assertFalse(isStreamIdValid(childChannel2.stream().id()));
Http2Headers headers1 = new DefaultHttp2Headers(); Http2Headers headers1 = newHttp2HeadersWithRequestPseudoHeaders();
Http2Headers headers2 = new DefaultHttp2Headers(); Http2Headers headers2 = newHttp2HeadersWithRequestPseudoHeaders();
// Test that streams can be made active (headers sent) in different order than the corresponding channels // Test that streams can be made active (headers sent) in different order than the corresponding channels
// have been created. // have been created.
childChannel2.writeAndFlush(new DefaultHttp2HeadersFrame(headers2)); childChannel2.writeAndFlush(new DefaultHttp2HeadersFrame(headers2));
@ -187,7 +188,7 @@ public class Http2MultiplexCodecBuilderTest {
assertTrue(childChannel.isRegistered()); assertTrue(childChannel.isRegistered());
assertTrue(childChannel.isActive()); assertTrue(childChannel.isActive());
Http2Headers headers = new DefaultHttp2Headers(); Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers)); childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
ByteBuf data = Unpooled.buffer(100).writeZero(100); ByteBuf data = Unpooled.buffer(100).writeZero(100);
childChannel.writeAndFlush(new DefaultHttp2DataFrame(data, true)); childChannel.writeAndFlush(new DefaultHttp2DataFrame(data, true));

View File

@ -95,6 +95,13 @@ public final class Http2TestUtil {
return data; 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. * Returns an {@link AsciiString} that wraps a randomly-filled byte array.
*/ */

View File

@ -244,8 +244,8 @@ public class HttpToHttp2ConnectionHandlerTest {
final HttpHeaders httpHeaders = request.headers(); final HttpHeaders httpHeaders = request.headers();
httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
final Http2Headers http2Headers = final Http2Headers http2Headers =
new DefaultHttp2Headers().method(new AsciiString("CONNECT")).path(new AsciiString("/")) new DefaultHttp2Headers().method(new AsciiString("CONNECT"))
.scheme(new AsciiString("http")).authority(new AsciiString("www.example.com:80")); .authority(new AsciiString("www.example.com:80"));
ChannelPromise writePromise = newPromise(); ChannelPromise writePromise = newPromise();
verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise)); verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));

View File

@ -42,6 +42,7 @@ import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames;
import io.netty.util.AsciiString; import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil; import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Future;
@ -255,12 +256,15 @@ public class InboundHttp2ToHttpAdapterTest {
httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path( httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
new AsciiString("/some/path/resource2")); final Http2Headers http2Headers = new DefaultHttp2Headers()
.method(new AsciiString("GET"))
.scheme(new AsciiString("http"))
.path(new AsciiString("/some/path/resource2"));
runInChannel(clientChannel, () -> { runInChannel(clientChannel, () -> {
clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient()); clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
clientHandler.encoder().writeData(ctxClient(), 3, content.retainedDuplicate(), 0, true, clientHandler.encoder().writeData(ctxClient(), 3, content.retainedDuplicate(), 0, true,
newPromiseClient()); newPromiseClient());
clientChannel.flush(); clientChannel.flush();
}); });
awaitRequests(); awaitRequests();
@ -285,8 +289,11 @@ public class InboundHttp2ToHttpAdapterTest {
httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path( httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
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 int midPoint = text.length() / 2; final int midPoint = text.length() / 2;
runInChannel(clientChannel, () -> { runInChannel(clientChannel, () -> {
clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient()); clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
@ -319,8 +326,11 @@ public class InboundHttp2ToHttpAdapterTest {
httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path( httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
new AsciiString("/some/path/resource2")); final Http2Headers http2Headers = new DefaultHttp2Headers()
.method(new AsciiString("GET"))
.scheme("http")
.path(new AsciiString("/some/path/resource2"));
runInChannel(clientChannel, () -> { runInChannel(clientChannel, () -> {
clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient()); clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
clientHandler.encoder().writeData(ctxClient(), 3, content.retain(), 0, false, newPromiseClient()); clientHandler.encoder().writeData(ctxClient(), 3, content.retain(), 0, false, newPromiseClient());
@ -350,12 +360,15 @@ public class InboundHttp2ToHttpAdapterTest {
httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
HttpHeaders trailingHeaders = request.trailingHeaders(); HttpHeaders trailingHeaders = request.trailingHeaders();
trailingHeaders.set(of("Foo"), of("goo")); trailingHeaders.set(of("Foo"), of("goo"));
trailingHeaders.set(of("fOo2"), of("goo2")); trailingHeaders.set(of("fOo2"), of("goo2"));
trailingHeaders.add(of("foO2"), of("goo3")); trailingHeaders.add(of("foO2"), of("goo3"));
final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path( final Http2Headers http2Headers = new DefaultHttp2Headers()
new AsciiString("/some/path/resource2")); .method(new AsciiString("GET"))
.scheme(new AsciiString("http"))
.path(new AsciiString("/some/path/resource2"));
final Http2Headers http2Headers2 = new DefaultHttp2Headers() final Http2Headers http2Headers2 = new DefaultHttp2Headers()
.set(new AsciiString("foo"), new AsciiString("goo")) .set(new AsciiString("foo"), new AsciiString("goo"))
.set(new AsciiString("foo2"), new AsciiString("goo2")) .set(new AsciiString("foo2"), new AsciiString("goo2"))
@ -393,24 +406,31 @@ public class InboundHttp2ToHttpAdapterTest {
httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length()); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
HttpHeaders httpHeaders2 = request2.headers(); HttpHeaders httpHeaders2 = request2.headers();
httpHeaders2.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); httpHeaders2.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
httpHeaders2.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3); httpHeaders2.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3);
httpHeaders2.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 123); httpHeaders2.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 123);
httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length()); httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length());
final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("PUT")).path(
new AsciiString("/some/path/resource")); httpHeaders2.set(ExtensionHeaderNames.SCHEME.text(), "http");
final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(new AsciiString("PUT")).path( final Http2Headers http2Headers = new DefaultHttp2Headers()
new AsciiString("/some/path/resource2")); .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, () -> { runInChannel(clientChannel, () -> {
clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient()); clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
clientHandler.encoder().writeHeaders(ctxClient(), 5, http2Headers2, 3, (short) 123, true, 0, clientHandler.encoder().writeHeaders(ctxClient(), 5, http2Headers2, 3, (short) 123, true, 0,
false, newPromiseClient()); false, newPromiseClient());
clientChannel.flush(); // Headers are queued in the flow controller and so flush them. clientChannel.flush(); // Headers are queued in the flow controller and so flush them.
clientHandler.encoder().writeData(ctxClient(), 3, content.retainedDuplicate(), 0, true, clientHandler.encoder().writeData(ctxClient(), 3, content.retainedDuplicate(), 0, true,
newPromiseClient()); newPromiseClient());
clientHandler.encoder().writeData(ctxClient(), 5, content2.retainedDuplicate(), 0, true, clientHandler.encoder().writeData(ctxClient(), 5, content2.retainedDuplicate(), 0, true,
newPromiseClient()); newPromiseClient());
clientChannel.flush(); clientChannel.flush();
}); });
awaitRequests(); awaitRequests();
@ -454,7 +474,10 @@ public class InboundHttp2ToHttpAdapterTest {
httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); 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")); .path(new AsciiString("/push/test"));
runInChannel(clientChannel, () -> { runInChannel(clientChannel, () -> {
clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers3, 0, true, newPromiseClient()); clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers3, 0, true, newPromiseClient());
@ -506,9 +529,11 @@ public class InboundHttp2ToHttpAdapterTest {
httpHeaders.set(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE); httpHeaders.set(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0); httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16); httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("PUT")) final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("PUT"))
.path(new AsciiString("/info/test")) .path(new AsciiString("/info/test"))
.scheme(new AsciiString("http"))
.set(new AsciiString(HttpHeaderNames.EXPECT.toString()), .set(new AsciiString(HttpHeaderNames.EXPECT.toString()),
new AsciiString(HttpHeaderValues.CONTINUE.toString())); new AsciiString(HttpHeaderValues.CONTINUE.toString()));
final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE); final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);

View File

@ -94,15 +94,6 @@
<excludeSpec>5.1 - half closed (remote): Sends a HEADERS frame</excludeSpec> <excludeSpec>5.1 - half closed (remote): Sends a HEADERS frame</excludeSpec>
<excludeSpec>5.1 - closed: Sends a HEADERS frame</excludeSpec> <excludeSpec>5.1 - closed: Sends a HEADERS frame</excludeSpec>
<excludeSpec>5.1.1 - Sends stream identifier that is numerically smaller than previous</excludeSpec> <excludeSpec>5.1.1 - Sends stream identifier that is numerically smaller than previous</excludeSpec>
<excludeSpec>8.1.2.2 - Sends a HEADERS frame that contains the connection-specific header field</excludeSpec>
<excludeSpec>8.1.2.2 - Sends a HEADERS frame that contains the TE header field with any value other than "trailers"</excludeSpec>
<excludeSpec>8.1.2.3 - Sends a HEADERS frame with empty ":path" pseudo-header field</excludeSpec>
<excludeSpec>8.1.2.3 - Sends a HEADERS frame that omits ":method" pseudo-header field</excludeSpec>
<excludeSpec>8.1.2.3 - Sends a HEADERS frame that omits ":scheme" pseudo-header field</excludeSpec>
<excludeSpec>8.1.2.3 - Sends a HEADERS frame that omits ":path" pseudo-header field</excludeSpec>
<excludeSpec>8.1.2.3 - Sends a HEADERS frame with duplicated ":method" pseudo-header field</excludeSpec>
<excludeSpec>8.1.2.3 - Sends a HEADERS frame with duplicated ":method" pseudo-header field</excludeSpec>
<excludeSpec>8.1.2.3 - Sends a HEADERS frame with duplicated ":scheme" pseudo-header field</excludeSpec>
<excludeSpec>8.1.2.6 - Sends a HEADERS frame with the "content-length" header field which does not equal the DATA frame payload length</excludeSpec> <excludeSpec>8.1.2.6 - Sends a HEADERS frame with the "content-length" header field which does not equal the DATA frame payload length</excludeSpec>
<excludeSpec>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</excludeSpec> <excludeSpec>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</excludeSpec>
</excludeSpecs> </excludeSpecs>