Skip to content

Commit eb16902

Browse files
committed
server: Allow to configure max_form_parameters across servers
- fix #3783
1 parent c7d0ad6 commit eb16902

File tree

13 files changed

+159
-39
lines changed

13 files changed

+159
-39
lines changed

jooby/src/main/java/io/jooby/Route.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,7 @@
2626
import edu.umd.cs.findbugs.annotations.NonNull;
2727
import edu.umd.cs.findbugs.annotations.Nullable;
2828
import io.jooby.annotation.Transactional;
29-
import io.jooby.exception.MethodNotAllowedException;
30-
import io.jooby.exception.NotAcceptableException;
31-
import io.jooby.exception.NotFoundException;
32-
import io.jooby.exception.StatusCodeException;
33-
import io.jooby.exception.UnsupportedMediaType;
29+
import io.jooby.exception.*;
3430

3531
/**
3632
* Route contains information about the HTTP method, path pattern, which content types consumes and
@@ -350,6 +346,19 @@ public interface Handler extends Serializable, Aware {
350346
}
351347
};
352348

349+
/** Handler for body error decoder responses. */
350+
public static final Handler FORM_DECODER_HANDLER =
351+
ctx -> {
352+
var tooManyFields = (Throwable) ctx.getAttributes().remove("__too_many_fields");
353+
BadRequestException cause;
354+
if (tooManyFields != null) {
355+
cause = new BadRequestException("Too many form fields", tooManyFields);
356+
} else {
357+
cause = new BadRequestException("Failed to decode HTTP body");
358+
}
359+
return ctx.sendError(cause);
360+
};
361+
353362
/** Handler for {@link StatusCode#REQUEST_ENTITY_TOO_LARGE} responses. */
354363
public static final Handler REQUEST_ENTITY_TOO_LARGE =
355364
ctx ->

