Issue #3549 - Using FileName properly in Directory Listings.

+ Even though this was reported against Windows, the solution
  implemented should be sane for all OS or FileSystem combinations.

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
Joakim Erdfelt 2019-04-15 07:11:31 -07:00
parent 608e4f5a98
commit 7b774d82e8
3 changed files with 230 additions and 40 deletions

View File

@ -21,6 +21,8 @@ package org.eclipse.jetty.servlet;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@ -61,6 +63,7 @@ import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
@ -69,6 +72,9 @@ public class DefaultServletTest
@Rule
public TestingDir testdir = new TestingDir();
// The name of the odd-jar used for testing "jar:file://" based resource access.
private static final String ODD_JAR = "jar-resource-odd.jar";
private Server server;
private LocalConnector connector;
private ServletContextHandler context;
@ -81,9 +87,16 @@ public class DefaultServletTest
connector = new LocalConnector(server);
connector.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration().setSendServerVersion(false);
File extraJarResources = MavenTestingUtils.getTestResourceFile(ODD_JAR);
URL urls[] = new URL[] { extraJarResources.toURI().toURL() };
ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader();
URLClassLoader extraClassLoader = new URLClassLoader(urls, parentClassLoader);
context = new ServletContextHandler();
context.setContextPath("/context");
context.setWelcomeFiles(new String[]{"index.html", "index.jsp", "index.htm"});
context.setClassLoader(extraClassLoader);
server.setHandler(context);
server.addConnector(connector);
@ -181,6 +194,122 @@ public class DefaultServletTest
assertResponseNotContains("\"onmouseover", response);
}
/**
* A regression on windows allowed the directory listing show
* the fully qualified paths within the directory listing.
* This test ensures that this behavior will not arise again.
*/
@Test
public void testListingFilenamesOnly() throws Exception
{
ServletHolder defholder = context.addServlet(DefaultServlet.class, "/*");
defholder.setInitParameter("dirAllowed", "true");
defholder.setInitParameter("redirectWelcome", "false");
defholder.setInitParameter("gzip", "false");
testdir.ensureEmpty();
/* create some content in the docroot */
File resBase = testdir.getPathFile("docroot").toFile();
FS.ensureDirExists(resBase);
File one = new File(resBase, "one");
assertTrue(one.mkdir());
File deep = new File(one, "deep");
assertTrue(deep.mkdir());
FS.touch(new File(deep, "foo"));
assertTrue(new File(resBase, "two").mkdir());
assertTrue(new File(resBase, "three").mkdir());
String resBasePath = resBase.getAbsolutePath();
defholder.setInitParameter("resourceBase", resBasePath);
StringBuffer req1 = new StringBuffer();
req1.append("GET /context/one/deep/ HTTP/1.0\n");
req1.append("\n");
String response = connector.getResponses(req1.toString());
assertResponseContains("/foo", response);
assertResponseNotContains(resBase.getAbsolutePath(), response);
}
/**
* A regression on windows allowed the directory listing show
* the fully qualified paths within the directory listing.
* This test ensures that this behavior will not arise again.
*/
@Test
public void testListingFilenamesOnly_UrlResource() throws Exception
{
URL extraResource = context.getClassLoader().getResource("rez/one");
assertNotNull("Must have extra jar resource in classloader", extraResource);
String extraResourceBaseString = extraResource.toURI().toASCIIString();
extraResourceBaseString = extraResourceBaseString.substring(0, extraResourceBaseString.length() - "/one".length());
ServletHolder defholder = context.addServlet(DefaultServlet.class, "/extra/*");
defholder.setInitParameter("resourceBase", extraResourceBaseString);
defholder.setInitParameter("pathInfoOnly", "true");
defholder.setInitParameter("dirAllowed", "true");
defholder.setInitParameter("redirectWelcome", "false");
defholder.setInitParameter("gzip", "false");
StringBuffer req1;
String response;
// Test that GET works first.
req1 = new StringBuffer();
req1.append("GET /context/extra/one HTTP/1.0\n");
req1.append("\n");
response = connector.getResponses(req1.toString());
assertResponseContains("200 OK", response);
assertResponseContains("is this the one?", response);
// Typical directory listing of location in jar:file:// URL
req1 = new StringBuffer();
req1.append("GET /context/extra/deep/ HTTP/1.0\r\n");
req1.append("\r\n");
response = connector.getResponses(req1.toString());
assertResponseContains("200 OK", response);
assertResponseContains("/xxx", response);
assertResponseContains("/yyy", response);
assertResponseContains("/zzz", response);
assertResponseNotContains(extraResourceBaseString, response);
assertResponseNotContains(ODD_JAR, response);
// Get deep resource
req1 = new StringBuffer();
req1.append("GET /context/extra/deep/yyy HTTP/1.0\r\n");
req1.append("\r\n");
response = connector.getResponses(req1.toString());
assertResponseContains("200 OK", response);
assertResponseContains("a file named yyy", response);
// Convoluted directory listing of location in jar:file:// URL
// This exists to test proper encoding output
req1 = new StringBuffer();
req1.append("GET /context/extra/oddities/ HTTP/1.0\r\n");
req1.append("\r\n");
response = connector.getResponses(req1.toString());
assertResponseContains("200 OK", response);
assertResponseContains(">#hashcode&nbsp;<", response); // text on page
assertResponseContains("/oddities/%23hashcode", response); // generated link
assertResponseContains(">other%2fkind%2Fof%2fslash&nbsp;<", response); // text on page
assertResponseContains("/oddities/other%252fkind%252Fof%252fslash", response); // generated link
assertResponseContains(">a file with a space&nbsp;<", response); // text on page
assertResponseContains("/oddities/a%20file%20with%20a%20space", response); // generated link
assertResponseNotContains(extraResourceBaseString, response);
assertResponseNotContains(ODD_JAR, response);
}
@Test
public void testListingProperUrlEncoding() throws Exception
{

Binary file not shown.

View File

@ -45,9 +45,11 @@ import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import static java.nio.charset.StandardCharsets.UTF_8;
/* ------------------------------------------------------------ */
/**
/**
* Abstract resource class.
* <p>
* This class provides a resource abstraction, where a resource may be
@ -76,7 +78,7 @@ public abstract class Resource implements ResourceFactory, Closeable
{
return __defaultUseCaches;
}
/* ------------------------------------------------------------ */
/** Construct a resource from a uri.
* @param uri A URI.
@ -88,7 +90,7 @@ public abstract class Resource implements ResourceFactory, Closeable
{
return newResource(uri.toURL());
}
/* ------------------------------------------------------------ */
/** Construct a resource from a url.
* @param url A URL.
@ -98,8 +100,8 @@ public abstract class Resource implements ResourceFactory, Closeable
{
return newResource(url, __defaultUseCaches);
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/**
* Construct a resource from a url.
* @param url the url for which to make the resource
@ -137,8 +139,8 @@ public abstract class Resource implements ResourceFactory, Closeable
return new URLResource(url,null,useCaches);
}
/* ------------------------------------------------------------ */
/** Construct a resource from a string.
* @param resource A URL or filename.
@ -150,7 +152,7 @@ public abstract class Resource implements ResourceFactory, Closeable
{
return newResource(resource, __defaultUseCaches);
}
/* ------------------------------------------------------------ */
/** Construct a resource from a string.
* @param resource A URL or filename.
@ -158,7 +160,7 @@ public abstract class Resource implements ResourceFactory, Closeable
* @return A Resource object.
* @throws MalformedURLException Problem accessing URI
*/
public static Resource newResource(String resource, boolean useCaches)
public static Resource newResource(String resource, boolean useCaches)
throws MalformedURLException
{
URL url=null;
@ -208,7 +210,7 @@ public abstract class Resource implements ResourceFactory, Closeable
/** Construct a system resource from a string.
* The resource is tried as classloader resource before being
* treated as a normal resource.
* @param resource Resource as string representation
* @param resource Resource as string representation
* @return The new Resource
* @throws IOException Problem accessing resource.
*/
@ -245,17 +247,17 @@ public abstract class Resource implements ResourceFactory, Closeable
url=loader.getResource(resource.substring(1));
}
}
if (url==null)
{
url=ClassLoader.getSystemResource(resource);
if (url==null && resource.startsWith("/"))
url=ClassLoader.getSystemResource(resource.substring(1));
}
if (url==null)
return null;
return newResource(url);
}
@ -277,21 +279,21 @@ public abstract class Resource implements ResourceFactory, Closeable
* Unlike {@link ClassLoader#getSystemResource(String)} this method does not check for normal resources.
* @param name The relative name of the resource
* @param useCaches True if URL caches are to be used.
* @param checkParents True if forced searching of parent Classloaders is performed to work around
* @param checkParents True if forced searching of parent Classloaders is performed to work around
* loaders with inverted priorities
* @return Resource or null
*/
public static Resource newClassPathResource(String name,boolean useCaches,boolean checkParents)
{
URL url=Resource.class.getResource(name);
if (url==null)
url=Loader.getResource(Resource.class,name);
if (url==null)
return null;
return newResource(url,useCaches);
}
/* ------------------------------------------------------------ */
public static boolean isContainedIn (Resource r, Resource containingResource) throws MalformedURLException
{
@ -304,11 +306,11 @@ public abstract class Resource implements ResourceFactory, Closeable
{
close();
}
/* ------------------------------------------------------------ */
public abstract boolean isContainedIn (Resource r) throws MalformedURLException;
/* ------------------------------------------------------------ */
/** Release any temporary resources held by the resource.
* @deprecated use {@link #close()}
@ -329,7 +331,7 @@ public abstract class Resource implements ResourceFactory, Closeable
* @return true if the represented resource exists.
*/
public abstract boolean exists();
/* ------------------------------------------------------------ */
/**
@ -355,7 +357,7 @@ public abstract class Resource implements ResourceFactory, Closeable
* @return the length of the resource
*/
public abstract long length();
/* ------------------------------------------------------------ */
/**
@ -384,7 +386,7 @@ public abstract class Resource implements ResourceFactory, Closeable
throw new RuntimeException(e);
}
}
/* ------------------------------------------------------------ */
/**
@ -396,7 +398,7 @@ public abstract class Resource implements ResourceFactory, Closeable
*/
public abstract File getFile()
throws IOException;
/* ------------------------------------------------------------ */
/**
@ -405,7 +407,7 @@ public abstract class Resource implements ResourceFactory, Closeable
* @return the name of the resource
*/
public abstract String getName();
/* ------------------------------------------------------------ */
/**
@ -416,7 +418,7 @@ public abstract class Resource implements ResourceFactory, Closeable
*/
public abstract InputStream getInputStream()
throws IOException;
/* ------------------------------------------------------------ */
/**
* Readable ByteChannel for the resource.
@ -436,7 +438,7 @@ public abstract class Resource implements ResourceFactory, Closeable
*/
public abstract boolean delete()
throws SecurityException;
/* ------------------------------------------------------------ */
/**
* Rename the given resource
@ -446,7 +448,7 @@ public abstract class Resource implements ResourceFactory, Closeable
*/
public abstract boolean renameTo(Resource dest)
throws SecurityException;
/* ------------------------------------------------------------ */
/**
* list of resource names contained in the given resource.
@ -490,7 +492,7 @@ public abstract class Resource implements ResourceFactory, Closeable
}
/* ------------------------------------------------------------ */
/**
/**
* @param uri the uri to encode
* @return null (this is deprecated)
* @deprecated use {@link URIUtil} or {@link UrlEncoded} instead
@ -500,7 +502,7 @@ public abstract class Resource implements ResourceFactory, Closeable
{
return null;
}
/* ------------------------------------------------------------ */
// FIXME: this appears to not be used
@SuppressWarnings("javadoc")
@ -534,14 +536,16 @@ public abstract class Resource implements ResourceFactory, Closeable
{
return null;
}
/* ------------------------------------------------------------ */
/** Get the resource list as a HTML directory listing.
* @param base The base URL
* @param parent True if the parent directory should be included
* @return String of HTML
* @throws IOException if unable to get the list of resources as HTML
* @deprecated use {@link #getListHTML(String, boolean, String)} instead
*/
@Deprecated
public String getListHTML(String base, boolean parent) throws IOException
{
return getListHTML(base, parent, null);
@ -712,7 +716,7 @@ public abstract class Resource implements ResourceFactory, Closeable
buf.append("<tbody>\n");
String encodedBase = hrefEncodeURI(base);
if (parent)
{
// Name
@ -730,12 +734,12 @@ public abstract class Resource implements ResourceFactory, Closeable
DateFormat.MEDIUM);
for (Resource item: items)
{
String name = item.getName();
int slashIdx = name.lastIndexOf('/');
if (slashIdx != -1)
String name = item.getFileName();
if (StringUtil.isBlank(name))
{
name = name.substring(slashIdx + 1);
continue; // skip
}
if (item.isDirectory() && !name.endsWith("/"))
{
name += URIUtil.SLASH;
@ -752,13 +756,21 @@ public abstract class Resource implements ResourceFactory, Closeable
// Last Modified
buf.append("<td class=\"lastmodified\">");
buf.append(dfmt.format(new Date(item.lastModified())));
buf.append("</td>");
long lastModified = item.lastModified();
if (lastModified > 0)
{
buf.append(dfmt.format(new Date(item.lastModified())));
}
buf.append("&nbsp;</td>");
// Size
buf.append("<td class=\"size\">");
buf.append(String.format("%,d", item.length()));
buf.append(" bytes&nbsp;</td></tr>\n");
long length = item.length();
if (length >= 0)
{
buf.append(String.format("%,d bytes", item.length()));
}
buf.append("&nbsp;</td></tr>\n");
}
buf.append("</tbody>\n");
buf.append("</table>\n");
@ -766,7 +778,56 @@ public abstract class Resource implements ResourceFactory, Closeable
return buf.toString();
}
/**
* Get the raw (decoded if possible) Filename for this Resource.
* This is the last segment of the path.
* @return the raw / decoded filename for this resource
*/
private String getFileName()
{
try
{
// if a Resource supports File
File file = getFile();
if (file != null)
{
return file.getName();
}
}
catch (Throwable ignore)
{
}
// All others use raw getName
try
{
String rawName = getName(); // gets long name "/foo/bar/xxx"
int idx = rawName.lastIndexOf('/');
if (idx == rawName.length()-1)
{
// hit a tail slash, aka a name for a directory "/foo/bar/"
idx = rawName.lastIndexOf('/', idx-1);
}
String encodedFileName;
if (idx >= 0)
{
encodedFileName = rawName.substring(idx + 1);
}
else
{
encodedFileName = rawName; // entire name
}
return UrlEncoded.decodeString(encodedFileName, 0, encodedFileName.length(), UTF_8);
}
catch (Throwable ignore)
{
}
return null;
}
/**
* Encode any characters that could break the URI string in an HREF.
* Such as <a href="/path/to;<script>Window.alert("XSS"+'%20'+"here");</script>">Link</a>