-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Jetty 12 - Simplification of aliases in PathResource
#8631
Jetty 12 - Simplification of aliases in PathResource
#8631
Conversation
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
Show resolved
Hide resolved
if (org.junit.jupiter.api.condition.OS.WINDOWS.isCurrentOs()) | ||
{ | ||
assertThat("getURI()", r.getPath().toString(), containsString("aa\\foo.txt")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test was already failing on 12.0.x branch when run on windows.
So this isn't directly related to the other changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it getURI()
and then using path to validate the string?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was fixed in PR #8706 btw (you should merge from origin/jetty-12.0.x
again into this branch)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Questions about normalize, and some nits about consistency in test cases.
jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java
Outdated
Show resolved
Hide resolved
jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java
Outdated
Show resolved
Hide resolved
jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java
Outdated
Show resolved
Hide resolved
jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
Show resolved
Hide resolved
jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
Outdated
Show resolved
Hide resolved
jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
Outdated
Show resolved
Hide resolved
jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
Outdated
Show resolved
Hide resolved
return true; | ||
if (isAlias()) | ||
return Files.exists(targetPath); | ||
return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Files.exists()
implicitly take a FOLLOW_SYMLINKS
, so the first at line 173 check should be enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not if the path has unresolved ..
segments, then Files.exists()
returns false even if it does exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with sbordet
If this.path
was created with one of the URI
based methods for creating a Path
(eg: Paths.of(URI)
or Paths.get(URI)
) then your statement is true.
As there is an expectation that the URI
it is given is absolute, canonical, and normalized.
But if this.path
was created from a String
or other Path
object, even the /../
segments have no impact on the Files.exists()
logic and will return true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But if this.path was created from a String or other Path object, even the /../ segments have no impact on the Files.exists() logic and will return true.
This is not correct.
The following test passes and shows that Files.exists does not work with /../
segments. And since you can create PathResource
with a Path
directly we cannot assume that the path does not contain /../
segments.
Path resourcePath = testDir.resolve("resource.txt");
assertTrue(Files.exists(resourcePath));
Path aliasPath = testDir.resolve("foo/../resource.txt");
assertFalse(Files.exists(aliasPath));
assertTrue(Files.exists(aliasPath.normalize()));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Files.exists()
does work with /../
if each element of the path exists.
In your case foo
does not exist on your filesystem.
This is part of the filesystem specific normalization techniques.
Some filesystems will find it (some will not).
Don't write your code assuming that all filesystems behave the same in this regard.
Example code (tested on Linux with ext4 filesystem)
package fs;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.eclipse.jetty.toolchain.test.FS;
public class PathsGetExample
{
public static void main(String[] args) throws IOException
{
boolean withFoo = true;
System.out.printf("withFoo = %b%n", withFoo);
// Create some content
Path testDir = Paths.get("/tmp/paths-get-examples");
FS.ensureDirExists(testDir);
Path fooDir = testDir.resolve("foo");
if (withFoo)
FS.ensureDirExists(testDir.resolve("foo"));
else
Files.deleteIfExists(fooDir);
FS.touch(testDir.resolve("test.txt"));
URI uri = URI.create(testDir.toUri().toASCIIString() + "foo/../test.txt");
Path a = Paths.get(uri);
System.out.printf("%s -> %s -> %b%n", uri, a, Files.exists(a));
URI urin = uri.normalize();
Path b = Paths.get(urin);
System.out.printf("%s -> %s -> %b%n", urin, b, Files.exists(b));
String str = "/tmp/paths-get-examples/foo/../test.txt";
Path c = Paths.get(str);
System.out.printf("%s -> %s -> %b%n", str, c, Files.exists(c));
}
}
Executing with "foo"
withFoo = true
file:///tmp/paths-get-examples/foo/../test.txt -> /tmp/paths-get-examples/foo/../test.txt -> true
file:/tmp/paths-get-examples/test.txt -> /tmp/paths-get-examples/test.txt -> true
/tmp/paths-get-examples/foo/../test.txt -> /tmp/paths-get-examples/foo/../test.txt -> true
Executing without "foo"
withFoo = false
file:///tmp/paths-get-examples/foo/../test.txt -> /tmp/paths-get-examples/foo/../test.txt -> false
file:/tmp/paths-get-examples/test.txt -> /tmp/paths-get-examples/test.txt -> true
/tmp/paths-get-examples/foo/../test.txt -> /tmp/paths-get-examples/foo/../test.txt -> false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the same behavior you get on linux with ls
...
[joakim@hyperion ~]$ ls -la /tmp/paths-get-examples/
total 512
drwxrwxr-x 2 joakim joakim 4096 Oct 14 07:45 ./
drwxrwxrwt 221 root root 516096 Oct 14 07:42 ../
-rw-rw-r-- 1 joakim joakim 0 Oct 14 07:42 test.txt
[joakim@hyperion ~]$ ls -la /tmp/paths-get-examples/foo/../test.txt
ls: cannot access '/tmp/paths-get-examples/foo/../test.txt': No such file or directory
[joakim@hyperion ~]$ mkdir /tmp/paths-get-examples/foo
[joakim@hyperion ~]$ ls -la /tmp/paths-get-examples/
total 516
drwxrwxr-x 3 joakim joakim 4096 Oct 14 07:45 ./
drwxrwxrwt 221 root root 516096 Oct 14 07:42 ../
drwxrwxr-x 2 joakim joakim 4096 Oct 14 07:45 foo/
-rw-rw-r-- 1 joakim joakim 0 Oct 14 07:42 test.txt
[joakim@hyperion ~]$ ls -la /tmp/paths-get-examples/foo/../test.txt
-rw-rw-r-- 1 joakim joakim 0 Oct 14 07:42 /tmp/paths-get-examples/foo/../test.txt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way I see it we have a few options here.
- We have all
Resource
implementations do the URI normalization, removing segments like/../
before creating thePath
object. - We have all
Resource
implementations honor the as-submittedURI
, with noResource
implementation normalization, and let the FileSystem suss out if the content can be referenced or not.
We are doing 1 currently, but should we?
Is it such a terrible idea to reject requests for content like dir-does-not-exist/../test.dat
and allow dir-that-exists/../test.dat
?
I could easily be convinced that the Resource
implementations should not perform URI normalization.
It just means a few types of torturous requests we used to receive in the past we no longer support.
I wonder, is supporting /dir-does-not-exist/
segments hiding or revealing extra information about the filesystem we shouldn't be doing? (need more time to ponder this)
What does supporting /dir-does-not-exist/
segments mean for ResourceCollection too? (if anything)
Or, if we want to support these requests still, it means that the normalization occurs at the layer above Resource.resolve(String)
(which kinda makes sense, and I suspect it would simplify the protected paths issues too)
return targetPath.toUri(); | ||
} | ||
|
||
private Path resolveTargetPath() | ||
private void checkAlias() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename to resolveAlias()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @sbordet, this is a resolveAlias()
or resolveTarget()
name.
jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
Outdated
Show resolved
Hide resolved
jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
Outdated
Show resolved
Hide resolved
PathResource
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree with the name change from target
to canonical
as that's not appropriate for Path
(or FileSystem
or FileStore
).
jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
Outdated
Show resolved
Hide resolved
return true; | ||
if (isAlias()) | ||
return Files.exists(targetPath); | ||
return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with sbordet
If this.path
was created with one of the URI
based methods for creating a Path
(eg: Paths.of(URI)
or Paths.get(URI)
) then your statement is true.
As there is an expectation that the URI
it is given is absolute, canonical, and normalized.
But if this.path
was created from a String
or other Path
object, even the /../
segments have no impact on the Files.exists()
logic and will return true.
return targetPath.toUri(); | ||
} | ||
|
||
private Path resolveTargetPath() | ||
private void checkAlias() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @sbordet, this is a resolveAlias()
or resolveTarget()
name.
jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
Outdated
Show resolved
Hide resolved
jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
Outdated
Show resolved
Hide resolved
jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
Outdated
Show resolved
Hide resolved
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
…PathResource-resolveTargetPath
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Canonical Path is the wrong name for multiple reasons.
Either correct the name to be what it represents (a real name, or target), or correct the API usage to use the old school canonicalization techniques that you apparently want to stick with.
jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
Show resolved
Hide resolved
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
if (getClass() != obj.getClass()) | ||
return false; | ||
PathResource other = (PathResource)obj; | ||
return Objects.equals(path, other.path) && Objects.equals(uri, other.uri); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need equals/hashCode at all?
The comparison on uri
is odd, that's a user-space provided field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why we need to compare PathResource
, I know there are tests for the equals()
but not sure if we actually use it anywhere. But to compare these correctly you need to take the uri
into account.
You can come up with examples where two different PathResource
s have the same path
field but one is classified as an alias and the other is not. So comparing only on path
isn't quite correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will impact the ResourceService cache though.
The same Resource will exist on multiple locations once this new equals/hashcode exists.
Is that an acceptable behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new equals/hashcode logic is not needed, nor is is critical for any step you have in this PR.
Also, if a Path doesn't exist, then it cannot have an alias.
assertThat("uri.targetURI", ResourceFactory.root().newResource(resBar.getURI()), isTargetFor(resFoo)); | ||
assertThat("file.targetURI", ResourceFactory.root().newResource(resBar.getPath()), isTargetFor(resFoo)); | ||
// This is an alias because the file does not exist. | ||
assertFalse(resFoo.exists()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What!?
If the file doesn't exist, it can never be an alias.
assertThat("file.targetURI", ResourceFactory.root().newResource(resBar.getPath()), isTargetFor(resFoo)); | ||
// This is an alias because the file does not exist. | ||
assertFalse(resFoo.exists()); | ||
assertTrue(resFoo.isAlias()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be false. Something is wrong here.
@@ -1067,10 +1066,14 @@ public void testSingleQuoteInFileName() throws Exception | |||
"URI[b] = " + refB; | |||
assertThat(msg, refA.equals(refB), is(false)); | |||
|
|||
// now show that Resource.equals() does work | |||
// These resources are not equal because they have different URIs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels dangerous for our mounting logic and also our caches.
The user-space provided URI should not be part of our equals/hashcode behaviors.
I don't see a use case in this PR that makes this change necessary.
if (org.junit.jupiter.api.condition.OS.WINDOWS.isCurrentOs()) | ||
{ | ||
assertThat("getURI()", r.getPath().toString(), containsString("aa\\foo.txt")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was fixed in PR #8706 btw (you should merge from origin/jetty-12.0.x
again into this branch)
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need protection for users that execute this test on filesystems that don't support symlink (don't check the OS here, check the success/failure of the createSymbolicLink method instead, like we do elsewhere)
assertTrue(symlinkResource.exists()); | ||
|
||
// After deleting file the Resources do not exist even though symlink file exists. | ||
Files.delete(resourcePath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This delete needs protection for users on filesystems that don't immediately delete.
Test that the resourcePath
was actually deleted in an assumption.
@@ -398,7 +398,7 @@ public void testDotAliasDirDoesNotExist(WorkDir workDir) | |||
Resource dot = resource.resolve("."); | |||
assertNotNull(dot); | |||
assertFalse(dot.exists()); | |||
assertFalse(dot.isAlias(), "Reference to '.' is not an alias as directory doesn't exist"); | |||
assertTrue(dot.isAlias(), "Reference to '.' is an alias as directory doesn't exist"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, if the resource doesn't exist, it cannot have an alias.
@@ -428,7 +428,7 @@ public void testDotAliasFileDoesNotExists(WorkDir workDir) throws IOException | |||
Resource dot = resource.resolve("."); | |||
assertNotNull(dot); | |||
assertFalse(dot.exists()); | |||
assertFalse(dot.isAlias(), "Reference to '.' is not an alias as file doesn't exist"); | |||
assertTrue(dot.isAlias(), "Reference to '.' is an alias as file doesn't exist"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, if the resource doesn't exist, it cannot have an alias.
if (checkAlias(pathInContext, resource)) | ||
return resource; | ||
return null; | ||
return baseResource.resolve(pathInContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think you can remove the alias checking here.
@@ -457,8 +457,6 @@ protected Resource resolve(String subUriPath) | |||
if (_baseResource != null) | |||
{ | |||
r = _baseResource.resolve(subUriPath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets move these alias checking changes in ee9 out to a different PR.
jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
Show resolved
Hide resolved
Closing in favor of #8734 |
resolveTargetPath()
method has been simplified.getTargetPath()
now always returns the target resource with all aliases resolved.isAlias()
should be used to determine whether it is an alias instead ofgetTargetPath() != null
.