Skip to content

Commit 219a684

Browse files
PR #15288 - add per-request limits and ensure error are saved in the CompletableFuture
Signed-off-by: Lachlan Roberts <lachlan.p.roberts@gmail.com>
1 parent 0f85bde commit 219a684

2 files changed

Lines changed: 102 additions & 26 deletions

File tree

jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -400,19 +400,23 @@ else if (attr instanceof Fields fields)
400400

401401
private static int findDefaultMaxFields(Request request)
402402
{
403-
int maxLength;
403+
int maxFields;
404404
if (request != null)
405405
{
406-
maxLength = parse(request.getContext().getAttribute(FormFields.MAX_FIELDS_ATTRIBUTE));
407-
if (maxLength != -1)
408-
return maxLength;
406+
maxFields = parse(request.getAttribute(FormFields.MAX_FIELDS_ATTRIBUTE));
407+
if (maxFields != -1)
408+
return maxFields;
409+
410+
maxFields = parse(request.getContext().getAttribute(FormFields.MAX_FIELDS_ATTRIBUTE));
411+
if (maxFields != -1)
412+
return maxFields;
409413
}
410414

411415
// Use value from the system property if it is set.
412416
String property = System.getProperty(FormFields.MAX_FIELDS_ATTRIBUTE);
413-
maxLength = parse(property);
414-
if (maxLength != -1)
415-
return maxLength;
417+
maxFields = parse(property);
418+
if (maxFields != -1)
419+
return maxFields;
416420

417421
return FormFields.MAX_FIELDS_DEFAULT;
418422
}
@@ -422,6 +426,10 @@ private static int findDefaultMaxLength(Request request)
422426
int maxLength;
423427
if (request != null)
424428
{
429+
maxLength = parse(request.getAttribute(FormFields.MAX_LENGTH_ATTRIBUTE));
430+
if (maxLength != -1)
431+
return maxLength;
432+
425433
maxLength = parse(request.getContext().getAttribute(FormFields.MAX_LENGTH_ATTRIBUTE));
426434
if (maxLength != -1)
427435
return maxLength;
@@ -450,6 +458,7 @@ private static int parse(Object value)
450458
}
451459
}
452460

461+
private final Content.Source _content;
453462
private final Fields _fields;
454463
private final CharsetStringBuilder _builder;
455464
private final int _maxFields;
@@ -462,19 +471,22 @@ private static int parse(Object value)
462471
private FormFields(Content.Source source, InvocationType invocationType, Charset charset, int maxFields, int maxSize)
463472
{
464473
super(source, invocationType);
474+
_content = source;
465475
_maxFields = maxFields;
466476
_maxLength = maxSize;
467477
_builder = CharsetStringBuilder.forCharset(charset);
468478
_fields = new Fields(true);
469-
if (_maxLength > 0 && source.getLength() > _maxLength)
470-
throw new HttpException.IllegalStateException(HttpStatus.PAYLOAD_TOO_LARGE_413, "form too large > " + _maxLength);
471479
}
472480

