Skip to content

Commit

Permalink
Prefer file resolution when loading SSL content
Browse files Browse the repository at this point in the history
Update `SslAutoConfiguration` so that the used resource loader prefers
file based resolution when paths are specified without a prefix. This
restores the behavior found in Spring Boot 3.3.

The `ApplicationResourceLoader` has been updated with a new `get` method
that accepts a `preferFileResolution` parameter. Unfortunately, we can't
directly influence the resource returned by the delegate
`ResourceLoader` since we can't override `getResourceByPath(...)`.
Instead we check if the returned type was likely to have been created
by a call to that method. If so, we change it to a `FileSystemResource`.

This approach should hopefully work with `DefaultResourceLoader` and
subclasses.

Fixes spring-projectsgh-43274
  • Loading branch information
philwebb committed Dec 3, 2024
1 parent 7a4e071 commit 3ddfd62
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class SslAutoConfiguration {
private final SslProperties sslProperties;

SslAutoConfiguration(ResourceLoader resourceLoader, SslProperties sslProperties) {
this.resourceLoader = ApplicationResourceLoader.get(resourceLoader);
this.resourceLoader = ApplicationResourceLoader.get(resourceLoader, true);
this.sslProperties = sslProperties;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,24 @@ void sslBundlesCreatedWithCustomSslBundle() {
});
}

@Test
void sslBundleWithoutClassPathPrefix() {
List<String> propertyValues = new ArrayList<>();
String location = "src/test/resources/org/springframework/boot/autoconfigure/ssl/";
propertyValues.add("spring.ssl.bundle.pem.test.key.alias=alias1");
propertyValues.add("spring.ssl.bundle.pem.test.key.password=secret1");
propertyValues.add("spring.ssl.bundle.pem.test.keystore.certificate=" + location + "rsa-cert.pem");
propertyValues.add("spring.ssl.bundle.pem.test.keystore.keystore.private-key=" + location + "rsa-key.pem");
propertyValues.add("spring.ssl.bundle.pem.test.truststore.certificate=" + location + "rsa-cert.pem");
this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> {
assertThat(context).hasSingleBean(SslBundles.class);
SslBundles bundles = context.getBean(SslBundles.class);
SslBundle bundle = bundles.getBundle("test");
assertThat(bundle.getStores().getKeyStore().getCertificate("alias1")).isNotNull();
assertThat(bundle.getStores().getTrustStore().getCertificate("ssl")).isNotNull();
});
}

@Configuration
@EnableConfigurationProperties(CustomSslProperties.class)
public static class CustomSslBundleConfiguration {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.util.List;

import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.ContextResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.FileSystemResource;
Expand All @@ -26,6 +27,7 @@
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

/**
Expand Down Expand Up @@ -120,8 +122,25 @@ public static ResourceLoader get(ClassLoader classLoader, SpringFactoriesLoader
* @since 3.4.0
*/
public static ResourceLoader get(ResourceLoader resourceLoader) {
return get(resourceLoader, false);
}

/**
* Return a {@link ResourceLoader} delegating to the given resource loader and
* supporting additional {@link ProtocolResolver ProtocolResolvers} registered in
* {@code spring.factories}. The factories file will be resolved using the default
* class loader at the time this call is made.
* @param resourceLoader the delegate resource loader
* @param preferFileResolution if file based resolution is preferred over
* {@code ServletContextResource} or {@link ClassPathResource} when no resource prefix
* is provided.
* @return a {@link ResourceLoader} instance
* @since 3.4.1
*/
public static ResourceLoader get(ResourceLoader resourceLoader, boolean preferFileResolution) {
Assert.notNull(resourceLoader, "'resourceLoader' must not be null");
return get(resourceLoader, SpringFactoriesLoader.forDefaultResourceLocation(resourceLoader.getClassLoader()));
return get(resourceLoader, SpringFactoriesLoader.forDefaultResourceLocation(resourceLoader.getClassLoader()),
preferFileResolution);
}

/**
Expand All @@ -135,9 +154,15 @@ public static ResourceLoader get(ResourceLoader resourceLoader) {
* @since 3.4.0
*/
public static ResourceLoader get(ResourceLoader resourceLoader, SpringFactoriesLoader springFactoriesLoader) {
return get(resourceLoader, springFactoriesLoader, false);
}

private static ResourceLoader get(ResourceLoader resourceLoader, SpringFactoriesLoader springFactoriesLoader,
boolean preferFileResolution) {
Assert.notNull(resourceLoader, "'resourceLoader' must not be null");
Assert.notNull(springFactoriesLoader, "'springFactoriesLoader' must not be null");
return new ProtocolResolvingResourceLoader(resourceLoader, springFactoriesLoader.load(ProtocolResolver.class));
return new ProtocolResolvingResourceLoader(resourceLoader, springFactoriesLoader.load(ProtocolResolver.class),
preferFileResolution);
}

/**
Expand Down Expand Up @@ -185,13 +210,30 @@ public String getPathWithinContext() {
*/
private static class ProtocolResolvingResourceLoader implements ResourceLoader {

private static final String SERVLET_CONTEXT_RESOURCE_CLASS_NAME = "org.springframework.web.context.support.ServletContextResource";

private final ResourceLoader resourceLoader;

private final List<ProtocolResolver> protocolResolvers;

ProtocolResolvingResourceLoader(ResourceLoader resourceLoader, List<ProtocolResolver> protocolResolvers) {
private final boolean preferFileResolution;

private Class<?> servletContextResourceClass;

ProtocolResolvingResourceLoader(ResourceLoader resourceLoader, List<ProtocolResolver> protocolResolvers,
boolean preferFileResolution) {
this.resourceLoader = resourceLoader;
this.protocolResolvers = protocolResolvers;
this.preferFileResolution = preferFileResolution;
this.servletContextResourceClass = resolveServletContextResourceClass(
resourceLoader.getClass().getClassLoader());
}

private static Class<?> resolveServletContextResourceClass(ClassLoader classLoader) {
if (!ClassUtils.isPresent(SERVLET_CONTEXT_RESOURCE_CLASS_NAME, classLoader)) {
return null;
}
return ClassUtils.resolveClassName(SERVLET_CONTEXT_RESOURCE_CLASS_NAME, classLoader);
}

@Override
Expand All @@ -204,7 +246,20 @@ public Resource getResource(String location) {
}
}
}
return this.resourceLoader.getResource(location);
Resource resource = this.resourceLoader.getResource(location);
if (this.preferFileResolution
&& (isClassPathResourceByPath(location, resource) || isServletResource(resource))) {
resource = new ApplicationResource(location);
}
return resource;
}

