From dc5b18d8989cf816c0870f236f6ccaade8b16ff3 Mon Sep 17 00:00:00 2001 From: Christopher Schultz Date: Mon, 9 Oct 2023 15:45:42 -0400 Subject: [PATCH] Add experimental FilterValve, which allows a Filter to be run within the Valve chain. --- .../catalina/startup/ContextRuleSet.java | 3 + .../catalina/startup/EngineRuleSet.java | 3 + .../apache/catalina/startup/HostRuleSet.java | 3 + .../apache/catalina/valves/FilterValve.java | 262 ++++++++++++++++++ 4 files changed, 271 insertions(+) create mode 100644 java/org/apache/catalina/valves/FilterValve.java diff --git a/java/org/apache/catalina/startup/ContextRuleSet.java b/java/org/apache/catalina/startup/ContextRuleSet.java index 4aa081e08308..5864a104c258 100644 --- a/java/org/apache/catalina/startup/ContextRuleSet.java +++ b/java/org/apache/catalina/startup/ContextRuleSet.java @@ -212,6 +212,9 @@ public void addRuleInstances(Digester digester) { 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 f98e285a866c..153d3648543c 100644 --- a/java/org/apache/catalina/startup/EngineRuleSet.java +++ b/java/org/apache/catalina/startup/EngineRuleSet.java @@ -111,6 +111,9 @@ public void addRuleInstances(Digester digester) { 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 3114cc1f9ad1..135ceb06c4c5 100644 --- a/java/org/apache/catalina/startup/HostRuleSet.java +++ b/java/org/apache/catalina/startup/HostRuleSet.java @@ -115,6 +115,9 @@ public void addRuleInstances(Digester digester) { 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 000000000000..44192671c998 --- /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; + +/** + *

A Valve to wrap a Filter, allowing a user to run Servlet Filters as a + * part of the Valve chain.

+ * + *

There are some caveats you must be aware of when using this Valve + * to wrap a Filter:

+ * + * + * + * @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 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 null + */ + @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 <Context>. + */ + @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 + * null 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 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; + } + } +}