Skip to content

Commit 8519136

Browse files
committed
Working
1 parent cff1c68 commit 8519136

File tree

2 files changed

+97
-9
lines changed

2 files changed

+97
-9
lines changed

src/main/java/io/fusionauth/http/io/ChunkedInputStream.java

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public int read(byte[] destination, int dOff, int dLen) throws IOException {
8282
// We have reached the end of the encoded payload. Push back any additional bytes read.
8383
if (state == ChunkedBodyState.Complete) {
8484
state = nextState;
85+
bufferIndex++;
8586
pushBackOverReadBytes();
8687
break;
8788
}
@@ -111,7 +112,6 @@ public int read(byte[] destination, int dOff, int dLen) throws IOException {
111112
// to process the final CRLF and hit the Complete state.
112113
if (chunkSize == 0) {
113114
state = nextState;
114-
bufferIndex++;
115115
continue;
116116
}
117117
}
@@ -280,14 +280,20 @@ public ChunkedBodyState next(byte ch, long length, long bytesRead) {
280280
@Override
281281
public ChunkedBodyState next(byte ch, long length, long bytesRead) {
282282
if (length == 0) {
283-
return Complete;
283+
// Following the final 0 length chunk, trailers are optional.
284+
if (HTTPTools.isURICharacter(ch)) {
285+
return Trailer;
286+
} else {
287+
return Complete;
288+
}
289+
284290
} else if (bytesRead == length && ch == '\r') {
285291
return ChunkCR;
286292
} else if (bytesRead < length) {
287293
return Chunk;
288-
} else {
289-
throw makeParseException(ch, this);
290294
}
295+
296+
throw makeParseException(ch, this);
291297
}
292298
},
293299

@@ -320,6 +326,36 @@ public ChunkedBodyState next(byte ch, long length, long bytesRead) {
320326
public ChunkedBodyState next(byte ch, long length, long bytesRead) {
321327
return Complete;
322328
}
329+
},
330+
Trailer {
331+
@Override
332+
public ChunkedBodyState next(byte ch, long length, long bytesRead) {
333+
if (ch == '\r') {
334+
return TrailerCR;
335+
} else {
336+
return Trailer;
337+
}
338+
}
339+
},
340+
TrailerCR {
341+
@Override
342+
public ChunkedBodyState next(byte ch, long length, long bytesRead) {
343+
if (ch == '\n') {
344+
return TrailerLF;
345+
}
346+
347+
throw makeParseException(ch, this);
348+
}
349+
},
350+
TrailerLF {
351+
@Override
352+
public ChunkedBodyState next(byte ch, long length, long bytesRead) {
353+
if (HTTPTools.isURICharacter(ch)) {
354+
return Trailer;
355+
} else {
356+
return Complete;
357+
}
358+
}
323359
};
324360

325361
public abstract ChunkedInputStream.ChunkedBodyState next(byte ch, long length, long bytesRead);

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

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public void chunkExtensions() throws Exception {
4444
// ;foo=;bar=baz Two extensions, one value, one equals
4545
// ;foo=bar;bar=baz Two extensions, two values
4646
// ; No extension, only a separator. Not sure if this is valid, but we should be able to ignore it.
47+
// 0;foo=bar;bar Extensions on the final 0 chunk
4748
withBody(
4849
"""
4950
3;foo=bar\r
@@ -68,10 +69,11 @@ public void chunkExtensions() throws Exception {
6869
sion\r
6970
2;\r
7071
s!\r
71-
0\r
72+
0;foo=bar;bar\r
7273
\r
7374
""")
74-
.assertResult("Hi mom! Look no extensions!");
75+
.assertResult("Hi mom! Look no extensions!")
76+
.assertLeftOverBytes(0);
7577
}
7678

7779
@Test
@@ -87,6 +89,7 @@ public void multipleChunks() throws Exception {
8789
\r
8890
"""
8991
).assertResult("123456789012345678901234567890123456789012345678901234567890")
92+
.assertLeftOverBytes(0)
9093
.assertNextRead(ChunkedInputStream::read, -1);
9194
}
9295

@@ -101,7 +104,8 @@ public void ok() throws Exception {
101104
0\r
102105
\r
103106
"""
104-
).assertResult("Hi mom!");
107+
).assertResult("Hi mom!")
108+
.assertLeftOverBytes(0);
105109
}
106110

107111
@Test
@@ -149,6 +153,30 @@ public void partialHeader() throws IOException {
149153
assertEquals(inputStream.read(buf), -1);
150154
}
151155

156+
@Test
157+
public void trailers() throws Exception {
158+
// It isn't clear if any HTTP server actually users or supports trailers. But, the spec indicates we should at least ignore them.
159+
// - https://www.rfc-editor.org/rfc/rfc2616.html#section-3.6.1
160+
withBody(
161+
"""
162+
30\r
163+
There is no fate but what we make for ourselves.\r
164+
12\r
165+
166+
- Sarah Connor
167+
\r
168+
0\r
169+
Judgement-Day: August 29, 1997 2:14 AM EDT\r
170+
\r
171+
""")
172+
.assertResult("""
173+
There is no fate but what we make for ourselves.
174+
- Sarah Connor
175+
""")
176+
// If we correctly read to the end of the InputStream we should not have any bytes left over in the PushbackInputStream
177+
.assertLeftOverBytes(0);
178+
}
179+
152180
private Builder withBody(String body) {
153181
return new Builder().withBody(body);
154182
}
@@ -157,20 +185,44 @@ private PushbackInputStream withParts(String... parts) {
157185
return new PushbackInputStream(new PieceMealInputStream(parts));
158186
}
159187

188+
@SuppressWarnings("UnusedReturnValue")
160189
private static class Builder {
161190
public String body;
162191

163192
public ChunkedInputStream chunkedInputStream;
164193

194+
public PushbackInputStream pushbackInputStream;
195+
196+
/**
197+
* Used to ensure the parser worked correctly and was able to read to the end of the encoded body.
198+
*
199+
* @param expected the number of expected bytes that were over-read.
200+
* @return this.
201+
*/
202+
public Builder assertLeftOverBytes(int expected) throws IOException {
203+
int actual = pushbackInputStream.getAvailableBufferedBytesRemaining();
204+
if (actual != expected) {
205+
if (actual > 0) {
206+
byte[] leftOverBytes = new byte[actual];
207+
int leftOverRead = pushbackInputStream.read(leftOverBytes);
208+
// No reason to think these would not be equal... but they better be.
209+
assertEquals(leftOverBytes.length, leftOverRead);
210+
assertEquals(actual, expected, "\nHere is what was left over in the buffer\n[" + new String(leftOverBytes) + "]");
211+
}
212+
}
213+
214+
return this;
215+
}
216+
165217
public Builder assertNextRead(ThrowingFunction<ChunkedInputStream, Integer> function, int expected) throws Exception {
166218
var result = function.apply(chunkedInputStream);
167219
assertEquals(result, expected);
168220
return this;
169221
}
170222

171223
public Builder assertResult(String expected) throws IOException {
172-
var bis = new PushbackInputStream(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)));
173-
chunkedInputStream = new ChunkedInputStream(bis, 2048);
224+
pushbackInputStream = new PushbackInputStream(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)));
225+
chunkedInputStream = new ChunkedInputStream(pushbackInputStream, 2048);
174226

175227
String actual = new String(chunkedInputStream.readAllBytes(), StandardCharsets.UTF_8);
176228
assertEquals(actual, expected);

0 commit comments

Comments
 (0)