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

This reverts commit dd5d4887ed.
This commit is contained in:
Norman Maurer 2019-12-17 17:45:17 +01:00
parent 0e4c073bcf
commit 93267f9b98
14 changed files with 90 additions and 500 deletions

View File

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

View File

@ -29,9 +29,6 @@ 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.Http2Exception.connectionError;
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.Http2Stream.State.CLOSED;
import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_REMOTE;
@ -60,7 +57,6 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
private final Http2PromisedRequestVerifier requestVerifier;
private final Http2SettingsReceivedConsumer settingsReceivedConsumer;
private final boolean autoAckPing;
private final boolean validateHeaders;
public DefaultHttp2ConnectionDecoder(Http2Connection connection,
Http2ConnectionEncoder encoder,
@ -95,15 +91,6 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
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.
* @param connection The {@link Http2Connection} associated with this decoder.
@ -124,8 +111,7 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
Http2FrameReader frameReader,
Http2PromisedRequestVerifier requestVerifier,
boolean autoAckSettings,
boolean autoAckPing,
boolean validateHeaders) {
boolean autoAckPing) {
this.autoAckPing = autoAckPing;
if (autoAckSettings) {
settingsReceivedConsumer = null;
@ -140,7 +126,6 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
this.frameReader = requireNonNull(frameReader, "frameReader");
this.encoder = requireNonNull(encoder, "encoder");
this.requestVerifier = requireNonNull(requestVerifier, "requestVerifier");
this.validateHeaders = validateHeaders;
if (connection.local().flowController() == null) {
connection.local().flowController(new DefaultHttp2LocalFlowController(connection));
}
@ -350,10 +335,6 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
streamId, endOfStream, stream.state());
}
if (validateHeaders) {
validateHeaders(streamId, headers, stream);
}
switch (stream.state()) {
case RESERVED_REMOTE:
stream.open(endOfStream);
@ -646,18 +627,6 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
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 {

View File

@ -393,8 +393,13 @@ final class HpackDecoder {
throw streamError(streamId, PROTOCOL_ERROR, "Invalid HTTP/2 pseudo-header '%s' encountered.", name);
}
return pseudoHeader.isRequestOnly() ?
final HeaderType currentHeaderType = pseudoHeader.isRequestOnly() ?
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;

View File

@ -1,159 +0,0 @@
/*
* 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,14 +391,9 @@ 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());
// 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));
}
setHttp2Scheme(inHeaders, requestTargetUri, out);
if (!isOriginForm(requestTargetUri) && !isAsteriskForm(requestTargetUri)) {
// Attempt to take from HOST header before taking from the request-line

View File

@ -68,7 +68,6 @@ 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;
@ -137,7 +136,7 @@ public class DataCompressionHttp2Test {
@Test
public void justHeadersNoData() throws Exception {
bootstrapEnv(0);
final Http2Headers headers = new DefaultHttp2Headers().method(GET).path(PATH).scheme(SCHEME)
final Http2Headers headers = new DefaultHttp2Headers().method(GET).path(PATH)
.set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
runInChannel(clientChannel, () -> {
@ -155,7 +154,7 @@ public class DataCompressionHttp2Test {
final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
bootstrapEnv(data.readableBytes());
try {
final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH).scheme(SCHEME)
final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
.set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
runInChannel(clientChannel, () -> {
@ -176,7 +175,7 @@ public class DataCompressionHttp2Test {
final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
bootstrapEnv(data.readableBytes());
try {
final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH).scheme(SCHEME)
final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
.set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
runInChannel(clientChannel, () -> {
@ -199,7 +198,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).scheme(SCHEME)
final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
.set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
runInChannel(clientChannel, () -> {
@ -224,7 +223,7 @@ public class DataCompressionHttp2Test {
bootstrapEnv(BUFFER_SIZE);
final ByteBuf data = Unpooled.wrappedBuffer(bytes);
try {
final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH).scheme(SCHEME)
final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
.set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.DEFLATE);
runInChannel(clientChannel, () -> {

View File

@ -23,13 +23,13 @@ import io.netty.channel.ChannelPromise;
import io.netty.channel.DefaultChannelPromise;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName;
import junit.framework.AssertionFailedError;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.Collections;
@ -39,11 +39,9 @@ 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.hamcrest.Matchers.instanceOf;
@ -185,8 +183,7 @@ public class DefaultHttp2ConnectionDecoderTest {
when(ctx.newPromise()).thenReturn(promise);
when(ctx.write(any())).thenReturn(future);
decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader,
Http2PromisedRequestVerifier.ALWAYS_VERIFY, true, true, true);
decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader);
decoder.lifecycleManager(lifecycleManager);
decoder.frameListener(listener);
@ -501,64 +498,6 @@ 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);

View File

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

View File

@ -1,174 +0,0 @@
/*
* 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-specific 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,7 +42,6 @@ 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;
@ -158,8 +157,8 @@ public class Http2MultiplexCodecBuilderTest {
assertTrue(childChannel2.isActive());
assertFalse(isStreamIdValid(childChannel2.stream().id()));
Http2Headers headers1 = newHttp2HeadersWithRequestPseudoHeaders();
Http2Headers headers2 = newHttp2HeadersWithRequestPseudoHeaders();
Http2Headers headers1 = new DefaultHttp2Headers();
Http2Headers headers2 = new DefaultHttp2Headers();
// Test that streams can be made active (headers sent) in different order than the corresponding channels
// have been created.
childChannel2.writeAndFlush(new DefaultHttp2HeadersFrame(headers2));
@ -188,7 +187,7 @@ public class Http2MultiplexCodecBuilderTest {
assertTrue(childChannel.isRegistered());
assertTrue(childChannel.isActive());
Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
Http2Headers headers = new DefaultHttp2Headers();
childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
ByteBuf data = Unpooled.buffer(100).writeZero(100);
childChannel.writeAndFlush(new DefaultHttp2DataFrame(data, true));

View File

@ -94,13 +94,6 @@ 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.
*/

View File

@ -244,8 +244,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"))
.authority(new AsciiString("www.example.com:80"));
new DefaultHttp2Headers().method(new AsciiString("CONNECT")).path(new AsciiString("/"))
.scheme(new AsciiString("http")).authority(new AsciiString("www.example.com:80"));
ChannelPromise writePromise = newPromise();
verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));

