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 c449ceac3a
commit 4f204009de
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 HeadersContinuation headersContinuation;
private int maxFrameSize; private int maxFrameSize;
/**
* Create a new instance.
* <p>
* Header names will be validated.
*/
public DefaultHttp2FrameReader() { 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) { 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.ByteStringValueConverter;
import io.netty.handler.codec.DefaultHeaders; import io.netty.handler.codec.DefaultHeaders;
import io.netty.handler.codec.Headers; import io.netty.handler.codec.Headers;
import io.netty.util.ByteProcessor;
import io.netty.util.ByteString; import io.netty.util.ByteString;
import io.netty.util.internal.PlatformDependent;
public class DefaultHttp2Headers extends DefaultHeaders<ByteString> implements Http2Headers { 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; 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() { 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 @Override

View File

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

View File

@ -69,11 +69,19 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
private long gracefulShutdownTimeoutMillis = DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS; private long gracefulShutdownTimeoutMillis = DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS;
public Http2ConnectionHandler(boolean server, Http2FrameListener listener) { 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) { 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, public Http2ConnectionHandler(Http2Connection connection, Http2FrameReader frameReader,

View File

@ -279,8 +279,8 @@ public final class HttpConversionUtil {
* </ul> * </ul>
* {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}. * {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}.
*/ */
public static Http2Headers toHttp2Headers(HttpMessage in) throws Exception { public static Http2Headers toHttp2Headers(HttpMessage in, boolean validateHeaders) throws Exception {
final Http2Headers out = new DefaultHttp2Headers(); final Http2Headers out = new DefaultHttp2Headers(validateHeaders);
HttpHeaders inHeaders = in.headers(); HttpHeaders inHeaders = in.headers();
if (in instanceof HttpRequest) { if (in instanceof HttpRequest) {
HttpRequest request = (HttpRequest) in; HttpRequest request = (HttpRequest) in;
@ -304,15 +304,15 @@ public final class HttpConversionUtil {
} }
// Add the HTTP headers which have not been consumed above // 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()) { if (inHeaders.isEmpty()) {
return EmptyHttp2Headers.INSTANCE; return EmptyHttp2Headers.INSTANCE;
} }
final Http2Headers out = new DefaultHttp2Headers(); final Http2Headers out = new DefaultHttp2Headers(validateHeaders);
for (Entry<CharSequence, CharSequence> entry : inHeaders) { for (Entry<CharSequence, CharSequence> entry : inHeaders) {
final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase(); final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase();

View File

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

View File

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

View File

@ -40,7 +40,7 @@ public class DefaultHttp2HeadersDecoderTest {
@Before @Before
public void setup() { public void setup() {
decoder = new DefaultHttp2HeadersDecoder(); decoder = new DefaultHttp2HeadersDecoder(false);
} }
@Test @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) { private static void verifyAllPseudoHeadersPresent(Http2Headers headers) {
for (PseudoHeaderName pseudoName : PseudoHeaderName.values()) { for (PseudoHeaderName pseudoName : PseudoHeaderName.values()) {
assertNotNull(headers.get(pseudoName.value())); assertNotNull(headers.get(pseudoName.value()));

View File

@ -482,7 +482,7 @@ public class Http2ConnectionRoundtripTest {
serverFrameCountDown = serverFrameCountDown =
new FrameCountDown(serverListener, serverSettingsAckLatch, new FrameCountDown(serverListener, serverSettingsAckLatch,
requestLatch, dataLatch, trailersLatch, goAwayLatch); 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 @Override
protected void initChannel(Channel ch) throws Exception { protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline(); 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() { 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")) .authority(new AsciiString("example.org")).path(new AsciiString("/some/path/resource2"))
.add(randomString(), randomString()); .add(randomString(), randomString());
} }

View File

@ -398,7 +398,7 @@ public class Http2FrameRoundtripTest {
} }
private static Http2Headers headers() { 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")) .authority(new AsciiString("example.org")).path(new AsciiString("/some/path/resource2"))
.add(randomString(), randomString()); .add(randomString(), randomString());
} }

View File

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

View File

@ -66,6 +66,11 @@ public class DefaultHeaders<T> implements Headers<T> {
int size; int size;
public interface NameValidator<T> { 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); void validateName(T name);
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
@ -79,7 +84,12 @@ public class DefaultHeaders<T> implements Headers<T> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public DefaultHeaders(ValueConverter<T> valueConverter) { 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, public DefaultHeaders(HashingStrategy<T> nameHashingStrategy,