Skip to content

Commit e2b9502

Browse files
committed
Working
1 parent 20b0da1 commit e2b9502

File tree

13 files changed

+583
-242
lines changed

13 files changed

+583
-242
lines changed

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

Lines changed: 0 additions & 92 deletions
This file was deleted.

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,15 @@ default T withLoggerFactory(LoggerFactory loggerFactory) {
168168
return (T) this;
169169
}
170170

171+
/**
172+
* The maximum size of the HTTP request body in bytes when the Content-Type is application/x-www-form-urlencoded. Defaults to 10
173+
* Megabytes.
174+
* <p>
175+
* Set this to -1 to disable this limitation.
176+
*
177+
* @param maxFormDataSize the maximum size of the HTTP request body in bytes.
178+
* @return This.
179+
*/
171180
default T withMaxFormDataSize(int maxFormDataSize) {
172181
configuration().withMaxFormDataSize(maxFormDataSize);
173182
return (T) this;
@@ -203,7 +212,9 @@ default T withMaxRequestBodySize(int maxRequestBodySize) {
203212
}
204213

205214
/**
206-
* Sets the maximum size of the HTTP request header. If this limit is exceeded, the connection will be closed. Defaults to 128 Kilobytes.
215+
* Sets the maximum size of the HTTP request header. The request header includes the HTTP request line, and all HTTP request headers,
216+
* essentially everything except the request body. If this maximum limit is exceeded, the connection will be closed. Defaults to 128
217+
* Kilobytes.
207218
* <p>
208219
* Set this to -1 to disable this limitation.
209220
*
@@ -228,7 +239,8 @@ default T withMaxRequestsPerConnection(int maxRequestsPerConnection) {
228239
}
229240

230241
/**
231-
* This configures the maximum size of a chunk in the response when the server is using chunked response encoding. Defaults to 16 Kilobytes.
242+
* This configures the maximum size of a chunk in the response when the server is using chunked response encoding. Defaults to 16
243+
* Kilobytes.
232244
*
233245
* @param size The size in bytes.
234246
* @return This.
@@ -342,8 +354,10 @@ default T withRequestBufferSize(int requestBufferSize) {
342354
* Sets the size of the buffer that is used to store the HTTP response before any bytes are written back to the client. This is useful
343355
* when the server is generating the response but encounters an error. In this case, the server will throw out the response and change to
344356
* a 500 error response. This defaults to 64 Kilobytes. Negative values disable the response buffer.
357+
* <p>
358+
* Set to -1 do disable buffering completely.
345359
*
346-
* @param responseBufferSize The size of the buffer. Set to -1 to disable buffering completely.
360+
* @param responseBufferSize The size of the buffer.
347361
* @return This.
348362
*/
349363
default T withResponseBufferSize(int responseBufferSize) {

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ public int getMaxRequestBodySize() {
226226
* @return the maximum size of the HTTP request header in bytes. This configuration does not affect the HTTP response header. Defaults to
227227
* 128 Kilobytes.
228228
*/
229+
// TODO : Naming : Preamble
229230
public int getMaxRequestHeaderSize() {
230231
// TODO : Notes:
231232
// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-limits.html
@@ -478,7 +479,10 @@ public HTTPServerConfiguration withLoggerFactory(LoggerFactory loggerFactory) {
478479
*/
479480
@Override
480481
public HTTPServerConfiguration withMaxFormDataSize(int maxFormDataSize) {
481-
// 0 turns this off
482+
if (maxFormDataSize != -1 && maxFormDataSize <= 0) {
483+
throw new IllegalArgumentException("The maximum form data size must be greater than 0. Set to -1 to disable this limitation.");
484+
}
485+
482486
this.maxFormDataSize = maxFormDataSize;
483487
return this;
484488
}
@@ -541,7 +545,7 @@ public HTTPServerConfiguration withMaxResponseChunkSize(int size) {
541545
*/
542546
@Override
543547
public HTTPServerConfiguration withMaximumBytesToDrain(int maxBytesToDrain) {
544-
if (maxBytesToDrain < 1024 || maxBytesToDrain >= 256 * 1024 * 1024) {
548+
if (maxBytesToDrain < 1024 || maxBytesToDrain > 256 * 1024 * 1024) {
545549
throw new IllegalArgumentException("The maximum bytes to drain must be greater than or equal to 1024 and less than or equal to 268,435,456 (256 Megabytes)");
546550
}
547551

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

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,15 @@
2222
import java.net.SocketTimeoutException;
2323
import java.nio.file.Files;
2424

25-
import io.fusionauth.http.ContentTooLargeException;
2625
import io.fusionauth.http.HTTPProcessingException;
2726
import io.fusionauth.http.HTTPValues;
2827
import io.fusionauth.http.HTTPValues.Connections;
2928
import io.fusionauth.http.HTTPValues.ContentTypes;
3029
import io.fusionauth.http.HTTPValues.Headers;
3130
import io.fusionauth.http.HTTPValues.Protocols;
3231
import io.fusionauth.http.ParseException;
33-
import io.fusionauth.http.RequestHeadersTooLargeException;
3432
import io.fusionauth.http.io.MultipartConfiguration;
3533
import io.fusionauth.http.io.PushbackInputStream;
36-
import io.fusionauth.http.io.ReadLimitedInputStream;
3734
import io.fusionauth.http.log.Logger;
3835
import io.fusionauth.http.server.ExceptionHandlerContext;
3936
import io.fusionauth.http.server.HTTPHandler;
@@ -69,8 +66,6 @@ public class HTTPWorker implements Runnable {
6966

7067
private final Logger logger;
7168

72-
private final ReadLimitedInputStream readLimitedInputStream;
73-
7469
private final Socket socket;
7570

7671
private final long startInstant;
@@ -90,8 +85,7 @@ public HTTPWorker(Socket socket, HTTPServerConfiguration configuration, Instrume
9085
this.throughput = throughput;
9186
this.buffers = new HTTPBuffers(configuration);
9287
this.logger = configuration.getLoggerFactory().getLogger(HTTPWorker.class);
93-
this.readLimitedInputStream = new ReadLimitedInputStream(new ThroughputInputStream(socket.getInputStream(), throughput));
94-
this.inputStream = new PushbackInputStream(readLimitedInputStream, instrumenter);
88+
this.inputStream = new PushbackInputStream(new ThroughputInputStream(socket.getInputStream(), throughput), instrumenter);
9589
this.state = State.Read;
9690
this.startInstant = System.currentTimeMillis();
9791
logger.trace("[{}] Starting HTTP worker.", Thread.currentThread().threadId());
@@ -134,14 +128,10 @@ public void run() {
134128
HTTPOutputStream outputStream = new HTTPOutputStream(configuration, request.getAcceptEncodings(), response, throughputOutputStream, buffers, () -> state = State.Write);
135129
response.setOutputStream(outputStream);
136130

137-
// Limit the maximum header size
138-
readLimitedInputStream.setMaximumBytesToRead(null, configuration.getMaxRequestHeaderSize(), maxSize ->
139-
new RequestHeadersTooLargeException(maxSize, "The maximum size of the request header has been exceeded. The maximum size is [" + maxSize + "] bytes."));
140-
141131
// Not this line of code will block
142132
// - When a client is using Keep-Alive - we will loop and block here while we wait for the client to send us bytes.
143133
byte[] requestBuffer = buffers.requestBuffer();
144-
HTTPTools.parseRequestPreamble(inputStream, request, requestBuffer, () -> state = State.Read);
134+
HTTPTools.parseRequestPreamble(inputStream, configuration.getMaxRequestHeaderSize(), request, requestBuffer, () -> state = State.Read);
145135
if (logger.isTraceEnabled()) {
146136
int availableBufferedBytes = inputStream.getAvailableBufferedBytesRemaining();
147137
if (availableBufferedBytes != 0) {
@@ -155,14 +145,9 @@ public void run() {
155145
instrumenter.acceptedRequest();
156146
}
157147

158-
// Limit the maximum body size
159-
var maximumContentLength = ContentTypes.Form.equalsIgnoreCase(request.getContentType())
160-
? configuration.getMaxFormDataSize()
161-
: configuration.getMaxRequestBodySize();
162-
readLimitedInputStream.setMaximumBytesToRead(request.getContentLength(), maximumContentLength, maxSize ->
163-
new ContentTooLargeException(maxSize, "The maximum size of the request body has been exceeded. The maximum size is [" + maxSize + "] bytes."));
164-
165-
httpInputStream = new HTTPInputStream(configuration, request, inputStream);
148+
// Configure maximum content length
149+
int maximumContentLength = getMaximumContentLength(request);
150+
httpInputStream = new HTTPInputStream(configuration, request, inputStream, maximumContentLength);
166151
request.setInputStream(httpInputStream);
167152

168153
// Set the Connection response header as soon as possible
@@ -251,8 +236,7 @@ public void run() {
251236
logger.trace("[{}] Closing socket. Client closed the connection. Reason [{}].", Thread.currentThread().threadId(), e.getMessage());
252237
closeSocketOnly(CloseSocketReason.Expected);
253238
} catch (HTTPProcessingException e) {
254-
// Note that I am only tracing this. This is sort of expected - in that it is possible that the request handler will catch this exception and handle it. If the request handler
255-
// does not handle this exception, it is totally fine to handle it here.
239+
// Note that I am only tracing this, because this exception is mostly expected. Use closeSocketOnError so we can attempt to write a response.
256240
logger.trace("[{}] Closing socket with status [{}]. An unhandled [{}] exception was taken. Reason [{}].", Thread.currentThread().threadId(), e.getStatus(), e.getClass().getSimpleName(), e.getMessage());
257241
closeSocketOnError(response, e.getStatus());
258242
} catch (TooManyBytesToDrainException e) {
@@ -327,9 +311,20 @@ private void closeSocketOnError(HTTPResponse response, int status) {
327311
response.setHeader(Headers.Connection, Connections.Close);
328312
response.setStatus(status);
329313
response.setContentLength(0L);
314+
// System.out.println("return [" + status + "]");
315+
316+
// Here
317+
// Close this sucker out!
318+
// socket.setSoLinger(true, 0);
319+
320+
// socket.shutdownInput();
321+
// socket.getInputStream().close();
330322
response.close();
331323
}
332324
} catch (IOException e) {
325+
System.out.println("\n\n\nHere!");
326+
System.out.println(e.getClass().getSimpleName());
327+
System.out.println(e.getMessage());
333328
logger.debug(String.format("[%s] Could not close the HTTP response.", Thread.currentThread().threadId()), e);
334329
} finally {
335330
// It is plausible that calling response.close() could throw an exception. We must ensure we close the socket.
@@ -349,6 +344,19 @@ private void closeSocketOnly(CloseSocketReason reason) {
349344
}
350345
}
351346

347+
private int getMaximumContentLength(HTTPRequest request) {
348+
var maximumContentLength = -1;
349+
if (ContentTypes.Form.equalsIgnoreCase(request.getContentType())) {
350+
maximumContentLength = configuration.getMaxFormDataSize();
351+
}
352+
353+
if (maximumContentLength == -1) {
354+
maximumContentLength = configuration.getMaxRequestBodySize();
355+
}
356+
357+
return maximumContentLength;
358+
}
359+
352360
private boolean handleExpectContinue(HTTPRequest request) throws IOException {
353361
var expectResponse = new HTTPResponse();
354362
configuration.getExpectValidator().validate(request, expectResponse);

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

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.io.IOException;
1919
import java.io.InputStream;
2020

21+
import io.fusionauth.http.ContentTooLargeException;
2122
import io.fusionauth.http.io.ChunkedInputStream;
2223
import io.fusionauth.http.io.PushbackInputStream;
2324
import io.fusionauth.http.log.Logger;
@@ -43,10 +44,14 @@ public class HTTPInputStream extends InputStream {
4344

4445
private final int maximumBytesToDrain;
4546

47+
private final int maximumContentLength;
48+
4649
private final PushbackInputStream pushbackInputStream;
4750

4851
private final HTTPRequest request;
4952

53+
private int bytesRead;
54+
5055
private long bytesRemaining;
5156

5257
private boolean closed;
@@ -57,14 +62,16 @@ public class HTTPInputStream extends InputStream {
5762

5863
private boolean drained;
5964

60-
public HTTPInputStream(HTTPServerConfiguration configuration, HTTPRequest request, PushbackInputStream pushbackInputStream) {
65+
public HTTPInputStream(HTTPServerConfiguration configuration, HTTPRequest request, PushbackInputStream pushbackInputStream,
66+
int maximumContentLength) {
6167
this.logger = configuration.getLoggerFactory().getLogger(HTTPInputStream.class);
6268
this.instrumenter = configuration.getInstrumenter();
6369
this.request = request;
6470
this.delegate = pushbackInputStream;
6571
this.pushbackInputStream = pushbackInputStream;
6672
this.chunkedBufferSize = configuration.getChunkedBufferSize();
6773
this.maximumBytesToDrain = configuration.getMaxBytesToDrain();
74+
this.maximumContentLength = maximumContentLength;
6875

6976
// Start the countdown
7077
if (request.getContentLength() != null) {
@@ -144,7 +151,9 @@ public int read(byte[] b, int off, int len) throws IOException {
144151

145152
// When we have a fixed length request, read beyond the remainingBytes if possible.
146153
// - If we have read past the end of the current request, push those bytes back onto the InputStream.
147-
int read = delegate.read(b, off, len);
154+
int maxLen = maximumContentLength == -1 ? len : Math.min(len, maximumContentLength - bytesRead + 1);
155+
int read = delegate.read(b, off, maxLen);
156+
148157
int reportBytesRead = read;
149158
if (fixedLength && read > 0) {
150159
int extraBytes = (int) (read - bytesRemaining);
@@ -161,14 +170,32 @@ public int read(byte[] b, int off, int len) throws IOException {
161170
}
162171

163172
// TODO : Daniel : Review : If we push back n bytes, don't we need to return read - n? This was previously read which ignored bytes pushed back.
173+
// TODO : Daniel : Write a test to prove this, send a content-length of 100, buffer size 80 (as an example), ensure this returns 80, and then
174+
// the next call returns 20?
175+
176+
177+
bytesRead += reportBytesRead;
178+
179+
// This won't cause us to fail as fast as we could, but it keeps the code a bit simpler.
180+
// - This means we will have read past the maximum by n where n is > 0 && < len. This seems like an acceptable over-read, in practice the buffers will be
181+
if (maximumContentLength != -1) {
182+
if (bytesRead > maximumContentLength) {
183+
String detailedMessage = "The maximum request size has been exceeded.The maximum request size is [" + maximumContentLength + "] bytes.";
184+
throw new ContentTooLargeException(maximumContentLength, detailedMessage);
185+
}
186+
}
187+
164188
return reportBytesRead;
165189
}
166190

167191
private void commit() {
168192
committed = true;
169193

194+
// TODO : Handle : Content-Encoding
195+
170196
// Note that isChunked() should take precedence over the fact that we have a Content-Length.
171197
// - The client should not send both, but in the case they are both present we ignore Content-Length
198+
// In practice, we will remove the Content-Length header when sent in addition to Transfer-Encoding. See HTTPWorker.validatePreamble.
172199
Long contentLength = request.getContentLength();
173200
boolean hasBody = (contentLength != null && contentLength > 0) || request.isChunked();
174201
if (!hasBody) {
@@ -184,5 +211,15 @@ private void commit() {
184211
} else {
185212
logger.trace("Client indicated it was NOT sending an entity-body in the request");
186213
}
214+
215+
// If we have a maximumContentLength, and this is a fixed content length request, before we read any bytes, fail early.
216+
// For good measure do this last so if anyone downstream wants to read from the InputStream they could in theory because
217+
// we will have set up the InputStream.
218+
if (contentLength != null && maximumContentLength != -1) {
219+
if (contentLength > maximumContentLength) {
220+
String detailedMessage = "The maximum request size has been exceeded. The reported Content-Length is [" + contentLength + "] and the maximum request size is [" + maximumContentLength + "] bytes.";
221+
throw new ContentTooLargeException(maximumContentLength, detailedMessage);
222+
}
223+
}
187224
}
188225
}

0 commit comments

Comments
 (0)