HTTP/2 Header Name Validation

Motivation:
The HTTP/2 header name validation was removed, and does not currently exist.

Modifications:
- Header name validation for HTTP/2 should be restored and set to the default mode of operation.

Result:
HTTP/2 header names are validated according to https://tools.ietf.org/html/rfc7540
This commit is contained in:
Scott Mitchell 2015-09-03 11:45:38 -07:00
parent 250a09df63
commit 47726991b2
14 changed files with 140 additions and 33 deletions

View File

@ -64,8 +64,22 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
private HeadersContinuation headersContinuation;
private int maxFrameSize;
/**
* Create a new instance.
* <p>
* Header names will be validated.
*/
public DefaultHttp2FrameReader() {
this(new DefaultHttp2HeadersDecoder());
this(true);
}
/**
* Create a new instance.
* @param validateHeaders {@code true} to validate headers. {@code false} to not validate headers.
* @see #DefaultHttp2HeadersDecoder(boolean)
*/
public DefaultHttp2FrameReader(boolean validateHeaders) {
this(new DefaultHttp2HeadersDecoder(validateHeaders));
}
public DefaultHttp2FrameReader(Http2HeadersDecoder headersDecoder) {

View File

@ -17,13 +17,50 @@ package io.netty.handler.codec.http2;
import io.netty.handler.codec.ByteStringValueConverter;
import io.netty.handler.codec.DefaultHeaders;
import io.netty.handler.codec.Headers;
import io.netty.util.ByteProcessor;
import io.netty.util.ByteString;
import io.netty.util.internal.PlatformDependent;
public class DefaultHttp2Headers extends DefaultHeaders<ByteString> implements Http2Headers {
private static final ByteProcessor HTTP2_NAME_VALIDATOR_PROCESSOR = new ByteProcessor() {
@Override
public boolean process(byte value) throws Exception {
if (value >= 'A' && value <= 'Z') {
throw new IllegalArgumentException("name must be all lower case but found: " + (char) value);
}
return true;
}
};
private static final NameValidator<ByteString> HTTP2_NAME_VALIDATOR = new NameValidator<ByteString>() {
@Override
public void validateName(ByteString name) {
try {
name.forEachByte(HTTP2_NAME_VALIDATOR_PROCESSOR);
} catch (Exception e) {
PlatformDependent.throwException(e);
}
}
};
private HeaderEntry<ByteString> firstNonPseudo = head;
/**
* Create a new instance.
* <p>
* Header names will be validated according to
* <a href="https://tools.ietf.org/html/rfc7540">rfc7540</a>.
*/
public DefaultHttp2Headers() {
super(ByteStringValueConverter.INSTANCE);
this(true);
}
/**
* Create a new instance.
* @param validate {@code true} to validate header names according to
* <a href="https://tools.ietf.org/html/rfc7540">rfc7540</a>. {@code false} to not validate header names.
*/
@SuppressWarnings("unchecked")
public DefaultHttp2Headers(boolean validate) {
super(ByteStringValueConverter.INSTANCE, validate ? HTTP2_NAME_VALIDATOR : NameValidator.NOT_NULL);
}
@Override

View File

@ -36,18 +36,28 @@ public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder, Http2Hea
private final int maxHeaderSize;
private final Decoder decoder;
private final Http2HeaderTable headerTable;
private final boolean validateHeaders;
public DefaultHttp2HeadersDecoder() {
this(DEFAULT_MAX_HEADER_SIZE, DEFAULT_HEADER_TABLE_SIZE);
this(true);
}
public DefaultHttp2HeadersDecoder(boolean validateHeaders) {
this(DEFAULT_MAX_HEADER_SIZE, DEFAULT_HEADER_TABLE_SIZE, validateHeaders);
}
public DefaultHttp2HeadersDecoder(int maxHeaderSize, int maxHeaderTableSize) {
this(maxHeaderSize, maxHeaderTableSize, true);
}
public DefaultHttp2HeadersDecoder(int maxHeaderSize, int maxHeaderTableSize, boolean validateHeaders) {
if (maxHeaderSize <= 0) {
throw new IllegalArgumentException("maxHeaderSize must be positive: " + maxHeaderSize);
}
decoder = new Decoder(maxHeaderSize, maxHeaderTableSize);
headerTable = new Http2HeaderTableDecoder();
this.maxHeaderSize = maxHeaderSize;
this.validateHeaders = validateHeaders;
}
@Override
@ -77,7 +87,7 @@ public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder, Http2Hea
public Http2Headers decodeHeaders(ByteBuf headerBlock) throws Http2Exception {
InputStream in = new ByteBufInputStream(headerBlock);
try {
final Http2Headers headers = new DefaultHttp2Headers();
final Http2Headers headers = new DefaultHttp2Headers(validateHeaders);
HeaderListener listener = new HeaderListener() {
@Override
public void addHeader(byte[] key, byte[] value, boolean sensitive) {

View File

@ -72,11 +72,19 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
private long gracefulShutdownTimeoutMillis = DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS;
public Http2ConnectionHandler(boolean server, Http2FrameListener listener) {
this(new DefaultHttp2Connection(server), listener);
this(server, listener, true);
}
public Http2ConnectionHandler(boolean server, Http2FrameListener listener, boolean validateHeaders) {
this(new DefaultHttp2Connection(server), listener, validateHeaders);
}
public Http2ConnectionHandler(Http2Connection connection, Http2FrameListener listener) {
this(connection, new DefaultHttp2FrameReader(), new DefaultHttp2FrameWriter(), listener);
this(connection, listener, true);
}
public Http2ConnectionHandler(Http2Connection connection, Http2FrameListener listener, boolean validateHeaders) {
this(connection, new DefaultHttp2FrameReader(validateHeaders), new DefaultHttp2FrameWriter(), listener);
}
public Http2ConnectionHandler(Http2Connection connection, Http2FrameReader frameReader,

View File

@ -280,8 +280,8 @@ public final class HttpConversionUtil {
* </ul>
* {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}.
*/
public static Http2Headers toHttp2Headers(HttpMessage in) throws Exception {
final Http2Headers out = new DefaultHttp2Headers();
public static Http2Headers toHttp2Headers(HttpMessage in, boolean validateHeaders) throws Exception {
final Http2Headers out = new DefaultHttp2Headers(validateHeaders);
HttpHeaders inHeaders = in.headers();
if (in instanceof HttpRequest) {
HttpRequest request = (HttpRequest) in;
@ -305,15 +305,15 @@ public final class HttpConversionUtil {
}
// Add the HTTP headers which have not been consumed above
return out.add(toHttp2Headers(inHeaders));
return out.add(toHttp2Headers(inHeaders, validateHeaders));
}
public static Http2Headers toHttp2Headers(HttpHeaders inHeaders) throws Exception {
public static Http2Headers toHttp2Headers(HttpHeaders inHeaders, boolean validateHeaders) throws Exception {
if (inHeaders.isEmpty()) {
return EmptyHttp2Headers.INSTANCE;
}
final Http2Headers out = new DefaultHttp2Headers();
final Http2Headers out = new DefaultHttp2Headers(validateHeaders);
Iterator<Entry<CharSequence, CharSequence>> iter = inHeaders.iteratorCharSequence();
while (iter.hasNext()) {

View File

@ -33,24 +33,48 @@ import io.netty.util.ReferenceCountUtil;
*/
public class HttpToHttp2ConnectionHandler extends Http2ConnectionHandler {
private final boolean validateHeaders;
private int currentStreamId;
public HttpToHttp2ConnectionHandler(boolean server, Http2FrameListener listener) {
this(server, listener, true);
}
public HttpToHttp2ConnectionHandler(boolean server, Http2FrameListener listener, boolean validateHeaders) {
super(server, listener);
this.validateHeaders = validateHeaders;
}
public HttpToHttp2ConnectionHandler(Http2Connection connection, Http2FrameListener listener) {
this(connection, listener, true);
}
public HttpToHttp2ConnectionHandler(Http2Connection connection, Http2FrameListener listener,
boolean validateHeaders) {
super(connection, listener);
this.validateHeaders = validateHeaders;
}
public HttpToHttp2ConnectionHandler(Http2Connection connection, Http2FrameReader frameReader,
Http2FrameWriter frameWriter, Http2FrameListener listener) {
this(connection, frameReader, frameWriter, listener, true);
}
public HttpToHttp2ConnectionHandler(Http2Connection connection, Http2FrameReader frameReader,
Http2FrameWriter frameWriter, Http2FrameListener listener, boolean validateHeaders) {
super(connection, frameReader, frameWriter, listener);
this.validateHeaders = validateHeaders;
}
public HttpToHttp2ConnectionHandler(Http2ConnectionDecoder decoder,
Http2ConnectionEncoder encoder) {
this(decoder, encoder, true);
}
public HttpToHttp2ConnectionHandler(Http2ConnectionDecoder decoder,
Http2ConnectionEncoder encoder, boolean validateHeaders) {
super(decoder, encoder);
this.validateHeaders = validateHeaders;
}
/**
@ -89,7 +113,7 @@ public class HttpToHttp2ConnectionHandler extends Http2ConnectionHandler {
currentStreamId = getStreamId(httpMsg.headers());
// Convert and write the headers.
Http2Headers http2Headers = HttpConversionUtil.toHttp2Headers(httpMsg);
Http2Headers http2Headers = HttpConversionUtil.toHttp2Headers(httpMsg, validateHeaders);
endStream = msg instanceof FullHttpMessage && !((FullHttpMessage) msg).content().isReadable();
encoder.writeHeaders(ctx, currentStreamId, http2Headers, 0, endStream, promiseAggregator.newPromise());
}
@ -102,7 +126,7 @@ public class HttpToHttp2ConnectionHandler extends Http2ConnectionHandler {
// Convert any trailing headers.
final LastHttpContent lastContent = (LastHttpContent) msg;
trailers = HttpConversionUtil.toHttp2Headers(lastContent.trailingHeaders());
trailers = HttpConversionUtil.toHttp2Headers(lastContent.trailingHeaders(), validateHeaders);
}
// Write the data

View File

@ -121,7 +121,7 @@ public class DefaultHttp2FrameIOTest {
}
}).when(ctx).write(any(), any(ChannelPromise.class));
reader = new DefaultHttp2FrameReader();
reader = new DefaultHttp2FrameReader(false);
writer = new DefaultHttp2FrameWriter();
}
@ -357,7 +357,7 @@ public class DefaultHttp2FrameIOTest {
}
private static Http2Headers dummyBinaryHeaders() {
DefaultHttp2Headers headers = new DefaultHttp2Headers();
DefaultHttp2Headers headers = new DefaultHttp2Headers(false);
for (int ix = 0; ix < 10; ++ix) {
headers.add(randomString(), randomString());
}
@ -365,13 +365,13 @@ public class DefaultHttp2FrameIOTest {
}
private static Http2Headers dummyHeaders() {
return new DefaultHttp2Headers().method(new AsciiString("GET")).scheme(new AsciiString("https"))
return new DefaultHttp2Headers(false).method(new AsciiString("GET")).scheme(new AsciiString("https"))
.authority(new AsciiString("example.org")).path(new AsciiString("/some/path"))
.add(new AsciiString("accept"), new AsciiString("*/*"));
}
private static Http2Headers largeHeaders() {
DefaultHttp2Headers headers = new DefaultHttp2Headers();
DefaultHttp2Headers headers = new DefaultHttp2Headers(false);
for (int i = 0; i < 100; ++i) {
String key = "this-is-a-test-header-key-" + i;
String value = "this-is-a-test-header-value-" + i;
@ -382,7 +382,7 @@ public class DefaultHttp2FrameIOTest {
private Http2Headers headersOfSize(final int minSize) {
final ByteString singleByte = new ByteString(new byte[]{0});
DefaultHttp2Headers headers = new DefaultHttp2Headers();
DefaultHttp2Headers headers = new DefaultHttp2Headers(false);
for (int size = 0; size < minSize; size += 2) {
headers.add(singleByte, singleByte);
}

View File

@ -40,7 +40,7 @@ public class DefaultHttp2HeadersDecoderTest {
@Before
public void setup() {
decoder = new DefaultHttp2HeadersDecoder();
decoder = new DefaultHttp2HeadersDecoder(false);
}
@Test

View File

@ -65,6 +65,13 @@ public class DefaultHttp2HeadersTest {
}
}
@Test(expected = IllegalArgumentException.class)
public void testHeaderNameValidation() {
Http2Headers headers = newHeaders();
headers.add(fromAscii("Foo"), fromAscii("foo"));
}
private static void verifyAllPseudoHeadersPresent(Http2Headers headers) {
for (PseudoHeaderName pseudoName : PseudoHeaderName.values()) {
assertNotNull(headers.get(pseudoName.value()));

View File

@ -482,7 +482,7 @@ public class Http2ConnectionRoundtripTest {
serverFrameCountDown =
new FrameCountDown(serverListener, serverSettingsAckLatch,
requestLatch, dataLatch, trailersLatch, goAwayLatch);
p.addLast(new Http2ConnectionHandler(true, serverFrameCountDown));
p.addLast(new Http2ConnectionHandler(true, serverFrameCountDown, false));
}
});
@ -492,7 +492,7 @@ public class Http2ConnectionRoundtripTest {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new Http2ConnectionHandler(false, clientListener));
p.addLast(new Http2ConnectionHandler(false, clientListener, false));
}
});
@ -513,7 +513,7 @@ public class Http2ConnectionRoundtripTest {
}
private static Http2Headers dummyHeaders() {
return new DefaultHttp2Headers().method(new AsciiString("GET")).scheme(new AsciiString("https"))
return new DefaultHttp2Headers(false).method(new AsciiString("GET")).scheme(new AsciiString("https"))
.authority(new AsciiString("example.org")).path(new AsciiString("/some/path/resource2"))
.add(randomString(), randomString());
}

View File

@ -398,7 +398,7 @@ public class Http2FrameRoundtripTest {
}
private static Http2Headers headers() {
return new DefaultHttp2Headers().method(new AsciiString("GET")).scheme(new AsciiString("https"))
return new DefaultHttp2Headers(false).method(new AsciiString("GET")).scheme(new AsciiString("https"))
.authority(new AsciiString("example.org")).path(new AsciiString("/some/path/resource2"))
.add(randomString(), randomString());
}

View File

@ -37,7 +37,7 @@ public class Http2HeaderBlockIOTest {
@Before
public void setup() {
encoder = new DefaultHttp2HeadersEncoder();
decoder = new DefaultHttp2HeadersDecoder();
decoder = new DefaultHttp2HeadersDecoder(false);
buffer = Unpooled.buffer();
}
@ -54,21 +54,18 @@ public class Http2HeaderBlockIOTest {
@Test
public void successiveCallsShouldSucceed() throws Http2Exception {
Http2Headers in =
new DefaultHttp2Headers().method(new AsciiString("GET")).scheme(new AsciiString("https"))
Http2Headers in = new DefaultHttp2Headers().method(new AsciiString("GET")).scheme(new AsciiString("https"))
.authority(new AsciiString("example.org")).path(new AsciiString("/some/path"))
.add(new AsciiString("accept"), new AsciiString("*/*"));
assertRoundtripSuccessful(in);
in =
new DefaultHttp2Headers().method(new AsciiString("GET")).scheme(new AsciiString("https"))
in = new DefaultHttp2Headers().method(new AsciiString("GET")).scheme(new AsciiString("https"))
.authority(new AsciiString("example.org")).path(new AsciiString("/some/path/resource1"))
.add(new AsciiString("accept"), new AsciiString("image/jpeg"))
.add(new AsciiString("cache-control"), new AsciiString("no-cache"));
assertRoundtripSuccessful(in);
in =
new DefaultHttp2Headers().method(new AsciiString("GET")).scheme(new AsciiString("https"))
in = new DefaultHttp2Headers().method(new AsciiString("GET")).scheme(new AsciiString("https"))
.authority(new AsciiString("example.org")).path(new AsciiString("/some/path/resource2"))
.add(new AsciiString("accept"), new AsciiString("image/png"))
.add(new AsciiString("cache-control"), new AsciiString("no-cache"));
@ -91,7 +88,7 @@ public class Http2HeaderBlockIOTest {
}
private static Http2Headers headers() {
return new DefaultHttp2Headers().method(new AsciiString("GET")).scheme(new AsciiString("https"))
return new DefaultHttp2Headers(false).method(new AsciiString("GET")).scheme(new AsciiString("https"))
.authority(new AsciiString("example.org")).path(new AsciiString("/some/path/resource2"))
.add(new AsciiString("accept"), new AsciiString("image/png"))
.add(new AsciiString("cache-control"), new AsciiString("no-cache"))

View File

@ -89,7 +89,7 @@ final class Http2TestUtil {
}
FrameAdapter(Http2Connection connection, Http2FrameListener listener, CountDownLatch latch) {
this(connection, new DefaultHttp2FrameReader(), listener, latch);
this(connection, new DefaultHttp2FrameReader(false), listener, latch);
}
FrameAdapter(Http2Connection connection, DefaultHttp2FrameReader reader, Http2FrameListener listener,

View File

@ -67,6 +67,11 @@ public class DefaultHeaders<T> implements Headers<T> {
int size;
public interface NameValidator<T> {
/**
* Verify that {@code name} is valid.
* @param name The name to validate.
* @throws RuntimeException if {@code name} is not valid.
*/
void validateName(T name);
@SuppressWarnings("rawtypes")
@ -80,7 +85,12 @@ public class DefaultHeaders<T> implements Headers<T> {
@SuppressWarnings("unchecked")
public DefaultHeaders(ValueConverter<T> valueConverter) {
this(JAVA_HASHER, valueConverter, NameValidator.NOT_NULL);
this(valueConverter, NameValidator.NOT_NULL);
}
@SuppressWarnings("unchecked")
public DefaultHeaders(ValueConverter<T> valueConverter, NameValidator<T> nameValidator) {
this(JAVA_HASHER, valueConverter, nameValidator);
}
public DefaultHeaders(HashingStrategy<T> nameHashingStrategy,