diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java index 8427ea9d8a52..374fa089af8a 100644 --- a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java @@ -1532,8 +1532,8 @@ public boolean handle(Request request, Response response, Callback callback) // Set the max number of concurrent requests, // for example in relation to the thread pool. qosHandler.setMaxRequestCount(maxThreads / 2); - // A suspended request may stay suspended for at most 15 seconds. - qosHandler.setMaxSuspend(Duration.ofSeconds(15)); + // A suspended request may stay suspended for at most 5 seconds. + qosHandler.setMaxSuspend(Duration.ofSeconds(5)); server.setHandler(qosHandler); // Provide quality of service to the shop diff --git a/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc b/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc index 849f839dddbb..6fd4234db760 100644 --- a/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc +++ b/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc @@ -343,6 +343,17 @@ However, we the RMI server is configured to bind to `localhost`, i.e. `127.0.0.1 If the system property `java.rmi.server.hostname` is not specified, the RMI client will try to connect to `127.0.1.1` (because that's what in the RMI stub) and fail because nothing is listening on that address. +[[qos]] +== Module `qos` + +The `qos` module installs the `QoSHandler` at the root of the `Handler` tree; the `QoSHandler` applies limits to the number of concurrent requests, as explained in xref:programming-guide:server/http.adoc#handler-use-qos[this section]. + +The module properties are: + +---- +include::{jetty-home}/modules/qos.mod[tags=documentation] +---- + [[requestlog]] == Module `requestlog` @@ -560,6 +571,17 @@ The module properties to configure the Jetty server scheduler are: include::{jetty-home}/modules/server.mod[tags=documentation-scheduler-config] ---- +[[size-limit]] +== Module `size-limit` + +The `size-limit` module installs the `SizeLimitHandler` at the root of the `Handler` tree; the `SizeLimitHandler` applies limits to the request content and the response content, as explained in xref:programming-guide:server/http.adoc#handler-use-size-limit[this section]. + +The module properties are: + +---- +include::{jetty-home}/modules/size-limit.mod[tags=documentation] +---- + [[ssl]] == Module `ssl` @@ -717,6 +739,17 @@ include::{jetty-home}/modules/test-keystore.mod[] Note how properties `jetty.sslContext.keyStorePath` and `jetty.sslContext.keyStorePassword` are configured, only if not already set (via the `?=` operator), directly in the module file, rather than in a `+*.ini+` file. This is done to avoid that these properties accidentally overwrite a real KeyStore configuration. +[[thread-limit]] +== Module `thread-limit` + +The `thread-limit` module installs the `ThreadLimitHandler` at the root of the `Handler` tree; the `ThreadLimitHandler` applies limits to the number of concurrent threads per remote IP address, as explained in xref:programming-guide:server/http.adoc#handler-use-thread-limit[this section]. + +The module properties are: + +---- +include::{jetty-home}/modules/thread-limit.mod[tags=documentation] +---- + [[threadpool]] == Module `threadpool` diff --git a/documentation/jetty/modules/programming-guide/pages/server/http.adoc b/documentation/jetty/modules/programming-guide/pages/server/http.adoc index 8683a74e0d55..e2dcc95efb9d 100644 --- a/documentation/jetty/modules/programming-guide/pages/server/http.adoc +++ b/documentation/jetty/modules/programming-guide/pages/server/http.adoc @@ -814,6 +814,44 @@ include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ht If the resource is not found, `ResourceHandler` will not return `true` from the `handle(\...)` method, so what happens next depends on the `Handler` tree structure. See also <> `DefaultHandler`. +[[handler-use-conditional]] +==== ConditionalHandler + +`ConditionalHandler` is an abstract `Handler` that matches conditions about the request, and allows subclasses to handle the request differently depending on whether the conditions have been met or not. + +You may subclass `ConditionalHandler.Abstract` and override these methods: + +* `boolean onConditionsMet(Request, Response, Callback)`, to handle the request when the conditions are met. +* `boolean onConditionsNotMet(Request, Response, Callback)`, to handle the request when the conditions are not met. + +Alternatively, you can use the following `ConditionalHandler` subclasses that implement common behaviors: + +* `ConditionalHandler.ElseNext`: +** When conditions are met, you have to write the implementation. +** When conditions are not met, the handling of the request is forwarded to the next `Handler`. +* `ConditionalHandler.Reject`: +** When conditions are met, a response is written with a configurable, non-2xx, HTTP status code. +** When conditions are not met, the handling of the request is forwarded to the next `Handler`. +* `ConditionalHandler.SkipNext`: +** When conditions are met, the handling of the request is forwarded to the next-next `Handler`, skipping the next `Handler`. +** When conditions are not met, the handling of the request is forwarded to the next `Handler`. +* `ConditionalHandler.DontHandle`: +** When conditions are met, the request is not handled. +** When conditions are not met, the handling of the request is forwarded to the next `Handler`. + +Conditions can be specified using an include/exclude mechanism, where a condition match if it is not excluded, and either it is included or the include set is empty. + +For example, you can specify to match only the specific HTTP request method `DELETE` by adding it to the include set; or you can specify to match all HTTP request methods apart `TRACE` by adding it to the exclude set. + +`ConditionalHandler` allows you to specify conditions for the following request properties: + +* The request HTTP method. +* The request link:{javadoc-url}/org/eclipse/jetty/server/Request.html#getPathInContext(org.eclipse.jetty.server.Request)[path in context], that is the request URI relative to the context path. +* The request remote address. +* Custom predicates that receive the request to match a custom condition. + +Notable subclasses of `ConditionalHandler` are, for example, <> and <>. + [[handler-use-gzip]] ==== GzipHandler @@ -905,7 +943,7 @@ Server └── ContextHandler N ---- -[[handler-use-sizelimit]] +[[handler-use-size-limit]] ==== SizeLimitHandler `SizeLimitHandler` tracks the sizes of request content and response content, and fails the request processing with an HTTP status code of https://www.rfc-editor.org/rfc/rfc9110.html#name-413-content-too-large[`413 Content Too Large`]. @@ -1038,9 +1076,13 @@ Refer to the `EventsHandler` link:{javadoc-url}/org/eclipse/jetty/server/handler [[handler-use-qos]] ==== QoSHandler -`QoSHandler` allows web applications to limit the number of concurrent requests, therefore implementing a quality of service (QoS) mechanism for end users. +`QoSHandler` allows web applications to limit the number of _active_ concurrent requests, that is requests that are currently being handled by a thread, and therefore are not waiting asynchronously, for example reading request content or writing response content. + +`QoSHandler` extends xref:handler-use-conditional[`ConditionalHandler`], so you may be able to restrict what `QoSHandler` does to only requests that match the conditions (for example, only to `POST` requests, or only for certain request URIs, etc.) Web applications may need to access resources with limited capacity, for example a relational database accessed through a JDBC connection pool. +Limiting the number of active concurrent requests helps to control the load on the server, so that it does not become overloaded and possibly unresponsive. +In turn, this improves the quality of service (QoS) for end users of the web application, because either their request is being handled actively by the server, or it is rejected sooner (after a configurable timeout in `QoSHandler`) rather than later (after a possibly longer idle timeout). Consider the case where each HTTP request results in a JDBC query, and the capacity of the database is of 400 queries/s. Allowing more than 400 HTTP requests/s into the system, for example 500 requests/s, results in 100 requests blocking waiting for a JDBC connection _for every second_. @@ -1049,7 +1091,7 @@ When no more threads are available, additional requests will queue up as tasks i This situation affects the whole server, so one bad behaving web application may affect other well behaving web applications. From the end user perspective the quality of service is terrible, because requests will take a lot of time to be served and eventually time out. -In cases of load spikes, caused for example by popular events (weather or social events), usage bursts (Black Friday sales), or even denial of service attacks, it is desirable to give priority to certain requests rather than others. +In cases of load spikes, caused for example by popular events (weather or social events), usage bursts (Black Friday sales), or even denial-of-service attacks, it is desirable to give priority to certain requests rather than others. For example, in an e-commerce site requests that lead to the checkout and to the payments should have higher priorities than requests to browse the products. Another example is to prioritize requests for certain users such as paying users or administrative users. @@ -1236,6 +1278,20 @@ This allows web applications that use blocking API calls such as `HttpServletReq For other types of request content, `EagerContentHandler` reads and retains request content bytes up to a configurable amount, and then invokes the next `Handler`, without any further processing of the request content bytes. This allows web applications that use blocking API calls such as `HttpServletRequest.getInputStream()` to avoid blocking in most cases (if the request is smaller than what has been configured in `EagerContentHandler`). +[[handler-use-thread-limit]] +==== ThreadLimitHandler + +`ThreadLimitHandler` tracks remote IP addresses and limits the number of concurrent requests (and therefore threads) for each remote IP address to protect against denial-of-service attacks. + +`ThreadLimitHandler` extends xref:handler-use-conditional[`ConditionalHandler`], so you may be able to restrict what `ThreadLimitHandler` does to only requests that match the conditions (for example, only to `POST` requests, or only for certain request URIs, etc.) + +The remote IP address can be derived from the network, or from the `Forwarded` (or the now obsolete `X-Forwarded-For`) HTTP header. +The `Forwarded` header is typically present in requests that have been forwarded to Jetty by a reverse proxy such as HAProxy, Nginx, Apache, etc. + +// TODO: mention the DoSHandler in Jetty 12.1.x. + +Note that `ThreadLimitHandler` is different from xref:handler-use-qos[`QoSHandler`] in that it limits the number of concurrent requests per remote IP address, while `QoSHandler` limits the total number of concurrent requests. + [[handler-use-servlet]] === Servlet API Handlers diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml index 459922747ba6..323479ec684f 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml @@ -55,6 +55,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-qos.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-qos.xml new file mode 100644 index 000000000000..935a831aa93d --- /dev/null +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-qos.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-size-limit.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-size-limit.xml new file mode 100644 index 000000000000..9779621af479 --- /dev/null +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-size-limit.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-thread-limit.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-thread-limit.xml new file mode 100644 index 000000000000..664296161f8a --- /dev/null +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-thread-limit.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-threadlimit.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-threadlimit.xml deleted file mode 100644 index 44b7b381987f..000000000000 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-threadlimit.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod index ffe2c1cb85b7..b0a1e623b630 100644 --- a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod @@ -13,7 +13,7 @@ server server [after] -threadlimit +thread-limit [xml] etc/jetty-delayed.xml diff --git a/jetty-core/jetty-server/src/main/config/modules/qos.mod b/jetty-core/jetty-server/src/main/config/modules/qos.mod new file mode 100644 index 000000000000..ceab41292e0a --- /dev/null +++ b/jetty-core/jetty-server/src/main/config/modules/qos.mod @@ -0,0 +1,47 @@ +[description] +Installs QoSHandler at the root of the `Handler` tree, +to limit the number of concurrent requests, for resource management. + +[tags] +server + +[before] +compression +gzip + +[depends] +server + +[xml] +etc/jetty-qos.xml + +[ini-template] +#tag::documentation[] +## The maximum number of concurrent requests allowed; use 0 for a default +## value calculated from the ThreadPool configuration or the number of CPU cores. +# jetty.qos.maxRequestCount=0 + +## The maximum number of requests that may be suspended. +# jetty.qos.maxSuspendedRequestCount=1024 + +## The maximum duration that a request may remain suspended, in milliseconds; use 0 for unlimited time. +# jetty.qos.maxSuspendDuration=0 + +## A comma-separated list of HTTP methods to include when matching a request. +# jetty.qos.include.method= + +## A comma-separated list of HTTP methods to exclude when matching a request. +# jetty.qos.exclude.method= + +## A comma-separated list of URI path patterns to include when matching a request. +# jetty.qos.include.path= + +## A comma-separated list of URI path patterns to exclude when matching a request. +# jetty.qos.exclude.path= + +## A comma-separated list of remote addresses patterns to include when matching a request. +# jetty.qos.include.inet= + +## A comma-separated list of remote addresses patterns to exclude when matching a request. +# jetty.qos.exclude.inet= +#end::documentation[] diff --git a/jetty-core/jetty-server/src/main/config/modules/size-limit.mod b/jetty-core/jetty-server/src/main/config/modules/size-limit.mod new file mode 100644 index 000000000000..58f43b3ac9c0 --- /dev/null +++ b/jetty-core/jetty-server/src/main/config/modules/size-limit.mod @@ -0,0 +1,25 @@ +[description] +Installs SizeLimitHandler at the root of the `Handler` tree, +to limit the request content size and response content size. + +[tags] +server + +[after] +compression +gzip + +[depends] +server + +[xml] +etc/jetty-size-limit.xml + +[ini-template] +#tag::documentation[] +## The maximum request content size in bytes, or -1 for unlimited. +# jetty.sizeLimit.maxRequestContentSize=-1 + +## The maximum response content size in bytes, or -1 for unlimited. +# jetty.sizeLimit.maxResponseContentSize=-1 +#end::documentation[] diff --git a/jetty-core/jetty-server/src/main/config/modules/thread-limit.mod b/jetty-core/jetty-server/src/main/config/modules/thread-limit.mod new file mode 100644 index 000000000000..0ba449025c6e --- /dev/null +++ b/jetty-core/jetty-server/src/main/config/modules/thread-limit.mod @@ -0,0 +1,47 @@ +[description] +Installs ThreadLimitHandler at the root of the `Handler` tree, to limit +the number of requests per IP address, for denial-of-service protection. + +[tags] +server + +[before] +compression +gzip + +[depends] +server + +[xml] +etc/jetty-thread-limit.xml + +[ini-template] +#tag::documentation[] +## Select style of reverse proxy forwarded header. +# jetty.threadlimit.forwardedHeader=X-Forwarded-For +# jetty.threadlimit.forwardedHeader=Forwarded + +## Whether thread limiting is enabled. +# jetty.threadlimit.enabled=true + +## The thread limit per remote IP address. +# jetty.threadlimit.threadLimit=10 + +## A comma-separated list of HTTP methods to include when matching a request. +# jetty.threadlimit.include.method= + +## A comma-separated list of HTTP methods to exclude when matching a request. +# jetty.threadlimit.exclude.method= + +## A comma-separated list of URI path patterns to include when matching a request. +# jetty.threadlimit.include.path= + +## A comma-separated list of URI path patterns to exclude when matching a request. +# jetty.threadlimit.exclude.path= + +## A comma-separated list of remote addresses patterns to include when matching a request. +# jetty.threadlimit.include.inet= + +## A comma-separated list of remote addresses patterns to exclude when matching a request. +# jetty.threadlimit.exclude.inet= +#end::documentation[] diff --git a/jetty-core/jetty-server/src/main/config/modules/threadlimit.mod b/jetty-core/jetty-server/src/main/config/modules/threadlimit.mod index 1276a00de93d..fcbd227bedf6 100644 --- a/jetty-core/jetty-server/src/main/config/modules/threadlimit.mod +++ b/jetty-core/jetty-server/src/main/config/modules/threadlimit.mod @@ -1,24 +1,5 @@ - [description] -Applies ThreadLimitHandler to entire server, to limit the threads per IP address for DOS protection. - -[tags] -server +DEPRECATED - use the thread-limit module instead. [depend] -server - -[xml] -etc/jetty-threadlimit.xml - -[ini-template] -## Select style of proxy forwarded header -#jetty.threadlimit.forwardedHeader=X-Forwarded-For -#jetty.threadlimit.forwardedHeader=Forwarded - -## Enabled by default? -#jetty.threadlimit.enabled=true - -## Thread limit per remote IP -#jetty.threadlimit.threadLimit=10 - +thread-limit diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SizeLimitHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SizeLimitHandler.java index 10352585930a..edbeb1a339c5 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SizeLimitHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SizeLimitHandler.java @@ -13,152 +13,14 @@ package org.eclipse.jetty.server; -import java.nio.ByteBuffer; - -import org.eclipse.jetty.http.BadMessageException; -import org.eclipse.jetty.http.HttpException; -import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.http.HttpFields; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.util.Callback; - /** - *

