This is an automated email from the ASF dual-hosted git repository. isapir pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/9.0.x by this push: new f5f2d956f4 Allow different implementations for RateLimitFilter f5f2d956f4 is described below commit f5f2d956f45adad6506a1aee8cccdfd506b379e2 Author: Igal Sapir <isa...@apache.org> AuthorDate: Sat Oct 5 23:49:17 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 58b8572a0c..688b54bda9 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 javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -28,11 +28,10 @@ import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.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..7ebf6a9079 --- /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 org.apache.tomcat.util.threads.ScheduledThreadPoolExecutor; + +import java.util.concurrent.ScheduledExecutorService; +import javax.servlet.FilterConfig; + +/** + * 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..338985d827 --- /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 javax.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 a0c83aca69..6608252f0c 100644 --- a/test/org/apache/catalina/filters/TestRateLimitFilter.java +++ b/test/org/apache/catalina/filters/TestRateLimitFilter.java @@ -24,6 +24,7 @@ import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.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: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org