All,
Okay, bear with me, here.
This was a crazy idea to see if I could get Igal's RateLimitFilter
running earlier in the process to provide protection earlier than the
Filters would get a chance to run.
For example, running before the AuthenticationFilter to prevent
/successful/ authentication spamming.
It may be a little contrived, but I was able to configure this for a
test inside a Host like this:
<Valve className="org.apache.catalina.valves.FilterValve"
filterClassName="org.apache.catalina.filters.RateLimitFilter">
<init-param>
<param-name>bucketDuration</param-name>
<param-value>1</param-value>
</init-param>
<init-param>
<param-name>bucketRequests</param-name>
<param-value>1</param-value>
</init-param>
</Valve>
It's very bare-bones. I have neither documented it nor added it to the
changelog because I'd like some feedback. If this is successful, we can
perhaps look at reducing the amount of code we have duplicated between
certain Valves and Filters.
There is a related conversation about how to deal with the application's
(i.e. Filter's) access to Tomcat internals through the request,
response, application, etc.. That is both complicated AND motivated by
the removal of the SecurityManager, which used to provide protection
against reflective-access to Tomcat internals. My concern is not
security-related, but stability-related. We would not want an
application to "accidentally" retain a reference to the underlying
request or response, so continuing to use facades still makes a fair bit
of sense.
Anyhow, enjoy.
-chris
On 10/9/23 15:47, schu...@apache.org wrote:
This is an automated email from the ASF dual-hosted git repository.
schultz 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 dc5b18d898 Add experimental FilterValve, which allows a Filter to be
run within the Valve chain.
dc5b18d898 is described below
commit dc5b18d8989cf816c0870f236f6ccaade8b16ff3
Author: Christopher Schultz <ch...@christopherschultz.net>
AuthorDate: Mon Oct 9 15:45:42 2023 -0400
Add experimental FilterValve, which allows a Filter to be run within the
Valve chain.
---
.../apache/catalina/startup/ContextRuleSet.java | 3 +
.../org/apache/catalina/startup/EngineRuleSet.java | 3 +
java/org/apache/catalina/startup/HostRuleSet.java | 3 +
java/org/apache/catalina/valves/FilterValve.java | 262 +++++++++++++++++++++
4 files changed, 271 insertions(+)
diff --git a/java/org/apache/catalina/startup/ContextRuleSet.java
b/java/org/apache/catalina/startup/ContextRuleSet.java
index 4aa081e083..5864a104c2 100644
--- a/java/org/apache/catalina/startup/ContextRuleSet.java
+++ b/java/org/apache/catalina/startup/ContextRuleSet.java
@@ -212,6 +212,9 @@ public class ContextRuleSet implements RuleSet {
null, // MUST be specified in the element
"className");
digester.addSetProperties(prefix + "Context/Valve");
+ digester.addCallMethod(prefix + "Context/Valve/init-param",
"addInitParam", 2);
+ digester.addCallParam(prefix + "Context/Valve/init-param/param-name",
0);
+ digester.addCallParam(prefix + "Context/Valve/init-param/param-value",
1);
digester.addSetNext(prefix + "Context/Valve",
"addValve",
"org.apache.catalina.Valve");
diff --git a/java/org/apache/catalina/startup/EngineRuleSet.java
b/java/org/apache/catalina/startup/EngineRuleSet.java
index f98e285a86..153d364854 100644
--- a/java/org/apache/catalina/startup/EngineRuleSet.java
+++ b/java/org/apache/catalina/startup/EngineRuleSet.java
@@ -111,6 +111,9 @@ public class EngineRuleSet implements RuleSet {
null, // MUST be specified in the element
"className");
digester.addSetProperties(prefix + "Engine/Valve");
+ digester.addCallMethod(prefix + "Engine/Valve/init-param",
"addInitParam", 2);
+ digester.addCallParam(prefix + "Engine/Valve/init-param/param-name",
0);
+ digester.addCallParam(prefix + "Engine/Valve/init-param/param-value",
1);
digester.addSetNext(prefix + "Engine/Valve",
"addValve",
"org.apache.catalina.Valve");
diff --git a/java/org/apache/catalina/startup/HostRuleSet.java
b/java/org/apache/catalina/startup/HostRuleSet.java
index 3114cc1f9a..135ceb06c4 100644
--- a/java/org/apache/catalina/startup/HostRuleSet.java
+++ b/java/org/apache/catalina/startup/HostRuleSet.java
@@ -115,6 +115,9 @@ public class HostRuleSet implements RuleSet {
null, // MUST be specified in the element
"className");
digester.addSetProperties(prefix + "Host/Valve");
+ digester.addCallMethod(prefix + "Host/Valve/init-param",
"addInitParam", 2);
+ digester.addCallParam(prefix + "Host/Valve/init-param/param-name", 0);
+ digester.addCallParam(prefix + "Host/Valve/init-param/param-value", 1);
digester.addSetNext(prefix + "Host/Valve",
"addValve",
"org.apache.catalina.Valve");
diff --git a/java/org/apache/catalina/valves/FilterValve.java
b/java/org/apache/catalina/valves/FilterValve.java
new file mode 100644
index 0000000000..44192671c9
--- /dev/null
+++ b/java/org/apache/catalina/valves/FilterValve.java
@@ -0,0 +1,262 @@
+package org.apache.catalina.valves;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.apache.catalina.Container;
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.tomcat.util.threads.ScheduledThreadPoolExecutor;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+
+/**
+ * <p>A Valve to wrap a Filter, allowing a user to run Servlet Filters as a
+ * part of the Valve chain.</p>
+ *
+ * <p>There are some caveats you must be aware of when using this Valve
+ * to wrap a Filter:</p>
+ *
+ * <ul>
+ * <li>You get a <i>separate instance</i> of your Filter class distinct
+ * from any that may be instantiated within your application.</li>
+ * <li>Calls to {@link FilterConfig#getFilterName()} will return
<code>null</code>.</li>
+ * <li>Calling {@link FilterConfig#getServletContext()} will return
+ * the proper ServletContext for a Valve/Filter attached to a
+ * <code><Context></code>, but will return a ServletContext
+ * which is nearly useless for any Valve/Filter specified on
+ * an <code><Engine></code> or <code>>Host<</code>.</li>
+ * <li>Your Filter <b>MUST NOT</b> wrap the {@link ServletRequest} or
+ * {@link ServletResponse} objects, or an exception will be thrown.</li>
+ * </ul>
+ *
+ * @see Valve
+ * @see Filter
+ */
+public class FilterValve
+ extends ValveBase
+ implements FilterConfig
+{
+ /**
+ * The name of the Filter class that will be instantiated.
+ */
+ private String filterClassName;
+
+ /**
+ * The init-params for the Filter.
+ */
+ private HashMap<String,String> filterInitParams;
+
+ /**
+ * The instance of the Filter to be invoked.
+ */
+ private Filter filter;
+
+ /**
+ * The ServletContet to be provided to the Filter if it requests one.
+ */
+ private ServletContext application;
+
+ /**
+ * Sets the name of the class for the Filter.
+ *
+ * @param filterClassName The class name for the Filter.
+ */
+ public void setFilterClass(String filterClassName) {
+ setFilterClassName(filterClassName);
+ }
+
+ /**
+ * Sets the name of the class for the Filter.
+ *
+ * @param filterClassName The class name for the Filter.
+ */
+ public void setFilterClassName(String filterClassName) {
+ this.filterClassName = filterClassName;
+ }
+
+ /**
+ * Gets the name of the class for the Filter.
+ *
+ * @return The class name for the Filter.
+ */
+ public String getFilterClassName() {
+ return filterClassName;
+ }
+
+ /**
+ * Adds an initialization parameter for the Filter.
+ *
+ * @param paramName The name of the parameter.
+ * @param paramValue The value of the parameter.
+ */
+ public void addInitParam(String paramName, String paramValue) {
+ if(null == filterInitParams) {
+ filterInitParams = new HashMap<>();
+ }
+
+ filterInitParams.put(paramName, paramValue);
+ }
+
+ /**
+ * Gets the name of the Filter.
+ *
+ * @return <code>null</code>
+ */
+ @Override
+ public String getFilterName() {
+ return null;
+ }
+
+ /**
+ * Gets the ServletContext.
+ *
+ * Note that this will be of limited use if the Valve/Filter is not
+ * attached to a <code><Context></code>.
+ */
+ @Override
+ public ServletContext getServletContext() {
+ if(null == application) {
+ throw new IllegalStateException("Filter " + filter + " has called
getServletContext from FilterValve, but this FilterValve is not ");
+ } else {
+ return application;
+ }
+ }
+
+ /**
+ * Gets the initialization parameter with the specified name.
+ *
+ * @param name The name of the initialization parameter.
+ *
+ * @return The value for the initialization parameter, or
+ * <code>null</code> if there is no value for the
+ * specified initialization parameter name.
+ */
+ @Override
+ public String getInitParameter(String name) {
+ if(null == filterInitParams) {
+ return null;
+ } else {
+ return filterInitParams.get(name);
+ }
+ }
+
+ /**
+ * Gets an enumeration of the names of all initialization parameters.
+ *
+ * @return An enumeration of the names of all initialization parameters.
+ */
+ @Override
+ public Enumeration<String> getInitParameterNames() {
+ if(null == filterInitParams) {
+ return Collections.emptyEnumeration();
+ } else {
+ return Collections.enumeration(filterInitParams.keySet());
+ }
+ }
+
+ @Override
+ protected synchronized void startInternal() throws LifecycleException {
+ super.startInternal();
+
+ if(null == getFilterClassName()) {
+ throw new LifecycleException("No filterClass specified for FilterValve:
a filterClass is required");
+ }
+ Container parent = super.getContainer();
+ if(parent instanceof Context) {
+ application = ((Context)parent).getServletContext();
+ } else {
+ final ScheduledExecutorService executor =
Container.getService(super.getContainer()).getServer().getUtilityExecutor();
+
+ // I don't feel like writing a whole trivial class just for this
one thing.
+ application =
(ServletContext)Proxy.newProxyInstance(getClass().getClassLoader(),
+ new Class<?>[] { ServletContext.class },
+ new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
+ if("getAttribute".equals(method.getName())
+ && null != args
+ && 1 == args.length
+ &&
ScheduledThreadPoolExecutor.class.getName().equals(args[0])) {
+ return executor;
+ } else {
+ throw new UnsupportedOperationException("This
ServletContet is not really meant to be used.");
+ }
+ }
+ });
+ }
+
+ try {
+ filter = (Filter)
Class.forName(getFilterClassName()).getConstructor(new Class[0]).newInstance();
+
+ filter.init(this);
+ } catch (ServletException | InstantiationException |
IllegalAccessException | IllegalArgumentException | InvocationTargetException |
NoSuchMethodException | SecurityException | ClassNotFoundException se) {
+ throw new LifecycleException("Failed to start FilterValve for filter
" + filter, se);
+ }
+ }
+
+ @Override
+ protected synchronized void stopInternal() throws LifecycleException {
+ super.stopInternal();
+
+ if(null != filter) {
+ try {
+ filter.destroy();
+ } finally {
+ filter = null;
+ }
+ }
+ }
+
+ @Override
+ public void invoke(final Request request, final Response response) throws
IOException, ServletException {
+ // Allow our FilterChain object to share data back to us.
+ //
+ // FilterCallInfo captures information about what the Filter
+ // ultimately did, specifically whether or not FilterChain.doFilter
+ // was called, and with what arguments.
+ final FilterCallInfo filterCallInfo = new FilterCallInfo();
+
+ filter.doFilter(request, response, filterCallInfo);
+
+ if(!filterCallInfo.stop) {
+ if(request != filterCallInfo.request) {
+ throw new IllegalStateException("Filter " + filter + " has
illegally changed or wrapped the request");
+ }
+
+ if(response != filterCallInfo.response) {
+ throw new IllegalStateException("Filter " + filter + " has
illegally changed or wrapped the response");
+ }
+
+ getNext().invoke(request, response);
+ }
+ }
+
+ private static final class FilterCallInfo implements FilterChain {
+ private boolean stop = true;
+ private ServletRequest request;
+ private ServletResponse response;
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
+ this.request = request;
+ this.response = response;
+ this.stop = false;
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org