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 0eb262e277..812be25754 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 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 @@ -105,7 +105,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\n" + "Host: localhost:" + PORT + "\r\n\n"; System.out.println(" " + a); os.write(a.getBytes()); os.flush(); @@ -167,10 +167,10 @@ 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\nHost: localhost:" + PORT + "\r\n\n"; + String staticRequest = "GET /static HTTP/1.1\nHost: localhost:" + PORT + "\r\n\n"; - String lastCometRequest = "GET " + alias + " HTTP/1.1\n" + "Host: localhost:" + PORT + "\nConnection: close\n\n"; + String lastCometRequest = "GET " + alias + " HTTP/1.1\n" + "Host: localhost:" + PORT + "\r\nConnection: close\r\n\n"; String pipelinedRequest1 = cometRequest + staticRequest + cometRequest; String pipelinedRequest2 = cometRequest + staticRequest + lastCometRequest; @@ -256,8 +256,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\nHost: localhost:" + PORT + "\r\n\n"; + String staticRequest = "GET /static HTTP/1.1\nHost: localhost:" + PORT + "\r\n\n"; try { os.write(cometRequest.getBytes()); 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 49bdd6be29..caaf9ed847 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, 2020 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 @@ -37,6 +37,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; @@ -108,6 +109,14 @@ public abstract class HttpCodecFilter extends HttpBaseFilter implements Monitori * @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 final boolean isStrictHeaderNameValidationSet = Boolean.parseBoolean(System.getProperty(STRICT_HEADER_NAME_VALIDATION_RFC_9110)); + + private final boolean isStrictHeaderValueValidationSet = Boolean.parseBoolean(System.getProperty(STRICT_HEADER_VALUE_VALIDATION_RFC_9110)); /** * File cache probes @@ -707,8 +716,13 @@ protected boolean parseHeaderFromBytes(final HttpHeader httpHeader, final MimeHe parsingState.subState++; } case 1: { // parse header name - if (!parseHeaderName(httpHeader, mimeHeaders, parsingState, input, end)) { + final int result = parseHeaderName(httpHeader, mimeHeaders, parsingState, input, end); + if (result == -1) { return false; + } else if (result == -2) { // EOL. ignore field-lines + parsingState.subState = 0; + parsingState.start = -1; + return true; } parsingState.subState++; @@ -754,7 +768,7 @@ protected boolean parseHeaderFromBytes(final HttpHeader httpHeader, final MimeHe } } - protected boolean parseHeaderName(final HttpHeader httpHeader, final MimeHeaders mimeHeaders, final HeaderParsingState parsingState, final byte[] input, + protected int parseHeaderName(final HttpHeader httpHeader, final MimeHeaders mimeHeaders, final HeaderParsingState parsingState, final byte[] input, final int end) { final int arrayOffs = parsingState.arrayOffset; @@ -770,22 +784,36 @@ protected boolean parseHeaderName(final HttpHeader httpHeader, final MimeHeaders parsingState.offset = offset + 1 - arrayOffs; finalizeKnownHeaderNames(httpHeader, parsingState, input, start, offset); - return true; + return 0; } else if (b >= Constants.A && b <= Constants.Z) { if (!preserveHeaderCase) { 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 + return -2; + } 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++; } parsingState.offset = offset - arrayOffs; - return false; + return -1; } - protected static int parseHeaderValue(final HttpHeader httpHeader, final HeaderParsingState parsingState, final byte[] input, final int end) { + protected int parseHeaderValue(final HttpHeader httpHeader, final HeaderParsingState parsingState, final byte[] input, final int end) { final int arrayOffs = parsingState.arrayOffset; final int limit = Math.min(end, arrayOffs + parsingState.packetLimit); @@ -797,24 +825,41 @@ protected static int parseHeaderValue(final HttpHeader httpHeader, final HeaderP while (offset < limit) { final byte b = input[offset]; if (b == Constants.CR) { - } else if (b == Constants.LF) { - // Check if it's not multi line header - if (offset + 1 < limit) { - final byte b2 = input[offset + 1]; - if (b2 == Constants.SP || b2 == Constants.HT) { - input[arrayOffs + parsingState.checkpoint++] = b2; - parsingState.offset = offset + 2 - arrayOffs; - return -2; - } else { - parsingState.offset = offset + 1 - arrayOffs; - finalizeKnownHeaderValues(httpHeader, parsingState, input, arrayOffs + parsingState.start, arrayOffs + parsingState.checkpoint2); - parsingState.headerValueStorage.setBytes(input, arrayOffs + parsingState.start, arrayOffs + parsingState.checkpoint2); + if (isStrictHeaderValueValidationSet) { + parsingState.offset = offset - arrayOffs; + final int eol = checkEOL(parsingState, input, end); + if (eol == 0) { // EOL + // the offset is already increased in the check + finalizeKnownHeaderValues(httpHeader, parsingState, input, arrayOffs + parsingState.start, + arrayOffs + parsingState.checkpoint2); + parsingState.headerValueStorage.setBytes(input, arrayOffs + parsingState.start, + arrayOffs + parsingState.checkpoint2); return 0; + } else if (eol == -2) { // not enough data + // by keeping the offset unchanged, we will recheck the EOL at the next opportunity. + return -1; } } - - parsingState.offset = offset - arrayOffs; - return -1; + } else if (b == Constants.LF) { + if (!isStrictHeaderValueValidationSet) { + // Check if it's not multi line header + if (offset + 1 < limit) { + final byte b2 = input[offset + 1]; + if (b2 == Constants.SP || b2 == Constants.HT) { + input[arrayOffs + parsingState.checkpoint++] = b2; + parsingState.offset = offset + 2 - arrayOffs; + return -2; + } else { + parsingState.offset = offset + 1 - arrayOffs; + finalizeKnownHeaderValues(httpHeader, parsingState, input, arrayOffs + parsingState.start, arrayOffs + parsingState.checkpoint2); + parsingState.headerValueStorage.setBytes(input, arrayOffs + parsingState.start, arrayOffs + parsingState.checkpoint2); + return 0; + } + } + // not enough data + parsingState.offset = offset - arrayOffs; + return -1; + } } else if (b == Constants.SP) { if (hasShift) { input[arrayOffs + parsingState.checkpoint++] = b; @@ -830,6 +875,10 @@ protected static int parseHeaderValue(final HttpHeader httpHeader, final HeaderP 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; @@ -963,8 +1012,13 @@ protected boolean parseHeaderFromBuffer(final HttpHeader httpHeader, final MimeH parsingState.subState++; } case 1: { // parse header name - if (!parseHeaderName(httpHeader, mimeHeaders, parsingState, input)) { + final int result = parseHeaderName(httpHeader, mimeHeaders, parsingState, input); + if (result == -1) { return false; + } else if (result == -2) { // EOL. ignore field-lines + parsingState.subState = 0; + parsingState.start = -1; + return true; } parsingState.subState++; @@ -1010,7 +1064,7 @@ protected boolean parseHeaderFromBuffer(final HttpHeader httpHeader, final MimeH } } - protected boolean parseHeaderName(final HttpHeader httpHeader, final MimeHeaders mimeHeaders, final HeaderParsingState parsingState, final Buffer input) { + protected int parseHeaderName(final HttpHeader httpHeader, final MimeHeaders mimeHeaders, final HeaderParsingState parsingState, final Buffer input) { final int limit = Math.min(input.limit(), parsingState.packetLimit); final int start = parsingState.start; int offset = parsingState.offset; @@ -1023,22 +1077,36 @@ protected boolean parseHeaderName(final HttpHeader httpHeader, final MimeHeaders parsingState.offset = offset + 1; finalizeKnownHeaderNames(httpHeader, parsingState, input, start, offset); - return true; + return 0; } else if (b >= Constants.A && b <= Constants.Z) { if (!preserveHeaderCase) { 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 + return -2; + } 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++; } parsingState.offset = offset; - return false; + return -1; } - protected static int parseHeaderValue(final HttpHeader httpHeader, final HeaderParsingState parsingState, final Buffer input) { + protected int parseHeaderValue(final HttpHeader httpHeader, final HeaderParsingState parsingState, final Buffer input) { final int limit = Math.min(input.limit(), parsingState.packetLimit); @@ -1049,24 +1117,40 @@ protected static int parseHeaderValue(final HttpHeader httpHeader, final HeaderP while (offset < limit) { final byte b = input.get(offset); if (b == Constants.CR) { - } else if (b == Constants.LF) { - // Check if it's not multi line header - if (offset + 1 < limit) { - final byte b2 = input.get(offset + 1); - if (b2 == Constants.SP || b2 == Constants.HT) { - input.put(parsingState.checkpoint++, b2); - parsingState.offset = offset + 2; - return -2; - } else { - parsingState.offset = offset + 1; - finalizeKnownHeaderValues(httpHeader, parsingState, input, parsingState.start, parsingState.checkpoint2); + if (isStrictHeaderValueValidationSet) { + parsingState.offset = offset; + final int eol = checkEOL(parsingState, input); + if (eol == 0) { // EOL + // the offset is already increased in the check + finalizeKnownHeaderValues(httpHeader, parsingState, input, parsingState.start, + parsingState.checkpoint2); parsingState.headerValueStorage.setBuffer(input, parsingState.start, parsingState.checkpoint2); return 0; + } else if (eol == -2) { // not enough data + // by keeping the offset unchanged, we will recheck the EOL at the next opportunity. + return -1; } } - - parsingState.offset = offset; - return -1; + } else if (b == Constants.LF) { + if (!isStrictHeaderValueValidationSet) { + // Check if it's not multi line header + if (offset + 1 < limit) { + final byte b2 = input.get(offset + 1); + if (b2 == Constants.SP || b2 == Constants.HT) { + input.put(parsingState.checkpoint++, b2); + parsingState.offset = offset + 2; + return -2; + } else { + parsingState.offset = offset + 1; + finalizeKnownHeaderValues(httpHeader, parsingState, input, parsingState.start, parsingState.checkpoint2); + parsingState.headerValueStorage.setBuffer(input, parsingState.start, parsingState.checkpoint2); + return 0; + } + } + // not enough data + parsingState.offset = offset; + return -1; + } } else if (b == Constants.SP) { if (hasShift) { input.put(parsingState.checkpoint++, b); @@ -1082,6 +1166,10 @@ protected static int parseHeaderValue(final HttpHeader httpHeader, final HeaderP 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 index eea8681c96..6e40e37d5b 100644 --- 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 @@ -236,6 +236,10 @@ private static ByteBuffer readToken(ByteBuffer byteBuffer) { } public static boolean isToken(int c) { + if (c < 0 || c >= ARRAY_SIZE) { + // out of bounds + return false; + } // Fast for correct values, slower for incorrect ones try { return IS_TOKEN[c]; @@ -244,6 +248,19 @@ public static boolean isToken(int c) { } } + public static boolean isText(int c) { + if (c < 0 || c >= 256) { + // out of bounds + return false; + } + // 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 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 844e44e1bd..2dc46537ba 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 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 @@ -307,13 +307,13 @@ private void doHttpRequestTest(int packetsNum, boolean hasContent, Map> entry : trailerHeaders.entrySet()) { final String value = entry.getValue().getFirst(); if (value != null) { - sb.append(entry.getKey()).append(": ").append(value).append(eol); + sb.append(entry.getKey()).append(": ").append(value).append("\r\n"); } } 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 79c10e622a..5315206955 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 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 @@ -19,13 +19,13 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; - +import java.util.HashMap; import org.glassfish.grizzly.Buffer; import org.glassfish.grizzly.Connection; import org.glassfish.grizzly.SocketConnectorHandler; @@ -48,25 +48,83 @@ import org.glassfish.grizzly.utils.ChunkingFilter; import org.glassfish.grizzly.utils.Pair; -import junit.framework.TestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static java.util.Arrays.asList; +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; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * Testing HTTP request parsing * * @author Alexey Stashok */ -public class HttpRequestParseTest extends TestCase { +@RunWith(Parameterized.class) +public class HttpRequestParseTest { public static final int PORT = 19000; + private final boolean isStrictHeaderNameValidationSet; + private final boolean isStrictHeaderValueValidationSet; + private final String isStrictHeaderNameValidationSetBefore; + private final String isStrictHeaderValueValidationSetBefore; + + @Parameterized.Parameters + public static Collection getMode() { + return asList(new Object[][] { { FALSE, FALSE }, { FALSE, TRUE }, { TRUE, FALSE }, { TRUE, TRUE } }); + } + + @Before + public void before() throws Exception { + if (isStrictHeaderNameValidationSet) { + System.setProperty(STRICT_HEADER_NAME_VALIDATION_RFC_9110, String.valueOf(Boolean.TRUE)); + } else { + System.setProperty(STRICT_HEADER_NAME_VALIDATION_RFC_9110, String.valueOf(Boolean.FALSE)); + } + if (isStrictHeaderValueValidationSet) { + System.setProperty(STRICT_HEADER_VALUE_VALIDATION_RFC_9110, String.valueOf(Boolean.TRUE)); + } else { + System.setProperty(STRICT_HEADER_VALUE_VALIDATION_RFC_9110, String.valueOf(Boolean.FALSE)); + } + } + + @After + public void after() throws Exception { + System.setProperty(STRICT_HEADER_NAME_VALIDATION_RFC_9110, + isStrictHeaderNameValidationSetBefore != null ? isStrictHeaderNameValidationSetBefore : + String.valueOf(Boolean.FALSE)); + System.setProperty(STRICT_HEADER_VALUE_VALIDATION_RFC_9110, + isStrictHeaderValueValidationSetBefore != null ? isStrictHeaderValueValidationSetBefore : + String.valueOf(Boolean.FALSE)); + } + + public HttpRequestParseTest(boolean isStrictHeaderNameValidationSet, boolean isStrictHeaderValueValidationSet) { + this.isStrictHeaderNameValidationSet = isStrictHeaderNameValidationSet; + this.isStrictHeaderValueValidationSet = isStrictHeaderValueValidationSet; + this.isStrictHeaderNameValidationSetBefore = System.getProperty(STRICT_HEADER_NAME_VALIDATION_RFC_9110); + this.isStrictHeaderValueValidationSetBefore = System.getProperty(STRICT_HEADER_VALUE_VALIDATION_RFC_9110); + } + + @Test public void testCustomMethod() throws Exception { doHttpRequestTest("TAKE", "/index.html", "HTTP/1.0", Collections.>emptyMap(), "\r\n"); } + @Test public void testHeaderlessRequestLine() throws Exception { doHttpRequestTest("GET", "/index.html", "HTTP/1.0", Collections.>emptyMap(), "\r\n"); } + @Test public void testSimpleHeaders() throws Exception { Map> headers = new HashMap<>(); headers.put("Host", new Pair<>("localhost", "localhost")); @@ -74,6 +132,7 @@ public void testSimpleHeaders() throws Exception { doHttpRequestTest("POST", "/index.html", "HTTP/1.1", headers, "\r\n"); } + @Test public void testSimpleHeadersPreserveCase() throws Exception { Map> headers = new HashMap<>(); headers.put("Host", new Pair<>("localhost", "localhost")); @@ -81,7 +140,94 @@ public void testSimpleHeadersPreserveCase() throws Exception { doHttpRequestTest("POST", "/index.html", "HTTP/1.1", headers, "\r\n", true); } + @Test + public void testDisallowedCharactersForHeaderNames() { + if (!isStrictHeaderNameValidationSet) { + return; + } + 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\r\nContent -Length: 1234\r\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\r\nContent-\rLength: 1234\r\n\n", 128); + fail("Bad HTTP headers exception had to be thrown"); + } catch (IllegalStateException e) { + // expected + } + } + + @Test + public void testDisallowedCharactersForHeaderContentValues() { + if (!isStrictHeaderValueValidationSet) { + return; + } + 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\r\nContent-Length: 1234\r\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\r\nContent-Length: 1234\r\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', '\n'}; + for (final char ch : invalidChars) { + try { + doTestDecoder("GET /index.html HTTP/1.1\nHost: localhost\r\nSome-Header: some-" + ch + "value\r\n\n", 128); + fail("Bad HTTP headers exception had to be thrown"); + } catch (IllegalStateException e) { + // expected + } + } + } + + @Test + public void testIgnoredHeaders() throws Exception { + if (!isStrictHeaderNameValidationSet) { + return; + } + 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"); + } + + @Test public void testMultiLineHeaders() throws Exception { + if (isStrictHeaderValueValidationSet) { + // Multiline headers should not be supported + return; + } Map> headers = new HashMap<>(); headers.put("Host", new Pair<>("localhost", "localhost")); headers.put("Multi-line", new Pair<>("first\r\n second\r\n third", "first second third")); @@ -89,7 +235,12 @@ public void testMultiLineHeaders() throws Exception { doHttpRequestTest("POST", "/index.html", "HTTP/1.1", headers, "\r\n"); } + @Test public void testHeadersN() throws Exception { + if (isStrictHeaderValueValidationSet) { + // Multiline headers should not be supported + return; + } 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")); @@ -97,26 +248,30 @@ public void testHeadersN() throws Exception { doHttpRequestTest("POST", "/index.html", "HTTP/1.1", headers, "\n"); } + @Test public void testCompleteURI() throws Exception { Map> headers = new HashMap<>(); headers.put("Host", new Pair(null, "localhost:8180")); 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); } + @Test public void testCompleteEmptyURI() throws Exception { Map> headers = new HashMap<>(); headers.put("Host", new Pair(null, "localhost:8180")); 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); } + @Test public void testDecoderOK() { doTestDecoder("GET /index.html HTTP/1.0\n\n", 4096); } + @Test public void testDecoderOverflowMethod() { try { doTestDecoder("GET /index.html HTTP/1.0\n\n", 2); @@ -126,6 +281,7 @@ public void testDecoderOverflowMethod() { } } + @Test public void testDecoderOverflowURI() { try { doTestDecoder("GET /index.html HTTP/1.0\n\n", 8); @@ -135,6 +291,7 @@ public void testDecoderOverflowURI() { } } + @Test public void testDecoderOverflowProtocol() { try { doTestDecoder("GET /index.html HTTP/1.0\n\n", 19); @@ -144,19 +301,22 @@ public void testDecoderOverflowProtocol() { } } + @Test public void testDecoderOverflowHeader1() { try { - doTestDecoder("GET /index.html HTTP/1.0\nHost: localhost\n\n", 41); + doTestDecoder("GET /index.html HTTP/1.0\nHost: localhost\r\n\n", 42); fail("Overflow exception had to be thrown"); } catch (IllegalStateException e) { // expected } } + @Test public void testDecoderOverflowHeader2() { - doTestDecoder("GET /index.html HTTP/1.0\nHost: localhost\n\n", 42); + doTestDecoder("GET /index.html HTTP/1.0\nHost: localhost\r\n\n", 43); } + @Test public void testDecoderOverflowHeader3() { try { doTestDecoder("GET /index.html HTTP/1.0\nHost: localhost\r\n\r\n", 43); @@ -166,12 +326,14 @@ public void testDecoderOverflowHeader3() { } } + @Test public void testDecoderOverflowHeader4() { doTestDecoder("GET /index.html HTTP/1.0\nHost: localhost\r\n\r\n", 44); } + @Test 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); + HttpPacket packet = doTestDecoder("POST /index.html HTTP/1.1\nHost: localhost\r\nTransfer-Encoding: CHUNked\r\n\r\n0\r\n\r\n", 4096); assertTrue(packet.getHttpHeader().isChunked()); } @@ -204,16 +366,29 @@ private void doHttpRequestTest(String method, String requestURI, String protocol new Pair<>(protocol, protocol), headers, eol, false); } + 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, boolean preserveCase) throws Exception { doHttpRequestTest(new Pair<>(method, method), new Pair<>(requestURI, requestURI), new Pair<>(protocol, protocol), headers, eol, preserveCase); } - @SuppressWarnings("unchecked") 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, + Map> expectedHeaders, String eol, + boolean preserveHeaderCase) throws Exception { final FutureImpl parseResult = SafeFutureImpl.create(); Connection connection = null; @@ -222,7 +397,7 @@ private void doHttpRequestTest(Pair method, Pair serverFilter.setPreserveHeaderCase(preserveHeaderCase); FilterChainBuilder filterChainBuilder = FilterChainBuilder.stateless().add(new TransportFilter()).add(new ChunkingFilter(2)).add(serverFilter) - .add(new HTTPRequestCheckFilter(parseResult, method, requestURI, protocol, headers, preserveHeaderCase)); + .add(new HTTPRequestCheckFilter(parseResult, 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 250d3836a7..e8af25b6c1 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 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 @@ -18,6 +18,7 @@ import java.io.IOException; import java.net.SocketAddress; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -48,22 +49,78 @@ import org.glassfish.grizzly.utils.ChunkingFilter; import org.glassfish.grizzly.utils.Pair; -import junit.framework.TestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static java.util.Arrays.asList; +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; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** * Testing HTTP response parsing * * @author Alexey Stashok */ -public class HttpResponseParseTest extends TestCase { +@RunWith(Parameterized.class) +public class HttpResponseParseTest { private static final Logger logger = Grizzly.logger(HttpResponseParseTest.class); public static final int PORT = 19021; + private final boolean isStrictHeaderNameValidationSet; + private final boolean isStrictHeaderValueValidationSet; + private final String isStrictHeaderNameValidationSetBefore; + private final String isStrictHeaderValueValidationSetBefore; + + @Parameterized.Parameters + public static Collection getMode() { + return asList(new Object[][] { { FALSE, FALSE }, { FALSE, TRUE }, { TRUE, FALSE }, { TRUE, TRUE } }); + } + + @Before + public void before() throws Exception { + if (isStrictHeaderNameValidationSet) { + System.setProperty(STRICT_HEADER_NAME_VALIDATION_RFC_9110, String.valueOf(Boolean.TRUE)); + } else { + System.setProperty(STRICT_HEADER_NAME_VALIDATION_RFC_9110, String.valueOf(Boolean.FALSE)); + } + if (isStrictHeaderValueValidationSet) { + System.setProperty(STRICT_HEADER_VALUE_VALIDATION_RFC_9110, String.valueOf(Boolean.TRUE)); + } else { + System.setProperty(STRICT_HEADER_VALUE_VALIDATION_RFC_9110, String.valueOf(Boolean.FALSE)); + } + } + + @After + public void after() throws Exception { + System.setProperty(STRICT_HEADER_NAME_VALIDATION_RFC_9110, + isStrictHeaderNameValidationSetBefore != null ? isStrictHeaderNameValidationSetBefore : + String.valueOf(Boolean.FALSE)); + System.setProperty(STRICT_HEADER_VALUE_VALIDATION_RFC_9110, + isStrictHeaderValueValidationSetBefore != null ? isStrictHeaderValueValidationSetBefore : + String.valueOf(Boolean.FALSE)); + } + + public HttpResponseParseTest(boolean isStrictHeaderNameValidationSet, boolean isStrictHeaderValueValidationSet) { + this.isStrictHeaderNameValidationSet = isStrictHeaderNameValidationSet; + this.isStrictHeaderValueValidationSet = isStrictHeaderValueValidationSet; + this.isStrictHeaderNameValidationSetBefore = System.getProperty(STRICT_HEADER_NAME_VALIDATION_RFC_9110); + this.isStrictHeaderValueValidationSetBefore = System.getProperty(STRICT_HEADER_VALUE_VALIDATION_RFC_9110); + } + + @Test public void testHeaderlessResponseLine() throws Exception { doHttpResponseTest("HTTP/1.0", 200, "OK", Collections.>emptyMap(), "\r\n"); } + @Test public void testSimpleHeaders() throws Exception { Map> headers = new HashMap<>(); headers.put("Header1", new Pair<>("localhost", "localhost")); @@ -71,7 +128,12 @@ public void testSimpleHeaders() throws Exception { doHttpResponseTest("HTTP/1.0", 200, "ALL RIGHT", headers, "\r\n"); } + @Test public void testMultiLineHeaders() throws Exception { + if (isStrictHeaderValueValidationSet) { + // Multiline headers should not be supported + return; + } Map> headers = new HashMap<>(); headers.put("Header1", new Pair<>("localhost", "localhost")); headers.put("Multi-line", new Pair<>("first\r\n second\r\n third", "first seconds third")); @@ -79,7 +141,12 @@ public void testMultiLineHeaders() throws Exception { doHttpResponseTest("HTTP/1.0", 200, "DONE", headers, "\r\n"); } + @Test public void testHeadersN() throws Exception { + if (isStrictHeaderValueValidationSet) { + // Multiline headers should not be supported + return; + } Map> headers = new HashMap<>(); headers.put("Header1", new Pair<>("localhost", "localhost")); headers.put("Multi-line", new Pair<>("first\n second\n third", "first seconds third")); @@ -87,6 +154,7 @@ public void testHeadersN() throws Exception { doHttpResponseTest("HTTP/1.0", 200, "DONE", headers, "\n"); } + @Test public void testDecoder100continueThen200() { try { doTestDecoder("HTTP/1.1 100 Continue\n\nHTTP/1.1 200 OK\n\n", 4096); @@ -97,6 +165,7 @@ public void testDecoder100continueThen200() { } } + @Test public void testDecoderOK() { try { doTestDecoder("HTTP/1.0 404 Not found\n\n", 4096); @@ -107,6 +176,7 @@ public void testDecoderOK() { } } + @Test public void testDecoderOverflowProtocol() { try { doTestDecoder("HTTP/1.0 404 Not found\n\n", 2); @@ -116,6 +186,7 @@ public void testDecoderOverflowProtocol() { } } + @Test public void testDecoderOverflowCode() { try { doTestDecoder("HTTP/1.0 404 Not found\n\n", 11); @@ -125,6 +196,7 @@ public void testDecoderOverflowCode() { } } + @Test public void testDecoderOverflowPhrase() { try { doTestDecoder("HTTP/1.0 404 Not found\n\n", 19); @@ -134,9 +206,10 @@ public void testDecoderOverflowPhrase() { } } + @Test public void testDecoderOverflowHeader() { try { - doTestDecoder("HTTP/1.0 404 Not found\nHeader1: somevalue\n\n", 30); + doTestDecoder("HTTP/1.0 404 Not found\nHeader1: somevalue\r\n\n", 31); assertTrue("Overflow exception had to be thrown", false); } catch (IllegalStateException e) { assertTrue(true); 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 8e3e4c934d..1f991932f5 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 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 @@ -677,7 +677,7 @@ public void testHttpHeadersLimit() throws Throwable { public void testExplicitConnectionCloseHeader() throws Throwable { final TCPNIOConnection connection = new TCPNIOConnection(TCPNIOTransportBuilder.newInstance().build(), null); - Buffer requestBuf = Buffers.wrap(connection.getMemoryManager(), "GET /path HTTP/1.1\n" + "Host: localhost:" + PORT + '\n' + '\n'); + Buffer requestBuf = Buffers.wrap(connection.getMemoryManager(), "GET /path HTTP/1.1\n" + "Host: localhost:" + PORT + "\r\n" + '\n'); FilterChainContext ctx = FilterChainContext.create(connection); ctx.setMessage(requestBuf);