Issue #4000 - PathResource alias detection work around alt UTF-8 style

+ OSX File is `swedish-å.txt`
+ OSX has NFD form UTF-8 characters. `swedish-a%CC%8A.txt`
+ HTTP uses normal form UTF-8. `swedish-%C3%A5.txt`
+ A HTTP GET request should work against the resource
  being requested, regardless of UTF-8 style used.

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
Joakim Erdfelt 2019-08-19 13:52:22 -05:00
parent 407b564320
commit ef3f696a11
3 changed files with 111 additions and 40 deletions

View File

@ -18,6 +18,20 @@
package org.eclipse.jetty.servlet;
import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeader;
import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeaderValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
@ -35,6 +49,7 @@ import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
@ -73,20 +88,6 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeader;
import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeaderValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@ExtendWith(WorkDirExtension.class)
public class DefaultServletTest
{
@ -2009,6 +2010,37 @@ public class DefaultServletTest
response = HttpTester.parseResponse(rawResponse);
assertThat(response.toString(), response.getStatus(), is(HttpStatus.PRECONDITION_FAILED_412));
}
@Test
public void testGetUnicodeFile() throws Exception
{
FS.ensureEmpty(docRoot);
System.err.printf("docRoot is %s%n", docRoot);
context.addServlet(DefaultServlet.class, "/");
createFile(docRoot.resolve("swedish-å.txt"), "hi a-with-circle");
createFile(docRoot.resolve("swedish-ä.txt"), "hi a-with-two-dots");
createFile(docRoot.resolve("swedish-ö.txt"), "hi o-with-two-dots");
String rawResponse;
HttpTester.Response response;
rawResponse = connector.getResponse("GET /context/swedish-%C3%A5.txt HTTP/1.1\r\nHost:test\r\nConnection:close\r\n\r\n");
response = HttpTester.parseResponse(rawResponse);
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(response.getContent(), is("hi a-with-circle"));
rawResponse = connector.getResponse("GET /context/swedish-%C3%A4.txt HTTP/1.1\r\nHost:test\r\nConnection:close\r\n\r\n");
response = HttpTester.parseResponse(rawResponse);
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(response.getContent(), is("hi a-with-two-dots"));
rawResponse = connector.getResponse("GET /context/swedish-%C3%B6.txt HTTP/1.1\r\nHost:test\r\nConnection:close\r\n\r\n");
response = HttpTester.parseResponse(rawResponse);
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(response.getContent(), is("hi o-with-two-dots"));
}
public static class OutputFilter implements Filter
{

View File

@ -80,7 +80,15 @@ public class PathResource extends Resource
{
try
{
return Paths.get(uri).toRealPath(FOLLOW_LINKS);
Path followed = Paths.get(uri).toRealPath(FOLLOW_LINKS);
if (!isSame(path, followed))
{
return followed;
}
else
{
return null;
}
}
catch (IOException ignored)
{
@ -137,23 +145,11 @@ public class PathResource extends Resource
* We also cannot rely on a.compareTo(b) as this is roughly equivalent
* in implementation to a.equals(b)
*/
int absCount = abs.getNameCount();
int realCount = real.getNameCount();
if (absCount != realCount)
if (!isSame(abs, real))
{
// different number of segments
return real;
}
// compare each segment of path, backwards
for (int i = realCount - 1; i >= 0; i--)
{
if (!abs.getName(i).toString().equals(real.getName(i).toString()))
{
return real;
}
}
}
}
catch (IOException e)
@ -166,6 +162,28 @@ public class PathResource extends Resource
}
return null;
}
private boolean isSame(Path pathA, Path pathB)
{
int aCount = pathA.getNameCount();
int bCount = pathB.getNameCount();
if (aCount != bCount)
{
// different number of segments
return false;
}
// compare each segment of path, backwards
for (int i = bCount - 1; i >= 0; i--)
{
if (!pathA.getName(i).toString().equals(pathB.getName(i).toString()))
{
return false;
}
}
return true;
}
/**
* Construct a new PathResource from a File object.

View File

@ -342,9 +342,13 @@ public class FileSystemResourceTest
Resource refA2 = base.addPath("swedish-ä.txt");
Resource refO1 = base.addPath("swedish-ö.txt");
assertThat("Ref A1", refA1.exists(), is(true));
assertThat("Ref A2", refA2.exists(), is(true));
assertThat("Ref O1", refO1.exists(), is(true));
assertThat("Ref A1 exists", refA1.exists(), is(true));
assertThat("Ref A2 exists", refA2.exists(), is(true));
assertThat("Ref O1 exists", refO1.exists(), is(true));
assertThat("Ref A1 alias", refA1.isAlias(), is(false));
assertThat("Ref A2 alias", refA2.isAlias(), is(false));
assertThat("Ref O1 alias", refO1.isAlias(), is(false));
assertThat("Ref A1 contents", toString(refA1), is("hi a-with-circle"));
assertThat("Ref A2 contents", toString(refA2), is("hi a-with-two-dots"));
@ -1416,11 +1420,19 @@ public class FileSystemResourceTest
Resource r = base.addPath("//foo.txt");
assertThat("getURI()", r.getURI().toASCIIString(), containsString("//foo.txt"));
assertThat("isAlias()", r.isAlias(), is(true));
assertThat("getAlias()", r.getAlias(), notNullValue());
assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("/foo.txt"));
assertThat("Exists: " + r, r.exists(), is(true));
if (PathResource.class.isAssignableFrom(resourceClass))
{
assertThat("isAlias()", r.isAlias(), is(false));
assertThat("getAlias()", r.getAlias(), nullValue());
}
else
{
assertThat("isAlias()", r.isAlias(), is(true));
assertThat("getAlias()", r.getAlias(), notNullValue());
assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("/foo.txt"));
}
}
catch (InvalidPathException e)
{
@ -1449,10 +1461,19 @@ public class FileSystemResourceTest
Resource r = base.addPath("aa//foo.txt");
assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa//foo.txt"));
assertThat("isAlias()", r.isAlias(), is(true));
assertThat("getAlias()", r.getAlias(), notNullValue());
assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("aa/foo.txt"));
if (PathResource.class.isAssignableFrom(resourceClass))
{
assertThat("isAlias()", r.isAlias(), is(false));
assertThat("getAlias()", r.getAlias(), nullValue());
}
else
{
assertThat("isAlias()", r.isAlias(), is(true));
assertThat("getAlias()", r.getAlias(), notNullValue());
assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("aa/foo.txt"));
}
assertThat("Exists: " + r, r.exists(), is(true));
}
catch (InvalidPathException e)