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>&lt;Context&gt;</code>, but will return a ServletContext
+ *       which is nearly useless for any Valve/Filter specified on
+ *       an <code>&lt;Engine&gt;</code> or <code>&gt;Host&lt;</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>&lt;Context&gt;</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

Reply via email to