Support loading of base64 encoded values as Resources

An ApplicationResourceLoader has been introduced to support loading
resources using registered ProtocolResolvers. All usages of
DefaultResourceLoader and ResourceUtils have been changed to use
the ApplicationResourceLoader.

A Base64ProtocolResolver has been added to support resources of type
`base64:` that contain base64 encoded values.

Closes gh-36033
This commit is contained in:
Scott Frederick 2024-03-20 10:33:35 -05:00 committed by Scott Frederick
parent 558d811b0a
commit 0962025c4b
20 changed files with 458 additions and 69 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2022-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -26,12 +26,16 @@ import java.util.List;
import java.util.stream.Collectors;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaCall;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClass.Predicates;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaParameter;
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
import com.tngtech.archunit.core.domain.properties.HasName;
import com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With;
import com.tngtech.archunit.core.domain.properties.HasParameterTypes;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
@ -58,11 +62,14 @@ import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.SkipWhenEmpty;
import org.gradle.api.tasks.TaskAction;
import org.springframework.util.ResourceUtils;
/**
* {@link Task} that checks for architecture problems.
*
* @author Andy Wilkinson
* @author Yanming Zhou
* @author Scott Frederick
*/
public abstract class ArchitectureCheck extends DefaultTask {
@ -75,7 +82,8 @@ public abstract class ArchitectureCheck extends DefaultTask {
allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters(),
noClassesShouldCallStepVerifierStepVerifyComplete(),
noClassesShouldConfigureDefaultStepVerifierTimeout(), noClassesShouldCallCollectorsToList(),
noClassesShouldCallURLEncoderWithStringEncoding(), noClassesShouldCallURLDecoderWithStringEncoding());
noClassesShouldCallURLEncoderWithStringEncoding(), noClassesShouldCallURLDecoderWithStringEncoding(),
noClassesShouldLoadResourcesUsingResourceUtils());
getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList()));
}
@ -208,6 +216,18 @@ public abstract class ArchitectureCheck extends DefaultTask {
.because("java.net.URLDecoder.decode(String s, Charset charset) should be used instead");
}
private ArchRule noClassesShouldLoadResourcesUsingResourceUtils() {
return ArchRuleDefinition.noClasses()
.should()
.callMethodWhere(JavaCall.Predicates.target(With.owner(Predicates.type(ResourceUtils.class)))
.and(JavaCall.Predicates.target(HasName.Predicates.name("getURL")))
.and(JavaCall.Predicates.target(HasParameterTypes.Predicates.rawParameterTypes(String.class)))
.or(JavaCall.Predicates.target(With.owner(Predicates.type(ResourceUtils.class)))
.and(JavaCall.Predicates.target(HasName.Predicates.name("getFile")))
.and(JavaCall.Predicates.target(HasParameterTypes.Predicates.rawParameterTypes(String.class)))))
.because("org.springframework.boot.io.ApplicationResourceLoader should be used instead");
}
public void setClasses(FileCollection classes) {
this.classes = classes;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* Tests for {@link ArchitectureCheck}.
*
* @author Andy Wilkinson
* @author Scott Frederick
*/
class ArchitectureCheckTests {
@ -121,6 +122,22 @@ class ArchitectureCheckTests {
});
}
@Test
void whenClassLoadsResourceUsingResourceUtilsTaskFailsAndWritesReport() throws Exception {
prepareTask("resources/loads", (architectureCheck) -> {
assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture);
assertThat(failureReport(architectureCheck)).isNotEmpty();
});
}
@Test
void whenClassUsesResourceUtilsWithoutLoadingResourcesTaskSucceedsAndWritesAnEmptyReport() throws Exception {
prepareTask("resources/noloads", (architectureCheck) -> {
architectureCheck.checkArchitecture();
assertThat(failureReport(architectureCheck)).isEmpty();
});
}
private void prepareTask(String classes, Callback<ArchitectureCheck> callback) throws Exception {
File projectDir = new File(this.temp, "project");
projectDir.mkdirs();

View File

@ -0,0 +1,29 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.architecture.resources.loads;
import java.io.FileNotFoundException;
import org.springframework.util.ResourceUtils;
public class ResourceUtilsResourceLoader {
void getResource() throws FileNotFoundException {
ResourceUtils.getURL("gradle.properties");
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.architecture.resources.noloads;
import java.net.MalformedURLException;
import java.net.URL;
import org.springframework.util.ResourceUtils;
public class ResourceUtilsWithoutLoading {
void inspectResourceLocation() throws MalformedURLException {
ResourceUtils.isUrl("gradle.properties");
ResourceUtils.isFileURL(new URL("gradle.properties"));
"test".startsWith(ResourceUtils.FILE_URL_PREFIX);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,13 +16,12 @@
package org.springframework.boot.autoconfigure.ssl;
import java.io.FileNotFoundException;
import java.net.URL;
import java.nio.file.Path;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.boot.ssl.pem.PemContent;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
/**
@ -57,9 +56,9 @@ record BundleContentProperty(String name, String value) {
private Path toPath() {
try {
URL url = toUrl();
Assert.state(isFileUrl(url), () -> "Value '%s' is not a file URL".formatted(url));
return Path.of(url.toURI()).toAbsolutePath();
Resource resource = getResource();
Assert.state(resource.isFile(), () -> "Value '%s' is not a file resource".formatted(this.value));
return Path.of(resource.getFile().getAbsolutePath());
}
catch (Exception ex) {
throw new IllegalStateException("Unable to convert value of property '%s' to a path".formatted(this.name),
@ -67,13 +66,10 @@ record BundleContentProperty(String name, String value) {
}
}
private URL toUrl() throws FileNotFoundException {
private Resource getResource() {
Assert.state(!isPemContent(), "Value contains PEM content");
return ResourceUtils.getURL(this.value);
}
private boolean isFileUrl(URL url) {
return "file".equalsIgnoreCase(url.getProtocol());
ApplicationResourceLoader resourceLoader = new ApplicationResourceLoader();
return resourceLoader.getResource(this.value);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,10 +16,11 @@
package org.springframework.boot.autoconfigure.ssl;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
@ -37,9 +38,6 @@ class BundleContentPropertyTests {
-----END CERTIFICATE-----
""";
@TempDir
Path temp;
@Test
void isPemContentWhenValueIsPemTextReturnsTrue() {
BundleContentProperty property = new BundleContentProperty("name", PEM_TEXT);
@ -78,8 +76,9 @@ class BundleContentPropertyTests {
}
@Test
void toWatchPathWhenPathReturnsPath() {
Path file = this.temp.toAbsolutePath().resolve("file.txt");
void toWatchPathWhenPathReturnsPath() throws URISyntaxException {
URL resource = getClass().getResource("keystore.jks");
Path file = Path.of(resource.toURI()).toAbsolutePath();
BundleContentProperty property = new BundleContentProperty("name", file.toString());
assertThat(property.toWatchPath()).isEqualTo(file);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,36 +19,26 @@ package org.springframework.boot.convert;
import java.io.File;
import java.io.IOException;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ResourceUtils;
/**
* {@link Converter} to convert from a {@link String} to a {@link File}. Supports basic
* file conversion as well as file URLs.
*
* @author Phillip Webb
* @author Scott Frederick
*/
class StringToFileConverter implements Converter<String, File> {
private static final ResourceLoader resourceLoader = new DefaultResourceLoader(null);
private static final ResourceLoader resourceLoader = new ApplicationResourceLoader();
@Override
public File convert(String source) {
if (ResourceUtils.isUrl(source)) {
return getFile(resourceLoader.getResource(source));
}
File file = new File(source);
if (file.exists()) {
return file;
}
Resource resource = resourceLoader.getResource(source);
if (resource.exists()) {
return getFile(resource);
}
return file;
return getFile(resource);
}
private File getFile(Resource resource) {

View File

@ -0,0 +1,73 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.io;
import org.springframework.core.io.ContextResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.ProtocolResolver;
import org.springframework.core.io.Resource;
/**
* A {@link DefaultResourceLoader} with any {@link ProtocolResolver}s registered in a
* {@code spring.factories} file applied to it. Plain paths without a qualifier will
* resolve to file system resources. This is different from {@code DefaultResourceLoader},
* which resolves unqualified paths to classpath resources.
*
* @author Scott Frederick
* @since 3.3.0
*/
public class ApplicationResourceLoader extends DefaultResourceLoader {
/**
* Create a new {@code ApplicationResourceLoader}.
*/
public ApplicationResourceLoader() {
super();
ProtocolResolvers.applyTo(this);
}
/**
* Create a new {@code ApplicationResourceLoader}.
* @param classLoader the {@link ClassLoader} to load class path resources with, or
* {@code null} for using the thread context class loader at the time of actual
* resource access
*/
public ApplicationResourceLoader(ClassLoader classLoader) {
super(classLoader);
ProtocolResolvers.applyTo(this);
}
@Override
protected Resource getResourceByPath(String path) {
return new FileSystemContextResource(path);
}
private static class FileSystemContextResource extends FileSystemResource implements ContextResource {
FileSystemContextResource(String path) {
super(path);
}
@Override
public String getPathWithinContext() {
return getPath();
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.io;
import java.util.Base64;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ProtocolResolver;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
/**
* {@link ProtocolResolver} for resources containing base 64 encoded text.
*
* @author Scott Frederick
*/
class Base64ProtocolResolver implements ProtocolResolver {
private static final String BASE64_PREFIX = "base64:";
@Override
public Resource resolve(String location, ResourceLoader resourceLoader) {
if (location.startsWith(BASE64_PREFIX)) {
return new Base64ByteArrayResource(location.substring(BASE64_PREFIX.length()));
}
return null;
}
static class Base64ByteArrayResource extends ByteArrayResource {
Base64ByteArrayResource(String location) {
super(Base64.getDecoder().decode(location.getBytes()));
}
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.io;
import java.util.List;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ProtocolResolver;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
/**
* {@link ProtocolResolver} implementations that are loaded from a
* {@code spring.factories} file.
*
* @author Scott Frederick
*/
final class ProtocolResolvers {
private ProtocolResolvers() {
}
static <T extends DefaultResourceLoader> void applyTo(T resourceLoader) {
Assert.notNull(resourceLoader, "ResourceLoader must not be null");
SpringFactoriesLoader loader = SpringFactoriesLoader
.forDefaultResourceLocation(resourceLoader.getClassLoader());
List<ProtocolResolver> resolvers = loader.load(ProtocolResolver.class);
resourceLoader.getProtocolResolvers().addAll(resolvers);
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Support for loading resources.
*/
package org.springframework.boot.io;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -28,6 +28,7 @@ import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.boot.logging.AbstractLoggingSystem;
import org.springframework.boot.logging.LogFile;
import org.springframework.boot.logging.LogLevel;
@ -37,10 +38,10 @@ import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.logging.LoggingSystemFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
/**
@ -102,8 +103,8 @@ public class JavaLoggingSystem extends AbstractLoggingSystem {
protected void loadConfiguration(String location, LogFile logFile) {
Assert.notNull(location, "Location must not be null");
try {
String configuration = FileCopyUtils
.copyToString(new InputStreamReader(ResourceUtils.getURL(location).openStream()));
Resource resource = new ApplicationResourceLoader().getResource(location);
String configuration = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream()));
if (logFile != null) {
configuration = configuration.replace("${LOG_FILE}", StringUtils.cleanPath(logFile.toString()));
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -55,6 +55,7 @@ import org.apache.logging.log4j.util.PropertiesUtil;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.boot.logging.AbstractLoggingSystem;
import org.springframework.boot.logging.LogFile;
import org.springframework.boot.logging.LogLevel;
@ -67,10 +68,10 @@ import org.springframework.core.Conventions;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
/**
@ -300,15 +301,16 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem {
}
private Configuration load(String location, LoggerContext context) throws IOException {
URL url = ResourceUtils.getURL(location);
ConfigurationSource source = getConfigurationSource(url);
Resource resource = new ApplicationResourceLoader().getResource(location);
ConfigurationSource source = getConfigurationSource(resource);
return ConfigurationFactory.getInstance().getConfiguration(context, source);
}
private ConfigurationSource getConfigurationSource(URL url) throws IOException {
if (FILE_PROTOCOL.equals(url.getProtocol())) {
return new ConfigurationSource(url.openStream(), ResourceUtils.getFile(url));
private ConfigurationSource getConfigurationSource(Resource resource) throws IOException {
if (resource.isFile()) {
return new ConfigurationSource(resource.getInputStream(), resource.getFile());
}
URL url = resource.getURL();
AuthorizationProvider authorizationProvider = ConfigurationFactory
.authorizationProvider(PropertiesUtil.getProperties());
SslConfiguration sslConfiguration = url.getProtocol().equals("https")

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -50,6 +50,7 @@ import org.springframework.aot.AotDetector;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.boot.logging.AbstractLoggingSystem;
import org.springframework.boot.logging.LogFile;
import org.springframework.boot.logging.LogLevel;
@ -62,9 +63,9 @@ import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
/**
@ -246,7 +247,8 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF
applySystemProperties(initializationContext.getEnvironment(), logFile);
}
try {
configureByResourceUrl(initializationContext, loggerContext, ResourceUtils.getURL(location));
Resource resource = new ApplicationResourceLoader().getResource(location);
configureByResourceUrl(initializationContext, loggerContext, resource.getURL());
}
catch (Exception ex) {
throw new IllegalStateException("Could not initialize Logback logging from " + location, ex);

View File

@ -18,17 +18,18 @@ package org.springframework.boot.ssl.jks;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
/**
@ -114,8 +115,9 @@ public class JksSslStoreBundle implements SslStoreBundle {
private void loadKeyStore(KeyStore store, String location, char[] password) {
Assert.state(StringUtils.hasText(location), () -> "Location must not be empty or null");
try {
URL url = ResourceUtils.getURL(location);
try (InputStream stream = url.openStream()) {
ResourceLoader resourceLoader = new ApplicationResourceLoader();
Resource resource = resourceLoader.getResource(location);
try (InputStream stream = resource.getInputStream()) {
store.load(stream, password);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,7 +19,6 @@ package org.springframework.boot.ssl.pem;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@ -30,8 +29,9 @@ import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StreamUtils;
/**
@ -119,7 +119,9 @@ public final class PemContent {
return new PemContent(content);
}
try {
return load(ResourceUtils.getURL(content));
ApplicationResourceLoader resourceLoader = new ApplicationResourceLoader();
Resource resource = resourceLoader.getResource(content);
return load(resource.getInputStream());
}
catch (IOException | UncheckedIOException ex) {
throw new IOException("Error reading certificate or key from file '%s'".formatted(content), ex);
@ -139,13 +141,6 @@ public final class PemContent {
}
}
private static PemContent load(URL url) throws IOException {
Assert.notNull(url, "Url must not be null");
try (InputStream in = url.openStream()) {
return load(in);
}
}
private static PemContent load(InputStream in) throws IOException {
return of(StreamUtils.copyToString(in, StandardCharsets.UTF_8));
}

View File

@ -101,3 +101,7 @@ org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitiali
org.springframework.boot.jdbc.SpringJdbcDependsOnDatabaseInitializationDetector,\
org.springframework.boot.jooq.JooqDependsOnDatabaseInitializationDetector,\
org.springframework.boot.orm.jpa.JpaDependsOnDatabaseInitializationDetector
# Resource Locator Protocol Resolvers
org.springframework.core.io.ProtocolResolver=\
org.springframework.boot.io.Base64ProtocolResolver

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -17,12 +17,14 @@
package org.springframework.boot.convert;
import java.io.File;
import java.io.IOException;
import java.util.stream.Stream;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.provider.Arguments;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
@ -30,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link StringToFileConverter}.
*
* @author Phillip Webb
* @author Scott Frederick
*/
class StringToFileConverterTests {
@ -48,6 +51,13 @@ class StringToFileConverterTests {
.isEqualTo(new File(this.temp, "test").getAbsoluteFile());
}
@ConversionServiceTest
void convertWhenClasspathPrefixedReturnsFile(ConversionService conversionService) throws IOException {
String resource = new ClassPathResource("test-banner.txt", this.getClass().getClassLoader()).getURL().getFile();
assertThat(convert(conversionService, "classpath:test-banner.txt").getAbsoluteFile())
.isEqualTo(new File(resource).getAbsoluteFile());
}
private File convert(ConversionService conversionService, String source) {
return conversionService.convert(source, File.class);
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.io;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link Base64ProtocolResolver}.
*
* @author Scott Frederick
*/
class Base64ProtocolResolverTests {
@Test
void base64LocationResolves() throws IOException {
String location = Base64.getEncoder().encodeToString("test value".getBytes());
Resource resource = new Base64ProtocolResolver().resolve("base64:" + location, new DefaultResourceLoader());
assertThat(resource).isNotNull();
assertThat(resource.getContentAsString(StandardCharsets.UTF_8)).isEqualTo("test value");
}
@Test
void base64LocationWithInvalidBase64ThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(
() -> new Base64ProtocolResolver().resolve("base64:not valid base64", new DefaultResourceLoader()))
.withMessageContaining("Illegal base64");
}
@Test
void locationWithoutPrefixDoesNotResolve() {
Resource resource = new Base64ProtocolResolver().resolve("file:notbase64.txt", new DefaultResourceLoader());
assertThat(resource).isNull();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,12 +16,17 @@
package org.springframework.boot.ssl.jks;
import java.io.IOException;
import java.nio.file.Files;
import java.security.KeyStore;
import java.util.Base64;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.boot.web.embedded.test.MockPkcs11Security;
import org.springframework.core.io.Resource;
import org.springframework.util.function.ThrowingConsumer;
import static org.assertj.core.api.Assertions.assertThat;
@ -122,6 +127,36 @@ class JksSslStoreBundleTests {
}).withMessageContaining("com.example.KeyStoreProvider");
}
@Test
void whenLocationsAreBase64Encoded() throws IOException {
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation(encodeFileContent("classpath:test.p12"))
.withPassword("secret");
JksSslStoreDetails trustStoreDetails = JksSslStoreDetails.forLocation(encodeFileContent("classpath:test.jks"))
.withPassword("secret");
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "secret"));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("test-alias", "password"));
}
@Test
void invalidBase64EncodedLocationThrowsException() {
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation("base64:not base 64");
assertThatIllegalStateException().isThrownBy(() -> new JksSslStoreBundle(keyStoreDetails, null))
.withMessageContaining("key store")
.withMessageContaining("base64:not base 64")
.havingRootCause()
.isInstanceOf(IllegalArgumentException.class)
.withMessageContaining("Illegal base64");
}
@Test
void invalidLocationThrowsException() {
JksSslStoreDetails trustStoreDetails = JksSslStoreDetails.forLocation("does-not-exist.p12");
assertThatIllegalStateException().isThrownBy(() -> new JksSslStoreBundle(null, trustStoreDetails))
.withMessageContaining("trust store")
.withMessageContaining("does-not-exist.p12");
}
private Consumer<KeyStore> storeContainingCertAndKey(String keyAlias, String keyPassword) {
return storeContainingCertAndKey(KeyStore.getDefaultType(), keyAlias, keyPassword);
}
@ -136,4 +171,10 @@ class JksSslStoreBundleTests {
});
}
private String encodeFileContent(String location) throws IOException {
Resource resource = new ApplicationResourceLoader().getResource(location);
byte[] bytes = Files.readAllBytes(resource.getFile().toPath());
return "base64:" + Base64.getEncoder().encodeToString(bytes);
}
}