Skip to content

Commit 58e11d8

Browse files
authored
SOLR-18041 PathExclusionFilter extracted from SolrDispatchFilter (#3981)
Collects and simplify all path exclusion logic into a single class configured in web.xml and makes our reliance on jetty's DefaultServlet (or whatever is configured with the name 'default') for serving static content explicit rather than an implicit feature one has to figure out (or already know about).
1 parent 0752362 commit 58e11d8

7 files changed

Lines changed: 129 additions & 91 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
2+
title: SOLR 18041 - Path exclusions for the admin UI are now defined in a separate servlet filter
3+
type: other # added, changed, fixed, deprecated, removed, dependency_update, security, other
4+
authors:
5+
- name: Gus Heck
6+
links:
7+
- name: SOLR-18041
8+
url: https://issues.apache.org/jira/browse/SOLR-18041

solr/core/src/java/org/apache/solr/servlet/PathExcluder.java

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.solr.servlet;
18+
19+
import jakarta.servlet.FilterChain;
20+
import jakarta.servlet.FilterConfig;
21+
import jakarta.servlet.RequestDispatcher;
22+
import jakarta.servlet.ServletException;
23+
import jakarta.servlet.http.HttpFilter;
24+
import jakarta.servlet.http.HttpServletRequest;
25+
import jakarta.servlet.http.HttpServletResponse;
26+
import java.io.IOException;
27+
import java.util.Arrays;
28+
import java.util.List;
29+
import java.util.regex.Matcher;
30+
import java.util.regex.Pattern;
31+
32+
/**
33+
* Filter to identify paths that should be processed by Jetty's DefaultServlet. Typically, these
34+
* paths contain static resources that need to be returned verbatim, with appropriate content type,
35+
* which Jetty will determine via
36+
*
37+
* <p>{@code org.eclipse.jetty.http.MimeTypes#getMimeByExtension(java.lang.String)}
38+
*/
39+
public class PathExclusionFilter extends HttpFilter {
40+
41+
private List<Pattern> excludePatterns;
42+
43+
boolean shouldBeExcluded(HttpServletRequest request) {
44+
String requestPath = ServletUtils.getPathAfterContext(request);
45+
if (excludePatterns != null) {
46+
return excludePatterns.stream().map(p -> p.matcher(requestPath)).anyMatch(Matcher::lookingAt);
47+
}
48+
return false;
49+
}
50+
51+
@Override
52+
public void init(FilterConfig config) throws ServletException {
53+
String patternConfig = config.getInitParameter("excludePatterns");
54+
if (patternConfig != null) {
55+
String[] excludeArray = patternConfig.split(",");
56+
this.excludePatterns = Arrays.stream(excludeArray).map(Pattern::compile).toList();
57+
}
58+
super.init(config);
59+
}
60+
61+
@Override
62+
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
63+
throws IOException, ServletException {
64+
if (shouldBeExcluded(req)) {
65+
// N.B. "default" is the name for org.eclipse.jetty.ee10.servlet.DefaultServlet
66+
// configured in solr/server/etc/webdefault.xml if it doesn't exist something is
67+
// very wrong.
68+
RequestDispatcher defaultServlet = req.getServletContext().getNamedDispatcher("default");
69+
if (defaultServlet == null) {
70+
res.sendError(
71+
500,
72+
"Server Misconfiguration: cannot find default servlet (normally defined as org.eclipse.jetty.ee10.servlet.DefaultServlet in webdefault.xml)");
73+
} else {
74+
defaultServlet.forward(req, res);
75+
}
76+
} else {
77+
chain.doFilter(req, res);
78+
}
79+
}
80+
}

solr/core/src/java/org/apache/solr/servlet/ServletUtils.java

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
import io.opentelemetry.api.trace.Span;
2121
import io.opentelemetry.context.Context;
22-
import jakarta.servlet.FilterChain;
2322
import jakarta.servlet.ReadListener;
2423
import jakarta.servlet.ServletException;
2524
import jakarta.servlet.ServletInputStream;
@@ -33,10 +32,6 @@
3332
import java.io.InputStream;
3433
import java.io.OutputStream;
3534
import java.lang.invoke.MethodHandles;
36-
import java.util.ArrayList;
37-
import java.util.List;
38-
import java.util.regex.Matcher;
39-
import java.util.regex.Pattern;
4035
import org.apache.solr.common.SolrException;
4136
import org.apache.solr.common.SolrException.ErrorCode;
4237
import org.apache.solr.common.util.Utils;
@@ -131,45 +126,6 @@ public void close() {
131126
};
132127
}
133128