View File

@ -42,7 +42,6 @@ import io.netty.handler.codec.http.HttpMethod;
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.HttpConversionUtil.ExtensionHeaderNames;
import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.Future;
@ -256,15 +255,12 @@ 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");
final Http2Headers http2Headers = new DefaultHttp2Headers()
.method(new AsciiString("GET"))
.scheme(new AsciiString("http"))
.path(new AsciiString("/some/path/resource2"));
final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path(
new AsciiString("/some/path/resource2"));
runInChannel(clientChannel, () -> {
clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
clientHandler.encoder().writeData(ctxClient(), 3, content.retainedDuplicate(), 0, true,
newPromiseClient());
newPromiseClient());
clientChannel.flush();
});
awaitRequests();
@ -289,11 +285,8 @@ 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");
final Http2Headers http2Headers = new DefaultHttp2Headers()
.method(new AsciiString("GET"))
.scheme(new AsciiString("http"))
.path(new AsciiString("/some/path/resource2"));
final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path(
new AsciiString("/some/path/resource2"));
final int midPoint = text.length() / 2;
runInChannel(clientChannel, () -> {
clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
@ -326,11 +319,8 @@ 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");
final Http2Headers http2Headers = new DefaultHttp2Headers()
.method(new AsciiString("GET"))
.scheme("http")
.path(new AsciiString("/some/path/resource2"));
final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path(
new AsciiString("/some/path/resource2"));
runInChannel(clientChannel, () -> {
clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
clientHandler.encoder().writeData(ctxClient(), 3, content.retain(), 0, false, newPromiseClient());
@ -360,15 +350,12 @@ 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"))
.scheme(new AsciiString("http"))
.path(new AsciiString("/some/path/resource2"));
final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).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"))
@ -406,31 +393,24 @@ 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());
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"));
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"));
runInChannel(clientChannel, () -> {
clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
clientHandler.encoder().writeHeaders(ctxClient(), 5, http2Headers2, 3, (short) 123, true, 0,
false, newPromiseClient());
clientChannel.flush(); // Headers are queued in the flow controller and so flush them.
clientHandler.encoder().writeData(ctxClient(), 3, content.retainedDuplicate(), 0, true,
newPromiseClient());
newPromiseClient());
clientHandler.encoder().writeData(ctxClient(), 5, content2.retainedDuplicate(), 0, true,
newPromiseClient());
newPromiseClient());
clientChannel.flush();
});
awaitRequests();
@ -474,10 +454,7 @@ 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);
httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
final Http2Headers http2Headers3 = new DefaultHttp2Headers()
.method(new AsciiString("GET"))
.scheme("http")
final Http2Headers http2Headers3 = new DefaultHttp2Headers().method(new AsciiString("GET"))
.path(new AsciiString("/push/test"));
runInChannel(clientChannel, () -> {
clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers3, 0, true, newPromiseClient());
@ -529,11 +506,9 @@ 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);

View File

@ -94,6 +94,15 @@
<excludeSpec>5.1 - half closed (remote): 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>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 sum of the multiple DATA frames payload length</excludeSpec>
</excludeSpecs>