Browse Source

fix: don't serve directories as static files (#12325)

Also prevents opening FileSystem for unknown schemes.

Modified cherry-picks of https://github.com/vaadin/flow/pull/11072 ,
https://github.com/vaadin/flow/pull/11147 , and
https://github.com/vaadin/flow/pull/11235
tags/8.14.0.alpha1
Anna Koskinen 2 years ago
parent
commit
f70bc4269c
No account linked to committer's email address

+ 144
- 4
server/src/main/java/com/vaadin/server/VaadinServlet.java View File

@@ -31,15 +31,22 @@ import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
@@ -190,6 +197,10 @@ public class VaadinServlet extends HttpServlet implements Constants {

private VaadinServletService servletService;

// Mapped uri is for the jar file
static final Map<URI, Integer> openFileSystems = new HashMap<>();
private static final Object fileSystemLock = new Object();

/**
* Called by the servlet container to indicate to a servlet that the servlet
* is being placed into service.
@@ -842,10 +853,12 @@ public class VaadinServlet extends HttpServlet implements Constants {
}

// security check: do not permit navigation out of the VAADIN
// directory
// directory or into any directory rather than a file
if (!isAllowedVAADINResourceUrl(request, resourceUrl)) {
getLogger().log(Level.INFO,
"Requested resource [{0}] not accessible in the VAADIN directory or access to it is forbidden.",
"Requested resource [{0}] is a directory, "
+ "is not within the VAADIN directory, "
+ "or access to it is forbidden.",
filename);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
@@ -1094,7 +1107,9 @@ public class VaadinServlet extends HttpServlet implements Constants {
// directory
if (!isAllowedVAADINResourceUrl(request, scssUrl)) {
getLogger().log(Level.INFO,
"Requested resource [{0}] not accessible in the VAADIN directory or access to it is forbidden.",
"Requested resource [{0}] is a directory, "
+ "is not within the VAADIN directory, "
+ "or access to it is forbidden.",
filename);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);

@@ -1203,7 +1218,8 @@ public class VaadinServlet extends HttpServlet implements Constants {

/**
* Check whether a URL obtained from a classloader refers to a valid static
* resource in the directory VAADIN.
* resource in the directory VAADIN. Directories do not count as valid
* resources.
*
* Warning: Overriding of this method is not recommended, but is possible to
* support non-default classloaders or servers that may produce URLs
@@ -1223,6 +1239,9 @@ public class VaadinServlet extends HttpServlet implements Constants {
@Deprecated
protected boolean isAllowedVAADINResourceUrl(HttpServletRequest request,
URL resourceUrl) {
if (resourceUrl == null || resourceIsDirectory(resourceUrl)) {
return false;
}
String resourcePath = resourceUrl.getPath();
if ("jar".equals(resourceUrl.getProtocol())) {
// This branch is used for accessing resources directly from the
@@ -1263,6 +1282,124 @@ public class VaadinServlet extends HttpServlet implements Constants {
}
}

private boolean resourceIsDirectory(URL resource) {
if (resource.getPath().endsWith("/")) {
return true;
}
URI resourceURI = null;
try {
resourceURI = resource.toURI();
} catch (URISyntaxException e) {
getLogger().log(Level.FINE,
"Syntax error in uri from getStaticResource", e);
// Return false as we couldn't determine if the resource is a
// directory.
return false;
}

if ("jar".equals(resource.getProtocol())) {
// Get the file path in jar
String pathInJar = resource.getPath()
.substring(resource.getPath().indexOf('!') + 1);
try {
FileSystem fileSystem = getFileSystem(resourceURI);
// Get the file path inside the jar.
Path path = fileSystem.getPath(pathInJar);

return Files.isDirectory(path);
} catch (IOException e) {
getLogger().log(Level.FINE, "failed to read jar file contents",
e);
} finally {
closeFileSystem(resourceURI);
}
}

// If not a jar check if a file path directory.
return "file".equals(resource.getProtocol())
&& Files.isDirectory(Paths.get(resourceURI));
}

/**
* Get the file URI for the resource jar file. Returns give URI if
* URI.scheme is not of type jar.
*
* The URI for a file inside a jar is composed as
* 'jar:file://...pathToJar.../jarFile.jar!/pathToFile'
*
* the first step strips away the initial scheme 'jar:' leaving us with
* 'file://...pathToJar.../jarFile.jar!/pathToFile' from which we remove the
* inside jar path giving the end result
* 'file://...pathToJar.../jarFile.jar'
*
* @param resourceURI
* resource URI to get file URI for
* @return file URI for resource jar or given resource if not a jar schemed
* URI
*/
private URI getFileURI(URI resourceURI) {
if (!"jar".equals(resourceURI.getScheme())) {
return resourceURI;
}
try {
String scheme = resourceURI.getRawSchemeSpecificPart();
int jarPartIndex = scheme.indexOf("!/");
if (jarPartIndex != -1) {
scheme = scheme.substring(0, jarPartIndex);
}
return new URI(scheme);
} catch (URISyntaxException syntaxException) {
throw new IllegalArgumentException(syntaxException.getMessage(),
syntaxException);
}
}

// Package protected for feature verification purpose
FileSystem getFileSystem(URI resourceURI) throws IOException {
synchronized (fileSystemLock) {
URI fileURI = getFileURI(resourceURI);
if (!fileURI.getScheme().equals("file")) {
throw new IOException("Can not read scheme '"
+ fileURI.getScheme() + "' for resource " + resourceURI
+ " and will determine this as not a folder");
}

Integer locks = openFileSystems.computeIfPresent(fileURI,
(key, value) -> value + 1);
if (locks != null) {
// Get filesystem is for the file to get the correct provider
return FileSystems.getFileSystem(resourceURI);
}
// Opened filesystem is for the file to get the correct provider
FileSystem fileSystem = FileSystems.newFileSystem(resourceURI,
Collections.emptyMap());
openFileSystems.put(fileURI, 1);
return fileSystem;
}
}

// Package protected for feature verification purpose
void closeFileSystem(URI resourceURI) {
synchronized (fileSystemLock) {
try {
URI fileURI = getFileURI(resourceURI);
Integer locks = openFileSystems.computeIfPresent(fileURI,
(key, value) -> value - 1);
if (locks != null && locks == 0) {
openFileSystems.remove(fileURI);
// Get filesystem is for the file to get the correct
// provider
FileSystems.getFileSystem(resourceURI).close();
}
} catch (IOException ioe) {
getLogger().log(Level.SEVERE,
"Failed to close FileSystem for '{}'", resourceURI);
getLogger().log(Level.INFO, "Exception closing FileSystem",
ioe);
}
}
}

/**
* Checks if the browser has an up to date cached version of requested
* resource. Currently the check is performed using the "If-Modified-Since"
@@ -1357,6 +1494,9 @@ public class VaadinServlet extends HttpServlet implements Constants {
/**
* Returns the relative path at which static files are served for a request
* (if any).
* <p>
* NOTE: This method does not check whether the requested resource is a
* directory and as such not a valid VAADIN resource.
*
* @param request
* HTTP request

+ 7
- 4
server/src/main/java/com/vaadin/server/VaadinServletService.java View File

@@ -54,7 +54,7 @@ public class VaadinServletService extends VaadinService {
* @since 8.2
*/
protected VaadinServletService() {
this.servlet = null;
servlet = null;
}

@Override
@@ -249,9 +249,12 @@ public class VaadinServletService extends VaadinService {
// security check: do not permit navigation out of the VAADIN
// directory
if (!getServlet().isAllowedVAADINResourceUrl(null, resourceUrl)) {
throw new IOException(String.format(
"Requested resource [{0}] not accessible in the VAADIN directory or access to it is forbidden.",
filename));
throw new IOException(
String.format(
"Requested resource [%s] is a directory, "
+ "is not within the VAADIN directory, "
+ "or access to it is forbidden.",
filename));
}

return resourceUrl.openStream();

+ 446
- 1
server/src/test/java/com/vaadin/server/VaadinServletTest.java View File

@@ -1,15 +1,54 @@
package com.vaadin.server;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;

/**
* Tests for {@link VaadinServlet}.
* <p>
* NOTE: some of these tests are not thread safe. Add
* {@code @net.jcip.annotations.NotThreadSafe} to this class if this module is
* converted to use {@code maven-failsafe-plugin} for parallelization.
*/
public class VaadinServletTest {

@After
public void tearDown() {
Assert.assertNull(VaadinService.getCurrent());
}

@Test
public void testGetLastPathParameter() {
assertEquals("",
@@ -111,13 +150,419 @@ public class VaadinServletTest {

}

@Test
@SuppressWarnings("deprecation")
public void directoryIsNotResourceRequest() throws Exception {
VaadinServlet servlet = new VaadinServlet();
servlet.init(new MockServletConfig());
// this request isn't actually used for anything within the
// isAllowedVAADINResourceUrl calls, no need to configure it
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);

TemporaryFolder folder = TemporaryFolder.builder().build();
folder.create();

try {
File vaadinFolder = folder.newFolder("VAADIN");
vaadinFolder.createNewFile();

// generate URL so it is not ending with / so that we test the
// correct method
String rootAbsolutePath = folder.getRoot().getAbsolutePath()
.replaceAll("\\\\", "/");
if (rootAbsolutePath.endsWith("/")) {
rootAbsolutePath = rootAbsolutePath.substring(0,
rootAbsolutePath.length() - 1);
}
URL folderPath = new URL("file:///" + rootAbsolutePath);

assertFalse("Folder on disk should not be an allowed resource.",
servlet.isAllowedVAADINResourceUrl(request, folderPath));

// Test any path ending with / to be seen as a directory
assertFalse(
"Fake should not check the file system nor be an allowed resource.",
servlet.isAllowedVAADINResourceUrl(request,
new URL("file:///fake/")));

File archiveFile = createJAR(folder);
Path tempArchive = archiveFile.toPath();
String tempArchivePath = tempArchive.toString().replaceAll("\\\\",
"/");

assertFalse(
"Folder 'VAADIN' in jar should not be an allowed resource.",
servlet.isAllowedVAADINResourceUrl(request, new URL(
"jar:file:///" + tempArchivePath + "!/VAADIN")));

assertFalse(
"File 'file.txt' inside jar should not be an allowed resource.",
servlet.isAllowedVAADINResourceUrl(request, new URL(
"jar:file:///" + tempArchivePath + "!/file.txt")));

assertTrue(
"File 'file.txt' inside VAADIN folder within jar should be an allowed resource.",
servlet.isAllowedVAADINResourceUrl(request,
new URL("jar:file:///" + tempArchivePath
+ "!/VAADIN/file.txt")));

assertFalse(
"Directory 'folder' inside VAADIN folder within jar should not be an allowed resource.",
servlet.isAllowedVAADINResourceUrl(request,
new URL("jar:file:///" + tempArchivePath
+ "!/VAADIN/folder")));

assertFalse(
"File 'file.txt' outside of a jar should not be an allowed resource.",
servlet.isAllowedVAADINResourceUrl(request, new URL(
"file:///" + rootAbsolutePath + "/file.txt")));

assertTrue(
"File 'file.txt' inside VAADIN folder outside of a jar should be an allowed resource.",
servlet.isAllowedVAADINResourceUrl(request,
new URL("file:///" + rootAbsolutePath
+ "/VAADIN/file.txt")));

} finally {
folder.delete();
}
}

@Test
@SuppressWarnings("deprecation")
public void isAllowedVAADINResource_jarWarFileScheme_detectsAsStaticResources()
throws IOException, URISyntaxException, ServletException {
assertTrue("Can not run concurrently with other test",
VaadinServlet.openFileSystems.isEmpty());

VaadinServlet servlet = new VaadinServlet();
servlet.init(new MockServletConfig());
// this request isn't actually used for anything within the
// isAllowedVAADINResourceUrl calls, no need to configure it
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);

TemporaryFolder folder = TemporaryFolder.builder().build();
folder.create();

try {
File archiveFile = createJAR(folder);
File warFile = createWAR(folder, archiveFile);

// Instantiate URL stream handler factory to be able to handle war:
WarURLStreamHandlerFactory.getInstance();

URL folderResourceURL = new URL("jar:war:" + warFile.toURI().toURL()
+ "!/" + archiveFile.getName() + "!/VAADIN/folder");

Assert.assertTrue(
"Should be evaluated as a static request because we cannot "
+ "determine non-file resources within jar files.",
servlet.isAllowedVAADINResourceUrl(request,
folderResourceURL));

URL fileResourceURL = new URL("jar:war:" + warFile.toURI().toURL()
+ "!/" + archiveFile.getName() + "!/VAADIN/file.txt");

Assert.assertTrue("Should be evaluated as a static request.",
servlet.isAllowedVAADINResourceUrl(request,
fileResourceURL));
} finally {
folder.delete();
}
}

@Test
@SuppressWarnings("deprecation")
public void isAllowedVAADINResource_jarInAJar_detectsAsStaticResources()
throws IOException, URISyntaxException, ServletException {
assertTrue("Can not run concurrently with other test",
VaadinServlet.openFileSystems.isEmpty());

VaadinServlet servlet = new VaadinServlet();
servlet.init(new MockServletConfig());
// this request isn't actually used for anything within the
// isAllowedVAADINResourceUrl calls, no need to configure it
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);

TemporaryFolder folder = TemporaryFolder.builder().build();
folder.create();

try {
File archiveFile = createJAR(folder);
File warFile = createWAR(folder, archiveFile);

URL folderResourceURL = new URL("jar:" + warFile.toURI().toURL()
+ "!/" + archiveFile.getName() + "!/VAADIN/folder");

Assert.assertTrue(
"Should be evaluated as a static request because we cannot "
+ "determine non-file resources within jar files.",
servlet.isAllowedVAADINResourceUrl(request,
folderResourceURL));

URL fileResourceURL = new URL("jar:" + warFile.toURI().toURL()
+ "!/" + archiveFile.getName() + "!/VAADIN/file.txt");

Assert.assertTrue("Should be evaluated as a static request.",
servlet.isAllowedVAADINResourceUrl(request,
fileResourceURL));

URL fileNonStaticResourceURL = new URL(
"jar:" + warFile.toURI().toURL() + "!/"
+ archiveFile.getName() + "!/file.txt");

Assert.assertFalse(
"Should not be evaluated as a static request even within a "
+ "jar because it's not within 'VAADIN' folder.",
servlet.isAllowedVAADINResourceUrl(request,
fileNonStaticResourceURL));
} finally {
folder.delete();
}
}

@Test
public void openingJarFileSystemForDifferentFilesInSameJar_existingFileSystemIsUsed()
throws IOException, URISyntaxException, ServletException {
assertTrue("Can not run concurrently with other test",
VaadinServlet.openFileSystems.isEmpty());

VaadinServlet servlet = new VaadinServlet();
servlet.init(new MockServletConfig());

TemporaryFolder folder = TemporaryFolder.builder().build();
folder.create();

try {
File archiveFile = createJAR(folder);
String tempArchivePath = archiveFile.toPath().toString()
.replaceAll("\\\\", "/");

URL folderResourceURL = new URL(
"jar:file:///" + tempArchivePath + "!/VAADIN");

URL fileResourceURL = new URL(
"jar:file:///" + tempArchivePath + "!/file.txt");

servlet.getFileSystem(folderResourceURL.toURI());
servlet.getFileSystem(fileResourceURL.toURI());

assertEquals("Same file should be marked for both resources",
(Integer) 2, VaadinServlet.openFileSystems.entrySet()
.iterator().next().getValue());
servlet.closeFileSystem(folderResourceURL.toURI());
assertEquals("Closing resource should be removed from jar uri",
(Integer) 1, VaadinServlet.openFileSystems.entrySet()
.iterator().next().getValue());
servlet.closeFileSystem(fileResourceURL.toURI());
assertTrue("Closing last resource should clear marking",
VaadinServlet.openFileSystems.isEmpty());

try {
FileSystems.getFileSystem(folderResourceURL.toURI());
fail("Jar FileSystem should have been closed");
} catch (FileSystemNotFoundException fsnfe) {
// This should happen as we should not have an open FileSystem
// here.
}
} finally {
folder.delete();
}
}

@Test
public void concurrentRequestsToJarResources_checksAreCorrect()
throws IOException, InterruptedException, ExecutionException,
URISyntaxException, ServletException {
assertTrue("Can not run concurrently with other test",
VaadinServlet.openFileSystems.isEmpty());

VaadinServlet servlet = new VaadinServlet();
servlet.init(new MockServletConfig());
// this request isn't actually used for anything within the
// isAllowedVAADINResourceUrl calls, no need to configure it
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);

TemporaryFolder folder = TemporaryFolder.builder().build();
folder.create();

try {
File archiveFile = createJAR(folder);
String tempArchivePath = archiveFile.toPath().toString()
.replaceAll("\\\\", "/");

URL fileNotResourceURL = new URL(
"jar:file:///" + tempArchivePath + "!/file.txt");
String fileNotResourceErrorMessage = "File file.text outside "
+ "folder 'VAADIN' in jar should not be a static resource.";

checkAllowedVAADINResourceConcurrently(servlet, request,
fileNotResourceURL, fileNotResourceErrorMessage, false);
ensureFileSystemsCleared(fileNotResourceURL);

URL folderNotResourceURL = new URL(
"jar:file:///" + tempArchivePath + "!/VAADIN");
String folderNotResourceErrorMessage = "Folder 'VAADIN' in "
+ "jar should not be a static resource.";

checkAllowedVAADINResourceConcurrently(servlet, request,
folderNotResourceURL, folderNotResourceErrorMessage, false);
ensureFileSystemsCleared(folderNotResourceURL);

URL fileIsResourceURL = new URL(
"jar:file:///" + tempArchivePath + "!/VAADIN/file.txt");
String fileIsResourceErrorMessage = "File 'file.txt' inside "
+ "VAADIN folder within jar should be a static resource.";

checkAllowedVAADINResourceConcurrently(servlet, request,
fileIsResourceURL, fileIsResourceErrorMessage, true);
ensureFileSystemsCleared(fileIsResourceURL);
} finally {
folder.delete();
}
}

private HttpServletRequest createServletRequest(String servletPath,
String pathInfo) {
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
Mockito.when(request.getServletPath()).thenReturn(servletPath);
Mockito.when(request.getPathInfo()).thenReturn(pathInfo);
Mockito.when(request.getRequestURI()).thenReturn("/context"+pathInfo);
Mockito.when(request.getRequestURI()).thenReturn("/context" + pathInfo);
Mockito.when(request.getContextPath()).thenReturn("/context");
return request;
}

/**
* Creates an archive file {@code fake.jar} that contains two
* {@code file.txt} files, one of which resides inside {@code VAADIN}
* directory.
*
* @param folder
* temporary folder that should house the archive file
* @return the archive file
* @throws IOException
*/
private File createJAR(TemporaryFolder folder) throws IOException {
File archiveFile = new File(folder.getRoot(), "fake.jar");
archiveFile.createNewFile();
Path tempArchive = archiveFile.toPath();

try (ZipOutputStream zipOutputStream = new ZipOutputStream(
Files.newOutputStream(tempArchive))) {
// Create a file to the zip
zipOutputStream.putNextEntry(new ZipEntry("/file.txt"));
zipOutputStream.closeEntry();
// Create a directory to the zip
zipOutputStream.putNextEntry(new ZipEntry("VAADIN/"));
zipOutputStream.closeEntry();
// Create a file to the directory
zipOutputStream.putNextEntry(new ZipEntry("VAADIN/file.txt"));
zipOutputStream.closeEntry();
// Create another directory to the zip
zipOutputStream.putNextEntry(new ZipEntry("VAADIN/folder/"));
zipOutputStream.closeEntry();
}
return archiveFile;
}

private File createWAR(TemporaryFolder folder, File archiveFile)
throws IOException {
Path tempArchive = archiveFile.toPath();
File warFile = new File(folder.getRoot(), "fake.war");
warFile.createNewFile();
Path warArchive = warFile.toPath();

try (ZipOutputStream warOutputStream = new ZipOutputStream(
Files.newOutputStream(warArchive))) {
// Create a file to the zip
warOutputStream.putNextEntry(new ZipEntry(archiveFile.getName()));
warOutputStream.write(Files.readAllBytes(tempArchive));

warOutputStream.closeEntry();
}
return warFile;
}

/**
* Performs the resource URL validity check in five threads simultaneously,
* and ensures that the results match the given expected value.
*
* @param servlet
* VaadinServlet instance
* @param request
* HttpServletRequest instance (does not need to be properly
* initialized)
* @param resourceURL
* the resource URL to validate
* @param resourceErrorMessage
* the error message if the validity check results don't match
* the expected value
* @param expected
* expected value from the validity check
*
* @throws InterruptedException
* @throws ExecutionException
*/
@SuppressWarnings("deprecation")
private void checkAllowedVAADINResourceConcurrently(VaadinServlet servlet,
HttpServletRequest request, URL resourceURL,
String resourceErrorMessage, boolean expected)
throws InterruptedException, ExecutionException {
int THREADS = 5;

List<Callable<Result>> fileNotResource = IntStream.range(0, THREADS)
.mapToObj(i -> {
Callable<Result> callable = () -> {
try {
if (expected != servlet.isAllowedVAADINResourceUrl(
request, resourceURL)) {
throw new IllegalArgumentException(
resourceErrorMessage);
}
} catch (Exception e) {
return new Result(e);
}
return new Result(null);
};
return callable;
}).collect(Collectors.toList());

ExecutorService executor = Executors.newFixedThreadPool(THREADS);
List<Future<Result>> futures = executor.invokeAll(fileNotResource);
List<String> exceptions = new ArrayList<>();

executor.shutdown();

for (Future<Result> resultFuture : futures) {
Result result = resultFuture.get();
if (result.exception != null) {
exceptions.add(result.exception.getMessage());
}
}

assertTrue("There were exceptions in concurrent calls {" + exceptions
+ "}", exceptions.isEmpty());
}

private void ensureFileSystemsCleared(URL fileResourceURL)
throws URISyntaxException {
assertFalse("URI should have been cleared",
VaadinServlet.openFileSystems
.containsKey(fileResourceURL.toURI()));
try {
FileSystems.getFileSystem(fileResourceURL.toURI());
fail("FileSystem for file resource should be closed");
} catch (FileSystemNotFoundException fsnfe) {
// This should happen as we should not have an open FileSystem
// here.
}
}

private static class Result {
final Exception exception;

Result(Exception exception) {
this.exception = exception;
}
}
}

