This is an automated email from the ASF dual-hosted git repository.

isapir pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new 9cc44dcca2 Allow different implementations for RateLimitFilter
9cc44dcca2 is described below

commit 9cc44dcca224ebc5fc4f8c835e3c64f42ab04fc1
Author: Igal Sapir <isa...@apache.org>
AuthorDate: Sat Oct 5 22:54:21 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 a3d1af57dc..3d236a77bf 100644
--- a/webapps/docs/config/filter.xml
+++ b/webapps/docs/config/filter.xml
@@ -1043,6 +1043,10 @@ FINE: Request "/docs/config/manager.html" with response 
status "200"
         Default is &quot;Too many requests&quot;.</p>
       </attribute>
 
+      <attribute name="className" required="false">
+        <p>The full class name of an implementation of the RateLimiter 
interface.
+        Default is &quot;org.apache.catalina.util.FastRateLimiter&quot;.</p>
+      </attribute>
     </attributes>
 
   </subsection>


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to