From cb076bc06bfd356b778d424224ba57b752ddfa49 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Sat, 1 Jul 2023 23:53:22 -0700 Subject: [PATCH] Added support to custom SSL conext - switcher.truststore --- README.md | 2 + .../switcherapi/client/ContextBuilder.java | 10 +++ .../client/SwitcherProperties.java | 22 ++++++ .../switcherapi/client/model/ContextKey.java | 12 ++- .../client/remote/ClientWSBuilder.java | 50 ++++++++++++ .../client/remote/ClientWSImpl.java | 26 +++---- .../service/remote/SwitcherRemoteService.java | 2 +- .../client/remote/ClientWSBuilderTest.java | 73 ++++++++++++++++++ src/test/resources/keystore.jks | Bin 0 -> 2324 bytes 9 files changed, 181 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/github/switcherapi/client/remote/ClientWSBuilder.java create mode 100644 src/test/java/com/github/switcherapi/client/remote/ClientWSBuilderTest.java create mode 100644 src/test/resources/keystore.jks diff --git a/README.md b/README.md index 10fa16f1..27ae8db4 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ switcher.snapshot.skipvalidation -> true/false Skip snapshotValidation() that ca switcher.snapshot.updateinterval -> Enable the Snapshot Auto Update given an interval of time - e.g. 1s (s: seconds, m: minutes) switcher.silent -> true/false Contingency in case of some problem with connectivity with the API switcher.retry -> Time given to the module to re-establish connectivity with the API - e.g. 5s (s: seconds - m: minutes - h: hours) +switcher.truststore.path -> Path to the truststore file +switcher.truststore.password -> Truststore password (Java 8 applications only) switcher.regextimeout -> Time in ms given to Timed Match Worker used for offline Regex (ReDoS safety mechanism) - 3000 default value diff --git a/src/main/java/com/github/switcherapi/client/ContextBuilder.java b/src/main/java/com/github/switcherapi/client/ContextBuilder.java index 16c64a71..8909c71a 100644 --- a/src/main/java/com/github/switcherapi/client/ContextBuilder.java +++ b/src/main/java/com/github/switcherapi/client/ContextBuilder.java @@ -110,4 +110,14 @@ public ContextBuilder offlineMode(boolean offlineMode) { return this; } + public ContextBuilder truststorePath(String truststorePath) { + properties.setTruststorePath(truststorePath); + return this; + } + + public ContextBuilder truststorePassword(String truststorePassword) { + properties.setTruststorePassword(truststorePassword); + return this; + } + } diff --git a/src/main/java/com/github/switcherapi/client/SwitcherProperties.java b/src/main/java/com/github/switcherapi/client/SwitcherProperties.java index e513913c..bb030f8b 100644 --- a/src/main/java/com/github/switcherapi/client/SwitcherProperties.java +++ b/src/main/java/com/github/switcherapi/client/SwitcherProperties.java @@ -51,6 +51,10 @@ class SwitcherProperties { private boolean offlineMode; + private String truststorePath; + + private String truststorePassword; + public SwitcherProperties() { this.environment = DEFAULT_ENV; this.regexTimeout = DEFAULT_REGEX_TIMEOUT; @@ -72,6 +76,8 @@ public void loadFromProperties(Properties prop) { setOfflineMode(Boolean.parseBoolean(SwitcherUtils.resolveProperties(ContextKey.OFFLINE_MODE.getParam(), prop))); setRetryAfter(SwitcherUtils.resolveProperties(ContextKey.RETRY_AFTER.getParam(), prop)); setRegexTimeout(SwitcherUtils.resolveProperties(ContextKey.REGEX_TIMEOUT.getParam(), prop)); + setTruststorePath(SwitcherUtils.resolveProperties(ContextKey.TRUSTSTORE_PATH.getParam(), prop)); + setTruststorePassword(SwitcherUtils.resolveProperties(ContextKey.TRUSTSTORE_PASSWORD.getParam(), prop)); } public T getValue(ContextKey contextKey, Class type) { @@ -209,4 +215,20 @@ public void setOfflineMode(boolean offlineMode) { this.offlineMode = offlineMode; } + public String getTruststorePath() { + return truststorePath; + } + + public void setTruststorePath(String truststorePath) { + this.truststorePath = truststorePath; + } + + public String getTruststorePassword() { + return truststorePassword; + } + + public void setTruststorePassword(String truststorePassword) { + this.truststorePassword = truststorePassword; + } + } diff --git a/src/main/java/com/github/switcherapi/client/model/ContextKey.java b/src/main/java/com/github/switcherapi/client/model/ContextKey.java index 02ed0d62..3046614b 100644 --- a/src/main/java/com/github/switcherapi/client/model/ContextKey.java +++ b/src/main/java/com/github/switcherapi/client/model/ContextKey.java @@ -82,7 +82,17 @@ public enum ContextKey { /** * (Number) Defines the Timed Match regex time out. */ - REGEX_TIMEOUT("switcher.regextimeout", "regexTimeout"); + REGEX_TIMEOUT("switcher.regextimeout", "regexTimeout"), + + /** + * (Path) Defines the path for the trustsore file. + */ + TRUSTSTORE_PATH("switcher.truststore.path", "truststorePath"), + + /** + * (String) Defines the password for the truststore file. + */ + TRUSTSTORE_PASSWORD("switcher.truststore.password", "truststorePassword"); private final String param; private final String propField; diff --git a/src/main/java/com/github/switcherapi/client/remote/ClientWSBuilder.java b/src/main/java/com/github/switcherapi/client/remote/ClientWSBuilder.java new file mode 100644 index 00000000..5b52c09a --- /dev/null +++ b/src/main/java/com/github/switcherapi/client/remote/ClientWSBuilder.java @@ -0,0 +1,50 @@ +package com.github.switcherapi.client.remote; + +import com.github.switcherapi.client.SwitcherContextBase; +import com.github.switcherapi.client.exception.SwitcherRemoteException; +import com.github.switcherapi.client.model.ContextKey; +import jakarta.ws.rs.client.ClientBuilder; +import org.apache.commons.lang3.StringUtils; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.KeyStore; + +public class ClientWSBuilder { + + private static final String KEYSTORE_TYPE = "JKS"; + + private static final String PROTOCOL = "TLSv1.2"; + + private ClientWSBuilder() { + throw new IllegalStateException("Utility class"); + } + + public static ClientBuilder builder() { + if (StringUtils.isNotBlank(SwitcherContextBase.contextStr(ContextKey.TRUSTSTORE_PATH))) { + return builderSSL(); + } + + return ClientBuilder.newBuilder(); + } + + public static ClientBuilder builderSSL() { + try (InputStream readStream = new FileInputStream(SwitcherContextBase.contextStr(ContextKey.TRUSTSTORE_PATH))) { + final KeyStore trustStore = KeyStore.getInstance(KEYSTORE_TYPE); + trustStore.load(readStream, SwitcherContextBase.contextStr(ContextKey.TRUSTSTORE_PASSWORD).toCharArray()); + + final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + + final SSLContext sslContext = SSLContext.getInstance(PROTOCOL); + sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + + return ClientBuilder.newBuilder().sslContext(sslContext); + } catch (Exception e) { + throw new SwitcherRemoteException("Error while building SSL context", e); + } + } + +} diff --git a/src/main/java/com/github/switcherapi/client/remote/ClientWSImpl.java b/src/main/java/com/github/switcherapi/client/remote/ClientWSImpl.java index 51c7a372..cd088d48 100644 --- a/src/main/java/com/github/switcherapi/client/remote/ClientWSImpl.java +++ b/src/main/java/com/github/switcherapi/client/remote/ClientWSImpl.java @@ -1,19 +1,7 @@ package com.github.switcherapi.client.remote; -import java.util.Optional; -import java.util.Set; - -import com.github.switcherapi.client.exception.*; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.client.WebTarget; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import com.github.switcherapi.client.SwitcherContextBase; +import com.github.switcherapi.client.exception.SwitcherRemoteException; import com.github.switcherapi.client.model.ContextKey; import com.github.switcherapi.client.model.Switcher; import com.github.switcherapi.client.model.criteria.Snapshot; @@ -22,6 +10,16 @@ import com.github.switcherapi.client.model.response.AuthResponse; import com.github.switcherapi.client.model.response.CriteriaResponse; import com.github.switcherapi.client.model.response.SnapshotVersionResponse; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Optional; +import java.util.Set; /** * @author Roger Floriano (petruki) @@ -42,7 +40,7 @@ public class ClientWSImpl implements ClientWS { private final Client client; public ClientWSImpl() { - this.client = ClientBuilder.newClient(); + this.client = ClientWSBuilder.builder().build(); } @Override diff --git a/src/main/java/com/github/switcherapi/client/service/remote/SwitcherRemoteService.java b/src/main/java/com/github/switcherapi/client/service/remote/SwitcherRemoteService.java index 1f2feec8..a3ade720 100644 --- a/src/main/java/com/github/switcherapi/client/service/remote/SwitcherRemoteService.java +++ b/src/main/java/com/github/switcherapi/client/service/remote/SwitcherRemoteService.java @@ -47,7 +47,7 @@ public CriteriaResponse executeCriteria(final Switcher switcher) { return response; } catch (final SwitcherRemoteException e) { - logger.error("Failed to execute criteria - {}", e.getMessage()); + logger.error("Failed to execute criteria - {}\nCause: {}", e.getMessage(), e.getCause()); return executeSilentCriteria(switcher, e); } } diff --git a/src/test/java/com/github/switcherapi/client/remote/ClientWSBuilderTest.java b/src/test/java/com/github/switcherapi/client/remote/ClientWSBuilderTest.java new file mode 100644 index 00000000..e8ee9f22 --- /dev/null +++ b/src/test/java/com/github/switcherapi/client/remote/ClientWSBuilderTest.java @@ -0,0 +1,73 @@ +package com.github.switcherapi.client.remote; + +import com.github.switcherapi.client.ContextBuilder; +import com.github.switcherapi.client.SwitcherContextBase; +import com.github.switcherapi.client.exception.SwitcherRemoteException; +import org.junit.jupiter.api.Test; + +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.*; + +class ClientWSBuilderTest { + + @Test + void shouldCreateClientBuilder() { + // given + SwitcherContextBase.configure(ContextBuilder.builder() + .truststorePath("") + .truststorePassword("")); + + // test + var clientBuilder = ClientWSBuilder.builder(); + assertNotNull(clientBuilder); + + var sslContext = clientBuilder.build().getSslContext(); + assertNotNull(sslContext); + assertEquals("TLS", sslContext.getProtocol()); + } + + @Test + void shouldCreateClientBuilderSSL() { + // given + var truststorePath = Objects.requireNonNull(getClass().getClassLoader() + .getResource("keystore.jks")).getPath(); + + SwitcherContextBase.configure(ContextBuilder.builder() + .truststorePath(truststorePath) + .truststorePassword("changeit")); + + // test + var clientBuilder = ClientWSBuilder.builder(); + assertNotNull(clientBuilder); + + var sslContext = clientBuilder.build().getSslContext(); + assertNotNull(sslContext); + assertEquals("TLSv1.2", sslContext.getProtocol()); + } + + @Test + void shouldNotCreateClientBuilderSSL_invalidKeystorePassword() { + // given + var truststorePath = Objects.requireNonNull(getClass().getClassLoader() + .getResource("keystore.jks")).getPath(); + + SwitcherContextBase.configure(ContextBuilder.builder() + .truststorePath(truststorePath) + .truststorePassword("INVALID")); + + // test + assertThrows(SwitcherRemoteException.class, ClientWSBuilder::builder); + } + + @Test + void shouldNotCreateClientBuilderSSL_invalidKeystorePath() { + // given + SwitcherContextBase.configure(ContextBuilder.builder() + .truststorePath("INVALID") + .truststorePassword("changeit")); + + // test + assertThrows(SwitcherRemoteException.class, ClientWSBuilder::builder); + } +} diff --git a/src/test/resources/keystore.jks b/src/test/resources/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..75709948af71c23a5c57c22fe91bd3baf6e02114 GIT binary patch literal 2324 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