Skip to content
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

Closed

Conversation

lachlan-roberts
Copy link
Contributor

  • The resolveTargetPath() method has been simplified.
  • The method getTargetPath() now always returns the target resource with all aliases resolved.
  • The check isAlias() should be used to determine whether it is an alias instead of getTargetPath() != null.

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>
if (org.junit.jupiter.api.condition.OS.WINDOWS.isCurrentOs())
{
assertThat("getURI()", r.getPath().toString(), containsString("aa\\foo.txt"));
Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor

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)

Copy link
Contributor

@joakime joakime left a 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.

return true;
if (isAlias())
return Files.exists(targetPath);
return false;
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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()));

Copy link
Contributor

@joakime joakime Oct 14, 2022

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

Copy link
Contributor

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

Copy link
Contributor

@joakime joakime Oct 14, 2022

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.

  1. We have all Resource implementations do the URI normalization, removing segments like /../ before creating the Path object.
  2. We have all Resource implementations honor the as-submitted URI, with no Resource 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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to resolveAlias().

Copy link
Contributor

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.

@joakime joakime changed the title Simplification of aliases in PathResource (Jetty-12) Jetty 12 - Simplification of aliases in PathResource Oct 7, 2022
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Copy link
Contributor

@joakime joakime left a 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).

return true;
if (isAlias())
return Files.exists(targetPath);
return false;
Copy link
Contributor

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()
Copy link
Contributor

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.

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Copy link
Contributor

@joakime joakime left a 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.

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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 PathResources 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.

Copy link
Contributor

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?

Copy link
Contributor

@joakime joakime left a 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());
Copy link
Contributor

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());
Copy link
Contributor

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.
Copy link
Contributor

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"));
Copy link
Contributor

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);
Copy link
Contributor

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);
Copy link
Contributor

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");
Copy link
Contributor

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");
Copy link
Contributor

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);
Copy link
Contributor

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);
Copy link
Contributor

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.

@joakime
Copy link
Contributor

joakime commented Oct 19, 2022

Closing in favor of #8734

@joakime joakime closed this Oct 19, 2022
@joakime joakime deleted the jetty-12.0.x-PathResource-resolveTargetPath branch November 21, 2022 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants