Merge pull request #4001 from eclipse/jetty-9.4.x-4000-swedish-unicode-file-serving
Issue #4000 - new SameFileAliasChecker to help with NFC/NFD UTF-8 differences
This commit is contained in:
commit
1867d24ef7
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.eclipse.jetty.server.handler.ContextHandler.AliasCheck;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
import org.eclipse.jetty.util.resource.PathResource;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
|
||||
/**
|
||||
* Alias checking for working with FileSystems that normalize access to the
|
||||
* File System.
|
||||
* <p>
|
||||
* The Java {@link Files#isSameFile(Path, Path)} method is used to determine
|
||||
* if the requested file is the same as the alias file.
|
||||
* </p>
|
||||
* <p>
|
||||
* For File Systems that are case insensitive (eg: Microsoft Windows FAT32 and NTFS),
|
||||
* the access to the file can be in any combination or style of upper and lowercase.
|
||||
* </p>
|
||||
* <p>
|
||||
* For File Systems that normalize UTF-8 access (eg: Mac OSX on HFS+ or APFS,
|
||||
* or Linux on XFS) the the actual file could be stored using UTF-16,
|
||||
* but be accessed using NFD UTF-8 or NFC UTF-8 for the same file.
|
||||
* </p>
|
||||
*/
|
||||
public class SameFileAliasChecker implements AliasCheck
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(SameFileAliasChecker.class);
|
||||
|
||||
@Override
|
||||
public boolean check(String uri, Resource resource)
|
||||
{
|
||||
// Only support PathResource alias checking
|
||||
if (!(resource instanceof PathResource))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
PathResource pathResource = (PathResource)resource;
|
||||
Path path = pathResource.getPath();
|
||||
Path alias = pathResource.getAliasPath();
|
||||
|
||||
if (Files.isSameFile(path, alias))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Allow alias to same file {} --> {}", path, alias);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
LOG.ignore(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -52,7 +52,7 @@ public class AllowSymLinkAliasChecker implements AliasCheck
|
|||
Path path = pathResource.getPath();
|
||||
Path alias = pathResource.getAliasPath();
|
||||
|
||||
if (path.equals(alias))
|
||||
if (PathResource.isSameName(alias, path))
|
||||
return false; // Unknown why this is an alias
|
||||
|
||||
if (hasSymbolicLink(path) && Files.isSameFile(path, alias))
|
||||
|
|
|
@ -54,12 +54,14 @@ import org.eclipse.jetty.server.HttpConfiguration;
|
|||
import org.eclipse.jetty.server.LocalConnector;
|
||||
import org.eclipse.jetty.server.ResourceContentFactory;
|
||||
import org.eclipse.jetty.server.ResourceService;
|
||||
import org.eclipse.jetty.server.SameFileAliasChecker;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.handler.AllowSymLinkAliasChecker;
|
||||
import org.eclipse.jetty.toolchain.test.FS;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.eclipse.jetty.util.TypeUtil;
|
||||
import org.eclipse.jetty.util.log.StacklessLogging;
|
||||
import org.eclipse.jetty.util.resource.PathResource;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
|
@ -73,6 +75,7 @@ import org.junit.jupiter.params.provider.Arguments;
|
|||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeader;
|
||||
import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeaderValue;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
@ -2009,6 +2012,76 @@ public class DefaultServletTest
|
|||
response = HttpTester.parseResponse(rawResponse);
|
||||
assertThat(response.toString(), response.getStatus(), is(HttpStatus.PRECONDITION_FAILED_412));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUtf8NfcFile() throws Exception
|
||||
{
|
||||
FS.ensureEmpty(docRoot);
|
||||
|
||||
context.addServlet(DefaultServlet.class, "/");
|
||||
context.addAliasCheck(new SameFileAliasChecker());
|
||||
|
||||
// UTF-8 NFC format
|
||||
String filename = "swedish-" + new String(TypeUtil.fromHexString("C3A5"), UTF_8) + ".txt";
|
||||
createFile(docRoot.resolve(filename), "hi a-with-circle");
|
||||
|
||||
String rawResponse;
|
||||
HttpTester.Response response;
|
||||
|
||||
// Request as UTF-8 NFC
|
||||
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"));
|
||||
|
||||
// Request as UTF-8 NFD
|
||||
rawResponse = connector.getResponse("GET /context/swedish-a%CC%8A.txt HTTP/1.1\r\nHost:test\r\nConnection:close\r\n\r\n");
|
||||
response = HttpTester.parseResponse(rawResponse);
|
||||
if (OS.MAC.isCurrentOs())
|
||||
{
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(response.getContent(), is("hi a-with-circle"));
|
||||
}
|
||||
else
|
||||
{
|
||||
assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND_404));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUtf8NfdFile() throws Exception
|
||||
{
|
||||
FS.ensureEmpty(docRoot);
|
||||
|
||||
context.addServlet(DefaultServlet.class, "/");
|
||||
context.addAliasCheck(new SameFileAliasChecker());
|
||||
|
||||
// UTF-8 NFD format
|
||||
String filename = "swedish-" + new String(TypeUtil.fromHexString("61CC8A"), UTF_8) + ".txt";
|
||||
createFile(docRoot.resolve(filename), "hi a-with-circle");
|
||||
|
||||
String rawResponse;
|
||||
HttpTester.Response response;
|
||||
|
||||
// Request as UTF-8 NFD
|
||||
rawResponse = connector.getResponse("GET /context/swedish-a%CC%8A.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"));
|
||||
|
||||
// Request as UTF-8 NFC
|
||||
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);
|
||||
if (OS.MAC.isCurrentOs())
|
||||
{
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(response.getContent(), is("hi a-with-circle"));
|
||||
}
|
||||
else
|
||||
{
|
||||
assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND_404));
|
||||
}
|
||||
}
|
||||
|
||||
public static class OutputFilter implements Filter
|
||||
{
|
||||
|
|
|
@ -104,56 +104,10 @@ public class PathResource extends Resource
|
|||
{
|
||||
Path real = abs.toRealPath(FOLLOW_LINKS);
|
||||
|
||||
/*
|
||||
* If the real path is not the same as the absolute path
|
||||
* then we know that the real path is the alias for the
|
||||
* provided path.
|
||||
*
|
||||
* For OS's that are case insensitive, this should
|
||||
* return the real (on-disk / case correct) version
|
||||
* of the path.
|
||||
*
|
||||
* We have to be careful on Windows and OSX.
|
||||
*
|
||||
* Assume we have the following scenario
|
||||
* Path a = new File("foo").toPath();
|
||||
* Files.createFile(a);
|
||||
* Path b = new File("FOO").toPath();
|
||||
*
|
||||
* There now exists a file called "foo" on disk.
|
||||
* Using Windows or OSX, with a Path reference of
|
||||
* "FOO", "Foo", "fOO", etc.. means the following
|
||||
*
|
||||
* | OSX | Windows | Linux
|
||||
* -----------------------+---------+------------+---------
|
||||
* Files.exists(a) | True | True | True
|
||||
* Files.exists(b) | True | True | False
|
||||
* Files.isSameFile(a,b) | True | True | False
|
||||
* a.equals(b) | False | True | False
|
||||
*
|
||||
* See the javadoc for Path.equals() for details about this FileSystem
|
||||
* behavior difference
|
||||
*
|
||||
* 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 (!isSameName(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)
|
||||
|
@ -167,6 +121,82 @@ public class PathResource extends Resource
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the paths are the same name.
|
||||
*
|
||||
* <p>
|
||||
* If the real path is not the same as the absolute path
|
||||
* then we know that the real path is the alias for the
|
||||
* provided path.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* For OS's that are case insensitive, this should
|
||||
* return the real (on-disk / case correct) version
|
||||
* of the path.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* We have to be careful on Windows and OSX.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Assume we have the following scenario:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* Path a = new File("foo").toPath();
|
||||
* Files.createFile(a);
|
||||
* Path b = new File("FOO").toPath();
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* There now exists a file called {@code foo} on disk.
|
||||
* Using Windows or OSX, with a Path reference of
|
||||
* {@code FOO}, {@code Foo}, {@code fOO}, etc.. means the following
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* | OSX | Windows | Linux
|
||||
* -----------------------+---------+------------+---------
|
||||
* Files.exists(a) | True | True | True
|
||||
* Files.exists(b) | True | True | False
|
||||
* Files.isSameFile(a,b) | True | True | False
|
||||
* a.equals(b) | False | True | False
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* See the javadoc for Path.equals() for details about this FileSystem
|
||||
* behavior difference
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* We also cannot rely on a.compareTo(b) as this is roughly equivalent
|
||||
* in implementation to a.equals(b)
|
||||
* </p>
|
||||
*/
|
||||
public static boolean isSameName(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; i-- > 0; )
|
||||
{
|
||||
if (!pathA.getName(i).toString().equals(pathB.getName(i).toString()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new PathResource from a File object.
|
||||
* <p>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
package org.eclipse.jetty.util.resource;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -318,7 +319,43 @@ public class FileSystemResourceTest
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fsResourceProvider")
|
||||
public void testAccessUniCodeFile(Class resourceClass) throws Exception
|
||||
{
|
||||
Path dir = workDir.getEmptyPathDir();
|
||||
|
||||
String readableRootDir = findRootDir(dir.getFileSystem());
|
||||
assumeTrue(readableRootDir != null, "Readable Root Dir found");
|
||||
|
||||
Path subdir = dir.resolve("sub");
|
||||
Files.createDirectories(subdir);
|
||||
|
||||
touchFile(subdir.resolve("swedish-å.txt"), "hi a-with-circle");
|
||||
touchFile(subdir.resolve("swedish-ä.txt"), "hi a-with-two-dots");
|
||||
touchFile(subdir.resolve("swedish-ö.txt"), "hi o-with-two-dots");
|
||||
|
||||
try (Resource base = newResource(resourceClass, subdir.toFile()))
|
||||
{
|
||||
Resource refA1 = base.addPath("swedish-å.txt");
|
||||
Resource refA2 = base.addPath("swedish-ä.txt");
|
||||
Resource refO1 = base.addPath("swedish-ö.txt");
|
||||
|
||||
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"));
|
||||
assertThat("Ref O1 contents", toString(refO1), is("hi o-with-two-dots"));
|
||||
}
|
||||
}
|
||||
|
||||
private String findRootDir(FileSystem fs) throws IOException
|
||||
{
|
||||
// look for a directory off of a root path
|
||||
|
@ -419,12 +456,7 @@ public class FileSystemResourceTest
|
|||
Files.createDirectories(dir);
|
||||
|
||||
Path file = dir.resolve("foo");
|
||||
|
||||
try (StringReader reader = new StringReader("foo");
|
||||
BufferedWriter writer = Files.newBufferedWriter(file))
|
||||
{
|
||||
IO.copy(reader, writer);
|
||||
}
|
||||
touchFile(file, "foo");
|
||||
|
||||
long expected = Files.size(file);
|
||||
|
||||
|
@ -513,12 +545,7 @@ public class FileSystemResourceTest
|
|||
|
||||
Path file = dir.resolve("foo");
|
||||
String content = "Foo is here";
|
||||
|
||||
try (StringReader reader = new StringReader(content);
|
||||
BufferedWriter writer = Files.newBufferedWriter(file))
|
||||
{
|
||||
IO.copy(reader, writer);
|
||||
}
|
||||
touchFile(file, content);
|
||||
|
||||
try (Resource base = newResource(resourceClass, dir.toFile()))
|
||||
{
|
||||
|
@ -1507,4 +1534,23 @@ public class FileSystemResourceTest
|
|||
assertThat("getAlias()", resource.getAlias(), nullValue());
|
||||
}
|
||||
}
|
||||
|
||||
private String toString(Resource resource) throws IOException
|
||||
{
|
||||
try (InputStream inputStream = resource.getInputStream();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream())
|
||||
{
|
||||
IO.copy(inputStream, outputStream);
|
||||
return outputStream.toString("utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
private void touchFile(Path outputFile, String content) throws IOException
|
||||
{
|
||||
try (StringReader reader = new StringReader(content);
|
||||
BufferedWriter writer = Files.newBufferedWriter(outputFile))
|
||||
{
|
||||
IO.copy(reader, writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue