* Issue #4203 - Updating test to verify report on Transfer-Encoding Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> * Fixes #4203 - Transfer-Encoding + Content-Length is 400 Bad Request + Fixing validation to not be header order dependent. Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> * Issue #4203 - Fixing hasTransferEncoding reset and testcase assumption Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> * Issue #4204 - Transfer-Encoding RFC7230 behaviors + More test cases and implementation. Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> * Issue #4204 - Transfer-Encoding RFC7230 behaviors + Adjusting HttpParser to handle the case where we have multiple Transfer-Encoding headers and none declare the 'chunked' token. Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> * Issue #4204 - Transfer-Encoding RFC7230 behaviors + Making changes from PR review Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com> * Issue #4203 Transfer Encoding request with TE and no chunking is a Bad Request Signed-off-by: Greg Wilkins <gregw@webtide.com>
This commit is contained in:
parent
3d19f61122
commit
890c0b26cb
|
@ -169,6 +169,7 @@ public class HttpParser
|
|||
private Utf8StringBuilder _uri = new Utf8StringBuilder(INITIAL_URI_LENGTH); // Tune?
|
||||
private EndOfContent _endOfContent;
|
||||
private boolean _hasContentLength;
|
||||
private boolean _hasTransferEncoding;
|
||||
private long _contentLength = -1;
|
||||
private long _contentPosition;
|
||||
private int _chunkLength;
|
||||
|
@ -955,6 +956,9 @@ public class HttpParser
|
|||
switch (_header)
|
||||
{
|
||||
case CONTENT_LENGTH:
|
||||
if (_hasTransferEncoding && complianceViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH))
|
||||
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Transfer-Encoding and Content-Length");
|
||||
|
||||
if (_hasContentLength)
|
||||
{
|
||||
if (complianceViolation(MULTIPLE_CONTENT_LENGTHS))
|
||||
|
@ -964,9 +968,6 @@ public class HttpParser
|
|||
}
|
||||
_hasContentLength = true;
|
||||
|
||||
if (_endOfContent == EndOfContent.CHUNKED_CONTENT && complianceViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH))
|
||||
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Content-Length");
|
||||
|
||||
if (_endOfContent != EndOfContent.CHUNKED_CONTENT)
|
||||
{
|
||||
_contentLength = convertContentLength(_valueString);
|
||||
|
@ -978,9 +979,15 @@ public class HttpParser
|
|||
break;
|
||||
|
||||
case TRANSFER_ENCODING:
|
||||
_hasTransferEncoding = true;
|
||||
|
||||
if (_hasContentLength && complianceViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH))
|
||||
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Transfer-Encoding and Content-Length");
|
||||
|
||||
// we encountered another Transfer-Encoding header, but chunked was already set
|
||||
if (_endOfContent == EndOfContent.CHUNKED_CONTENT)
|
||||
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, chunked not last");
|
||||
|
||||
if (HttpHeaderValue.CHUNKED.is(_valueString))
|
||||
{
|
||||
_endOfContent = EndOfContent.CHUNKED_CONTENT;
|
||||
|
@ -989,15 +996,26 @@ public class HttpParser
|
|||
else
|
||||
{
|
||||
List<String> values = new QuotedCSV(_valueString).getValues();
|
||||
if (!values.isEmpty() && HttpHeaderValue.CHUNKED.is(values.get(values.size() - 1)))
|
||||
int chunked = -1;
|
||||
int len = values.size();
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
_endOfContent = EndOfContent.CHUNKED_CONTENT;
|
||||
_contentLength = -1;
|
||||
if (HttpHeaderValue.CHUNKED.is(values.get(i)))
|
||||
{
|
||||
if (chunked != -1)
|
||||
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, multiple chunked tokens");
|
||||
chunked = i;
|
||||
// declared chunked
|
||||
_endOfContent = EndOfContent.CHUNKED_CONTENT;
|
||||
_contentLength = -1;
|
||||
}
|
||||
// we have a non-chunked token after a declared chunked token
|
||||
else if (_endOfContent == EndOfContent.CHUNKED_CONTENT)
|
||||
{
|
||||
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, chunked not last");
|
||||
}
|
||||
}
|
||||
else if (values.stream().anyMatch(HttpHeaderValue.CHUNKED::is))
|
||||
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad chunking");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case HOST:
|
||||
|
@ -1139,6 +1157,17 @@ public class HttpParser
|
|||
return _handler.messageComplete();
|
||||
}
|
||||
|
||||
// We found Transfer-Encoding headers, but none declared the 'chunked' token
|
||||
if (_hasTransferEncoding && _endOfContent != EndOfContent.CHUNKED_CONTENT)
|
||||
{
|
||||
if (_responseHandler == null || _endOfContent != EndOfContent.EOF_CONTENT)
|
||||
{
|
||||
// Transfer-Encoding chunked not specified
|
||||
// https://tools.ietf.org/html/rfc7230#section-3.3.1
|
||||
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, chunked not last");
|
||||
}
|
||||
}
|
||||
|
||||
// Was there a required host header?
|
||||
if (!_host && _version == HttpVersion.HTTP_1_1 && _requestHandler != null)
|
||||
{
|
||||
|
@ -1818,6 +1847,7 @@ public class HttpParser
|
|||
_endOfContent = EndOfContent.UNKNOWN_CONTENT;
|
||||
_contentLength = -1;
|
||||
_hasContentLength = false;
|
||||
_hasTransferEncoding = false;
|
||||
_contentPosition = 0;
|
||||
_responseStatus = 0;
|
||||
_contentChunk = null;
|
||||
|
|
|
@ -1050,7 +1050,7 @@ public class HttpParserTest
|
|||
assertEquals("GET", _methodOrVersion);
|
||||
assertEquals("/chunk", _uriOrStatus);
|
||||
assertEquals("HTTP/1.0", _versionOrReason);
|
||||
assertThat(_bad, containsString("Bad chunking"));
|
||||
assertThat(_bad, containsString("Bad Transfer-Encoding"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -30,10 +30,13 @@ import java.io.IOException;
|
|||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringReader;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
@ -54,6 +57,9 @@ import org.hamcrest.Matchers;
|
|||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
@ -263,43 +269,172 @@ public class HttpConnectionTest
|
|||
}
|
||||
}
|
||||
|
||||
static final int CHUNKED = -1;
|
||||
static final int DQUOTED_CHUNKED = -2;
|
||||
static final int BAD_CHUNKED = -3;
|
||||
static final int UNKNOWN_TE = -4;
|
||||
|
||||
public static Stream<Arguments> http11ContentLengthAndChunkedData()
|
||||
{
|
||||
return Stream.of(
|
||||
Arguments.of(new int[]{CHUNKED, 8}),
|
||||
Arguments.of(new int[]{8, CHUNKED}),
|
||||
Arguments.of(new int[]{8, CHUNKED, 8}),
|
||||
Arguments.of(new int[]{DQUOTED_CHUNKED, 8}),
|
||||
Arguments.of(new int[]{8, DQUOTED_CHUNKED}),
|
||||
Arguments.of(new int[]{8, DQUOTED_CHUNKED, 8}),
|
||||
Arguments.of(new int[]{BAD_CHUNKED, 8}),
|
||||
Arguments.of(new int[]{8, BAD_CHUNKED}),
|
||||
Arguments.of(new int[]{8, BAD_CHUNKED, 8}),
|
||||
Arguments.of(new int[]{UNKNOWN_TE, 8}),
|
||||
Arguments.of(new int[]{8, UNKNOWN_TE}),
|
||||
Arguments.of(new int[]{8, UNKNOWN_TE, 8}),
|
||||
Arguments.of(new int[]{8, UNKNOWN_TE, CHUNKED, DQUOTED_CHUNKED, BAD_CHUNKED, 8})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* More then 1 Content-Length is a bad requests per HTTP rfcs.
|
||||
*/
|
||||
@Test
|
||||
public void testHttp11ContentLengthAndChunk() throws Exception
|
||||
@ParameterizedTest
|
||||
@MethodSource("http11ContentLengthAndChunkedData")
|
||||
public void testHttp11ContentLengthAndChunk(int[] contentLengths) throws Exception
|
||||
{
|
||||
HttpParser.LOG.info("badMessage: 400 Bad messages EXPECTED...");
|
||||
int[][] contentLengths = {
|
||||
{-1, 8},
|
||||
{8, -1},
|
||||
{8, -1, 8},
|
||||
};
|
||||
|
||||
for (int x = 0; x < contentLengths.length; x++)
|
||||
StringBuilder request = new StringBuilder();
|
||||
request.append("POST / HTTP/1.1\r\n");
|
||||
request.append("Host: local\r\n");
|
||||
for (int n = 0; n < contentLengths.length; n++)
|
||||
{
|
||||
StringBuilder request = new StringBuilder();
|
||||
request.append("POST /?id=").append(Integer.toString(x)).append(" HTTP/1.1\r\n");
|
||||
request.append("Host: local\r\n");
|
||||
int[] clen = contentLengths[x];
|
||||
for (int n = 0; n < clen.length; n++)
|
||||
switch (contentLengths[n])
|
||||
{
|
||||
if (clen[n] == -1)
|
||||
case CHUNKED:
|
||||
request.append("Transfer-Encoding: chunked\r\n");
|
||||
else
|
||||
request.append("Content-Length: ").append(Integer.toString(clen[n])).append("\r\n");
|
||||
break;
|
||||
case DQUOTED_CHUNKED:
|
||||
request.append("Transfer-Encoding: \"chunked\"\r\n");
|
||||
break;
|
||||
case BAD_CHUNKED:
|
||||
request.append("Transfer-Encoding: 'chunked'\r\n");
|
||||
break;
|
||||
case UNKNOWN_TE:
|
||||
request.append("Transfer-Encoding: bogus\r\n");
|
||||
break;
|
||||
default:
|
||||
request.append("Content-Length: ").append(contentLengths[n]).append("\r\n");
|
||||
break;
|
||||
}
|
||||
request.append("Content-Type: text/plain\r\n");
|
||||
request.append("Connection: close\r\n");
|
||||
request.append("\r\n");
|
||||
request.append("8;\r\n"); // chunk header
|
||||
request.append("abcdefgh"); // actual content of 8 bytes
|
||||
request.append("\r\n0;\r\n"); // last chunk
|
||||
|
||||
String rawResponse = connector.getResponse(request.toString());
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
assertThat("Response.status", response.getStatus(), is(HttpServletResponse.SC_BAD_REQUEST));
|
||||
}
|
||||
request.append("Content-Type: text/plain\r\n");
|
||||
request.append("\r\n");
|
||||
request.append("8;\r\n"); // chunk header
|
||||
request.append("abcdefgh"); // actual content of 8 bytes
|
||||
request.append("\r\n0;\r\n\r\n"); // last chunk
|
||||
|
||||
String rawResponse = connector.getResponse(request.toString());
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
assertThat("Response.status", response.getStatus(), is(HttpServletResponse.SC_BAD_REQUEST));
|
||||
}
|
||||
|
||||
/**
|
||||
* Examples of valid Chunked behaviors.
|
||||
*/
|
||||
public static Stream<Arguments> http11TransferEncodingChunked()
|
||||
{
|
||||
return Stream.of(
|
||||
Arguments.of(Arrays.asList("chunked, ")), // results in 1 entry
|
||||
Arguments.of(Arrays.asList(", chunked")),
|
||||
|
||||
// invalid tokens with chunked as last
|
||||
// no conflicts, chunked token is specified and is last, will result in chunked
|
||||
Arguments.of(Arrays.asList("bogus, chunked")),
|
||||
Arguments.of(Arrays.asList("'chunked', chunked")), // apostrophe characters with and without
|
||||
Arguments.of(Arrays.asList("identity, chunked")), // identity was removed in RFC2616 errata and has been dropped in RFC7230
|
||||
|
||||
// multiple headers
|
||||
Arguments.of(Arrays.asList("identity", "chunked")), // 2 separate headers
|
||||
Arguments.of(Arrays.asList("", "chunked")) // 2 separate headers
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Chunked Transfer-Encoding behavior indicated by
|
||||
* https://tools.ietf.org/html/rfc7230#section-3.3.1
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("http11TransferEncodingChunked")
|
||||
public void testHttp11TransferEncodingChunked(List<String> tokens) throws Exception
|
||||
{
|
||||
StringBuilder request = new StringBuilder();
|
||||
request.append("POST / HTTP/1.1\r\n");
|
||||
request.append("Host: local\r\n");
|
||||
tokens.forEach((token) -> request.append("Transfer-Encoding: ").append(token).append("\r\n"));
|
||||
request.append("Content-Type: text/plain\r\n");
|
||||
request.append("\r\n");
|
||||
request.append("8;\r\n"); // chunk header
|
||||
request.append("abcdefgh"); // actual content of 8 bytes
|
||||
request.append("\r\n0;\r\n\r\n"); // last chunk
|
||||
|
||||
System.out.println(request.toString());
|
||||
|
||||
String rawResponse = connector.getResponse(request.toString());
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
assertThat("Response.status (" + response.getReason() + ")", response.getStatus(), is(HttpServletResponse.SC_OK));
|
||||
}
|
||||
|
||||
public static Stream<Arguments> http11TransferEncodingInvalidChunked()
|
||||
{
|
||||
return Stream.of(
|
||||
// == Results in 400 Bad Request
|
||||
Arguments.of(Arrays.asList("bogus", "identity")), // 2 separate headers
|
||||
|
||||
Arguments.of(Arrays.asList("bad")),
|
||||
Arguments.of(Arrays.asList("identity")), // identity was removed in RFC2616 errata and has been dropped in RFC7230
|
||||
Arguments.of(Arrays.asList("'chunked'")), // apostrophe characters
|
||||
Arguments.of(Arrays.asList("`chunked`")), // backtick "quote" characters
|
||||
Arguments.of(Arrays.asList("[chunked]")), // bracketed (seen as mistake in several REST libraries)
|
||||
Arguments.of(Arrays.asList("{chunked}")), // json'd (seen as mistake in several REST libraries)
|
||||
Arguments.of(Arrays.asList("\u201Cchunked\u201D")), // opening and closing (fancy) double quotes characters
|
||||
|
||||
// invalid tokens with chunked not as last
|
||||
Arguments.of(Arrays.asList("chunked, bogus")),
|
||||
Arguments.of(Arrays.asList("chunked, 'chunked'")),
|
||||
Arguments.of(Arrays.asList("chunked, identity")),
|
||||
Arguments.of(Arrays.asList("chunked, identity, chunked")), // duplicate chunked
|
||||
Arguments.of(Arrays.asList("chunked", "identity")), // 2 separate header lines
|
||||
|
||||
// multiple chunked tokens present
|
||||
Arguments.of(Arrays.asList("chunked", "identity", "chunked")), // 3 separate header lines
|
||||
Arguments.of(Arrays.asList("chunked", "chunked")), // 2 separate header lines
|
||||
Arguments.of(Arrays.asList("chunked, chunked")) // on same line
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test bad Transfer-Encoding behavior as indicated by
|
||||
* https://tools.ietf.org/html/rfc7230#section-3.3.1
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("http11TransferEncodingInvalidChunked")
|
||||
public void testHttp11TransferEncodingInvalidChunked(List<String> tokens) throws Exception
|
||||
{
|
||||
HttpParser.LOG.info("badMessage: 400 Bad messages EXPECTED...");
|
||||
StringBuilder request = new StringBuilder();
|
||||
request.append("POST / HTTP/1.1\r\n");
|
||||
request.append("Host: local\r\n");
|
||||
tokens.forEach((token) -> request.append("Transfer-Encoding: ").append(token).append("\r\n"));
|
||||
request.append("Content-Type: text/plain\r\n");
|
||||
request.append("\r\n");
|
||||
request.append("8;\r\n"); // chunk header
|
||||
request.append("abcdefgh"); // actual content of 8 bytes
|
||||
request.append("\r\n0;\r\n\r\n"); // last chunk
|
||||
|
||||
System.out.println(request.toString());
|
||||
|
||||
String rawResponse = connector.getResponse(request.toString());
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
assertThat("Response.status", response.getStatus(), is(HttpServletResponse.SC_BAD_REQUEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -541,11 +676,10 @@ public class HttpConnectionTest
|
|||
"Host: localhost\r\n" +
|
||||
"Transfer-Encoding: chunked\r\n" +
|
||||
"Content-Type: text/plain\r\n" +
|
||||
"Connection: close\r\n" +
|
||||
"\r\n" +
|
||||
"A\r\n" +
|
||||
"0123456789\r\n" +
|
||||
"0\r\n");
|
||||
"0\r\n\r\n");
|
||||
|
||||
int offset = 0;
|
||||
offset = checkContains(response, offset, "HTTP/1.1 200");
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.junit.jupiter.api.Test;
|
|||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
@ -348,12 +349,10 @@ public class PartialRFC2616Test
|
|||
"\n");
|
||||
offset = 0;
|
||||
response = endp.getResponse();
|
||||
offset = checkContains(response, offset, "HTTP/1.1 200 OK", "2. identity") + 10;
|
||||
offset = checkContains(response, offset, "/R1", "2. identity") + 3;
|
||||
offset = checkContains(response, offset, "HTTP/1.1 400 ", "2. identity") + 10;
|
||||
offset = 0;
|
||||
response = endp.getResponse();
|
||||
offset = checkContains(response, offset, "HTTP/1.1 200 OK", "2. identity") + 10;
|
||||
offset = checkContains(response, offset, "/R2", "2. identity") + 3;
|
||||
assertThat("There should be no next response as first one closed connection", response, is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -385,7 +384,7 @@ public class PartialRFC2616Test
|
|||
"\n" +
|
||||
"abcdef");
|
||||
response = endp.getResponse();
|
||||
offset = checkContains(response, offset, "HTTP/1.1 400 Bad", "3. ignore c-l") + 1;
|
||||
offset = checkContains(response, offset, "HTTP/1.1 400 ", "3. ignore c-l") + 1;
|
||||
checkNotContained(response, offset, "/R2", "3. _content-length");
|
||||
}
|
||||
|
||||
|
|
|
@ -368,20 +368,11 @@ public abstract class RFC2616BaseTest
|
|||
req1.append("\n");
|
||||
req1.append("123\r\n");
|
||||
|
||||
req1.append("GET /echo/R2 HTTP/1.1\n");
|
||||
req1.append("Host: localhost\n");
|
||||
req1.append("Connection: close\n");
|
||||
req1.append("\n");
|
||||
|
||||
List<HttpTester.Response> responses = http.requests(req1);
|
||||
assertEquals(2, responses.size(), "Response Count");
|
||||
assertEquals(1, responses.size(), "Response Count");
|
||||
|
||||
HttpTester.Response response = responses.get(0);
|
||||
assertThat("4.4.2 Message Length / Response Code", response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat("4.4.2 Message Length / Body", response.getContent(), Matchers.containsString("123\n"));
|
||||
response = responses.get(1);
|
||||
assertThat("4.4.2 Message Length / Response Code", response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertEquals("", response.getContent(), "4.4.2 Message Length / No Body");
|
||||
assertThat("4.4.2 Message Length / Response Code", response.getStatus(), is(HttpStatus.BAD_REQUEST_400));
|
||||
|
||||
// 4.4.3 -
|
||||
// Client - do not send 'Content-Length' if entity-length
|
||||
|
|
Loading…
Reference in New Issue