Jetty 12 - Simplification of aliases in `PathResource` (Take 2) (#8734)

* simplify the PathResource.resolveTargetPath code
* changes to how PathResource handles aliases
* fix usages of Resource.getTargetUri()
* fixes for FileSystemResourceTest
* update javadoc for Resource.getTargetURI()
* rename getTargetURI to getCanonicalURI
* let resolveCanonicalPath return null if resource does not exist
* add test in PathResourceTest for broken symlinks
* some changes from review + optimization for exists()
* restore name to getTargetUri in Resource
* fix some tests related to PathResource changes
* revert changes to PathResource equals and hashcode
* also compare URI in PathResource
* checkAlias to resolveAlias
* PathResource cleanup
+ Adding comments about class fields.
+ Removing normalization from
  input/output/comparison flows.
+ Collapsing `resolveTargetPath`
  into `resolveAlias` to react
  accordingly to the exceptions
  that can flow out of Path.toRealPath().
+ Failure on Path.toRealPath() is never
  an alias, as the resource cannot ever
  be served anyway.
+ More comments in `resolveAlias()`
+ Failed / Bad / Nonexistent / Inaccessible
  resources are not aliases to anything.
* Renames of targetPath/targetUri
  `targetPath` to `realPath`
  `targetURI` to `realURI`
* Cleanup alias/aliasResolved booleans
* More testcase cleanup around not-exist
* Don't resolve alias on Error during toRealPath
* Add test to check how Alias check behaves if non-existent resource exists later

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Co-authored-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Joakim Erdfelt 2022-10-19 11:17:23 -05:00 committed by GitHub
parent 6ba81ce10d
commit 9061348ec4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 456 additions and 268 deletions

View File

@ -867,7 +867,7 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace
if (resource.isAlias()) if (resource.isAlias())
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Aliased resource: {} -> {}", resource, resource.getTargetURI()); LOG.debug("Aliased resource: {} -> {}", resource, resource.getRealURI());
// alias checks // alias checks
for (AliasCheck check : _aliasChecks) for (AliasCheck check : _aliasChecks)

View File

@ -344,7 +344,7 @@ public class ContextHandlerGetResourceTest
Resource resource = context.getResource(path); Resource resource = context.getResource(path);
assertNotNull(resource); assertNotNull(resource);
assertEquals(context.getResource("/subdir/TextFile.Long.txt").getURI(), resource.getTargetURI()); assertEquals(context.getResource("/subdir/TextFile.Long.txt").getURI(), resource.getRealURI());
URL url = context.getServletContext().getResource(path); URL url = context.getServletContext().getResource(path);
assertNotNull(url); assertNotNull(url);

View File

@ -47,10 +47,14 @@ public class PathResource extends Resource
.with("jrt") .with("jrt")
.build(); .build();
// The path object represented by this instance
private final Path path; private final Path path;
// The as-requested URI for this path object
private final URI uri; private final URI uri;
private boolean targetResolved = false; // True / False to indicate if this is an alias of something else, or null if the alias hasn't been resolved
private Path targetPath; private Boolean alias;
// The Path representing the real-path of this PathResource instance. (populated during alias checking)
private Path realPath;
/** /**
* Test if the paths are the same name. * Test if the paths are the same name.
@ -142,9 +146,7 @@ public class PathResource extends Resource
PathResource(URI uri, boolean bypassAllowedSchemeCheck) PathResource(URI uri, boolean bypassAllowedSchemeCheck)
{ {
// normalize to referenced location, Paths.get() doesn't like "/bar/../foo/text.txt" style references this(Paths.get(uri), uri, bypassAllowedSchemeCheck);
// and will return a Path that will not be found with `Files.exists()` or `Files.isDirectory()`
this(Paths.get(uri.normalize()), uri, bypassAllowedSchemeCheck);
} }
PathResource(Path path) PathResource(Path path)
@ -152,6 +154,13 @@ public class PathResource extends Resource
this(path, path.toUri(), true); this(path, path.toUri(), true);
} }
/**
* Create a PathResource.
*
* @param path the Path object
* @param uri the as-requested URI for the resource
* @param bypassAllowedSchemeCheck true to bypass the allowed schemes check
*/
PathResource(Path path, URI uri, boolean bypassAllowedSchemeCheck) PathResource(Path path, URI uri, boolean bypassAllowedSchemeCheck)
{ {
if (!uri.isAbsolute()) if (!uri.isAbsolute())
@ -159,9 +168,12 @@ public class PathResource extends Resource
if (!bypassAllowedSchemeCheck && !ALLOWED_SCHEMES.contains(uri.getScheme())) if (!bypassAllowedSchemeCheck && !ALLOWED_SCHEMES.contains(uri.getScheme()))
throw new IllegalArgumentException("not an allowed scheme: " + uri); throw new IllegalArgumentException("not an allowed scheme: " + uri);
String uriString = uri.toASCIIString(); if (Files.isDirectory(path))
if (Files.isDirectory(path) && !uriString.endsWith(URIUtil.SLASH)) {
uri = URIUtil.correctFileURI(URI.create(uriString + URIUtil.SLASH)); String uriString = uri.toASCIIString();
if (!uriString.endsWith(URIUtil.SLASH))
uri = URIUtil.correctFileURI(URI.create(uriString + URIUtil.SLASH));
}
this.path = path; this.path = path;
this.uri = uri; this.uri = uri;
@ -170,36 +182,38 @@ public class PathResource extends Resource
@Override @Override
public boolean exists() public boolean exists()
{ {
return Files.exists(targetPath != null ? targetPath : path); if (alias == null)
{
// no alias check performed
return Files.exists(path);
}
else
{
if (realPath == null)
return false;
return Files.exists(realPath);
}
} }
@Override @Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
PathResource other = (PathResource)obj;
return Objects.equals(path, other.path);
}
/**
* @return the {@link Path} of the resource
*/
public Path getPath() public Path getPath()
{ {
return path; return path;
} }
public Path getRealPath()
{
resolveAlias();
return realPath;
}
@Override
public URI getRealURI()
{
Path realPath = getRealPath();
return (realPath == null) ? null : realPath.toUri();
}
public List<Resource> list() public List<Resource> list()
{ {
if (!isDirectory()) if (!isDirectory())
@ -220,6 +234,13 @@ public class PathResource extends Resource
return List.of(); // empty return List.of(); // empty
} }
@Override
public boolean isAlias()
{
resolveAlias();
return alias != null && alias;
}
@Override @Override
public String getName() public String getName()
{ {
@ -241,12 +262,6 @@ public class PathResource extends Resource
return this.uri; return this.uri;
} }
@Override
public int hashCode()
{
return Objects.hashCode(path);
}
@Override @Override
public boolean isContainedIn(Resource r) public boolean isContainedIn(Resource r)
{ {
@ -255,88 +270,141 @@ public class PathResource extends Resource
return r.getClass() == PathResource.class && path.startsWith(r.getPath()); return r.getClass() == PathResource.class && path.startsWith(r.getPath());
} }
@Override /**
public URI getTargetURI() * <p>
* Perform a check of the original Path and as-requested URI to determine
* if this resource is an alias to another name/location.
* </p>
*
* <table>
* <thead>
* <tr>
* <th>path</th>
* <th>realPath</th>
* <th>uri-as-requested</th>
* <th>uri-from-realPath</th>
* <th>alias</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td><code>C:/temp/aa./foo.txt</code></td>
* <td><code>C:/temp/aa/foo.txt</code></td>
* <td><code>file:///C:/temp/aa./foo.txt</code></td>
* <td><code>file:///C:/temp/aa./foo.txt</code></td>
* <td>true</td>
* </tr>
* <tr>
* <td><code>/tmp/foo-symlink</code></td>
* <td><code>/tmp/bar.txt</code></td>
* <td><code>file:///tmp/foo-symlink</code></td>
* <td><code>file:///tmp/bar.txt</code></td>
* <td>true</td>
* </tr>
* <tr>
* <td><code>C:/temp/aa.txt</code></td>
* <td><code>C:/temp/AA.txt</code></td>
* <td><code>file:///C:/temp/aa.txt</code></td>
* <td><code>file:///C:/temp/AA.txt</code></td>
* <td>true</td>
* </tr>
* <tr>
* <td><code>/tmp/bar-exists/../foo.txt</code></td>
* <td><code>/tmp/foo.txt</code></td>
* <td><code>file:///tmp/bar-exists/../foo.txt</code></td>
* <td><code>file:///tmp/foo.txt</code></td>
* <td>true</td>
* </tr>
* <tr>
* <td><code>/tmp/doesnt-exist.txt</code></td>
* <td>null (does not exist)</td>
* <td><code>file:///tmp/doesnt-exist.txt</code></td>
* <td>null (does not exist)</td>
* <td>false</td>
* </tr>
* <tr>
* <td><code>/tmp/doesnt-exist/../foo.txt</code></td>
* <td>null (intermediate does not exist)</td>
* <td><code>file:///tmp/doesnt-exist/../foo.txt</code></td>
* <td>null (intermediate does not exist)</td>
* <td>false</td>
* </tr>
* <tr>
* <td><code>/var/protected/config.xml</code></td>
* <td>null (no permissions)</td>
* <td><code>file:///var/protected/config.xml</code></td>
* <td>null (no permission)</td>
* <td>false</td>
* </tr>
* <tr>
* <td><code>/tmp/foo-symlink</code></td>
* <td>null (broken symlink, doesn't point to anything)</td>
* <td><code>file:///tmp/foo-symlink</code></td>
* <td>null (broken symlink, doesn't point to anything)</td>
* <td>false</td>
* </tr>
* <tr>
* <td><code>C:/temp/cannot:be:referenced</code></td>
* <td>null (illegal filename)</td>
* <td><code>file:///C:/temp/cannot:be:referenced</code></td>
* <td>null (illegal filename)</td>
* <td>false</td>
* </tr>
* </tbody>
* </table>
*/
private void resolveAlias()
{ {
if (!targetResolved) if (alias == null)
{
targetPath = resolveTargetPath();
targetResolved = true;
}
if (targetPath == null)
return null;
return targetPath.toUri();
}
private Path resolveTargetPath()
{
Path abs = path;
// TODO: is this a valid shortcut?
// If the path doesn't exist, then there's no alias to reference
if (!Files.exists(path))
return null;
/* Catch situation where the Path class has already normalized
* the URI eg. input path "aa./foo.txt"
* from an #resolve(String) is normalized away during
* the creation of a Path object reference.
* If the URI is different from the Path.toUri() then
* we will just use the original URI to construct the
* alias reference Path.
*
* We use the method `toUri(Path)` here, instead of
* Path.toUri() to ensure that the path contains
* a trailing slash if it's a directory, (something
* not all FileSystems seem to support)
*/
if (!URIUtil.equalsIgnoreEncodings(uri, toUri(path)))
{ {
try try
{ {
// Use normalized path to get past navigational references like "/bar/../foo/test.txt" // Default behavior is to follow symlinks.
Path ref = Paths.get(uri.normalize()); // We don't want to use the NO_FOLLOW_LINKS parameter as that takes this call from
return ref.toRealPath(); // being filesystem aware, and using FileSystem specific techniques to find
// the real file, to being done in-API (which doesn't work reliably on
// filesystems that have different names for the same file.
// eg: case-insensitive file systems, unicode name normalization,
// alternate names, etc)
// We also don't want to use Path.normalize() here as that eliminates
// the knowledge of what directories are being navigated through.
realPath = path.toRealPath();
} }
catch (IOException ioe) catch (Exception e)
{ {
// If the toRealPath() call fails, then let if (e instanceof IOException)
// the alias checking routines continue on LOG.trace("IGNORED", e);
// to other techniques. else
LOG.trace("IGNORED", ioe); LOG.warn("bad alias ({} {}) for {}", e.getClass().getName(), e.getMessage(), path);
// Not possible to serve this resource.
// - This resource doesn't exist.
// - No access rights to this resource.
// - Unable to read the file or directory.
// - Navigation segments (eg: "foo/../test.txt") would go through something that doesn't exist, or not accessible.
// - FileSystem doesn't support toRealPath.
return;
} }
}
if (!abs.isAbsolute()) /* If the path and realPath are the same, also check
abs = path.toAbsolutePath(); * The as-requested URI as it will represent what was
* URI created this PathResource.
// Any normalization difference means it's an alias, * e.g. the input of `resolve("aa./foo.txt")
// and we don't want to bother further to follow * on windows would resolve the path, but the Path.toUri() would
// symlinks as it's an alias anyway. * not always show this extension-less access.
Path normal = path.normalize(); * The as-requested URI will retain this extra '.' and be used
if (!isSameName(abs, normal)) * to evaluate if the realPath.toUri() is the same as the as-requested URI.
return normal; *
* // On Windows
try * PathResource resource = PathResource("C:/temp");
{ * PathResource child = resource.resolve("aa./foo.txt");
if (Files.isSymbolicLink(path)) * child.exists() == true
return path.getParent().resolve(Files.readSymbolicLink(path)); * child.isAlias() == true
if (Files.exists(path)) * child.toUri() == "file:///C:/temp/aa./foo.txt"
{ * child.getPath().toUri() == "file:///C:/temp/aa/foo.txt"
Path real = abs.toRealPath(); * child.getRealURI() == "file:///C:/temp/aa/foo.txt"
if (!isSameName(abs, real)) */
return real; alias = !isSameName(path, realPath) || !Objects.equals(uri, toUri(realPath));
}
} }
catch (IOException e)
{
LOG.trace("IGNORED", e);
}
catch (Exception e)
{
LOG.warn("bad alias ({} {}) for {}", e.getClass().getName(), e.getMessage(), path);
}
return null;
} }
@Override @Override
@ -373,6 +441,25 @@ public class PathResource extends Resource
return pathUri; return pathUri;
} }
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PathResource other = (PathResource)obj;
return Objects.equals(path, other.path) && Objects.equals(uri, other.uri);
}
@Override
public int hashCode()
{
return Objects.hash(path, uri);
}
@Override @Override
public String toString() public String toString()
{ {

View File

@ -297,19 +297,20 @@ public abstract class Resource implements Iterable<Resource>
*/ */
public boolean isAlias() public boolean isAlias()
{ {
return getTargetURI() != null; return false;
} }
/** /**
* If this Resource is an alias pointing to a different location, * <p>The real URI of the resource.</p>
* return the target location as URI. * <p>If this Resource is an alias, ({@link #isAlias()}), this
* URI will be different from {@link #getURI()}, and will point to the real name/location
* of the Resource.</p>
* *
* @return The target URI location of this resource, * @return The real URI location of this resource.
* or null if there is no target URI location (eg: not an alias, or a symlink)
*/ */
public URI getTargetURI() public URI getRealURI()
{ {
return null; return getURI();
} }
/** /**

View File

@ -13,7 +13,6 @@
package org.eclipse.jetty.util.ssl; package org.eclipse.jetty.util.ssl;
import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@ -48,16 +47,15 @@ public class KeyStoreScanner extends ContainerLifeCycle implements Scanner.Discr
{ {
this.sslContextFactory = sslContextFactory; this.sslContextFactory = sslContextFactory;
Resource keystoreResource = sslContextFactory.getKeyStoreResource(); Resource keystoreResource = sslContextFactory.getKeyStoreResource();
Path monitoredFile = keystoreResource.getPath(); if (!keystoreResource.exists())
if (monitoredFile == null || !Files.exists(monitoredFile))
throw new IllegalArgumentException("keystore file does not exist"); throw new IllegalArgumentException("keystore file does not exist");
if (Files.isDirectory(monitoredFile)) if (keystoreResource.isDirectory())
throw new IllegalArgumentException("expected keystore file not directory"); throw new IllegalArgumentException("expected keystore file not directory");
// Use real location of keystore (if different), so that change monitoring can work properly // Use real location of keystore (if different), so that change monitoring can work properly
URI targetURI = keystoreResource.getTargetURI(); Path monitoredFile = keystoreResource.getPath();
if (targetURI != null) if (keystoreResource.isAlias())
monitoredFile = Paths.get(targetURI); monitoredFile = Paths.get(keystoreResource.getRealURI());
keystoreFile = monitoredFile; keystoreFile = monitoredFile;
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())

View File

@ -55,13 +55,15 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue;
@ -86,32 +88,55 @@ public class FileSystemResourceTest
assertThat(FileSystemPool.INSTANCE.mounts(), empty()); assertThat(FileSystemPool.INSTANCE.mounts(), empty());
} }
private Matcher<Resource> hasNoTargetURI() private Matcher<Resource> isAlias()
{ {
return new BaseMatcher<>() return new BaseMatcher<>()
{ {
@Override @Override
public boolean matches(Object item) public boolean matches(Object item)
{ {
final Resource res = (Resource)item; return ((Resource)item).isAlias();
return res.getTargetURI() == null;
} }
@Override @Override
public void describeTo(Description description) public void describeTo(Description description)
{ {
description.appendText("getTargetURI should return null"); description.appendText("isAlias() should return true");
} }
@Override @Override
public void describeMismatch(Object item, Description description) public void describeMismatch(Object item, Description description)
{ {
description.appendText("was ").appendValue(((Resource)item).getTargetURI()); description.appendText("was ").appendValue(((Resource)item).getRealURI());
} }
}; };
} }
private Matcher<Resource> isTargetFor(final Resource resource) private Matcher<Resource> isNotAlias()
{
return new BaseMatcher<>()
{
@Override
public boolean matches(Object item)
{
return !((Resource)item).isAlias();
}
@Override
public void describeTo(Description description)
{
description.appendText("isAlias() should return false");
}
@Override
public void describeMismatch(Object item, Description description)
{
description.appendText("was ").appendValue(((Resource)item).getRealURI());
}
};
}
private Matcher<Resource> isRealResourceFor(final Resource resource)
{ {
return new BaseMatcher<>() return new BaseMatcher<>()
{ {
@ -119,27 +144,19 @@ public class FileSystemResourceTest
public boolean matches(Object item) public boolean matches(Object item)
{ {
final Resource ritem = (Resource)item; final Resource ritem = (Resource)item;
final URI alias = ritem.getTargetURI(); return ritem.getRealURI().equals(resource.getRealURI());
if (alias == null)
{
return resource.getTargetURI() == null;
}
else
{
return alias.equals(resource.getURI());
}
} }
@Override @Override
public void describeTo(Description description) public void describeTo(Description description)
{ {
description.appendText("getTargetURI should return ").appendValue(resource.getURI()); description.appendText("getRealURI should return ").appendValue(resource.getURI());
} }
@Override @Override
public void describeMismatch(Object item, Description description) public void describeMismatch(Object item, Description description)
{ {
description.appendText("was ").appendValue(((Resource)item).getTargetURI()); description.appendText("was ").appendValue(((Resource)item).getRealURI());
} }
}; };
} }
@ -596,13 +613,13 @@ public class FileSystemResourceTest
// Access to the same resource, but via a symlink means that they are not equivalent // Access to the same resource, but via a symlink means that they are not equivalent
assertThat("foo.equals(bar)", resFoo.equals(resBar), is(false)); assertThat("foo.equals(bar)", resFoo.equals(resBar), is(false));
assertThat("resource.targetURI", resFoo, hasNoTargetURI()); assertThat("resource.targetURI", resFoo, isNotAlias());
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resFoo.getURI()), hasNoTargetURI()); assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resFoo.getURI()), isNotAlias());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resFoo.getPath()), hasNoTargetURI()); assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resFoo.getPath()), isNotAlias());
assertThat("targetURI", resBar, isTargetFor(resFoo)); assertThat("targetURI", resBar, isRealResourceFor(resFoo));
assertThat("uri.targetURI", ResourceFactory.root().newResource(resBar.getURI()), isTargetFor(resFoo)); assertThat("uri.targetURI", ResourceFactory.root().newResource(resBar.getURI()), isRealResourceFor(resFoo));
assertThat("file.targetURI", ResourceFactory.root().newResource(resBar.getPath()), isTargetFor(resFoo)); assertThat("file.targetURI", ResourceFactory.root().newResource(resBar.getPath()), isRealResourceFor(resFoo));
} }
@Test @Test
@ -618,6 +635,7 @@ public class FileSystemResourceTest
try try
{ {
Files.createSymbolicLink(bar, foo); Files.createSymbolicLink(bar, foo);
// Now a "bar" symlink exists, pointing to a "foo" that doesn't exist
symlinkSupported = true; symlinkSupported = true;
} }
catch (UnsupportedOperationException | FileSystemException e) catch (UnsupportedOperationException | FileSystemException e)
@ -636,13 +654,20 @@ public class FileSystemResourceTest
// Access to the same resource, but via a symlink means that they are not equivalent // Access to the same resource, but via a symlink means that they are not equivalent
assertThat("foo.equals(bar)", resFoo.equals(resBar), is(false)); assertThat("foo.equals(bar)", resFoo.equals(resBar), is(false));
assertThat("resource.targetURI", resFoo, hasNoTargetURI()); // This is not an alias because the file does not exist.
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resFoo.getURI()), hasNoTargetURI()); assertFalse(resFoo.exists());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resFoo.getPath()), hasNoTargetURI()); assertFalse(Files.exists(resFoo.getPath()));
assertFalse(resFoo.isAlias());
assertNotNull(resFoo.getURI());
assertNull(resFoo.getRealURI());
assertThat("targetURI", resBar, isTargetFor(resFoo)); // This is alias because the target file does not exist even though the symlink file does exist.
assertThat("uri.targetURI", ResourceFactory.root().newResource(resBar.getURI()), isTargetFor(resFoo)); // This resource cannot be served, so it should not exist, nor have an alias
assertThat("file.targetURI", ResourceFactory.root().newResource(resBar.getPath()), isTargetFor(resFoo)); assertFalse(resBar.exists());
assertFalse(Files.exists(resBar.getPath()));
assertFalse(resBar.isAlias());
assertNotNull(resBar.getURI());
assertNull(resBar.getRealURI());
} }
@Test @Test
@ -657,9 +682,9 @@ public class FileSystemResourceTest
// Reference to actual resource that exists // Reference to actual resource that exists
Resource resource = base.resolve("file"); Resource resource = base.resolve("file");
assertThat("resource.targetURI", resource, hasNoTargetURI()); assertThat("resource.targetURI", resource, isNotAlias());
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), hasNoTargetURI()); assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), isNotAlias());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), hasNoTargetURI()); assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), isNotAlias());
// On some case-insensitive file systems, lets see if an alternate // On some case-insensitive file systems, lets see if an alternate
// case for the filename results in an alias reference // case for the filename results in an alias reference
@ -667,9 +692,9 @@ public class FileSystemResourceTest
if (alias.exists()) if (alias.exists())
{ {
// If it exists, it must be an alias // If it exists, it must be an alias
assertThat("targetURI", alias, isTargetFor(resource)); assertThat("targetURI", alias, isRealResourceFor(resource));
assertThat("targetURI.uri", ResourceFactory.root().newResource(alias.getURI()), isTargetFor(resource)); assertThat("targetURI.uri", ResourceFactory.root().newResource(alias.getURI()), isRealResourceFor(resource));
assertThat("targetURI.file", ResourceFactory.root().newResource(alias.getPath()), isTargetFor(resource)); assertThat("targetURI.file", ResourceFactory.root().newResource(alias.getPath()), isRealResourceFor(resource));
} }
} }
@ -695,9 +720,9 @@ public class FileSystemResourceTest
// Long filename // Long filename
Resource resource = base.resolve("TextFile.Long.txt"); Resource resource = base.resolve("TextFile.Long.txt");
assertThat("resource.targetURI", resource, hasNoTargetURI()); assertThat("resource.targetURI", resource, isNotAlias());
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), hasNoTargetURI()); assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), isNotAlias());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), hasNoTargetURI()); assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), isNotAlias());
// On some versions of Windows, the long filename can be referenced // On some versions of Windows, the long filename can be referenced
// via a short 8.3 equivalent filename. // via a short 8.3 equivalent filename.
@ -705,9 +730,9 @@ public class FileSystemResourceTest
if (alias.exists()) if (alias.exists())
{ {
// If it exists, it must be an alias // If it exists, it must be an alias
assertThat("targetURI", alias, isTargetFor(resource)); assertThat("targetURI", alias, isRealResourceFor(resource));
assertThat("targetURI.uri", ResourceFactory.root().newResource(alias.getURI()), isTargetFor(resource)); assertThat("targetURI.uri", ResourceFactory.root().newResource(alias.getURI()), isRealResourceFor(resource));
assertThat("targetURI.file", ResourceFactory.root().newResource(alias.getPath()), isTargetFor(resource)); assertThat("targetURI.file", ResourceFactory.root().newResource(alias.getPath()), isRealResourceFor(resource));
} }
} }
@ -731,9 +756,9 @@ public class FileSystemResourceTest
Resource base = ResourceFactory.root().newResource(dir); Resource base = ResourceFactory.root().newResource(dir);
Resource resource = base.resolve("testfile"); Resource resource = base.resolve("testfile");
assertThat("resource.targetURI", resource, hasNoTargetURI()); assertThat("resource.targetURI", resource, isNotAlias());
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), hasNoTargetURI()); assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), isNotAlias());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), hasNoTargetURI()); assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), isNotAlias());
try try
{ {
@ -742,9 +767,9 @@ public class FileSystemResourceTest
if (alias.exists()) if (alias.exists())
{ {
// If it exists, it must be an alias // If it exists, it must be an alias
assertThat("resource.targetURI", alias, isTargetFor(resource)); assertThat("resource.targetURI", alias, isRealResourceFor(resource));
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(alias.getURI()), isTargetFor(resource)); assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(alias.getURI()), isRealResourceFor(resource));
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(alias.getPath()), isTargetFor(resource)); assertThat("resource.file.targetURI", ResourceFactory.root().newResource(alias.getPath()), isRealResourceFor(resource));
} }
} }
catch (InvalidPathException e) catch (InvalidPathException e)
@ -773,9 +798,9 @@ public class FileSystemResourceTest
Resource base = ResourceFactory.root().newResource(dir); Resource base = ResourceFactory.root().newResource(dir);
Resource resource = base.resolve("testfile"); Resource resource = base.resolve("testfile");
assertThat("resource.targetURI", resource, hasNoTargetURI()); assertThat("resource.targetURI", resource, isNotAlias());
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), hasNoTargetURI()); assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), isNotAlias());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), hasNoTargetURI()); assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), isNotAlias());
try try
{ {
@ -786,9 +811,9 @@ public class FileSystemResourceTest
assumeTrue(alias.getURI().getScheme().equals("file")); assumeTrue(alias.getURI().getScheme().equals("file"));
// If it exists, it must be an alias // If it exists, it must be an alias
assertThat("resource.targetURI", alias, isTargetFor(resource)); assertThat("resource.targetURI", alias, isRealResourceFor(resource));
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(alias.getURI()), isTargetFor(resource)); assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(alias.getURI()), isRealResourceFor(resource));
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(alias.getPath()), isTargetFor(resource)); assertThat("resource.file.targetURI", ResourceFactory.root().newResource(alias.getPath()), isRealResourceFor(resource));
} }
} }
catch (InvalidPathException e) catch (InvalidPathException e)
@ -817,9 +842,9 @@ public class FileSystemResourceTest
Resource base = ResourceFactory.root().newResource(dir); Resource base = ResourceFactory.root().newResource(dir);
Resource resource = base.resolve("testfile"); Resource resource = base.resolve("testfile");
assertThat("resource.targetURI", resource, hasNoTargetURI()); assertThat("resource.targetURI", resource, isNotAlias());
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), hasNoTargetURI()); assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), isNotAlias());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), hasNoTargetURI()); assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), isNotAlias());
try try
{ {
@ -828,9 +853,9 @@ public class FileSystemResourceTest
if (alias.exists()) if (alias.exists())
{ {
// If it exists, it must be an alias // If it exists, it must be an alias
assertThat("resource.targetURI", alias, isTargetFor(resource)); assertThat("resource.targetURI", alias, isRealResourceFor(resource));
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(alias.getURI()), isTargetFor(resource)); assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(alias.getURI()), isRealResourceFor(resource));
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(alias.getPath()), isTargetFor(resource)); assertThat("resource.file.targetURI", ResourceFactory.root().newResource(alias.getPath()), isRealResourceFor(resource));
} }
} }
catch (InvalidPathException e) catch (InvalidPathException e)
@ -857,7 +882,7 @@ public class FileSystemResourceTest
Resource base = ResourceFactory.root().newResource(dir); Resource base = ResourceFactory.root().newResource(dir);
Resource res = base.resolve("foo;"); Resource res = base.resolve("foo;");
assertThat("Target URI: " + res, res, hasNoTargetURI()); assertThat("Target URI: " + res, res, isNotAlias());
} }
@Test @Test
@ -1052,25 +1077,25 @@ public class FileSystemResourceTest
Resource fileres = ResourceFactory.root().newResource(refQuoted); Resource fileres = ResourceFactory.root().newResource(refQuoted);
assertThat("Exists: " + refQuoted, fileres.exists(), is(true)); assertThat("Exists: " + refQuoted, fileres.exists(), is(true));
assertThat("Target URI: " + refQuoted, fileres, hasNoTargetURI()); assertThat("Is Not Alias: " + refQuoted, fileres, isNotAlias());
URI refEncoded = dir.toUri().resolve("foo%27s.txt"); URI refEncoded = dir.toUri().resolve("foo%27s.txt");
fileres = ResourceFactory.root().newResource(refEncoded); fileres = ResourceFactory.root().newResource(refEncoded);
assertThat("Exists: " + refEncoded, fileres.exists(), is(true)); assertThat("Exists: " + refEncoded, fileres.exists(), is(true));
assertThat("Target URI: " + refEncoded, fileres, hasNoTargetURI()); assertThat("Is Alias: " + refEncoded, fileres, isAlias());
URI refQuoteSpace = dir.toUri().resolve("f%20o's.txt"); URI refQuoteSpace = dir.toUri().resolve("f%20o's.txt");
fileres = ResourceFactory.root().newResource(refQuoteSpace); fileres = ResourceFactory.root().newResource(refQuoteSpace);
assertThat("Exists: " + refQuoteSpace, fileres.exists(), is(true)); assertThat("Exists: " + refQuoteSpace, fileres.exists(), is(true));
assertThat("Target URI: " + refQuoteSpace, fileres, hasNoTargetURI()); assertThat("Is Not Alias: " + refQuoteSpace, fileres, isNotAlias());
URI refEncodedSpace = dir.toUri().resolve("f%20o%27s.txt"); URI refEncodedSpace = dir.toUri().resolve("f%20o%27s.txt");
fileres = ResourceFactory.root().newResource(refEncodedSpace); fileres = ResourceFactory.root().newResource(refEncodedSpace);
assertThat("Exists: " + refEncodedSpace, fileres.exists(), is(true)); assertThat("Exists: " + refEncodedSpace, fileres.exists(), is(true));
assertThat("Target URI: " + refEncodedSpace, fileres, hasNoTargetURI()); assertThat("Is Alias: " + refEncodedSpace, fileres, isAlias());
URI refA = dir.toUri().resolve("foo's.txt"); URI refA = dir.toUri().resolve("foo's.txt");
URI refB = dir.toUri().resolve("foo%27s.txt"); URI refB = dir.toUri().resolve("foo%27s.txt");
@ -1081,10 +1106,14 @@ public class FileSystemResourceTest
"URI[b] = " + refB; "URI[b] = " + refB;
assertThat(msg, refA.equals(refB), is(false)); assertThat(msg, refA.equals(refB), is(false));
// now show that Resource.equals() does work // These resources are not equal because they have different request URIs.
Resource a = ResourceFactory.root().newResource(refA); Resource a = ResourceFactory.root().newResource(refA);
Resource b = ResourceFactory.root().newResource(refB); Resource b = ResourceFactory.root().newResource(refB);
assertThat("A.equals(B)", a.equals(b), is(true)); assertThat("A.equals(B)", a.equals(b), is(false));
assertThat(a.getPath(), equalTo(b.getPath()));
assertThat(a.getRealURI(), equalTo(b.getRealURI()));
assertFalse(a.isAlias());
assertTrue(b.isAlias());
} }
@Test @Test
@ -1156,7 +1185,7 @@ public class FileSystemResourceTest
try try
{ {
assertThat("Exists: " + basePath, base.exists(), is(true)); assertThat("Exists: " + basePath, base.exists(), is(true));
assertThat("Target URI: " + basePath, base, hasNoTargetURI()); assertThat("Target URI: " + basePath, base, isNotAlias());
Resource r = base.resolve("aa%5C/foo.txt"); Resource r = base.resolve("aa%5C/foo.txt");
assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa%5C/foo.txt")); assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa%5C/foo.txt"));
@ -1165,8 +1194,8 @@ public class FileSystemResourceTest
{ {
assertThat("getPath().toString()", r.getPath().toString(), containsString("aa\\foo.txt")); assertThat("getPath().toString()", r.getPath().toString(), containsString("aa\\foo.txt"));
assertThat("isAlias()", r.isAlias(), is(true)); assertThat("isAlias()", r.isAlias(), is(true));
assertThat("getTargetURI()", r.getTargetURI(), notNullValue()); assertThat("getRealURI()", r.getRealURI(), notNullValue());
assertThat("getTargetURI()", r.getTargetURI().toASCIIString(), containsString("aa/foo.txt")); assertThat("getRealURI()", r.getRealURI().toASCIIString(), containsString("aa/foo.txt"));
assertThat("Exists: " + r, r.exists(), is(true)); assertThat("Exists: " + r, r.exists(), is(true));
} }
else else
@ -1200,7 +1229,7 @@ public class FileSystemResourceTest
try try
{ {
assertThat("Exists: " + basePath, base.exists(), is(true)); assertThat("Exists: " + basePath, base.exists(), is(true));
assertThat("Target URI: " + basePath, base, hasNoTargetURI()); assertThat("Is Not Alias: " + basePath, base, isNotAlias());
Resource r = base.resolve("aa./foo.txt"); Resource r = base.resolve("aa./foo.txt");
assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa./foo.txt")); assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa./foo.txt"));
@ -1208,8 +1237,8 @@ public class FileSystemResourceTest
if (WINDOWS.isCurrentOs()) if (WINDOWS.isCurrentOs())
{ {
assertThat("isAlias()", r.isAlias(), is(true)); assertThat("isAlias()", r.isAlias(), is(true));
assertThat("getTargetURI()", r.getTargetURI(), notNullValue()); assertThat("getRealURI()", r.getRealURI(), notNullValue());
assertThat("getTargetURI()", r.getTargetURI().toASCIIString(), containsString("aa/foo.txt")); assertThat("getRealURI()", r.getRealURI().toASCIIString(), containsString("aa/foo.txt"));
assertThat("Exists: " + r, r.exists(), is(true)); assertThat("Exists: " + r, r.exists(), is(true));
} }
else else
@ -1240,7 +1269,7 @@ public class FileSystemResourceTest
try try
{ {
assertThat("Exists: " + basePath, base.exists(), is(true)); assertThat("Exists: " + basePath, base.exists(), is(true));
assertThat("Target URI: " + basePath, base, hasNoTargetURI()); assertThat("Is Not Alias: " + basePath, base, isNotAlias());
Resource r = base.resolve("/foo.txt"); Resource r = base.resolve("/foo.txt");
assertThat("getURI()", r.getURI().toASCIIString(), containsString("/foo.txt")); assertThat("getURI()", r.getURI().toASCIIString(), containsString("/foo.txt"));
@ -1270,13 +1299,13 @@ public class FileSystemResourceTest
try try
{ {
assertThat("Exists: " + basePath, base.exists(), is(true)); assertThat("Exists: " + basePath, base.exists(), is(true));
assertThat("Target URI: " + basePath, base, hasNoTargetURI()); assertThat("Is Not Alias: " + basePath, base, isNotAlias());
Resource r = base.resolve("//foo.txt"); Resource r = base.resolve("//foo.txt");
assertThat("getURI()", r.getURI().toASCIIString(), containsString("/foo.txt")); assertThat("getURI()", r.getURI().toASCIIString(), containsString("/foo.txt"));
assertThat("isAlias()", r.isAlias(), is(false)); assertThat("isAlias()", r.isAlias(), is(false));
assertThat("getTargetURI()", r.getTargetURI(), nullValue()); assertThat("Is Not Alias: " + r.getPath(), r, isNotAlias());
} }
catch (IllegalArgumentException e) catch (IllegalArgumentException e)
{ {
@ -1302,13 +1331,13 @@ public class FileSystemResourceTest
try try
{ {
assertThat("Exists: " + basePath, base.exists(), is(true)); assertThat("Exists: " + basePath, base.exists(), is(true));
assertThat("Target URI: " + basePath, base, hasNoTargetURI()); assertThat("Is Not Alias: " + basePath, base, isNotAlias());
Resource r = base.resolve("aa//foo.txt"); Resource r = base.resolve("aa//foo.txt");
assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa/foo.txt")); assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa/foo.txt"));
assertThat("isAlias()", r.isAlias(), is(false)); assertThat("isAlias()", r.isAlias(), is(false));
assertThat("getTargetURI()", r.getTargetURI(), nullValue()); assertThat("Is Not Alias: " + r.getPath(), r, isNotAlias());
} }
catch (IllegalArgumentException e) catch (IllegalArgumentException e)
{ {
@ -1356,11 +1385,11 @@ public class FileSystemResourceTest
Resource base = ResourceFactory.root().newResource(utf8Dir); Resource base = ResourceFactory.root().newResource(utf8Dir);
assertThat("Exists: " + utf8Dir, base.exists(), is(true)); assertThat("Exists: " + utf8Dir, base.exists(), is(true));
assertThat("Target URI: " + utf8Dir, base, hasNoTargetURI()); assertThat("Is Not Alias: " + utf8Dir, base, isNotAlias());
Resource r = base.resolve("file.txt"); Resource r = base.resolve("file.txt");
assertThat("Exists: " + r, r.exists(), is(true)); assertThat("Exists: " + r, r.exists(), is(true));
assertThat("Target URI: " + r, r, hasNoTargetURI()); assertThat("Is Not Alias: " + r, r, isNotAlias());
} }
@Test @Test
@ -1371,7 +1400,7 @@ public class FileSystemResourceTest
Resource resource = base.resolve("WEB-INF/"); Resource resource = base.resolve("WEB-INF/");
assertThat("getURI()", resource.getURI().toASCIIString(), containsString("path/WEB-INF/")); assertThat("getURI()", resource.getURI().toASCIIString(), containsString("path/WEB-INF/"));
assertThat("isAlias()", resource.isAlias(), is(false)); assertThat("isAlias()", resource.isAlias(), is(false));
assertThat("getTargetURI()", resource.getTargetURI(), nullValue()); assertThat("Is Not Alias: " + resource.getPath(), resource, isNotAlias());
} }
private String toString(Resource resource) throws IOException private String toString(Resource resource) throws IOException

View File

@ -29,6 +29,7 @@ import java.util.Map;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.URIUtil;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -38,6 +39,7 @@ import org.slf4j.LoggerFactory;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
@ -303,37 +305,37 @@ public class PathResourceTest
// Test not alias paths // Test not alias paths
Resource resource = resourceFactory.newResource(file); Resource resource = resourceFactory.newResource(file);
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNull(resource.getTargetURI()); assertFalse(resource.isAlias());
resource = resourceFactory.newResource(file.toAbsolutePath()); resource = resourceFactory.newResource(file.toAbsolutePath());
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNull(resource.getTargetURI()); assertFalse(resource.isAlias());
resource = resourceFactory.newResource(file.toUri()); resource = resourceFactory.newResource(file.toUri());
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNull(resource.getTargetURI()); assertFalse(resource.isAlias());
resource = resourceFactory.newResource(file.toUri().toString()); resource = resourceFactory.newResource(file.toUri().toString());
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNull(resource.getTargetURI()); assertFalse(resource.isAlias());
resource = archiveResource.resolve("test.txt"); resource = archiveResource.resolve("test.txt");
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNull(resource.getTargetURI()); assertFalse(resource.isAlias());
// Test alias paths // Test alias paths
resource = resourceFactory.newResource(file0); resource = resourceFactory.newResource(file0);
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNotNull(resource.getTargetURI()); assertTrue(resource.isAlias());
resource = resourceFactory.newResource(file0.toAbsolutePath()); resource = resourceFactory.newResource(file0.toAbsolutePath());
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNotNull(resource.getTargetURI()); assertTrue(resource.isAlias());
resource = resourceFactory.newResource(file0.toUri()); resource = resourceFactory.newResource(file0.toUri());
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNotNull(resource.getTargetURI()); assertTrue(resource.isAlias());
resource = resourceFactory.newResource(file0.toUri().toString()); resource = resourceFactory.newResource(file0.toUri().toString());
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNotNull(resource.getTargetURI()); assertTrue(resource.isAlias());
resource = archiveResource.resolve("test.txt\0"); resource = archiveResource.resolve("test.txt\0");
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNotNull(resource.getTargetURI()); assertTrue(resource.isAlias());
} }
catch (InvalidPathException e) catch (InvalidPathException e)
{ {
@ -402,9 +404,9 @@ public class PathResourceTest
assertThat("resource.uri.alias", resourceFactory.newResource(resFoo.getURI()).isAlias(), is(false)); assertThat("resource.uri.alias", resourceFactory.newResource(resFoo.getURI()).isAlias(), is(false));
assertThat("resource.file.alias", resourceFactory.newResource(resFoo.getPath()).isAlias(), is(false)); assertThat("resource.file.alias", resourceFactory.newResource(resFoo.getPath()).isAlias(), is(false));
assertThat("targetURI", resBar.getTargetURI(), is(resFoo.getURI())); assertThat("targetURI", resBar.getRealURI(), is(resFoo.getURI()));
assertThat("uri.targetURI", resourceFactory.newResource(resBar.getURI()).getTargetURI(), is(resFoo.getURI())); assertThat("uri.targetURI", resourceFactory.newResource(resBar.getURI()).getRealURI(), is(resFoo.getURI()));
assertThat("file.targetURI", resourceFactory.newResource(resBar.getPath()).getTargetURI(), is(resFoo.getURI())); assertThat("file.targetURI", resourceFactory.newResource(resBar.getPath()).getRealURI(), is(resFoo.getURI()));
} }
catch (InvalidPathException e) catch (InvalidPathException e)
{ {
@ -413,6 +415,43 @@ public class PathResourceTest
} }
} }
@Test
public void testBrokenSymlink(WorkDir workDir) throws Exception
{
Path testDir = workDir.getEmptyPathDir();
Path resourcePath = testDir.resolve("resource.txt");
IO.copy(MavenTestingUtils.getTestResourcePathFile("resource.txt").toFile(), resourcePath.toFile());
Path symlinkPath = Files.createSymbolicLink(testDir.resolve("symlink.txt"), resourcePath);
PathResource fileResource = new PathResource(resourcePath);
assertTrue(fileResource.exists());
PathResource symlinkResource = new PathResource(symlinkPath);
assertTrue(symlinkResource.exists());
// Their paths are not equal but not their canonical paths are.
assertThat(fileResource.getPath(), not(equalTo(symlinkResource.getPath())));
assertThat(fileResource.getPath(), equalTo(symlinkResource.getRealPath()));
assertFalse(fileResource.isAlias());
assertTrue(symlinkResource.isAlias());
assertTrue(fileResource.exists());
assertTrue(symlinkResource.exists());
// After deleting file the Resources do not exist even though symlink file exists.
assumeTrue(Files.deleteIfExists(resourcePath));
assertFalse(fileResource.exists());
assertFalse(symlinkResource.exists());
// Re-create and test the resources now that the file has been deleted.
fileResource = new PathResource(resourcePath);
assertFalse(fileResource.exists());
assertNull(fileResource.getRealPath());
assertTrue(symlinkResource.isAlias());
symlinkResource = new PathResource(symlinkPath);
assertFalse(symlinkResource.exists());
assertNull(symlinkResource.getRealPath());
assertFalse(symlinkResource.isAlias());
}
@Test @Test
public void testResolveNavigation(WorkDir workDir) throws Exception public void testResolveNavigation(WorkDir workDir) throws Exception
{ {
@ -421,15 +460,22 @@ public class PathResourceTest
Path dir = docroot.resolve("dir"); Path dir = docroot.resolve("dir");
Files.createDirectory(dir); Files.createDirectory(dir);
Path foo = docroot.resolve("foo");
Files.createDirectory(foo);
Path testText = dir.resolve("test.txt"); Path testText = dir.resolve("test.txt");
Files.createFile(testText); Files.createFile(testText);
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable()) try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{ {
Resource rootRes = resourceFactory.newResource(docroot); Resource rootRes = resourceFactory.newResource(docroot);
// This is the heart of the test, we should support this // Test navigation through a directory that doesn't exist
Resource fileRes = rootRes.resolve("bar/../dir/test.txt"); Resource fileResViaBar = rootRes.resolve("bar/../dir/test.txt");
assertTrue(fileRes.exists()); assertFalse(fileResViaBar.exists());
// Test navigation through a directory that does exist
Resource fileResViaFoo = rootRes.resolve("foo/../dir/test.txt");
assertTrue(fileResViaFoo.exists());
} }
} }

View File

@ -33,8 +33,6 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(WorkDirExtension.class) @ExtendWith(WorkDirExtension.class)
@ -62,16 +60,34 @@ public class ResourceAliasTest
@Test @Test
public void testAliasNavigation() throws IOException public void testAliasNavigation() throws IOException
{ {
Path baseDir = workDir.getEmptyPathDir(); Path docroot = workDir.getEmptyPathDir();
Path foo = baseDir.resolve("foo"); Path dir = docroot.resolve("dir");
Files.createDirectories(foo); Files.createDirectory(dir);
Files.writeString(foo.resolve("test.txt"), "Contents of test.txt", StandardCharsets.UTF_8);
Resource resource = ResourceFactory.root().newResource(baseDir); Path foo = docroot.resolve("foo");
Resource test = resource.resolve("/bar/../foo/test.txt"); Files.createDirectory(foo);
assertTrue(test.exists(), "Should exist");
assertTrue(test.isAlias(), "Should be an alias"); Path testText = dir.resolve("test.txt");
Files.writeString(testText, "Contents of test.txt", StandardCharsets.UTF_8);
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource rootRes = resourceFactory.newResource(docroot);
// Test navigation through a directory that doesn't exist
Resource fileResViaBar = rootRes.resolve("bar/../dir/test.txt");
assertFalse(fileResViaBar.exists(), "Should not exist");
assertFalse(fileResViaBar.isAlias(), "Should not be an alias");
Files.createDirectory(docroot.resolve("bar"));
assertTrue(fileResViaBar.exists(), "Should exist");
assertTrue(fileResViaBar.isAlias(), "Should be an alias");
// Test navigation through a directory that does exist
Resource fileResViaFoo = rootRes.resolve("foo/../dir/test.txt");
assertTrue(fileResViaFoo.exists(), "Should exist");
assertTrue(fileResViaFoo.isAlias(), "Should be an alias");
}
} }
@Test @Test
@ -132,37 +148,37 @@ public class ResourceAliasTest
// Test not alias paths // Test not alias paths
Resource resource = resourceFactory.newResource(file); Resource resource = resourceFactory.newResource(file);
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNull(resource.getTargetURI()); assertFalse(resource.isAlias());
resource = resourceFactory.newResource(file.toAbsolutePath()); resource = resourceFactory.newResource(file.toAbsolutePath());
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNull(resource.getTargetURI()); assertFalse(resource.isAlias());
resource = resourceFactory.newResource(file.toUri()); resource = resourceFactory.newResource(file.toUri());
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNull(resource.getTargetURI()); assertFalse(resource.isAlias());
resource = resourceFactory.newResource(file.toUri().toString()); resource = resourceFactory.newResource(file.toUri().toString());
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNull(resource.getTargetURI()); assertFalse(resource.isAlias());
resource = dir.resolve("test.txt"); resource = dir.resolve("test.txt");
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNull(resource.getTargetURI()); assertFalse(resource.isAlias());
// Test alias paths // Test alias paths
resource = resourceFactory.newResource(file0); resource = resourceFactory.newResource(file0);
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNotNull(resource.getTargetURI()); assertTrue(resource.isAlias());
resource = resourceFactory.newResource(file0.toAbsolutePath()); resource = resourceFactory.newResource(file0.toAbsolutePath());
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNotNull(resource.getTargetURI()); assertTrue(resource.isAlias());
resource = resourceFactory.newResource(file0.toUri()); resource = resourceFactory.newResource(file0.toUri());
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNotNull(resource.getTargetURI()); assertTrue(resource.isAlias());
resource = resourceFactory.newResource(file0.toUri().toString()); resource = resourceFactory.newResource(file0.toUri().toString());
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNotNull(resource.getTargetURI()); assertTrue(resource.isAlias());
resource = dir.resolve("test.txt\0"); resource = dir.resolve("test.txt\0");
assertTrue(resource.exists()); assertTrue(resource.exists());
assertNotNull(resource.getTargetURI()); assertTrue(resource.isAlias());
} }
catch (InvalidPathException e) catch (InvalidPathException e)
{ {

View File

@ -380,7 +380,7 @@ public class ResourceTest
assertNotNull(dot); assertNotNull(dot);
assertTrue(dot.exists()); assertTrue(dot.exists());
assertTrue(dot.isAlias(), "Reference to '.' is an alias to itself"); assertTrue(dot.isAlias(), "Reference to '.' is an alias to itself");
assertTrue(Files.isSameFile(dot.getPath(), Paths.get(dot.getTargetURI()))); assertTrue(Files.isSameFile(dot.getPath(), Paths.get(dot.getRealURI())));
} }
@Test @Test
@ -402,12 +402,13 @@ public class ResourceTest
FS.ensureDirExists(dir); FS.ensureDirExists(dir);
Path file = dir.resolve("bar.txt"); Path file = dir.resolve("bar.txt");
FS.touch(file); FS.touch(file);
assertTrue(Files.exists(file));
Resource resource = resourceFactory.newResource(file); Resource resource = resourceFactory.newResource(file);
Resource dot = resource.resolve("."); Resource dot = resource.resolve(".");
// We are now pointing to a resource at ".../testDotAliasFileExists/foo/bar.txt/."
assertNotNull(dot); assertNotNull(dot);
assertTrue(dot.exists()); assertFalse(dot.exists());
assertTrue(dot.isAlias(), "Reference to '.' is an alias to itself"); assertFalse(dot.isAlias(), "Reference to '.' against a file is not an alias");
assertTrue(Files.isSameFile(dot.getPath(), Paths.get(dot.getTargetURI())));
} }
@Test @Test
@ -420,9 +421,10 @@ public class ResourceTest
assertFalse(Files.exists(file)); assertFalse(Files.exists(file));
Resource resource = resourceFactory.newResource(file); Resource resource = resourceFactory.newResource(file);
Resource dot = resource.resolve("."); Resource dot = resource.resolve(".");
// We are now pointing to a resource at ".../testDotAliasFileDoesNotExists/foo/bar.txt/."
assertNotNull(dot); assertNotNull(dot);
assertFalse(dot.exists()); assertFalse(dot.exists());
assertFalse(dot.isAlias(), "Reference to '.' is not an alias as file doesn't exist"); assertFalse(dot.isAlias(), "Reference to '.' against a file is not an alias (the file also does not exist)");
} }
@Test @Test

View File

@ -74,7 +74,6 @@ import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler;
import org.eclipse.jetty.ee10.servlet.security.SecurityHandler; import org.eclipse.jetty.ee10.servlet.security.SecurityHandler;
import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.http.pathmap.MatchedResource; import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
@ -1081,7 +1080,7 @@ public class ServletContextHandler extends ContextHandler implements Graceful
Resource baseResource = getBaseResource(); Resource baseResource = getBaseResource();
if (baseResource != null && baseResource.isAlias()) if (baseResource != null && baseResource.isAlias())
LOG.warn("BaseResource {} is aliased to {} in {}. May not be supported in future releases.", LOG.warn("BaseResource {} is aliased to {} in {}. May not be supported in future releases.",
baseResource, baseResource.getTargetURI(), this); baseResource, baseResource.getRealURI(), this);
if (_logger == null) if (_logger == null)
_logger = LoggerFactory.getLogger(ContextHandler.class.getName() + getLogNameSuffix()); _logger = LoggerFactory.getLogger(ContextHandler.class.getName() + getLogNameSuffix());

View File

@ -603,6 +603,7 @@ public class MetaInfConfiguration extends AbstractConfiguration
* @return the list of tlds found * @return the list of tlds found
* @throws IOException if unable to scan the directory * @throws IOException if unable to scan the directory
*/ */
// TODO: Needs to use resource.
public Collection<URL> getTlds(Path dir) throws IOException public Collection<URL> getTlds(Path dir) throws IOException
{ {
if (dir == null || !Files.isDirectory(dir)) if (dir == null || !Files.isDirectory(dir))

View File

@ -292,14 +292,14 @@ public class WebInfConfiguration extends AbstractConfiguration
throw new IllegalStateException("No resourceBase or war set for context"); throw new IllegalStateException("No resourceBase or war set for context");
// Use real location (if different) for WAR file, so that change/modification monitoring can work. // Use real location (if different) for WAR file, so that change/modification monitoring can work.
URI targetURI = webApp.getTargetURI(); if (webApp.isAlias())
if (targetURI != null)
{ {
URI realURI = webApp.getRealURI();
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("{} anti-aliased to {}", webApp, targetURI); LOG.debug("{} anti-aliased to {}", webApp, realURI);
Resource targetWebApp = context.newResource(targetURI); Resource realWebApp = context.newResource(realURI);
if (targetWebApp != null && targetWebApp.exists()) if (realWebApp != null && realWebApp.exists())
webApp = targetWebApp; webApp = realWebApp;
} }
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())

View File

@ -231,7 +231,7 @@ public class CachedContentFactory implements HttpContent.ContentFactory
{ {
compressedContent = null; compressedContent = null;
Resource compressedResource = _factory.newResource(compressedPathInContext); Resource compressedResource = _factory.newResource(compressedPathInContext);
if (compressedResource.exists() && ResourceContentFactory.newerThanOrEqual(compressedResource, resource) && if (compressedResource != null && compressedResource.exists() && ResourceContentFactory.newerThanOrEqual(compressedResource, resource) &&
compressedResource.length() < resource.length()) compressedResource.length() < resource.length())
{ {
compressedContent = new CachedHttpContent(compressedPathInContext, compressedResource, null); compressedContent = new CachedHttpContent(compressedPathInContext, compressedResource, null);

View File

@ -1383,11 +1383,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
// addPath with accept non-canonical paths that don't go above the root, // addPath with accept non-canonical paths that don't go above the root,
// but will treat them as aliases. So unless allowed by an AliasChecker // but will treat them as aliases. So unless allowed by an AliasChecker
// they will be rejected below. // they will be rejected below.
Resource resource = baseResource.resolve(pathInContext); return baseResource.resolve(pathInContext);
if (checkAlias(pathInContext, resource))
return resource;
return null;
} }
catch (Exception e) catch (Exception e)
{ {
@ -1408,7 +1404,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
if (resource.isAlias()) if (resource.isAlias())
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Alias resource {} for {}", resource, resource.getTargetURI()); LOG.debug("Alias resource {} for {}", resource, resource.getRealURI());
// alias checks // alias checks
for (AliasCheck check : getAliasChecks()) for (AliasCheck check : getAliasChecks())

View File

@ -253,6 +253,15 @@ public class ResourceService
return response.isCommitted(); return response.isCommitted();
} }
ContextHandler contextHandler = ContextHandler.getContextHandler(request.getServletContext());
if (contextHandler != null && !contextHandler.checkAlias(pathInContext, content.getResource()))
{
if (included)
throw new FileNotFoundException("!" + pathInContext);
notFound(request, response);
return response.isCommitted();
}
// Directory? // Directory?
if (content.getResource().isDirectory()) if (content.getResource().isDirectory())
{ {
@ -289,6 +298,10 @@ public class ResourceService
HttpContent precompressedContent = precompressedContents.get(precompressedContentEncoding); HttpContent precompressedContent = precompressedContents.get(precompressedContentEncoding);
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("precompressed={}", precompressedContent); LOG.debug("precompressed={}", precompressedContent);
if (contextHandler != null && !contextHandler.checkAlias(pathInContext, precompressedContent.getResource()))
content = null;
content = precompressedContent; content = precompressedContent;
response.setHeader(HttpHeader.CONTENT_ENCODING.asString(), precompressedContentEncoding.getEncoding()); response.setHeader(HttpHeader.CONTENT_ENCODING.asString(), precompressedContentEncoding.getEncoding());
} }

View File

@ -457,8 +457,6 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory, Welc
if (_baseResource != null) if (_baseResource != null)
{ {
r = _baseResource.resolve(subUriPath); r = _baseResource.resolve(subUriPath);
if (!_contextHandler.checkAlias(subUriPath, r))
r = null;
} }
else if (_servletContext instanceof ContextHandler.APIContext) else if (_servletContext instanceof ContextHandler.APIContext)
{ {

View File

@ -290,12 +290,14 @@ public class WebInfConfiguration extends AbstractConfiguration
throw new IllegalStateException("No resourceBase or war set for context"); throw new IllegalStateException("No resourceBase or war set for context");
// Use real location (if different) for WAR file, so that change/modification monitoring can work. // Use real location (if different) for WAR file, so that change/modification monitoring can work.
URI targetURI = webApp.getTargetURI(); if (webApp.isAlias())
if (targetURI != null)
{ {
URI realURI = webApp.getRealURI();
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("{} anti-aliased to {}", webApp, targetURI); LOG.debug("{} anti-aliased to {}", webApp, realURI);
webApp = context.newResource(targetURI); Resource realWebApp = context.newResource(realURI);
if (realWebApp != null && realWebApp.exists())
webApp = realWebApp;
} }
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())