diff --git a/modules/comet/src/test/java/org/glassfish/grizzly/comet/BasicCometTest.java b/modules/comet/src/test/java/org/glassfish/grizzly/comet/BasicCometTest.java
index dec29cefe9..80504e9360 100644
--- a/modules/comet/src/test/java/org/glassfish/grizzly/comet/BasicCometTest.java
+++ b/modules/comet/src/test/java/org/glassfish/grizzly/comet/BasicCometTest.java
@@ -100,7 +100,7 @@ public void testClientCloseConnection() throws Exception {
s.setSoLinger(false, 0);
s.setSoTimeout(500);
OutputStream os = s.getOutputStream();
- String a = "GET " + alias + " HTTP/1.1\n" + "Host: localhost:" + PORT + "\n\n";
+ String a = "GET " + alias + " HTTP/1.1\r\n" + "Host: localhost:" + PORT + "\r\n\r\n";
System.out.println(" " + a);
os.write(a.getBytes());
os.flush();
@@ -162,11 +162,11 @@ public void service(Request request, Response response) throws Exception {
Socket s = new Socket("localhost", PORT);
s.setSoTimeout(10 * 1000);
OutputStream os = s.getOutputStream();
- String cometRequest = "GET " + alias + " HTTP/1.1\nHost: localhost:" + PORT + "\n\n";
- String staticRequest = "GET /static HTTP/1.1\nHost: localhost:" + PORT + "\n\n";
+ String cometRequest = "GET " + alias + " HTTP/1.1\r\nHost: localhost:" + PORT + "\r\n\r\n";
+ String staticRequest = "GET /static HTTP/1.1\r\nHost: localhost:" + PORT + "\r\n\r\n";
- String lastCometRequest = "GET " + alias + " HTTP/1.1\n"+"Host: localhost:" + PORT + "\nConnection: close\n\n";
+ String lastCometRequest = "GET " + alias + " HTTP/1.1\r\n"+"Host: localhost:" + PORT + "\r\nConnection: close\r\n\r\n";
String pipelinedRequest1 = cometRequest + staticRequest + cometRequest;
@@ -254,8 +254,8 @@ public void service(Request request, Response response) throws Exception {
Socket s = new Socket("localhost", PORT);
s.setSoTimeout(10 * 1000);
OutputStream os = s.getOutputStream();
- String cometRequest = "GET " + alias + " HTTP/1.1\nHost: localhost:" + PORT + "\n\n";
- String staticRequest = "GET /static HTTP/1.1\nHost: localhost:" + PORT + "\n\n";
+ String cometRequest = "GET " + alias + " HTTP/1.1\r\nHost: localhost:" + PORT + "\r\n\r\n";
+ String staticRequest = "GET /static HTTP/1.1\r\nHost: localhost:" + PORT + "\r\n\r\n";
try {
diff --git a/modules/http/src/main/java/org/glassfish/grizzly/http/HttpCodecFilter.java b/modules/http/src/main/java/org/glassfish/grizzly/http/HttpCodecFilter.java
index e4cf05122e..deefc9b2f8 100644
--- a/modules/http/src/main/java/org/glassfish/grizzly/http/HttpCodecFilter.java
+++ b/modules/http/src/main/java/org/glassfish/grizzly/http/HttpCodecFilter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2017 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2024 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -31,6 +31,7 @@
import org.glassfish.grizzly.http.util.ByteChunk;
import org.glassfish.grizzly.http.util.CacheableDataChunk;
import org.glassfish.grizzly.http.util.Constants;
+import org.glassfish.grizzly.http.util.CookieHeaderParser;
import org.glassfish.grizzly.http.util.DataChunk;
import org.glassfish.grizzly.http.util.Header;
import org.glassfish.grizzly.http.util.MimeHeaders;
@@ -129,6 +130,14 @@ public abstract class HttpCodecFilter extends HttpBaseFilter
* @see #setRemoveHandledContentEncodingHeaders
*/
private boolean removeHandledContentEncodingHeaders = false;
+
+ public static final String STRICT_HEADER_NAME_VALIDATION_RFC_9110 = "org.glassfish.grizzly.http.STRICT_HEADER_NAME_VALIDATION_RFC_9110";
+
+ public static final String STRICT_HEADER_VALUE_VALIDATION_RFC_9110 = "org.glassfish.grizzly.http.STRICT_HEADER_VALUE_VALIDATION_RFC_9110";
+
+ private static final boolean isStrictHeaderNameValidationSet = Boolean.parseBoolean((System.getProperty(STRICT_HEADER_NAME_VALIDATION_RFC_9110) == null) ? "true" : System.getProperty(STRICT_HEADER_NAME_VALIDATION_RFC_9110));
+
+ private static final boolean isStrictHeaderValueValidationSet = Boolean.parseBoolean((System.getProperty(STRICT_HEADER_VALUE_VALIDATION_RFC_9110) == null) ? "true": System.getProperty(STRICT_HEADER_VALUE_VALIDATION_RFC_9110));
/**
* File cache probes
@@ -809,6 +818,10 @@ protected boolean parseHeaderFromBytes(final HttpHeader httpHeader,
return false;
}
+ if (parsingState.subState == 0 && parsingState.start == -1) { // EOL. ignore field-lines
+ return true;
+ }
+
parsingState.subState++;
parsingState.start = -1;
}
@@ -880,6 +893,23 @@ protected boolean parseHeaderName(final HttpHeader httpHeader,
b -= Constants.LC_OFFSET;
}
input[offset] = b;
+ } else if (isStrictHeaderNameValidationSet && b == Constants.CR) {
+ parsingState.offset = offset - arrayOffs;
+ final int eol = checkEOL(parsingState, input, end);
+ if (eol == 0) { // EOL
+ // the offset is already increased in the check
+ parsingState.subState = 0;
+ parsingState.start = -1;
+ return true;
+ } else if (eol == -2) { // not enough data
+ // by keeping the offset unchanged, we will recheck the EOL at the next opportunity.
+ break;
+ }
+ }
+
+ if (isStrictHeaderNameValidationSet && !CookieHeaderParser.isToken(b)) {
+ throw new IllegalStateException(
+ "An invalid character 0x" + Integer.toHexString(b) + " was found in the header name");
}
offset++;
@@ -903,6 +933,20 @@ protected static int parseHeaderValue(final HttpHeader httpHeader,
while (offset < limit) {
final byte b = input[offset];
if (b == Constants.CR) {
+ if (isStrictHeaderValueValidationSet) {
+ if (offset + 1 < limit) {
+ final byte b2 = input[offset + 1];
+ if (b2 == Constants.LF) {
+ // Continue for next parsing without the validation
+ offset++;
+ continue;
+ }
+ } else {
+ // not enough data
+ parsingState.offset = offset - arrayOffs;
+ return -1;
+ }
+ }
} else if (b == Constants.LF) {
// Check if it's not multi line header
if (offset + 1 < limit) {
@@ -912,6 +956,11 @@ protected static int parseHeaderValue(final HttpHeader httpHeader,
parsingState.offset = offset + 2 - arrayOffs;
return -2;
} else {
+ final byte b3 = input[offset - 1];
+ if (!(b3 == Constants.CR) && isStrictHeaderValueValidationSet) {
+ throw new IllegalStateException(
+ "An invalid character 0x" + Integer.toHexString(b) + " was found in the header value");
+ }
parsingState.offset = offset + 1 - arrayOffs;
finalizeKnownHeaderValues(httpHeader, parsingState, input,
arrayOffs + parsingState.start,
@@ -940,6 +989,10 @@ protected static int parseHeaderValue(final HttpHeader httpHeader,
parsingState.checkpoint2 = parsingState.checkpoint;
}
+ if (isStrictHeaderValueValidationSet && !CookieHeaderParser.isText(b)) {
+ throw new IllegalStateException(
+ "An invalid character 0x" + Integer.toHexString(b) + " was found in the header value");
+ }
offset++;
}
parsingState.offset = offset - arrayOffs;
@@ -1093,6 +1146,10 @@ protected boolean parseHeaderFromBuffer(final HttpHeader httpHeader,
return false;
}
+ if (parsingState.subState == 0 && parsingState.start == -1) { // EOL. ignore field-lines
+ return true;
+ }
+
parsingState.subState++;
parsingState.start = -1;
}
@@ -1159,6 +1216,23 @@ protected boolean parseHeaderName(final HttpHeader httpHeader,
b -= Constants.LC_OFFSET;
}
input.put(offset, b);
+ } else if (isStrictHeaderNameValidationSet && b == Constants.CR) {
+ parsingState.offset = offset;
+ final int eol = checkEOL(parsingState, input);
+ if (eol == 0) { // EOL
+ // the offset is already increased in the check
+ parsingState.subState = 0;
+ parsingState.start = -1;
+ return true;
+ } else if (eol == -2) { // not enough data
+ // by keeping the offset unchanged, we will recheck the EOL at the next opportunity.
+ break;
+ }
+ }
+
+ if (isStrictHeaderNameValidationSet && !CookieHeaderParser.isToken(b)) {
+ throw new IllegalStateException(
+ "An invalid character 0x" + Integer.toHexString(b) + " was found in the header name");
}
offset++;
@@ -1180,6 +1254,20 @@ protected static int parseHeaderValue(final HttpHeader httpHeader,
while(offset < limit) {
final byte b = input.get(offset);
if (b == Constants.CR) {
+ if (isStrictHeaderValueValidationSet) {
+ if (offset + 1 < limit) {
+ final byte b2 = input.get(offset + 1);
+ if (b2 == Constants.LF) {
+ // Continue for next parsing without the validation
+ offset++;
+ continue;
+ }
+ } else {
+ // not enough data
+ parsingState.offset = offset;
+ return -1;
+ }
+ }
} else if (b == Constants.LF) {
// Check if it's not multi line header
if (offset + 1 < limit) {
@@ -1189,6 +1277,11 @@ protected static int parseHeaderValue(final HttpHeader httpHeader,
parsingState.offset = offset + 2;
return -2;
} else {
+ final byte b3 = input.get(offset - 1);
+ if (!(b3 == Constants.CR) && isStrictHeaderValueValidationSet) {
+ throw new IllegalStateException(
+ "An invalid character 0x" + Integer.toHexString(b) + " was found in the header value");
+ }
parsingState.offset = offset + 1;
finalizeKnownHeaderValues(httpHeader, parsingState, input,
parsingState.start, parsingState.checkpoint2);
@@ -1215,6 +1308,10 @@ protected static int parseHeaderValue(final HttpHeader httpHeader,
parsingState.checkpoint2 = parsingState.checkpoint;
}
+ if (isStrictHeaderValueValidationSet && !CookieHeaderParser.isText(b)) {
+ throw new IllegalStateException(
+ "An invalid character 0x" + Integer.toHexString(b) + " was found in the header value");
+ }
offset++;
}
parsingState.offset = offset;
diff --git a/modules/http/src/main/java/org/glassfish/grizzly/http/util/CookieHeaderParser.java b/modules/http/src/main/java/org/glassfish/grizzly/http/util/CookieHeaderParser.java
new file mode 100644
index 0000000000..634d517d84
--- /dev/null
+++ b/modules/http/src/main/java/org/glassfish/grizzly/http/util/CookieHeaderParser.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation
+ * Copyright 2004, 2022 The Apache Software Foundation
+ *
+ * Licensed 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 org.glassfish.grizzly.http.util;
+
+import org.glassfish.grizzly.http.Cookies;
+import org.glassfish.grizzly.http.LazyCookieState;
+
+/**
+ *
Cookie header parser based on RFC6265
+ * The parsing of cookies using RFC6265 is more relaxed that the
+ * specification in the following ways:
+ *
+ * - Values 0x80 to 0xFF are permitted in cookie-octet to support the use of
+ * UTF-8 in cookie values as used by HTML 5.
+ * - For cookies without a value, the '=' is not required after the name as
+ * some browsers do not sent it.
+ *
+ *
+ * Implementation note:
+ * This class has been carefully tuned.
+ *
+ * @author The Tomcat team
+ * @author Arjan Tijms
+ */
+public class CookieHeaderParser {
+
+ private static final boolean isCookieOctet[] = new boolean[256];
+ private static final boolean isText[] = new boolean[256];
+ private static final byte[] EMPTY_BYTES = new byte[0];
+ private static final byte TAB_BYTE = (byte) 0x09;
+ private static final byte SPACE_BYTE = (byte) 0x20;
+ private static final byte QUOTE_BYTE = (byte) 0x22;
+ private static final byte COMMA_BYTE = (byte) 0x2C;
+ private static final byte SEMICOLON_BYTE = (byte) 0x3B;
+ private static final byte EQUALS_BYTE = (byte) 0x3D;
+ private static final byte SLASH_BYTE = (byte) 0x5C;
+ private static final byte DEL_BYTE = (byte) 0x7F;
+
+ private static final int ARRAY_SIZE = 128;
+ private static final boolean[] IS_CONTROL = new boolean[ARRAY_SIZE];
+ private static final boolean[] IS_SEPARATOR = new boolean[ARRAY_SIZE];
+ private static final boolean[] IS_TOKEN = new boolean[ARRAY_SIZE];
+
+ static {
+ // %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E (RFC6265)
+ // %x80 to %xFF (UTF-8)
+ for (int i = 0; i < 256; i++) {
+ if (i < 0x21 || i == QUOTE_BYTE || i == COMMA_BYTE || i == SEMICOLON_BYTE || i == SLASH_BYTE || i == DEL_BYTE) {
+ isCookieOctet[i] = false;
+ } else {
+ isCookieOctet[i] = true;
+ }
+ }
+
+ for (int i = 0; i < 256; i++) {
+ if (i < TAB_BYTE || (i > TAB_BYTE && i < SPACE_BYTE) || i == DEL_BYTE) {
+ isText[i] = false;
+ } else {
+ isText[i] = true;
+ }
+ }
+
+ for (int i = 0; i < ARRAY_SIZE; i++) {
+ // Control> 0-31, 127
+ if (i < 32 || i == 127) {
+ IS_CONTROL[i] = true;
+ }
+
+ // Separator
+ if (i == '(' || i == ')' || i == '<' || i == '>' || i == '@' ||
+ i == ',' || i == ';' || i == ':' || i == '\\' || i == '\"' ||
+ i == '/' || i == '[' || i == ']' || i == '?' || i == '=' ||
+ i == '{' || i == '}' || i == ' ' || i == '\t') {
+ IS_SEPARATOR[i] = true;
+ }
+
+ // Token: Anything 0-127 that is not a control and not a separator
+ if (!IS_CONTROL[i] && !IS_SEPARATOR[i] && i < 128) {
+ IS_TOKEN[i] = true;
+ }
+ }
+ }
+
+
+ private CookieHeaderParser() {
+ // Hide default constructor
+ }
+
+
+ public static void parseCookie(byte[] bytes, int offset, int len, Cookies serverCookies) {
+
+ // ByteBuffer is used throughout this parser as it allows the byte[]
+ // and position information to be easily passed between parsing methods
+ ByteBuffer byteBuffer = new ByteBuffer(bytes, offset, len);
+
+ boolean moreToProcess = true;
+
+ while (moreToProcess) {
+ skipWhiteSpace(byteBuffer);
+
+ ByteBuffer name = readToken(byteBuffer);
+ ByteBuffer value = null;
+
+ skipWhiteSpace(byteBuffer);
+
+ SkipResult skipResult = skipByte(byteBuffer, EQUALS_BYTE);
+ if (skipResult == SkipResult.FOUND) {
+ skipWhiteSpace(byteBuffer);
+ value = readCookieValueRfc6265(byteBuffer);
+ if (value == null) {
+ // Invalid cookie value. Skip to the next semi-colon
+ skipUntilSemiColon(byteBuffer);
+ continue;
+ }
+ skipWhiteSpace(byteBuffer);
+ }
+
+ skipResult = skipByte(byteBuffer, SEMICOLON_BYTE);
+ if (skipResult == SkipResult.FOUND) {
+ // NO-OP
+ } else if (skipResult == SkipResult.NOT_FOUND) {
+ // Invalid cookie. Ignore it and skip to the next semi-colon
+ skipUntilSemiColon(byteBuffer);
+ continue;
+ } else {
+ // SkipResult.EOF
+ moreToProcess = false;
+ }
+
+ if (name.hasRemaining()) {
+ LazyCookieState lazyCookie = serverCookies.getNextUnusedCookie().getLazyCookieState();
+ lazyCookie.getName().setBytes(name.array(), name.position(), name.position() + name.remaining());
+ if (value == null) {
+ lazyCookie.getValue().setBytes(EMPTY_BYTES, 0, EMPTY_BYTES.length);
+ } else {
+ lazyCookie.getValue().setBytes(value.array(), value.position(), value.position() + value.remaining());
+ }
+ }
+ }
+ }
+
+
+ private static void skipWhiteSpace(ByteBuffer byteBuffer) {
+ while (byteBuffer.hasRemaining()) {
+ byte b = byteBuffer.get();
+ if (b != TAB_BYTE && b != SPACE_BYTE) {
+ byteBuffer.rewind();
+ break;
+ }
+ }
+ }
+
+
+ private static void skipUntilSemiColon(ByteBuffer byteBuffer) {
+ while (byteBuffer.hasRemaining()) {
+ if (byteBuffer.get() == SEMICOLON_BYTE) {
+ break;
+ }
+ }
+ }
+
+
+ private static SkipResult skipByte(ByteBuffer byteBuffer, byte target) {
+ if (!byteBuffer.hasRemaining()) {
+ return SkipResult.EOF;
+ }
+ if (byteBuffer.get() == target) {
+ return SkipResult.FOUND;
+ }
+
+ byteBuffer.rewind();
+ return SkipResult.NOT_FOUND;
+ }
+
+
+ /**
+ * Similar to readCookieValue() but treats a comma as part of an invalid
+ * value.
+ */
+ private static ByteBuffer readCookieValueRfc6265(ByteBuffer byteBuffer) {
+ boolean quoted = false;
+ if (byteBuffer.hasRemaining()) {
+ if (byteBuffer.get() == QUOTE_BYTE) {
+ quoted = true;
+ } else {
+ byteBuffer.rewind();
+ }
+ }
+
+ int start = byteBuffer.position();
+ int end = byteBuffer.limit();
+ while (byteBuffer.hasRemaining()) {
+ byte b = byteBuffer.get();
+ if (isCookieOctet[(b & 0xFF)]) {
+ // NO-OP
+ } else if (b == SEMICOLON_BYTE || b == SPACE_BYTE || b == TAB_BYTE) {
+ end = byteBuffer.position() - 1;
+ byteBuffer.position(end);
+ break;
+ } else if (quoted && b == QUOTE_BYTE) {
+ end = byteBuffer.position() - 1;
+ break;
+ } else {
+ // Invalid cookie
+ return null;
+ }
+ }
+
+ return new ByteBuffer(byteBuffer.bytes, start, end - start);
+ }
+
+
+ private static ByteBuffer readToken(ByteBuffer byteBuffer) {
+ final int start = byteBuffer.position();
+ int end = byteBuffer.limit();
+ while (byteBuffer.hasRemaining()) {
+ if (!isToken(byteBuffer.get())) {
+ end = byteBuffer.position() - 1;
+ byteBuffer.position(end);
+ break;
+ }
+ }
+
+ return new ByteBuffer(byteBuffer.bytes, start, end - start);
+ }
+
+ public static boolean isToken(int c) {
+ // Fast for correct values, slower for incorrect ones
+ try {
+ return IS_TOKEN[c];
+ } catch (ArrayIndexOutOfBoundsException ex) {
+ return false;
+ }
+ }
+
+ public static boolean isText(int c) {
+ // Fast for correct values, slower for incorrect ones
+ try {
+ return isText[c];
+ } catch (ArrayIndexOutOfBoundsException ex) {
+ return false;
+ }
+ }
+
+
+ /**
+ * Custom implementation that skips many of the safety checks in
+ * {@link java.nio.ByteBuffer}.
+ */
+ private static class ByteBuffer {
+
+ private final byte[] bytes;
+ private int limit;
+ private int position = 0;
+
+ public ByteBuffer(byte[] bytes, int offset, int len) {
+ this.bytes = bytes;
+ this.position = offset;
+ this.limit = offset + len;
+ }
+
+ public int position() {
+ return position;
+ }
+
+ public void position(int position) {
+ this.position = position;
+ }
+
+ public int limit() {
+ return limit;
+ }
+
+ public int remaining() {
+ return limit - position;
+ }
+
+ public boolean hasRemaining() {
+ return position < limit;
+ }
+
+ public byte get() {
+ return bytes[position++];
+ }
+
+ public void rewind() {
+ position--;
+ }
+
+ public byte[] array() {
+ return bytes;
+ }
+
+ // For debug purposes
+ @Override
+ public String toString() {
+ return "position [" + position + "], limit [" + limit + "]";
+ }
+ }
+
+ private static enum SkipResult {
+ FOUND,
+ NOT_FOUND,
+ EOF
+ }
+}
diff --git a/modules/http/src/test/java/org/glassfish/grizzly/http/ChunkedTransferEncodingTest.java b/modules/http/src/test/java/org/glassfish/grizzly/http/ChunkedTransferEncodingTest.java
index bc0c4ccad5..ba02d6de82 100644
--- a/modules/http/src/test/java/org/glassfish/grizzly/http/ChunkedTransferEncodingTest.java
+++ b/modules/http/src/test/java/org/glassfish/grizzly/http/ChunkedTransferEncodingTest.java
@@ -144,7 +144,7 @@ public void after() throws Exception {
}
public ChunkedTransferEncodingTest(String eol, boolean isChunkWhenParsing) {
- this.eol = eol;
+ this.eol = "\r\n";
this.isChunkWhenParsing = isChunkWhenParsing;
}
diff --git a/modules/http/src/test/java/org/glassfish/grizzly/http/HttpRequestParseTest.java b/modules/http/src/test/java/org/glassfish/grizzly/http/HttpRequestParseTest.java
index 1faa8db3c2..def98be4da 100644
--- a/modules/http/src/test/java/org/glassfish/grizzly/http/HttpRequestParseTest.java
+++ b/modules/http/src/test/java/org/glassfish/grizzly/http/HttpRequestParseTest.java
@@ -48,6 +48,9 @@
import org.glassfish.grizzly.utils.ChunkingFilter;
import org.glassfish.grizzly.utils.Pair;
+import static org.glassfish.grizzly.http.HttpCodecFilter.STRICT_HEADER_NAME_VALIDATION_RFC_9110;
+import static org.glassfish.grizzly.http.HttpCodecFilter.STRICT_HEADER_VALUE_VALIDATION_RFC_9110;
+
/**
* Testing HTTP request parsing
*
@@ -57,6 +60,20 @@ public class HttpRequestParseTest extends TestCase {
public static final int PORT = 19000;
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ System.setProperty(STRICT_HEADER_NAME_VALIDATION_RFC_9110, String.valueOf(Boolean.TRUE));
+ System.setProperty(STRICT_HEADER_VALUE_VALIDATION_RFC_9110, String.valueOf(Boolean.TRUE));
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ System.setProperty(STRICT_HEADER_NAME_VALIDATION_RFC_9110, String.valueOf(Boolean.FALSE));
+ System.setProperty(STRICT_HEADER_VALUE_VALIDATION_RFC_9110, String.valueOf(Boolean.FALSE));
+ }
+
public void testCustomMethod() throws Exception {
doHttpRequestTest("TAKE", "/index.html", "HTTP/1.0", Collections.>emptyMap(), "\r\n");
}
@@ -81,6 +98,82 @@ public void testSimpleHeadersPreserveCase() throws Exception {
doHttpRequestTest("POST", "/index.html", "HTTP/1.1", headers, "\r\n", true);
}
+ public void testDisallowedHeaders() {
+ final StringBuilder sb = new StringBuilder("GET / HTTP/1.1\r\n");
+ sb.append("Host: localhost\r\n");
+ sb.append(new char[]{0x00, 0x01, 0x02, '\t', '\n', '\r', ' ', '\"', '(', ')', '/', ';', '<', '=', '>', '?', '@',
+ '[', 0x5c, ']', '{', '}'}).append(": some-value\r\n");
+ sb.append("\r\n");
+ try {
+ doTestDecoder(sb.toString(), 128);
+ fail("Bad HTTP headers exception had to be thrown");
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ try {
+ doTestDecoder("GET /index.html HTTP/1.1\nHost: localhost\nContent -Length: 1234\n\n", 128);
+ fail("Bad HTTP headers exception had to be thrown");
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ try {
+ doTestDecoder("GET /index.html HTTP/1.1\nHost: localhost\nContent-\rLength: 1234\n\n", 128);
+ fail("Bad HTTP headers exception had to be thrown");
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ }
+
+ public void testDisallowedCharactersForHeaderContentValues() {
+ final StringBuilder sb = new StringBuilder("GET / HTTP/1.1\r\n");
+ sb.append("Host: localhost\r\n");
+ sb.append("Some-Header: some-");
+ // valid header values
+ sb.append(new char[]{'\t', ' ', '\"', '(', ')', '/', ';', '<', '=', '>', '?', '@', '[', 0x5c, ']', '{', '}'})
+ .append("\r\n");
+ sb.append("\r\n");
+ doTestDecoder(sb.toString(), 128);
+
+ try {
+ doTestDecoder("GET /index.html HTTP/1.1\nHost: loca\\rlhost\nContent -Length: 1234\n\n", 128);
+ fail("Bad HTTP headers exception had to be thrown");
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ try {
+ doTestDecoder("GET /index.html HTTP/1.1\nHost: loca\\nlhost\nContent-Length: 1234\n\n", 128);
+ fail("Bad HTTP headers exception had to be thrown");
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ try {
+ doTestDecoder("GET /index.html HTTP/1.1\nHost: loca\\0lhost\nContent-Length: 1234\n\n", 128);
+ fail("Bad HTTP headers exception had to be thrown");
+ } catch (IllegalStateException e) {
+ // expected
+ }
+
+ final char[] invalidChars = new char[]{0x00, 0x01, 0x02, '\r'};
+ for (final char ch : invalidChars) {
+ try {
+ doTestDecoder("GET /index.html HTTP/1.1\nHost: localhost\nSome-Header: some-" + ch + "value\n\n", 128);
+ fail("Bad HTTP headers exception had to be thrown");
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ }
+ }
+
+ public void testIgnoredHeaders() throws Exception {
+ final Map> headers = new HashMap<>();
+ headers.put("Host", new Pair<>("localhost", "localhost"));
+ headers.put("Ignore\r\nContent-length", new Pair<>("2345", "2345"));
+ final Map> expectedHeaders = new HashMap<>();
+ expectedHeaders.put("Host", new Pair<>("localhost", "localhost"));
+ expectedHeaders.put("Content-length", new Pair<>("2345", "2345"));
+ doHttpRequestTest("POST", "/index.html", "HTTP/1.1", headers, expectedHeaders, "\r\n");
+ }
+
public void testMultiLineHeaders() throws Exception {
Map> headers =
new HashMap>();
@@ -94,9 +187,9 @@ public void testHeadersN() throws Exception {
Map> headers =
new HashMap>();
headers.put("Host", new Pair("localhost", "localhost"));
- headers.put("Multi-line", new Pair("first\r\n second\n third", "first second third"));
+ headers.put("Multi-line", new Pair("first\r\n second\r\n third", "first second third"));
headers.put("Content-length", new Pair("2345", "2345"));
- doHttpRequestTest("POST", "/index.html", "HTTP/1.1", headers, "\n");
+ doHttpRequestTest("POST", "/index.html", "HTTP/1.1", headers, "\r\n");
}
public void testCompleteURI() throws Exception {
@@ -106,7 +199,7 @@ public void testCompleteURI() throws Exception {
headers.put("Content-length", new Pair("2345", "2345"));
doHttpRequestTest(new Pair("POST", "POST"),
new Pair("http://localhost:8180/index.html", "/index.html"),
- new Pair("HTTP/1.1", "HTTP/1.1"), headers, "\n", false);
+ new Pair("HTTP/1.1", "HTTP/1.1"), headers, "\r\n", false);
}
public void testCompleteEmptyURI() throws Exception {
@@ -116,7 +209,7 @@ public void testCompleteEmptyURI() throws Exception {
headers.put("Content-length", new Pair("2345", "2345"));
doHttpRequestTest(new Pair("POST", "POST"),
new Pair("http://localhost:8180", "/"),
- new Pair("HTTP/1.1", "HTTP/1.1"), headers, "\n", false);
+ new Pair("HTTP/1.1", "HTTP/1.1"), headers, "\r\n", false);
}
public void testDecoderOK() {
@@ -160,7 +253,7 @@ public void testDecoderOverflowHeader1() {
}
public void testDecoderOverflowHeader2() {
- doTestDecoder("GET /index.html HTTP/1.0\nHost: localhost\n\n", 42);
+ doTestDecoder("GET /index.html HTTP/1.0\r\nHost: localhost\r\n\r\n", 50);
}
public void testDecoderOverflowHeader3() {
@@ -178,7 +271,7 @@ public void testDecoderOverflowHeader4() {
public void testChunkedTransferEncodingCaseInsensitive() {
HttpPacket packet = doTestDecoder(
- "POST /index.html HTTP/1.1\nHost: localhost\nTransfer-Encoding: CHUNked\r\n\r\n0\r\n\r\n", 4096);
+ "POST /index.html HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: CHUNked\r\n\r\n0\r\n\r\n", 4096);
assertTrue(packet.getHttpHeader().isChunked());
}
@@ -208,6 +301,13 @@ protected void onHttpHeaderError(final HttpHeader httpHeader,
}
}
+ private void doHttpRequestTest(String method, String requestURI, String protocol,
+ Map> headers,
+ Map> expectedHeaders, String eol) throws Exception {
+ doHttpRequestTest(new Pair<>(method, method), new Pair<>(requestURI, requestURI),
+ new Pair<>(protocol, protocol), headers, expectedHeaders, eol, false);
+ }
+
private void doHttpRequestTest(String method, String requestURI,
String protocol, Map> headers, String eol)
throws Exception {
@@ -225,11 +325,15 @@ private void doHttpRequestTest(String method, String requestURI,
headers, eol, preserveCase);
}
+ private void doHttpRequestTest(Pair method, Pair requestURI, Pair protocol,
+ Map> headers, String eol, boolean preserveHeaderCase) throws Exception {
+ doHttpRequestTest(method, requestURI, protocol, headers, headers, eol, preserveHeaderCase);
+ }
@SuppressWarnings("unchecked")
private void doHttpRequestTest(Pair method,
Pair requestURI, Pair protocol,
- Map> headers, String eol,
+ Map> headers, Map> expectedHeaders, String eol,
boolean preserveHeaderCase)
throws Exception {
@@ -245,7 +349,7 @@ private void doHttpRequestTest(Pair method,
.add(new ChunkingFilter(2))
.add(serverFilter)
.add(new HTTPRequestCheckFilter(parseResult,
- method, requestURI, protocol, headers, preserveHeaderCase));
+ method, requestURI, protocol, expectedHeaders == null ? headers : expectedHeaders, preserveHeaderCase));
TCPNIOTransport transport = TCPNIOTransportBuilder.newInstance().build();
transport.setProcessor(filterChainBuilder.build());
diff --git a/modules/http/src/test/java/org/glassfish/grizzly/http/HttpResponseParseTest.java b/modules/http/src/test/java/org/glassfish/grizzly/http/HttpResponseParseTest.java
index bdf5d6d2b8..6beb9ba94c 100644
--- a/modules/http/src/test/java/org/glassfish/grizzly/http/HttpResponseParseTest.java
+++ b/modules/http/src/test/java/org/glassfish/grizzly/http/HttpResponseParseTest.java
@@ -89,7 +89,7 @@ public void testHeadersN() throws Exception {
headers.put("Header1", new Pair<>("localhost", "localhost"));
headers.put("Multi-line", new Pair<>("first\n second\n third", "first seconds third"));
headers.put("Content-length", new Pair<>("2345", "2345"));
- doHttpResponseTest("HTTP/1.0", 200, "DONE", headers, "\n");
+ doHttpResponseTest("HTTP/1.0", 200, "DONE", headers, "\r\n");
}
public void testDecoder100continueThen200() {
diff --git a/modules/http/src/test/java/org/glassfish/grizzly/http/HttpSemanticsTest.java b/modules/http/src/test/java/org/glassfish/grizzly/http/HttpSemanticsTest.java
index 97d6ac3754..1b8642d444 100644
--- a/modules/http/src/test/java/org/glassfish/grizzly/http/HttpSemanticsTest.java
+++ b/modules/http/src/test/java/org/glassfish/grizzly/http/HttpSemanticsTest.java
@@ -817,9 +817,9 @@ public void testExplicitConnectionCloseHeader() throws Throwable {
TCPNIOTransportBuilder.newInstance().build(), null);
Buffer requestBuf = Buffers.wrap(connection.getMemoryManager(),
- "GET /path HTTP/1.1\n"
- + "Host: localhost:" + PORT + '\n'
- + '\n');
+ "GET /path HTTP/1.1\r\n"
+ + "Host: localhost:" + PORT + "\r\n"
+ + "\r\n");
FilterChainContext ctx = FilterChainContext.create(connection);
ctx.setMessage(requestBuf);