jooby/src/main/java/io/jooby/ServerOptions.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ public class ServerOptions {
106106
*/
107107
private int maxRequestSize = _10MB;
108108

109+
/** Max number of form fields. Default: <code>1000</code>. */
110+
private int maxFormFields = 1000;
111+
109112
/** The maximum size in bytes of a http request header. Default is <code>8kb</code> */
110113
private int maxHeaderSize = _8KB;
111114

@@ -131,7 +134,7 @@ public class ServerOptions {
131134
* @param conf Configuration object.
132135
* @return Server options.
133136
*/
134-
public static @NonNull Optional<ServerOptions> from(@NonNull Config conf) {
137+
public static Optional<ServerOptions> from(@NonNull Config conf) {
135138
if (conf.hasPath("server")) {
136139
ServerOptions options = new ServerOptions();
137140
if (conf.hasPath("server.port")) {
@@ -162,6 +165,9 @@ public class ServerOptions {
162165
if (conf.hasPath("server.maxRequestSize")) {
163166
options.setMaxRequestSize((int) conf.getMemorySize("server.maxRequestSize").toBytes());
164167
}
168+
if (conf.hasPath("server.maxFormFields")) {
169+
options.setMaxFormFields(conf.getInt("server.maxFormFields"));
170+
}
165171
if (conf.hasPath("server.workerThreads")) {
166172
options.setWorkerThreads(conf.getInt("server.workerThreads"));
167173
}
@@ -445,11 +451,31 @@ public int getMaxRequestSize() {
445451
* @param maxRequestSize Max request size in bytes.
446452
* @return This options.
447453
*/
448-
public @NonNull ServerOptions setMaxRequestSize(int maxRequestSize) {
454+
public ServerOptions setMaxRequestSize(int maxRequestSize) {
449455
this.maxRequestSize = maxRequestSize;
450456
return this;
451457
}
452458

459+
/**
460+
* Max number of form fields. Default: <code>1000</code>.
461+
*
462+
* @return Max number of form fields. Default: <code>1000</code>.
463+
*/
464+
public int getMaxFormFields() {
465+
return maxFormFields;
466+
}
467+
468+
/**
469+
* Set max number of form fields. Default: <code>1000</code>.
470+
*
471+
* @param maxFormFields Max number of form fields.
472+
* @return Max number of form fields. Default: <code>1000</code>.
473+
*/
474+
public ServerOptions setMaxFormFields(int maxFormFields) {
475+
this.maxFormFields = maxFormFields;
476+
return this;
477+
}
478+
453479
/**
454480
* The maximum size in bytes of an http request header. Exceeding the size generates a different
455481
* response across server implementations.

jooby/src/main/java/io/jooby/internal/RouterMatch.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ public RouterMatch missing(String method, String path, MessageEncoder encoder) {
109109
}
110110
this.route = new Route(method, path, h);
111111
this.route.setEncoder(encoder);
112-
// this.route.setReturnType(Context.class);
113112
return this;
114113
}
115114
}

modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyContext.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,7 @@ private FileUpload register(FileUpload upload) {
697697
return upload;
698698
}
699699

700-
private static void formParam(Request request, Formdata form) {
700+
private void formParam(Request request, Formdata form) {
701701
try {
702702
var params = Request.getParameters(request);
703703
for (Fields.Field param : params) {
@@ -710,7 +710,19 @@ private static void formParam(Request request, Formdata form) {
710710
}
711711
}
712712
} catch (Exception ex) {
713-
throw SneakyThrows.propagate(ex);
713+
if (ex instanceof IllegalStateException
714+
&& ex.getMessage() != null
715+
&& ex.getMessage().startsWith("form with too many")) {
716+
this.setAttribute("__too_many_fields", ex);
717+
try {
718+
Route.FORM_DECODER_HANDLER.apply(this);
719+
} catch (Exception cause) {
720+
throw SneakyThrows.propagate(cause);
721+
}
722+
throw new JettyStopPipeline();
723+
} else {
724+
throw SneakyThrows.propagate(ex);
725+
}
714726
}
715727
}
716728
}

modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyHandler.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@ public boolean handle(Request request, Response response, Callback callback) {
3939
if (defaultHeaders) {
4040
responseHeaders.put(HttpHeader.SERVER.asString(), "J");
4141
}
42-
var context =
43-
new JettyContext(
44-
getInvocationType(), request, response, callback, router, bufferSize, maxRequestSize);
45-
router.match(context).execute(context);
42+
try {
43+
var context =
44+
new JettyContext(
45+
getInvocationType(), request, response, callback, router, bufferSize, maxRequestSize);
46+
router.match(context).execute(context);
47+
} catch (JettyStopPipeline ignored) {
48+
// handled already,
49+
}
4650
return true;
4751
}
4852
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.jetty;
7+
8+
public class JettyStopPipeline extends RuntimeException {}

modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,6 @@ public io.jooby.Server start(@NonNull Jooby... application) {
9898
var portInUse = options.getPort();
9999
try {
100100
this.applications = List.of(application);
101-
/* Set max request size attribute: */
102-
System.setProperty(
103-
"org.eclipse.jetty.server.Request.maxFormContentSize",
104-
Long.toString(options.getMaxRequestSize()));
105101

106102
addShutdownHook();
107103

@@ -114,7 +110,7 @@ public io.jooby.Server start(@NonNull Jooby... application) {
114110

115111
var acceptors = 1;
116112
var selectors = options.getIoThreads();
117-
this.server = new Server(threadPool);
113+
server = new Server(threadPool);
118114
server.setStopAtShutdown(false);
119115

120116
JettyHttp2Configurer http2 =
@@ -200,7 +196,8 @@ public io.jooby.Server start(@NonNull Jooby... application) {
200196
}
201197

202198
var context = new ContextHandler();
203-
199+
context.setAttribute(FormFields.MAX_FIELDS_ATTRIBUTE, options.getMaxFormFields());
200+
context.setAttribute(FormFields.MAX_LENGTH_ATTRIBUTE, options.getMaxRequestSize());
204201
boolean webSockets =
205202
application[0].getRoutes().stream().anyMatch(it -> it.getMethod().equals(Router.WS));
206203

modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class NettyHandler extends ChannelInboundHandlerAdapter {
3434
private final int bufferSize;
3535
private final boolean defaultHeaders;
3636
private final long maxRequestSize;
37+
private final int maxFormFields;
3738
private long contentLength;
3839
private long chunkSize;
3940
private final boolean http2;
@@ -43,13 +44,15 @@ public NettyHandler(
4344
NettyDateService dateService,
4445
List<Jooby> applications,
4546
long maxRequestSize,
47+
int maxFormFields,
4648
int bufferSize,
4749
boolean defaultHeaders,
4850
boolean http2) {
4951
this.serverDate = dateService;
5052
this.applications = applications;
5153
this.ctxSelector = Context.Selector.create(applications);
5254
this.maxRequestSize = maxRequestSize;
55+
this.maxFormFields = maxFormFields;
5356
this.bufferSize = bufferSize;
5457
this.defaultHeaders = defaultHeaders;
5558
this.http2 = http2;
@@ -79,7 +82,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) {
7982
if (contentLength > 0 || isTransferEncodingChunked(req)) {
8083
context.httpDataFactory = new DefaultHttpDataFactory(bufferSize);
8184
context.httpDataFactory.setBaseDir(app.getTmpdir().toString());
82-
context.decoder = newDecoder(req, context.httpDataFactory);
85+
context.decoder = newDecoder(req, context.httpDataFactory, maxFormFields);
8386
} else {
8487
// no body, move on
8588
router.match(context).execute(context);
@@ -90,10 +93,11 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) {
9093
try {
9194
// when decoder == null, chunk is always a LastHttpContent.EMPTY, ignore it
9295
if (context.decoder != null) {
93-
offer(context, chunk);
94-
Router.Match route = router.match(context);
95-
resetDecoderState(context, !route.matches());
96-
route.execute(context);
96+
if (offer(context, chunk)) {
97+
Router.Match route = router.match(context);
98+
resetDecoderState(context, !route.matches());
99+
route.execute(context);
100+
}
97101
}
98102
} finally {
99103
release(chunk);
@@ -172,14 +176,16 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
172176
}
173177
}
174178

175-
private void offer(NettyContext context, HttpContent chunk) {
179+
private boolean offer(NettyContext context, HttpContent chunk) {
176180
try {
177181
context.decoder.offer(chunk);
178-
} catch (HttpPostRequestDecoder.ErrorDataDecoderException
179-
| HttpPostRequestDecoder.TooLongFormFieldException
180-
| HttpPostRequestDecoder.TooManyFormFieldsException x) {
181-
resetDecoderState(context, true);
182-
context.sendError(x, StatusCode.BAD_REQUEST);
182+
return true;
183+
} catch (Exception x) {
184+
if (x instanceof HttpPostRequestDecoder.TooManyFormFieldsException) {
185+
context.setAttribute("__too_many_fields", x);
186+
}
187+
router.match(context).execute(context, Route.FORM_DECODER_HANDLER);
188+
return false;
183189
}
184190
}
185191

@@ -197,14 +203,16 @@ private void resetDecoderState(NettyContext context, boolean destroy) {
197203
}
198204

199205
private static InterfaceHttpPostRequestDecoder newDecoder(
200-
HttpRequest request, HttpDataFactory factory) {
206+
HttpRequest request, HttpDataFactory factory, int maxFormFields) {
201207
String contentType = request.headers().get(HttpHeaderNames.CONTENT_TYPE);
202208
if (contentType != null) {
203209
String lowerContentType = contentType.toLowerCase();
204210
if (lowerContentType.startsWith(MediaType.MULTIPART_FORMDATA)) {
205-
return new HttpPostMultipartRequestDecoder(factory, request, StandardCharsets.UTF_8);
211+
return new HttpPostMultipartRequestDecoder(
212+
factory, request, StandardCharsets.UTF_8, maxFormFields, -1);
206213
} else if (lowerContentType.startsWith(MediaType.FORM_URLENCODED)) {
207-
return new HttpPostStandardRequestDecoder(factory, request, StandardCharsets.UTF_8);
214+
return new HttpPostStandardRequestDecoder(
215+
factory, request, StandardCharsets.UTF_8, maxFormFields, -1);
208216
}
209217
}
210218
return new HttpRawPostRequestDecoder(factory, request);

modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class NettyPipeline extends ChannelInitializer<SocketChannel> {
2424
private final HttpDecoderConfig decoderConfig;
2525
private final List<Jooby> applications;
2626
private final long maxRequestSize;
27+
private final int maxFormFields;
2728
private final int bufferSize;
2829
private final boolean defaultHeaders;
2930
private final boolean http2;
@@ -36,6 +37,7 @@ public NettyPipeline(
3637
HttpDecoderConfig decoderConfig,
3738
List<Jooby> applications,
3839
long maxRequestSize,
40+
int maxFormFields,
3941
int bufferSize,
4042
boolean defaultHeaders,
4143
boolean http2,
@@ -46,6 +48,7 @@ public NettyPipeline(
4648
this.decoderConfig = decoderConfig;
4749
this.applications = applications;
4850
this.maxRequestSize = maxRequestSize;
51+
this.maxFormFields = maxFormFields;
4952
this.bufferSize = bufferSize;
5053
this.defaultHeaders = defaultHeaders;
5154
this.http2 = http2;
@@ -55,7 +58,7 @@ public NettyPipeline(
5558

5659
private NettyHandler createHandler() {
5760
return new NettyHandler(
58-
serverDate, applications, maxRequestSize, bufferSize, defaultHeaders, http2);
61+
serverDate, applications, maxRequestSize, maxFormFields, bufferSize, defaultHeaders, http2);
5962
}
6063

6164
@Override

modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ private NettyPipeline newPipeline(
213213
decoderConfig,
214214
applications,
215215
options.getMaxRequestSize(),
216+
options.getMaxFormFields(),
216217
bufferSize,
217218
options.getDefaultHeaders(),
218219
http2,

0 commit comments

Comments
 (0)