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/pom.xml b/pom.xml index cbac6b43..74648cf0 100644 --- a/pom.xml +++ b/pom.xml @@ -51,9 +51,9 @@ ${java.version} - 2.39 - 2.39 - 2.39 + 2.40 + 2.40 + 2.40 2.10.1 @@ -63,7 +63,7 @@ 5.0.0-alpha.11 - 5.9.2 + 5.9.3 1.9.1 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..d6c9f9a2 --- /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.SwitcherException; +import com.github.switcherapi.client.model.ContextKey; +import org.apache.commons.lang3.StringUtils; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import javax.ws.rs.client.ClientBuilder; +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 SwitcherException("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 ae233a6b..3be88f06 100644 --- a/src/main/java/com/github/switcherapi/client/remote/ClientWSImpl.java +++ b/src/main/java/com/github/switcherapi/client/remote/ClientWSImpl.java @@ -14,7 +14,6 @@ import org.apache.logging.log4j.Logger; import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.MediaType; @@ -41,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..8e50aae6 --- /dev/null +++ b/src/test/java/com/github/switcherapi/client/remote/ClientWSBuilderTest.java @@ -0,0 +1,75 @@ +package com.github.switcherapi.client.remote; + +import com.github.switcherapi.client.ContextBuilder; +import com.github.switcherapi.client.SwitcherContextBase; +import com.github.switcherapi.client.exception.SwitcherException; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; +import javax.ws.rs.client.ClientBuilder; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.*; + +class ClientWSBuilderTest { + + @Test + void shouldCreateClientBuilder() { + // given + SwitcherContextBase.configure(ContextBuilder.builder() + .truststorePath("") + .truststorePassword("")); + + // test + ClientBuilder clientBuilder = ClientWSBuilder.builder(); + assertNotNull(clientBuilder); + + SSLContext sslContext = clientBuilder.build().getSslContext(); + assertNotNull(sslContext); + assertEquals("TLS", sslContext.getProtocol()); + } + + @Test + void shouldCreateClientBuilderSSL() { + // given + String truststorePath = Objects.requireNonNull(getClass().getClassLoader() + .getResource("keystore.jks")).getPath(); + + SwitcherContextBase.configure(ContextBuilder.builder() + .truststorePath(truststorePath) + .truststorePassword("changeit")); + + // test + ClientBuilder clientBuilder = ClientWSBuilder.builder(); + assertNotNull(clientBuilder); + + SSLContext sslContext = clientBuilder.build().getSslContext(); + assertNotNull(sslContext); + assertEquals("TLSv1.2", sslContext.getProtocol()); + } + + @Test + void shouldNotCreateClientBuilderSSL_invalidKeystorePassword() { + // given + String truststorePath = Objects.requireNonNull(getClass().getClassLoader() + .getResource("keystore.jks")).getPath(); + + SwitcherContextBase.configure(ContextBuilder.builder() + .truststorePath(truststorePath) + .truststorePassword("INVALID")); + + // test + assertThrows(SwitcherException.class, ClientWSBuilder::builder); + } + + @Test + void shouldNotCreateClientBuilderSSL_invalidKeystorePath() { + // given + SwitcherContextBase.configure(ContextBuilder.builder() + .truststorePath("INVALID") + .truststorePassword("changeit")); + + // test + assertThrows(SwitcherException.class, ClientWSBuilder::builder); + } +} diff --git a/src/test/resources/keystore.jks b/src/test/resources/keystore.jks new file mode 100644 index 00000000..75709948 Binary files /dev/null and b/src/test/resources/keystore.jks differ