netty5/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersValidator.java
Julien Hoarau dd5d4887ed 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
2019-10-27 16:20:18 +01:00

160 lines
7.2 KiB
Java

/*
* 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());
}
}
}