diff options
author | Anna Koskinen <Ansku@users.noreply.github.com> | 2021-06-23 15:29:23 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-23 15:29:23 +0300 |
commit | f70bc4269c264214f0ab8ac637df877ded55bddf (patch) | |
tree | b68724842f1e9171b8e358f84c7853759d7c1df6 /server | |
parent | ba02350206ef25f6c29618f3cf7458f43543f3e8 (diff) | |
download | vaadin-framework-f70bc4269c264214f0ab8ac637df877ded55bddf.tar.gz vaadin-framework-f70bc4269c264214f0ab8ac637df877ded55bddf.zip |
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
Diffstat (limited to 'server')
4 files changed, 793 insertions, 9 deletions
diff --git a/server/src/main/java/com/vaadin/server/VaadinServlet.java b/server/src/main/java/com/vaadin/server/VaadinServlet.java index 58320ecb0a..3ddbd956e9 100644 --- a/server/src/main/java/com/vaadin/server/VaadinServlet.java +++ b/server/src/main/java/com/vaadin/server/VaadinServlet.java @@ -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 diff --git a/server/src/main/java/com/vaadin/server/VaadinServletService.java b/server/src/main/java/com/vaadin/server/VaadinServletService.java index efb613ceb6..d171562180 100644 --- a/server/src/main/java/com/vaadin/server/VaadinServletService.java +++ b/server/src/main/java/com/vaadin/server/VaadinServletService.java @@ -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(); diff --git a/server/src/test/java/com/vaadin/server/VaadinServletTest.java b/server/src/test/java/com/vaadin/server/VaadinServletTest.java index 652dc30665..006872b212 100644 --- a/server/src/test/java/com/vaadin/server/VaadinServletTest.java +++ b/server/src/test/java/com/vaadin/server/VaadinServletTest.java @@ -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; + } + } } diff --git a/server/src/test/java/com/vaadin/server/WarURLStreamHandlerFactory.java b/server/src/test/java/com/vaadin/server/WarURLStreamHandlerFactory.java new file mode 100644 index 0000000000..5197c1456e --- /dev/null +++ b/server/src/test/java/com/vaadin/server/WarURLStreamHandlerFactory.java @@ -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); + } + } +}
\ No newline at end of file |