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:
parent
4be554a21f
commit
dd5d4887ed
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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, () -> {
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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));
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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));
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user