headerValues, boolean expectsValid) {
@ParameterizedTest
@MethodSource("headers")
void testHeaders(Http.Header header, boolean expectsValid) {
- Http1ClientRequest request = client.get("http://localhost:" + dummyPort + "/test");
+ Http1Client clientValidateRequestHeaders = Http1Client.builder()
+ .protocolConfig(it -> {
+ it.validateRequestHeaders(true);
+ it.validateResponseHeaders(false);
+ })
+ .build();
+ Http1ClientRequest request = clientValidateRequestHeaders.get("http://localhost:" + dummyPort + "/test");
request.connection(new FakeHttp1ClientConnection());
request.header(header);
if (expectsValid) {
@@ -267,10 +279,13 @@ void testHeaders(Http.Header header, boolean expectsValid) {
@ParameterizedTest
@MethodSource("headers")
void testDisableHeaderValidation(Http.Header header, boolean expectsValid) {
- Http1Client clientWithNoHeaderValidation = Http1Client.builder()
- .protocolConfig(it -> it.validateHeaders(false))
+ Http1Client clientWithDisabledHeaderValidation = Http1Client.builder()
+ .protocolConfig(it -> {
+ it.validateRequestHeaders(false);
+ it.validateResponseHeaders(false);
+ })
.build();
- Http1ClientRequest request = clientWithNoHeaderValidation.put("http://localhost:" + dummyPort + "/test");
+ Http1ClientRequest request = clientWithDisabledHeaderValidation.put("http://localhost:" + dummyPort + "/test");
request.header(header);
request.connection(new FakeHttp1ClientConnection());
HttpClientResponse response = request.submit("Sending Something");
@@ -284,7 +299,13 @@ void testDisableHeaderValidation(Http.Header header, boolean expectsValid) {
@ParameterizedTest
@MethodSource("responseHeaders")
void testHeadersFromResponse(String headerName, String headerValue, boolean expectsValid) {
- Http1ClientRequest request = client.get("http://localhost:" + dummyPort + BAD_HEADER_PATH);
+ Http1Client clientValidateResponseHeaders = Http1Client.builder()
+ .protocolConfig(it -> {
+ it.validateRequestHeaders(false);
+ it.validateResponseHeaders(true);
+ })
+ .build();
+ Http1ClientRequest request = clientValidateResponseHeaders.get("http://localhost:" + dummyPort + BAD_HEADER_PATH);
request.connection(new FakeHttp1ClientConnection());
String headerNameAndValue = headerName + HEADER_NAME_VALUE_DELIMETER + headerValue;
if (expectsValid) {
@@ -301,7 +322,10 @@ void testHeadersFromResponse(String headerName, String headerValue, boolean expe
@MethodSource("responseHeadersForDisabledValidation")
void testDisableValidationForHeadersFromResponse(String headerName, String headerValue) {
Http1Client clientWithNoHeaderValidation = Http1Client.builder()
- .protocolConfig(it -> it.validateHeaders(false))
+ .protocolConfig(it -> {
+ it.validateRequestHeaders(false);
+ it.validateResponseHeaders(false);
+ })
.build();
Http1ClientRequest request = clientWithNoHeaderValidation.put("http://localhost:" + dummyPort + BAD_HEADER_PATH);
request.connection(new FakeHttp1ClientConnection());
diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ConfigBlueprint.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ConfigBlueprint.java
index 2f3f9e200b8..c7270aac065 100644
--- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ConfigBlueprint.java
+++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ConfigBlueprint.java
@@ -60,11 +60,29 @@ interface Http1ConfigBlueprint extends ProtocolConfig {
* are validated by format
* (content length is always validated as it is part of protocol processing (other headers may be validated if
* features use them)).
+ *
+ * Defaults to {@code true}.
+ *
*
* @return whether to validate headers
*/
@ConfiguredOption("true")
- boolean validateHeaders();
+ boolean validateRequestHeaders();
+
+ /**
+ * Whether to validate headers.
+ * If set to false, any value is accepted, otherwise validates headers + known headers
+ * are validated by format
+ * (content length is always validated as it is part of protocol processing (other headers may be validated if
+ * features use them)).
+ *
+ * Defaults to {@code false} as user has control on the header creation.
+ *
+ *
+ * @return whether to validate headers
+ */
+ @ConfiguredOption("false")
+ boolean validateResponseHeaders();
/**
* If set to false, any path is accepted (even containing illegal characters).
diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java
index 3f7435b8645..e4c3e74addc 100644
--- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java
+++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java
@@ -104,7 +104,7 @@ public class Http1Connection implements ServerConnection, InterruptableTask headers) {
writer,
request,
!request.headers()
- .contains(Headers.CONNECTION_CLOSE));
+ .contains(Headers.CONNECTION_CLOSE),
+ http1Config.validateResponseHeaders());
routing.route(ctx, request, response);
// we have handled a request without request entity
@@ -355,7 +356,7 @@ private void route(HttpPrologue prologue, WritableHeaders> headers) {
}
} else {
// Check whether Content-Encoding header is present when headers validation is enabled
- if (http1Config.validateHeaders() && headers.contains(Http.HeaderNames.CONTENT_ENCODING)) {
+ if (http1Config.validateRequestHeaders() && headers.contains(Http.HeaderNames.CONTENT_ENCODING)) {
throw RequestException.builder()
.type(EventType.BAD_REQUEST)
.request(DirectTransportRequest.create(prologue, headers))
@@ -382,7 +383,8 @@ private void route(HttpPrologue prologue, WritableHeaders> headers) {
writer,
request,
!request.headers()
- .contains(Headers.CONNECTION_CLOSE));
+ .contains(Headers.CONNECTION_CLOSE),
+ http1Config.validateResponseHeaders());
routing.route(ctx, request, response);
@@ -436,7 +438,8 @@ private void handleRequestException(RequestException e) {
byte[] message = response.entity().orElse(BufferData.EMPTY_BYTES);
headers.set(Headers.create(Http.HeaderNames.CONTENT_LENGTH, String.valueOf(message.length)));
- Http1ServerResponse.nonEntityBytes(headers, response.status(), buffer, response.keepAlive());
+ Http1ServerResponse.nonEntityBytes(headers, response.status(), buffer, response.keepAlive(),
+ http1Config.validateResponseHeaders());
if (message.length != 0) {
buffer.write(message);
}
diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ServerResponse.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ServerResponse.java
index de20d0e71c3..1ca41402fb0 100644
--- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ServerResponse.java
+++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ServerResponse.java
@@ -76,12 +76,14 @@ class Http1ServerResponse extends ServerResponseBase {
private ClosingBufferedOutputStream outputStream;
private long entitySize;
private String streamResult = "";
+ private final boolean validateHeaders;
Http1ServerResponse(ConnectionContext ctx,
Http1ConnectionListener sendListener,
DataWriter dataWriter,
Http1ServerRequest request,
- boolean keepAlive) {
+ boolean keepAlive,
+ boolean validateHeaders) {
super(ctx, request);
this.ctx = ctx;
@@ -90,12 +92,14 @@ class Http1ServerResponse extends ServerResponseBase {
this.request = request;
this.headers = ServerResponseHeaders.create();
this.keepAlive = keepAlive;
+ this.validateHeaders = validateHeaders;
}
static void nonEntityBytes(ServerResponseHeaders headers,
Http.Status status,
BufferData buffer,
- boolean keepAlive) {
+ boolean keepAlive,
+ boolean validateHeaders) {
// first write status
if (status == null || status == Http.Status.OK_200) {
@@ -124,7 +128,7 @@ static void nonEntityBytes(ServerResponseHeaders headers,
}
// write headers followed by empty line
- writeHeaders(headers, buffer);
+ writeHeaders(headers, buffer, validateHeaders);
buffer.write('\r'); // "\r\n" - empty line after headers
buffer.write('\n');
@@ -175,7 +179,8 @@ public OutputStream outputStream() {
ctx,
sendListener,
request,
- keepAlive);
+ keepAlive,
+ validateHeaders);
int writeBufferSize = ctx.listenerContext().config().writeBufferSize();
outputStream = new ClosingBufferedOutputStream(bos, writeBufferSize);
@@ -267,7 +272,10 @@ private void handleSinkData(Object data, MediaType mediaType) {
}
}
- private static void writeHeaders(io.helidon.common.http.Headers headers, BufferData buffer) {
+ private static void writeHeaders(io.helidon.common.http.Headers headers, BufferData buffer, boolean validate) {
+ if (validate) {
+ headers.forEach(Header::validate);
+ }
for (Header header : headers) {
header.writeHttp1Header(buffer);
}
@@ -293,7 +301,7 @@ private BufferData responseBuffer(byte[] bytes) {
// give some space for code and headers + entity
BufferData responseBuffer = BufferData.growing(256 + bytes.length);
- nonEntityBytes(headers, status(), responseBuffer, keepAlive);
+ nonEntityBytes(headers, status(), responseBuffer, keepAlive, validateHeaders);
if (bytes.length > 0) {
responseBuffer.write(bytes);
}
@@ -324,6 +332,7 @@ private static class BlockingOutputStream extends OutputStream {
private boolean firstByte = true;
private long responseBytesTotal;
private boolean closing = false;
+ private boolean validateHeaders = false;
private BlockingOutputStream(ServerResponseHeaders headers,
WritableHeaders> trailers,
@@ -334,7 +343,8 @@ private BlockingOutputStream(ServerResponseHeaders headers,
ConnectionContext ctx,
Http1ConnectionListener sendListener,
Http1ServerRequest request,
- boolean keepAlive) {
+ boolean keepAlive,
+ boolean validateHeaders) {
this.headers = headers;
this.trailers = trailers;
this.status = status;
@@ -348,6 +358,7 @@ private BlockingOutputStream(ServerResponseHeaders headers,
this.request = request;
this.keepAlive = keepAlive;
this.forcedChunked = headers.contains(Http.Headers.TRANSFER_ENCODING_CHUNKED);
+ this.validateHeaders = validateHeaders;
}
@Override
@@ -422,7 +433,7 @@ void commit() {
trailers.set(STREAM_STATUS_NAME, String.valueOf(status.get().code()));
trailers.set(STREAM_RESULT_NAME, streamResult.get());
BufferData buffer = BufferData.growing(128);
- writeHeaders(trailers, buffer);
+ writeHeaders(trailers, buffer, this.validateHeaders);
buffer.write('\r'); // "\r\n" - empty line after headers
buffer.write('\n');
dataWriter.write(buffer);
@@ -458,7 +469,7 @@ private void write(BufferData buffer) throws IOException {
sendListener.headers(ctx, headers);
// write headers and payload part in one buffer to avoid TCP/ACK delay problems
BufferData growing = BufferData.growing(256 + buffer.available());
- nonEntityBytes(headers, status.get(), growing, keepAlive);
+ nonEntityBytes(headers, status.get(), growing, keepAlive, validateHeaders);
// check not exceeding content-length
bytesWritten += buffer.available();
checkContentLength(buffer);
@@ -511,7 +522,7 @@ private void sendFirstChunkOnly() {
// at this moment, we must send headers
sendListener.headers(ctx, headers);
BufferData bufferData = BufferData.growing(contentLength + 256);
- nonEntityBytes(headers, status.get(), bufferData, keepAlive);
+ nonEntityBytes(headers, status.get(), bufferData, keepAlive, validateHeaders);
if (firstBuffer != null) {
bufferData.write(firstBuffer);
@@ -542,7 +553,7 @@ private void sendHeadersAndPrepare() {
// at this moment, we must send headers
sendListener.headers(ctx, headers);
BufferData bufferData = BufferData.growing(256);
- nonEntityBytes(headers, status.get(), bufferData, keepAlive);
+ nonEntityBytes(headers, status.get(), bufferData, keepAlive, validateHeaders);
sendListener.data(ctx, bufferData);
responseBytesTotal += bufferData.available();
dataWriter.write(bufferData);
diff --git a/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/http1/ConnectionConfigTest.java b/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/http1/ConnectionConfigTest.java
index 1880c92f155..084a311d6b6 100644
--- a/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/http1/ConnectionConfigTest.java
+++ b/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/http1/ConnectionConfigTest.java
@@ -48,13 +48,15 @@ void testConnectionConfig() {
assertThat(http1Config.maxPrologueLength(), is(4096));
assertThat(http1Config.maxHeadersSize(), is(8192));
assertThat(http1Config.validatePath(), is(true));
- assertThat(http1Config.validateHeaders(), is(true));
+ assertThat(http1Config.validateRequestHeaders(), is(true));
+ assertThat(http1Config.validateResponseHeaders(), is(false));
http1Config = http1Configs.get("other");
assertThat(http1Config.maxPrologueLength(), is(81));
assertThat(http1Config.maxHeadersSize(), is(42));
assertThat(http1Config.validatePath(), is(false));
- assertThat(http1Config.validateHeaders(), is(false));
+ assertThat(http1Config.validateRequestHeaders(), is(false));
+ assertThat(http1Config.validateResponseHeaders(), is(true));
}
}
diff --git a/nima/webserver/webserver/src/test/resources/application.yaml b/nima/webserver/webserver/src/test/resources/application.yaml
index 4e33aee74c6..7cf1c19f59c 100644
--- a/nima/webserver/webserver/src/test/resources/application.yaml
+++ b/nima/webserver/webserver/src/test/resources/application.yaml
@@ -31,7 +31,8 @@ server:
protocols:
providers:
http_1_1:
- validate-headers: false
+ validate-request-headers: false
+ validate-response-headers: true
validate-path: false
max-prologue-length: 81
max-headers-size: 42