Skip to content

Commit afc52e1

Browse files
committed
Cleanup
1 parent 89362fe commit afc52e1

File tree

7 files changed

+83
-72
lines changed

7 files changed

+83
-72
lines changed

src/main/java/io/fusionauth/http/server/HTTPRequest.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -626,12 +626,16 @@ public void setURLParameters(Map<String, List<String>> parameters) {
626626
* {@code Content-Length} header was provided.
627627
*/
628628
public boolean hasBody() {
629+
if (isChunked()) {
630+
return true;
631+
}
632+
629633
Long contentLength = getContentLength();
630-
return isChunked() || (contentLength != null && contentLength > 0);
634+
return contentLength != null && contentLength > 0;
631635
}
632636

633637
public boolean isChunked() {
634-
return getTransferEncoding() != null && getTransferEncoding().equalsIgnoreCase(TransferEncodings.Chunked);
638+
return TransferEncodings.Chunked.equalsIgnoreCase(getTransferEncoding());
635639
}
636640

637641
/**

src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,6 @@ private Integer validatePreamble(HTTPRequest request) {
418418
// However, as long as we ignore Content-Length we should be ok. Earlier specs indicate Transfer-Encoding should take precedence,
419419
// later specs imply it is an error. Seems ok to allow it and just ignore it.
420420
if (request.getHeader(Headers.TransferEncoding) == null) {
421-
var contentLength = request.getContentLength();
422421
var requestedContentLengthHeaders = request.getHeaders(Headers.ContentLength);
423422
if (requestedContentLengthHeaders != null) {
424423
if (requestedContentLengthHeaders.size() != 1) {
@@ -430,6 +429,7 @@ private Integer validatePreamble(HTTPRequest request) {
430429
return Status.BadRequest;
431430
}
432431

432+
var contentLength = request.getContentLength();
433433
if (contentLength == null || contentLength < 0) {
434434
if (debugEnabled) {
435435
logger.debug("Invalid request. The Content-Length must be >= 0 and <= 9,223,372,036,854,775,807. [{}]", requestedContentLengthHeaders.getFirst());
@@ -445,13 +445,9 @@ private Integer validatePreamble(HTTPRequest request) {
445445
request.removeHeader(Headers.ContentLength);
446446
}
447447

448-
// Content-Encoding
448+
// Validate Content-Encoding, we currently support deflate and gzip.
449+
// - If we see anything else we should fail, we will be unable to handle the request.
449450
var contentEncodings = request.getContentEncodings();
450-
// TODO : If provided, I think we can safely remove the Content-Length header if present?
451-
// Although, I suppose as long as we decompress early, we should still be able to use the Content-Length header as long as
452-
// it represents the un-compressed payload.
453-
// Maybe write a test to prove that we can compress with a fixed-length w/ a Content-Length header?
454-
// Current support is for gzip and deflate.
455451
for (var encoding : contentEncodings) {
456452
if (!encoding.equalsIgnoreCase(ContentEncodings.Gzip) && !encoding.equalsIgnoreCase(ContentEncodings.Deflate)) {
457453
// Note that while we do not expect multiple Content-Encoding headers, the last one will be used. For good measure,

src/main/java/io/fusionauth/http/server/io/HTTPInputStream.java

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -159,28 +159,24 @@ public int read(byte[] b, int off, int len) throws IOException {
159159
private void initialize() throws IOException {
160160
initialized = true;
161161

162-
// Note that isChunked() should take precedence over the fact that we have a Content-Length.
163-
// - The client should not send both, but in the case they are both present we ignore Content-Length
164-
// In practice, we will remove the Content-Length header when sent in addition to Transfer-Encoding. See HTTPWorker.validatePreamble.
165-
Long contentLength = request.getContentLength();
166-
boolean hasBody = (contentLength != null && contentLength > 0) || request.isChunked();
167-
if (!hasBody) {
168-
delegate = InputStream.nullInputStream();
169-
} else if (request.isChunked()) {
170-
logger.trace("Client indicated it was sending an entity-body in the request. Handling body using chunked encoding.");
171-
delegate = new ChunkedInputStream(pushbackInputStream, chunkedBufferSize);
172-
if (instrumenter != null) {
173-
instrumenter.chunkedRequest();
162+
// hasBody means we are either using chunked transfer encoding or we have a non-zero Content-Length.
163+
boolean hasBody = request.hasBody();
164+
if (hasBody) {
165+
Long contentLength = request.getContentLength();
166+
// Transfer-Encoding always takes precedence over Content-Length. In practice if they were to both be present on
167+
// the request we would have removed Content-Length during validation to remove ambiguity. See HTTPWorker.validatePreamble.
168+
if (request.isChunked()) {
169+
logger.trace("Client indicated it was sending an entity-body in the request. Handling body using chunked encoding.");
170+
delegate = new ChunkedInputStream(pushbackInputStream, chunkedBufferSize);
171+
if (instrumenter != null) {
172+
instrumenter.chunkedRequest();
173+
}
174+
} else {
175+
logger.trace("Client indicated it was sending an entity-body in the request. Handling body using Content-Length header {}.", contentLength);
176+
delegate = new FixedLengthInputStream(pushbackInputStream, contentLength);
174177
}
175-
} else if (contentLength != null) {
176-
logger.trace("Client indicated it was sending an entity-body in the request. Handling body using Content-Length header {}.", contentLength);
177-
delegate = new FixedLengthInputStream(pushbackInputStream, contentLength);
178-
} else {
179-
logger.trace("Client indicated it was NOT sending an entity-body in the request");
180-
}
181178

182-
// Now that we have the InputStream set up to read the body, handle decompression.
183-
if (hasBody) {
179+
// Now that we have the InputStream set up to read the body, handle decompression.
184180
// The request may contain more than one value, apply in reverse order.
185181
// - These are both using the default 512 buffer size.
186182
for (String contentEncoding : request.getContentEncodings().reversed()) {
@@ -190,15 +186,23 @@ private void initialize() throws IOException {
190186
delegate = new GZIPInputStream(delegate);
191187
}
192188
}
193-
}
194189

195-
// If we have a fixed length request that is reporting a contentLength larger than the configured maximum, fail earl.
196-
// - Do this last so if anyone downstream wants to read from the InputStream it would work.
197-
// - Note that it is possible that the body is compressed which would mean the contentLength represents the compressed value.
198-
// But when we decompress the bytes the result will be larger than the reported contentLength, so we can safely throw this exception.
199-
if (contentLength != null && maximumContentLength != -1 && contentLength > maximumContentLength) {
200-
String detailedMessage = "The maximum request size has been exceeded. The reported Content-Length is [" + contentLength + "] and the maximum request size is [" + maximumContentLength + "] bytes.";
201-
throw new ContentTooLargeException(maximumContentLength, detailedMessage);
190+
// If we have a fixed length request that is reporting a contentLength larger than the configured maximum, fail early.
191+
// - Do this last so if anyone downstream wants to read from the InputStream it would work.
192+
// - Note that it is possible that the body is compressed which would mean the contentLength represents the compressed value.
193+
// But when we decompress the bytes the result will be larger than the reported contentLength, so we can safely throw this exception.
194+
if (contentLength != null && maximumContentLength != -1 && contentLength > maximumContentLength) {
195+
String detailedMessage = "The maximum request size has been exceeded. The reported Content-Length is [" + contentLength + "] and the maximum request size is [" + maximumContentLength + "] bytes.";
196+
throw new ContentTooLargeException(maximumContentLength, detailedMessage);
197+
}
198+
} else {
199+
// This means that we did not find Content-Length or Transfer-Encoding on the request. Do not attempt to read from the InputStream.
200+
// - Note that the spec indicates it is plausible for a client to send an entity body and omit these two headers and the server can optionally
201+
// read bytes until the end of the InputStream is reached. This would assume Connection: close was also sent because if we do not know
202+
// how to delimit the request we cannot use a persistent connection.
203+
// - We aren't doing any of that - if the client wants to send bytes, it needs to send a Content-Length header, or specify Transfer-Encoding: chunked.
204+
logger.trace("Client indicated it was NOT sending an entity-body in the request");
205+
delegate = InputStream.nullInputStream();
202206
}
203207
}
204208
}

src/test/java/io/fusionauth/http/BaseSocketTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ private void assertResponse(String request, String chunkedExtension, String resp
6767

6868
if (request.contains("Transfer-Encoding: chunked")) {
6969
// Chunk in 100 byte increments. Using a smaller chunk size to ensure we don't end up with a single chunk.
70-
body = new String(chunkEncoded(body.getBytes(StandardCharsets.UTF_8), 100, chunkedExtension));
70+
body = new String(chunkEncode(body.getBytes(StandardCharsets.UTF_8), 100, chunkedExtension));
7171
}
7272

7373
request = request.replace("{body}", body);

src/test/java/io/fusionauth/http/BaseTest.java

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -105,21 +105,6 @@
105105
* @author Brian Pontarelli
106106
*/
107107
public abstract class BaseTest {
108-
protected byte[] compressUsingContentEncoding(byte[] bytes, String contentEncoding) throws Exception {
109-
if (!contentEncoding.isEmpty()) {
110-
var requestEncodings = contentEncoding.toLowerCase().trim().split(",");
111-
for (String part : requestEncodings) {
112-
String encoding = part.trim();
113-
if (encoding.equals(ContentEncodings.Deflate)) {
114-
bytes = deflate(bytes);
115-
} else if (encoding.equals(ContentEncodings.Gzip)) {
116-
bytes = gzip(bytes);
117-
}
118-
}
119-
}
120-
121-
return bytes;
122-
}
123108
/**
124109
* This timeout is used for the HttpClient during each test. If you are in a debugger, you will need to change this timeout to be much
125110
* larger, otherwise, the client might truncate the request to the server.
@@ -266,11 +251,11 @@ public Object[][] connections() {
266251
@DataProvider(name = "contentEncoding")
267252
public Object[][] contentEncoding() {
268253
return new Object[][]{
269-
{""},
270-
{"gzip"},
271-
{"deflate"},
272-
{"gzip, deflate"},
273-
{"deflate, gzip"}
254+
{""}, // No compression
255+
{"gzip"}, // gzip only
256+
{"deflate"}, // deflate only
257+
{"gzip, deflate"}, // gzip, then deflate
258+
{"deflate, gzip"} // deflate, then gzip
274259
};
275260
}
276261

@@ -460,7 +445,7 @@ protected void assertHTTPResponseEquals(Socket socket, String expectedResponse)
460445
}
461446
}
462447

463-
protected byte[] chunkEncoded(byte[] bytes, int chunkSize, String chunkedExtension) throws IOException {
448+
protected byte[] chunkEncode(byte[] bytes, int chunkSize, String chunkedExtension) throws IOException {
464449
ByteArrayOutputStream out = new ByteArrayOutputStream();
465450
for (var i = 0; i < bytes.length; i += chunkSize) {
466451
var endIndex = Math.min(i + chunkSize, bytes.length);
@@ -485,6 +470,22 @@ protected byte[] chunkEncoded(byte[] bytes, int chunkSize, String chunkedExtensi
485470
return out.toByteArray();
486471
}
487472

473+
protected byte[] compressUsingContentEncoding(byte[] bytes, String contentEncoding) throws Exception {
474+
if (!contentEncoding.isEmpty()) {
475+
var requestEncodings = contentEncoding.toLowerCase().trim().split(",");
476+
for (String part : requestEncodings) {
477+
String encoding = part.trim();
478+
if (encoding.equals(ContentEncodings.Deflate)) {
479+
bytes = deflate(bytes);
480+
} else if (encoding.equals(ContentEncodings.Gzip)) {
481+
bytes = gzip(bytes);
482+
}
483+
}
484+
}
485+
486+
return bytes;
487+
}
488+
488489
protected byte[] deflate(byte[] bytes) throws Exception {
489490
ByteArrayOutputStream baseOutputStream = new ByteArrayOutputStream();
490491
try (DeflaterOutputStream out = new DeflaterOutputStream(baseOutputStream, true)) {

src/test/java/io/fusionauth/http/FormDataTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ public Builder expectResponse(String response) throws Exception {
286286

287287
// Convert body to chunked
288288
// - Using a small chunk to ensure we end up with more than one chunk.
289-
body = new String(chunkEncoded(body.getBytes(StandardCharsets.UTF_8), 100, null));
289+
body = new String(chunkEncode(body.getBytes(StandardCharsets.UTF_8), 100, null));
290290
} else {
291291
var contentLength = body.getBytes(StandardCharsets.UTF_8).length;
292292
if (contentLength > 0) {

src/test/java/io/fusionauth/http/io/HTTPInputStreamTest.java

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,10 @@
2020
import java.nio.charset.StandardCharsets;
2121

2222
import io.fusionauth.http.BaseTest;
23-
import io.fusionauth.http.HTTPValues.ContentEncodings;
2423
import io.fusionauth.http.HTTPValues.Headers;
2524
import io.fusionauth.http.server.HTTPRequest;
2625
import io.fusionauth.http.server.HTTPServerConfiguration;
2726
import io.fusionauth.http.server.io.HTTPInputStream;
28-
import org.testng.annotations.DataProvider;
2927
import org.testng.annotations.Test;
3028
import static org.testng.Assert.assertEquals;
3129

@@ -37,6 +35,7 @@ public class HTTPInputStreamTest extends BaseTest {
3735
public void read_chunked_withPushback(String contentEncoding) throws Exception {
3836
// Ensure that when we read a chunked encoded body that the InputStream returns the correct number of bytes read even when
3937
// we read past the end of the current request and use the PushbackInputStream.
38+
// - Test with optional compression as well.
4039

4140
String content = "These pretzels are making me thirsty. These pretzels are making me thirsty. These pretzels are making me thirsty.";
4241
byte[] payload = content.getBytes(StandardCharsets.UTF_8);
@@ -46,26 +45,29 @@ public void read_chunked_withPushback(String contentEncoding) throws Exception {
4645
payload = compressUsingContentEncoding(payload, contentEncoding);
4746

4847
// Chunk the content, add part of the next request
49-
payload = chunkEncoded(payload, 38, null);
48+
payload = chunkEncode(payload, 38, null);
5049

5150
ByteArrayOutputStream out = new ByteArrayOutputStream();
5251
out.write(payload);
5352

5453
// Add part of the next request
55-
out.write("GET / HTTP/1.1\r\n".getBytes(StandardCharsets.UTF_8));
54+
String nextRequest = "GET / HTTP/1.1\n\r";
55+
byte[] nextRequestBytes = nextRequest.getBytes(StandardCharsets.UTF_8);
56+
out.write(nextRequestBytes);
5657

5758
HTTPRequest request = new HTTPRequest();
5859
request.setHeader(Headers.ContentEncoding, contentEncoding);
5960
request.setHeader(Headers.TransferEncoding, "chunked");
6061

6162
byte[] bytes = out.toByteArray();
62-
assertReadWithPushback(bytes, content, contentLength, 16, request);
63+
assertReadWithPushback(bytes, content, contentLength, nextRequestBytes, request);
6364
}
6465

6566
@Test(dataProvider = "contentEncoding")
6667
public void read_fixedLength_withPushback(String contentEncoding) throws Exception {
6768
// Ensure that when we read a fixed length body that the InputStream returns the correct number of bytes read even when
6869
// we read past the end of the current request and use the PushbackInputStream.
70+
// - Test with optional compression as well.
6971

7072
// Fixed length body with the start of the next request in the buffer
7173
String content = "These pretzels are making me thirsty. These pretzels are making me thirsty. These pretzels are making me thirsty.";
@@ -82,7 +84,9 @@ public void read_fixedLength_withPushback(String contentEncoding) throws Excepti
8284
out.write(payload);
8385

8486
// Add part of the next request
85-
out.write("GET / HTTP/1.1\r\n".getBytes(StandardCharsets.UTF_8));
87+
String nextRequest = "GET / HTTP/1.1\n\r";
88+
byte[] nextRequestBytes = nextRequest.getBytes(StandardCharsets.UTF_8);
89+
out.write(nextRequestBytes);
8690

8791
HTTPRequest request = new HTTPRequest();
8892
request.setHeader(Headers.ContentEncoding, contentEncoding);
@@ -91,10 +95,10 @@ public void read_fixedLength_withPushback(String contentEncoding) throws Excepti
9195
// body length is 113, when compressed it is 68 (gzip) or 78 (deflate)
9296
// The number of bytes available is 129.
9397
byte[] bytes = out.toByteArray();
94-
assertReadWithPushback(bytes, content, contentLength, 16, request);
98+
assertReadWithPushback(bytes, content, contentLength, nextRequestBytes, request);
9599
}
96100

97-
private void assertReadWithPushback(byte[] bytes, String content, int contentLength, int pushedBack, HTTPRequest request)
101+
private void assertReadWithPushback(byte[] bytes, String content, int contentLength, byte[] pushedBackBytes, HTTPRequest request)
98102
throws Exception {
99103
int bytesAvailable = bytes.length;
100104
HTTPServerConfiguration configuration = new HTTPServerConfiguration().withRequestBufferSize(bytesAvailable + 100);
@@ -106,7 +110,9 @@ private void assertReadWithPushback(byte[] bytes, String content, int contentLen
106110
byte[] buffer = new byte[configuration.getRequestBufferSize()];
107111
int read = httpInputStream.read(buffer);
108112

109-
// TODO : Hmm.. with compression, this is returning the compressed bytes read instead of the un-compressed bytes read. WTF?
113+
// Note that the HTTPInputStream read will return the number of uncompressed bytes read. So the contentLength passed in
114+
// needs to represent the actual length of the request body, not the value of the Content-Length sent on the request which
115+
// would represent the size of the compressed entity body.
110116
assertEquals(read, contentLength);
111117
assertEquals(new String(buffer, 0, read), content);
112118

@@ -115,12 +121,12 @@ private void assertReadWithPushback(byte[] bytes, String content, int contentLen
115121
assertEquals(secondRead, -1);
116122

117123
// We have 16 bytes left over
118-
assertEquals(pushbackInputStream.getAvailableBufferedBytesRemaining(), pushedBack);
124+
assertEquals(pushbackInputStream.getAvailableBufferedBytesRemaining(), pushedBackBytes.length);
119125

120126
// Next read should start at the next request
121127
byte[] leftOverBuffer = new byte[100];
122128
int leftOverRead = pushbackInputStream.read(leftOverBuffer);
123-
assertEquals(leftOverRead, pushedBack);
124-
assertEquals(new String(leftOverBuffer, 0, leftOverRead), "GET / HTTP/1.1\r\n");
129+
assertEquals(leftOverRead, pushedBackBytes.length);
130+
assertEquals(new String(leftOverBuffer, 0, leftOverRead), new String(pushedBackBytes));
125131
}
126132
}

0 commit comments

Comments
 (0)