diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java index 8b7923bb82..8dc3b442e4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java @@ -18,8 +18,8 @@ package org.springframework.boot.web.embedded.undertow; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; +import java.net.URLEncoder; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -33,6 +33,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; @@ -95,6 +96,8 @@ import org.springframework.util.Assert; public class UndertowServletWebServerFactory extends AbstractServletWebServerFactory implements ConfigurableUndertowWebServerFactory, ResourceLoaderAware { + private static final Pattern ENCODED_SLASH = Pattern.compile("%2F", Pattern.LITERAL); + private static final Set> NO_CLASSES = Collections.emptySet(); private Set builderCustomizers = new LinkedHashSet<>(); @@ -583,14 +586,15 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac private URLResource getMetaInfResource(URL resourceJar, String path) { try { - URL resourceUrl = new URL(resourceJar + "META-INF/resources" + path); + String urlPath = URLEncoder.encode(ENCODED_SLASH.matcher(path).replaceAll("/"), "UTF-8"); + URL resourceUrl = new URL(resourceJar + "META-INF/resources" + urlPath); URLResource resource = new URLResource(resourceUrl, path); if (resource.getContentLength() < 0) { return null; } return resource; } - catch (MalformedURLException ex) { + catch (Exception ex) { return null; } } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/ApplicationBuilder.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/ApplicationBuilder.java index 5ef76574cc..cb8c483998 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/ApplicationBuilder.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/ApplicationBuilder.java @@ -98,6 +98,10 @@ class ApplicationBuilder { resourcesJarStream.putNextEntry(new ZipEntry("META-INF/resources/nested-meta-inf-resource.txt")); resourcesJarStream.write("nested".getBytes()); resourcesJarStream.closeEntry(); + resourcesJarStream.putNextEntry( + new ZipEntry("META-INF/resources/nested-reserved-!#$%&()*+,:=?@[]-meta-inf-resource.txt")); + resourcesJarStream.write("encoded-name".getBytes()); + resourcesJarStream.closeEntry(); return resourcesJar; } } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarDevelopmentIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarDevelopmentIntegrationTests.java index 54174d20e4..3e08be3e0c 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarDevelopmentIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarDevelopmentIntegrationTests.java @@ -40,6 +40,16 @@ public class EmbeddedServletContainerJarDevelopmentIntegrationTests { assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } + @TestTemplate + public void metaInfResourceFromDependencyWithNameThatContainsReservedCharactersIsAvailableViaHttp( + RestTemplate rest) { + ResponseEntity entity = rest.getForEntity( + "/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("encoded-name"); + } + @TestTemplate public void metaInfResourceFromDependencyIsAvailableViaServletContext(RestTemplate rest) { ResponseEntity entity = rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt", diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java index 04b0c5f2b5..740bd2dd2c 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java @@ -40,6 +40,15 @@ public class EmbeddedServletContainerJarPackagingIntegrationTests { assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } + @TestTemplate + public void nestedMetaInfResourceWithNameThatContainsReservedCharactersIsAvailableViaHttp(RestTemplate rest) { + ResponseEntity entity = rest.getForEntity( + "/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("encoded-name"); + } + @TestTemplate public void nestedMetaInfResourceIsAvailableViaServletContext(RestTemplate rest) { ResponseEntity entity = rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt", diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarDevelopmentIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarDevelopmentIntegrationTests.java index 9b17e56394..14129dfcb4 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarDevelopmentIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarDevelopmentIntegrationTests.java @@ -47,6 +47,16 @@ public class EmbeddedServletContainerWarDevelopmentIntegrationTests { assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } + @TestTemplate + public void metaInfResourceFromDependencyWithNameThatContainsReservedCharactersIsAvailableViaHttp( + RestTemplate rest) { + ResponseEntity entity = rest.getForEntity( + "/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("encoded-name"); + } + @TestTemplate public void metaInfResourceFromDependencyIsAvailableViaServletContext(RestTemplate rest) { ResponseEntity entity = rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt", diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java index f700c4305a..bd7cc699f5 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java @@ -47,6 +47,15 @@ public class EmbeddedServletContainerWarPackagingIntegrationTests { assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } + @TestTemplate + public void nestedMetaInfResourceWithNameThatContainsReservedCharactersIsAvailableViaHttp(RestTemplate rest) { + ResponseEntity entity = rest.getForEntity( + "/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("encoded-name"); + } + @TestTemplate public void nestedMetaInfResourceIsAvailableViaServletContext(RestTemplate rest) { ResponseEntity entity = rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt",