Skip to content

Commit b4fd8c2

Browse files
committed
working
1 parent dc6d844 commit b4fd8c2

File tree

3 files changed

+89
-72
lines changed

3 files changed

+89
-72
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright (c) 2025, FusionAuth, All Rights Reserved
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing,
11+
* software distributed under the License is distributed on an
12+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific
14+
* language governing permissions and limitations under the License.
15+
*/
16+
package io.fusionauth.http.io;
17+
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
21+
/**
22+
* A filter InputStream that reads a fixed length body.
23+
*
24+
* @author Daniel DeGroff
25+
*/
26+
public class FixedLengthInputStream extends InputStream {
27+
private final byte[] b1 = new byte[1];
28+
29+
private final PushbackInputStream delegate;
30+
31+
private long bytesRemaining;
32+
33+
public FixedLengthInputStream(PushbackInputStream delegate, long contentLength) {
34+
this.delegate = delegate;
35+
this.bytesRemaining = contentLength;
36+
}
37+
38+
@Override
39+
public int read(byte[] b, int off, int len) throws IOException {
40+
if (bytesRemaining <= 0) {
41+
return -1;
42+
}
43+
44+
int read = delegate.read(b, off, len);
45+
int reportBytesRead = read;
46+
if (read > 0) {
47+
int extraBytes = (int) (read - bytesRemaining);
48+
if (extraBytes > 0) {
49+
reportBytesRead -= extraBytes;
50+
delegate.push(b, (int) bytesRemaining, extraBytes);
51+
}
52+
53+
bytesRemaining -= reportBytesRead;
54+
}
55+
56+
return reportBytesRead;
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+
}

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

Lines changed: 11 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.fusionauth.http.ContentTooLargeException;
2424
import io.fusionauth.http.HTTPValues.ContentEncodings;
2525
import io.fusionauth.http.io.ChunkedInputStream;
26+
import io.fusionauth.http.io.FixedLengthInputStream;
2627
import io.fusionauth.http.io.PushbackInputStream;
2728
import io.fusionauth.http.log.Logger;
2829
import io.fusionauth.http.server.HTTPRequest;
@@ -55,8 +56,6 @@ public class HTTPInputStream extends InputStream {
5556

5657
private int bytesRead;
5758

58-
private long bytesRemaining;
59-
6059
private boolean closed;
6160

6261
private InputStream delegate;
@@ -75,11 +74,6 @@ public HTTPInputStream(HTTPServerConfiguration configuration, HTTPRequest reques
7574
this.chunkedBufferSize = configuration.getChunkedBufferSize();
7675
this.maximumBytesToDrain = configuration.getMaxBytesToDrain();
7776
this.maximumContentLength = maximumContentLength;
78-
79-
// Start the countdown
80-
if (request.getContentLength() != null) {
81-
this.bytesRemaining = request.getContentLength();
82-
}
8377
}
8478

8579
@Override
@@ -142,23 +136,6 @@ public int read(byte[] b, int off, int len) throws IOException {
142136
return 0;
143137
}
144138

145-
// TODO : Re: Compression - fixedLength and Content-Length needs to account for the Content-Length referring to the compressed body.
146-
147-
// Preamble, this could push back compressed bytes.
148-
// Pushback > Throughput > Socket
149-
//
150-
// HTTPInputStream (this) -> Decompress > Pushback > Throughput > Socket
151-
// HTTPInputStream (this) -> Decompress > Chunked > Pushback > Throughput > Socket
152-
//
153-
// Pushback doesn't care if the bytes are compressed or not, it is up to the caller?
154-
//
155-
156-
// If this is a fixed length request, and we have less than or equal to 0 bytes remaining, return -1
157-
boolean fixedLength = !request.isChunked();
158-
if (fixedLength && bytesRemaining <= 0) {
159-
return -1;
160-
}
161-
162139
if (!initialized) {
163140
initialize();
164141
}
@@ -167,49 +144,19 @@ public int read(byte[] b, int off, int len) throws IOException {
167144
// - If we have read past the end of the current request, push those bytes back onto the InputStream.
168145
// - When a maximum content length has been specified, read at most one byte past the maximum.
169146
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-
180147
int read = delegate.read(b, off, maxReadLen);
181-
182-
// TODO : Can I optionally never override here? If I am fixed length, I could change len -> maxLen., and then never pushback.
183-
// Would this help with compression?
184-
// int maxLen = (int) Math.min(len, bytesRemaining);
185-
// int maxLen = len;
186-
// int read = delegate.read(b, off, maxLen);
187-
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.
192-
int reportBytesRead = read;
193-
if (fixedLength && read > 0) {
194-
int extraBytes = (int) (read - bytesRemaining);
195-
if (extraBytes > 0) {
196-
reportBytesRead -= extraBytes;
197-
pushbackInputStream.push(b, (int) bytesRemaining, extraBytes);
198-
}
199-
200-
bytesRemaining -= reportBytesRead;
148+
if (read > 0) {
149+
bytesRead += read;
201150
}
202151

203-
bytesRead += reportBytesRead;
204-
205152
// Note that when the request is fixed length, we will have failed early during commit().
206153
// - This will handle all requests that are not fixed length.
207154
if (maximumContentLength != -1 && bytesRead > maximumContentLength) {
208155
String detailedMessage = "The maximum request size has been exceeded. The maximum request size is [" + maximumContentLength + "] bytes.";
209156
throw new ContentTooLargeException(maximumContentLength, detailedMessage);
210157
}
211158

212-
return reportBytesRead;
159+
return read;
213160
}
214161

215162
private void initialize() throws IOException {
@@ -248,29 +195,26 @@ private void initialize() throws IOException {
248195
}
249196
} else if (contentLength != null) {
250197
logger.trace("Client indicated it was sending an entity-body in the request. Handling body using Content-Length header {}.", contentLength);
198+
delegate = new FixedLengthInputStream(pushbackInputStream, contentLength);
251199
} else {
252200
logger.trace("Client indicated it was NOT sending an entity-body in the request");
253201
}
254202

255203
// If we have a maximumContentLength, and this is a fixed content length request, before we read any bytes, fail early.
256204
// For good measure do this last so if anyone downstream wants to read from the InputStream they could in theory because
257205
// we will have set up the InputStream.
206+
// TODO : Compression : we may have to delete this code, or only enforce it when compression was not used.
207+
// Content-Length represents the compressed size, not the uncompressed bytes.
258208
if (contentLength != null && maximumContentLength != -1 && contentLength > maximumContentLength) {
259209
String detailedMessage = "The maximum request size has been exceeded. The reported Content-Length is [" + contentLength + "] and the maximum request size is [" + maximumContentLength + "] bytes.";
260210
throw new ContentTooLargeException(maximumContentLength, detailedMessage);
261211
}
262212

263-
// Those who push back:
264-
// HTTPInputStream when fixed
265-
// ChunkedInputStream when chunked
266-
// Preamble parser
267-
268-
// HTTPInputStream (this) > Pushback (delegate) > Throughput > Socket
269-
// HTTPInputStream (this) > Chunked (delegate) > Pushback > Throughput > Socket
270-
271213
// The way it is currently coded
272-
// HTTPInputStream (this) > Decompress > Pushback > Throughput > Socket
273-
// HTTPInputStream (this) > Decompress > Chunked > Pushback > Throughput > Socket
214+
// HTTPInputStream (this) > Decompress > Fixed > Pushback > Throughput > Socket
215+
216+
// HTTPInputStream (this) > Chunked > Pushback > Throughput > Socket
217+
// > HTTPInputStream (this) > Decompress > Chunked > Pushback > Throughput > Socket
274218

275219
// 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?
276220
// Seems like ideally we would normalize them to lowercase earlier.

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,8 @@ public void read_chunked_withPushback(String contentEncoding) throws Exception {
5050
// we read past the end of the current request and use the PushbackInputStream.
5151

5252
String content = "These pretzels are making me thirsty. These pretzels are making me thirsty. These pretzels are making me thirsty.";
53-
int contentLength = content.getBytes(StandardCharsets.UTF_8).length;
54-
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);
53+
byte[] payload = content.getBytes(StandardCharsets.UTF_8);
54+
int contentLength = payload.length;
5855

5956
// Optionally compress the payload
6057
if (!contentEncoding.isEmpty()) {
@@ -69,6 +66,14 @@ public void read_chunked_withPushback(String contentEncoding) throws Exception {
6966
}
7067
}
7168

69+
// Chunk the content, add part of the next request
70+
var temp1 = payload;
71+
var temp2 = new String(payload, StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8);
72+
assertEquals(temp1, temp2);
73+
74+
String chunked = chunkItUp(new String(payload, StandardCharsets.UTF_8), 38, null);
75+
payload = chunked.getBytes(StandardCharsets.UTF_8);
76+
7277
ByteArrayOutputStream out = new ByteArrayOutputStream();
7378
out.write(payload);
7479

0 commit comments

Comments
 (0)