Skip to content

Commit 5ab3414

Browse files
Support loading of base64 encoded values from the Environment
1 parent 8c708b1 commit 5ab3414

File tree

12 files changed

+364
-39
lines changed

12 files changed

+364
-39
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java

+9-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,13 +16,12 @@
1616

1717
package org.springframework.boot.autoconfigure.ssl;
1818

19-
import java.io.FileNotFoundException;
20-
import java.net.URL;
2119
import java.nio.file.Path;
2220

21+
import org.springframework.boot.io.ApplicationResourceLoader;
2322
import org.springframework.boot.ssl.pem.PemContent;
23+
import org.springframework.core.io.Resource;
2424
import org.springframework.util.Assert;
25-
import org.springframework.util.ResourceUtils;
2625
import org.springframework.util.StringUtils;
2726

2827
/**
@@ -57,23 +56,20 @@ Path toWatchPath() {
5756

5857
private Path toPath() {
5958
try {
60-
URL url = toUrl();
61-
Assert.state(isFileUrl(url), () -> "Value '%s' is not a file URL".formatted(url));
62-
return Path.of(url.toURI()).toAbsolutePath();
59+
Resource resource = getResource();
60+
Assert.state(resource.isFile(), () -> "Value '%s' is not a file resource".formatted(this.value));
61+
return Path.of(resource.getFile().getAbsolutePath());
6362
}
6463
catch (Exception ex) {
6564
throw new IllegalStateException("Unable to convert value of property '%s' to a path".formatted(this.name),
6665
ex);
6766
}
6867
}
6968

70-
private URL toUrl() throws FileNotFoundException {
69+
private Resource getResource() {
7170
Assert.state(!isPemContent(), "Value contains PEM content");
72-
return ResourceUtils.getURL(this.value);
73-
}
74-
75-
private boolean isFileUrl(URL url) {
76-
return "file".equalsIgnoreCase(url.getProtocol());
71+
ApplicationResourceLoader resourceLoader = new ApplicationResourceLoader();
72+
return resourceLoader.getResource(this.value);
7773
}
7874

7975
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/JksSslBundleProperties.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -62,7 +62,7 @@ public static class Store {
6262
private String provider;
6363

6464
/**
65-
* Location of the resource containing the store content.
65+
* Resource containing the store content.
6666
*/
6767
private String location;
6868

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,10 +16,11 @@
1616

1717
package org.springframework.boot.autoconfigure.ssl;
1818

19+
import java.net.URISyntaxException;
20+
import java.net.URL;
1921
import java.nio.file.Path;
2022

2123
import org.junit.jupiter.api.Test;
22-
import org.junit.jupiter.api.io.TempDir;
2324

