diff --git a/java/org/apache/catalina/filters/RateLimitFilter.java b/java/org/apache/catalina/filters/RateLimitFilter.java
index 8e07ca81828a..80315948bb33 100644
--- a/java/org/apache/catalina/filters/RateLimitFilter.java
+++ b/java/org/apache/catalina/filters/RateLimitFilter.java
@@ -47,8 +47,11 @@
* 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 org.apache.catalina.util.RateLimiter
interface.
+ * configuration of 100 Requests per 60 seconds, has the real values of 109 Requests per 65 seconds. An alternative
+ * implementation, org.apache.catalina.util.ExactRateLimiter
, is intended to provide a less efficient but
+ * more accurate control, whose effective duration in seconds and number of requests configuration are consist with the
+ * user declared. You can specify a different class as long as it implements the
+ * org.apache.catalina.util.RateLimiter
interface.
*
* It is common to set up different restrictions for different URIs. For example, a login page or authentication script
diff --git a/java/org/apache/catalina/util/ExactRateLimiter.java b/java/org/apache/catalina/util/ExactRateLimiter.java
new file mode 100644
index 000000000000..7be1c2575176
--- /dev/null
+++ b/java/org/apache/catalina/util/ExactRateLimiter.java
@@ -0,0 +1,66 @@
+/*
+ * 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 java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * A RateLimiter that compromises efficiency for accurate in order to provide exact rate limiting.
+ */
+public class ExactRateLimiter extends RateLimiterBase {
+
+ @Override
+ protected String getDefaultPolicyName() {
+ return "exact";
+ }
+
+ @Override
+ protected TimeBucketCounterBase newCounterInstance(ScheduledExecutorService executorService, int duration) {
+ return new ExactTimeBucketCounter(executorService, duration);
+ }
+
+ /**
+ * An accurate counter with exact bucket index, but slightly less efficient than another fast counter provided with
+ * the {@link FastRateLimiter}.
+ */
+ class ExactTimeBucketCounter extends TimeBucketCounterBase {
+
+ ExactTimeBucketCounter(ScheduledExecutorService executorService, int bucketDuration) {
+ super(executorService, bucketDuration);
+ }
+
+ @Override
+ public long getBucketIndex(long timestamp) {
+ return (timestamp / 1000) / getBucketDuration();
+ }
+
+ @Override
+ public double getRatio() {
+ // actual value is exactly same with declared.
+ return 1.0d;
+ }
+
+ @Override
+ public long getMillisUntilNextBucket() {
+ long millis = System.currentTimeMillis();
+
+ long nextTimeBucketMillis = (getBucketIndex(millis) + 1) * getBucketDuration() * 1000;
+ long delta = nextTimeBucketMillis - millis;
+ return delta;
+ }
+ }
+}
diff --git a/java/org/apache/catalina/util/FastRateLimiter.java b/java/org/apache/catalina/util/FastRateLimiter.java
index 486a133a6489..d4feb93a4a68 100644
--- a/java/org/apache/catalina/util/FastRateLimiter.java
+++ b/java/org/apache/catalina/util/FastRateLimiter.java
@@ -18,88 +18,24 @@
package org.apache.catalina.util;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import jakarta.servlet.FilterConfig;
-
-import org.apache.tomcat.util.threads.ScheduledThreadPoolExecutor;
/**
* A RateLimiter that compromises accuracy for speed in order to provide maximum throughput.
*/
-public class FastRateLimiter implements RateLimiter {
-
- private static AtomicInteger index = new AtomicInteger();
-
- TimeBucketCounter bucketCounter;
-
- int duration;
-
- int requests;
-
- int actualRequests;
-
- int actualDuration;
-
- // Initial policy name can be rewritten by setPolicyName()
- private String policyName = "fast-" + index.incrementAndGet();
-
- @Override
- public String getPolicyName() {
- return policyName;
- }
-
- @Override
- public void setPolicyName(String name) {
- this.policyName = name;
- }
-
- @Override
- public int getDuration() {
- return actualDuration;
- }
-
- @Override
- public void setDuration(int duration) {
- this.duration = duration;
- }
-
- @Override
- public int getRequests() {
- return actualRequests;
- }
+public class FastRateLimiter extends RateLimiterBase implements RateLimiter {
@Override
- public void setRequests(int requests) {
- this.requests = requests;
+ protected String getDefaultPolicyName() {
+ return "fast";
}
@Override
- public int increment(String ipAddress) {
- return bucketCounter.increment(ipAddress);
+ protected TimeBucketCounterBase newCounterInstance(ScheduledExecutorService executorService, int duration) {
+ return new TimeBucketCounter(executorService, duration);
}
@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;
+ return (TimeBucketCounter)bucketCounter;
}
}
diff --git a/java/org/apache/catalina/util/RateLimiter.java b/java/org/apache/catalina/util/RateLimiter.java
index d1beb653990d..8ca41937a85b 100644
--- a/java/org/apache/catalina/util/RateLimiter.java
+++ b/java/org/apache/catalina/util/RateLimiter.java
@@ -46,13 +46,13 @@ public interface RateLimiter {
void setRequests(int requests);
/**
- * Increments the number of requests by the given ipAddress in the current time window.
+ * Increments the number of requests by the given identifier in the current time window.
*
- * @param ipAddress the ip address
+ * @param identifier of target request
*
* @return the new value after incrementing
*/
- int increment(String ipAddress);
+ int increment(String identifier);
/**
* Cleanup no longer needed resources.
diff --git a/java/org/apache/catalina/util/RateLimiterBase.java b/java/org/apache/catalina/util/RateLimiterBase.java
new file mode 100644
index 000000000000..589d80b1a84c
--- /dev/null
+++ b/java/org/apache/catalina/util/RateLimiterBase.java
@@ -0,0 +1,143 @@
+/*
+ * 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 java.util.Objects;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.servlet.FilterConfig;
+
+/**
+ * Basic implementation of {@link RateLimiter}, provides runtime data maintenance mechanism monitor.
+ */
+public abstract class RateLimiterBase implements RateLimiter {
+
+ private static final AtomicInteger index = new AtomicInteger();
+
+ TimeBucketCounterBase bucketCounter;
+
+ int requests;
+ int actualRequests;
+
+ int duration;
+ int actualDuration;
+
+ // Initial policy name can be rewritten by setPolicyName()
+ private String policyName = null;
+
+ /**
+ * If policy name has not been specified, the first call of {@link #getPolicyName()} returns a auto-generated policy
+ * name using the default policy name as prefix and followed by auto-increase index.
+ *
+ * @return default policy name, as a prefix of auto-generated policy name.
+ */
+ protected abstract String getDefaultPolicyName();
+
+ @Override
+ public String getPolicyName() {
+ if (policyName == null) {
+ policyName = getDefaultPolicyName() + "-" + index.incrementAndGet();
+ }
+ return policyName;
+ }
+
+ @Override
+ public void setPolicyName(String name) {
+ Objects.requireNonNull(name);
+ this.policyName = name;
+ }
+
+ @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 identifier) {
+ return bucketCounter.increment(identifier);
+ }
+
+ @Override
+ public void destroy() {
+ bucketCounter.destroy();
+ if (newExecutorService != null) {
+ try {
+ newExecutorService.shutdown();
+ } catch (SecurityException e) {
+ // ignore
+ }
+ }
+ }
+
+ /**
+ * Instantiate an instance of {@link TimeBucketCounterBase} for specific time bucket size. Concrete classes determine
+ * its counter policy by returning different implementation instance.
+ *
+ * @param utilityExecutor the executor
+ * @param duration size of each time bucket in seconds
+ *
+ * @return counter instance of {@link TimeBucketCounterBase}
+ */
+ protected abstract TimeBucketCounterBase newCounterInstance(ScheduledExecutorService utilityExecutor, int duration);
+
+ @Override
+ public void setFilterConfig(FilterConfig filterConfig) {
+
+ ScheduledExecutorService executorService = (ScheduledExecutorService) filterConfig.getServletContext()
+ .getAttribute(ScheduledThreadPoolExecutor.class.getName());
+
+ if (executorService == null) {
+ newExecutorService = new java.util.concurrent.ScheduledThreadPoolExecutor(1);
+ executorService = newExecutorService;
+ }
+
+ bucketCounter = newCounterInstance(executorService, duration);
+ actualDuration = bucketCounter.getBucketDuration();
+ actualRequests = (int) Math.round(bucketCounter.getRatio() * requests);
+ }
+ /**
+ * Returns the internal instance of {@link TimeBucketCounterBase}
+ *
+ * @return instance of {@link TimeBucketCounterBase}
+ */
+ public TimeBucketCounterBase getBucketCounter() {
+ return bucketCounter;
+ }
+
+ /**
+ * The self-owned utility executor, will be instantiated only when ScheduledThreadPoolExecutor is absent during
+ * filter configure phase.
+ */
+ private ScheduledThreadPoolExecutor newExecutorService = null;
+}
diff --git a/java/org/apache/catalina/util/TimeBucketCounter.java b/java/org/apache/catalina/util/TimeBucketCounter.java
index 3b4726f7ff4c..b4bde87ee219 100644
--- a/java/org/apache/catalina/util/TimeBucketCounter.java
+++ b/java/org/apache/catalina/util/TimeBucketCounter.java
@@ -14,34 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package org.apache.catalina.util;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.apache.juli.logging.Log;
-import org.apache.juli.logging.LogFactory;
-import org.apache.tomcat.util.res.StringManager;
/**
- * This class maintains a thread safe hash map that has timestamp-based buckets followed by a string for a key, and a
- * counter for a value. each time the increment() method is called it adds the key if it does not exist, increments its
- * value and returns it. a maintenance thread cleans up keys that are prefixed by previous timestamp buckets.
+ * A fast counter that optimizes efficiency at the expense of approximate bucket indexing.
*/
-public class TimeBucketCounter {
+public class TimeBucketCounter extends TimeBucketCounterBase {
- private static final Log log = LogFactory.getLog(TimeBucketCounter.class);
- private static final StringManager sm = StringManager.getManager(TimeBucketCounter.class);
+ TimeBucketCounter(int bucketDuration,ScheduledExecutorService utilityExecutor) {
+ this(utilityExecutor,bucketDuration);
+ }
- /**
- * Map to hold the buckets
- */
- private final ConcurrentHashMap
+ * WARNING: This method is used for test purpose.
+ *
+ * @return the number of milliseconds until the next bucket
+ */
+ @Deprecated
+ public abstract long getMillisUntilNextBucket();
+
+ /**
+ * Destroy resources
+ */
+ public void destroy() {
+ this.map.clear();
+ if (monitorFuture != null) {
+ monitorFuture.cancel(true);
+ monitorFuture = null;
+ }
+ if (maintenanceFuture != null) {
+ maintenanceFuture.cancel(true);
+ maintenanceFuture = null;
+ }
+ }
+
+ /**
+ * Periodic evict, perform removal of obsolete bucket items. Absence of this operation may result in OOM after a
+ * long run.
+ */
+ public void periodicEvict() {
+ final long minBucketIndex = getBucketIndex(System.currentTimeMillis());
+ // assume that elapsed time of periodicEvict less than 1x bucketDuration.
+ // to avoid extreme case: 999999-xxx vs 1000000-xxx
+ final long maxBucket = minBucketIndex + 2;
+
+ final String minBucketPrefix = minBucketIndex + BUCKET_KEY_DELIMITER;
+ final String maxBucketPrefix = maxBucket + BUCKET_KEY_DELIMITER;
+
+ // remove obsolete items whose key are less than minBucketPrefix and maxBucketPrefix in same time.
+ map.keySet().removeIf(k -> k.compareTo(minBucketPrefix) < 0 && k.compareTo(maxBucketPrefix) < 0);
+ }
+
+
+ private class Maintenance implements Runnable {
+ @Override
+ public void run() {
+ periodicEvict();
+ }
+ }
+
+ private class MaintenanceMonitor implements Runnable {
+ @Override
+ public void run() {
+ if (sleeptime > 0 && (maintenanceFuture == null || maintenanceFuture.isDone())) {
+ if (maintenanceFuture != null && maintenanceFuture.isDone()) {
+ // There was an error executing the scheduled task, get it and log it
+ try {
+ maintenanceFuture.get();
+ } catch (InterruptedException | ExecutionException e) {
+ log.error(sm.getString("timebucket.maintenance.error"), e);
+ }
+ }
+ maintenanceFuture = executorService.scheduleWithFixedDelay(new Maintenance(), sleeptime, sleeptime,
+ TimeUnit.MILLISECONDS);
+ }
+ }
+ }
+
+}
diff --git a/test/org/apache/catalina/filters/TestRateLimitFilter.java b/test/org/apache/catalina/filters/TestRateLimitFilter.java
index 74a1ecae3844..3533fa9d75c6 100644
--- a/test/org/apache/catalina/filters/TestRateLimitFilter.java
+++ b/test/org/apache/catalina/filters/TestRateLimitFilter.java
@@ -52,11 +52,11 @@ private void testRateLimitWith4Clients(boolean exposeHeaders, boolean enforce) t
Tomcat tomcat = getTomcatInstance();
Context root = tomcat.addContext("", TEMP_DIR);
- tomcat.start();
-
MockFilterChain filterChain = new MockFilterChain();
RateLimitFilter rateLimitFilter = testRateLimitFilter(filterDef, root);
+ tomcat.start();
+
FastRateLimiter fastRateLimiter = (FastRateLimiter) rateLimitFilter.rateLimiter;
int allowedRequests = fastRateLimiter.getRequests();
@@ -140,9 +140,6 @@ private RateLimitFilter testRateLimitFilter(FilterDef filterDef, Context root) t
root.addFilterMap(filterMap);
FilterConfig filterConfig = TesterFilterConfigs.generateFilterConfig(filterDef);
-
- rateLimitFilter.init(filterConfig);
-
return rateLimitFilter;
}
diff --git a/test/org/apache/catalina/filters/TestRateLimitFilterWithExactRateLimiter.java b/test/org/apache/catalina/filters/TestRateLimitFilterWithExactRateLimiter.java
new file mode 100644
index 000000000000..a293336b4104
--- /dev/null
+++ b/test/org/apache/catalina/filters/TestRateLimitFilterWithExactRateLimiter.java
@@ -0,0 +1,215 @@
+/*
+ * 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.filters;
+
+import java.io.IOException;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.filters.TestRemoteIpFilter.MockFilterChain;
+import org.apache.catalina.filters.TestRemoteIpFilter.MockHttpServletRequest;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.catalina.util.ExactRateLimiter;
+import org.apache.tomcat.unittest.TesterResponse;
+import org.apache.tomcat.util.descriptor.web.FilterDef;
+import org.apache.tomcat.util.descriptor.web.FilterMap;
+
+public class TestRateLimitFilterWithExactRateLimiter extends TomcatBaseTest {
+ private void testRateLimitWith1Clients(boolean exposeHeaders, boolean enforce) throws Exception {
+
+ int bucketRequests = 40;
+ int bucketDuration = 4;
+
+ FilterDef filterDef = new FilterDef();
+ filterDef.addInitParameter("bucketRequests", String.valueOf(bucketRequests));
+ filterDef.addInitParameter("bucketDuration", String.valueOf(bucketDuration));
+ filterDef.addInitParameter("enforce", String.valueOf(enforce));
+ filterDef.addInitParameter("exposeHeaders", String.valueOf(exposeHeaders));
+ filterDef.addInitParameter("rateLimitClassName", "org.apache.catalina.util.ExactRateLimiter");
+
+ Tomcat tomcat = getTomcatInstance();
+ Context root = tomcat.addContext("", TEMP_DIR);
+
+ MockFilterChain filterChain = new MockFilterChain();
+ RateLimitFilter rateLimitFilter = testRateLimitFilter(filterDef, root);
+ tomcat.start();
+
+ ExactRateLimiter exactRateLimiter = (ExactRateLimiter) rateLimitFilter.rateLimiter;
+
+ int allowedRequests = exactRateLimiter.getRequests();
+ long sleepTime = exactRateLimiter.getBucketCounter().getMillisUntilNextBucket();
+ System.out.printf("Sleeping %d millis for the next time bucket to start\n", Long.valueOf(sleepTime));
+ Thread.sleep(sleepTime);
+
+ TestClient tc1 = new TestClient(rateLimitFilter, filterChain, "10.20.20.5", 50, 5); // TPS: 5
+ TestClient tc2 = new TestClient(rateLimitFilter, filterChain, "10.20.20.10", 100, 10); // TPS: 10
+
+ TestClient tc3 = new TestClient(rateLimitFilter, filterChain, "10.20.20.20", 200, 20); // TPS: 20
+ TestClient tc4 = new TestClient(rateLimitFilter, filterChain, "10.20.20.40", 400, 40); // TPS: 40
+ tc1.join();
+ tc2.join();
+ tc3.join();
+ tc4.join();
+ Assert.assertEquals(200, tc1.results[24]); // only 25 requests made in 5 seconds, all allowed
+
+ Assert.assertEquals(200, tc2.results[49]); // only 50 requests made in 5 seconds, all allowed
+
+ Assert.assertEquals(200, tc3.results[39]); // first allowedRequests allowed
+
+ if (enforce) {
+ Assert.assertEquals(429, tc3.results[allowedRequests]); // subsequent requests dropped
+ } else {
+ Assert.assertEquals(200, tc3.results[allowedRequests]);
+ }
+
+ Assert.assertEquals(200, tc4.results[allowedRequests - 1]); // first allowedRequests allowed
+
+ if (enforce) {
+ Assert.assertEquals(429, tc4.results[allowedRequests]); // subsequent requests dropped
+ } else {
+ Assert.assertEquals(200, tc4.results[allowedRequests]);
+ }
+
+ if (exposeHeaders) {
+ Assert.assertTrue(tc3.rlpHeader[24].contains("q=" + allowedRequests));
+ Assert.assertTrue(tc3.rlpHeader[allowedRequests].contains("q=" + allowedRequests));
+ if (enforce) {
+ Assert.assertTrue(tc3.rlHeader[24].contains("r="));
+ Assert.assertFalse(tc3.rlHeader[24].contains("r=0"));
+ Assert.assertTrue(tc3.rlHeader[allowedRequests].contains("r=0"));
+ }
+ } else {
+ Assert.assertTrue(tc3.rlpHeader[24] == null);
+ Assert.assertTrue(tc3.rlHeader[24] == null);
+ Assert.assertTrue(tc3.rlpHeader[allowedRequests] == null);
+ Assert.assertTrue(tc3.rlHeader[allowedRequests] == null);
+ }
+ tomcat.stop();
+ }
+
+ @Test
+ public void testExposeHeaderAndRerferenceRateLimitWith4Clients() throws Exception {
+ testRateLimitWith1Clients(true, false);
+ }
+
+ @Test
+ public void testUnexposeHeaderAndRerferenceRateLimitWith4Clients() throws Exception {
+ testRateLimitWith1Clients(false, false);
+ }
+
+ @Test
+ public void testExposeHeaderAndEnforceRateLimitWith4Clients() throws Exception {
+ testRateLimitWith1Clients(true, true);
+ }
+
+ @Test
+ public void testUnexposeHeaderAndEnforceRateLimitWith4Clients() throws Exception {
+ testRateLimitWith1Clients(false, true);
+ }
+
+ private RateLimitFilter testRateLimitFilter(FilterDef filterDef, Context root) throws ServletException {
+
+ RateLimitFilter rateLimitFilter = new RateLimitFilter();
+ filterDef.setFilterClass(RateLimitFilter.class.getName());
+ filterDef.setFilter(rateLimitFilter);
+ filterDef.setFilterName(RateLimitFilter.class.getName());
+ root.addFilterDef(filterDef);
+
+ FilterMap filterMap = new FilterMap();
+ filterMap.setFilterName(RateLimitFilter.class.getName());
+ filterMap.addURLPatternDecoded("*");
+ root.addFilterMap(filterMap);
+
+ FilterConfig filterConfig = TesterFilterConfigs.generateFilterConfig(filterDef);
+
+ return rateLimitFilter;
+ }
+
+ static class TestClient extends Thread {
+ RateLimitFilter filter;
+ FilterChain filterChain;
+ String ip;
+
+ int requests;
+ int sleep;
+
+ int[] results;
+ volatile String[] rlpHeader;
+ volatile String[] rlHeader;
+
+ TestClient(RateLimitFilter filter, FilterChain filterChain, String ip, int requests, int rps) {
+ this.filter = filter;
+ this.filterChain = filterChain;
+ this.ip = ip;
+ this.requests = requests;
+ this.sleep = 1000 / rps;
+ this.results = new int[requests];
+ this.rlpHeader = new String[requests];
+ this.rlHeader = new String[requests];
+ super.setDaemon(true);
+ super.start();
+ }
+
+ @Override
+ public void run() {
+ try {
+ for (int i = 0; i < requests; i++) {
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.setRemoteAddr(ip);
+ TesterResponse response = new TesterResponseWithStatus();
+ response.setRequest(request);
+ filter.doFilter(request, response, filterChain);
+ results[i] = response.getStatus();
+
+ rlpHeader[i] = response.getHeader(RateLimitFilter.HEADER_RATE_LIMIT_POLICY);
+ rlHeader[i] = response.getHeader(RateLimitFilter.HEADER_RATE_LIMIT);
+
+ if (results[i] != 200) {
+ break;
+ }
+ sleep(sleep);
+ }
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+ }
+
+ static class TesterResponseWithStatus extends TesterResponse {
+
+ int status = 200;
+ String message = "OK";
+
+ @Override
+ public void sendError(int status, String message) throws IOException {
+ this.status = status;
+ this.message = message;
+ }
+
+ @Override
+ public int getStatus() {
+ return status;
+ }
+ }
+}
diff --git a/webapps/docs/config/filter.xml b/webapps/docs/config/filter.xml
index 5a879727ab9f..a62f3d0284cd 100644
--- a/webapps/docs/config/filter.xml
+++ b/webapps/docs/config/filter.xml
@@ -968,13 +968,21 @@ FINE: Request "/docs/config/manager.html" with response status "200"
from that IP are dropped with a "429 Too many requests" response
until the bucket time ends and a new bucket starts. 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 It is common to set up different restrictions for different URIs.
For example, a login page or authentication script is typically expected
@@ -1047,7 +1055,10 @@ FINE: Request "/docs/config/manager.html" with response status "200"
The full class name of an implementation of the RateLimiter interface.
- Default is "org.apache.catalina.util.FastRateLimiter".null
.
+ */
+ public TimeBucketCounterBase(ScheduledExecutorService utilityExecutor, int bucketDuration) {
+ Objects.requireNonNull(utilityExecutor);
+ this.executorService = utilityExecutor;
+ this.bucketDuration = bucketDuration;
+
+ int cleanupsPerBucketDuration = (bucketDuration >= 60) ? 6 : 3;
+ sleeptime = bucketDuration * 1000 / cleanupsPerBucketDuration;
+
+ // Start our thread
+ if (sleeptime > 0) {
+ monitorFuture = executorService.scheduleWithFixedDelay(new MaintenanceMonitor(), 0, 60, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * @return bucketDuration in seconds
+ */
+ public int getBucketDuration() {
+ return bucketDuration;
+ }
+
+ /**
+ * Returns the ratio between the configured duration param and the actual duration.
+ * @return the ratio between the configured duration param and the actual duration.
+ */
+ public abstract double getRatio();
+
+ /**
+ * Increments the counter for the passed identifier in the current time bucket and returns the new value.
+ *
+ * @param identifier an identifier for which we want to maintain count, e.g. IP Address
+ *
+ * @return the count within the current time bucket
+ *
+ * @see TimeBucketCounterBase#genKey(String)
+ */
+ public final int increment(String identifier) {
+ String key = genKey(identifier);
+ AtomicInteger ai = map.computeIfAbsent(key, v -> new AtomicInteger());
+ return ai.incrementAndGet();
+ }
+
+ /**
+ * Generates the key of timeBucket counter maps with the specific identifier, and the timestamp is implicitly
+ * equivalent to "now".
+ *
+ * @param identifier an identifier for which we want to maintain count
+ *
+ * @return key of timeBucket counter maps
+ */
+ protected final String genKey(String identifier) {
+ return genKey(identifier, System.currentTimeMillis());
+ }
+
+ /**
+ * Generates the key of timeBucket counter maps with the specific identifier and timestamp.
+ *
+ * @param identifier of target request
+ * @param timestamp when target request received
+ *
+ * @return key of timeBucket counter maps
+ */
+ protected final String genKey(String identifier, long timestamp) {
+ return getBucketIndex(timestamp) + BUCKET_KEY_DELIMITER + identifier;
+ }
+
+ /**
+ * Calculate the bucket index for the specific timestamp, concrete subclass.
+ *
+ * @param timestamp the specific timestamp in milliseconds
+ *
+ * @return prefix the bucket key prefix for the specific timestamp
+ */
+ protected abstract long getBucketIndex(long timestamp);
+
+ /**
+ * Returns current bucket prefix
+ * @return bucket index
+ */
+ protected int getCurrentBucketPrefix() {
+ return (int)getBucketIndex(System.currentTimeMillis());
+ }
+ /**
+ * When we want to test a full bucket duration we need to sleep until the next bucket starts.
+ * rateLimitClassName
+ init param. The default implementation,
+ org.apache.catalina.util.FastRateLimiter
, 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. An alternative implementation,
+ org.apache.catalina.util.ExactRateLimiter
, is intended to provide
+ a less efficient but more accurate control, whose effective duration in seconds
+ and number of requests configuration are consist with the user declared. You can
+ specify a different class as long as it implements the
+ org.apache.catalina.util.RateLimiter
interface.