Skip to content

Commit dc6d844

Browse files
committed
working
1 parent 08b1a66 commit dc6d844

File tree

6 files changed

+133
-58
lines changed

6 files changed

+133
-58
lines changed

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,16 @@ public int read(byte[] b, int off, int len) throws IOException {
167167
// - If we have read past the end of the current request, push those bytes back onto the InputStream.
168168
// - When a maximum content length has been specified, read at most one byte past the maximum.
169169
int maxReadLen = maximumContentLength == -1 ? len : Math.min(len, maximumContentLength - bytesRead + 1);
170+
171+
// TODO : Hack - This makes compression work with fixed length requests
172+
if (fixedLength) {
173+
// TODO : Note : The len arg for an inflater stream is the number of un-compressed bytes.
174+
// The returned number of bytes is the un-compressed bytes. So the problem here
175+
// is that the bytesRemaining value we have is the compressed size of the payload so we can't
176+
// us it to ensure we do not read past the current fixed length request. Sad.
177+
// maxReadLen = Math.min(maxReadLen, (int) bytesRemaining);
178+
}
179+
170180
int read = delegate.read(b, off, maxReadLen);
171181

172182
// TODO : Can I optionally never override here? If I am fixed length, I could change len -> maxLen., and then never pushback.
@@ -175,6 +185,10 @@ public int read(byte[] b, int off, int len) throws IOException {
175185
// int maxLen = len;
176186
// int read = delegate.read(b, off, maxLen);
177187

188+
// TODO : This is busted with compression.
189+
// bytesRemaining is calculated based upon the Content-Length.
190+
// But the bytes read from the InputStream will be the bytes read that are un-compressed.
191+
// So we can't use the bytes read to calculate pushback. This has to go below the Decompression in the chain.
178192
int reportBytesRead = read;
179193
if (fixedLength && read > 0) {
180194
int extraBytes = (int) (read - bytesRemaining);
@@ -254,8 +268,9 @@ private void initialize() throws IOException {
254268
// HTTPInputStream (this) > Pushback (delegate) > Throughput > Socket
255269
// HTTPInputStream (this) > Chunked (delegate) > Pushback > Throughput > Socket
256270

257-
// HTTPInputStream (this) > Pushback > Decompress > Throughput > Socket
258-
// HTTPInputStream (this) > Pushback > Decompress > Chunked > Throughput > Socket
271+
// The way it is currently coded
272+
// HTTPInputStream (this) > Decompress > Pushback > Throughput > Socket
273+
// HTTPInputStream (this) > Decompress > Chunked > Pushback > Throughput > Socket
259274

260275
// TODO : Note I could leave this alone, but when we parse the header we can lower case these values and then remove the equalsIgnoreCase here?
261276
// Seems like ideally we would normalize them to lowercase earlier.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ private void assertResponse(String request, String chunkedExtension, String resp
6666
var body = bodyString.repeat(((requestBufferSize / bodyString.length())) * 2);
6767

6868
if (request.contains("Transfer-Encoding: chunked")) {
69-
body = chunkItUp(body, chunkedExtension);
69+
// Chunk in 100 byte increments. Using a smaller chunk size to ensure we don't end up with a single chunk.
70+
body = chunkItUp(body, 100, chunkedExtension);
7071
}
7172

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

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -436,10 +436,8 @@ protected void assertHTTPResponseEquals(Socket socket, String expectedResponse)
436436
}
437437
}
438438

439-
protected String chunkItUp(String body, String chunkedExtension) {
439+
protected String chunkItUp(String body, int chunkSize, String chunkedExtension) {
440440
List<String> result = new ArrayList<>();
441-
// Chunk in 100 byte increments. Using a smaller chunk size to ensure we don't end up with a single chunk.
442-
int chunkSize = 100;
443441
for (var i = 0; i < body.length(); i += chunkSize) {
444442
var endIndex = Math.min(i + chunkSize, body.length());
445443
var chunk = body.substring(i, endIndex);

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

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,19 +92,23 @@ public void compress(String scheme, String contentEncoding, String acceptEncodin
9292
var requestEncodings = contentEncoding.toLowerCase().trim().split(",");
9393
for (String part : requestEncodings) {
9494
String encoding = part.trim();
95-
payload = encoding.equals(ContentEncodings.Deflate)
96-
? deflate(payload)
97-
: gzip(payload);
95+
if (encoding.equals(ContentEncodings.Deflate)) {
96+
payload = deflate(payload);
97+
} else if (encoding.equals(ContentEncodings.Gzip) || encoding.equals(ContentEncodings.XGzip)) {
98+
payload = gzip(payload);
99+
}
98100
}
99101

100102
byte[] uncompressedBody = payload;
101103

102104
// Sanity check on round trip compress/decompress
103105
for (int i = requestEncodings.length - 1; i >= 0; i--) {
104106
String encoding = requestEncodings[i].trim();
105-
uncompressedBody = encoding.equals(ContentEncodings.Deflate)
106-
? inflate(uncompressedBody)
107-
: ungzip(uncompressedBody);
107+
if (encoding.equals(ContentEncodings.Deflate)) {
108+
uncompressedBody = inflate(uncompressedBody);
109+
} else if (encoding.equals(ContentEncodings.Gzip) || encoding.equals(ContentEncodings.XGzip)) {
110+
uncompressedBody = ungzip(uncompressedBody);
111+
}
108112
}
109113

110114
assertEquals(uncompressedBody, bodyBytes);
@@ -140,17 +144,18 @@ public void compress(String scheme, String contentEncoding, String acceptEncodin
140144

141145
assertNotNull(expectedResponseEncoding);
142146

143-
String result;
147+
String result = null;
144148
InputStream responseInputStream = response.body();
145149

146150
if (expectedResponseEncoding.isEmpty()) {
147151
result = new String(responseInputStream.readAllBytes());
148152
} else {
149153
assertEquals(response.headers().firstValue(Headers.ContentEncoding).orElse(null), expectedResponseEncoding);
150-
result = new String(
151-
expectedResponseEncoding.equals(ContentEncodings.Deflate)
152-
? new InflaterInputStream(responseInputStream).readAllBytes()
153-
: new GZIPInputStream(responseInputStream).readAllBytes(), StandardCharsets.UTF_8);
154+
if (expectedResponseEncoding.equals(ContentEncodings.Deflate)) {
155+
result = new String(new InflaterInputStream(responseInputStream).readAllBytes());
156+
} else if (expectedResponseEncoding.equals(ContentEncodings.Gzip) || expectedResponseEncoding.equals(ContentEncodings.XGzip)) {
157+
result = new String(new GZIPInputStream(responseInputStream).readAllBytes(), StandardCharsets.UTF_8);
158+
}
154159
}
155160

156161
assertEquals(result, Files.readString(file));

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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,18 @@ public Builder(String scheme) {
197197
this.scheme = scheme;
198198
}
199199

200+
public Builder assertOptionalExceptionOnWrite(Class<? extends Exception> clazz) {
201+
// Note that this assertion really depends upon the system the test is run on, the size of the request, and the amount of data that can be cached.
202+
// - So this is an optional assertion - if exception is not null, then we should be able to assert some attributes.
203+
// - With the larger sizes this exception is mostly always thrown when running tests locally, but in GHA, it doesn't always occur.
204+
if (thrownOnWrite != null) {
205+
assertEquals(thrownOnWrite.getClass(), clazz);
206+
assertEquals(thrownOnWrite.getMessage(), "Broken pipe");
207+
}
208+
209+
return this;
210+
}
211+
200212
public Builder expectNoExceptionOnWrite() {
201213
assertNull(thrownOnWrite);
202214
return this;
@@ -272,7 +284,8 @@ public Builder expectResponse(String response) throws Exception {
272284
""";
273285

274286
// Convert body to chunked
275-
body = chunkItUp(body, null);
287+
// - Using a small chunk to ensure we end up with more than one chunk.
288+
body = chunkItUp(body, 100, null);
276289
} else {
277290
var contentLength = body.getBytes(StandardCharsets.UTF_8).length;
278291
if (contentLength > 0) {
@@ -307,18 +320,6 @@ public Builder expectResponse(String response) throws Exception {
307320
return this;
308321
}
309322

310-
public Builder assertOptionalExceptionOnWrite(Class<? extends Exception> clazz) {
311-
// Note that this assertion really depends upon the system the test is run on, the size of the request, and the amount of data that can be cached.
312-
// - So this is an optional assertion - if exception is not null, then we should be able to assert some attributes.
313-
// - With the larger sizes this exception is mostly always thrown when running tests locally, but in GHA, it doesn't always occur.
314-
if (thrownOnWrite != null) {
315-
assertEquals(thrownOnWrite.getClass(), clazz);
316-
assertEquals(thrownOnWrite.getMessage(), "Broken pipe");
317-
}
318-
319-
return this;
320-
}
321-
322323
public Builder withBodyParameterCount(int bodyParameterCount) {
323324
this.bodyParameterCount = bodyParameterCount;
324325
return this;

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

Lines changed: 83 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,63 +16,117 @@
1616
package io.fusionauth.http.io;
1717

1818
import java.io.ByteArrayInputStream;
19+
import java.io.ByteArrayOutputStream;
1920
import java.nio.charset.StandardCharsets;
2021

22+
import io.fusionauth.http.BaseTest;
23+
import io.fusionauth.http.HTTPValues.ContentEncodings;
24+
import io.fusionauth.http.HTTPValues.Headers;
2125
import io.fusionauth.http.server.HTTPRequest;
2226
import io.fusionauth.http.server.HTTPServerConfiguration;
2327
import io.fusionauth.http.server.io.HTTPInputStream;
28+
import org.testng.annotations.DataProvider;
2429
import org.testng.annotations.Test;
2530
import static org.testng.Assert.assertEquals;
2631

2732
/**
2833
* @author Daniel DeGroff
2934
*/
30-
public class HTTPInputStreamTest {
31-
@Test
32-
public void read_chunked_withPushback() throws Exception {
35+
public class HTTPInputStreamTest extends BaseTest {
36+
@DataProvider(name = "contentEncoding")
37+
public Object[][] contentEncoding() {
38+
return new Object[][]{
39+
{""},
40+
{"gzip"},
41+
{"deflate"},
42+
{"gzip, deflate"},
43+
{"deflate, gzip"}
44+
};
45+
}
46+
47+
@Test(dataProvider = "contentEncoding")
48+
public void read_chunked_withPushback(String contentEncoding) throws Exception {
3349
// Ensure that when we read a chunked encoded body that the InputStream returns the correct number of bytes read even when
3450
// we read past the end of the current request and use the PushbackInputStream.
3551

3652
String content = "These pretzels are making me thirsty. These pretzels are making me thirsty. These pretzels are making me thirsty.";
3753
int contentLength = content.getBytes(StandardCharsets.UTF_8).length;
3854

39-
// Chunk the content
40-
byte[] bytes = """
41-
26\r
42-
These pretzels are making me thirsty. \r
43-
26\r
44-
These pretzels are making me thirsty. \r
45-
25\r
46-
These pretzels are making me thirsty.\r
47-
0\r
48-
\r
49-
GET / HTTP/1.1\r
50-
""".getBytes();
55+
// Chunk the content, add part of the next request
56+
String chunked = chunkItUp(content, 38, null);
57+
byte[] payload = chunked.getBytes(StandardCharsets.UTF_8);
58+
59+
// Optionally compress the payload
60+
if (!contentEncoding.isEmpty()) {
61+
var requestEncodings = contentEncoding.toLowerCase().trim().split(",");
62+
for (String part : requestEncodings) {
63+
String encoding = part.trim();
64+
if (encoding.equals(ContentEncodings.Deflate)) {
65+
payload = deflate(payload);
66+
} else if (encoding.equals(ContentEncodings.Gzip)) {
67+
payload = gzip(payload);
68+
}
69+
}
70+
}
71+
72+
ByteArrayOutputStream out = new ByteArrayOutputStream();
73+
out.write(payload);
74+
75+
// Add part of the next request
76+
out.write("GET / HTTP/1.1\r\n".getBytes(StandardCharsets.UTF_8));
5177

5278
HTTPRequest request = new HTTPRequest();
53-
request.setHeader("Transfer-Encoding", "chunked");
79+
request.setHeader(Headers.ContentEncoding, contentEncoding);
80+
request.setHeader(Headers.TransferEncoding, "chunked");
5481

55-
assertReadWithPushback(bytes, content, contentLength, request);
82+
byte[] bytes = out.toByteArray();
83+
assertReadWithPushback(bytes, content, contentLength, 16, request);
5684
}
5785

58-
@Test
59-
public void read_fixedLength_withPushback() throws Exception {
86+
@Test(dataProvider = "contentEncoding")
87+
public void read_fixedLength_withPushback(String contentEncoding) throws Exception {
6088
// Ensure that when we read a fixed length body that the InputStream returns the correct number of bytes read even when
6189
// we read past the end of the current request and use the PushbackInputStream.
6290

63-
String content = "These pretzels are making me thirsty. These pretzels are making me thirsty. These pretzels are making me thirsty.";
64-
int contentLength = content.getBytes(StandardCharsets.UTF_8).length;
65-
6691
// Fixed length body with the start of the next request in the buffer
67-
byte[] bytes = (content + "GET / HTTP/1.1\r\n").getBytes(StandardCharsets.UTF_8);
92+
String content = "These pretzels are making me thirsty. These pretzels are making me thirsty. These pretzels are making me thirsty.";
93+
byte[] payload = content.getBytes(StandardCharsets.UTF_8);
94+
int contentLength = payload.length;
95+
96+
// Optionally compress the payload
97+
if (!contentEncoding.isEmpty()) {
98+
var requestEncodings = contentEncoding.toLowerCase().trim().split(",");
99+
for (String part : requestEncodings) {
100+
String encoding = part.trim();
101+
if (encoding.equals(ContentEncodings.Deflate)) {
102+
payload = deflate(payload);
103+
} else if (encoding.equals(ContentEncodings.Gzip)) {
104+
payload = gzip(payload);
105+
}
106+
}
107+
}
108+
109+
// Content-Length must be the compressed length
110+
int compressedLength = payload.length;
111+
112+
ByteArrayOutputStream out = new ByteArrayOutputStream();
113+
out.write(payload);
114+
115+
// Add part of the next request
116+
out.write("GET / HTTP/1.1\r\n".getBytes(StandardCharsets.UTF_8));
68117

69118
HTTPRequest request = new HTTPRequest();
70-
request.setHeader("Content-Length", contentLength + "");
119+
request.setHeader(Headers.ContentEncoding, contentEncoding);
120+
request.setHeader(Headers.ContentLength, compressedLength + "");
71121

72-
assertReadWithPushback(bytes, content, contentLength, request);
122+
// body length is 113, when compressed it is 68 (gzip) or 78 (deflate)
123+
// The number of bytes available is 129.
124+
byte[] bytes = out.toByteArray();
125+
assertReadWithPushback(bytes, content, contentLength, 16, request);
73126
}
74127

75-
private void assertReadWithPushback(byte[] bytes, String content, int contentLength, HTTPRequest request) throws Exception {
128+
private void assertReadWithPushback(byte[] bytes, String content, int contentLength, int pushedBack, HTTPRequest request)
129+
throws Exception {
76130
int bytesAvailable = bytes.length;
77131
HTTPServerConfiguration configuration = new HTTPServerConfiguration().withRequestBufferSize(bytesAvailable + 100);
78132

@@ -83,6 +137,7 @@ private void assertReadWithPushback(byte[] bytes, String content, int contentLen
83137
byte[] buffer = new byte[configuration.getRequestBufferSize()];
84138
int read = httpInputStream.read(buffer);
85139

140+
// TODO : Hmm.. with compression, this is returning the compressed bytes read instead of the un-compressed bytes read. WTF?
86141
assertEquals(read, contentLength);
87142
assertEquals(new String(buffer, 0, read), content);
88143

@@ -91,12 +146,12 @@ private void assertReadWithPushback(byte[] bytes, String content, int contentLen
91146
assertEquals(secondRead, -1);
92147

93148
// We have 16 bytes left over
94-
assertEquals(pushbackInputStream.getAvailableBufferedBytesRemaining(), 16);
149+
assertEquals(pushbackInputStream.getAvailableBufferedBytesRemaining(), pushedBack);
95150

96151
// Next read should start at the next request
97152
byte[] leftOverBuffer = new byte[100];
98153
int leftOverRead = pushbackInputStream.read(leftOverBuffer);
99-
assertEquals(leftOverRead, 16);
154+
assertEquals(leftOverRead, pushedBack);
100155
assertEquals(new String(leftOverBuffer, 0, leftOverRead), "GET / HTTP/1.1\r\n");
101156
}
102157
}

0 commit comments

Comments
 (0)