2425
import static org.assertj.core.api.Assertions.assertThat;
2526
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
@@ -37,9 +38,6 @@ class BundleContentPropertyTests {
3738
-----END CERTIFICATE-----
3839
""";
3940

40-
@TempDir
41-
Path temp;
42-
4341
@Test
4442
void isPemContentWhenValueIsPemTextReturnsTrue() {
4543
BundleContentProperty property = new BundleContentProperty("name", PEM_TEXT);
@@ -78,8 +76,9 @@ void toWatchPathWhenNotPathThrowsException() {
7876
}
7977

8078
@Test
81-
void toWatchPathWhenPathReturnsPath() {
82-
Path file = this.temp.toAbsolutePath().resolve("file.txt");
79+
void toWatchPathWhenPathReturnsPath() throws URISyntaxException {
80+
URL resource = getClass().getResource("keystore.jks");
81+
Path file = Path.of(resource.toURI()).toAbsolutePath();
8382
BundleContentProperty property = new BundleContentProperty("name", file.toString());
8483
assertThat(property.toWatchPath()).isEqualTo(file);
8584
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.io;
18+
19+
import org.springframework.core.io.ContextResource;
20+
import org.springframework.core.io.DefaultResourceLoader;
21+
import org.springframework.core.io.FileSystemResource;
22+
import org.springframework.core.io.ProtocolResolver;
23+
import org.springframework.core.io.Resource;
24+
import org.springframework.util.ResourceUtils;
25+
26+
/**
27+
* A {@link DefaultResourceLoader} with any {@link ProtocolResolver}s registered in a
28+
* {@code spring.factories} file applied to it. Plain paths without a qualifier will
29+
* resolve to file system resources. This matches the behavior of
30+
* {@link ResourceUtils#getURL(String)} but is different from
31+
* {@code DefaultResourceLoader}, which resolves unqualified paths to classpath resources.
32+
*
33+
* @author Scott Frederick
34+
* @since 3.3.0
35+
*/
36+
public class ApplicationResourceLoader extends DefaultResourceLoader {
37+
38+
/**
39+
* Create a new {@code ApplicationResourceLoader}.
40+
*/
41+
public ApplicationResourceLoader() {
42+
super();
43+
ProtocolResolvers.applyTo(this);
44+
}
45+
46+
/**
47+
* Create a new {@code ApplicationResourceLoader}.
48+
* @param classLoader the {@link ClassLoader} to load class path resources with, or
49+
* {@code null} for using the thread context class loader at the time of actual
50+
* resource access
51+
*/
52+
public ApplicationResourceLoader(ClassLoader classLoader) {
53+
super(classLoader);
54+
ProtocolResolvers.applyTo(this);
55+
}
56+
57+
@Override
58+
protected Resource getResourceByPath(String path) {
59+
return new FileSystemContextResource(path);
60+
}
61+
62+
private static class FileSystemContextResource extends FileSystemResource implements ContextResource {
63+
64+
FileSystemContextResource(String path) {
65+
super(path);
66+
}
67+
68+
@Override
69+
public String getPathWithinContext() {
70+
return getPath();
71+
}
72+
73+
}
74+
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.io;
18+
19+
import java.util.Base64;
20+
21+
import org.springframework.core.io.ByteArrayResource;
22+
import org.springframework.core.io.ProtocolResolver;
23+
import org.springframework.core.io.Resource;
24+
import org.springframework.core.io.ResourceLoader;
25+
26+
/**
27+
* {@link ProtocolResolver} for resources containing base 64 encoded text.
28+
*
29+
* @author Scott Frederick
30+
*/
31+
class Base64ProtocolResolver implements ProtocolResolver {
32+
33+
private static final String BASE64_PREFIX = "base64:";
34+
35+
@Override
36+
public Resource resolve(String location, ResourceLoader resourceLoader) {
37+
if (location.startsWith(BASE64_PREFIX)) {
38+
return new Base64ByteArrayResource(location.substring(BASE64_PREFIX.length()));
39+
}
40+
return null;
41+
}
42+
43+
static class Base64ByteArrayResource extends ByteArrayResource {
44+
45+
Base64ByteArrayResource(String location) {
46+
super(Base64.getDecoder().decode(location.getBytes()));
47+
}
48+
49+
}
50+
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.io;
18+
19+
import java.util.List;
20+
21+
import org.springframework.beans.factory.BeanFactory;
22+
import org.springframework.boot.context.event.ApplicationPreparedEvent;
23+
import org.springframework.context.ApplicationListener;
24+
import org.springframework.context.ConfigurableApplicationContext;
25+
import org.springframework.core.env.Environment;
26+
import org.springframework.core.io.DefaultResourceLoader;
27+
import org.springframework.core.io.ProtocolResolver;
28+
import org.springframework.core.io.support.SpringFactoriesLoader;
29+
import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver;
30+
import org.springframework.util.Assert;
31+
32+
/**
33+
* {@link ProtocolResolver} implementations that are loaded from a
34+
* {@code spring.factories} file.
35+
*
36+
* @author Scott Frederick
37+
*/
38+
final class ProtocolResolvers {
39+
40+
private static List<ProtocolResolver> protocolResolvers;
41+
42+
private ProtocolResolvers() {
43+
}
44+
45+
/**
46+
* Apply all discovered {@link ProtocolResolver}s to the provided
47+
* {@link DefaultResourceLoader}.
48+
* @param resourceLoader the resource loader to apply protocol resolvers to
49+
* @param <T> a resource loader
50+
*/
51+
static <T extends DefaultResourceLoader> void applyTo(T resourceLoader) {
52+
Assert.notNull(resourceLoader, "ResourceLoader must not be null");
53+
if (protocolResolvers != null) {
54+
resourceLoader.getProtocolResolvers().addAll(protocolResolvers);
55+
}
56+
}
57+
58+
private static void initialize(ConfigurableApplicationContext context) {
59+
ArgumentResolver argumentResolver = getArgumentResolver(context);
60+
SpringFactoriesLoader loader = SpringFactoriesLoader
61+
.forDefaultResourceLocation((context != null) ? context.getClassLoader() : null);
62+
protocolResolvers = loader.load(ProtocolResolver.class, argumentResolver);
63+
}
64+
65+
private static ArgumentResolver getArgumentResolver(ConfigurableApplicationContext context) {
66+
if (context == null) {
67+
return null;
68+
}
69+
return ArgumentResolver.of(BeanFactory.class, context.getBeanFactory())
70+
.and(Environment.class, context.getEnvironment());
71+
}
72+
73+
static class ProtocolResolversInitializer implements ApplicationListener<ApplicationPreparedEvent> {
74+
75+
@Override
76+
public void onApplicationEvent(ApplicationPreparedEvent event) {
77+
ConfigurableApplicationContext context = event.getApplicationContext();
78+
ProtocolResolvers.initialize(context);
79+
}
80+
81+
}
82+
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Support for loading resources.
19+
*/
20+
package org.springframework.boot.io;

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreBundle.java

+6-4
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,18 @@
1818

1919
import java.io.IOException;
2020
import java.io.InputStream;
21-
import java.net.URL;
2221
import java.security.KeyStore;
2322
import java.security.KeyStoreException;
2423
import java.security.NoSuchAlgorithmException;
2524
import java.security.NoSuchProviderException;
2625
import java.security.cert.CertificateException;
2726

27+
import org.springframework.boot.io.ApplicationResourceLoader;
2828
import org.springframework.boot.ssl.SslStoreBundle;
29+
import org.springframework.core.io.Resource;
30+
import org.springframework.core.io.ResourceLoader;
2931
import org.springframework.core.style.ToStringCreator;
3032
import org.springframework.util.Assert;
31-
import org.springframework.util.ResourceUtils;
3233
import org.springframework.util.StringUtils;
3334

3435
/**
@@ -114,8 +115,9 @@ private void loadHardwareKeyStore(KeyStore store, String location, char[] passwo
114115
private void loadKeyStore(KeyStore store, String location, char[] password) {
115116
Assert.state(StringUtils.hasText(location), () -> "Location must not be empty or null");
116117
try {
117-
URL url = ResourceUtils.getURL(location);
118-
try (InputStream stream = url.openStream()) {
118+
ResourceLoader resourceLoader = new ApplicationResourceLoader();
119+
Resource resource = resourceLoader.getResource(location);
120+
try (InputStream stream = resource.getInputStream()) {
119121
store.load(stream, password);
120122
}
121123
}

0 commit comments

Comments
 (0)