134-
static boolean excludedPath(
135-
List<Pattern> excludePatterns,
136-
HttpServletRequest request,
137-
HttpServletResponse response,
138-
FilterChain chain)
139-
throws IOException, ServletException {
140-
String requestPath = getPathAfterContext(request);
141-
// No need to even create the HttpSolrCall object if this path is excluded.
142-
if (excludePatterns != null) {
143-
for (Pattern p : excludePatterns) {
144-
Matcher matcher = p.matcher(requestPath);
145-
if (matcher.lookingAt()) {
146-
if (chain != null) {
147-
chain.doFilter(request, response);
148-
}
149-
return true;
150-
}
151-
}
152-
}
153-
return false;
154-
}
155-
156-
static boolean excludedPath(
157-
List<Pattern> excludePatterns, HttpServletRequest request, HttpServletResponse response)
158-
throws IOException, ServletException {
159-
return excludedPath(excludePatterns, request, response, null);
160-
}
161-
162-
static void configExcludes(PathExcluder excluder, String patternConfig) {
163-
if (patternConfig != null) {
164-
String[] excludeArray = patternConfig.split(",");
165-
List<Pattern> patterns = new ArrayList<>();
166-
excluder.setExcludePatterns(patterns);
167-
for (String element : excludeArray) {
168-
patterns.add(Pattern.compile(element));
169-
}
170-
}
171-
}
172-
173129
/**
174130
* Enforces rate limiting for a request. Should be converted to a servlet filter at some point.
175131
* Currently, this is tightly coupled with request tracing which is not ideal either.

solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
package org.apache.solr.servlet;
1818

1919
import static org.apache.solr.servlet.ServletUtils.closeShield;
20-
import static org.apache.solr.servlet.ServletUtils.configExcludes;
21-
import static org.apache.solr.servlet.ServletUtils.excludedPath;
2220
import static org.apache.solr.util.tracing.TraceUtils.getSpan;
2321
import static org.apache.solr.util.tracing.TraceUtils.setTracer;
2422

@@ -67,7 +65,7 @@
6765
// servlets that are more focused in scope. This should become possible now that we have a
6866
// ServletContextListener for startup/shutdown of CoreContainer that sets up a service from which
6967
// things like CoreContainer can be requested. (or better yet injected)
70-
public class SolrDispatchFilter extends HttpFilter implements PathExcluder {
68+
public class SolrDispatchFilter extends HttpFilter {
7169
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
7270

7371
private CoreContainerProvider containerProvider;
@@ -78,11 +76,6 @@ public class SolrDispatchFilter extends HttpFilter implements PathExcluder {
7876

7977
private HttpSolrCallFactory solrCallFactory;
8078

81-
@Override
82-
public void setExcludePatterns(List<Pattern> excludePatterns) {
83-
this.excludePatterns = excludePatterns;
84-
}
85-
8679
private List<Pattern> excludePatterns;
8780

8881
public final boolean isV2Enabled = V2ApiUtils.isEnabled();
@@ -133,7 +126,6 @@ public void init(FilterConfig config) throws ServletException {
133126
log.trace("SolrDispatchFilter.init(): {}", this.getClass().getClassLoader());
134127
}
135128

136-
configExcludes(this, config.getInitParameter("excludePatterns"));
137129
} catch (Throwable t) {
138130
// catch this so our filter still works
139131
log.error("Could not start Dispatch Filter.", t);
@@ -157,9 +149,6 @@ public CoreContainer getCores() throws UnavailableException {
157149
"Set the thread contextClassLoader for all 3rd party dependencies that we cannot control")
158150
public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
159151
throws IOException, ServletException {
160-
if (excludedPath(excludePatterns, request, response, chain)) {
161-
return;
162-
}
163152

164153
try (var mdcSnapshot = MDCSnapshot.create()) {
165154
assert null != mdcSnapshot; // prevent compiler warning

solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import org.apache.solr.core.CoreContainer;
6262
import org.apache.solr.metrics.SolrMetricManager;
6363
import org.apache.solr.servlet.CoreContainerProvider;
64+
import org.apache.solr.servlet.PathExclusionFilter;
6465
import org.apache.solr.servlet.SolrDispatchFilter;
6566
import org.apache.solr.util.SocketProxy;
6667
import org.apache.solr.util.TimeOut;
@@ -109,8 +110,9 @@ public class JettySolrRunner {
109110

110111
private Server server;
111112

112-
volatile FilterHolder dispatchFilter;
113113
volatile FilterHolder debugFilter;
114+
volatile FilterHolder pathExcludeFilter;
115+
volatile FilterHolder dispatchFilter;
114116

115117
private int jettyPort = -1;
116118

@@ -398,14 +400,35 @@ public void contextInitialized(ServletContextEvent event) {
398400
for (Map.Entry<ServletHolder, String> entry : config.extraServlets.entrySet()) {
399401
root.addServlet(entry.getKey(), entry.getValue());
400402
}
403+
// TODO: This needs to be driven by a parsing of web.xml eventually
404+
// though we still want to avoid classpath scanning.
405+
406+
// this path excludes filter isn't actually necessary for any tests, but it's being
407+
// added for parity with the live application.
408+
pathExcludeFilter = root.getServletHandler().newFilterHolder(Source.EMBEDDED);
409+
pathExcludeFilter.setHeldClass(PathExclusionFilter.class);
410+
pathExcludeFilter.setInitParameter("excludePatterns", excludePatterns);
411+
412+
// This is our main workhorse
401413
dispatchFilter = root.getServletHandler().newFilterHolder(Source.EMBEDDED);
402414
dispatchFilter.setHeldClass(SolrDispatchFilter.class);
403-
dispatchFilter.setInitParameter("excludePatterns", excludePatterns);
415+
404416
// Map dispatchFilter in same path as in web.xml
417+
root.addFilter(pathExcludeFilter, "/*", EnumSet.of(DispatcherType.REQUEST));
405418
root.addFilter(dispatchFilter, "/*", EnumSet.of(DispatcherType.REQUEST));
406419

407420
// Default servlet as a fall-through
408-
root.addServlet(Servlet404.class, "/");
421+
ServletHolder defaultHolder = root.getServletHandler().newServletHolder(Source.EMBEDDED);
422+
423+
// considered adding DefaultServlet.class here but perhaps that might grant our unit tests
424+
// the power to serve static resources on the build machines? Not sure, so I'll just give a
425+
// name to our existing hack. The tests passed without this, but it will ensure that if anyone
426+
// ever hits the PathExcludeFilter in the unit test they get a 404 as before not a 500
427+
defaultHolder.setHeldClass(Servlet404.class);
428+
defaultHolder.setName("default");
429+
root.addServlet(defaultHolder, "/");
430+
431+
// TODO: end area that should be driven by web.xml and webdefault.xml
409432
chain = root;
410433
}
411434

solr/webapp/web/WEB-INF/web.xml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@
2626
</listener>
2727
<!-- Any path (name) registered in solrconfig.xml will be sent to that filter -->
2828
<filter>
29-
<filter-name>SolrRequestFilter</filter-name>
30-
<filter-class>org.apache.solr.servlet.SolrDispatchFilter</filter-class>
29+
<filter-name>PathExclusionsFilter</filter-name>
30+
<filter-class>org.apache.solr.servlet.PathExclusionFilter</filter-class>
3131
<!--
32-
Exclude patterns is a list of directories that would be short circuited by the
33-
SolrDispatchFilter. It includes all Admin UI related static content.
32+
Exclude patterns is a list of directories that would be short-circuited by this
33+
Filter. It includes all Admin UI related static content.
3434
NOTE: It is NOT a pattern but only matches the start of the HTTP ServletPath.
3535
-->
3636
<init-param>
@@ -39,6 +39,16 @@
3939
</init-param>
4040
</filter>
4141

42+
<filter-mapping>
43+
<filter-name>PathExclusionsFilter</filter-name>
44+
<url-pattern>/*</url-pattern>
45+
</filter-mapping>
46+
47+
<filter>
48+
<filter-name>SolrRequestFilter</filter-name>
49+
<filter-class>org.apache.solr.servlet.SolrDispatchFilter</filter-class>
50+
</filter>
51+
4252
<filter-mapping>
4353
<filter-name>SolrRequestFilter</filter-name>
4454
<url-pattern>/*</url-pattern>

0 commit comments

Comments
 (0)