Skip to content

Commit cff1c68

Browse files
committed
Working
1 parent 6a73fa4 commit cff1c68

File tree

6 files changed

+111
-106
lines changed

6 files changed

+111
-106
lines changed

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

Lines changed: 76 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public class ChunkedInputStream extends InputStream {
4040

4141
private int bufferLength;
4242

43+
private int chunkBytesRead;
44+
4345
private int chunkBytesRemaining;
4446

4547
private int chunkSize;
@@ -52,119 +54,127 @@ public ChunkedInputStream(PushbackInputStream delegate, int bufferSize) {
5254
}
5355

5456
@Override
55-
public int read(byte[] b, int off, int len) throws IOException {
56-
return processChunk(b, off, len);
57-
}
58-
59-
@Override
60-
public int read() throws IOException {
61-
var read = read(b1);
62-
if (read <= 0) {
63-
return read;
64-
}
65-
66-
return b1[0] & 0xFF;
67-
}
68-
69-
private int processChunk(byte[] destination, int offset, int length) throws IOException {
70-
int totalRead = 0;
71-
while (totalRead < length) {
72-
// Bail early if the state machine is done
73-
if (state == ChunkedBodyState.Complete) {
74-
// We need to push back any remaining bytes to the InputStream since we may have read more bytes than we needed.
75-
int leftOver = bufferLength - bufferIndex;
76-
if (leftOver > 0) {
77-
delegate.push(buffer, bufferIndex, leftOver);
78-
}
79-
80-
return -1;
57+
public int read(byte[] destination, int dOff, int dLen) throws IOException {
58+
int dIndex = dOff;
59+
while (dIndex < dLen) {
60+
if (state == ChunkedInputStream.ChunkedBodyState.Complete) {
61+
pushBackOverReadBytes();
62+
break;
8163
}
8264

8365
// Read some more if we are out of bytes
8466
if (bufferIndex >= bufferLength) {
8567
bufferIndex = 0;
8668
bufferLength = delegate.read(buffer);
87-
if (bufferLength > 0) {
88-
totalRead += bufferLength;
89-
}
9069
}
9170

92-
for (; bufferIndex < bufferLength; bufferIndex++) {
71+
// Process the buffer
72+
while (bufferIndex < bufferLength) {
9373
ChunkedBodyState nextState;
9474
try {
95-
nextState = state.next(buffer[bufferIndex], chunkSize, chunkSize - chunkBytesRemaining);
75+
nextState = state.next(buffer[bufferIndex], chunkSize, chunkBytesRead);
9676
} catch (ParseException e) {
9777
// This allows us to add the index to the exception. Useful for debugging.
9878
e.setIndex(bufferIndex);
9979
throw e;
10080
}
10181

102-
// We are DONE!
103-
if (nextState == ChunkedBodyState.Complete) {
82+
// We have reached the end of the encoded payload. Push back any additional bytes read.
83+
if (state == ChunkedBodyState.Complete) {
10484
state = nextState;
105-
bufferIndex++;
106-
int leftOver = bufferLength - bufferIndex;
107-
if (leftOver > 0) {
108-
delegate.push(buffer, bufferIndex, leftOver);
109-
}
110-
111-
return -1;
85+
pushBackOverReadBytes();
86+
break;
11287
}
11388

114-
// Record the size hex digit
89+
// Capture the character to calculate the next chunk size
11590
if (nextState == ChunkedBodyState.ChunkSize) {
11691
headerSizeHex.appendCodePoint(buffer[bufferIndex]);
11792
state = nextState;
93+
bufferIndex++;
11894
continue;
11995
}
12096

121-
// Capture the chunk size
97+
// We have found the chunk, this means we can now convert the captured chunk size bytes and then try and read the chunk.
12298
if (state != ChunkedBodyState.Chunk && nextState == ChunkedBodyState.Chunk) {
123-
// This means we finished reading the size and are ready to start processing
12499
if (headerSizeHex.isEmpty()) {
125100
throw new ChunkException("Chunk size is missing");
126101
}
127102

128103
// This is the start of a chunk, so set the size and counter and reset the size hex string
129104
chunkSize = (int) Long.parseLong(headerSizeHex, 0, headerSizeHex.length(), 16);
130105

106+
chunkBytesRead = 0;
131107
chunkBytesRemaining = chunkSize;
132108
headerSizeHex.delete(0, headerSizeHex.length());
133109

134-
// AF\r1234 i=3 length=42 read=7 chunkSize=175(AF)
110+
// A chunk size of 0 indicates this is the terminating chunk. Continue and we will expect the state machine
111+
// to process the final CRLF and hit the Complete state.
135112
if (chunkSize == 0) {
136113
state = nextState;
137-
return 0;
114+
bufferIndex++;
115+
continue;
138116
}
139117
}
140118

141-
int remainingInBuffer = bufferLength - bufferIndex;
142-
int lengthToCopy = Math.min(Math.min(chunkBytesRemaining, remainingInBuffer), length); // That's an ugly baby!
119+
int lengthToCopy;
120+
if (chunkBytesRemaining > 0) {
121+
int remainingInBuffer = bufferLength - bufferIndex;
122+
lengthToCopy = Math.min(Math.min(chunkBytesRemaining, remainingInBuffer), dLen - dIndex); // That's an ugly baby!
143123

144-
// If we don't have anything to copy, continue.
145-
if (lengthToCopy == 0) {
124+
// This means we don't have room in the destination buffer
125+
if (lengthToCopy == 0) {
126+
state = nextState;
127+
bufferIndex++;
128+
return dIndex - dOff;
129+
}
130+
} else {
131+
// Nothing to do, continue to the next state.
146132
state = nextState;
133+
bufferIndex++;
147134
continue;
148135
}
149136

150-
// TODO : Daniel : If we wanted to handle more than one chunk at a time, I think we would potentially continue here
151-
// and see if we can get more than one chunk completed before copying back to the destination.
152-
// Discuss with Brian.
153-
// Should we try this right now, or is this ok? Load testing with a small body,
154-
// Chunked is slower than fixed length, I suppose this makes sense. In practice I don't think browsers
155-
// and such use chunked for small requests.
156-
157-
System.arraycopy(buffer, bufferIndex, destination, offset, lengthToCopy);
137+
// Copy 'lengthToCopy' to the destination buffer
138+
System.arraycopy(buffer, bufferIndex, destination, dIndex, lengthToCopy);
158139
bufferIndex += lengthToCopy;
140+
chunkBytesRead += lengthToCopy;
159141
chunkBytesRemaining -= lengthToCopy;
142+
dIndex += lengthToCopy;
160143
state = nextState;
161-
return lengthToCopy;
144+
145+
// If we have bytes to copy, we are at the correct location in the state machine, If we don't have room, break.
146+
// - This will break the while loop, and return at the end of this method the total bytes we have written to the destination buffer.
147+
if (dIndex == dLen) {
148+
break;
149+
}
162150
}
163151
}
164152

165-
return 0;
153+
int total = dIndex - dOff;
154+
return total == 0 ? -1 : total;
155+
}
156+
157+
@Override
158+
public int read() throws IOException {
159+
var read = read(b1);
160+
if (read <= 0) {
161+
return read;
162+
}
163+
164+
return b1[0] & 0xFF;
165+
}
166+
167+
private void pushBackOverReadBytes() {
168+
int leftOver = bufferLength - bufferIndex;
169+
if (leftOver > 0) {
170+
delegate.push(buffer, bufferIndex, leftOver);
171+
172+
// Move the pointer to the end of the buffer, We have used up the bytes by pushing them back.
173+
bufferIndex = bufferLength;
174+
}
166175
}
167176

177+
168178
public enum ChunkedBodyState {
169179
ChunkExtensionStart {
170180
@Override
@@ -266,17 +276,18 @@ public ChunkedBodyState next(byte ch, long length, long bytesRead) {
266276
return Chunk;
267277
}
268278
},
269-
270279
Chunk {
271280
@Override
272281
public ChunkedBodyState next(byte ch, long length, long bytesRead) {
273-
if (bytesRead < length) {
274-
return Chunk;
282+
if (length == 0) {
283+
return Complete;
275284
} else if (bytesRead == length && ch == '\r') {
276285
return ChunkCR;
286+
} else if (bytesRead < length) {
287+
return Chunk;
288+
} else {
289+
throw makeParseException(ch, this);
277290
}
278-
279-
throw makeParseException(ch, this);
280291
}
281292
},
282293

@@ -311,6 +322,6 @@ public ChunkedBodyState next(byte ch, long length, long bytesRead) {
311322
}
312323
};
313324

314-
public abstract ChunkedBodyState next(byte ch, long length, long bytesRead);
325+
public abstract ChunkedInputStream.ChunkedBodyState next(byte ch, long length, long bytesRead);
315326
}
316327
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ default T withBaseDir(Path baseDir) {
4747
}
4848

4949
/**
50-
* Sets the buffer size for the chunked input stream. Defaults to 2k.
50+
* Sets the buffer size for the chunked input stream. Defaults to 4k.
5151
*
5252
* @param chunkedBufferSize the buffer size used to read a request body that was encoded using 'chunked' transfer-encoding.
5353
* @return This.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class HTTPServerConfiguration implements Configurable<HTTPServerConfigura
3434

3535
private Path baseDir = Path.of("");
3636

37-
private int chunkedBufferSize = 2 * 1024; // 2k bytes
37+
private int chunkedBufferSize = 4 * 1024; // 4k bytes
3838

3939
private boolean compressByDefault = true;
4040

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

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,19 @@ public int read() throws IOException {
122122
}
123123

124124
@Override
125-
public int read(byte[] buffer) throws IOException {
126-
return read(buffer, 0, buffer.length);
125+
public int read(byte[] b) throws IOException {
126+
return read(b, 0, b.length);
127127
}
128128

129129
@Override
130-
public int read(byte[] buffer, int offset, int length) throws IOException {
130+
public int read(byte[] b, int off, int len) throws IOException {
131+
if (len == 0) {
132+
return 0;
133+
}
134+
131135
// If this is a fixed length request, and we have less than or equal to 0 bytes remaining, return -1
132-
if (!request.isChunked() && bytesRemaining <= 0) {
136+
boolean fixedLength = !request.isChunked();
137+
if (fixedLength && bytesRemaining <= 0) {
133138
return -1;
134139
}
135140

@@ -140,19 +145,24 @@ public int read(byte[] buffer, int offset, int length) throws IOException {
140145
// When we have a fixed length request, read beyond the remainingBytes if possible.
141146
// - Under heavy load we may be able to start reading the next request. Just push those bytes
142147
// back onto the InputStream and we will read them later.
143-
int read = delegate.read(buffer, offset, length);
144-
if (!request.isChunked()) {
148+
int read = delegate.read(b, off, len);
149+
if (fixedLength && read > 0) {
145150
int extraBytes = (int) (read - bytesRemaining);
146151
if (extraBytes > 0) {
147-
pushbackInputStream.push(buffer, (int) bytesRemaining, extraBytes);
152+
pushbackInputStream.push(b, (int) bytesRemaining, extraBytes);
148153
}
149154
}
150155

151-
if (instrumenter != null) {
152-
instrumenter.readFromClient(read);
156+
if (read > 0) {
157+
if (instrumenter != null) {
158+
instrumenter.readFromClient(read);
159+
}
160+
161+
if (fixedLength) {
162+
bytesRemaining -= read;
163+
}
153164
}
154165

155-
bytesRemaining -= read;
156166
return read;
157167
}
158168

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ protected Builder withRequest(String request) {
3636
private void assertResponse(String request, String chunkedExtension, String response) throws Exception {
3737
HTTPHandler handler = (req, res) -> {
3838
// Read the request body
39-
byte[] bodyBytes = req.getInputStream().readAllBytes();
40-
System.out.println(bodyBytes.length);
41-
39+
req.getInputStream().readAllBytes();
4240
res.setStatus(200);
4341
};
4442

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

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -120,22 +120,10 @@ public void partialChunks() throws IOException {
120120
0\r
121121
\r
122122
"""), 1024);
123-
assertEquals(inputStream.read(buf), 8);
124-
var result = new String(buf, 0, 8);
125-
assertEquals(result, "12345678");
126-
127-
assertEquals(inputStream.read(buf), 2);
128-
result = new String(buf, 0, 2);
129-
assertEquals(result, "90");
130-
131-
assertEquals(inputStream.read(buf), 20);
132-
result = new String(buf, 0, 20);
133-
assertEquals(result, "12345678901234567890");
134-
135-
assertEquals(inputStream.read(buf), 30);
136-
result = new String(buf, 0, 30);
137-
assertEquals(result, "123456789012345678901234567890");
138-
assertEquals(inputStream.read(), 0);
123+
// All chunks will be read on the first attempt because the buffer is large enough
124+
assertEquals(inputStream.read(buf), 60);
125+
var result = new String(buf, 0, 60);
126+
assertEquals(result, "123456789012345678901234567890123456789012345678901234567890");
139127
assertEquals(inputStream.read(), -1);
140128
}
141129

@@ -153,15 +141,12 @@ public void partialHeader() throws IOException {
153141
0\r
154142
\r
155143
"""), 1024);
156-
assertEquals(inputStream.read(buf), 10);
157-
var result = new String(buf, 0, 10);
158-
assertEquals(result, "1234567890");
159-
160-
assertEquals(inputStream.read(buf), 20);
161-
result = new String(buf, 0, 20);
162-
assertEquals(result, "12345678901234567890");
163-
assertEquals(inputStream.read(), 0);
164-
assertEquals(inputStream.read(), -1);
144+
145+
// All chunks will be read on the first attempt because the buffer is large enough
146+
assertEquals(inputStream.read(buf), 30);
147+
var result = new String(buf, 0, 30);
148+
assertEquals(result, "123456789012345678901234567890");
149+
assertEquals(inputStream.read(buf), -1);
165150
}
166151

167152
private Builder withBody(String body) {
@@ -224,7 +209,8 @@ public int read(byte[] b, int off, int len) {
224209
return -1;
225210
}
226211

227-
// We may only read part way through one of the parts. If we didn't read all the way through, use the subPartIndex
212+
// We may only read part way through one of the parts.
213+
// If we didn't read all the way through, use the subPartIndex
228214
int read = Math.min(parts[partsIndex].length - subPartIndex, b.length);
229215
System.arraycopy(parts[partsIndex], 0, b, 0, read);
230216
if (read < parts[partsIndex].length - subPartIndex) {

0 commit comments

Comments
 (0)