From 4f204009dedac6104f67188348b1cfc27e358109 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Thu, 3 Sep 2015 11:45:38 -0700 Subject: [PATCH] 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 --- .../codec/http2/DefaultHttp2FrameReader.java | 16 +++++++- .../codec/http2/DefaultHttp2Headers.java | 39 ++++++++++++++++++- .../http2/DefaultHttp2HeadersDecoder.java | 14 ++++++- .../codec/http2/Http2ConnectionHandler.java | 12 +++++- .../codec/http2/HttpConversionUtil.java | 10 ++--- .../http2/HttpToHttp2ConnectionHandler.java | 28 ++++++++++++- .../codec/http2/DefaultHttp2FrameIOTest.java | 10 ++--- .../http2/DefaultHttp2HeadersDecoderTest.java | 2 +- .../codec/http2/DefaultHttp2HeadersTest.java | 7 ++++ .../http2/Http2ConnectionRoundtripTest.java | 6 +-- .../codec/http2/Http2FrameRoundtripTest.java | 2 +- .../codec/http2/Http2HeaderBlockIOTest.java | 13 +++---- .../handler/codec/http2/Http2TestUtil.java | 2 +- .../netty/handler/codec/DefaultHeaders.java | 12 +++++- 14 files changed, 140 insertions(+), 33 deletions(-) diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameReader.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameReader.java index 035fc949e8..96bb2ca5c8 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameReader.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameReader.java @@ -64,8 +64,22 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize private HeadersContinuation headersContinuation; private int maxFrameSize; + /** + * Create a new instance. + *

+ * 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) { diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java index 6aef682649..88ecd92341 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java @@ -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 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 HTTP2_NAME_VALIDATOR = new NameValidator() { + @Override + public void validateName(ByteString name) { + try { + name.forEachByte(HTTP2_NAME_VALIDATOR_PROCESSOR); + } catch (Exception e) { + PlatformDependent.throwException(e); + } + } + }; private HeaderEntry firstNonPseudo = head; + /** + * Create a new instance. + *

+ * Header names will be validated according to + * rfc7540. + */ public DefaultHttp2Headers() { - super(ByteStringValueConverter.INSTANCE); + this(true); + } + + /** + * Create a new instance. + * @param validate {@code true} to validate header names according to + * rfc7540. {@code false} to not validate header names. + */ + @SuppressWarnings("unchecked") + public DefaultHttp2Headers(boolean validate) { + super(ByteStringValueConverter.INSTANCE, validate ? HTTP2_NAME_VALIDATOR : NameValidator.NOT_NULL); } @Override diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java index 2b35e61602..b932b8277e 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java @@ -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) { diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java index a19fb0c60e..64f5ed8a43 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java @@ -69,11 +69,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, diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java index 59c1025d19..75e7e90f2b 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java @@ -279,8 +279,8 @@ public final class HttpConversionUtil { * * {@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; @@ -304,15 +304,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); for (Entry entry : inHeaders) { final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase(); diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandler.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandler.java index 8f5da2f129..6a8328190b 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandler.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandler.java @@ -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 diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameIOTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameIOTest.java index b975281602..756501c880 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameIOTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameIOTest.java @@ -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); } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java index b825373fb2..fd12cfe163 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java @@ -40,7 +40,7 @@ public class DefaultHttp2HeadersDecoderTest { @Before public void setup() { - decoder = new DefaultHttp2HeadersDecoder(); + decoder = new DefaultHttp2HeadersDecoder(false); } @Test diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java index 7839a8c0f9..b14b4346f0 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java @@ -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())); diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java index 6127f2853a..54c3497107 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java @@ -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()); } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameRoundtripTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameRoundtripTest.java index 98d5795c42..1f699d2afa 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameRoundtripTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameRoundtripTest.java @@ -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()); } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeaderBlockIOTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeaderBlockIOTest.java index 951d96af17..6060b4c326 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeaderBlockIOTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeaderBlockIOTest.java @@ -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")) diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java index 68ed6a241a..5ffc4ae166 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java @@ -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, diff --git a/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java b/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java index 09781e2530..1021d8d69f 100644 --- a/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java @@ -66,6 +66,11 @@ public class DefaultHeaders implements Headers { int size; public interface NameValidator { + /** + * 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") @@ -79,7 +84,12 @@ public class DefaultHeaders implements Headers { @SuppressWarnings("unchecked") public DefaultHeaders(ValueConverter valueConverter) { - this(JAVA_HASHER, valueConverter, NameValidator.NOT_NULL); + this(valueConverter, NameValidator.NOT_NULL); + } + + @SuppressWarnings("unchecked") + public DefaultHeaders(ValueConverter valueConverter, NameValidator nameValidator) { + this(JAVA_HASHER, valueConverter, nameValidator); } public DefaultHeaders(HashingStrategy nameHashingStrategy,