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;
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.
}
// 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;
// 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);
/**
* 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
@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
}
}
+ 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"
/**
* 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
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("",
}
+ @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;
+ }
+ }
}
--- /dev/null
+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);
+ }
+ }
+}
\ No newline at end of file