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:

+ * + * + *

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);