+ 196
- 0
server/src/test/java/com/vaadin/server/WarURLStreamHandlerFactory.java View File

@@ -0,0 +1,196 @@
package com.vaadin.server;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.security.Permission;

/**
* Test factory for URL stream protocol handlers, needed for WAR handling in
* {@link VaadinServletTest}. Cherry-picked from Flow, some of the
* implementation details are not needed for Vaadin 8 at the moment, but they
* are left in because they aren't interfering either.
*/
public class WarURLStreamHandlerFactory
implements URLStreamHandlerFactory, Serializable {

private static final String WAR_PROTOCOL = "war";

// Singleton instance
private static volatile WarURLStreamHandlerFactory instance = null;

private final boolean registered;

/**
* Obtain a reference to the singleton instance. It is recommended that
* callers check the value of {@link #isRegistered()} before using the
* returned instance.
*
* @return A reference to the singleton instance
*/
public static WarURLStreamHandlerFactory getInstance() {
getInstanceInternal(true);
return instance;
}

private static WarURLStreamHandlerFactory getInstanceInternal(
boolean register) {
// Double checked locking. OK because instance is volatile.
if (instance == null) {
synchronized (WarURLStreamHandlerFactory.class) {
if (instance == null) {
instance = new WarURLStreamHandlerFactory(register);
}
}
}
return instance;
}

private WarURLStreamHandlerFactory(boolean register) {
// Hide default constructor
// Singleton pattern to ensure there is only one instance of this
// factory
registered = register;
if (register) {
URL.setURLStreamHandlerFactory(this);
}
}

public boolean isRegistered() {
return registered;
}

/**
* Register this factory with the JVM. May be called more than once. The
* implementation ensures that registration only occurs once.
*
* @return <code>true</code> if the factory is already registered with the
* JVM or was successfully registered as a result of this call.
* <code>false</code> if the factory was disabled prior to this
* call.
*/
public static boolean register() {
return getInstanceInternal(true).isRegistered();
}

/**
* Prevent this this factory from registering with the JVM. May be called
* more than once.
*
* @return <code>true</code> if the factory is already disabled or was
* successfully disabled as a result of this call.
* <code>false</code> if the factory was already registered prior to
* this call.
*/
public static boolean disable() {
return !getInstanceInternal(false).isRegistered();
}

@Override
public URLStreamHandler createURLStreamHandler(String protocol) {

// Tomcat's handler always takes priority so applications can't override
// it.
if (WAR_PROTOCOL.equals(protocol)) {
return new WarHandler();
}

// Unknown protocol
return null;
}

public static class WarHandler extends URLStreamHandler
implements Serializable {

@Override
protected URLConnection openConnection(URL u) throws IOException {
return new WarURLConnection(u);
}

@Override
protected void setURL(URL u, String protocol, String host, int port,
String authority, String userInfo, String path, String query,
String ref) {
if (path.startsWith("file:") && !path.startsWith("file:/")) {
/*
* Work around a problem with the URLs in the security policy
* file. On Windows, the use of ${catalina.[home|base]} in the
* policy file results in codebase URLs of the form file:C:/...
* when they should be file:/C:/...
*
* For file: and jar: URLs, the JRE compensates for this. It
* does not compensate for this for war:file:... URLs.
* Therefore, we do that here
*/
path = "file:/" + path.substring(5);
}
super.setURL(u, protocol, host, port, authority, userInfo, path,
query, ref);
}

}

public static class WarURLConnection extends URLConnection
implements Serializable {

private final URLConnection wrappedJarUrlConnection;
private boolean connected;

protected WarURLConnection(URL url) throws IOException {
super(url);
URL innerJarUrl = warToJar(url);
wrappedJarUrlConnection = innerJarUrl.openConnection();
}

@Override
public void connect() throws IOException {
if (!connected) {
wrappedJarUrlConnection.connect();
connected = true;
}
}

@Override
public InputStream getInputStream() throws IOException {
connect();
return wrappedJarUrlConnection.getInputStream();
}

@Override
public Permission getPermission() throws IOException {
return wrappedJarUrlConnection.getPermission();
}

@Override
public long getLastModified() {
return wrappedJarUrlConnection.getLastModified();
}

@Override
public int getContentLength() {
return wrappedJarUrlConnection.getContentLength();
}

@Override
public long getContentLengthLong() {
return wrappedJarUrlConnection.getContentLengthLong();
}

public static URL warToJar(URL warUrl) throws MalformedURLException {
// Assumes that the spec is absolute and starts war:file:/...
String file = warUrl.getFile();
if (file.contains("*/")) {
file = file.replaceFirst("\\*/", "!/");
} else if (file.contains("^/")) {
file = file.replaceFirst("\\^/", "!/");
}

return new URL("jar", warUrl.getHost(), warUrl.getPort(), file);
}
}
}

Loading…
Cancel
Save