This is an automated email from the ASF dual-hosted git repository.
isapir pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/10.1.x by this push:
new ae96cbd19c Allow different implementations for RateLimitFilter
ae96cbd19c is described below
commit ae96cbd19ca776e74f701b1bca0014c2529a47f3
Author: Igal Sapir <[email protected]>
AuthorDate: Sat Oct 5 23:34:14 2024 -0700
Allow different implementations for RateLimitFilter
---
.../apache/catalina/filters/RateLimitFilter.java | 82 ++++++++++----------
java/org/apache/catalina/util/FastRateLimiter.java | 88 ++++++++++++++++++++++
java/org/apache/catalina/util/RateLimiter.java | 67 ++++++++++++++++
.../catalina/filters/TestRateLimitFilter.java | 7 +-
webapps/docs/config/filter.xml | 4 +
5 files changed, 206 insertions(+), 42 deletions(-)
diff --git a/java/org/apache/catalina/filters/RateLimitFilter.java
b/java/org/apache/catalina/filters/RateLimitFilter.java
index fe4674ccb7..a6cccbea30 100644
--- a/java/org/apache/catalina/filters/RateLimitFilter.java
+++ b/java/org/apache/catalina/filters/RateLimitFilter.java
@@ -18,7 +18,7 @@
package org.apache.catalina.filters;
import java.io.IOException;
-import java.util.concurrent.ScheduledExecutorService;
+import java.lang.reflect.InvocationTargetException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
@@ -28,11 +28,10 @@ import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
-import org.apache.catalina.util.TimeBucketCounter;
+import org.apache.catalina.util.RateLimiter;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.res.StringManager;
-import org.apache.tomcat.util.threads.ScheduledThreadPoolExecutor;
/**
* <p>
@@ -46,11 +45,13 @@ import
org.apache.tomcat.util.threads.ScheduledThreadPoolExecutor;
* the bucket time ends and a new bucket starts.
* </p>
* <p>
- * The filter is optimized for efficiency and low overhead, so it converts
some configured values to more efficient
- * values. For example, a configuration of a 60 seconds time bucket is
converted to 65.536 seconds. That allows for very
- * fast bucket calculation using bit shift arithmetic. In order to remain true
to the user intent, the configured number
- * of requests is then multiplied by the same ratio, so a configuration of 100
Requests per 60 seconds, has the real
- * values of 109 Requests per 65 seconds.
+ * The RateLimiter implementation can be set via the <code>className</code>
init param. The default implementation,
+ * <code>org.apache.catalina.util.FastRateLimiter</code>, is optimized for
efficiency and low overhead so it converts
+ * some configured values to more efficient values. For example, a
configuration of a 60 seconds time bucket is
+ * converted to 65.536 seconds. That allows for very fast bucket calculation
using bit shift arithmetic. In order to
+ * remain true to the user intent, the configured number of requests is then
multiplied by the same ratio, so a
+ * configuration of 100 Requests per 60 seconds, has the real values of 109
Requests per 65 seconds. You can specify
+ * a different class as long as it implements the
<code>org.apache.catalina.util.RateLimiter</code> interface.
* </p>
* <p>
* It is common to set up different restrictions for different URIs. For
example, a login page or authentication script
@@ -125,14 +126,19 @@ public class RateLimitFilter extends GenericFilter {
*/
public static final String PARAM_STATUS_CODE = "statusCode";
+ /**
+ * init-param to set a class name that implements RateLimiter
+ */
+ public static final String PARAM_CLASS_NAME = "className";
+
/**
* init-param to set a custom status message if requests per duration
exceeded
*/
public static final String PARAM_STATUS_MESSAGE = "statusMessage";
- transient TimeBucketCounter bucketCounter;
+ transient RateLimiter rateLimiter;
- private int actualRequests;
+ private String rateLimitClassName =
"org.apache.catalina.util.FastRateLimiter";
private int bucketRequests = DEFAULT_BUCKET_REQUESTS;
@@ -148,20 +154,6 @@ public class RateLimitFilter extends GenericFilter {
private static final StringManager sm =
StringManager.getManager(RateLimitFilter.class);
- /**
- * @return the actual maximum allowed requests per time bucket
- */
- public int getActualRequests() {
- return actualRequests;
- }
-
- /**
- * @return the actual duration of a time bucket in milliseconds
- */
- public int getActualDurationInSeconds() {
- return bucketCounter.getActualDuration() / 1000;
- }
-
@Override
public void init() throws ServletException {
@@ -193,18 +185,26 @@ public class RateLimitFilter extends GenericFilter {
statusMessage = param;
}
- ScheduledExecutorService executorService = (ScheduledExecutorService)
getServletContext()
- .getAttribute(ScheduledThreadPoolExecutor.class.getName());
- if (executorService == null) {
- executorService = new
java.util.concurrent.ScheduledThreadPoolExecutor(1);
+ param = config.getInitParameter(PARAM_CLASS_NAME);
+ if (param != null) {
+ rateLimitClassName = param;
+ }
+
+ try {
+ rateLimiter =
(RateLimiter)Class.forName(rateLimitClassName).getConstructor().newInstance();
+ } catch (InstantiationException | IllegalAccessException |
InvocationTargetException |
+ NoSuchMethodException | ClassNotFoundException e) {
+ throw new ServletException(e);
}
- bucketCounter = new TimeBucketCounter(bucketDuration, executorService);
- actualRequests = (int) Math.round(bucketCounter.getRatio() *
bucketRequests);
+ rateLimiter.setDuration(bucketDuration);
+ rateLimiter.setRequests(bucketRequests);
+ rateLimiter.setFilterConfig(super.getFilterConfig());
- log.info(sm.getString("rateLimitFilter.initialized",
super.getFilterName(), Integer.valueOf(bucketRequests),
- Integer.valueOf(bucketDuration),
Integer.valueOf(getActualRequests()),
- Integer.valueOf(getActualDurationInSeconds()), (!enforce ?
"Not " : "") + "enforcing"));
+ log.info(sm.getString("rateLimitFilter.initialized",
super.getFilterName(),
+ Integer.valueOf(bucketRequests), Integer.valueOf(bucketDuration),
+ Integer.valueOf(rateLimiter.getRequests()),
Integer.valueOf(rateLimiter.getDuration()),
+ (!enforce ? "Not " : "") + "enforcing"));
}
@Override
@@ -212,18 +212,20 @@ public class RateLimitFilter extends GenericFilter {
throws IOException, ServletException {
String ipAddr = request.getRemoteAddr();
- int reqCount = bucketCounter.increment(ipAddr);
+ int reqCount = rateLimiter.increment(ipAddr);
request.setAttribute(RATE_LIMIT_ATTRIBUTE_COUNT,
Integer.valueOf(reqCount));
- if (enforce && (reqCount > actualRequests)) {
+ if (reqCount > rateLimiter.getRequests()) {
- ((HttpServletResponse) response).sendError(statusCode,
statusMessage);
log.warn(sm.getString("rateLimitFilter.maxRequestsExceeded",
super.getFilterName(),
- Integer.valueOf(reqCount), ipAddr,
Integer.valueOf(getActualRequests()),
- Integer.valueOf(getActualDurationInSeconds())));
+ Integer.valueOf(reqCount), ipAddr,
Integer.valueOf(rateLimiter.getRequests()),
+ Integer.valueOf(rateLimiter.getDuration())));
- return;
+ if (enforce) {
+ ((HttpServletResponse) response).sendError(statusCode,
statusMessage);
+ return;
+ }
}
chain.doFilter(request, response);
@@ -231,7 +233,7 @@ public class RateLimitFilter extends GenericFilter {
@Override
public void destroy() {
- this.bucketCounter.destroy();
+ rateLimiter.destroy();
super.destroy();
}
}
diff --git a/java/org/apache/catalina/util/FastRateLimiter.java
b/java/org/apache/catalina/util/FastRateLimiter.java
new file mode 100644
index 0000000000..bf2404ed1b
--- /dev/null
+++ b/java/org/apache/catalina/util/FastRateLimiter.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.util;
+
+import jakarta.servlet.FilterConfig;
+import org.apache.tomcat.util.threads.ScheduledThreadPoolExecutor;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * A RateLimiter that compromises accuracy for speed in order to provide
maximum throughput
+ */
+public class FastRateLimiter implements RateLimiter {
+
+ TimeBucketCounter bucketCounter;
+
+ int duration;
+
+ int requests;
+
+ int actualRequests;
+
+ int actualDuration;
+
+ @Override
+ public int getDuration() {
+ return actualDuration;
+ }
+
+ @Override
+ public void setDuration(int duration) {
+ this.duration = duration;
+ }
+
+ @Override
+ public int getRequests() {
+ return actualRequests;
+ }
+
+ @Override
+ public void setRequests(int requests) {
+ this.requests = requests;
+ }
+
+ @Override
+ public int increment(String ipAddress) {
+ return bucketCounter.increment(ipAddress);
+ }
+
+ @Override
+ public void destroy() {
+ bucketCounter.destroy();
+ }
+
+ @Override
+ public void setFilterConfig(FilterConfig filterConfig) {
+
+ ScheduledExecutorService executorService = (ScheduledExecutorService)
filterConfig.getServletContext()
+ .getAttribute(ScheduledThreadPoolExecutor.class.getName());
+
+ if (executorService == null) {
+ executorService = new
java.util.concurrent.ScheduledThreadPoolExecutor(1);
+ }
+
+ bucketCounter = new TimeBucketCounter(duration, executorService);
+ actualRequests = (int) Math.round(bucketCounter.getRatio() * requests);
+ actualDuration = bucketCounter.getActualDuration() / 1000;
+ }
+
+ public TimeBucketCounter getBucketCounter() {
+ return bucketCounter;
+ }
+}
diff --git a/java/org/apache/catalina/util/RateLimiter.java
b/java/org/apache/catalina/util/RateLimiter.java
new file mode 100644
index 0000000000..fb3bb0d855
--- /dev/null
+++ b/java/org/apache/catalina/util/RateLimiter.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.util;
+
+import jakarta.servlet.FilterConfig;
+
+public interface RateLimiter {
+
+ /**
+ * @return the actual duration of a time window in seconds
+ */
+ int getDuration();
+
+ /**
+ * sets the configured duration value in seconds
+ *
+ * @param duration
+ */
+ void setDuration(int duration);
+
+ /**
+ * @return the maximum number of requests allowed per time window
+ */
+ int getRequests();
+
+ /**
+ * sets the configured number of requests allowed per time window
+ *
+ * @param requests
+ */
+ void setRequests(int requests);
+
+ /**
+ * increments the number of requests by the given ipAddress in the current
time window
+ *
+ * @param ipAddress the ip address
+ * @return the new value after incrementing
+ */
+ int increment(String ipAddress);
+
+ /**
+ * cleanup no longer needed resources
+ */
+ void destroy();
+
+ /**
+ * pass the FilterConfig to configure the filter
+ *
+ * @param filterConfig
+ */
+ void setFilterConfig(FilterConfig filterConfig);
+}
diff --git a/test/org/apache/catalina/filters/TestRateLimitFilter.java
b/test/org/apache/catalina/filters/TestRateLimitFilter.java
index 0d918285c7..ff9bdbf351 100644
--- a/test/org/apache/catalina/filters/TestRateLimitFilter.java
+++ b/test/org/apache/catalina/filters/TestRateLimitFilter.java
@@ -24,6 +24,7 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
+import org.apache.catalina.util.FastRateLimiter;
import org.junit.Assert;
import org.junit.Test;
@@ -55,9 +56,11 @@ public class TestRateLimitFilter extends TomcatBaseTest {
MockFilterChain filterChain = new MockFilterChain();
RateLimitFilter rateLimitFilter = testRateLimitFilter(filterDef, root);
- int allowedRequests = (int)
Math.round(rateLimitFilter.bucketCounter.getRatio() * bucketRequests);
+ FastRateLimiter tbc = (FastRateLimiter) rateLimitFilter.rateLimiter;
- long sleepTime =
rateLimitFilter.bucketCounter.getMillisUntilNextBucket();
+ int allowedRequests = (int)
Math.round(tbc.getBucketCounter().getRatio() * bucketRequests);
+
+ long sleepTime = tbc.getBucketCounter().getMillisUntilNextBucket();
System.out.printf("Sleeping %d millis for the next time bucket to
start\n", Long.valueOf(sleepTime));
Thread.sleep(sleepTime);
diff --git a/webapps/docs/config/filter.xml b/webapps/docs/config/filter.xml
index 8dcbd39ef8..577ea5542b 100644
--- a/webapps/docs/config/filter.xml
+++ b/webapps/docs/config/filter.xml
@@ -1097,6 +1097,10 @@ FINE: Request "/docs/config/manager.html" with response
status "200"
Default is "Too many requests".</p>
</attribute>
+ <attribute name="className" required="false">
+ <p>The full class name of an implementation of the RateLimiter
interface.
+ Default is "org.apache.catalina.util.FastRateLimiter".</p>
+ </attribute>
</attributes>
</subsection>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]