private boolean isClassPathResourceByPath(String location, Resource resource) {
return (resource instanceof ClassPathResource) && !location.startsWith(CLASSPATH_URL_PREFIX);
}

private boolean isServletResource(Resource resource) {
return this.servletContextResourceClass != null && this.servletContextResourceClass.isInstance(resource);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,19 @@
import java.util.Enumeration;
import java.util.function.UnaryOperator;

import jakarta.servlet.ServletContext;
import org.junit.jupiter.api.Test;

import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.context.support.ServletContextResourceLoader;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
Expand Down Expand Up @@ -152,6 +158,48 @@ void getResourceWhenPathIsNull() {
.withMessage("Location must not be null");
}

@Test
void getResourceWithPreferFileResolutionWhenFullPathWithClassPathResource() throws Exception {
File file = new File("src/main/resources/a-file");
ResourceLoader loader = ApplicationResourceLoader.get(new DefaultResourceLoader(), true);
Resource resource = loader.getResource(file.getAbsolutePath());
assertThat(resource).isInstanceOf(FileSystemResource.class);
assertThat(resource.getFile().getAbsoluteFile()).isEqualTo(file.getAbsoluteFile());
ResourceLoader regularLoader = ApplicationResourceLoader.get(new DefaultResourceLoader(), false);
assertThat(regularLoader.getResource(file.getAbsolutePath())).isInstanceOf(ClassPathResource.class);
}

@Test
void getResourceWithPreferFileResolutionWhenRelativePathWithClassPathResource() throws Exception {
ResourceLoader loader = ApplicationResourceLoader.get(new DefaultResourceLoader(), true);
Resource resource = loader.getResource("src/main/resources/a-file");
assertThat(resource).isInstanceOf(FileSystemResource.class);
assertThat(resource.getFile().getAbsoluteFile())
.isEqualTo(new File("src/main/resources/a-file").getAbsoluteFile());
ResourceLoader regularLoader = ApplicationResourceLoader.get(new DefaultResourceLoader(), false);
assertThat(regularLoader.getResource("src/main/resources/a-file")).isInstanceOf(ClassPathResource.class);
}

@Test
void getResourceWithPreferFileResolutionWhenExplicitClassPathPrefix() {
ResourceLoader loader = ApplicationResourceLoader.get(new DefaultResourceLoader(), true);
Resource resource = loader.getResource("classpath:a-file");
assertThat(resource).isInstanceOf(ClassPathResource.class);
}

@Test
void getResourceWithPreferFileResolutionWhenPathWithServletContextResource() throws Exception {
ServletContext servletContext = new MockServletContext();
ServletContextResourceLoader servletContextResourceLoader = new ServletContextResourceLoader(servletContext);
ResourceLoader loader = ApplicationResourceLoader.get(servletContextResourceLoader, true);
Resource resource = loader.getResource("src/main/resources/a-file");
assertThat(resource).isInstanceOf(FileSystemResource.class);
assertThat(resource.getFile().getAbsoluteFile())
.isEqualTo(new File("src/main/resources/a-file").getAbsoluteFile());
ResourceLoader regularLoader = ApplicationResourceLoader.get(servletContextResourceLoader, false);
assertThat(regularLoader.getResource("src/main/resources/a-file")).isInstanceOf(ServletContextResource.class);
}

@Test
void getClassLoaderReturnsDelegateClassLoader() {
ClassLoader classLoader = new TestClassLoader(this::useTestProtocolResolversFactories);
Expand Down

0 comments on commit 3ddfd62

Please sign in to comment.