Cherry-pick of Improvements to PathSpec for Jetty 10.0.x (#8136)

* Cherry-pick of Improvements to PathSpec.
* From commit: 5b4d1dd1c6
* Fixing ConstraintSecurityHandler usage of PathMappings
* Fixing bad INCLUDE logic from cherry-pick in ServletHandler.doScope()
* Cleanup of non ServletPathSpec behaviors in ServletPathMapping class
* Skip optional group name/info lookup if regex fails.
* Prevent NPE on static servletPathMappings
* Update WebSocketMappings to use new PathMappings.getMatched(String)

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
Joakim Erdfelt 2022-06-08 12:36:30 -05:00
parent 351fe53c9a
commit 25dd6d0ef7
No known key found for this signature in database
GPG Key ID: 2D0E1FB8FE4B68B4
6 changed files with 211 additions and 76 deletions

View File

@ -178,13 +178,14 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
{ {
MappedResource<E> candidate = _exactMap.getBest(path, 0, i--); MappedResource<E> candidate = _exactMap.getBest(path, 0, i--);
if (candidate == null) if (candidate == null)
continue; break;
matchedPath = candidate.getPathSpec().matched(path); matchedPath = candidate.getPathSpec().matched(path);
if (matchedPath != null) if (matchedPath != null)
{ {
return new MatchedResource<>(candidate.getResource(), candidate.getPathSpec(), matchedPath); return new MatchedResource<>(candidate.getResource(), candidate.getPathSpec(), matchedPath);
} }
i--;
} }
// If we reached here, there's NO optimized EXACT Match possible, skip simple match below // If we reached here, there's NO optimized EXACT Match possible, skip simple match below
skipRestOfGroup = true; skipRestOfGroup = true;
@ -201,11 +202,12 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
{ {
MappedResource<E> candidate = _prefixMap.getBest(path, 0, i--); MappedResource<E> candidate = _prefixMap.getBest(path, 0, i--);
if (candidate == null) if (candidate == null)
continue; break;
matchedPath = candidate.getPathSpec().matched(path); matchedPath = candidate.getPathSpec().matched(path);
if (matchedPath != null) if (matchedPath != null)
return new MatchedResource<>(candidate.getResource(), candidate.getPathSpec(), matchedPath); return new MatchedResource<>(candidate.getResource(), candidate.getPathSpec(), matchedPath);
i--;
} }
// If we reached here, there's NO optimized PREFIX Match possible, skip simple match below // If we reached here, there's NO optimized PREFIX Match possible, skip simple match below
skipRestOfGroup = true; skipRestOfGroup = true;
@ -227,7 +229,7 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
{ {
MappedResource<E> candidate = _suffixMap.get(path, i + 1, path.length() - i - 1); MappedResource<E> candidate = _suffixMap.get(path, i + 1, path.length() - i - 1);
if (candidate == null) if (candidate == null)
continue; break;
matchedPath = candidate.getPathSpec().matched(path); matchedPath = candidate.getPathSpec().matched(path);
if (matchedPath != null) if (matchedPath != null)
@ -259,6 +261,15 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
return _mappings.iterator(); return _mappings.iterator();
} }
/**
* @deprecated use {@link PathSpec#from(String)} instead
*/
@Deprecated
public static PathSpec asPathSpec(String pathSpecString)
{
return PathSpec.from(pathSpecString);
}
public E get(PathSpec spec) public E get(PathSpec spec)
{ {
return _mappings.stream() return _mappings.stream()

View File

@ -125,9 +125,8 @@ public class RegexPathSpec extends AbstractPathSpec
declaration = regex; declaration = regex;
int specLength = declaration.length(); int specLength = declaration.length();
// build up a simple signature we can use to identify the grouping // build up a simple signature we can use to identify the grouping
boolean inCharacterClass = false; boolean inTextList = false;
boolean inQuantifier = false; boolean inQuantifier = false;
boolean inCaptureGroup = false;
StringBuilder signature = new StringBuilder(); StringBuilder signature = new StringBuilder();
int pathDepth = 0; int pathDepth = 0;
@ -140,6 +139,8 @@ public class RegexPathSpec extends AbstractPathSpec
case '^': // ignore anchors case '^': // ignore anchors
case '$': // ignore anchors case '$': // ignore anchors
case '\'': // ignore escaping case '\'': // ignore escaping
case '(': // ignore grouping
case ')': // ignore grouping
break; break;
case '+': // single char quantifier case '+': // single char quantifier
case '?': // single char quantifier case '?': // single char quantifier
@ -148,32 +149,25 @@ public class RegexPathSpec extends AbstractPathSpec
case '.': // any char token case '.': // any char token
signature.append('g'); // glob signature.append('g'); // glob
break; break;
case '(': // in regex capture group case '{':
inCaptureGroup = true;
break;
case ')':
inCaptureGroup = false;
signature.append('g');
break;
case '{': // in regex quantifier
inQuantifier = true; inQuantifier = true;
break; break;
case '}': case '}':
inQuantifier = false; inQuantifier = false;
break; break;
case '[': // in regex character class case '[':
inCharacterClass = true; inTextList = true;
break; break;
case ']': case ']':
inCharacterClass = false; inTextList = false;
signature.append('g'); // glob signature.append('g'); // glob
break; break;
case '/': case '/':
if (!inCharacterClass && !inQuantifier && !inCaptureGroup) if (!inTextList && !inQuantifier)
pathDepth++; pathDepth++;
break; break;
default: default:
if (!inCharacterClass && !inQuantifier && !inCaptureGroup && Character.isLetterOrDigit(c)) if (!inTextList && !inQuantifier && Character.isLetterOrDigit(c))
{ {
if (last == '\\') // escaped if (last == '\\') // escaped
{ {
@ -325,6 +319,101 @@ public class RegexPathSpec extends AbstractPathSpec
return null; return null;
} }
private class RegexMatchedPath implements MatchedPath
{
private final RegexPathSpec pathSpec;
private final String path;
private final Matcher matcher;
public RegexMatchedPath(RegexPathSpec regexPathSpec, String path, Matcher matcher)
{
this.pathSpec = regexPathSpec;
this.path = path;
this.matcher = matcher;
}
@Override
public String getPathMatch()
{
try
{
String p = matcher.group("name");
if (p != null)
{
return p;
}
}
catch (IllegalArgumentException ignore)
{
// ignore if group name not found.
}
if (pathSpec.getGroup() == PathSpecGroup.PREFIX_GLOB && matcher.groupCount() >= 1)
{
int idx = matcher.start(1);
if (idx > 0)
{
if (this.path.charAt(idx - 1) == '/')
idx--;
return this.path.substring(0, idx);
}
}
// default is the full path
return this.path;
}
@Override
public String getPathInfo()
{
try
{
String p = matcher.group("info");
if (p != null)
{
return p;
}
}
catch (IllegalArgumentException ignore)
{
// ignore if group info not found.
}
// Path Info only valid for PREFIX_GLOB
if (pathSpec.getGroup() == PathSpecGroup.PREFIX_GLOB && matcher.groupCount() >= 1)
{
String pathInfo = matcher.group(1);
if ("".equals(pathInfo))
return "/";
else
return pathInfo;
}
// default is null
return null;
}
@Override
public String toString()
{
return getClass().getSimpleName() + "[" +
"pathSpec=" + pathSpec +
", path=\"" + path + "\"" +
", matcher=" + matcher +
']';
}
@Override
public MatchedPath matched(String path)
{
Matcher matcher = getMatcher(path);
if (matcher.matches())
{
return new RegexMatchedPath(this, path, matcher);
}
return null;
}
private class RegexMatchedPath implements MatchedPath private class RegexMatchedPath implements MatchedPath
{ {
private final RegexPathSpec pathSpec; private final RegexPathSpec pathSpec;

View File

@ -28,6 +28,9 @@ import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class RegexPathSpecTest public class RegexPathSpecTest
{ {
@ -201,8 +204,8 @@ public class RegexPathSpecTest
assertNotMatches(spec, "/aa/bb"); assertNotMatches(spec, "/aa/bb");
assertNotMatches(spec, "/aa/bb.do/more"); assertNotMatches(spec, "/aa/bb.do/more");
assertThat(spec.getPathMatch("/a/b/c.do"), equalTo("")); assertThat(spec.getPathMatch("/a/b/c.do"), equalTo("/a/b/c.do"));
assertThat(spec.getPathInfo("/a/b/c.do"), equalTo("/a/b/c.do")); assertThat(spec.getPathInfo("/a/b/c.do"), nullValue());
} }
/** /**

View File

@ -3,3 +3,4 @@
#org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.LEVEL=DEBUG
#org.eclipse.jetty.server.LEVEL=DEBUG #org.eclipse.jetty.server.LEVEL=DEBUG
#org.eclipse.jetty.http.LEVEL=DEBUG #org.eclipse.jetty.http.LEVEL=DEBUG
#org.eclipse.jetty.http.pathmap.LEVEL=DEBUG

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.ee10.servlet;
import jakarta.servlet.http.HttpServletMapping; import jakarta.servlet.http.HttpServletMapping;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.MappingMatch; import jakarta.servlet.http.MappingMatch;
import org.eclipse.jetty.http.pathmap.MatchedPath;
import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.http.pathmap.ServletPathSpec; import org.eclipse.jetty.http.pathmap.ServletPathSpec;
@ -38,71 +39,100 @@ public class ServletPathMapping implements HttpServletMapping
private final String _servletPath; private final String _servletPath;
private final String _pathInfo; private final String _pathInfo;
public ServletPathMapping(PathSpec pathSpec, String servletName, String pathInContext) public ServletPathMapping(PathSpec pathSpec, String servletName, String pathInContext, MatchedPath matchedPath)
{ {
_servletName = (servletName == null ? "" : servletName); _servletName = (servletName == null ? "" : servletName);
_pattern = pathSpec == null ? null : pathSpec.getDeclaration();
if (pathSpec instanceof ServletPathSpec && pathInContext != null) if (pathSpec == null)
{
switch (pathSpec.getGroup())
{
case ROOT:
_mappingMatch = MappingMatch.CONTEXT_ROOT;
_matchValue = "";
_servletPath = "";
_pathInfo = "/";
break;
case DEFAULT:
_mappingMatch = MappingMatch.DEFAULT;
_matchValue = "";
_servletPath = pathInContext;
_pathInfo = null;
break;
case EXACT:
_mappingMatch = MappingMatch.EXACT;
_matchValue = _pattern.startsWith("/") ? _pattern.substring(1) : _pattern;
_servletPath = _pattern;
_pathInfo = null;
break;
case PREFIX_GLOB:
_mappingMatch = MappingMatch.PATH;
_servletPath = pathSpec.getPrefix();
// TODO avoid the substring on the known servletPath!
_matchValue = _servletPath.startsWith("/") ? _servletPath.substring(1) : _servletPath;
_pathInfo = pathSpec.getPathInfo(pathInContext);
break;
case SUFFIX_GLOB:
_mappingMatch = MappingMatch.EXTENSION;
int dot = pathInContext.lastIndexOf('.');
_matchValue = pathInContext.substring(pathInContext.startsWith("/") ? 1 : 0, dot);
_servletPath = pathInContext;
_pathInfo = null;
break;
case MIDDLE_GLOB:
default:
throw new IllegalStateException();
}
}
else if (pathSpec != null)
{
_mappingMatch = null;
_servletPath = pathSpec.getPathMatch(pathInContext);
_matchValue = _servletPath.startsWith("/") ? _servletPath.substring(1) : _servletPath;
_pathInfo = pathSpec.getPathInfo(pathInContext);
}
else
{ {
_pattern = null;
_mappingMatch = null; _mappingMatch = null;
_matchValue = ""; _matchValue = "";
_servletPath = pathInContext; _servletPath = pathInContext;
_pathInfo = null; _pathInfo = null;
return;
} }
if (pathInContext == null)
{
_pattern = pathSpec.getDeclaration();
_mappingMatch = null;
_matchValue = "";
_servletPath = "";
_pathInfo = null;
return;
}
// Path Spec types that are not ServletPathSpec
if (!(pathSpec instanceof ServletPathSpec))
{
_pattern = pathSpec.getDeclaration();
_mappingMatch = null;
if (matchedPath != null)
{
_servletPath = matchedPath.getPathMatch();
_pathInfo = matchedPath.getPathInfo();
}
else
{
_servletPath = pathInContext;
_pathInfo = null;
}
_matchValue = _servletPath.substring(_servletPath.charAt(0) == '/' ? 1 : 0);
return;
}
// from here down is ServletPathSpec behavior
_pattern = pathSpec.getDeclaration();
switch (pathSpec.getGroup())
{
case ROOT:
_mappingMatch = MappingMatch.CONTEXT_ROOT;
_matchValue = "";
_servletPath = "";
_pathInfo = "/";
break;
case DEFAULT:
_mappingMatch = MappingMatch.DEFAULT;
_matchValue = "";
_servletPath = pathInContext;
_pathInfo = null;
break;
case EXACT:
_mappingMatch = MappingMatch.EXACT;
_matchValue = _pattern.startsWith("/") ? _pattern.substring(1) : _pattern;
_servletPath = _pattern;
_pathInfo = null;
break;
case PREFIX_GLOB:
_mappingMatch = MappingMatch.PATH;
_servletPath = pathSpec.getPrefix();
// TODO avoid the substring on the known servletPath!
_matchValue = _servletPath.startsWith("/") ? _servletPath.substring(1) : _servletPath;
_pathInfo = matchedPath != null ? matchedPath.getPathInfo() : null;
break;
case SUFFIX_GLOB:
_mappingMatch = MappingMatch.EXTENSION;
int dot = pathInContext.lastIndexOf('.');
_matchValue = pathInContext.substring(pathInContext.startsWith("/") ? 1 : 0, dot);
_servletPath = pathInContext;
_pathInfo = null;
break;
case MIDDLE_GLOB:
default:
throw new IllegalStateException("ServletPathSpec of type MIDDLE_GLOB");
}
}
public ServletPathMapping(PathSpec pathSpec, String servletName, String pathInContext)
{
this(pathSpec, servletName, pathInContext, null);
} }
@Override @Override

View File

@ -64,6 +64,7 @@ import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.UriCompliance; import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.pathmap.MatchedPath;
import org.eclipse.jetty.http.pathmap.RegexPathSpec; import org.eclipse.jetty.http.pathmap.RegexPathSpec;
import org.eclipse.jetty.http.pathmap.MatchedPath; import org.eclipse.jetty.http.pathmap.MatchedPath;
import org.eclipse.jetty.http.pathmap.RegexPathSpec; import org.eclipse.jetty.http.pathmap.RegexPathSpec;