473481
@Override
474482
protected Fields parse(Content.Chunk chunk)
475483
{
476484
if (_maxLength >= 0)
477485
{
486+
// Fast fail here if content-length is known.
487+
if (_length == 0 && _content.getLength() > _maxLength)
488+
throw new HttpException.IllegalStateException(HttpStatus.PAYLOAD_TOO_LARGE_413, "form too large > " + _maxLength);
489+
478490
_length += chunk.remaining();
479491
if (_length > _maxLength)
480492
throw new HttpException.IllegalStateException(HttpStatus.PAYLOAD_TOO_LARGE_413, "form too large > " + _maxLength);

jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/EagerContentHandlerTest.java

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.concurrent.CountDownLatch;
2323
import java.util.concurrent.Exchanger;
2424
import java.util.concurrent.TimeUnit;
25+
import java.util.stream.Stream;
2526

2627
import org.awaitility.Awaitility;
2728
import org.eclipse.jetty.http.HttpException;
@@ -44,6 +45,9 @@
4445
import org.junit.jupiter.api.AfterEach;
4546
import org.junit.jupiter.api.BeforeEach;
4647
import org.junit.jupiter.api.Test;
48+
import org.junit.jupiter.params.ParameterizedTest;
49+
import org.junit.jupiter.params.provider.Arguments;
50+
import org.junit.jupiter.params.provider.MethodSource;
4751

4852
import static org.hamcrest.MatcherAssert.assertThat;
4953
import static org.hamcrest.Matchers.containsString;
@@ -536,12 +540,10 @@ public boolean handle(Request request, Response response, Callback callback) thr
536540
@Test
537541
public void testEagerFormFieldsLimits() throws Exception
538542
{
539-
// Set the global form limits for maxFormContentSize.
540-
System.setProperty("org.eclipse.jetty.server.Request.maxFormKeys", "-1");
541-
System.setProperty("org.eclipse.jetty.server.Request.maxFormContentSize", "15");
543+
// Set the server context limit for maxFormContentSize.
544+
_server.getContext().setAttribute("org.eclipse.jetty.server.Request.maxFormContentSize", "15");
542545

543-
CountDownLatch processing = new CountDownLatch(1);
544-
CompletableFuture<Throwable> contentLoaderErrorFuture = new CompletableFuture<>();
546+
CountDownLatch processing = new CountDownLatch(2);
545547
CompletableFuture<Throwable> handlerErrorFuture = new CompletableFuture<>();
546548
EagerContentHandler eagerContentHandler = new EagerContentHandler(new EagerContentHandler.FormContentLoaderFactory()
547549
{
@@ -554,16 +556,8 @@ public EagerContentHandler.ContentLoader newContentLoader(String contentType, St
554556
@Override
555557
protected void load() throws Exception
556558
{
557-
try
558-
{
559-
contentLoader.load();
560-
processing.countDown();
561-
}
562-
catch (Throwable t)
563-
{
564-
contentLoaderErrorFuture.complete(t);
565-
throw t;
566-
}
559+
contentLoader.load();
560+
processing.countDown();
567561
}
568562
};
569563
}
@@ -605,8 +599,8 @@ public boolean handle(Request request, Response response, Callback callback) thr
605599
output.write(request.getBytes(StandardCharsets.UTF_8));
606600
output.flush();
607601

608-
// The EagerContentHandler's ContentLoader should throw so we never reach the handler.
609-
Throwable throwable = contentLoaderErrorFuture.get(5, TimeUnit.SECONDS);
602+
// The failure should not be thrown from the content loader but the handler.
603+
Throwable throwable = handlerErrorFuture.get(5, TimeUnit.SECONDS);
610604
assertThat(throwable, instanceOf(HttpException.IllegalStateException.class));
611605

612606
// The response code should be 413 PAYLOAD_TOO_LARGE.
@@ -658,6 +652,76 @@ public boolean handle(Request request, Response response, Callback callback) thr
658652
}
659653
}
660654

655+
public static Stream<Arguments> formLimitsProvider()
656+
{
657+
// The form content has a size of 27 bytes with 2 fields.
658+
return Stream.of(
659+
Arguments.of(-1, -1, HttpStatus.OK_200),
660+
Arguments.of(100, 100, HttpStatus.OK_200),
661+
Arguments.of(2, -1, HttpStatus.OK_200),
662+
Arguments.of(1, -1, HttpStatus.PAYLOAD_TOO_LARGE_413),
663+
Arguments.of(-1, 27, HttpStatus.OK_200),
664+
Arguments.of(-1, 26, HttpStatus.PAYLOAD_TOO_LARGE_413)
665+
);
666+
}
667+
668+
@ParameterizedTest
669+
@MethodSource("formLimitsProvider")
670+
public void perRequestFormLimitsTest(int maxFormFields, int maxFormLength, int expectedStatusCode) throws Exception
671+
{
672+
_server.setHandler(new Handler.Abstract()
673+
{
674+
@Override
675+
public boolean handle(Request request, Response response, Callback callback) throws Exception
676+
{
677+
String maxFieldsAttribute = request.getHeaders().get(FormFields.MAX_FIELDS_ATTRIBUTE);
678+
if (maxFieldsAttribute != null)
679+
request.setAttribute(FormFields.MAX_FIELDS_ATTRIBUTE, maxFieldsAttribute);
680+
681+
String maxLengthAttribute = request.getHeaders().get(FormFields.MAX_LENGTH_ATTRIBUTE);
682+
if (maxLengthAttribute != null)
683+
request.setAttribute(FormFields.MAX_LENGTH_ATTRIBUTE, maxLengthAttribute);
684+
685+
Fields fields = FormFields.getFields(request);
686+
Content.Sink.write(response, true, String.valueOf(fields), callback);
687+
return true;
688+
}
689+
});
690+
_server.start();
691+
692+
HttpTester.Response response = getResponse(maxFormFields, maxFormLength);
693+
assertThat(response.getStatus(), is(expectedStatusCode));
694+
}
695+
696+
private HttpTester.Response getResponse(int maxFormFields, int maxFormLength) throws Exception
697+
{
698+
try (Socket socket = new Socket("localhost", _connector.getLocalPort()))
699+
{
700+
// Write the first request content which does not exceed the form limits.
701+
StringBuilder request = new StringBuilder();
702+
request.append("""
703+
POST /foo HTTP/1.1\r
704+
Host: localhost\r
705+
""");
706+
if (maxFormFields != -1)
707+
request.append(FormFields.MAX_FIELDS_ATTRIBUTE).append(": ").append(maxFormFields).append("\r\n");
708+
if (maxFormLength != -1)
709+
request.append(FormFields.MAX_LENGTH_ATTRIBUTE).append(": ").append(maxFormLength).append("\r\n");
710+
request.append("""
711+
Content-Type: application/x-www-form-urlencoded\r
712+
Content-Length: 27\r
713+
\r
714+
param1=value1&param2=value2\
715+
""");
716+
OutputStream output = socket.getOutputStream();
717+
output.write(request.toString().getBytes(StandardCharsets.UTF_8));
718+
output.flush();
719+
720+
HttpTester.Input input = HttpTester.from(socket.getInputStream());
721+
return HttpTester.parseResponse(input);
722+
}
723+
}
724+
661725
@Test
662726
public void testDirectCallFormFields() throws Exception
663727
{

0 commit comments

Comments
 (0)