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--);
if (candidate == null)
continue;
break;
matchedPath = candidate.getPathSpec().matched(path);
if (matchedPath != null)
{
return new MatchedResource<>(candidate.getResource(), candidate.getPathSpec(), matchedPath);
}
i--;
}
// If we reached here, there's NO optimized EXACT Match possible, skip simple match below
skipRestOfGroup = true;
@ -201,11 +202,12 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
{
MappedResource<E> candidate = _prefixMap.getBest(path, 0, i--);
if (candidate == null)
continue;
break;
matchedPath = candidate.getPathSpec().matched(path);
if (matchedPath != null)
return new MatchedResource<>(candidate.getResource(), candidate.getPathSpec(), matchedPath);
i--;
}
// If we reached here, there's NO optimized PREFIX Match possible, skip simple match below
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);
if (candidate == null)
continue;
break;
matchedPath = candidate.getPathSpec().matched(path);
if (matchedPath != null)
@ -259,6 +261,15 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
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)
{
return _mappings.stream()

View File

@ -125,9 +125,8 @@ public class RegexPathSpec extends AbstractPathSpec
declaration = regex;
int specLength = declaration.length();
// build up a simple signature we can use to identify the grouping
boolean inCharacterClass = false;
boolean inTextList = false;
boolean inQuantifier = false;
boolean inCaptureGroup = false;
StringBuilder signature = new StringBuilder();
int pathDepth = 0;
@ -140,6 +139,8 @@ public class RegexPathSpec extends AbstractPathSpec
case '^': // ignore anchors
case '$': // ignore anchors
case '\'': // ignore escaping
case '(': // ignore grouping
case ')': // ignore grouping
break;
case '+': // single char quantifier
case '?': // single char quantifier
@ -148,32 +149,25 @@ public class RegexPathSpec extends AbstractPathSpec
case '.': // any char token
signature.append('g'); // glob
break;
case '(': // in regex capture group
inCaptureGroup = true;
break;
case ')':
inCaptureGroup = false;
signature.append('g');
break;
case '{': // in regex quantifier
case '{':
inQuantifier = true;
break;
case '}':
inQuantifier = false;
break;
case '[': // in regex character class
inCharacterClass = true;
case '[':
inTextList = true;
break;
case ']':
inCharacterClass = false;
inTextList = false;
signature.append('g'); // glob
break;
case '/':
if (!inCharacterClass && !inQuantifier && !inCaptureGroup)
if (!inTextList && !inQuantifier)
pathDepth++;
break;
default:
if (!inCharacterClass && !inQuantifier && !inCaptureGroup && Character.isLetterOrDigit(c))
if (!inTextList && !inQuantifier && Character.isLetterOrDigit(c))
{
if (last == '\\') // escaped
{
@ -325,6 +319,101 @@ public class RegexPathSpec extends AbstractPathSpec
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 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.assertNotNull;
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
{
@ -201,8 +204,8 @@ public class RegexPathSpecTest
assertNotMatches(spec, "/aa/bb");
assertNotMatches(spec, "/aa/bb.do/more");
assertThat(spec.getPathMatch("/a/b/c.do"), equalTo(""));
assertThat(spec.getPathInfo("/a/b/c.do"), equalTo("/a/b/c.do"));
assertThat(spec.getPathMatch("/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.server.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.HttpServletRequest;
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.ServletPathSpec;
@ -38,13 +39,52 @@ public class ServletPathMapping implements HttpServletMapping
private final String _servletPath;
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);
_pattern = pathSpec == null ? null : pathSpec.getDeclaration();
if (pathSpec instanceof ServletPathSpec && pathInContext != null)
if (pathSpec == null)
{
_pattern = null;
_mappingMatch = null;
_matchValue = "";
_servletPath = pathInContext;
_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:
@ -73,7 +113,7 @@ public class ServletPathMapping implements HttpServletMapping
_servletPath = pathSpec.getPrefix();
// TODO avoid the substring on the known servletPath!
_matchValue = _servletPath.startsWith("/") ? _servletPath.substring(1) : _servletPath;
_pathInfo = pathSpec.getPathInfo(pathInContext);
_pathInfo = matchedPath != null ? matchedPath.getPathInfo() : null;
break;
case SUFFIX_GLOB:
@ -86,23 +126,13 @@ public class ServletPathMapping implements HttpServletMapping
case MIDDLE_GLOB:
default:
throw new IllegalStateException();
throw new IllegalStateException("ServletPathSpec of type MIDDLE_GLOB");
}
}
else if (pathSpec != null)
public ServletPathMapping(PathSpec pathSpec, String servletName, String pathInContext)
{
_mappingMatch = null;
_servletPath = pathSpec.getPathMatch(pathInContext);
_matchValue = _servletPath.startsWith("/") ? _servletPath.substring(1) : _servletPath;
_pathInfo = pathSpec.getPathInfo(pathInContext);
}
else
{
_mappingMatch = null;
_matchValue = "";
_servletPath = pathInContext;
_pathInfo = null;
}
this(pathSpec, servletName, pathInContext, null);
}
@Override

View File

@ -64,6 +64,7 @@ import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MimeTypes;
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.MatchedPath;
import org.eclipse.jetty.http.pathmap.RegexPathSpec;