All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.catalina.filters.RateLimitFilter Maven / Gradle / Ivy

There is a newer version: 11.0.0-M20
Show newest version
/*
 * 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 java.util.concurrent.ScheduledExecutorService;

import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.GenericFilter;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;

import org.apache.catalina.util.TimeBucketCounter;
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;

/**
 * 

* Servlet filter that can help mitigate Denial of Service (DoS) and Brute Force attacks by limiting the number of a * requests that are allowed from a single IP address within a time window (also referred to as a time bucket), e.g. 300 * Requests per 60 seconds. *

*

* The filter works by incrementing a counter in a time bucket for each IP address, and if the counter exceeds the * allowed limit then further requests 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. *

*

* It is common to set up different restrictions for different URIs. For example, a login page or authentication script * is typically expected to get far less requests than the rest of the application, so you can add a filter definition * that would allow only 5 requests per 15 seconds and map those URIs to it. *

*

* You can set enforce to false to disable the termination of requests that exceed the allowed * limit. Then your application code can inspect the Request Attribute * org.apache.catalina.filters.RateLimitFilter.Count and decide how to handle the request based on other * information that it has, e.g. allow more requests to certain users based on roles, etc. *

*

* WARNING: if Tomcat is behind a reverse proxy then you must make sure that the Rate Limit Filter sees * the client IP address, so if for example you are using the Remote IP Filter, then the * filter mapping for the Rate Limit Filter must come after the mapping of the Remote IP Filter to ensure that * each request has its IP address resolved before the Rate Limit Filter is applied. Failure to do so will count * requests from different IPs in the same bucket and will result in a self inflicted DoS attack. *

*/ public class RateLimitFilter extends GenericFilter { private static final long serialVersionUID = 1L; /** * default duration in seconds */ public static final int DEFAULT_BUCKET_DURATION = 60; /** * default number of requests per duration */ public static final int DEFAULT_BUCKET_REQUESTS = 300; /** * default value for enforce */ public static final boolean DEFAULT_ENFORCE = true; /** * default status code to return if requests per duration exceeded */ public static final int DEFAULT_STATUS_CODE = 429; /** * default status message to return if requests per duration exceeded */ public static final String DEFAULT_STATUS_MESSAGE = "Too many requests"; /** * request attribute that will contain the number of requests per duration */ public static final String RATE_LIMIT_ATTRIBUTE_COUNT = "org.apache.catalina.filters.RateLimitFilter.Count"; /** * init-param to set the bucket duration in seconds */ public static final String PARAM_BUCKET_DURATION = "bucketDuration"; /** * init-param to set the bucket number of requests */ public static final String PARAM_BUCKET_REQUESTS = "bucketRequests"; /** * init-param to set the enforce flag */ public static final String PARAM_ENFORCE = "enforce"; /** * init-param to set a custom status code if requests per duration exceeded */ public static final String PARAM_STATUS_CODE = "statusCode"; /** * init-param to set a custom status message if requests per duration exceeded */ public static final String PARAM_STATUS_MESSAGE = "statusMessage"; transient TimeBucketCounter bucketCounter; private int actualRequests; private int bucketRequests = DEFAULT_BUCKET_REQUESTS; private int bucketDuration = DEFAULT_BUCKET_DURATION; private boolean enforce = DEFAULT_ENFORCE; private int statusCode = DEFAULT_STATUS_CODE; private String statusMessage = DEFAULT_STATUS_MESSAGE; private transient Log log = LogFactory.getLog(RateLimitFilter.class); 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 { FilterConfig config = getFilterConfig(); String param; param = config.getInitParameter(PARAM_BUCKET_DURATION); if (param != null) { bucketDuration = Integer.parseInt(param); } param = config.getInitParameter(PARAM_BUCKET_REQUESTS); if (param != null) { bucketRequests = Integer.parseInt(param); } param = config.getInitParameter(PARAM_ENFORCE); if (param != null) { enforce = Boolean.parseBoolean(param); } param = config.getInitParameter(PARAM_STATUS_CODE); if (param != null) { statusCode = Integer.parseInt(param); } param = config.getInitParameter(PARAM_STATUS_MESSAGE); if (param != null) { statusMessage = param; } ScheduledExecutorService executorService = (ScheduledExecutorService) getServletContext() .getAttribute(ScheduledThreadPoolExecutor.class.getName()); if (executorService == null) { executorService = new java.util.concurrent.ScheduledThreadPoolExecutor(1); } bucketCounter = new TimeBucketCounter(bucketDuration, executorService); actualRequests = (int) Math.round(bucketCounter.getRatio() * bucketRequests); log.info(sm.getString("rateLimitFilter.initialized", super.getFilterName(), Integer.valueOf(bucketRequests), Integer.valueOf(bucketDuration), Integer.valueOf(getActualRequests()), Integer.valueOf(getActualDurationInSeconds()), (!enforce ? "Not " : "") + "enforcing")); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String ipAddr = request.getRemoteAddr(); int reqCount = bucketCounter.increment(ipAddr); request.setAttribute(RATE_LIMIT_ATTRIBUTE_COUNT, Integer.valueOf(reqCount)); if (enforce && (reqCount > actualRequests)) { ((HttpServletResponse) response).sendError(statusCode, statusMessage); log.warn(sm.getString("rateLimitFilter.maxRequestsExceeded", super.getFilterName(), Integer.valueOf(reqCount), ipAddr, Integer.valueOf(getActualRequests()), Integer.valueOf(getActualDurationInSeconds()))); return; } chain.doFilter(request, response); } @Override public void destroy() { this.bucketCounter.destroy(); super.destroy(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy