diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AllowedResourceAliasChecker.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AllowedResourceAliasChecker.java index da8c2785347..50d00d0ba77 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AllowedResourceAliasChecker.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AllowedResourceAliasChecker.java @@ -27,6 +27,7 @@ import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.Resources; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -158,6 +159,11 @@ public class AllowedResourceAliasChecker extends AbstractLifeCycle implements Al protected boolean check(String pathInContext, Resource resource) { + // If there is a single Path available, check it + Path path = resource.getPath(); + if (path != null && Files.exists(path)) + return check(pathInContext, path); + // Allow any aliases (symlinks, 8.3, casing, etc.) so long as // the resulting real file is allowed. for (Resource r : resource) @@ -186,7 +192,7 @@ public class AllowedResourceAliasChecker extends AbstractLifeCycle implements Al for (String protectedTarget : _protected) { Resource p = _baseResource.resolve(protectedTarget); - if (p == null) + if (Resources.missing(p)) continue; for (Resource r : p) { diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SymlinkAllowedResourceAliasChecker.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SymlinkAllowedResourceAliasChecker.java index 6460a0e766f..f629dc41beb 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SymlinkAllowedResourceAliasChecker.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SymlinkAllowedResourceAliasChecker.java @@ -67,10 +67,11 @@ public class SymlinkAllowedResourceAliasChecker extends AllowedResourceAliasChec // Add the segment to the path and realURI. segmentPath.append("/").append(segment); Resource fromBase = _baseResource.resolve(segmentPath.toString()); - for (Resource r : fromBase) - { - Path p = r.getPath(); + // If there is a single path, check it + Path p = fromBase.getPath(); + if (p != null) + { // If the ancestor of the alias is a symlink, then check if the real URI is protected, otherwise allow. // This allows symlinks like /other->/WEB-INF and /external->/var/lib/docroot // This does not allow symlinks like /WeB-InF->/var/lib/other @@ -80,10 +81,28 @@ public class SymlinkAllowedResourceAliasChecker extends AllowedResourceAliasChec // If the ancestor is not allowed then do not allow. if (!isAllowed(p)) return false; - - // TODO as we are building the realURI of the resource, it would be possible to - // re-check that against security constraints. } + else + { + // otherwise check all possibles + for (Resource r : fromBase) + { + p = r.getPath(); + + // If the ancestor of the alias is a symlink, then check if the real URI is protected, otherwise allow. + // This allows symlinks like /other->/WEB-INF and /external->/var/lib/docroot + // This does not allow symlinks like /WeB-InF->/var/lib/other + if (Files.isSymbolicLink(p)) + return !getContextHandler().isProtectedTarget(segmentPath.toString()); + + // If the ancestor is not allowed then do not allow. + if (!isAllowed(p)) + return false; + } + } + + // TODO as we are building the realURI of the resource, it would be possible to + // re-check that against security constraints. } } catch (Throwable t) diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java index d65aa3d3752..d98fdb82ea7 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java @@ -81,6 +81,8 @@ public class Jetty { try { + if (StringUtil.isBlank(timestamp)) + return "unknown"; long epochMillis = Long.parseLong(timestamp); return Instant.ofEpochMilli(epochMillis).toString(); } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/AttributeNormalizer.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/AttributeNormalizer.java index b5a956037d4..afa0f7da6ae 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/AttributeNormalizer.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/AttributeNormalizer.java @@ -29,7 +29,6 @@ import java.util.regex.Pattern; import java.util.stream.Stream; import org.eclipse.jetty.util.StringUtil; -import org.eclipse.jetty.util.URIUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -411,17 +410,11 @@ public class AttributeNormalizer { case "WAR", "WAR.path" -> { - Resource r = baseResource.resolve(suffix); - if (r == null) - return prefix + URIUtil.addPaths(baseResource.iterator().next().getPath().toString(), suffix); - return prefix + r.getPath(); + return prefix + baseResource.resolve(suffix).getPath(); } case "WAR.uri" -> { - Resource r = baseResource.resolve(suffix); - if (r == null) - return prefix + URIUtil.addPaths(baseResource.iterator().next().getURI().toString(), suffix); - return prefix + r.getURI(); + return prefix + baseResource.resolve(suffix).getURI(); } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/CombinedResource.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/CombinedResource.java index 516968a9699..7d22ed2db04 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/CombinedResource.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/CombinedResource.java @@ -153,20 +153,23 @@ public class CombinedResource extends Resource // Attempt a simple (single) Resource lookup that exists Resource resolved = null; + Resource notFound = null; for (Resource res : _resources) { resolved = res.resolve(subUriPath); - if (Resources.missing(resolved)) - continue; // skip, doesn't exist - if (!resolved.isDirectory()) + if (!Resources.missing(resolved) && !resolved.isDirectory()) return resolved; // Return simple (non-directory) Resource + + if (Resources.missing(resolved) && notFound == null) + notFound = resolved; + if (resources == null) resources = new ArrayList<>(); resources.add(resolved); } if (resources == null) - return resolved; // This will not exist + return notFound; // This will not exist if (resources.size() == 1) return resources.get(0); @@ -177,13 +180,28 @@ public class CombinedResource extends Resource @Override public boolean exists() { - return _resources.stream().anyMatch(Resource::exists); + for (Resource r : _resources) + if (r.exists()) + return true; + return false; } @Override public Path getPath() { - return null; + int exists = 0; + Path path = null; + for (Resource r : _resources) + { + if (r.exists() && exists++ == 0) + path = r.getPath(); + } + return switch (exists) + { + case 0 -> _resources.get(0).getPath(); + case 1 -> path; + default -> null; + }; } @Override @@ -213,7 +231,19 @@ public class CombinedResource extends Resource @Override public URI getURI() { - return null; + int exists = 0; + URI uri = null; + for (Resource r : _resources) + { + if (r.exists() && exists++ == 0) + uri = r.getURI(); + } + return switch (exists) + { + case 0 -> _resources.get(0).getURI(); + case 1 -> uri; + default -> null; + }; } @Override @@ -289,6 +319,8 @@ public class CombinedResource extends Resource Collection all = getAllResources(); for (Resource r : all) { + if (!r.exists()) + continue; Path relative = getPathTo(r); Path pathTo = Objects.equals(relative.getFileSystem(), destination.getFileSystem()) ? destination.resolve(relative) @@ -335,6 +367,37 @@ public class CombinedResource extends Resource return Objects.hash(_resources); } + @Override + public boolean isAlias() + { + for (Resource r : _resources) + { + if (r.isAlias()) + return true; + } + return false; + } + + @Override + public URI getRealURI() + { + if (!isAlias()) + return getURI(); + int exists = 0; + URI uri = null; + for (Resource r : _resources) + { + if (r.exists() && exists++ == 0) + uri = r.getRealURI(); + } + return switch (exists) + { + case 0 -> _resources.get(0).getRealURI(); + case 1 -> uri; + default -> null; + }; + } + /** * @return the list of resources */ @@ -375,6 +438,8 @@ public class CombinedResource extends Resource // return true it's relative location to the first matching resource. for (Resource r : _resources) { + if (!r.exists()) + continue; Path path = r.getPath(); if (otherPath.startsWith(path)) return path.relativize(otherPath); @@ -387,8 +452,14 @@ public class CombinedResource extends Resource Path relative = null; loop : for (Resource o : other) { + if (!o.exists()) + continue; + for (Resource r : _resources) { + if (!r.exists()) + continue; + if (o.getPath().startsWith(r.getPath())) { Path rel = r.getPath().relativize(o.getPath()); diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java index 14013f9d557..b095f922e49 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java @@ -292,10 +292,7 @@ public class PathResource extends Resource URI uri = getURI(); URI resolvedUri = URIUtil.addPath(uri, subUriPath); Path path = Paths.get(resolvedUri); - if (Files.exists(path)) - return newResource(path, resolvedUri); - - return null; + return newResource(path, resolvedUri); } /** diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResourceFactory.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResourceFactory.java index 2c739ca225e..c86d2b10a6b 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResourceFactory.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResourceFactory.java @@ -14,8 +14,6 @@ package org.eclipse.jetty.util.resource; import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; public class PathResourceFactory implements ResourceFactory @@ -23,9 +21,6 @@ public class PathResourceFactory implements ResourceFactory @Override public Resource newResource(URI uri) { - Path path = Paths.get(uri.normalize()); - if (!Files.exists(path)) - return null; - return new PathResource(path, uri, false); + return new PathResource(Paths.get(uri.normalize()), uri, false); } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java index 62ae66247ed..ec0dce3ed14 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java @@ -268,7 +268,8 @@ public abstract class Resource implements Iterable * Resolve an existing Resource. * * @param subUriPath the encoded subUriPath - * @return an existing Resource representing the requested subUriPath, or null if resource does not exist. + * @return a Resource representing the requested subUriPath, which may not {@link #exists() exist}, + * or null if the resource cannot exist. * @throws IllegalArgumentException if subUriPath is invalid */ public abstract Resource resolve(String subUriPath); @@ -303,6 +304,9 @@ public abstract class Resource implements Iterable public void copyTo(Path destination) throws IOException { + if (!exists()) + throw new IOException("Resource does not exist: " + getFileName()); + Path src = getPath(); if (src == null) { diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/CombinedResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/CombinedResourceTest.java index f45c7a0ee9e..17b5f2a468e 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/CombinedResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/CombinedResourceTest.java @@ -49,7 +49,6 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; @ExtendWith(WorkDirExtension.class) public class CombinedResourceTest @@ -118,7 +117,7 @@ public class CombinedResourceTest assertThat(relative, containsInAnyOrder(expected)); Resource unk = rc.resolve("unknown"); - assertNull(unk); + assertFalse(unk.exists()); assertEquals(getContent(rc, "1.txt"), "1 - one"); assertEquals(getContent(rc, "2.txt"), "2 - two"); diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java index a183776aec6..c0739721063 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java @@ -406,7 +406,7 @@ public class PathResourceTest // Resolve to name, but different case testText = archiveResource.resolve("/TEST.TXT"); - assertNull(testText); + assertFalse(testText.exists()); // Resolve using path navigation testText = archiveResource.resolve("/foo/../test.txt"); @@ -464,9 +464,9 @@ public class PathResourceTest // Resolve file to name, but different case testText = archiveResource.resolve("/dir/TEST.TXT"); - assertNull(testText); + assertFalse(testText.exists()); testText = archiveResource.resolve("/DIR/test.txt"); - assertNull(testText); + assertFalse(testText.exists()); // Resolve file using path navigation testText = archiveResource.resolve("/foo/../dir/test.txt"); @@ -480,7 +480,7 @@ public class PathResourceTest // Resolve file using extension-less directory testText = archiveResource.resolve("/dir./test.txt"); - assertNull(testText); + assertFalse(testText.exists()); // Resolve directory to name, no slash Resource dirResource = archiveResource.resolve("/dir"); diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java index ec2396805cd..a3d939a2a51 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java @@ -21,6 +21,7 @@ import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; @@ -266,7 +267,7 @@ public class ResourceTest if (data.exists) assertThat("Exists: " + res.getName(), res.exists(), equalTo(data.exists)); else - assertNull(res); + assertFalse(res.exists()); } @ParameterizedTest @@ -299,10 +300,16 @@ public class ResourceTest Assumptions.assumeTrue(resource != null); Path targetDir = workDir.getEmptyPathDir(); - resource.copyTo(targetDir); - - Path targetToTest = resource.isDirectory() ? targetDir : targetDir.resolve(resource.getFileName()); - assertResourceSameAsPath(resource, targetToTest); + if (Resources.exists(resource)) + { + resource.copyTo(targetDir); + Path targetToTest = resource.isDirectory() ? targetDir : targetDir.resolve(resource.getFileName()); + assertResourceSameAsPath(resource, targetToTest); + } + else + { + assertThrows(IOException.class, () -> resource.copyTo(targetDir)); + } } @ParameterizedTest @@ -317,9 +324,40 @@ public class ResourceTest String filename = resource.getFileName(); Path targetDir = workDir.getEmptyPathDir(); Path targetFile = targetDir.resolve(filename); - resource.copyTo(targetFile); + if (Resources.exists(resource)) + { + resource.copyTo(targetFile); + assertResourceSameAsPath(resource, targetFile); + } + else + { + assertThrows(IOException.class, () -> resource.copyTo(targetFile)); + } + } - assertResourceSameAsPath(resource, targetFile); + @Test + public void testNonExistentResource() + { + Path nonExistentFile = workDir.getPathFile("does-not-exists"); + Resource resource = resourceFactory.newResource(nonExistentFile); + assertFalse(resource.exists()); + assertThrows(IOException.class, () -> resource.copyTo(workDir.getEmptyPathDir())); + assertTrue(resource.list().isEmpty()); + assertFalse(resource.contains(resourceFactory.newResource(workDir.getPath()))); + assertEquals("does-not-exists", resource.getFileName()); + assertFalse(resource.isReadable()); + assertEquals(nonExistentFile, resource.getPath()); + assertEquals(Instant.EPOCH, resource.lastModified()); + assertEquals(0L, resource.length()); + assertThrows(IOException.class, resource::newInputStream); + assertThrows(IOException.class, resource::newReadableByteChannel); + assertEquals(nonExistentFile.toUri(), resource.getURI()); + assertFalse(resource.isAlias()); + assertNull(resource.getRealURI()); + assertNotNull(resource.getName()); + Resource subResource = resource.resolve("does-not-exist-too"); + assertFalse(subResource.exists()); + assertEquals(nonExistentFile.resolve("does-not-exist-too"), subResource.getPath()); } @Test @@ -430,7 +468,7 @@ public class ResourceTest Path dir = workDir.getEmptyPathDir().resolve("foo/bar"); // at this point we have a directory reference that does not exist Resource resource = resourceFactory.newResource(dir); - assertNull(resource); + assertFalse(resource.exists()); } @Test @@ -442,7 +480,7 @@ public class ResourceTest // at this point we have a file reference that does not exist assertFalse(Files.exists(file)); Resource resource = resourceFactory.newResource(file); - assertNull(resource); + assertFalse(resource.exists()); } @Test diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java index 7e89fec12c3..cd71b15111f 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java @@ -1364,7 +1364,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL // If a WAR file is mounted, or is extracted to a temp directory, // then the first entry of the resource base must be the WAR file. Resource resource = WebAppContext.this.getResource(path); - if (resource == null) + if (Resources.missing(resource)) return null; for (Resource r: resource) diff --git a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java index c76a53dfe2f..8cd667bbd77 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java +++ b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java @@ -1429,7 +1429,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL // If a WAR file is mounted, or is extracted to a temp directory, // then the first entry of the resource base must be the WAR file. Resource resource = WebAppContext.this.getResource(path); - if (resource == null) + if (Resources.missing(resource)) return null; for (Resource r: resource)