diff --git a/CHANGES.txt b/CHANGES.txt index ceaef9e6a..4dbbb9ee4 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +4.17.0 (Aug 22, 2025) +- Added a maximum size payload when posting unique keys telemetry in batches +- Added ProxyConfiguration parameter to support proxies, including Harness Forward Proxy, allowing also for more secured authentication options: MTLS, Bearer token and user/password authentication. Read more in our docs. + 4.16.1 (Jul 21, 2025) - Fixed vulnerabilities: - Upgraded org.apache.commons-commons-lang3 to 3.18.0 diff --git a/client/pom.xml b/client/pom.xml index b6337c14c..768f79e9f 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -5,9 +5,9 @@ io.split.client java-client-parent - 4.16.1 + 4.17.0 - 4.16.1 + 4.17.0 java-client jar Java Client @@ -24,7 +24,7 @@ 0.8.0 true - true + false central false published diff --git a/client/src/main/java/io/split/client/HttpClientDynamicCredentials.java b/client/src/main/java/io/split/client/HttpClientDynamicCredentials.java new file mode 100644 index 000000000..ebcbe6676 --- /dev/null +++ b/client/src/main/java/io/split/client/HttpClientDynamicCredentials.java @@ -0,0 +1,26 @@ +package io.split.client; + +import io.split.client.dtos.BearerCredentialsProvider; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.BearerToken; +import org.apache.hc.client5.http.auth.Credentials; +import org.apache.hc.core5.http.protocol.HttpContext; + +class HttpClientDynamicCredentials implements org.apache.hc.client5.http.auth.CredentialsProvider { + + private final BearerCredentialsProvider _bearerCredentialsProvider; + + public HttpClientDynamicCredentials (BearerCredentialsProvider bearerCredentialsProvider) { + _bearerCredentialsProvider = bearerCredentialsProvider; + } + + @Override + public Credentials getCredentials(AuthScope authScope, HttpContext context) { + + // This Provider is invoked every time a request is made. + // This should invoke a user-custom provider responsible for: + return new BearerToken(_bearerCredentialsProvider.getToken()); + } + +} + diff --git a/client/src/main/java/io/split/client/SplitClientConfig.java b/client/src/main/java/io/split/client/SplitClientConfig.java index fd312c3b2..f32b9b091 100644 --- a/client/src/main/java/io/split/client/SplitClientConfig.java +++ b/client/src/main/java/io/split/client/SplitClientConfig.java @@ -1,5 +1,6 @@ package io.split.client; +import io.split.client.dtos.ProxyConfiguration; import io.split.client.impressions.ImpressionListener; import io.split.client.impressions.ImpressionsManager; import io.split.client.utils.FileTypeEnum; @@ -8,6 +9,7 @@ import io.split.storages.enums.OperationMode; import io.split.storages.enums.StorageMode; import org.apache.hc.core5.http.HttpHost; +import org.slf4j.LoggerFactory; import pluggable.CustomStorageWrapper; import java.io.IOException; @@ -27,6 +29,7 @@ */ public class SplitClientConfig { + private static final org.slf4j.Logger _log = LoggerFactory.getLogger(SplitClientConfig.class); public static final String LOCALHOST_DEFAULT_FILE = "split.yaml"; public static final String SDK_ENDPOINT = "https://sdk.split.io"; public static final String EVENTS_ENDPOINT = "https://events.split.io"; @@ -34,6 +37,14 @@ public class SplitClientConfig { public static final String STREAMING_ENDPOINT = "https://streaming.split.io/sse"; public static final String TELEMETRY_ENDPOINT = "https://telemetry.split.io/api/v1"; + public static class HttpScheme { + private HttpScheme() { + throw new IllegalStateException("Utility class"); + } + public static final String HTTP = "http"; + public static final String HTTPS = "https"; + } + private final String _endpoint; private final String _eventsEndpoint; @@ -82,6 +93,7 @@ public class SplitClientConfig { private final ThreadFactory _threadFactory; // Proxy configs + private final ProxyConfiguration _proxyConfiguration; private final HttpHost _proxy; private final String _proxyUsername; private final String _proxyPassword; @@ -118,6 +130,7 @@ private SplitClientConfig(String endpoint, HttpHost proxy, String proxyUsername, String proxyPassword, + ProxyConfiguration proxyConfiguration, int eventsQueueSize, long eventSendIntervalInMillis, int maxStringLength, @@ -171,6 +184,7 @@ private SplitClientConfig(String endpoint, _proxy = proxy; _proxyUsername = proxyUsername; _proxyPassword = proxyPassword; + _proxyConfiguration = proxyConfiguration; _eventsQueueSize = eventsQueueSize; _eventSendIntervalInMillis = eventSendIntervalInMillis; _maxStringLength = maxStringLength; @@ -302,6 +316,10 @@ public String proxyPassword() { return _proxyPassword; } + public ProxyConfiguration proxyConfiguration() { + return _proxyConfiguration; + } + public long eventSendIntervalInMillis() { return _eventSendIntervalInMillis; } @@ -417,8 +435,8 @@ public boolean isSdkEndpointOverridden() { } public CustomHttpModule alternativeHTTPModule() { return _alternativeHTTPModule; } - public static final class Builder { + public static final class Builder { private String _endpoint = SDK_ENDPOINT; private boolean _endpointSet = false; private String _eventsEndpoint = EVENTS_ENDPOINT; @@ -442,6 +460,7 @@ public static final class Builder { private int _proxyPort = -1; private String _proxyUsername; private String _proxyPassword; + private ProxyConfiguration _proxyConfiguration; private int _eventsQueueSize = 500; private long _eventSendIntervalInMillis = 30 * (long)1000; private int _maxStringLength = 250; @@ -734,10 +753,14 @@ public Builder waitBeforeShutdown(int waitTime) { /** * The host location of the proxy. Default is localhost. + * @deprecated + * This method is deprecated. + *

Use {@link ProxyConfiguration)} instead. * * @param proxyHost location of the proxy * @return this builder */ + @Deprecated public Builder proxyHost(String proxyHost) { _proxyHost = proxyHost; return this; @@ -745,10 +768,14 @@ public Builder proxyHost(String proxyHost) { /** * The port of the proxy. Default is -1. + * @deprecated + * This method is deprecated. + *

Use {@link ProxyConfiguration)} instead. * * @param proxyPort port for the proxy * @return this builder */ + @Deprecated public Builder proxyPort(int proxyPort) { _proxyPort = proxyPort; return this; @@ -756,10 +783,14 @@ public Builder proxyPort(int proxyPort) { /** * Set the username for authentication against the proxy (if proxy settings are enabled). (Optional). + * @deprecated + * This method is deprecated. + *

Use {@link ProxyConfiguration)} instead. * * @param proxyUsername * @return this builder */ + @Deprecated public Builder proxyUsername(String proxyUsername) { _proxyUsername = proxyUsername; return this; @@ -776,6 +807,17 @@ public Builder proxyPassword(String proxyPassword) { return this; } + /** + * Set the mtls authentication against the proxy (if proxy settings are enabled). (Optional). + * + * @param proxyConfiguration + * @return this builder + */ + public Builder proxyConfiguration(ProxyConfiguration proxyConfiguration) { + _proxyConfiguration = proxyConfiguration; + return this; + } + /** * Disables running destroy() on shutdown by default. * @@ -927,7 +969,7 @@ public Builder operationMode(OperationMode mode) { /** * - * @param storage mode + * @param mode * @return this builder */ public Builder storageMode(StorageMode mode) { @@ -1096,6 +1138,26 @@ private void verifyAlternativeClient() { } } + private void verifyProxy() { + if (_proxyConfiguration == null) + return; + + if (_proxyPort != -1) { + _log.warn("Both the deprecated proxy configuration methods (`proxyHost`, `proxyPort`, `proxyUsername`, or `proxyPassword`) " + + "and the new `ProxyConfiguration` builder are being used. `ProxyConfiguration` will take precedence."); + } + + if (!(_proxyConfiguration.getHost().getSchemeName().equals(HttpScheme.HTTP) || + _proxyConfiguration.getHost().getSchemeName().equals(HttpScheme.HTTPS))) { + throw new IllegalArgumentException("Proxy scheme must be either http or https."); + } + + if ((_proxyConfiguration.getP12File() != null && _proxyConfiguration.getPassKey() == null) || + (_proxyConfiguration.getP12File() == null && _proxyConfiguration.getPassKey() != null)) { + throw new IllegalArgumentException("Proxy mTLS must have p12 file path and name, and pass phrase."); + } + } + public SplitClientConfig build() { verifyRates(); @@ -1108,6 +1170,8 @@ public SplitClientConfig build() { verifyAlternativeClient(); + verifyProxy(); + if (_numThreadsForSegmentFetch <= 0) { throw new IllegalArgumentException("Number of threads for fetching segments MUST be greater than zero"); } @@ -1133,6 +1197,7 @@ public SplitClientConfig build() { proxy(), _proxyUsername, _proxyPassword, + _proxyConfiguration, _eventsQueueSize, _eventSendIntervalInMillis, _maxStringLength, diff --git a/client/src/main/java/io/split/client/SplitFactoryImpl.java b/client/src/main/java/io/split/client/SplitFactoryImpl.java index 9932cbf8c..cca655612 100644 --- a/client/src/main/java/io/split/client/SplitFactoryImpl.java +++ b/client/src/main/java/io/split/client/SplitFactoryImpl.java @@ -1,6 +1,7 @@ package io.split.client; import com.google.common.io.Files; +import io.split.client.dtos.BearerCredentialsProvider; import io.split.client.dtos.Metadata; import io.split.client.events.EventsSender; import io.split.client.events.EventsStorage; @@ -105,6 +106,7 @@ import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; +import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.http.ssl.TLS; import org.apache.hc.core5.ssl.SSLContexts; @@ -113,11 +115,13 @@ import org.slf4j.LoggerFactory; import pluggable.CustomStorageWrapper; +import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; +import java.security.KeyStore; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; import java.util.HashSet; @@ -517,9 +521,12 @@ public boolean isDestroyed() { protected static SplitHttpClient buildSplitHttpClient(String apiToken, SplitClientConfig config, SDKMetadata sdkMetadata, RequestDecorator requestDecorator) - throws URISyntaxException { + throws URISyntaxException, IOException { + + SSLContext sslContext = buildSSLContext(config); + SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create() - .setSslContext(SSLContexts.createSystemDefault()) + .setSslContext(sslContext) .setTlsVersions(TLS.V_1_1, TLS.V_1_2) .build(); @@ -545,7 +552,7 @@ protected static SplitHttpClient buildSplitHttpClient(String apiToken, SplitClie .addResponseInterceptorLast((new GzipDecoderResponseInterceptor())); // Set up proxy is it exists - if (config.proxy() != null) { + if (config.proxy() != null || config.proxyConfiguration() != null) { httpClientbuilder = setupProxy(httpClientbuilder, config); } @@ -556,13 +563,15 @@ protected static SplitHttpClient buildSplitHttpClient(String apiToken, SplitClie } private static CloseableHttpClient buildSSEdHttpClient(String apiToken, SplitClientConfig config, - SDKMetadata sdkMetadata) { + SDKMetadata sdkMetadata) throws IOException { RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(Timeout.ofMilliseconds(SSE_CONNECT_TIMEOUT)) .build(); + SSLContext sslContext = buildSSLContext(config); + SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create() - .setSslContext(SSLContexts.createSystemDefault()) + .setSslContext(sslContext) .setTlsVersions(TLS.V_1_1, TLS.V_1_2) .build(); @@ -582,28 +591,91 @@ private static CloseableHttpClient buildSSEdHttpClient(String apiToken, SplitCli .addRequestInterceptorLast(ClientKeyInterceptorFilter.instance(apiToken)); // Set up proxy is it exists - if (config.proxy() != null) { + if (config.proxy() != null || config.proxyConfiguration() != null) { httpClientbuilder = setupProxy(httpClientbuilder, config); } return httpClientbuilder.build(); } + private static SSLContext buildSSLContext(SplitClientConfig config) throws IOException, NullPointerException { + SSLContext sslContext; + if (config.proxyConfiguration() != null && config.proxyConfiguration().getP12File() != null) { + _log.debug("Proxy setup using mTLS"); + InputStream keystoreStream = null; + try { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keystoreStream = config.proxyConfiguration().getP12File(); + keyStore.load(keystoreStream, config.proxyConfiguration().getPassKey().toCharArray()); + sslContext = SSLContexts.custom() + .loadKeyMaterial(keyStore, config.proxyConfiguration().getPassKey().toCharArray()) + .build(); + } catch (Exception e) { + _log.error("Exception caught while processing p12 file for Proxy mTLS auth: ", e); + _log.warn("Ignoring p12 mTLS config and switching to default context"); + sslContext = SSLContexts.createSystemDefault(); + } finally { + if (keystoreStream != null) { + keystoreStream.close(); + } + } + } else { + sslContext = SSLContexts.createSystemDefault(); + } + return sslContext; + } + private static HttpClientBuilder setupProxy(HttpClientBuilder httpClientbuilder, SplitClientConfig config) { _log.info("Initializing Split SDK with proxy settings"); - DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(config.proxy()); - httpClientbuilder.setRoutePlanner(routePlanner); + if (config.proxyConfiguration() != null) { + return useProxyConfiguration(httpClientbuilder, config); + } else { + return useLegacyProxyConfiguration(httpClientbuilder, config); + } + } + private static HttpClientBuilder useLegacyProxyConfiguration(HttpClientBuilder httpClientbuilder, SplitClientConfig config) { + HttpHost proxyHost = config.proxy(); + addProxyHost(httpClientbuilder, proxyHost); if (config.proxyUsername() != null && config.proxyPassword() != null) { - _log.debug("Proxy setup using credentials"); - BasicCredentialsProvider credsProvider = new BasicCredentialsProvider(); - AuthScope siteScope = new AuthScope(config.proxy().getHostName(), config.proxy().getPort()); - Credentials siteCreds = new UsernamePasswordCredentials(config.proxyUsername(), - config.proxyPassword().toCharArray()); - credsProvider.setCredentials(siteScope, siteCreds); - httpClientbuilder.setDefaultCredentialsProvider(credsProvider); + return addProxyBasicAuth(httpClientbuilder, proxyHost, config.proxyUsername(), config.proxyPassword()); + } + + return httpClientbuilder; + } + + private static HttpClientBuilder useProxyConfiguration(HttpClientBuilder httpClientbuilder, SplitClientConfig config) { + HttpHost proxyHost = config.proxyConfiguration().getHost(); + addProxyHost(httpClientbuilder, proxyHost); + if (config.proxyConfiguration().getProxyCredentialsProvider() == null) { + return httpClientbuilder; + } + + if (config.proxyConfiguration().getProxyCredentialsProvider() instanceof io.split.client.dtos.BasicCredentialsProvider) { + io.split.client.dtos.BasicCredentialsProvider basicAuth = + (io.split.client.dtos.BasicCredentialsProvider) config.proxyConfiguration().getProxyCredentialsProvider(); + return addProxyBasicAuth(httpClientbuilder, proxyHost, basicAuth.getUsername(), basicAuth.getPassword()); } + _log.debug("Proxy setup using Bearer token"); + httpClientbuilder.setDefaultCredentialsProvider(new HttpClientDynamicCredentials( + (BearerCredentialsProvider) config.proxyConfiguration().getProxyCredentialsProvider())); + return httpClientbuilder; + } + + private static void addProxyHost(HttpClientBuilder httpClientbuilder, HttpHost proxyHost) { + DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxyHost); + httpClientbuilder.setRoutePlanner(routePlanner); + } + + private static HttpClientBuilder addProxyBasicAuth(HttpClientBuilder httpClientbuilder, HttpHost proxyHost, String userName, String password) { + _log.debug("Proxy setup using Basic authentication"); + BasicCredentialsProvider credsProvider = new BasicCredentialsProvider(); + AuthScope siteScope = new AuthScope(proxyHost.getHostName(), proxyHost.getPort()); + Credentials siteCreds = new UsernamePasswordCredentials(userName, + password.toCharArray()); + credsProvider.setCredentials(siteScope, siteCreds); + httpClientbuilder.setDefaultCredentialsProvider(credsProvider); return httpClientbuilder; } diff --git a/client/src/main/java/io/split/client/dtos/BasicCredentialsProvider.java b/client/src/main/java/io/split/client/dtos/BasicCredentialsProvider.java new file mode 100644 index 000000000..b77c9f599 --- /dev/null +++ b/client/src/main/java/io/split/client/dtos/BasicCredentialsProvider.java @@ -0,0 +1,7 @@ +package io.split.client.dtos; + +public interface BasicCredentialsProvider extends ProxyCredentialsProvider +{ + String getUsername(); + String getPassword(); +} diff --git a/client/src/main/java/io/split/client/dtos/BearerCredentialsProvider.java b/client/src/main/java/io/split/client/dtos/BearerCredentialsProvider.java new file mode 100644 index 000000000..d4e98c5ff --- /dev/null +++ b/client/src/main/java/io/split/client/dtos/BearerCredentialsProvider.java @@ -0,0 +1,6 @@ +package io.split.client.dtos; + +public interface BearerCredentialsProvider extends ProxyCredentialsProvider +{ + String getToken(); +} diff --git a/client/src/main/java/io/split/client/dtos/ProxyConfiguration.java b/client/src/main/java/io/split/client/dtos/ProxyConfiguration.java new file mode 100644 index 000000000..c1ed2b409 --- /dev/null +++ b/client/src/main/java/io/split/client/dtos/ProxyConfiguration.java @@ -0,0 +1,66 @@ +package io.split.client.dtos; + +import org.apache.hc.core5.http.HttpHost; + +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; + +public class ProxyConfiguration { + private final HttpHost _proxyHost; + private ProxyCredentialsProvider _provider; + private final InputStream _p12File; + private final String _passKey; + + private ProxyConfiguration(HttpHost proxyHost, + ProxyCredentialsProvider proxyCredentialsProvider, + InputStream p12File, String passKey) { + _proxyHost = proxyHost; + _p12File = p12File; + _passKey = passKey; + _provider = proxyCredentialsProvider; + } + + public HttpHost getHost() { return _proxyHost; } + public InputStream getP12File() { return _p12File; } + public String getPassKey() { return _passKey; } + public ProxyCredentialsProvider getProxyCredentialsProvider() { return _provider; } + + public static ProxyConfiguration.Builder builder() { + return new ProxyConfiguration.Builder(); + } + + public static class Builder { + private ProxyCredentialsProvider _provider; + private HttpHost _proxyHost; + private InputStream _p12File; + private String _passKey; + + public ProxyConfiguration.Builder credentialsProvider(ProxyCredentialsProvider provider) { + _provider = provider; + return this; + } + + public ProxyConfiguration.Builder url(URL url) throws MalformedURLException { + try { + _proxyHost = new HttpHost(url.getProtocol(), url.getHost(), url.getPort()); + } catch (Exception exc) { + throw new MalformedURLException("Proxy configuration is invalid. The proxy `url` is malformed"); + } + return this; + } + + public ProxyConfiguration.Builder mtls(InputStream p12File, String passKey) { + _passKey = passKey; + _p12File = p12File; + return this; + } + + public ProxyConfiguration build() { + if (_proxyHost == null) { + throw new IllegalArgumentException("Proxy configuration is invalid. The proxy `url` was not provided"); + } + return new ProxyConfiguration(_proxyHost, _provider, _p12File, _passKey); + } + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/client/dtos/ProxyCredentialsProvider.java b/client/src/main/java/io/split/client/dtos/ProxyCredentialsProvider.java new file mode 100644 index 000000000..e1653d5f4 --- /dev/null +++ b/client/src/main/java/io/split/client/dtos/ProxyCredentialsProvider.java @@ -0,0 +1,4 @@ +package io.split.client.dtos; + +public interface ProxyCredentialsProvider +{} diff --git a/client/src/main/java/io/split/client/impressions/UniqueKeysTrackerImp.java b/client/src/main/java/io/split/client/impressions/UniqueKeysTrackerImp.java index 80b3703d7..c0034b6b2 100644 --- a/client/src/main/java/io/split/client/impressions/UniqueKeysTrackerImp.java +++ b/client/src/main/java/io/split/client/impressions/UniqueKeysTrackerImp.java @@ -1,5 +1,6 @@ package io.split.client.impressions; +import com.google.common.collect.Lists; import io.split.client.dtos.UniqueKeys; import io.split.client.impressions.filters.BloomFilterImp; import io.split.client.impressions.filters.Filter; @@ -21,12 +22,14 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; public class UniqueKeysTrackerImp implements UniqueKeysTracker{ private static final Logger _log = LoggerFactory.getLogger(UniqueKeysTrackerImp.class); private static final double MARGIN_ERROR = 0.01; - private static final int MAX_AMOUNT_OF_TRACKED_UNIQUE_KEYS = 30000; + private static final int MAX_UNIQUE_KEYS_POST_SIZE = 5000; private static final int MAX_AMOUNT_OF_KEYS = 10000000; + private final AtomicInteger trackerKeysSize = new AtomicInteger(0); private FilterAdapter filterAdapter; private final TelemetrySynchronizer _telemetrySynchronizer; private final ScheduledExecutorService _uniqueKeysSyncScheduledExecutorService; @@ -59,10 +62,11 @@ public boolean track(String featureFlagName, String key) { (feature, current) -> { HashSet keysByFeature = Optional.ofNullable(current).orElse(new HashSet<>()); keysByFeature.add(key); + trackerKeysSize.incrementAndGet(); return keysByFeature; }); _logger.debug("The feature flag " + featureFlagName + " and key " + key + " was added"); - if (uniqueKeysTracker.size() >= MAX_AMOUNT_OF_TRACKED_UNIQUE_KEYS){ + if (trackerKeysSize.intValue() >= MAX_UNIQUE_KEYS_POST_SIZE){ _logger.warn("The UniqueKeysTracker size reached the maximum limit"); try { sendUniqueKeys(); @@ -107,6 +111,7 @@ public HashMap> popAll(){ HashSet value = uniqueKeysTracker.remove(key); toReturn.put(key, value); } + trackerKeysSize.set(0); return toReturn; } @@ -115,26 +120,71 @@ private void sendUniqueKeys(){ _log.debug("SendUniqueKeys already running"); return; } + try { - if (uniqueKeysTracker.size() == 0) { + if (uniqueKeysTracker.isEmpty()) { _log.debug("The Unique Keys Tracker is empty"); return; } + HashMap> uniqueKeysHashMap = popAll(); List uniqueKeysFromPopAll = new ArrayList<>(); for (Map.Entry> uniqueKeyEntry : uniqueKeysHashMap.entrySet()) { UniqueKeys.UniqueKey uniqueKey = new UniqueKeys.UniqueKey(uniqueKeyEntry.getKey(), new ArrayList<>(uniqueKeyEntry.getValue())); uniqueKeysFromPopAll.add(uniqueKey); } - _telemetrySynchronizer.synchronizeUniqueKeys(new UniqueKeys(uniqueKeysFromPopAll)); + uniqueKeysFromPopAll = capChunksToMaxSize(uniqueKeysFromPopAll); + + for (List chunk : getChunks(uniqueKeysFromPopAll)) { + _telemetrySynchronizer.synchronizeUniqueKeys(new UniqueKeys(chunk)); + } } finally { sendGuard.set(false); } } + private List capChunksToMaxSize(List uniqueKeys) { + List finalChunk = new ArrayList<>(); + for (UniqueKeys.UniqueKey uniqueKey : uniqueKeys) { + if (uniqueKey.keysDto.size() > MAX_UNIQUE_KEYS_POST_SIZE) { + for(List subChunk : Lists.partition(uniqueKey.keysDto, MAX_UNIQUE_KEYS_POST_SIZE)) { + finalChunk.add(new UniqueKeys.UniqueKey(uniqueKey.featureName, subChunk)); + } + continue; + } + finalChunk.add(uniqueKey); + } + return finalChunk; + } + + private List> getChunks(List uniqueKeys) { + List> chunks = new ArrayList<>(); + List intermediateChunk = new ArrayList<>(); + for (UniqueKeys.UniqueKey uniqueKey : uniqueKeys) { + if ((getChunkSize(intermediateChunk) + uniqueKey.keysDto.size()) > MAX_UNIQUE_KEYS_POST_SIZE) { + chunks.add(intermediateChunk); + intermediateChunk = new ArrayList<>(); + } + intermediateChunk.add(uniqueKey); + } + if (!intermediateChunk.isEmpty()) { + chunks.add(intermediateChunk); + } + return chunks; + } + + private int getChunkSize(List uniqueKeysChunk) { + int totalSize = 0; + for (UniqueKeys.UniqueKey uniqueKey : uniqueKeysChunk) { + totalSize += uniqueKey.keysDto.size(); + } + return totalSize; + } + private interface ExecuteUniqueKeysAction{ void execute(); } + private class ExecuteCleanFilter implements ExecuteUniqueKeysAction { @Override diff --git a/client/src/test/java/io/split/client/SplitClientConfigTest.java b/client/src/test/java/io/split/client/SplitClientConfigTest.java index 1b640071c..be9e85544 100644 --- a/client/src/test/java/io/split/client/SplitClientConfigTest.java +++ b/client/src/test/java/io/split/client/SplitClientConfigTest.java @@ -1,6 +1,9 @@ package io.split.client; import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.split.client.dtos.BasicCredentialsProvider; +import io.split.client.dtos.BearerCredentialsProvider; +import io.split.client.dtos.ProxyConfiguration; import io.split.client.impressions.Impression; import io.split.client.impressions.ImpressionListener; import io.split.client.impressions.ImpressionsManager; @@ -10,6 +13,10 @@ import org.junit.Test; import org.mockito.Mockito; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -252,6 +259,107 @@ public Map> getHeaderOverrides(RequestContext context) { SplitClientConfig config2 = SplitClientConfig.builder().build(); Assert.assertNull(config2.customHeaderDecorator()); + } + + @Test + public void checkProxyParams() throws MalformedURLException, FileNotFoundException { + SplitClientConfig config = SplitClientConfig.builder() + .proxyConfiguration(new ProxyConfiguration.Builder() + .url(new URL("https://proxy-host:8888")) + .build()) + .build(); + Assert.assertEquals("proxy-host", config.proxyConfiguration().getHost().getHostName()); + Assert.assertEquals(8888, config.proxyConfiguration().getHost().getPort()); + Assert.assertEquals("https", config.proxyConfiguration().getHost().getSchemeName()); + + config = SplitClientConfig.builder() + .proxyConfiguration(new ProxyConfiguration.Builder() + .url(new URL("https://proxy-host:888")) + .credentialsProvider(new io.split.client.dtos.BasicCredentialsProvider() { + @Override + public String getUsername() { + return "user"; + } + + @Override + public String getPassword() { + return "pass"; + } + }) + .build()) + .build(); + io.split.client.dtos.BasicCredentialsProvider basicAuth = (io.split.client.dtos.BasicCredentialsProvider) config.proxyConfiguration().getProxyCredentialsProvider(); + Assert.assertEquals("user", basicAuth.getUsername()); + Assert.assertEquals("pass", basicAuth.getPassword()); + + io.split.client.dtos.BearerCredentialsProvider bearerCredentialsProvider = new io.split.client.dtos.BearerCredentialsProvider() { + @Override + public String getToken() { + return "my-token"; + } + }; + config = SplitClientConfig.builder() + .proxyConfiguration(new ProxyConfiguration.Builder() + .url(new URL("https://proxy-host:888")) + .credentialsProvider(bearerCredentialsProvider) + .build()) + .build(); + Assert.assertEquals(bearerCredentialsProvider, config.proxyConfiguration().getProxyCredentialsProvider()); + FileInputStream p12File = new FileInputStream("src/test/resources/keyStore.p12"); + config = SplitClientConfig.builder() + .proxyConfiguration(new ProxyConfiguration.Builder() + .url(new URL("https://proxy-host:888")) + .mtls(p12File, "pass-key") + .build()) + .build(); + Assert.assertEquals(p12File, config.proxyConfiguration().getP12File()); + Assert.assertEquals("pass-key", config.proxyConfiguration().getPassKey()); + } + + @Test(expected = IllegalArgumentException.class) + public void cannotUseInvalidHttpScheme() throws MalformedURLException { + SplitClientConfig.builder() + .proxyConfiguration(new ProxyConfiguration.Builder() + .url(new URL("ftp://proxy-host:888")) + .build()) + .build(); + } + + @Test(expected = MalformedURLException.class) + public void cannotUseInvalidUrl() throws MalformedURLException { + SplitClientConfig.builder() + .proxyConfiguration(new ProxyConfiguration.Builder() + .url(new URL("")) + .build()) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void mustUseUrl() throws MalformedURLException { + SplitClientConfig.builder() + .proxyConfiguration(new ProxyConfiguration.Builder() + .build()) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void mustUseP12FileWithProxyMtls() throws MalformedURLException { + SplitClientConfig.builder() + .proxyConfiguration(new ProxyConfiguration.Builder() + .url(new URL("https://proxy-host:888")) + .mtls(null, "pass-key") + .build()) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void mustUseP12PassKeyWithProxyMtls() throws MalformedURLException, FileNotFoundException { + SplitClientConfig.builder() + .proxyConfiguration(new ProxyConfiguration.Builder() + .url(new URL("https://proxy-host:888")) + .mtls(new FileInputStream("src/test/resources/keyStore.p12"), null) + .build()) + .build(); } } \ No newline at end of file diff --git a/client/src/test/java/io/split/client/SplitFactoryImplTest.java b/client/src/test/java/io/split/client/SplitFactoryImplTest.java index a6da10692..9826b47e2 100644 --- a/client/src/test/java/io/split/client/SplitFactoryImplTest.java +++ b/client/src/test/java/io/split/client/SplitFactoryImplTest.java @@ -1,16 +1,25 @@ package io.split.client; +import io.split.client.dtos.ProxyConfiguration; import io.split.client.impressions.ImpressionsManager; import io.split.client.utils.FileTypeEnum; import io.split.integrations.IntegrationsConfig; +import io.split.service.SplitHttpClientImpl; import io.split.storages.enums.OperationMode; import io.split.storages.pluggable.domain.UserStorageWrapper; import io.split.telemetry.storage.TelemetryStorage; import io.split.telemetry.synchronizer.TelemetrySynchronizer; import junit.framework.TestCase; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.io.DefaultHttpClientConnectionOperator; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.config.Registry; import org.awaitility.Awaitility; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; import static org.mockito.Mockito.when; @@ -24,6 +33,9 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URISyntaxException; +import java.net.URL; +import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -87,24 +99,205 @@ public void testFactoryInstantiationIntegrationsConfig() throws Exception { } @Test - public void testFactoryInstantiationWithProxy() throws Exception { + public void testFactoryInstantiationWithLegacyProxy() throws Exception { SplitClientConfig splitClientConfig = SplitClientConfig.builder() .enableDebug() .impressionsMode(ImpressionsManager.Mode.DEBUG) .impressionsRefreshRate(1) - .endpoint(ENDPOINT,EVENTS_ENDPOINT) + .endpoint(ENDPOINT, EVENTS_ENDPOINT) .telemetryURL(SplitClientConfig.TELEMETRY_ENDPOINT) .authServiceURL(AUTH_SERVICE) .setBlockUntilReadyTimeout(1000) - .proxyPort(6060) - .proxyUsername("test") - .proxyPassword("password") - .proxyHost(ENDPOINT) + .proxyPort(8888) + .proxyHost("proxy-host") + .proxyUsername("user") + .proxyPassword("pass") .build(); SplitFactoryImpl splitFactory = new SplitFactoryImpl(API_KEY, splitClientConfig); + assertNotNull(splitFactory.client()); + + splitFactory.destroy(); + } + @Test + public void testFactoryInstantiationWithProxyCredentials() throws Exception { + class MyBearerCredentialsProvider implements io.split.client.dtos.BasicCredentialsProvider { + @Override + public String getUsername() { + return "test"; + } + @Override + public String getPassword() { + return "password"; + } + }; + + SplitClientConfig splitClientConfig = SplitClientConfig.builder() + .enableDebug() + .impressionsMode(ImpressionsManager.Mode.DEBUG) + .impressionsRefreshRate(1) + .endpoint(ENDPOINT, EVENTS_ENDPOINT) + .telemetryURL(SplitClientConfig.TELEMETRY_ENDPOINT) + .authServiceURL(AUTH_SERVICE) + .setBlockUntilReadyTimeout(1000) + .proxyConfiguration(ProxyConfiguration.builder() + .url(new URL("http://proxy-name:6060")) + .credentialsProvider(new MyBearerCredentialsProvider()) + .build()) + .build(); + SplitFactoryImpl splitFactory = new SplitFactoryImpl(API_KEY, splitClientConfig); assertNotNull(splitFactory.client()); assertNotNull(splitFactory.manager()); + + Field splitHttpClientField = SplitFactoryImpl.class.getDeclaredField("_splitHttpClient"); + splitHttpClientField.setAccessible(true); + SplitHttpClientImpl client = (SplitHttpClientImpl) splitHttpClientField.get(splitFactory); + + Field httpClientField = SplitHttpClientImpl.class.getDeclaredField("_client"); + httpClientField.setAccessible(true); + Class InternalHttp = Class.forName("org.apache.hc.client5.http.impl.classic.InternalHttpClient"); + + Field routePlannerField = InternalHttp.getDeclaredField("routePlanner"); + routePlannerField.setAccessible(true); + DefaultProxyRoutePlanner routePlanner = (DefaultProxyRoutePlanner) routePlannerField.get(InternalHttp.cast(httpClientField.get(client))); + + Field proxyField = DefaultProxyRoutePlanner.class.getDeclaredField("proxy"); + proxyField.setAccessible(true); + HttpHost proxy = (HttpHost) proxyField.get(routePlanner); + + Assert.assertEquals("http", proxy.getSchemeName()); + Assert.assertEquals("proxy-name", proxy.getHostName()); + Assert.assertEquals(6060, proxy.getPort()); + + Field credentialsProviderField = InternalHttp.getDeclaredField("credentialsProvider"); + credentialsProviderField.setAccessible(true); + BasicCredentialsProvider credentialsProvider = (BasicCredentialsProvider) credentialsProviderField.get(InternalHttp.cast(httpClientField.get(client))); + + Field credMapField = BasicCredentialsProvider.class.getDeclaredField("credMap"); + credMapField.setAccessible(true); + ConcurrentHashMap credMap = (ConcurrentHashMap) credMapField.get(credentialsProvider); + + Assert.assertEquals("test", credMap.entrySet().stream().iterator().next().getValue().getUserName()); + assertNotNull(credMap.entrySet().stream().iterator().next().getValue().getUserPassword()); + + splitFactory.destroy(); + } + + @Test + public void testFactoryInstantiationWithProxyToken() throws Exception { + class MyBearerCredentialsProvider implements io.split.client.dtos.BearerCredentialsProvider { + @Override + public String getToken() { + return "123456789"; + } + }; + + SplitClientConfig splitClientConfig = SplitClientConfig.builder() + .enableDebug() + .impressionsMode(ImpressionsManager.Mode.DEBUG) + .impressionsRefreshRate(1) + .endpoint(ENDPOINT, EVENTS_ENDPOINT) + .telemetryURL(SplitClientConfig.TELEMETRY_ENDPOINT) + .authServiceURL(AUTH_SERVICE) + .setBlockUntilReadyTimeout(1000) + .proxyConfiguration(ProxyConfiguration.builder() + .url(new URL("http://proxy-name:6060")) + .credentialsProvider(new MyBearerCredentialsProvider()) + .build()) + .build(); + SplitFactoryImpl splitFactory2 = new SplitFactoryImpl(API_KEY, splitClientConfig); + assertNotNull(splitFactory2.client()); + assertNotNull(splitFactory2.manager()); + + Field splitHttpClientField2 = SplitFactoryImpl.class.getDeclaredField("_splitHttpClient"); + splitHttpClientField2.setAccessible(true); + SplitHttpClientImpl client2 = (SplitHttpClientImpl) splitHttpClientField2.get(splitFactory2); + + Field httpClientField2 = SplitHttpClientImpl.class.getDeclaredField("_client"); + httpClientField2.setAccessible(true); + Class InternalHttp2 = Class.forName("org.apache.hc.client5.http.impl.classic.InternalHttpClient"); + + Field credentialsProviderField2 = InternalHttp2.getDeclaredField("credentialsProvider"); + credentialsProviderField2.setAccessible(true); + HttpClientDynamicCredentials credentialsProvider2 = (HttpClientDynamicCredentials) credentialsProviderField2.get(InternalHttp2.cast(httpClientField2.get(client2))); + + Field proxyRuntimeField = HttpClientDynamicCredentials.class.getDeclaredField("_bearerCredentialsProvider"); + proxyRuntimeField.setAccessible(true); + MyBearerCredentialsProvider proxyRuntime = (MyBearerCredentialsProvider) proxyRuntimeField.get(credentialsProvider2); + + assertNotNull("123456789", proxyRuntime.getToken()); + + splitFactory2.destroy(); + } + + @Test + public void testFactoryInstantiationWithProxyMtls() throws Exception { + SplitClientConfig splitClientConfig = SplitClientConfig.builder() + .enableDebug() + .impressionsMode(ImpressionsManager.Mode.DEBUG) + .impressionsRefreshRate(1) + .endpoint(ENDPOINT,EVENTS_ENDPOINT) + .telemetryURL(SplitClientConfig.TELEMETRY_ENDPOINT) + .authServiceURL(AUTH_SERVICE) + .setBlockUntilReadyTimeout(1000) + .proxyConfiguration(ProxyConfiguration.builder() + .url(new URL("http://proxy-name:6060")) + .mtls(new FileInputStream("src/test/resources/keyStore.p12"), "split") + .build()) + .build(); + SplitFactoryImpl splitFactory3 = new SplitFactoryImpl(API_KEY, splitClientConfig); + assertNotNull(splitFactory3.client()); + assertNotNull(splitFactory3.manager()); + + Field splitHttpClientField3 = SplitFactoryImpl.class.getDeclaredField("_splitHttpClient"); + splitHttpClientField3.setAccessible(true); + SplitHttpClientImpl client3 = (SplitHttpClientImpl) splitHttpClientField3.get(splitFactory3); + + Field httpClientField3 = SplitHttpClientImpl.class.getDeclaredField("_client"); + httpClientField3.setAccessible(true); + Class InternalHttp3 = Class.forName("org.apache.hc.client5.http.impl.classic.InternalHttpClient"); + + Field connManagerField = InternalHttp3.getDeclaredField("connManager"); + connManagerField.setAccessible(true); + PoolingHttpClientConnectionManager connManager = (PoolingHttpClientConnectionManager) connManagerField.get(InternalHttp3.cast(httpClientField3.get(client3))); + + Field connectionOperatorField = PoolingHttpClientConnectionManager.class.getDeclaredField("connectionOperator"); + connectionOperatorField.setAccessible(true); + DefaultHttpClientConnectionOperator connectionOperator = (DefaultHttpClientConnectionOperator) connectionOperatorField.get(connManager); + + Field tlsSocketStrategyLookupField = DefaultHttpClientConnectionOperator.class.getDeclaredField("tlsSocketStrategyLookup"); + tlsSocketStrategyLookupField.setAccessible(true); + Registry tlsSocketStrategyLookup = (Registry) tlsSocketStrategyLookupField.get(connectionOperator); + + Field mapField = Registry.class.getDeclaredField("map"); + mapField.setAccessible(true); + Class map = mapField.get(tlsSocketStrategyLookup).getClass(); + + Class value = ((ConcurrentHashMap) map.cast(mapField.get(tlsSocketStrategyLookup))).get("https").getClass(); + + Field arg1Field = value.getDeclaredField("arg$1"); + arg1Field.setAccessible(true); + Class sslConnectionSocketFactory = arg1Field.get(((ConcurrentHashMap) map.cast(mapField.get(tlsSocketStrategyLookup))).get("https")).getClass(); + + Field socketFactoryField = sslConnectionSocketFactory.getDeclaredField("socketFactory"); + socketFactoryField.setAccessible(true); + Class socketFactory = socketFactoryField.get(arg1Field.get(((ConcurrentHashMap) map.cast(mapField.get(tlsSocketStrategyLookup))).get("https"))).getClass(); + + Field contextField = socketFactory.getDeclaredField("context"); + contextField.setAccessible(true); + Class context = Class.forName("sun.security.ssl.SSLContextImpl"); + + Field keyManagerField = context.getDeclaredField("keyManager"); + keyManagerField.setAccessible(true); + Class keyManager = keyManagerField.get(contextField.get(socketFactoryField.get(arg1Field.get(((ConcurrentHashMap) map.cast(mapField.get(tlsSocketStrategyLookup))).get("https"))))).getClass(); + + Field credentialsMapField = keyManager.getDeclaredField("credentialsMap"); + credentialsMapField.setAccessible(true); + HashMap credentialsMap = (HashMap) credentialsMapField.get(keyManagerField.get(contextField.get(socketFactoryField.get(arg1Field.get(((ConcurrentHashMap) map.cast(mapField.get(tlsSocketStrategyLookup))).get("https")))))); + + assertNotNull(credentialsMap.get("1")); + + splitFactory3.destroy(); } @Test diff --git a/client/src/test/java/io/split/client/impressions/UniqueKeysTrackerImpTest.java b/client/src/test/java/io/split/client/impressions/UniqueKeysTrackerImpTest.java index a15940110..e758369eb 100644 --- a/client/src/test/java/io/split/client/impressions/UniqueKeysTrackerImpTest.java +++ b/client/src/test/java/io/split/client/impressions/UniqueKeysTrackerImpTest.java @@ -1,13 +1,21 @@ package io.split.client.impressions; +import io.split.client.dtos.UniqueKeys; import io.split.telemetry.synchronizer.TelemetryInMemorySubmitter; import io.split.telemetry.synchronizer.TelemetrySynchronizer; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicInteger; public class UniqueKeysTrackerImpTest { private static TelemetrySynchronizer _telemetrySynchronizer = Mockito.mock(TelemetryInMemorySubmitter.class); @@ -100,4 +108,71 @@ public void testStopSynchronization() throws Exception { uniqueKeysTrackerImp.stop(); Mockito.verify(telemetrySynchronizer, Mockito.times(1)).synchronizeUniqueKeys(Mockito.anyObject()); } + + @Test + public void testUniqueKeysChunks() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + UniqueKeysTrackerImp uniqueKeysTrackerImp = new UniqueKeysTrackerImp(_telemetrySynchronizer, 10000, 10000, null); + HashMap> uniqueKeysHashMap = new HashMap<>(); + HashSet feature1 = new HashSet<>(); + HashSet feature2 = new HashSet<>(); + HashSet feature3 = new HashSet<>(); + HashSet feature4 = new HashSet<>(); + HashSet feature5 = new HashSet<>(); + for (Integer i=1; i<6000; i++) { + if (i <= 1000) { + feature1.add("key" + i); + } + if (i <= 2000) { + feature2.add("key" + i); + } + if (i <= 3000) { + feature3.add("key" + i); + } + if (i <= 4000) { + feature4.add("key" + i); + } + feature5.add("key" + i); + } + uniqueKeysHashMap.put("feature1", feature1); + uniqueKeysHashMap.put("feature2", feature2); + uniqueKeysHashMap.put("feature3", feature3); + uniqueKeysHashMap.put("feature4", feature4); + uniqueKeysHashMap.put("feature5", feature5); + + List uniqueKeysFromPopAll = new ArrayList<>(); + for (Map.Entry> uniqueKeyEntry : uniqueKeysHashMap.entrySet()) { + UniqueKeys.UniqueKey uniqueKey = new UniqueKeys.UniqueKey(uniqueKeyEntry.getKey(), new ArrayList<>(uniqueKeyEntry.getValue())); + uniqueKeysFromPopAll.add(uniqueKey); + } + Method methodCapChunks = uniqueKeysTrackerImp.getClass().getDeclaredMethod("capChunksToMaxSize", List.class); + methodCapChunks.setAccessible(true); + uniqueKeysFromPopAll = (List)methodCapChunks.invoke(uniqueKeysTrackerImp, uniqueKeysFromPopAll); + + Method methodGetChunks = uniqueKeysTrackerImp.getClass().getDeclaredMethod("getChunks", List.class); + methodGetChunks.setAccessible(true); + List> keysChunks = (List>) methodGetChunks.invoke(uniqueKeysTrackerImp, uniqueKeysFromPopAll); + for (List chunk : keysChunks) { + int chunkSize = 0; + for (UniqueKeys.UniqueKey keys : chunk) { + chunkSize += keys.keysDto.size(); + } + Assert.assertTrue(chunkSize <= 5000); + } + } + + @Test + public void testTrackReachMaxKeys() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException { + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetryInMemorySubmitter.class); + UniqueKeysTrackerImp uniqueKeysTrackerImp = new UniqueKeysTrackerImp(telemetrySynchronizer, 10000, 10000, null); + for (int i=1; i<6000; i++) { + Assert.assertTrue(uniqueKeysTrackerImp.track("feature1", "key" + i)); + Assert.assertTrue(uniqueKeysTrackerImp.track("feature2", "key" + i)); + } + Mockito.verify(telemetrySynchronizer, Mockito.times(2)).synchronizeUniqueKeys(Mockito.anyObject()); + + Field getTrackerSize = uniqueKeysTrackerImp.getClass().getDeclaredField("trackerKeysSize"); + getTrackerSize.setAccessible(true); + AtomicInteger trackerSize = (AtomicInteger) getTrackerSize.get(uniqueKeysTrackerImp); + Assert.assertTrue(trackerSize.intValue() == 1998); + } } \ No newline at end of file diff --git a/client/src/test/resources/keyStore.p12 b/client/src/test/resources/keyStore.p12 new file mode 100644 index 000000000..ce2b34171 Binary files /dev/null and b/client/src/test/resources/keyStore.p12 differ diff --git a/okhttp-modules/pom.xml b/okhttp-modules/pom.xml index 7346518d7..fc646f3d7 100644 --- a/okhttp-modules/pom.xml +++ b/okhttp-modules/pom.xml @@ -5,10 +5,10 @@ java-client-parent io.split.client - 4.16.1 + 4.17.0 4.0.0 - 4.16.1 + 4.17.0 okhttp-modules jar http-modules @@ -25,7 +25,7 @@ 0.8.0 true - true + false central false published diff --git a/pluggable-storage/pom.xml b/pluggable-storage/pom.xml index 5c304f981..4b7e01562 100644 --- a/pluggable-storage/pom.xml +++ b/pluggable-storage/pom.xml @@ -6,7 +6,7 @@ java-client-parent io.split.client - 4.16.1 + 4.17.0 2.1.0 diff --git a/pom.xml b/pom.xml index 9f91ded1b..c20814b19 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.split.client java-client-parent - 4.16.1 + 4.17.0 diff --git a/redis-wrapper/pom.xml b/redis-wrapper/pom.xml index 9ca70894b..8bf6c4246 100644 --- a/redis-wrapper/pom.xml +++ b/redis-wrapper/pom.xml @@ -6,7 +6,7 @@ java-client-parent io.split.client - 4.16.1 + 4.17.0 redis-wrapper 3.1.1 diff --git a/testing/pom.xml b/testing/pom.xml index 5f0aa0c57..b7f0bf906 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -5,11 +5,11 @@ io.split.client java-client-parent - 4.16.1 + 4.17.0 java-client-testing jar - 4.16.0 + 4.17.0 Java Client For Testing Testing suite for Java SDK for Split @@ -39,7 +39,7 @@ central false published - true + false