diff --git a/geode-spring-boot-starter/src/main/java/org/springframework/boot/data/geode/autoconfigure/SslAutoConfiguration.java b/geode-spring-boot-starter/src/main/java/org/springframework/boot/data/geode/autoconfigure/SslAutoConfiguration.java index a34d5797..94166d1a 100644 --- a/geode-spring-boot-starter/src/main/java/org/springframework/boot/data/geode/autoconfigure/SslAutoConfiguration.java +++ b/geode-spring-boot-starter/src/main/java/org/springframework/boot/data/geode/autoconfigure/SslAutoConfiguration.java @@ -16,15 +16,39 @@ package org.springframework.boot.data.geode.autoconfigure; +import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Optional; + import org.apache.geode.cache.client.ClientCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.data.gemfire.client.ClientCacheFactoryBean; import org.springframework.data.gemfire.config.annotation.EnableSsl; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; /** * The SslAutoConfiguration class... @@ -37,8 +61,173 @@ import org.springframework.data.gemfire.config.annotation.EnableSsl; @Conditional(SslAutoConfiguration.EnableSslCondition.class) @AutoConfigureBefore(ClientCacheAutoConfiguration.class) @EnableSsl +@SuppressWarnings("unused") public class SslAutoConfiguration { + private static final String CURRENT_WORKING_DIRECTORY = System.getProperty("user.dir"); + private static final String GEMFIRE_SSL_KEYSTORE_PROPERTY = "gemfire.ssl-keystore"; + private static final String GEMFIRE_SSL_TRUSTSTORE_PROPERTY = "gemfire.ssl-truststore"; + private static final String SECURITY_SSL_KEYSTORE_PROPERTY = "spring.data.gemfire.security.ssl.keystore"; + private static final String SECURITY_SSL_TRUSTSTORE_PROPERTY = "spring.data.gemfire.security.ssl.truststore"; + private static final String SSL_KEYSTORE_PROPERTY = "ssl-keystore"; + private static final String SSL_TRUSTSTORE_PROPERTY = "ssl-truststore"; + private static final String TRUSTED_KEYSTORE_FILENAME = "trusted.keystore"; + private static final String USER_HOME_DIRECTORY = System.getProperty("user.home"); + + private static final Logger logger = LoggerFactory.getLogger(SslAutoConfiguration.class); + + private static boolean isSslConfigured(Environment environment) { + + return (environment.containsProperty(SECURITY_SSL_KEYSTORE_PROPERTY) + && environment.containsProperty(SECURITY_SSL_TRUSTSTORE_PROPERTY)) + || (environment.containsProperty(GEMFIRE_SSL_KEYSTORE_PROPERTY) + && environment.containsProperty(GEMFIRE_SSL_TRUSTSTORE_PROPERTY)) + || (environment.containsProperty(SSL_KEYSTORE_PROPERTY) + && environment.containsProperty(SSL_TRUSTSTORE_PROPERTY)); + } + + private static boolean sslIsNotConfigured(Environment environment) { + return !isSslConfigured(environment); + } + + private static String resolveTrustedKeyStore(Environment environment) { + + return locateKeyStoreInFileSystem() + .map(File::getAbsolutePath) + .orElseGet(() -> locateKeyStoreInUserHome() + .map(File::getAbsolutePath) + .orElseGet(() -> resolveKeyStoreFromClassPathAsPathname() + .orElse(null))); + } + + private static Optional resolveKeyStoreFromClassPathAsPathname() { + + return resolveKeyStoreFromClassPath() + .filter(File::isFile) + .map(File::getAbsolutePath) + .filter(StringUtils::hasText); + } + + private static Optional resolveKeyStoreFromClassPath() { + + /* + System.err.printf("KEYSTORE LOCATION [%s]%n", ObjectUtils.doOperationSafely(() -> + new File(new ClassPathResource(keystoreName).getURL().toURI())).getAbsolutePath()); + */ + + return locateKeyStoreInClassPath().map(resource -> { + + File trustedKeyStore = null; + + try { + + URL url = resource.getURL(); + + if (ResourceUtils.isFileURL(url)) { + trustedKeyStore = new File(url.toURI()); + } + else if (ResourceUtils.isJarURL(url)) { + trustedKeyStore = new File(CURRENT_WORKING_DIRECTORY, TRUSTED_KEYSTORE_FILENAME); + FileCopyUtils.copy(url.openStream(), new FileOutputStream(trustedKeyStore)); + } + } + catch (IOException | URISyntaxException cause) { + + if (logger.isWarnEnabled()) { + + logger.warn("Trusted KeyStore {} found in Class Path but is not resolvable as a File: {}", + resource, cause.getMessage()); + + if (logger.isTraceEnabled()) { + logger.trace("Caused by:", cause); + } + } + } + + return trustedKeyStore; + }); + + /* + return locateKeyStoreInClassPath() + .map(it -> ObjectUtils.doOperationSafely(it::getURL)) + .map(url -> ObjectUtils.doOperationSafely(url::toURI)) + .map(uri -> ObjectUtils.doOperationSafely(() -> new File(uri))) + .filter(File::isFile); + */ + } + + private static Optional locateKeyStoreInClassPath() { + return locateKeyStoreInClassPath(TRUSTED_KEYSTORE_FILENAME); + } + + @SuppressWarnings("all") + private static Optional locateKeyStoreInClassPath(String keystoreName) { + + return Optional.of(new ClassPathResource(keystoreName)) + .filter(Resource::exists); + } + + private static Optional locateKeyStoreInFileSystem() { + return locateKeyStoreInFileSystem(new File(CURRENT_WORKING_DIRECTORY)); + } + + private static Optional locateKeyStoreInFileSystem(File directory) { + return locateKeyStoreInFileSystem(directory, TRUSTED_KEYSTORE_FILENAME); + } + + @SuppressWarnings("all") + private static Optional locateKeyStoreInFileSystem(File directory, String keystoreFilename) { + + assertDirectory(directory); + + //System.err.printf("Searching [%s]...%n", directory); + + for (File file : nullSafeListFiles(directory)) { + + //System.err.printf("Testing [%s]...%n", file); + + if (isDirectory(file)) { + + Optional theFile = locateKeyStoreInFileSystem(file, keystoreFilename); + + if (theFile.isPresent()) { + return theFile; + } + else { + continue; + } + } + + if (file.getName().equals(keystoreFilename)) { + return Optional.of(file); + } + } + + return Optional.empty(); + } + + private static Optional locateKeyStoreInUserHome() { + return locateKeyStoreInUserHome(TRUSTED_KEYSTORE_FILENAME); + } + + private static Optional locateKeyStoreInUserHome(String keystoreFilename) { + + return Optional.of(new File(USER_HOME_DIRECTORY, keystoreFilename)) + .filter(File::isFile); + } + + private static void assertDirectory(File path) { + Assert.isTrue(isDirectory(path), String.format("[%s] is not a valid directory", path)); + } + + private static boolean isDirectory(File path) { + return path != null && path.isDirectory(); + } + + private static File[] nullSafeListFiles(File directory) { + return nullSafeArray(directory.listFiles(), File.class); + } + @SuppressWarnings("unused") static class EnableSslCondition extends AnyNestedCondition { @@ -46,6 +235,9 @@ public class SslAutoConfiguration { super(ConfigurationPhase.PARSE_CONFIGURATION); } + @Conditional(TrustedKeyStoreIsPresentCondition.class) + static class TrustedKeyStoreCondition {} + @ConditionalOnProperty(prefix = "spring.data.gemfire.security.ssl", name = { "keystore", "truststore", }) static class SpringDataGeodeSslContextCondition {} @@ -53,4 +245,32 @@ public class SslAutoConfiguration { static class StandaloneApacheGeodeSslContextCondition {} } + + static class SslEnvironmentPostProcessor implements EnvironmentPostProcessor { + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + + Optional.of(environment) + .filter(SslAutoConfiguration::sslIsNotConfigured) + .map(SslAutoConfiguration::resolveTrustedKeyStore) + .filter(StringUtils::hasText) + .ifPresent(trustedKeyStore -> { + System.setProperty(SECURITY_SSL_KEYSTORE_PROPERTY, trustedKeyStore); + System.setProperty(SECURITY_SSL_TRUSTSTORE_PROPERTY, trustedKeyStore); + }); + } + } + + static class TrustedKeyStoreIsPresentCondition implements Condition { + + @Override + @SuppressWarnings("all") + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + + return locateKeyStoreInClassPath().isPresent() + || locateKeyStoreInUserHome().isPresent() + || locateKeyStoreInFileSystem().isPresent(); + } + } } diff --git a/geode-spring-boot-starter/src/main/resources/META-INF/spring.factories b/geode-spring-boot-starter/src/main/resources/META-INF/spring.factories index bafedecd..495c4bbd 100644 --- a/geode-spring-boot-starter/src/main/resources/META-INF/spring.factories +++ b/geode-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -7,3 +7,7 @@ org.springframework.boot.data.geode.autoconfigure.FunctionExecutionAutoConfigura org.springframework.boot.data.geode.autoconfigure.RepositoriesAutoConfiguration,\ org.springframework.boot.data.geode.autoconfigure.SecurityAutoConfiguration,\ org.springframework.boot.data.geode.autoconfigure.SslAutoConfiguration + +# Environment Post Processing +org.springframework.boot.env.EnvironmentPostProcessor=\ +org.springframework.boot.data.geode.autoconfigure.SslAutoConfiguration.SslEnvironmentPostProcessor