From 3747a5c6f031a8fc507eb241ddffaef67b5c8d78 Mon Sep 17 00:00:00 2001 From: Gus Heck Date: Tue, 23 Dec 2025 21:15:49 -0500 Subject: [PATCH 1/7] SOLR-18041 PathExclusionFilter extracted from SolrDispatchFilter --- .../solr/servlet/PathExclusionFilter.java | 56 +++++++++++++++++++ .../org/apache/solr/servlet/ServletUtils.java | 3 - .../solr/servlet/SolrDispatchFilter.java | 13 +---- solr/webapp/web/WEB-INF/web.xml | 14 ++++- 4 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java diff --git a/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java b/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java new file mode 100644 index 000000000000..b29392d339e5 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java @@ -0,0 +1,56 @@ +/* + * 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.solr.servlet; + +import static org.apache.solr.servlet.ServletUtils.configExcludes; +import static org.apache.solr.servlet.ServletUtils.excludedPath; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.regex.Pattern; + +public class PathExclusionFilter extends HttpFilter implements PathExcluder { + + private List excludePatterns; + + @Override + public void init(FilterConfig config) throws ServletException { + configExcludes(this, config.getInitParameter("excludePatterns")); + super.init(config); + } + + @Override + protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) + throws IOException, ServletException { + if (!excludedPath(excludePatterns, req, res, chain)) { + chain.doFilter(req, res); + } else { + req.getServletContext().getNamedDispatcher("default").forward(req, res); + } + } + + @Override + public void setExcludePatterns(List excludePatterns) { + this.excludePatterns = excludePatterns; + } +} diff --git a/solr/core/src/java/org/apache/solr/servlet/ServletUtils.java b/solr/core/src/java/org/apache/solr/servlet/ServletUtils.java index e02f6802f93f..f5d9c7bc33b6 100644 --- a/solr/core/src/java/org/apache/solr/servlet/ServletUtils.java +++ b/solr/core/src/java/org/apache/solr/servlet/ServletUtils.java @@ -143,9 +143,6 @@ static boolean excludedPath( for (Pattern p : excludePatterns) { Matcher matcher = p.matcher(requestPath); if (matcher.lookingAt()) { - if (chain != null) { - chain.doFilter(request, response); - } return true; } } diff --git a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java index 632c8aef5046..aad2d3530520 100644 --- a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java +++ b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java @@ -17,8 +17,6 @@ package org.apache.solr.servlet; import static org.apache.solr.servlet.ServletUtils.closeShield; -import static org.apache.solr.servlet.ServletUtils.configExcludes; -import static org.apache.solr.servlet.ServletUtils.excludedPath; import static org.apache.solr.util.tracing.TraceUtils.getSpan; import static org.apache.solr.util.tracing.TraceUtils.setTracer; @@ -67,7 +65,7 @@ // servlets that are more focused in scope. This should become possible now that we have a // ServletContextListener for startup/shutdown of CoreContainer that sets up a service from which // things like CoreContainer can be requested. (or better yet injected) -public class SolrDispatchFilter extends HttpFilter implements PathExcluder { +public class SolrDispatchFilter extends HttpFilter { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private CoreContainerProvider containerProvider; @@ -78,11 +76,6 @@ public class SolrDispatchFilter extends HttpFilter implements PathExcluder { private HttpSolrCallFactory solrCallFactory; - @Override - public void setExcludePatterns(List excludePatterns) { - this.excludePatterns = excludePatterns; - } - private List excludePatterns; public final boolean isV2Enabled = V2ApiUtils.isEnabled(); @@ -133,7 +126,6 @@ public void init(FilterConfig config) throws ServletException { log.trace("SolrDispatchFilter.init(): {}", this.getClass().getClassLoader()); } - configExcludes(this, config.getInitParameter("excludePatterns")); } catch (Throwable t) { // catch this so our filter still works log.error("Could not start Dispatch Filter.", t); @@ -157,9 +149,6 @@ public CoreContainer getCores() throws UnavailableException { "Set the thread contextClassLoader for all 3rd party dependencies that we cannot control") public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - if (excludedPath(excludePatterns, request, response, chain)) { - return; - } try (var mdcSnapshot = MDCSnapshot.create()) { assert null != mdcSnapshot; // prevent compiler warning diff --git a/solr/webapp/web/WEB-INF/web.xml b/solr/webapp/web/WEB-INF/web.xml index 37a68b4692b3..ce84f5099f11 100644 --- a/solr/webapp/web/WEB-INF/web.xml +++ b/solr/webapp/web/WEB-INF/web.xml @@ -26,8 +26,8 @@ - SolrRequestFilter - org.apache.solr.servlet.SolrDispatchFilter + PathExclusionsFilter + org.apache.solr.servlet.PathExclusionFilter From 2278ebe9dcd54e73bbaedd4cfbb426445b13aeea Mon Sep 17 00:00:00 2001 From: Gus Heck Date: Tue, 23 Dec 2025 21:28:59 -0500 Subject: [PATCH 3/7] SOLR-18041 changelog file --- changelog/unreleased/SOLR-18041.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 changelog/unreleased/SOLR-18041.yml diff --git a/changelog/unreleased/SOLR-18041.yml b/changelog/unreleased/SOLR-18041.yml new file mode 100644 index 000000000000..d36eaad3ee53 --- /dev/null +++ b/changelog/unreleased/SOLR-18041.yml @@ -0,0 +1,8 @@ +# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc +title: SOLR 18041 - Path exclusions for the admin UI are now defined in a separate servlet filter +type: other # added, changed, fixed, deprecated, removed, dependency_update, security, other +authors: + - name: Gus Heck +links: + - name: SOLR-18041 + url: https://issues.apache.org/jira/browse/SOLR-18041 From f38ff1b4b56bec2020c9362daf10e105fae61e37 Mon Sep 17 00:00:00 2001 From: Gus Heck Date: Wed, 24 Dec 2025 00:54:08 -0500 Subject: [PATCH 4/7] SOLR-18041 pull methods into filter class, remove PathExcluder.java --- .../org/apache/solr/servlet/PathExcluder.java | 28 ------------- .../solr/servlet/PathExclusionFilter.java | 28 ++++++++----- .../org/apache/solr/servlet/ServletUtils.java | 41 ------------------- 3 files changed, 17 insertions(+), 80 deletions(-) delete mode 100644 solr/core/src/java/org/apache/solr/servlet/PathExcluder.java diff --git a/solr/core/src/java/org/apache/solr/servlet/PathExcluder.java b/solr/core/src/java/org/apache/solr/servlet/PathExcluder.java deleted file mode 100644 index 47d5b5c95780..000000000000 --- a/solr/core/src/java/org/apache/solr/servlet/PathExcluder.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.solr.servlet; - -import java.util.List; -import java.util.regex.Pattern; - -/** - * Denotes an object, usually a servlet that denies access to some paths based on the supplied - * patterns. Typically, this would be implemented via compiled regular expressions. - */ -public interface PathExcluder { - void setExcludePatterns(List excludePatterns); -} diff --git a/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java b/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java index b29392d339e5..5f8933811cb8 100644 --- a/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java +++ b/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java @@ -16,9 +16,6 @@ */ package org.apache.solr.servlet; -import static org.apache.solr.servlet.ServletUtils.configExcludes; -import static org.apache.solr.servlet.ServletUtils.excludedPath; - import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; @@ -26,31 +23,40 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Arrays; import java.util.List; +import java.util.regex.Matcher; import java.util.regex.Pattern; -public class PathExclusionFilter extends HttpFilter implements PathExcluder { +public class PathExclusionFilter extends HttpFilter { private List excludePatterns; + boolean shouldBeExcluded(HttpServletRequest request) { + String requestPath = ServletUtils.getPathAfterContext(request); + if (excludePatterns != null) { + return excludePatterns.stream().map(p -> p.matcher(requestPath)).anyMatch(Matcher::lookingAt); + } + return false; + } + @Override public void init(FilterConfig config) throws ServletException { - configExcludes(this, config.getInitParameter("excludePatterns")); + String patternConfig = config.getInitParameter("excludePatterns"); + if (patternConfig != null) { + String[] excludeArray = patternConfig.split(","); + this.excludePatterns = Arrays.stream(excludeArray).map(Pattern::compile).toList(); + } super.init(config); } @Override protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { - if (!excludedPath(excludePatterns, req, res, chain)) { + if (!shouldBeExcluded(req)) { chain.doFilter(req, res); } else { req.getServletContext().getNamedDispatcher("default").forward(req, res); } } - - @Override - public void setExcludePatterns(List excludePatterns) { - this.excludePatterns = excludePatterns; - } } diff --git a/solr/core/src/java/org/apache/solr/servlet/ServletUtils.java b/solr/core/src/java/org/apache/solr/servlet/ServletUtils.java index f5d9c7bc33b6..f63a16187d25 100644 --- a/solr/core/src/java/org/apache/solr/servlet/ServletUtils.java +++ b/solr/core/src/java/org/apache/solr/servlet/ServletUtils.java @@ -19,7 +19,6 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; -import jakarta.servlet.FilterChain; import jakarta.servlet.ReadListener; import jakarta.servlet.ServletException; import jakarta.servlet.ServletInputStream; @@ -33,10 +32,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.lang.invoke.MethodHandles; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException.ErrorCode; import org.apache.solr.common.util.Utils; @@ -131,42 +126,6 @@ public void close() { }; } - static boolean excludedPath( - List excludePatterns, - HttpServletRequest request, - HttpServletResponse response, - FilterChain chain) - throws IOException, ServletException { - String requestPath = getPathAfterContext(request); - // No need to even create the HttpSolrCall object if this path is excluded. - if (excludePatterns != null) { - for (Pattern p : excludePatterns) { - Matcher matcher = p.matcher(requestPath); - if (matcher.lookingAt()) { - return true; - } - } - } - return false; - } - - static boolean excludedPath( - List excludePatterns, HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException { - return excludedPath(excludePatterns, request, response, null); - } - - static void configExcludes(PathExcluder excluder, String patternConfig) { - if (patternConfig != null) { - String[] excludeArray = patternConfig.split(","); - List patterns = new ArrayList<>(); - excluder.setExcludePatterns(patterns); - for (String element : excludeArray) { - patterns.add(Pattern.compile(element)); - } - } - } - /** * Enforces rate limiting for a request. Should be converted to a servlet filter at some point. * Currently, this is tightly coupled with request tracing which is not ideal either. From dce4665c8a3a70107003d04c6f9cf74a2c7d2df6 Mon Sep 17 00:00:00 2001 From: Gus Heck Date: Wed, 24 Dec 2025 01:13:19 -0500 Subject: [PATCH 5/7] SOLR-18041 add a comment clarifying where the name "default" originates from. --- .../src/java/org/apache/solr/servlet/PathExclusionFilter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java b/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java index 5f8933811cb8..514de0126eb9 100644 --- a/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java +++ b/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java @@ -56,6 +56,8 @@ protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterC if (!shouldBeExcluded(req)) { chain.doFilter(req, res); } else { + // N.B. "default" is the name for org.eclipse.jetty.ee10.servlet.DefaultServlet + // configured in solr/server/etc/webdefault.xml req.getServletContext().getNamedDispatcher("default").forward(req, res); } } From 6f7b4ebf387faefe62cbee1d2456ac29e00a3419 Mon Sep 17 00:00:00 2001 From: Gus Heck Date: Wed, 24 Dec 2025 12:40:42 -0500 Subject: [PATCH 6/7] SOLR-18041 Class Javadoc --- .../java/org/apache/solr/servlet/PathExclusionFilter.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java b/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java index 514de0126eb9..6d6540ce8315 100644 --- a/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java +++ b/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java @@ -28,6 +28,13 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * Filter to identify paths that should be processed by Jetty's DefaultServlet. Typically, these + * paths contain static resources that need to be returned verbatim, with appropriate content type, + * which Jetty will determine via + * + *

{@code org.eclipse.jetty.http.MimeTypes#getMimeByExtension(java.lang.String)} + */ public class PathExclusionFilter extends HttpFilter { private List excludePatterns; From 25997eb212ce33918454e827203f933735f9381a Mon Sep 17 00:00:00 2001 From: Gus Heck Date: Thu, 25 Dec 2025 16:29:52 -0500 Subject: [PATCH 7/7] SOLR-18041 Better error message if default servlet not found, maintain some realism in JettySolrRunner --- .../solr/servlet/PathExclusionFilter.java | 19 ++++++++---- .../apache/solr/embedded/JettySolrRunner.java | 29 +++++++++++++++++-- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java b/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java index 6d6540ce8315..87e1e195a21a 100644 --- a/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java +++ b/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java @@ -18,6 +18,7 @@ import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; +import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpFilter; import jakarta.servlet.http.HttpServletRequest; @@ -60,12 +61,20 @@ public void init(FilterConfig config) throws ServletException { @Override protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { - if (!shouldBeExcluded(req)) { - chain.doFilter(req, res); - } else { + if (shouldBeExcluded(req)) { // N.B. "default" is the name for org.eclipse.jetty.ee10.servlet.DefaultServlet - // configured in solr/server/etc/webdefault.xml - req.getServletContext().getNamedDispatcher("default").forward(req, res); + // configured in solr/server/etc/webdefault.xml if it doesn't exist something is + // very wrong. + RequestDispatcher defaultServlet = req.getServletContext().getNamedDispatcher("default"); + if (defaultServlet == null) { + res.sendError( + 500, + "Server Misconfiguration: cannot find default servlet (normally defined as org.eclipse.jetty.ee10.servlet.DefaultServlet in webdefault.xml)"); + } else { + defaultServlet.forward(req, res); + } + } else { + chain.doFilter(req, res); } } } diff --git a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java index 522ec25491e4..982fc98c135b 100644 --- a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java +++ b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java @@ -61,6 +61,7 @@ import org.apache.solr.core.CoreContainer; import org.apache.solr.metrics.SolrMetricManager; import org.apache.solr.servlet.CoreContainerProvider; +import org.apache.solr.servlet.PathExclusionFilter; import org.apache.solr.servlet.SolrDispatchFilter; import org.apache.solr.util.SocketProxy; import org.apache.solr.util.TimeOut; @@ -109,8 +110,9 @@ public class JettySolrRunner { private Server server; - volatile FilterHolder dispatchFilter; volatile FilterHolder debugFilter; + volatile FilterHolder pathExcludeFilter; + volatile FilterHolder dispatchFilter; private int jettyPort = -1; @@ -398,14 +400,35 @@ public void contextInitialized(ServletContextEvent event) { for (Map.Entry entry : config.extraServlets.entrySet()) { root.addServlet(entry.getKey(), entry.getValue()); } + // TODO: This needs to be driven by a parsing of web.xml eventually + // though we still want to avoid classpath scanning. + + // this path excludes filter isn't actually necessary for any tests, but it's being + // added for parity with the live application. + pathExcludeFilter = root.getServletHandler().newFilterHolder(Source.EMBEDDED); + pathExcludeFilter.setHeldClass(PathExclusionFilter.class); + pathExcludeFilter.setInitParameter("excludePatterns", excludePatterns); + + // This is our main workhorse dispatchFilter = root.getServletHandler().newFilterHolder(Source.EMBEDDED); dispatchFilter.setHeldClass(SolrDispatchFilter.class); - dispatchFilter.setInitParameter("excludePatterns", excludePatterns); + // Map dispatchFilter in same path as in web.xml + root.addFilter(pathExcludeFilter, "/*", EnumSet.of(DispatcherType.REQUEST)); root.addFilter(dispatchFilter, "/*", EnumSet.of(DispatcherType.REQUEST)); // Default servlet as a fall-through - root.addServlet(Servlet404.class, "/"); + ServletHolder defaultHolder = root.getServletHandler().newServletHolder(Source.EMBEDDED); + + // considered adding DefaultServlet.class here but perhaps that might grant our unit tests + // the power to serve static resources on the build machines? Not sure, so I'll just give a + // name to our existing hack. The tests passed without this, but it will ensure that if anyone + // ever hits the PathExcludeFilter in the unit test they get a 404 as before not a 500 + defaultHolder.setHeldClass(Servlet404.class); + defaultHolder.setName("default"); + root.addServlet(defaultHolder, "/"); + + // TODO: end area that should be driven by web.xml and webdefault.xml chain = root; }