A {@link Handler} that can limit the size of message bodies in requests and responses.

- *

The optional request and response limits are imposed by checking the {@code Content-Length} - * header or observing the actual bytes seen by this Handler.

- *

Handler order is important; for example, if this handler is before the {@link GzipHandler}, - * then it will limit compressed sizes, if it as after the {@link GzipHandler} then it will limit - * uncompressed sizes.

- *

If a size limit is exceeded then {@link BadMessageException} is thrown with a - * {@link HttpStatus#PAYLOAD_TOO_LARGE_413} status.

+ * @deprecated use {@link org.eclipse.jetty.server.handler.SizeLimitHandler} instead. */ -public class SizeLimitHandler extends Handler.Wrapper +@Deprecated(forRemoval = true, since = "12.0.16") +public class SizeLimitHandler extends org.eclipse.jetty.server.handler.SizeLimitHandler { - private final long _requestLimit; - private final long _responseLimit; - - /** - * @param requestLimit The request body size limit in bytes or -1 for no limit - * @param responseLimit The response body size limit in bytes or -1 for no limit - */ public SizeLimitHandler(long requestLimit, long responseLimit) { - _requestLimit = requestLimit; - _responseLimit = responseLimit; - } - - @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception - { - HttpField contentLengthField = request.getHeaders().getField(HttpHeader.CONTENT_LENGTH); - if (contentLengthField != null) - { - long contentLength = contentLengthField.getLongValue(); - if (_requestLimit >= 0 && contentLength > _requestLimit) - { - String s = "Request body is too large: " + contentLength + ">" + _requestLimit; - Response.writeError(request, response, callback, HttpStatus.PAYLOAD_TOO_LARGE_413, s); - return true; - } - } - - SizeLimitRequestWrapper wrappedRequest = new SizeLimitRequestWrapper(request); - SizeLimitResponseWrapper wrappedResponse = new SizeLimitResponseWrapper(wrappedRequest, response); - return super.handle(wrappedRequest, wrappedResponse, callback); - } - - private class SizeLimitRequestWrapper extends Request.Wrapper - { - private long _read = 0; - - public SizeLimitRequestWrapper(Request wrapped) - { - super(wrapped); - } - - @Override - public Content.Chunk read() - { - Content.Chunk chunk = super.read(); - if (chunk == null) - return null; - if (chunk.getFailure() != null) - return chunk; - - // Check request content limit. - ByteBuffer content = chunk.getByteBuffer(); - if (content != null && content.remaining() > 0) - { - _read += content.remaining(); - if (_requestLimit >= 0 && _read > _requestLimit) - { - BadMessageException e = new BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413, "Request body is too large: " + _read + ">" + _requestLimit); - getWrapped().fail(e); - return null; - } - } - - return chunk; - } - } - - private class SizeLimitResponseWrapper extends Response.Wrapper - { - private final HttpFields.Mutable _httpFields; - private long _written = 0; - private HttpException.RuntimeException _failure; - - public SizeLimitResponseWrapper(Request request, Response wrapped) - { - super(request, wrapped); - - _httpFields = new HttpFields.Mutable.Wrapper(wrapped.getHeaders()) - { - @Override - public HttpField onAddField(HttpField field) - { - if (field.getHeader() == HttpHeader.CONTENT_LENGTH) - { - long contentLength = field.getLongValue(); - if (_responseLimit >= 0 && contentLength > _responseLimit) - throw new HttpException.RuntimeException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Response body is too large: " + contentLength + ">" + _responseLimit); - } - return super.onAddField(field); - } - }; - } - - @Override - public HttpFields.Mutable getHeaders() - { - return _httpFields; - } - - @Override - public void write(boolean last, ByteBuffer content, Callback callback) - { - if (_failure != null) - { - callback.failed(_failure); - return; - } - - if (content != null && content.remaining() > 0) - { - if (_responseLimit >= 0 && (_written + content.remaining()) > _responseLimit) - { - _failure = new HttpException.RuntimeException(HttpStatus.INTERNAL_SERVER_ERROR_500, - "Response body is too large: %d>%d".formatted(_written + content.remaining(), _responseLimit)); - callback.failed(_failure); - return; - } - _written += content.remaining(); - } - - super.write(last, content, callback); - } + super(requestLimit, responseLimit); } } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SizeLimitHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SizeLimitHandler.java new file mode 100644 index 000000000000..d584bf268b7c --- /dev/null +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SizeLimitHandler.java @@ -0,0 +1,167 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.server.handler; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.HttpException; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.util.Callback; + +/** + *

A {@link Handler} that can limit the size of message bodies in requests and responses.

+ *

The optional request and response limits are imposed by checking the {@code Content-Length} + * header or observing the actual bytes seen by this Handler.

+ *

Handler order is important; for example, if this handler is before the {@link GzipHandler}, + * then it will limit compressed sizes, if it as after the {@link GzipHandler} then it will limit + * uncompressed sizes.

+ *

If a size limit is exceeded then {@link BadMessageException} is thrown with a + * {@link HttpStatus#PAYLOAD_TOO_LARGE_413} status.

+ */ +public class SizeLimitHandler extends Handler.Wrapper +{ + private final long _requestLimit; + private final long _responseLimit; + + /** + * @param requestLimit The request body size limit in bytes or -1 for no limit + * @param responseLimit The response body size limit in bytes or -1 for no limit + */ + public SizeLimitHandler(long requestLimit, long responseLimit) + { + _requestLimit = requestLimit; + _responseLimit = responseLimit; + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + HttpField contentLengthField = request.getHeaders().getField(HttpHeader.CONTENT_LENGTH); + if (contentLengthField != null) + { + long contentLength = contentLengthField.getLongValue(); + if (_requestLimit >= 0 && contentLength > _requestLimit) + { + String s = "Request body is too large: " + contentLength + ">" + _requestLimit; + Response.writeError(request, response, callback, HttpStatus.PAYLOAD_TOO_LARGE_413, s); + return true; + } + } + + SizeLimitRequestWrapper wrappedRequest = new SizeLimitRequestWrapper(request); + SizeLimitResponseWrapper wrappedResponse = new SizeLimitResponseWrapper(wrappedRequest, response); + return super.handle(wrappedRequest, wrappedResponse, callback); + } + + private class SizeLimitRequestWrapper extends Request.Wrapper + { + private long _read = 0; + + public SizeLimitRequestWrapper(Request wrapped) + { + super(wrapped); + } + + @Override + public Content.Chunk read() + { + Content.Chunk chunk = super.read(); + if (chunk == null) + return null; + if (chunk.getFailure() != null) + return chunk; + + // Check request content limit. + ByteBuffer content = chunk.getByteBuffer(); + if (content != null && content.remaining() > 0) + { + _read += content.remaining(); + if (_requestLimit >= 0 && _read > _requestLimit) + { + BadMessageException e = new BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413, "Request body is too large: " + _read + ">" + _requestLimit); + getWrapped().fail(e); + return null; + } + } + + return chunk; + } + } + + private class SizeLimitResponseWrapper extends Response.Wrapper + { + private final HttpFields.Mutable _httpFields; + private long _written = 0; + private HttpException.RuntimeException _failure; + + public SizeLimitResponseWrapper(Request request, Response wrapped) + { + super(request, wrapped); + + _httpFields = new HttpFields.Mutable.Wrapper(wrapped.getHeaders()) + { + @Override + public HttpField onAddField(HttpField field) + { + if (field.getHeader() == HttpHeader.CONTENT_LENGTH) + { + long contentLength = field.getLongValue(); + if (_responseLimit >= 0 && contentLength > _responseLimit) + throw new HttpException.RuntimeException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Response body is too large: " + contentLength + ">" + _responseLimit); + } + return super.onAddField(field); + } + }; + } + + @Override + public HttpFields.Mutable getHeaders() + { + return _httpFields; + } + + @Override + public void write(boolean last, ByteBuffer content, Callback callback) + { + if (_failure != null) + { + callback.failed(_failure); + return; + } + + if (content != null && content.remaining() > 0) + { + if (_responseLimit >= 0 && (_written + content.remaining()) > _responseLimit) + { + _failure = new HttpException.RuntimeException(HttpStatus.INTERNAL_SERVER_ERROR_500, + "Response body is too large: %d>%d".formatted(_written + content.remaining(), _responseLimit)); + callback.failed(_failure); + return; + } + _written += content.remaining(); + } + + super.write(last, content, callback); + } + } +} diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java index 2648ae9f834c..47fb39c4a06e 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java @@ -46,8 +46,8 @@ import org.slf4j.LoggerFactory; /** - *

Handler to limit the threads per IP address for DOS protection

- *

The ThreadLimitHandler applies a limit to the number of Threads + *

Handler to limit the number of concurrent threads per remote IP address, for DOS protection.

+ *

ThreadLimitHandler applies a limit to the number of threads * that can be used simultaneously per remote IP address.

*

The handler makes a determination of the remote IP separately to * any that may be made by the {@link ForwardedRequestCustomizer} or similar:

@@ -103,7 +103,7 @@ protected void doStart() throws Exception LOG.info(String.format("ThreadLimitHandler enable=%b limit=%d", _enabled, _threadLimit)); } - @ManagedAttribute("true if this handler is enabled") + @ManagedAttribute("Whether this handler is enabled") public boolean isEnabled() { return _enabled; diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SizeLimitHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SizeLimitHandlerTest.java index 7c141f8696ce..2d764bf172e3 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SizeLimitHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SizeLimitHandlerTest.java @@ -32,7 +32,6 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.SizeLimitHandler; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IO; diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SizeLimitHandlerServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SizeLimitHandlerServletTest.java index 0668fa49a52d..a9c191d1778b 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SizeLimitHandlerServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SizeLimitHandlerServletTest.java @@ -36,7 +36,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.SizeLimitHandler; +import org.eclipse.jetty.server.handler.SizeLimitHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.IO; diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java index 2abbf4966ac5..6128ecac2d45 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java @@ -2158,4 +2158,57 @@ public void testEagerMultiPartContentHandler(HttpVersion httpVersion) throws Exc } } } + + @ParameterizedTest + @ValueSource(strings = {"ee8", "ee9", "ee10"}) + public void testLimitHandlers(String env) throws Exception + { + String jettyVersion = System.getProperty("jettyVersion"); + JettyHomeTester distribution = JettyHomeTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .build(); + + String[] modules = { + "http", + "qos", + "size-limit", + "thread-limit", + toEnvironment("webapp", env), + toEnvironment("deploy", env) + }; + try (JettyHomeTester.Run run1 = distribution.start("--add-modules=" + String.join(",", modules))) + { + assertTrue(run1.awaitFor(START_TIMEOUT, TimeUnit.SECONDS)); + assertEquals(0, run1.getExitValue()); + + Path jettyLogging = distribution.getJettyBase().resolve("resources/jetty-logging.properties"); + String loggingConfig = """ + org.eclipse.jetty.LEVEL=DEBUG + """; + Files.writeString(jettyLogging, loggingConfig, StandardOpenOption.TRUNCATE_EXISTING); + + Path war = distribution.resolveArtifact("org.eclipse.jetty." + env + ".demos:jetty-" + env + "-demo-simple-webapp:war:" + jettyVersion); + distribution.installWar(war, "test"); + + int port = Tester.freePort(); + try (JettyHomeTester.Run run2 = distribution.start("jetty.http.selectors=1", "jetty.http.port=" + port)) + { + try + { + assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); + + startHttpClient(); + URI serverUri = URI.create("http://localhost:" + port + "/test/"); + ContentResponse response = client.newRequest(serverUri) + .timeout(15, TimeUnit.SECONDS) + .send(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + finally + { + run2.getLogs().forEach(System.err::println); + } + } + } + } }