From 4cef69ca8a86e774be6c148b5c3a79006bfa0195 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 29 Nov 2024 09:16:10 +1100 Subject: [PATCH] Enhance HTTP Compliance CRLF modes (#12564) Added modes to allow strict requirement for CRLF termination of headers and/or chunks --- .../docs/programming/server/ServerDocs.java | 6 +- .../pages/modules/standard.adoc | 2 +- .../pages/server/compliance.adoc | 5 +- .../org/eclipse/jetty/client/HttpClient.java | 4 +- .../eclipse/jetty/http/HttpCompliance.java | 53 +- .../org/eclipse/jetty/http/HttpParser.java | 150 +- .../org/eclipse/jetty/http/HttpTokens.java | 3 + .../eclipse/jetty/http/HttpParserTest.java | 2295 ++++++++++------- .../src/main/config/etc/jetty.xml | 2 +- .../src/main/config/modules/server.mod | 4 +- 10 files changed, 1588 insertions(+), 936 deletions(-) diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java index b147d5eff245..19f21052d30c 100644 --- a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java @@ -271,7 +271,7 @@ public void httpCompliance() { // tag::httpCompliance[] HttpConfiguration httpConfiguration = new HttpConfiguration(); - httpConfiguration.setHttpCompliance(HttpCompliance.RFC7230); + httpConfiguration.setHttpCompliance(HttpCompliance.RFC9110); // end::httpCompliance[] } @@ -280,8 +280,8 @@ public void httpComplianceCustom() // tag::httpComplianceCustom[] HttpConfiguration httpConfiguration = new HttpConfiguration(); - // RFC7230 compliance, but allow Violation.MULTIPLE_CONTENT_LENGTHS. - HttpCompliance customHttpCompliance = HttpCompliance.from("RFC7230,MULTIPLE_CONTENT_LENGTHS"); + // RFC9110 compliance, but allow Violation.MULTIPLE_CONTENT_LENGTHS. + HttpCompliance customHttpCompliance = HttpCompliance.from("RFC9110,MULTIPLE_CONTENT_LENGTHS"); httpConfiguration.setHttpCompliance(customHttpCompliance); // end::httpComplianceCustom[] diff --git a/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc b/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc index d7bf67251665..45b14005a65b 100644 --- a/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc +++ b/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc @@ -558,7 +558,7 @@ Among the configurable properties, the most relevant are: Configures the compliance to HTTP specifications. The value could be: -* One of the predefined link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html[`HttpCompliance`] constants, such as `RFC7230` or `RFC2616`. +* One of the predefined link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html[`HttpCompliance`] constants, such as `RFC9110`, RFC7230` or `RFC2616`. For example: `jetty.httpConfig.compliance=RFC2616`. * A comma-separated list of violations to allow or forbid, as specified by the link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html#from(java.lang.String)[`HttpCompliance.from(String)`] method. For example, `jetty.httpConfig.compliance=RFC7230,MULTIPLE_CONTENT_LENGTHS` means that the HTTP compliance is that defined by `RFC7230`, but also allows the `HttpCompliance.Violation.MULTIPLE_CONTENT_LENGTHS`, so that requests that have multiple `Content-Length` headers are accepted (they would be rejected when using just `HttpCompliance.RFC7230`). diff --git a/documentation/jetty/modules/programming-guide/pages/server/compliance.adoc b/documentation/jetty/modules/programming-guide/pages/server/compliance.adoc index 6707b836b86b..db219b6f8f9c 100644 --- a/documentation/jetty/modules/programming-guide/pages/server/compliance.adoc +++ b/documentation/jetty/modules/programming-guide/pages/server/compliance.adoc @@ -25,7 +25,7 @@ There are compliance modes provided for: Compliance modes can be configured to allow violations from the RFC requirements, or in some cases to allow additional behaviors that Jetty has implemented in excess of the RFC (for example, to allow <>). -For example, the HTTP RFCs require that request HTTP methods are https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1[case sensitive], however Jetty can allow case-insensitive HTTP methods by including the link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.Violation.html#CASE_INSENSITIVE_METHOD[`HttpCompliance.Violation.CASE_INSENSITIVE_METHOD`] in the link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html[`HttpCompliance`] set of allowed violations. +For example, the HTTP RFCs require that request HTTP methods are https://datatracker.ietf.org/doc/html/rfc9110#section-9.1[case sensitive], however Jetty can allow case-insensitive HTTP methods by including the link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.Violation.html#CASE_INSENSITIVE_METHOD[`HttpCompliance.Violation.CASE_INSENSITIVE_METHOD`] in the link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html[`HttpCompliance`] set of allowed violations. [[http]] == HTTP Compliance Modes @@ -37,11 +37,12 @@ In 1995, when Jetty was first implemented, there were no RFC specification of HT * https://datatracker.ietf.org/doc/html/rfc2616[RFC 2616] for HTTP/1.1 bis in 1999 * https://datatracker.ietf.org/doc/html/rfc7230[RFC 7230], https://datatracker.ietf.org/doc/html/rfc7231[RFC 7231], https://datatracker.ietf.org/doc/html/rfc7232[RFC 7232], https://datatracker.ietf.org/doc/html/rfc7233[RFC 7233], https://datatracker.ietf.org/doc/html/rfc7234[RFC 7234], https://datatracker.ietf.org/doc/html/rfc7235[RFC 7235] again for HTTP/1.1 in 2014 * https://datatracker.ietf.org/doc/html/rfc7540[RFC 7540] for HTTP/2.0 in 2015 +* https://datatracker.ietf.org/doc/html/rfc9110[RFC 9110] for common HTTP semantics and https://datatracker.ietf.org/doc/html/rfc9112[RFC 9112] for HTTP/1.1 In addition to these evolving requirements, some earlier version of Jetty did not completely or strictly implement the RFC at the time (for example, case-insensitive HTTP methods). Therefore, upgrading to a newer Jetty version may cause runtime behavior differences that may break your applications. -The link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.Violation.html[`HttpCompliance.Violation`] enumeration defines the RFC requirements that may be optionally enforced by Jetty, to support legacy deployments. These possible violations are grouped into modes by the link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html[`HttpCompliance`] class, which also defines several named modes that support common deployed sets of violations (with the default being link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html#RFC7230[`HttpCompliance.RFC7230`]). +The link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.Violation.html[`HttpCompliance.Violation`] enumeration defines the RFC requirements that may be optionally enforced by Jetty, to support legacy deployments. These possible violations are grouped into modes by the link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html[`HttpCompliance`] class, which also defines several named modes that support common deployed sets of violations (with the default being link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html#RFC9110[`HttpCompliance.RFC9110`]). For example: diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index 387c8db7feca..0c7701676e88 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -131,7 +131,7 @@ public class HttpClient extends ContainerLifeCycle implements AutoCloseable private boolean strictEventOrdering = false; private long destinationIdleTimeout; private String name = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); - private HttpCompliance httpCompliance = HttpCompliance.RFC7230; + private HttpCompliance httpCompliance = HttpCompliance.RFC9110; private String defaultRequestContentType = "application/octet-stream"; private boolean useInputDirectByteBuffers = true; private boolean useOutputDirectByteBuffers = true; @@ -917,7 +917,7 @@ public void setMaxRedirects(int maxRedirects) /** * Gets the http compliance mode for parsing http responses. - * The default http compliance level is {@link HttpCompliance#RFC7230} which is the latest HTTP/1.1 specification + * The default http compliance level is {@link HttpCompliance#RFC9110} which is the latest HTTP specification * * @return the HttpCompliance instance */ diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java index 41cebbf55d0f..10e007f9726c 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java @@ -54,7 +54,7 @@ public enum Violation implements ComplianceViolation * match and handle fields names insensitively and this violation only affects how the names are reported to the application. * There is a small performance and garbage impact of using this mode. */ - CASE_SENSITIVE_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2", "Field name is case-insensitive"), + CASE_SENSITIVE_FIELD_NAME("https://datatracker.ietf.org/doc/html/rfc9110#name-field-names", "Field name is case-insensitive"), /** * The HTTP RFC(s) require that method names are case-sensitive, so that "{@code Get}" and "{@code GET}" are considered @@ -62,20 +62,20 @@ public enum Violation implements ComplianceViolation * was violated. Deployments which wish to retain this legacy violation can include this violation in the * {@link HttpCompliance} mode. */ - CASE_INSENSITIVE_METHOD("https://tools.ietf.org/html/rfc7230#section-3.1.1", "Method is case-sensitive"), + CASE_INSENSITIVE_METHOD("https://datatracker.ietf.org/doc/html/rfc9110#name-methods", "Method is case-sensitive"), /** * Since RFC 7230, the expectation that HTTP/0.9 is supported has been removed from the specification. If a deployment * wished to accept HTTP/0.9 requests, then it can include this violation in it's {@link HttpCompliance} mode. */ - HTTP_0_9("https://tools.ietf.org/html/rfc7230#appendix-A.2", "HTTP/0.9 not supported"), + HTTP_0_9("https://datatracker.ietf.org/doc/html/rfc9112#appendix-C.1", "HTTP/0.9 not supported"), /** * Since RFC 7230, the HTTP protocol no longer supports * line folding, which allows a field value to be provided over several lines. Deployments that wish to receive folder * field values may include this violation in their {@link HttpCompliance} mode. */ - MULTILINE_FIELD_VALUE("https://tools.ietf.org/html/rfc7230#section-3.2.4", "Line Folding not supported"), + MULTILINE_FIELD_VALUE("https://datatracker.ietf.org/doc/html/rfc9112#name-obsolete-line-folding", "Line Folding not supported"), /** * Since RFC 7230, the HTTP protocol has required that @@ -90,42 +90,52 @@ public enum Violation implements ComplianceViolation * a request is invalid if it contains both a {@code Transfer-Encoding} field and {@code Content-Length} field. * A deployment may include this violation to allow both fields to be in a received request. */ - TRANSFER_ENCODING_WITH_CONTENT_LENGTH("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Transfer-Encoding and Content-Length"), + TRANSFER_ENCODING_WITH_CONTENT_LENGTH("https://datatracker.ietf.org/doc/html/rfc9112#name-content-length", "Transfer-Encoding and Content-Length"), /** * Since RFC 7230, the HTTP protocol has required that * a request header field has no white space after the field name and before the ':'. * A deployment may include this violation to allow such fields to be in a received request. */ - WHITESPACE_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2.4", "Whitespace not allowed after field name"), + WHITESPACE_AFTER_FIELD_NAME("https://datatracker.ietf.org/doc/html/rfc9112#name-field-syntax", "Whitespace not allowed after field name"), /** * Prior to RFC 7230, the HTTP protocol allowed a header * line of a single token with neither a colon nor value following, to be interpreted as a field name with no value. * A deployment may include this violation to allow such fields to be in a received request. */ - NO_COLON_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2", "Fields must have a Colon"), + NO_COLON_AFTER_FIELD_NAME("https://datatracker.ietf.org/doc/html/rfc9112#name-field-syntax", "Fields must have a Colon"), /** * Since RFC 7230: Section 5.4, the HTTP protocol * says that a Server must reject a request duplicate host headers. * A deployment may include this violation to allow duplicate host headers on a received request. */ - DUPLICATE_HOST_HEADERS("https://www.rfc-editor.org/rfc/rfc7230#section-5.4", "Duplicate Host Header"), + DUPLICATE_HOST_HEADERS("https://datatracker.ietf.org/doc/html/rfc9112#name-request-target", "Duplicate Host Header"), /** * Since RFC 7230, the HTTP protocol * should reject a request if the Host headers contains an invalid / unsafe authority. - * A deployment may include this violation to allow unsafe host headesr on a received request. + * A deployment may include this violation to allow unsafe host headers on a received request. */ - UNSAFE_HOST_HEADER("https://www.rfc-editor.org/rfc/rfc7230#section-2.7.1", "Invalid Authority"), + UNSAFE_HOST_HEADER("https://datatracker.ietf.org/doc/html/rfc9112#name-request-target", "Invalid Authority"), /** * Since RFC 7230: Section 5.4, the HTTP protocol * must reject a request if the target URI has an authority that is different than a provided Host header. * A deployment may include this violation to allow different values on the target URI and the Host header on a received request. */ - MISMATCHED_AUTHORITY("https://www.rfc-editor.org/rfc/rfc7230#section-5.4", "Mismatched Authority"); + MISMATCHED_AUTHORITY("https://datatracker.ietf.org/doc/html/rfc9112#name-request-target", "Mismatched Authority"), + + /** + * Allow LF termination of start line and header fields. + */ + LF_HEADER_TERMINATION("https://www.rfc-editor.org/rfc/rfc9112.html#section-2.2", "LF line terminator in header"), + + /** + * Allow LF termination of chunk headers and chunks + */ + LF_CHUNK_TERMINATION("https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1", "LF line terminator in chunk"); private final String url; private final String description; @@ -167,10 +177,19 @@ public String getDescription() public static final String VIOLATIONS_ATTR = ComplianceViolation.CapturingListener.VIOLATIONS_ATTR_KEY; /** - * The HttpCompliance mode that supports RFC 7230 - * with no known violations. + * The HttpCompliance mode that supports no known violations. + */ + public static final HttpCompliance STRICT = new HttpCompliance("STRICT", noneOf(Violation.class)); + + /** + * The HttpCompliance mode that supports RFC 9110. + */ + public static final HttpCompliance RFC9110 = new HttpCompliance("RFC9110", of(Violation.LF_HEADER_TERMINATION)); + + /** + * The HttpCompliance mode that supports RFC 7230. */ - public static final HttpCompliance RFC7230 = new HttpCompliance("RFC7230", noneOf(Violation.class)); + public static final HttpCompliance RFC7230 = new HttpCompliance("RFC7230", of(Violation.LF_CHUNK_TERMINATION, Violation.LF_HEADER_TERMINATION)); /** * The HttpCompliance mode that supports RFC 7230 @@ -179,7 +198,9 @@ public String getDescription() public static final HttpCompliance RFC2616 = new HttpCompliance("RFC2616", of( Violation.HTTP_0_9, Violation.MULTILINE_FIELD_VALUE, - Violation.MISMATCHED_AUTHORITY + Violation.MISMATCHED_AUTHORITY, + Violation.LF_CHUNK_TERMINATION, + Violation.LF_HEADER_TERMINATION )); /** @@ -202,7 +223,7 @@ public String getDescription() */ public static final HttpCompliance RFC7230_LEGACY = RFC7230.with("RFC7230_LEGACY", Violation.CASE_INSENSITIVE_METHOD); - private static final List KNOWN_MODES = Arrays.asList(RFC7230, RFC2616, LEGACY, RFC2616_LEGACY, RFC7230_LEGACY); + private static final List KNOWN_MODES = Arrays.asList(STRICT, RFC9110, RFC7230, RFC2616, LEGACY, RFC2616_LEGACY, RFC7230_LEGACY); private static final AtomicInteger __custom = new AtomicInteger(); /** diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java index d21ec9e22bcd..a8bc84fdfd87 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java @@ -20,6 +20,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import org.eclipse.jetty.http.HttpTokens.EndOfContent; import org.eclipse.jetty.util.BufferUtil; @@ -31,21 +32,25 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static org.eclipse.jetty.http.HttpCompliance.RFC7230; +import static org.eclipse.jetty.http.HttpCompliance.RFC9110; import static org.eclipse.jetty.http.HttpCompliance.Violation; import static org.eclipse.jetty.http.HttpCompliance.Violation.CASE_SENSITIVE_FIELD_NAME; import static org.eclipse.jetty.http.HttpCompliance.Violation.DUPLICATE_HOST_HEADERS; import static org.eclipse.jetty.http.HttpCompliance.Violation.HTTP_0_9; +import static org.eclipse.jetty.http.HttpCompliance.Violation.LF_CHUNK_TERMINATION; +import static org.eclipse.jetty.http.HttpCompliance.Violation.LF_HEADER_TERMINATION; import static org.eclipse.jetty.http.HttpCompliance.Violation.MULTIPLE_CONTENT_LENGTHS; import static org.eclipse.jetty.http.HttpCompliance.Violation.NO_COLON_AFTER_FIELD_NAME; import static org.eclipse.jetty.http.HttpCompliance.Violation.TRANSFER_ENCODING_WITH_CONTENT_LENGTH; import static org.eclipse.jetty.http.HttpCompliance.Violation.UNSAFE_HOST_HEADER; import static org.eclipse.jetty.http.HttpCompliance.Violation.WHITESPACE_AFTER_FIELD_NAME; import static org.eclipse.jetty.http.HttpTokens.CARRIAGE_RETURN; +import static org.eclipse.jetty.http.HttpTokens.EOL_CRLF; +import static org.eclipse.jetty.http.HttpTokens.EOL_LF; import static org.eclipse.jetty.http.HttpTokens.LINE_FEED; /** - * A Parser for 1.0 and 1.1 as defined by RFC7230 + * A Parser for 1.0 and 1.1 as defined by RFC 9112 *

* This parser parses HTTP client and server messages from buffers * passed in the {@link #parseNext(ByteBuffer)} method. The parsed @@ -80,14 +85,16 @@ *

* The parser can work in varying compliance modes: *

- *
RFC7230
(default) Compliance with RFC7230
- *
RFC2616
Wrapped headers and HTTP/0.9 supported
- *
LEGACY
Adherence to Servlet Specification requirement for + *
{@link HttpCompliance#RFC9110}
(default) Compliance with RFC9110 and RFC9112
+ *
{@link HttpCompliance#RFC7230}
(default) Compliance with RFC7230
+ *
{@link HttpCompliance#RFC2616}
Wrapped headers and HTTP/0.9 supported
+ *
{@link HttpCompliance#LEGACY}
Adherence to Servlet Specification requirement for * exact case of header names, bypassing the header caches, which are case insensitive, * otherwise equivalent to RFC2616
*
* - * @see RFC 7230 + * @see RFC 9110 + * @see RFC 9112 */ public class HttpParser { @@ -220,6 +227,7 @@ public enum State CHUNK_SIZE, CHUNK_PARAMS, CHUNK, + CHUNK_END, CONTENT_END, TRAILER, END, @@ -270,7 +278,7 @@ public enum State private static HttpCompliance compliance() { - return RFC7230; + return RFC9110; } public HttpParser(RequestHandler handler) @@ -303,6 +311,11 @@ public HttpParser(RequestHandler handler, int maxHeaderBytes, HttpCompliance com this(handler, null, maxHeaderBytes, compliance == null ? compliance() : compliance); } + public HttpParser(ResponseHandler handler, HttpCompliance compliance) + { + this(handler, -1, compliance); + } + public HttpParser(ResponseHandler handler, int maxHeaderBytes, HttpCompliance compliance) { this(null, handler, maxHeaderBytes, compliance == null ? compliance() : compliance); @@ -479,8 +492,12 @@ private HttpTokens.Token next(ByteBuffer buffer) throw new IllegalCharacterException(_state, t, buffer); case LF: - _cr = false; - break; + if (_cr) + { + _cr = false; + return EOL_CRLF; + } + return EOL_LF; case CR: if (_cr) @@ -496,7 +513,7 @@ private HttpTokens.Token next(ByteBuffer buffer) return switch (t.getType()) { case CNTL -> throw new IllegalCharacterException(_state, t, buffer); - case LF -> t; + case LF -> EOL_CRLF; default -> throw new BadMessageException("Bad EOL"); }; } @@ -647,6 +664,8 @@ private void quickStart(ByteBuffer buffer) HttpTokens.Token t = next(buffer); if (t == null) break; + if (t == EOL_LF) + checkViolation(LF_HEADER_TERMINATION); switch (t.getType()) { @@ -723,6 +742,8 @@ private boolean parseLine(ByteBuffer buffer) HttpTokens.Token t = next(buffer); if (t == null) break; + if (t == EOL_LF) + checkViolation(LF_HEADER_TERMINATION); if (_maxHeaderBytes > 0 && ++_headerBytes > _maxHeaderBytes) { @@ -773,7 +794,7 @@ else if (Violation.CASE_INSENSITIVE_METHOD.isAllowedBy(_complianceMode)) setState(State.SPACE1); break; - case LF: + case EOL: throw new BadMessageException("No URI"); case ALPHA: @@ -805,8 +826,7 @@ else if (Violation.CASE_INSENSITIVE_METHOD.isAllowedBy(_complianceMode)) case COLON: _string.append(t.getChar()); break; - case CR: - case LF: + case EOL: throw new BadMessageException("No Status"); default: throw new IllegalCharacterException(_state, t, buffer); @@ -887,7 +907,7 @@ else if (Violation.CASE_INSENSITIVE_METHOD.isAllowedBy(_complianceMode)) throw new BadMessageException("Bad status"); break; - case LF: + case EOL: _fieldCache.prepare(); setState(State.HEADER); _responseHandler.startResponse(_version, _responseStatus, null); @@ -930,7 +950,7 @@ else if (Violation.CASE_INSENSITIVE_METHOD.isAllowedBy(_complianceMode)) setState(State.SPACE2); break; - case LF: + case EOL: // HTTP/0.9 if (Violation.HTTP_0_9.isAllowedBy(_complianceMode)) { @@ -978,7 +998,7 @@ else if (Violation.CASE_INSENSITIVE_METHOD.isAllowedBy(_complianceMode)) setState(_requestParser ? State.REQUEST_VERSION : State.REASON); break; - case LF: + case EOL: if (!_requestParser) { _fieldCache.prepare(); @@ -1005,7 +1025,7 @@ else if (Violation.CASE_INSENSITIVE_METHOD.isAllowedBy(_complianceMode)) case REQUEST_VERSION: switch (t.getType()) { - case LF: + case EOL: if (_version == null) { _length = _string.length(); @@ -1014,7 +1034,6 @@ else if (Violation.CASE_INSENSITIVE_METHOD.isAllowedBy(_complianceMode)) checkVersion(); _fieldCache.prepare(); setState(State.HEADER); - _requestHandler.startRequest(_methodString, _uri.toCompleteString(), _version); continue; @@ -1023,6 +1042,37 @@ else if (Violation.CASE_INSENSITIVE_METHOD.isAllowedBy(_complianceMode)) case TCHAR: case VCHAR: case COLON: + if (_string.isEmpty()) + { + HttpVersion version = HttpVersion.CACHE.getBest(buffer); + if (version != null) + { + buffer.position(buffer.position() + version.asString().length()); + + HttpTokens.Token eol = next(buffer); + if (eol == null) + { + _string.append(version.asString()); + } + else if (eol.getType() == HttpTokens.Type.EOL) + { + if (eol == EOL_LF) + checkViolation(LF_HEADER_TERMINATION); + _version = version; + checkVersion(); + _fieldCache.prepare(); + setState(State.HEADER); + _requestHandler.startRequest(_methodString, _uri.toCompleteString(), _version); + continue; + } + else + { + _string.append(version.asString()); + buffer.position(buffer.position() - 1); + } + continue; + } + } _string.append(t.getChar()); break; @@ -1035,7 +1085,7 @@ else if (Violation.CASE_INSENSITIVE_METHOD.isAllowedBy(_complianceMode)) assert !_requestParser; switch (t.getType()) { - case LF: + case EOL: String reason = takeString(); _fieldCache.prepare(); setState(State.HEADER); @@ -1259,6 +1309,8 @@ protected boolean parseFields(ByteBuffer buffer) HttpTokens.Token t = next(buffer); if (t == null) break; + if (t == EOL_LF) + checkViolation(LF_HEADER_TERMINATION); if (_maxHeaderBytes > 0 && ++_headerBytes > _maxHeaderBytes) { @@ -1299,7 +1351,7 @@ protected boolean parseFields(ByteBuffer buffer) break; } - case LF: + case EOL: { // process previous header if (_state == State.HEADER) @@ -1439,6 +1491,7 @@ else if (_endOfContent == EndOfContent.UNKNOWN_CONTENT) buffer.position(posAfterValue + 1); if (peek == LINE_FEED) { + checkViolation(LF_HEADER_TERMINATION); setState(FieldState.FIELD); break; } @@ -1490,7 +1543,7 @@ else if (_endOfContent == EndOfContent.UNKNOWN_CONTENT) setState(FieldState.VALUE); break; - case LF: + case EOL: _headerString = takeString(); _header = HttpHeader.CACHE.get(_headerString); _string.setLength(0); @@ -1528,7 +1581,7 @@ else if (_endOfContent == EndOfContent.UNKNOWN_CONTENT) setState(FieldState.VALUE); break; - case LF: + case EOL: if (NO_COLON_AFTER_FIELD_NAME.isAllowedBy(_complianceMode)) { reportComplianceViolation(NO_COLON_AFTER_FIELD_NAME, "Field " + _headerString); @@ -1545,7 +1598,7 @@ else if (_endOfContent == EndOfContent.UNKNOWN_CONTENT) case VALUE: switch (t.getType()) { - case LF: + case EOL: _string.setLength(0); _valueString = ""; _length = -1; @@ -1576,7 +1629,7 @@ else if (_endOfContent == EndOfContent.UNKNOWN_CONTENT) case IN_VALUE: switch (t.getType()) { - case LF: + case EOL: if (_length > 0) { _valueString = takeString(); @@ -1866,9 +1919,6 @@ protected boolean parseContent(ByteBuffer buffer) break; switch (t.getType()) { - case LF: - break; - case DIGIT: _chunkLength = t.getHexDigit(); _chunkPosition = 0; @@ -1899,7 +1949,10 @@ protected boolean parseContent(ByteBuffer buffer) switch (t.getType()) { - case LF: + case EOL: + if (t == EOL_LF) + checkViolation(LF_CHUNK_TERMINATION); + if (_chunkLength == 0) { setState(State.TRAILER); @@ -1935,20 +1988,19 @@ protected boolean parseContent(ByteBuffer buffer) if (t == null) break; - switch (t.getType()) + if (Objects.requireNonNull(t.getType()) == HttpTokens.Type.EOL) { - case LF: - if (_chunkLength == 0) - { - setState(State.TRAILER); - if (_handler.contentComplete()) - return true; - } - else - setState(State.CHUNK); - break; - default: - break; // TODO review + if (t == EOL_LF) + checkViolation(LF_CHUNK_TERMINATION); + + if (_chunkLength == 0) + { + setState(State.TRAILER); + if (_handler.contentComplete()) + return true; + } + else + setState(State.CHUNK); } break; } @@ -1958,7 +2010,7 @@ protected boolean parseContent(ByteBuffer buffer) int chunk = _chunkLength - _chunkPosition; if (chunk == 0) { - setState(State.CHUNKED_CONTENT); + setState(State.CHUNK_END); } else { @@ -1977,6 +2029,20 @@ protected boolean parseContent(ByteBuffer buffer) break; } + case CHUNK_END: + { + HttpTokens.Token t = next(buffer); + if (t == null) + break; + if (Objects.requireNonNull(t.getType()) == HttpTokens.Type.EOL) + { + if (t == EOL_LF) + checkViolation(LF_CHUNK_TERMINATION); + setState(State.CHUNKED_CONTENT); + } + break; + } + case CONTENT_END: { setState(_endOfContent == EndOfContent.EOF_CONTENT ? State.CLOSED : State.END); diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java index 725722fd2597..046acf767609 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java @@ -38,6 +38,7 @@ public enum Type HTAB, // Horizontal tab LF, // Line feed CR, // Carriage return + EOL, // A CRLF or LF (depending on configuration) SPACE, // Space COLON, // Colon character DIGIT, // Digit @@ -161,6 +162,8 @@ public String toString() } } + public static final Token EOL_LF = new Token(LINE_FEED, Type.EOL); + public static final Token EOL_CRLF = new Token(LINE_FEED, Type.EOL); public static final Token[] TOKENS = new Token[256]; static diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java index dc797dbb21d9..5bfd396d63a6 100644 --- a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java +++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java @@ -36,6 +36,9 @@ import static org.eclipse.jetty.http.HttpCompliance.Violation.CASE_INSENSITIVE_METHOD; import static org.eclipse.jetty.http.HttpCompliance.Violation.CASE_SENSITIVE_FIELD_NAME; +import static org.eclipse.jetty.http.HttpCompliance.Violation.HTTP_0_9; +import static org.eclipse.jetty.http.HttpCompliance.Violation.LF_CHUNK_TERMINATION; +import static org.eclipse.jetty.http.HttpCompliance.Violation.LF_HEADER_TERMINATION; import static org.eclipse.jetty.http.HttpCompliance.Violation.MULTILINE_FIELD_VALUE; import static org.eclipse.jetty.http.HttpCompliance.Violation.TRANSFER_ENCODING_WITH_CONTENT_LENGTH; import static org.hamcrest.MatcherAssert.assertThat; @@ -43,6 +46,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; @@ -113,29 +117,40 @@ public void httpMethodNameTest(String methodName) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testLineParseMockIP(String eoln) + @MethodSource("scenarios") + public void testLineParseMockIP(Scenario scenario) { - ByteBuffer buffer = BufferUtil.toBuffer("POST /mock/127.0.0.1 HTTP/1.1" + eoln + eoln); + ByteBuffer buffer = BufferUtil.toBuffer("POST /mock/127.0.0.1 HTTP/1.0" + scenario.eol + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + assertNull(_bad); assertEquals("POST", _methodOrVersion); assertEquals("/mock/127.0.0.1", _uriOrStatus); - assertEquals("HTTP/1.1", _versionOrReason); + assertEquals("HTTP/1.0", _versionOrReason); assertEquals(-1, _headers); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testLineParse0(String eoln) + @MethodSource("scenarios") + public void testLineParse0(Scenario scenario) { - ByteBuffer buffer = BufferUtil.toBuffer("POST /foo HTTP/1.0" + eoln + eoln); + ByteBuffer buffer = BufferUtil.toBuffer("POST /foo HTTP/1.0" + scenario.eol + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("POST", _methodOrVersion); assertEquals("/foo", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -143,41 +158,50 @@ public void testLineParse0(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testLineParse1RFC2616(String eoln) + @MethodSource("scenarios") + public void testLineParse1Http9(Scenario scenario) { - ByteBuffer buffer = BufferUtil.toBuffer("GET /999" + eoln); + ByteBuffer buffer = BufferUtil.toBuffer("GET /999" + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY); + HttpParser parser = new HttpParser(handler, scenario.compliance.with("test", HTTP_0_9)); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertNull(_bad); assertEquals("GET", _methodOrVersion); assertEquals("/999", _uriOrStatus); assertEquals("HTTP/0.9", _versionOrReason); assertEquals(-1, _headers); - assertThat(_complianceViolation, contains(HttpCompliance.Violation.HTTP_0_9)); + assertTrue(_complianceViolation.contains(HTTP_0_9)); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testLineParse1(String eoln) + @MethodSource("scenarios") + public void testLineParse1(Scenario scenario) { - ByteBuffer buffer = BufferUtil.toBuffer("GET /999" + eoln); + ByteBuffer buffer = BufferUtil.toBuffer("GET /999" + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance.without("no 0.9", HTTP_0_9)); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("HTTP/0.9 not supported", _bad); - assertThat(_complianceViolation, Matchers.empty()); + assertThat(_complianceViolation, scenario.isViolation() ? not(empty()) : empty()); } - @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testLineParse2RFC2616(String eoln) + @Test + public void testLineParse2RFC2616() { - ByteBuffer buffer = BufferUtil.toBuffer("POST /222 " + eoln); + ByteBuffer buffer = BufferUtil.toBuffer("POST /222 \r\n"); HttpParser.RequestHandler handler = new Handler(); HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY); @@ -192,28 +216,38 @@ public void testLineParse2RFC2616(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testLineParse2(String eoln) + @MethodSource("scenarios") + public void testLineParse2(Scenario scenario) { - ByteBuffer buffer = BufferUtil.toBuffer("POST /222 " + eoln); + ByteBuffer buffer = BufferUtil.toBuffer("POST /222 " + scenario.eol); _versionOrReason = null; HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance.without("no 0.9", HTTP_0_9)); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("HTTP/0.9 not supported", _bad); - assertThat(_complianceViolation, Matchers.empty()); + assertThat(_complianceViolation, scenario.isViolation() ? not(empty()) : empty()); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testLineParse3(String eoln) + @MethodSource("scenarios") + public void testLineParse3(Scenario scenario) { - ByteBuffer buffer = BufferUtil.toBuffer("POST /fo\u0690 HTTP/1.0" + eoln + eoln, StandardCharsets.UTF_8); + ByteBuffer buffer = BufferUtil.toBuffer("POST /fo\u0690 HTTP/1.0" + scenario.eol + scenario.eol, StandardCharsets.UTF_8); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("POST", _methodOrVersion); assertEquals("/fo\u0690", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -221,14 +255,19 @@ public void testLineParse3(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testLineParse4(String eoln) + @MethodSource("scenarios") + public void testLineParse4(Scenario scenario) { - ByteBuffer buffer = BufferUtil.toBuffer("POST /foo?param=\u0690 HTTP/1.0" + eoln + eoln, StandardCharsets.UTF_8); + ByteBuffer buffer = BufferUtil.toBuffer("POST /foo?param=\u0690 HTTP/1.0" + scenario.eol + scenario.eol, StandardCharsets.UTF_8); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("POST", _methodOrVersion); assertEquals("/foo?param=\u0690", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -236,14 +275,19 @@ public void testLineParse4(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testLineParse5(String eoln) + @MethodSource("scenarios") + public void testLineParse5(Scenario scenario) { - ByteBuffer buffer = BufferUtil.toBuffer("GET /ctx/testLoginPage;jsessionid=123456789;other HTTP/1.0" + eoln + eoln, StandardCharsets.UTF_8); + ByteBuffer buffer = BufferUtil.toBuffer("GET /ctx/testLoginPage;jsessionid=123456789;other HTTP/1.0" + scenario.eol + scenario.eol, StandardCharsets.UTF_8); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/ctx/testLoginPage;jsessionid=123456789;other", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -251,14 +295,19 @@ public void testLineParse5(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testLongURLParse(String eoln) + @MethodSource("scenarios") + public void testLongURLParse(Scenario scenario) { - ByteBuffer buffer = BufferUtil.toBuffer("POST /123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/ HTTP/1.0" + eoln + eoln); + ByteBuffer buffer = BufferUtil.toBuffer("POST /123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/ HTTP/1.0" + scenario.eol + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("POST", _methodOrVersion); assertEquals("/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -266,14 +315,19 @@ public void testLongURLParse(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testAllowedLinePreamble(String eoln) + @MethodSource("scenarios") + public void testAllowedLinePreamble(Scenario scenario) { - ByteBuffer buffer = BufferUtil.toBuffer(eoln + eoln + "GET / HTTP/1.0" + eoln); + ByteBuffer buffer = BufferUtil.toBuffer(scenario.eol + scenario.eol + "GET / HTTP/1.0" + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -281,25 +335,35 @@ public void testAllowedLinePreamble(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testDisallowedLinePreamble(String eoln) + @MethodSource("scenarios") + public void testDisallowedLinePreamble(Scenario scenario) { - ByteBuffer buffer = BufferUtil.toBuffer(eoln + " " + eoln + "GET / HTTP/1.0" + eoln); + ByteBuffer buffer = BufferUtil.toBuffer(scenario.eol + " " + scenario.eol + "GET / HTTP/1.0" + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("Illegal character SPACE=' '", _bad); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testConnect(String eoln) + @MethodSource("scenarios") + public void testConnect(Scenario scenario) { - ByteBuffer buffer = BufferUtil.toBuffer("CONNECT 192.168.1.2:80 HTTP/1.1" + eoln + eoln); + ByteBuffer buffer = BufferUtil.toBuffer("CONNECT 192.168.1.2:80 HTTP/1.1" + scenario.eol + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("CONNECT", _methodOrVersion); assertEquals("192.168.1.2:80", _uriOrStatus); assertEquals("HTTP/1.1", _versionOrReason); @@ -307,18 +371,23 @@ public void testConnect(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testSimple(String eoln) + @MethodSource("scenarios") + public void testSimple(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "Host: localhost" + eoln + - "Connection: close" + eoln + - eoln); + "GET / HTTP/1.0" + scenario.eol + + "Host: localhost" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(_headerCompleted); assertTrue(_messageCompleted); @@ -333,18 +402,23 @@ public void testSimple(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testLowerCaseVersion(String eoln) + @MethodSource("scenarios") + public void testLowerCaseVersion(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / http/1.1" + eoln + - "Host: localhost" + eoln + - "Connection: close" + eoln + - eoln); + "GET / http/1.1" + scenario.eol + + "Host: localhost" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(_headerCompleted); assertTrue(_messageCompleted); @@ -373,18 +447,23 @@ public void testHeaderCache() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testHeaderCacheNearMiss(String eoln) + @MethodSource("scenarios") + public void testHeaderCacheNearMiss(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "Host: localhost" + eoln + - "Connection: closed" + eoln + - eoln); + "GET / HTTP/1.0" + scenario.eol + + "Host: localhost" + scenario.eol + + "Connection: closed" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(_headerCompleted); assertTrue(_messageCompleted); @@ -399,21 +478,26 @@ public void testHeaderCacheNearMiss(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testHeaderCacheSplitNearMiss(String eoln) + @MethodSource("scenarios") + public void testHeaderCacheSplitNearMiss(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "Host: localhost" + eoln + + "GET / HTTP/1.0" + scenario.eol + + "Host: localhost" + scenario.eol + "Connection: close"); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); assertFalse(parser.parseNext(buffer)); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } buffer = BufferUtil.toBuffer( - "d" + eoln + - eoln); + "d" + scenario.eol + + scenario.eol); assertTrue(parser.parseNext(buffer)); assertTrue(_headerCompleted); @@ -429,21 +513,26 @@ public void testHeaderCacheSplitNearMiss(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testFoldedField2616(String eoln) + @MethodSource("scenarios") + public void testFoldedFieldMultiLine(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "Host: localhost" + eoln + - "Name: value" + eoln + - " extra" + eoln + - "Name2: " + eoln + - "\tvalue2" + eoln + - eoln); + "GET / HTTP/1.0" + scenario.eol + + "Host: localhost" + scenario.eol + + "Name: value" + scenario.eol + + " extra" + scenario.eol + + "Name2: " + scenario.eol + + "\tvalue2" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY); + HttpParser parser = new HttpParser(handler, scenario.compliance.with("test", MULTILINE_FIELD_VALUE)); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertThat(_bad, Matchers.nullValue()); assertEquals("Host", _hdr[0]); @@ -453,56 +542,41 @@ public void testFoldedField2616(String eoln) assertEquals("value extra", _val[1]); assertEquals("Name2", _hdr[2]); assertEquals("value2", _val[2]); - assertThat(_complianceViolation, contains(MULTILINE_FIELD_VALUE, MULTILINE_FIELD_VALUE)); - } - - @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testFoldedField7230(String eoln) - { - ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "Host: localhost" + eoln + - "Name: value" + eoln + - " extra" + eoln + - eoln); - - HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler, 4096, HttpCompliance.RFC7230_LEGACY); - parseAll(parser, buffer); - - assertThat(_bad, Matchers.notNullValue()); - assertThat(_bad, containsString("Line Folding not supported")); - assertThat(_complianceViolation, Matchers.empty()); + assertTrue(_complianceViolation.contains(MULTILINE_FIELD_VALUE)); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testWhiteSpaceInName(String eoln) + @MethodSource("scenarios") + public void testWhiteSpaceInName(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "Host: localhost" + eoln + - "N ame: value" + eoln + - eoln); + "GET / HTTP/1.0" + scenario.eol + + "Host: localhost" + scenario.eol + + "N ame: value" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler, 4096, HttpCompliance.RFC7230_LEGACY); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertThat(_bad, Matchers.notNullValue()); assertThat(_bad, containsString("Illegal character")); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testWhiteSpaceAfterName(String eoln) + @MethodSource("scenarios") + public void testWhiteSpaceAfterName(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "Host: localhost" + eoln + - "Name : value" + eoln + - eoln); + "GET / HTTP/1.0" + scenario.eol + + "Host: localhost" + scenario.eol + + "Name : value" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); HttpParser parser = new HttpParser(handler, 4096, HttpCompliance.RFC7230_LEGACY); @@ -566,17 +640,17 @@ public void testWhiteSpaceBeforeRequest() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testNoValue(String eoln) + @MethodSource("scenarios") + public void testNoValue(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "Host: localhost" + eoln + - "Name0: " + eoln + - "Name1:" + eoln + - "Authorization: " + eoln + - "Authorization:" + eoln + - eoln); + "GET / HTTP/1.0" + scenario.eol + + "Host: localhost" + scenario.eol + + "Name0: " + scenario.eol + + "Name1:" + scenario.eol + + "Authorization: " + scenario.eol + + "Authorization:" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler() { @@ -587,9 +661,14 @@ public void badMessage(HttpException failure) super.badMessage(failure); } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.setHeaderCacheSize(1024); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(_headerCompleted); assertTrue(_messageCompleted); @@ -610,18 +689,23 @@ public void badMessage(HttpException failure) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testTrailingSpacesInHeaderNameNoCustom0(String eoln) + @MethodSource("scenarios") + public void testTrailingSpacesInHeaderNameNoCustom0(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 204 No Content" + eoln + - "Access-Control-Allow-Headers : Origin" + eoln + - "Other: value" + eoln + - eoln); + "HTTP/1.1 204 No Content" + scenario.eol + + "Access-Control-Allow-Headers : Origin" + scenario.eol + + "Other: value" + scenario.eol + + scenario.eol); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, -1, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("HTTP/1.1", _methodOrVersion); assertEquals("204", _uriOrStatus); @@ -630,47 +714,52 @@ public void testTrailingSpacesInHeaderNameNoCustom0(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testNoColon7230(String eoln) + @MethodSource("scenarios") + public void testNoColon7230(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "Host: localhost" + eoln + - "Name" + eoln + - eoln); + "GET / HTTP/1.0" + scenario.eol + + "Host: localhost" + scenario.eol + + "Name" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); HttpParser parser = new HttpParser(handler, HttpCompliance.RFC7230_LEGACY); parseAll(parser, buffer); assertThat(_bad, containsString("Illegal character")); - assertThat(_complianceViolation, Matchers.empty()); + assertThat(_complianceViolation, scenario.isViolation() ? not(empty()) : empty()); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testHeaderParseDirect(String eoln) + @MethodSource("scenarios") + public void testHeaderParseDirect(Scenario scenario) { ByteBuffer b0 = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "Host: localhost" + eoln + - "Header1: value1" + eoln + - "Header2: value 2a " + eoln + - "Header3: 3" + eoln + - "Header4:value4" + eoln + - "Server5: notServer" + eoln + - "HostHeader: notHost" + eoln + - "Connection: close" + eoln + - "Accept-Encoding: gzip, deflated" + eoln + - "Accept: unknown" + eoln + - eoln); + "GET / HTTP/1.0" + scenario.eol + + "Host: localhost" + scenario.eol + + "Header1: value1" + scenario.eol + + "Header2: value 2a " + scenario.eol + + "Header3: 3" + scenario.eol + + "Header4:value4" + scenario.eol + + "Server5: notServer" + scenario.eol + + "HostHeader: notHost" + scenario.eol + + "Connection: close" + scenario.eol + + "Accept-Encoding: gzip, deflated" + scenario.eol + + "Accept: unknown" + scenario.eol + + scenario.eol); ByteBuffer buffer = BufferUtil.allocateDirect(b0.capacity()); int pos = BufferUtil.flipToFill(buffer); BufferUtil.put(b0, buffer); BufferUtil.flipToFlush(buffer, pos); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/", _uriOrStatus); @@ -699,26 +788,32 @@ public void testHeaderParseDirect(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testHeaderParseCRLF(String eoln) + @MethodSource("scenarios") + public void testHeaderParseCRLF(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "Host: localhost" + eoln + - "Header1: value1" + eoln + - "Header2: value 2a " + eoln + - "Header3: 3" + eoln + - "Header4:value4" + eoln + - "Server5: notServer" + eoln + - "HostHeader: notHost" + eoln + - "Connection: close" + eoln + - "Accept-Encoding: gzip, deflated" + eoln + - "Accept: unknown" + eoln + - eoln); + "GET / HTTP/1.0" + scenario.eol + + "Host: localhost" + scenario.eol + + "Header1: value1" + scenario.eol + + "Header2: value 2a " + scenario.eol + + "Header3: 3" + scenario.eol + + "Header4:value4" + scenario.eol + + "Server5: notServer" + scenario.eol + + "HostHeader: notHost" + scenario.eol + + "Connection: close" + scenario.eol + + "Accept-Encoding: gzip, deflated" + scenario.eol + + "Accept: unknown" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + assertEquals("GET", _methodOrVersion); assertEquals("/", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -746,25 +841,30 @@ public void testHeaderParseCRLF(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testHeaderParse(String eoln) + @MethodSource("scenarios") + public void testHeaderParse(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "Host: localhost" + eoln + - "Header1: value1" + eoln + - "Header2: value 2a value 2b " + eoln + - "Header3: 3" + eoln + - "Header4:value4" + eoln + - "Server5: notServer" + eoln + - "HostHeader: notHost" + eoln + - "Connection: close" + eoln + - "Accept-Encoding: gzip, deflated" + eoln + - "Accept: unknown" + eoln + - eoln); + "GET / HTTP/1.0" + scenario.eol + + "Host: localhost" + scenario.eol + + "Header1: value1" + scenario.eol + + "Header2: value 2a value 2b " + scenario.eol + + "Header3: 3" + scenario.eol + + "Header4:value4" + scenario.eol + + "Server5: notServer" + scenario.eol + + "HostHeader: notHost" + scenario.eol + + "Connection: close" + scenario.eol + + "Accept-Encoding: gzip, deflated" + scenario.eol + + "Accept: unknown" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/", _uriOrStatus); @@ -793,18 +893,23 @@ public void testHeaderParse(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testQuoted(String eoln) + @MethodSource("scenarios") + public void testQuoted(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "Name0: \"value0\"\t" + eoln + - "Name1: \"value\t1\"" + eoln + - "Name2: \"value\t2A\",\"value,2B\"\t" + eoln + - eoln); + "GET / HTTP/1.0" + scenario.eol + + "Name0: \"value0\"\t" + scenario.eol + + "Name1: \"value\t1\"" + scenario.eol + + "Name2: \"value\t2A\",\"value,2B\"\t" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/", _uriOrStatus); @@ -819,24 +924,29 @@ public void testQuoted(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testEncodedHeader(String eoln) + @MethodSource("scenarios") + public void testEncodedHeader(Scenario scenario) { ByteBuffer buffer = BufferUtil.allocate(4096); BufferUtil.flipToFill(buffer); BufferUtil.put(BufferUtil.toBuffer("GET "), buffer); buffer.put("/foo/\u0690/".getBytes(StandardCharsets.UTF_8)); - BufferUtil.put(BufferUtil.toBuffer(" HTTP/1.0" + eoln), buffer); + BufferUtil.put(BufferUtil.toBuffer(" HTTP/1.0" + scenario.eol), buffer); BufferUtil.put(BufferUtil.toBuffer("Header1: "), buffer); buffer.put("\u00e6 \u00e6".getBytes(StandardCharsets.ISO_8859_1)); - BufferUtil.put(BufferUtil.toBuffer(" " + eoln + "Header2: "), buffer); + BufferUtil.put(BufferUtil.toBuffer(" " + scenario.eol + "Header2: "), buffer); buffer.put((byte)-1); - BufferUtil.put(BufferUtil.toBuffer(eoln + eoln), buffer); + BufferUtil.put(BufferUtil.toBuffer(scenario.eol + scenario.eol), buffer); BufferUtil.flipToFlush(buffer, 0); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/foo/\u0690/", _uriOrStatus); @@ -850,65 +960,70 @@ public void testEncodedHeader(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testResponseBufferUpgradeFrom(String eoln) + @MethodSource("scenarios") + public void testResponseBufferUpgradeFrom(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 101 Upgrade" + eoln + - "Connection: upgrade" + eoln + - "Content-Length: 0" + eoln + - "Sec-WebSocket-Accept: 4GnyoUP4Sc1JD+2pCbNYAhFYVVA" + eoln + - eoln + + "HTTP/1.1 101 Upgrade" + scenario.eol + + "Connection: upgrade" + scenario.eol + + "Content-Length: 0" + scenario.eol + + "Sec-WebSocket-Accept: 4GnyoUP4Sc1JD+2pCbNYAhFYVVA" + scenario.eol + + scenario.eol + "FOOGRADE"); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); while (!parser.isState(State.END)) { parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } } assertThat(BufferUtil.toUTF8String(buffer), Matchers.is("FOOGRADE")); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testBadMethodEncoding(String eoln) + @MethodSource("scenarios") + public void testBadMethodEncoding(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "G\u00e6T / HTTP/1.0" + eoln + "Header0: value0" + eoln + eoln); + "G\u00e6T / HTTP/1.0" + scenario.eol + "Header0: value0" + scenario.eol + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); - assertThat(_bad, Matchers.notNullValue()); + assertThat(_bad, containsString("Illegal character")); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testBadVersionEncoding(String eoln) + @MethodSource("scenarios") + public void testBadVersionEncoding(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / H\u00e6P/1.0" + eoln + "Header0: value0" + eoln + eoln); + "GET / H\u00e6P/1.0" + scenario.eol + "Header0: value0" + scenario.eol + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); assertThat(_bad, Matchers.notNullValue()); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testBadHeaderEncoding(String eoln) + @MethodSource("scenarios") + public void testBadHeaderEncoding(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + - "H\u00e6der0: value0" + eoln + + "GET / HTTP/1.0" + scenario.eol + + "H\u00e6der0: value0" + scenario.eol + "\n\n"); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); assertThat(_bad, Matchers.notNullValue()); } @@ -948,18 +1063,23 @@ public void testBadHeaderNames() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testHeaderTab(String eoln) + @MethodSource("scenarios") + public void testHeaderTab(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.1" + eoln + - "Host: localhost" + eoln + - "Header: value\talternate" + eoln + - "\n\n"); + "GET / HTTP/1.1" + scenario.eol + + "Host: localhost" + scenario.eol + + "Header: value\talternate" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/", _uriOrStatus); @@ -971,51 +1091,58 @@ public void testHeaderTab(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testCaseSensitiveMethod(String eoln) + @MethodSource("scenarios") + public void testCaseSensitiveMethod(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "gEt / http/1.0" + eoln + - "Host: localhost" + eoln + - "Connection: close" + eoln + - eoln); + "gEt / http/1.0" + scenario.eol + + "Host: localhost" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler, -1, HttpCompliance.RFC7230_LEGACY); + HttpParser parser = new HttpParser(handler, -1, scenario.compliance.with("test", CASE_INSENSITIVE_METHOD)); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertNull(_bad); assertEquals("GET", _methodOrVersion); - assertThat(_complianceViolation, contains(CASE_INSENSITIVE_METHOD)); + assertTrue(_complianceViolation.contains(CASE_INSENSITIVE_METHOD)); } - @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testCaseSensitiveMethodLegacy(String eoln) + @Test + public void testCaseSensitiveMethodLegacy() { - ByteBuffer buffer = BufferUtil.toBuffer( - "gEt / http/1.0" + eoln + - "Host: localhost" + eoln + - "Connection: close" + eoln + - eoln); + ByteBuffer buffer = BufferUtil.toBuffer(""" + gEt / http/1.0\r + Host: localhost\r + Connection: close\r + \r + """); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler, -1, HttpCompliance.LEGACY); + HttpParser parser = new HttpParser(handler, HttpCompliance.LEGACY); parseAll(parser, buffer); + assertNull(_bad); assertEquals("gEt", _methodOrVersion); assertThat(_complianceViolation, Matchers.empty()); } - @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testCaseInsensitiveHeader(String eoln) + @Test + public void testCaseInsensitiveHeader() { - ByteBuffer buffer = BufferUtil.toBuffer( - "GET / http/1.0" + eoln + - "HOST: localhost" + eoln + - "cOnNeCtIoN: ClOsE" + eoln + - eoln); + ByteBuffer buffer = BufferUtil.toBuffer(""" + GET / http/1.0\r + HOST: localhost\r + cOnNeCtIoN: ClOsE\r + \r + """); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler, -1, HttpCompliance.RFC7230_LEGACY); + HttpParser parser = new HttpParser(handler, HttpCompliance.RFC7230_LEGACY); parseAll(parser, buffer); + assertNull(_bad); assertEquals("GET", _methodOrVersion); assertEquals("/", _uriOrStatus); @@ -1028,17 +1155,17 @@ public void testCaseInsensitiveHeader(String eoln) assertThat(_complianceViolation, Matchers.empty()); } - @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testCaseInSensitiveHeaderLegacy(String eoln) + @Test + public void testCaseInSensitiveHeaderLegacy() { - ByteBuffer buffer = BufferUtil.toBuffer( - "GET / http/1.0" + eoln + - "HOST: localhost" + eoln + - "cOnNeCtIoN: ClOsE" + eoln + - eoln); + ByteBuffer buffer = BufferUtil.toBuffer(""" + GET / http/1.0\r + HOST: localhost\r + cOnNeCtIoN: ClOsE\r + \r + """); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler, -1, HttpCompliance.LEGACY); + HttpParser parser = new HttpParser(handler, HttpCompliance.LEGACY); parser.setHeaderCacheCaseSensitive(true); parseAll(parser, buffer); assertNull(_bad); @@ -1054,18 +1181,18 @@ public void testCaseInSensitiveHeaderLegacy(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testSplitHeaderParse(String eoln) + @MethodSource("scenarios") + public void testSplitHeaderParse(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "XXXXSPLIT / HTTP/1.0" + eoln + - "Host: localhost" + eoln + - "Header1: value1" + eoln + - "Header2: value 2a " + eoln + - "Header3: 3" + eoln + - "Header4:value4" + eoln + - "Server5: notServer" + eoln + - eoln + + "XXXXSPLIT / HTTP/1.0" + scenario.eol + + "Host: localhost" + scenario.eol + + "Header1: value1" + scenario.eol + + "Header2: value 2a " + scenario.eol + + "Header3: 3" + scenario.eol + + "Header4:value4" + scenario.eol + + "Server5: notServer" + scenario.eol + + scenario.eol + "ZZZZ"); buffer.position(2); buffer.limit(buffer.capacity() - 2); @@ -1074,7 +1201,7 @@ public void testSplitHeaderParse(String eoln) for (int i = 0; i < buffer.capacity() - 4; i++) { HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); buffer.limit(2 + i); buffer.position(2); @@ -1089,6 +1216,12 @@ public void testSplitHeaderParse(String eoln) parser.parseNext(buffer); } + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + assertEquals("SPLIT", _methodOrVersion); assertEquals("/", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -1109,24 +1242,37 @@ public void testSplitHeaderParse(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testChunkParse(String eoln) + @MethodSource("scenarios") + public void testChunkParse(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET /chunk HTTP/1.0" + eoln + - "Header1: value1" + eoln + - "Transfer-Encoding: chunked" + eoln + - eoln + - "a;" + eoln + - "0123456789" + eoln + - "1a" + eoln + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + eoln + - "0" + eoln + - eoln); + "GET /chunk HTTP/1.0" + scenario.eol + + "Header1: value1" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + scenario.eol + + "a;" + scenario.eolChunk + + "0123456789" + scenario.eolChunk + + "1a" + scenario.eolChunk + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + scenario.eolChunk + + "0" + scenario.eolChunk + + scenario.eolChunk); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + + if (scenario.expectEarly()) + { + assertTrue(_early); + return; + } + + assertFalse(_early); assertEquals("GET", _methodOrVersion); assertEquals("/chunk", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -1140,24 +1286,34 @@ public void testChunkParse(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testBadChunkLength(String eoln) + @MethodSource("scenarios") + public void testBadChunkLength(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET /chunk HTTP/1.0" + eoln + - "Header1: value1" + eoln + - "Transfer-Encoding: chunked" + eoln + - eoln + - "a;" + eoln + - "0123456789" + eoln + - "xx" + eoln + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + eoln + - "0" + eoln + - eoln + "GET /chunk HTTP/1.0" + scenario.eol + + "Header1: value1" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + scenario.eol + + "a;" + scenario.eolChunk + + "0123456789" + scenario.eolChunk + + "xx" + scenario.eolChunk + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + scenario.eolChunk + + "0" + scenario.eolChunk + + scenario.eol ); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + if (scenario.expectEarly()) + { + assertTrue(_early); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/chunk", _uriOrStatus); @@ -1171,23 +1327,28 @@ public void testBadChunkLength(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testBadTransferEncoding(String eoln) + @MethodSource("scenarios") + public void testBadTransferEncoding(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET /chunk HTTP/1.0" + eoln + - "Header1: value1" + eoln + - "Transfer-Encoding: chunked, identity" + eoln + - eoln + - "a;" + eoln + - "0123456789" + eoln + - "1a" + eoln + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + eoln + - "0" + eoln + - eoln); + "GET /chunk HTTP/1.0" + scenario.eol + + "Header1: value1" + scenario.eol + + "Transfer-Encoding: chunked, identity" + scenario.eol + + scenario.eol + + "a;" + scenario.eolChunk + + "0123456789" + scenario.eolChunk + + "1a" + scenario.eolChunk + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + scenario.eolChunk + + "0" + scenario.eolChunk + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/chunk", _uriOrStatus); @@ -1196,24 +1357,34 @@ public void testBadTransferEncoding(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testChunkParseTrailer(String eoln) + @MethodSource("scenarios") + public void testChunkParseTrailer(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET /chunk HTTP/1.0" + eoln + - "Header1: value1" + eoln + - "Transfer-Encoding: chunked" + eoln + - eoln + - "a;" + eoln + - "0123456789" + eoln + - "1a" + eoln + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + eoln + - "0" + eoln + - "Trailer: value" + eoln + - eoln); + "GET /chunk HTTP/1.0" + scenario.eol + + "Header1: value1" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + scenario.eol + + "a;" + scenario.eolChunk + + "0123456789" + scenario.eolChunk + + "1a" + scenario.eolChunk + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + scenario.eolChunk + + "0" + scenario.eolChunk + + "Trailer: value" + scenario.eolChunk + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + if (scenario.expectEarly()) + { + assertTrue(_early); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/chunk", _uriOrStatus); @@ -1232,24 +1403,34 @@ public void testChunkParseTrailer(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testChunkParseTrailers(String eoln) + @MethodSource("scenarios") + public void testChunkParseTrailers(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET /chunk HTTP/1.0" + eoln + - "Transfer-Encoding: chunked" + eoln + - eoln + - "a;" + eoln + - "0123456789" + eoln + - "1a" + eoln + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + eoln + - "0" + eoln + - "Trailer: value" + eoln + - "Foo: bar" + eoln + - eoln); + "GET /chunk HTTP/1.0" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + scenario.eol + + "a;" + scenario.eolChunk + + "0123456789" + scenario.eolChunk + + "1a" + scenario.eolChunk + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + scenario.eolChunk + + "0" + scenario.eolChunk + + "Trailer: value" + scenario.eol + + "Foo: bar" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + if (scenario.expectEarly()) + { + assertTrue(_early); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/chunk", _uriOrStatus); @@ -1271,26 +1452,37 @@ public void testChunkParseTrailers(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testChunkParseBadTrailer(String eoln) + @MethodSource("scenarios") + public void testChunkParseBadTrailer(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET /chunk HTTP/1.0" + eoln + - "Header1: value1" + eoln + - "Transfer-Encoding: chunked" + eoln + - eoln + - "a;" + eoln + - "0123456789" + eoln + - "1a" + eoln + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + eoln + - "0" + eoln + + "GET /chunk HTTP/1.0" + scenario.eol + + "Header1: value1" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + scenario.eol + + "a;" + scenario.eolChunk + + "0123456789" + scenario.eolChunk + + "1a" + scenario.eolChunk + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + scenario.eolChunk + + "0" + scenario.eolChunk + "Trailer: value"); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); parser.atEOF(); parser.parseNext(BufferUtil.EMPTY_BUFFER); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + if (scenario.expectEarly()) + { + assertTrue(_early); + return; + } + assertEquals("GET", _methodOrVersion); assertEquals("/chunk", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -1304,22 +1496,32 @@ public void testChunkParseBadTrailer(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testChunkParseNoTrailer(String eoln) + @MethodSource("scenarios") + public void testChunkParseNoTrailer(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET /chunk HTTP/1.0" + eoln + - "Header1: value1" + eoln + - "Transfer-Encoding: chunked" + eoln + - eoln + - "a;" + eoln + - "0123456789" + eoln + - "1a" + eoln + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + eoln + - "0" + eoln); + "GET /chunk HTTP/1.0" + scenario.eol + + "Header1: value1" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + scenario.eol + + "a;" + scenario.eolChunk + + "0123456789" + scenario.eolChunk + + "1a" + scenario.eolChunk + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + scenario.eolChunk + + "0" + scenario.eolChunk); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + if (scenario.expectEarly()) + { + assertTrue(_early); + return; + } parser.atEOF(); parser.parseNext(BufferUtil.EMPTY_BUFFER); @@ -1348,18 +1550,23 @@ public void testStartEOF() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testEarlyEOF(String eoln) + @MethodSource("scenarios") + public void testEarlyEOF(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET /uri HTTP/1.0" + eoln + - "Content-Length: 20" + eoln + - eoln + + "GET /uri HTTP/1.0" + scenario.eol + + "Content-Length: 20" + scenario.eol + + scenario.eol + "0123456789"); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.atEOF(); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/uri", _uriOrStatus); @@ -1370,21 +1577,33 @@ public void testEarlyEOF(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testChunkEarlyEOF(String eoln) + @MethodSource("scenarios") + public void testChunkEarlyEOF(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET /chunk HTTP/1.0" + eoln + - "Header1: value1" + eoln + - "Transfer-Encoding: chunked" + eoln + - eoln + - "a;" + eoln + - "0123456789" + eoln); + "GET /chunk HTTP/1.0" + scenario.eol + + "Header1: value1" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + scenario.eol + + "a;" + scenario.eolChunk + + "0123456789" + scenario.eolChunk); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.atEOF(); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + + if (scenario.expectEarly()) + { + assertTrue(_early); + return; + } + assertEquals("GET", _methodOrVersion); assertEquals("/chunk", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -1397,39 +1616,49 @@ public void testChunkEarlyEOF(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testMultiParse(String eoln) + @MethodSource("scenarios") + public void testMultiParse(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET /mp HTTP/1.0" + eoln + - "Connection: Keep-Alive" + eoln + - "Header1: value1" + eoln + - "Transfer-Encoding: chunked" + eoln + - eoln + - "a;" + eoln + - "0123456789" + eoln + - "1a" + eoln + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + eoln + - "0" + eoln + - - eoln + - - "POST /foo HTTP/1.0" + eoln + - "Connection: Keep-Alive" + eoln + - "Header2: value2" + eoln + - "Content-Length: 0" + eoln + - eoln + - - "PUT /doodle HTTP/1.0" + eoln + - "Connection: close" + eoln + - "Header3: value3" + eoln + - "Content-Length: 10" + eoln + - eoln + - "0123456789" + eoln); + "GET /mp HTTP/1.0" + scenario.eol + + "Connection: Keep-Alive" + scenario.eol + + "Header1: value1" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + scenario.eol + + "a;" + scenario.eolChunk + + "0123456789" + scenario.eolChunk + + "1a" + scenario.eolChunk + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + scenario.eolChunk + + "0" + scenario.eolChunk + + + scenario.eol + + + "POST /foo HTTP/1.0" + scenario.eol + + "Connection: Keep-Alive" + scenario.eol + + "Header2: value2" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol + + + "PUT /doodle HTTP/1.0" + scenario.eol + + "Connection: close" + scenario.eol + + "Header3: value3" + scenario.eol + + "Content-Length: 10" + scenario.eol + + scenario.eol + + "0123456789" + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + if (scenario.expectEarly()) + { + assertTrue(_early); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/mp", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -1463,41 +1692,56 @@ public void testMultiParse(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testMultiParseEarlyEOF(String eoln) + @MethodSource("scenarios") + public void testMultiParseEarlyEOF(Scenario scenario) { ByteBuffer buffer0 = BufferUtil.toBuffer( - "GET /mp HTTP/1.0" + eoln + - "Connection: Keep-Alive" + eoln); - - ByteBuffer buffer1 = BufferUtil.toBuffer("Header1: value1" + eoln + - "Transfer-Encoding: chunked" + eoln + - eoln + - "a;" + eoln + - "0123456789" + eoln + - "1a" + eoln + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + eoln + - "0" + eoln + - - eoln + - - "POST /foo HTTP/1.0" + eoln + - "Connection: Keep-Alive" + eoln + - "Header2: value2" + eoln + - "Content-Length: 0" + eoln + - eoln + - - "PUT /doodle HTTP/1.0" + eoln + - "Connection: close" + eoln + "Header3: value3" + eoln + - "Content-Length: 10" + eoln + - eoln + - "0123456789" + eoln); + "GET /mp HTTP/1.0" + scenario.eol + + "Connection: Keep-Alive" + scenario.eol); + + ByteBuffer buffer1 = BufferUtil.toBuffer("Header1: value1" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + scenario.eol + + "a;" + scenario.eolChunk + + "0123456789" + scenario.eolChunk + + "1a" + scenario.eolChunk + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + scenario.eolChunk + + "0" + scenario.eolChunk + + + scenario.eol + + + "POST /foo HTTP/1.0" + scenario.eol + + "Connection: Keep-Alive" + scenario.eol + + "Header2: value2" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol + + + "PUT /doodle HTTP/1.0" + scenario.eol + + "Connection: close" + scenario.eol + "Header3: value3" + scenario.eol + + "Content-Length: 10" + scenario.eol + + scenario.eol + + "0123456789" + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer0); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } parser.atEOF(); parser.parseNext(buffer1); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + if (scenario.expectEarly()) + { + assertTrue(_early); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/mp", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -1530,19 +1774,24 @@ public void testMultiParseEarlyEOF(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testResponseParse0(String eoln) + @MethodSource("scenarios") + public void testResponseParse0(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 Correct" + eoln + - "Content-Length: 10" + eoln + - "Content-Type: text/plain" + eoln + - eoln + - "0123456789" + eoln); + "HTTP/1.1 200 Correct" + scenario.eol + + "Content-Length: 10" + scenario.eol + + "Content-Type: text/plain" + scenario.eol + + scenario.eol + + "0123456789" + scenario.eol); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, -1, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("HTTP/1.1", _methodOrVersion); assertEquals("200", _uriOrStatus); assertEquals("Correct", _versionOrReason); @@ -1552,17 +1801,22 @@ public void testResponseParse0(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testResponseParse1(String eoln) + @MethodSource("scenarios") + public void testResponseParse1(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 304 Not-Modified" + eoln + - "Connection: close" + eoln + - eoln); + "HTTP/1.1 304 Not-Modified" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, -1, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("HTTP/1.1", _methodOrVersion); assertEquals("304", _uriOrStatus); assertEquals("Not-Modified", _versionOrReason); @@ -1571,23 +1825,28 @@ public void testResponseParse1(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testResponseParse2(String eoln) + @MethodSource("scenarios") + public void testResponseParse2(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 204 No-Content" + eoln + - "Header: value" + eoln + - eoln + + "HTTP/1.1 204 No-Content" + scenario.eol + + "Header: value" + scenario.eol + + scenario.eol + - "HTTP/1.1 200 Correct" + eoln + - "Content-Length: 10" + eoln + - "Content-Type: text/plain" + eoln + - eoln + - "0123456789" + eoln); + "HTTP/1.1 200 Correct" + scenario.eol + + "Content-Length: 10" + scenario.eol + + "Content-Type: text/plain" + scenario.eol + + scenario.eol + + "0123456789" + scenario.eol); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("HTTP/1.1", _methodOrVersion); assertEquals("204", _uriOrStatus); assertEquals("No-Content", _versionOrReason); @@ -1608,19 +1867,24 @@ public void testResponseParse2(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testResponseParse3(String eoln) + @MethodSource("scenarios") + public void testResponseParse3(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200" + eoln + - "Content-Length: 10" + eoln + - "Content-Type: text/plain" + eoln + - eoln + - "0123456789" + eoln); + "HTTP/1.1 200" + scenario.eol + + "Content-Length: 10" + scenario.eol + + "Content-Type: text/plain" + scenario.eol + + scenario.eol + + "0123456789" + scenario.eol); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("HTTP/1.1", _methodOrVersion); assertEquals("200", _uriOrStatus); assertNull(_versionOrReason); @@ -1630,19 +1894,24 @@ public void testResponseParse3(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testResponseParse4(String eoln) + @MethodSource("scenarios") + public void testResponseParse4(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 " + eoln + - "Content-Length: 10" + eoln + - "Content-Type: text/plain" + eoln + - eoln + - "0123456789" + eoln); + "HTTP/1.1 200 " + scenario.eol + + "Content-Length: 10" + scenario.eol + + "Content-Type: text/plain" + scenario.eol + + scenario.eol + + "0123456789" + scenario.eol); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("HTTP/1.1", _methodOrVersion); assertEquals("200", _uriOrStatus); assertNull(_versionOrReason); @@ -1652,41 +1921,51 @@ public void testResponseParse4(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testResponseEOFContent(String eoln) + @MethodSource("scenarios") + public void testResponseEOFContent(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 " + eoln + - "Content-Type: text/plain" + eoln + - eoln + - "0123456789" + eoln); + "HTTP/1.1 200 " + scenario.eol + + "Content-Type: text/plain" + scenario.eol + + scenario.eol + + "0123456789" + scenario.eol); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.atEOF(); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("HTTP/1.1", _methodOrVersion); assertEquals("200", _uriOrStatus); assertNull(_versionOrReason); - assertEquals(10 + eoln.length(), _content.length()); - assertEquals("0123456789" + eoln, _content); + assertEquals(10 + scenario.eol.length(), _content.length()); + assertEquals("0123456789" + scenario.eol, _content); assertTrue(_headerCompleted); assertTrue(_messageCompleted); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testResponse304WithContentLength(String eoln) + @MethodSource("scenarios") + public void testResponse304WithContentLength(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 304 found" + eoln + - "Content-Length: 10" + eoln + - eoln); + "HTTP/1.1 304 found" + scenario.eol + + "Content-Length: 10" + scenario.eol + + scenario.eol); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("HTTP/1.1", _methodOrVersion); assertEquals("304", _uriOrStatus); assertEquals("found", _versionOrReason); @@ -1696,17 +1975,22 @@ public void testResponse304WithContentLength(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testResponse101WithTransferEncoding(String eoln) + @MethodSource("scenarios") + public void testResponse101WithTransferEncoding(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 101 switching protocols" + eoln + - "Transfer-Encoding: chunked" + eoln + - eoln); + "HTTP/1.1 101 switching protocols" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + scenario.eol); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("HTTP/1.1", _methodOrVersion); assertEquals("101", _uriOrStatus); assertEquals("switching protocols", _versionOrReason); @@ -1732,38 +2016,50 @@ public void testBadResponseStatus(String status) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testResponseReasonIso88591(String eoln) + @MethodSource("scenarios") + public void testResponseReasonIso88591(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 302 déplacé temporairement" + eoln + - "Content-Length: 0" + eoln + - eoln, StandardCharsets.ISO_8859_1); + "HTTP/1.1 302 déplacé temporairement" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol, StandardCharsets.ISO_8859_1); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + assertEquals("HTTP/1.1", _methodOrVersion); assertEquals("302", _uriOrStatus); assertEquals("déplacé temporairement", _versionOrReason); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testSeekEOF(String eoln) + @MethodSource("scenarios") + public void testSeekEOF(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 OK" + eoln + - "Content-Length: 0" + eoln + - "Connection: close" + eoln + - eoln + - eoln + // extra CRLF ignored - "HTTP/1.1 400 OK" + eoln); // extra data causes close ?? + "HTTP/1.1 200 OK" + scenario.eol + + "Content-Length: 0" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol + + scenario.eol + // extra CRLF ignored + "HTTP/1.1 400 OK" + scenario.eol); // extra data causes close ?? HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("HTTP/1.1", _methodOrVersion); assertEquals("200", _uriOrStatus); assertEquals("OK", _versionOrReason); @@ -1782,19 +2078,24 @@ public void testSeekEOF(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testNoURI(String eoln) + @MethodSource("scenarios") + public void testNoURI(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET" + eoln + - "Content-Length: 0" + eoln + - "Connection: close" + eoln + - eoln); + "GET" + scenario.eol + + "Content-Length: 0" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertNull(_methodOrVersion); assertEquals("No URI", _bad); assertFalse(buffer.hasRemaining()); @@ -1805,19 +2106,24 @@ public void testNoURI(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testNoURI2(String eoln) + @MethodSource("scenarios") + public void testNoURI2(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET " + eoln + - "Content-Length: 0" + eoln + - "Connection: close" + eoln + - eoln); + "GET " + scenario.eol + + "Content-Length: 0" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertNull(_methodOrVersion); assertEquals("No URI", _bad); assertFalse(buffer.hasRemaining()); @@ -1828,35 +2134,42 @@ public void testNoURI2(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testUnknownRequestVersion(String eoln) + @MethodSource("scenarios") + public void testUnknownRequestVersion(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP" + eoln + - "Host: localhost" + eoln + - eoln); + "GET / HTTP" + scenario.eol + + "Host: localhost" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("Unknown Version", _bad); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testUnknownResponseVersion(String eoln) + @MethodSource("scenarios") + public void testUnknownResponseVersion(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HPPT/7.7 200 OK" + eoln + - "Content-Length: 0" + eoln + - "Connection: close" + eoln + - eoln); + "HPPT/7.7 200 OK" + scenario.eol + + "Content-Length: 0" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + assertNull(_methodOrVersion); assertEquals("Unknown Version", _bad); assertFalse(buffer.hasRemaining()); @@ -1867,19 +2180,24 @@ public void testUnknownResponseVersion(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testNoStatus(String eoln) + @MethodSource("scenarios") + public void testNoStatus(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1" + eoln + - "Content-Length: 0" + eoln + - "Connection: close" + eoln + - eoln); + "HTTP/1.1" + scenario.eol + + "Content-Length: 0" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertNull(_methodOrVersion); assertEquals("No Status", _bad); assertFalse(buffer.hasRemaining()); @@ -1890,19 +2208,24 @@ public void testNoStatus(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testNoStatus2(String eoln) + @MethodSource("scenarios") + public void testNoStatus2(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 " + eoln + - "Content-Length: 0" + eoln + - "Connection: close" + eoln + - eoln); + "HTTP/1.1 " + scenario.eol + + "Content-Length: 0" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.ResponseHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertNull(_methodOrVersion); assertEquals("No Status", _bad); assertFalse(buffer.hasRemaining()); @@ -1913,19 +2236,24 @@ public void testNoStatus2(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testBadRequestVersion(String eoln) + @MethodSource("scenarios") + public void testBadRequestVersion(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HPPT/7.7" + eoln + - "Content-Length: 0" + eoln + - "Connection: close" + eoln + - eoln); + "GET / HPPT/7.7" + scenario.eol + + "Content-Length: 0" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertNull(_methodOrVersion); assertEquals("Unknown Version", _bad); assertFalse(buffer.hasRemaining()); @@ -1935,13 +2263,13 @@ public void testBadRequestVersion(String eoln) assertEquals(HttpParser.State.CLOSED, parser.getState()); buffer = BufferUtil.toBuffer( - "GET / HTTP/1.01" + eoln + - "Content-Length: 0" + eoln + - "Connection: close" + eoln + - eoln); + "GET / HTTP/1.01" + scenario.eol + + "Content-Length: 0" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); handler = new Handler(); - parser = new HttpParser(handler); + parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); assertNull(_methodOrVersion); @@ -1954,19 +2282,26 @@ public void testBadRequestVersion(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testBadCR(String eoln) + @MethodSource("scenarios") + public void testBadCR(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.0" + eoln + + "GET / HTTP/1.0" + scenario.eol + "Content-Length: 0\r" + "Connection: close\r" + "\r"); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + assertEquals("Bad EOL", _bad); assertFalse(buffer.hasRemaining()); assertEquals(HttpParser.State.CLOSE, parser.getState()); @@ -1981,7 +2316,7 @@ public void testBadCR(String eoln) "\r"); handler = new Handler(); - parser = new HttpParser(handler); + parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); assertEquals("Bad EOL", _bad); @@ -2136,21 +2471,26 @@ public void testHostWithOWS(String host) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testMultipleContentLengthWithLargerThenCorrectValue(String eoln) + @MethodSource("scenarios") + public void testMultipleContentLengthWithLargerThenCorrectValue(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "POST / HTTP/1.1" + eoln + - "Content-Length: 2" + eoln + - "Content-Length: 1" + eoln + - "Connection: close" + eoln + - eoln + + "POST / HTTP/1.1" + scenario.eol + + "Content-Length: 2" + scenario.eol + + "Content-Length: 1" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol + "X"); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("POST", _methodOrVersion); assertEquals("Multiple Content-Lengths", _bad); assertFalse(buffer.hasRemaining()); @@ -2161,21 +2501,26 @@ public void testMultipleContentLengthWithLargerThenCorrectValue(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testMultipleContentLengthWithCorrectThenLargerValue(String eoln) + @MethodSource("scenarios") + public void testMultipleContentLengthWithCorrectThenLargerValue(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "POST / HTTP/1.1" + eoln + - "Content-Length: 1" + eoln + - "Content-Length: 2" + eoln + - "Connection: close" + eoln + - eoln + + "POST / HTTP/1.1" + scenario.eol + + "Content-Length: 1" + scenario.eol + + "Content-Length: 2" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol + "X"); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("POST", _methodOrVersion); assertEquals("Multiple Content-Lengths", _bad); assertFalse(buffer.hasRemaining()); @@ -2186,23 +2531,33 @@ public void testMultipleContentLengthWithCorrectThenLargerValue(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testTransferEncodingChunkedThenContentLength(String eoln) + @MethodSource("scenarios") + public void testTransferEncodingChunkedThenContentLength(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "POST /chunk HTTP/1.1" + eoln + - "Host: localhost" + eoln + - "Transfer-Encoding: chunked" + eoln + - "Content-Length: 1" + eoln + - eoln + - "1" + eoln + - "X" + eoln + - "0" + eoln + - eoln); + "POST /chunk HTTP/1.1" + scenario.eol + + "Host: localhost" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + "Content-Length: 1" + scenario.eol + + scenario.eol + + "1" + scenario.eolChunk + + "X" + scenario.eolChunk + + "0" + scenario.eolChunk + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY); + HttpParser parser = new HttpParser(handler, scenario.compliance.with("test", TRANSFER_ENCODING_WITH_CONTENT_LENGTH)); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + if (scenario.expectEarly()) + { + assertTrue(_early); + return; + } assertEquals("POST", _methodOrVersion); assertEquals("/chunk", _uriOrStatus); @@ -2212,27 +2567,37 @@ public void testTransferEncodingChunkedThenContentLength(String eoln) assertTrue(_headerCompleted); assertTrue(_messageCompleted); - assertThat(_complianceViolation, contains(TRANSFER_ENCODING_WITH_CONTENT_LENGTH)); + assertTrue(_complianceViolation.contains(TRANSFER_ENCODING_WITH_CONTENT_LENGTH)); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testContentLengthThenTransferEncodingChunked(String eoln) + @MethodSource("scenarios") + public void testContentLengthThenTransferEncodingChunked(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "POST /chunk HTTP/1.1" + eoln + - "Host: localhost" + eoln + - "Content-Length: 1" + eoln + - "Transfer-Encoding: chunked" + eoln + - eoln + - "1" + eoln + - "X" + eoln + - "0" + eoln + - eoln); + "POST /chunk HTTP/1.1" + scenario.eol + + "Host: localhost" + scenario.eol + + "Content-Length: 1" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + scenario.eol + + "1" + scenario.eolChunk + + "X" + scenario.eolChunk + + "0" + scenario.eolChunk + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY); + HttpParser parser = new HttpParser(handler, scenario.compliance.with("test", TRANSFER_ENCODING_WITH_CONTENT_LENGTH)); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + if (scenario.expectEarly()) + { + assertTrue(_early); + return; + } assertEquals("POST", _methodOrVersion); assertEquals("/chunk", _uriOrStatus); @@ -2242,141 +2607,181 @@ public void testContentLengthThenTransferEncodingChunked(String eoln) assertTrue(_headerCompleted); assertTrue(_messageCompleted); - assertThat(_complianceViolation, contains(TRANSFER_ENCODING_WITH_CONTENT_LENGTH)); + assertTrue(_complianceViolation.contains(TRANSFER_ENCODING_WITH_CONTENT_LENGTH)); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testHost(String eoln) + @MethodSource("scenarios") + public void testHost(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.1" + eoln + - "Host: host" + eoln + - "Connection: close" + eoln + - eoln); + "GET / HTTP/1.1" + scenario.eol + + "Host: host" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("host", _host); assertEquals(URIUtil.UNDEFINED_PORT, _port); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testUriHost11(String eoln) + @MethodSource("scenarios") + public void testUriHost11(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET http://host/ HTTP/1.1" + eoln + - "Connection: close" + eoln + - eoln); + "GET http://host/ HTTP/1.1" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("No Host", _bad); assertEquals("http://host/", _uriOrStatus); assertEquals(0, _port); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testUriHost10(String eoln) + @MethodSource("scenarios") + public void testUriHost10(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET http://host/ HTTP/1.0" + eoln + - eoln); + "GET http://host/ HTTP/1.0" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertNull(_bad); assertEquals("http://host/", _uriOrStatus); assertEquals(0, _port); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testNoHost(String eoln) + @MethodSource("scenarios") + public void testNoHost(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.1" + eoln + - "Connection: close" + eoln + - eoln); + "GET / HTTP/1.1" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("No Host", _bad); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testIPHost(String eoln) + @MethodSource("scenarios") + public void testIPHost(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.1" + eoln + - "Host: 192.168.0.1" + eoln + - "Connection: close" + eoln + - eoln); + "GET / HTTP/1.1" + scenario.eol + + "Host: 192.168.0.1" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("192.168.0.1", _host); assertEquals(URIUtil.UNDEFINED_PORT, _port); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testIPv6Host(String eoln) + @MethodSource("scenarios") + public void testIPv6Host(Scenario scenario) { Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable()); ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.1" + eoln + - "Host: [::1]" + eoln + - "Connection: close" + eoln + - eoln); + "GET / HTTP/1.1" + scenario.eol + + "Host: [::1]" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("[::1]", _host); assertEquals(URIUtil.UNDEFINED_PORT, _port); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testBadIPv6Host(String eoln) + @MethodSource("scenarios") + public void testBadIPv6Host(Scenario scenario) { try (StacklessLogging ignored = new StacklessLogging(HttpParser.class)) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.1" + eoln + - "Host: [::1" + eoln + - "Connection: close" + eoln + - eoln); + "GET / HTTP/1.1" + scenario.eol + + "Host: [::1" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertThat(_bad, containsString("Bad")); } } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testHostPort(String eoln) + @MethodSource("scenarios") + public void testHostPort(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.1" + eoln + - "Host: myhost:8888" + eoln + - "Connection: close" + eoln + - eoln); + "GET / HTTP/1.1" + scenario.eol + + "Host: myhost:8888" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("myhost", _host); assertEquals(8888, _port); } @@ -2526,66 +2931,81 @@ public void testGoodHost(String hostline) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testIPHostPort(String eoln) + @MethodSource("scenarios") + public void testIPHostPort(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.1" + eoln + - "Host: 192.168.0.1:8888" + eoln + - "Connection: close" + eoln + - eoln); + "GET / HTTP/1.1" + scenario.eol + + "Host: 192.168.0.1:8888" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("192.168.0.1", _host); assertEquals(8888, _port); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testIPv6HostPort(String eoln) + @MethodSource("scenarios") + public void testIPv6HostPort(Scenario scenario) { Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable()); ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.1" + eoln + - "Host: [::1]:8888" + eoln + - "Connection: close" + eoln + - eoln); + "GET / HTTP/1.1" + scenario.eol + + "Host: [::1]:8888" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("[::1]", _host); assertEquals(8888, _port); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testEmptyHostPort(String eoln) + @MethodSource("scenarios") + public void testEmptyHostPort(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.1" + eoln + - "Host:" + eoln + - "Connection: close" + eoln + - eoln); + "GET / HTTP/1.1" + scenario.eol + + "Host:" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertNull(_host); assertNull(_bad); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testRequestMaxHeaderBytesURITooLong(String eoln) + @MethodSource("scenarios") + public void testRequestMaxHeaderBytesURITooLong(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET /long/nested/path/uri HTTP/1.1" + eoln + - "Host: example.com" + eoln + - "Connection: close" + eoln + - eoln); + "GET /long/nested/path/uri HTTP/1.1" + scenario.eol + + "Host: example.com" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); int maxHeaderBytes = 5; HttpParser.RequestHandler handler = new Handler(); @@ -2596,15 +3016,15 @@ public void testRequestMaxHeaderBytesURITooLong(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testRequestMaxHeaderBytesCumulative(String eoln) + @MethodSource("scenarios") + public void testRequestMaxHeaderBytesCumulative(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET /nested/path/uri HTTP/1.1" + eoln + - "Host: example.com" + eoln + - "X-Large-Header: lorem-ipsum-dolor-sit" + eoln + - "Connection: close" + eoln + - eoln); + "GET /nested/path/uri HTTP/1.1" + scenario.eol + + "Host: example.com" + scenario.eol + + "X-Large-Header: lorem-ipsum-dolor-sit" + scenario.eol + + "Connection: close" + scenario.eol + + scenario.eol); int maxHeaderBytes = 64; HttpParser.RequestHandler handler = new Handler(); @@ -2615,36 +3035,46 @@ public void testRequestMaxHeaderBytesCumulative(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) + @MethodSource("scenarios") @SuppressWarnings("ReferenceEquality") - public void testInsensitiveCachedField(String eoln) + public void testInsensitiveCachedField(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.1" + eoln + - "Content-Type: text/plain;Charset=UTF-8" + eoln + - eoln); + "GET / HTTP/1.1" + scenario.eol + + "Content-Type: text/plain;Charset=UTF-8" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } HttpField field = _fields.get(0); assertThat(field.getValue(), is("text/plain;charset=utf-8")); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) + @MethodSource("scenarios") @SuppressWarnings("ReferenceEquality") - public void testDynamicCachedField(String eoln) + public void testDynamicCachedField(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.1" + eoln + - "Host: www.smh.com.au" + eoln + - eoln); + "GET / HTTP/1.1" + scenario.eol + + "Host: www.smh.com.au" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("www.smh.com.au", parser.getFieldCache().get("Host: www.smh.com.au").getValue()); HttpField field = _fields.get(0); @@ -2654,21 +3084,26 @@ public void testDynamicCachedField(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testParseRequest(String eoln) + @MethodSource("scenarios") + public void testParseRequest(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "GET / HTTP/1.1" + eoln + - "Host: localhost" + eoln + - "Header1: value1" + eoln + - "Connection: close" + eoln + - "Accept-Encoding: gzip, deflated" + eoln + - "Accept: unknown" + eoln + - eoln); + "GET / HTTP/1.1" + scenario.eol + + "Host: localhost" + scenario.eol + + "Header1: value1" + scenario.eol + + "Connection: close" + scenario.eol + + "Accept-Encoding: gzip, deflated" + scenario.eol + + "Accept: unknown" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertEquals("GET", _methodOrVersion); assertEquals("/", _uriOrStatus); @@ -2684,18 +3119,23 @@ public void testParseRequest(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testHTTP2Preface(String eoln) + @MethodSource("scenarios") + public void testHTTP2Preface(Scenario scenario) { ByteBuffer buffer = BufferUtil.toBuffer( - "PRI * HTTP/2.0" + eoln + - eoln + - "SM" + eoln + - eoln); + "PRI * HTTP/2.0" + scenario.eol + + scenario.eol + + "SM" + scenario.eol + + scenario.eol); HttpParser.RequestHandler handler = new Handler(); - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parseAll(parser, buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(_headerCompleted); assertTrue(_messageCompleted); @@ -2707,8 +3147,8 @@ public void testHTTP2Preface(String eoln) } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testForHTTP09HeaderCompleteTrueDoesNotEmitContentComplete(String eoln) + @MethodSource("scenarios") + public void testForHTTP09HeaderCompleteTrueDoesNotEmitContentComplete(Scenario scenario) { HttpParser.RequestHandler handler = new Handler() { @@ -2721,7 +3161,7 @@ public boolean headerComplete() }; HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY); - ByteBuffer buffer = BufferUtil.toBuffer("GET /path" + eoln); + ByteBuffer buffer = BufferUtil.toBuffer("GET /path" + scenario.eol); boolean handle = parser.parseNext(buffer); assertTrue(handle); assertFalse(buffer.hasRemaining()); @@ -2741,8 +3181,8 @@ public boolean headerComplete() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testForContentLengthZeroHeaderCompleteTrueDoesNotEmitContentComplete(String eoln) + @MethodSource("scenarios") + public void testForContentLengthZeroHeaderCompleteTrueDoesNotEmitContentComplete(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -2753,13 +3193,18 @@ public boolean headerComplete() return true; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 OK" + eoln + - "Content-Length: 0" + eoln + - eoln); + "HTTP/1.1 200 OK" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol); boolean handle = parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(handle); assertFalse(buffer.hasRemaining()); assertFalse(_contentCompleted); @@ -2773,8 +3218,8 @@ public boolean headerComplete() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testForEmptyChunkedContentHeaderCompleteTrueDoesNotEmitContentComplete(String eoln) + @MethodSource("scenarios") + public void testForEmptyChunkedContentHeaderCompleteTrueDoesNotEmitContentComplete(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -2785,15 +3230,20 @@ public boolean headerComplete() return true; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 OK" + eoln + - "Transfer-Encoding: chunked" + eoln + - eoln + - "0" + eoln + - eoln); + "HTTP/1.1 200 OK" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + scenario.eol + + "0" + scenario.eolChunk + + scenario.eol); boolean handle = parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(handle); assertTrue(buffer.hasRemaining()); assertFalse(_contentCompleted); @@ -2801,14 +3251,19 @@ public boolean headerComplete() // Need to parse more to advance the parser. handle = parser.parseNext(buffer); + if (scenario.expectEarly()) + { + assertTrue(_early); + return; + } assertTrue(handle); assertTrue(_contentCompleted); assertTrue(_messageCompleted); } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testForContentLengthZeroContentCompleteTrueDoesNotEmitMessageComplete(String eoln) + @MethodSource("scenarios") + public void testForContentLengthZeroContentCompleteTrueDoesNotEmitMessageComplete(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -2819,13 +3274,20 @@ public boolean contentComplete() return true; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 OK" + eoln + - "Content-Length: 0" + eoln + - eoln); + "HTTP/1.1 200 OK" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol); boolean handle = parser.parseNext(buffer); + + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + assertTrue(handle); assertFalse(buffer.hasRemaining()); assertFalse(_messageCompleted); @@ -2837,8 +3299,8 @@ public boolean contentComplete() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testForEmptyChunkedContentContentCompleteTrueDoesNotEmitMessageComplete(String eoln) + @MethodSource("scenarios") + public void testForEmptyChunkedContentContentCompleteTrueDoesNotEmitMessageComplete(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -2849,15 +3311,25 @@ public boolean contentComplete() return true; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 OK" + eoln + - "Transfer-Encoding: chunked" + eoln + - eoln + - "0" + eoln + - eoln); + "HTTP/1.1 200 OK" + scenario.eol + + "Transfer-Encoding: chunked" + scenario.eol + + scenario.eol + + "0" + scenario.eolChunk + + scenario.eol); boolean handle = parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } + if (scenario.expectEarly()) + { + assertTrue(_early); + return; + } assertTrue(handle); assertTrue(buffer.hasRemaining()); assertFalse(_messageCompleted); @@ -2869,8 +3341,8 @@ public boolean contentComplete() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testHeaderAfterContentLengthZeroContentCompleteTrue(String eoln) + @MethodSource("scenarios") + public void testHeaderAfterContentLengthZeroContentCompleteTrue(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -2881,15 +3353,20 @@ public boolean contentComplete() return true; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); - String header = "Header: Foobar" + eoln; + String header = "Header: Foobar" + scenario.eol; ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 OK" + eoln + - "Content-Length: 0" + eoln + - eoln + + "HTTP/1.1 200 OK" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol + header); boolean handle = parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(handle); assertTrue(buffer.hasRemaining()); assertEquals(header, BufferUtil.toString(buffer)); @@ -2905,8 +3382,8 @@ public boolean contentComplete() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testSmallContentLengthContentCompleteTrue(String eoln) + @MethodSource("scenarios") + public void testSmallContentLengthContentCompleteTrue(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -2917,16 +3394,21 @@ public boolean contentComplete() return true; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); - String header = "Header: Foobar" + eoln; + String header = "Header: Foobar" + scenario.eol; ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 OK" + eoln + - "Content-Length: 1" + eoln + - eoln + + "HTTP/1.1 200 OK" + scenario.eol + + "Content-Length: 1" + scenario.eol + + scenario.eol + "0" + header); boolean handle = parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(handle); assertTrue(buffer.hasRemaining()); assertEquals(header, BufferUtil.toString(buffer)); @@ -2942,8 +3424,8 @@ public boolean contentComplete() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testHeaderAfterSmallContentLengthContentCompleteTrue(String eoln) + @MethodSource("scenarios") + public void testHeaderAfterSmallContentLengthContentCompleteTrue(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -2954,14 +3436,19 @@ public boolean contentComplete() return true; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 OK" + eoln + - "Content-Length: 1" + eoln + - eoln + + "HTTP/1.1 200 OK" + scenario.eol + + "Content-Length: 1" + scenario.eol + + scenario.eol + "0"); boolean handle = parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(handle); assertFalse(buffer.hasRemaining()); assertTrue(_contentCompleted); @@ -2975,8 +3462,8 @@ public boolean contentComplete() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testEOFContentContentCompleteTrue(String eoln) + @MethodSource("scenarios") + public void testEOFContentContentCompleteTrue(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -2987,13 +3474,18 @@ public boolean contentComplete() return true; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 OK" + eoln + - eoln + + "HTTP/1.1 200 OK" + scenario.eol + + scenario.eol + "0"); boolean handle = parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertFalse(handle); assertFalse(buffer.hasRemaining()); assertEquals("0", _content); @@ -3017,8 +3509,8 @@ public boolean contentComplete() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testHEADRequestHeaderCompleteTrue(String eoln) + @MethodSource("scenarios") + public void testHEADRequestHeaderCompleteTrue(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -3036,13 +3528,18 @@ public boolean contentComplete() return true; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); parser.setHeadResponse(true); ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 OK" + eoln + - eoln); + "HTTP/1.1 200 OK" + scenario.eol + + scenario.eol); boolean handle = parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(handle); assertFalse(buffer.hasRemaining()); assertFalse(_contentCompleted); @@ -3063,8 +3560,8 @@ public boolean contentComplete() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testNoContentHeaderCompleteTrue(String eoln) + @MethodSource("scenarios") + public void testNoContentHeaderCompleteTrue(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -3082,13 +3579,18 @@ public boolean contentComplete() return true; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); // HTTP 304 does not have a body. ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 304 Not Modified" + eoln + - eoln); + "HTTP/1.1 304 Not Modified" + scenario.eol + + scenario.eol); boolean handle = parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(handle); assertFalse(buffer.hasRemaining()); assertFalse(_contentCompleted); @@ -3109,8 +3611,8 @@ public boolean contentComplete() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testCRLFAfterResponseHeaderCompleteTrue(String eoln) + @MethodSource("scenarios") + public void testCRLFAfterResponseHeaderCompleteTrue(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -3121,22 +3623,27 @@ public boolean headerComplete() return true; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 304 Not Modified" + eoln + - eoln + - eoln + - eoln + - "HTTP/1.1 200 OK" + eoln + - "Content-Length: 0" + eoln + - eoln + - eoln + - eoln + - "HTTP/1.1 303 See Other" + eoln + - "Content-Length: 0" + eoln + - eoln); + "HTTP/1.1 304 Not Modified" + scenario.eol + + scenario.eol + + scenario.eol + + scenario.eol + + "HTTP/1.1 200 OK" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol + + scenario.eol + + scenario.eol + + "HTTP/1.1 303 See Other" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol); boolean handle = parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(handle); assertTrue(buffer.hasRemaining()); assertEquals("304", _uriOrStatus); @@ -3186,8 +3693,8 @@ public boolean headerComplete() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testCRLFAfterResponseContentCompleteTrue(String eoln) + @MethodSource("scenarios") + public void testCRLFAfterResponseContentCompleteTrue(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -3198,22 +3705,27 @@ public boolean contentComplete() return true; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 304 Not Modified" + eoln + - eoln + - eoln + - eoln + - "HTTP/1.1 200 OK" + eoln + - "Content-Length: 0" + eoln + - eoln + - eoln + - eoln + - "HTTP/1.1 303 See Other" + eoln + - "Content-Length: 0" + eoln + - eoln); + "HTTP/1.1 304 Not Modified" + scenario.eol + + scenario.eol + + scenario.eol + + scenario.eol + + "HTTP/1.1 200 OK" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol + + scenario.eol + + scenario.eol + + "HTTP/1.1 303 See Other" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol); boolean handle = parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertTrue(handle); assertTrue(buffer.hasRemaining()); assertEquals("304", _uriOrStatus); @@ -3260,8 +3772,8 @@ public boolean contentComplete() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testCRLFAfterResponseMessageCompleteFalse(String eoln) + @MethodSource("scenarios") + public void testCRLFAfterResponseMessageCompleteFalse(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -3272,22 +3784,27 @@ public boolean messageComplete() return false; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 304 Not Modified" + eoln + - eoln + - eoln + - eoln + - "HTTP/1.1 200 OK" + eoln + - "Content-Length: 0" + eoln + - eoln + - eoln + - eoln + - "HTTP/1.1 303 See Other" + eoln + - "Content-Length: 0" + eoln + - eoln); + "HTTP/1.1 304 Not Modified" + scenario.eol + + scenario.eol + + scenario.eol + + scenario.eol + + "HTTP/1.1 200 OK" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol + + scenario.eol + + scenario.eol + + "HTTP/1.1 303 See Other" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol); boolean handle = parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertFalse(handle); assertTrue(buffer.hasRemaining()); assertEquals("304", _uriOrStatus); @@ -3316,8 +3833,8 @@ public boolean messageComplete() } @ParameterizedTest - @ValueSource(strings = {"\r\n", "\n"}) - public void testSPAfterResponseMessageCompleteFalse(String eoln) + @MethodSource("scenarios") + public void testSPAfterResponseMessageCompleteFalse(Scenario scenario) { HttpParser.ResponseHandler handler = new Handler() { @@ -3328,16 +3845,21 @@ public boolean messageComplete() return false; } }; - HttpParser parser = new HttpParser(handler); + HttpParser parser = new HttpParser(handler, scenario.compliance); ByteBuffer buffer = BufferUtil.toBuffer( - "HTTP/1.1 304 Not Modified" + eoln + - eoln + + "HTTP/1.1 304 Not Modified" + scenario.eol + + scenario.eol + " " + // Single SP. - "HTTP/1.1 200 OK" + eoln + - "Content-Length: 0" + eoln + - eoln); + "HTTP/1.1 200 OK" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol); boolean handle = parser.parseNext(buffer); + if (scenario.expectBad()) + { + assertThat(_bad, containsString("LF line terminator")); + return; + } assertFalse(handle); assertTrue(buffer.hasRemaining()); assertEquals("304", _uriOrStatus); @@ -3353,14 +3875,14 @@ public boolean messageComplete() assertNotNull(_bad); buffer = BufferUtil.toBuffer( - "HTTP/1.1 200 OK" + eoln + - "Content-Length: 0" + eoln + - eoln + + "HTTP/1.1 200 OK" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol + " " + // Single SP. - "HTTP/1.1 303 See Other" + eoln + - "Content-Length: 0" + eoln + - eoln); - parser = new HttpParser(handler); + "HTTP/1.1 303 See Other" + scenario.eol + + "Content-Length: 0" + scenario.eol + + scenario.eol); + parser = new HttpParser(handler, scenario.compliance); handle = parser.parseNext(buffer); assertFalse(handle); assertTrue(buffer.hasRemaining()); @@ -3487,7 +4009,8 @@ public boolean messageComplete() public void badMessage(HttpException failure) { String reason = failure.getReason(); - _bad = reason == null ? String.valueOf(failure.getCode()) : reason; + if (_bad == null) + _bad = reason == null ? String.valueOf(failure.getCode()) : reason; } @Override @@ -3595,4 +4118,42 @@ public void testHttpHeaderValueParseCsv() assertThat(list, contains(HttpHeaderValue.CLOSE)); assertThat(unknowns, empty()); } + + public static Stream scenarios() + { + List scenarios = new ArrayList<>(); + for (HttpCompliance compliance : new HttpCompliance[] {HttpCompliance.STRICT, HttpCompliance.RFC9110, HttpCompliance.RFC7230, HttpCompliance.RFC2616, HttpCompliance.RFC7230_LEGACY, HttpCompliance.RFC2616_LEGACY}) + for (String eol : new String[] {"\r\n", "\n"}) + for (String eolChunk : new String[] {"\r\n", "\n"}) + scenarios.add(new Scenario(eol, eolChunk, compliance)); + return scenarios.stream(); + } + + public record Scenario(String eol, String eolChunk, HttpCompliance compliance) + { + public boolean isViolation() + { + return !eol.equals("\r\n"); + } + + public boolean isChunkViolation() + { + return !eolChunk.equals("\r\n"); + } + + public boolean expectBad() + { + return isViolation() && !compliance.allows(LF_HEADER_TERMINATION); + } + + public boolean expectEarly() + { + return isChunkViolation() && !compliance.allows(LF_CHUNK_TERMINATION); + } + + public String toString() + { + return "%s[eol=%s, eolChunk=%s, c=%s".formatted(this.getClass().getSimpleName(), isViolation() ? "LF" : "CRLF", isChunkViolation() ? "LF" : "CRLF", compliance.getName()); + } + } } diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty.xml b/jetty-core/jetty-server/src/main/config/etc/jetty.xml index 2d5df23200c8..3acfe12d9a01 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty.xml @@ -63,7 +63,7 @@ - + diff --git a/jetty-core/jetty-server/src/main/config/modules/server.mod b/jetty-core/jetty-server/src/main/config/modules/server.mod index 251a4cb4343c..4094f31a4b2b 100644 --- a/jetty-core/jetty-server/src/main/config/modules/server.mod +++ b/jetty-core/jetty-server/src/main/config/modules/server.mod @@ -80,8 +80,8 @@ etc/jetty.xml # end::documentation-http-config[] # tag::documentation-server-compliance[] -## HTTP Compliance: RFC7230, RFC7230_LEGACY, RFC2616, RFC2616_LEGACY, LEGACY -# jetty.httpConfig.compliance=RFC7230 +## HTTP Compliance: STRICT, RFC9110, RFC7230, RFC7230_LEGACY, RFC2616, RFC2616_LEGACY, LEGACY +# jetty.httpConfig.compliance=RFC9110 ## URI Compliance: DEFAULT, LEGACY, RFC3986, RFC3986_UNAMBIGUOUS, UNSAFE # jetty.httpConfig.uriCompliance=DEFAULT