diff --git a/CHANGES.txt b/CHANGES.txt index 072fab1f9..9c10df67f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +4.13.0 (Sep 13, 2024) +- Added support for Kerberos Proxy authentication. + 4.12.1 (Jun 10, 2024) - Fixed deadlock for virtual thread in Push Manager and SSE Client. diff --git a/client/pom.xml b/client/pom.xml index b8d94bba7..2c1892ca2 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>io.split.client</groupId> <artifactId>java-client-parent</artifactId> - <version>4.12.1</version> + <version>4.13.0</version> </parent> <artifactId>java-client</artifactId> <packaging>jar</packaging> @@ -64,6 +64,7 @@ <include>io.split.schemas:*</include> <include>io.codigo.grammar:*</include> <include>org.apache.httpcomponents.*</include> + <include>org.apache.hc.*</include> <include>com.google.*</include> <include>org.yaml:snakeyaml:*</include> @@ -238,5 +239,17 @@ <version>4.0.3</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-module-junit4</artifactId> + <version>1.7.4</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-api-mockito</artifactId> + <version>1.7.4</version> + <scope>test</scope> + </dependency> </dependencies> </project> diff --git a/client/src/main/java/io/split/client/RequestDecorator.java b/client/src/main/java/io/split/client/RequestDecorator.java index 1572463ef..33059e617 100644 --- a/client/src/main/java/io/split/client/RequestDecorator.java +++ b/client/src/main/java/io/split/client/RequestDecorator.java @@ -2,16 +2,12 @@ import io.split.client.dtos.RequestContext; -import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.Header; import java.util.HashSet; -import java.util.HashMap; import java.util.Map; import java.util.Arrays; -import java.util.ArrayList; import java.util.Set; -import java.util.List; +import java.util.stream.Collectors; public final class RequestDecorator { CustomHeaderDecorator _headerDecorator; @@ -36,42 +32,16 @@ public RequestDecorator(CustomHeaderDecorator headerDecorator) { : headerDecorator; } - public HttpRequest decorateHeaders(HttpRequest request) { + public RequestContext decorateHeaders(RequestContext request) { try { - Map<String, List<String>> headers = _headerDecorator - .getHeaderOverrides(new RequestContext(convertToMap(request.getHeaders()))); - for (Map.Entry<String, List<String>> entry : headers.entrySet()) { - if (isHeaderAllowed(entry.getKey())) { - List<String> values = entry.getValue(); - for (int i = 0; i < values.size(); i++) { - if (i == 0) { - request.setHeader(entry.getKey(), values.get(i)); - } else { - request.addHeader(entry.getKey(), values.get(i)); - } - } - } - } + return new RequestContext(_headerDecorator.getHeaderOverrides(request) + .entrySet() + .stream() + .filter(e -> !forbiddenHeaders.contains(e.getKey().toLowerCase())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); } catch (Exception e) { throw new IllegalArgumentException( String.format("Problem adding custom headers to request decorator: %s", e), e); } - - return request; - } - - private boolean isHeaderAllowed(String headerName) { - return !forbiddenHeaders.contains(headerName.toLowerCase()); - } - - private Map<String, List<String>> convertToMap(Header[] to_convert) { - Map<String, List<String>> to_return = new HashMap<String, List<String>>(); - for (Integer i = 0; i < to_convert.length; i++) { - if (!to_return.containsKey(to_convert[i].getName())) { - to_return.put(to_convert[i].getName(), new ArrayList<String>()); - } - to_return.get(to_convert[i].getName()).add(to_convert[i].getValue()); - } - return to_return; } } diff --git a/client/src/main/java/io/split/client/SplitClientConfig.java b/client/src/main/java/io/split/client/SplitClientConfig.java index 2f29c1719..8787c1069 100644 --- a/client/src/main/java/io/split/client/SplitClientConfig.java +++ b/client/src/main/java/io/split/client/SplitClientConfig.java @@ -4,6 +4,7 @@ import io.split.client.impressions.ImpressionsManager; import io.split.client.utils.FileTypeEnum; import io.split.integrations.IntegrationsConfig; +import io.split.service.CustomHttpModule; import io.split.storages.enums.OperationMode; import io.split.storages.enums.StorageMode; import org.apache.hc.core5.http.HttpHost; @@ -91,7 +92,7 @@ public class SplitClientConfig { private final HashSet<String> _flagSetsFilter; private final int _invalidSets; private final CustomHeaderDecorator _customHeaderDecorator; - + private final CustomHttpModule _alternativeHTTPModule; public static Builder builder() { return new Builder(); @@ -148,7 +149,8 @@ private SplitClientConfig(String endpoint, ThreadFactory threadFactory, HashSet<String> flagSetsFilter, int invalidSets, - CustomHeaderDecorator customHeaderDecorator) { + CustomHeaderDecorator customHeaderDecorator, + CustomHttpModule alternativeHTTPModule) { _endpoint = endpoint; _eventsEndpoint = eventsEndpoint; _featuresRefreshRate = pollForFeatureChangesEveryNSeconds; @@ -201,6 +203,7 @@ private SplitClientConfig(String endpoint, _flagSetsFilter = flagSetsFilter; _invalidSets = invalidSets; _customHeaderDecorator = customHeaderDecorator; + _alternativeHTTPModule = alternativeHTTPModule; Properties props = new Properties(); try { @@ -409,6 +412,7 @@ public CustomHeaderDecorator customHeaderDecorator() { return _customHeaderDecorator; } + public CustomHttpModule alternativeHTTPModule() { return _alternativeHTTPModule; } public static final class Builder { private String _endpoint = SDK_ENDPOINT; @@ -466,6 +470,7 @@ public static final class Builder { private HashSet<String> _flagSetsFilter = new HashSet<>(); private int _invalidSetsCount = 0; private CustomHeaderDecorator _customHeaderDecorator = null; + private CustomHttpModule _alternativeHTTPModule = null; public Builder() { } @@ -960,6 +965,17 @@ public Builder customHeaderDecorator(CustomHeaderDecorator customHeaderDecorator return this; } + /** + * Alternative Http Client + * + * @param alternativeHTTPModule + * @return this builder + */ + public Builder alternativeHTTPModule(CustomHttpModule alternativeHTTPModule) { + _alternativeHTTPModule = alternativeHTTPModule; + return this; + } + /** * Thread Factory * @@ -971,7 +987,7 @@ public Builder threadFactory(ThreadFactory threadFactory) { return this; } - public SplitClientConfig build() { + private void verifyRates() { if (_featuresRefreshRate < 5 ) { throw new IllegalArgumentException("featuresRefreshRate must be >= 5: " + _featuresRefreshRate); } @@ -980,15 +996,6 @@ public SplitClientConfig build() { throw new IllegalArgumentException("segmentsRefreshRate must be >= 30: " + _segmentsRefreshRate); } - switch (_impressionsMode) { - case OPTIMIZED: - _impressionsRefreshRate = (_impressionsRefreshRate <= 0) ? 300 : Math.max(60, _impressionsRefreshRate); - break; - case DEBUG: - _impressionsRefreshRate = (_impressionsRefreshRate <= 0) ? 60 : _impressionsRefreshRate; - break; - } - if (_eventSendIntervalInMillis < 1000) { throw new IllegalArgumentException("_eventSendIntervalInMillis must be >= 1000: " + _eventSendIntervalInMillis); } @@ -996,19 +1003,12 @@ public SplitClientConfig build() { if (_metricsRefreshRate < 30) { throw new IllegalArgumentException("metricsRefreshRate must be >= 30: " + _metricsRefreshRate); } - - if (_impressionsQueueSize <=0 ) { - throw new IllegalArgumentException("impressionsQueueSize must be > 0: " + _impressionsQueueSize); - } - - if (_connectionTimeout <= 0) { - throw new IllegalArgumentException("connectionTimeOutInMs must be > 0: " + _connectionTimeout); - } - - if (_readTimeout <= 0) { - throw new IllegalArgumentException("readTimeout must be > 0: " + _readTimeout); + if(_telemetryRefreshRate < 60) { + throw new IllegalStateException("_telemetryRefreshRate must be >= 60"); } + } + private void verifyEndPoints() { if (_endpoint == null) { throw new IllegalArgumentException("endpoint must not be null"); } @@ -1021,18 +1021,6 @@ public SplitClientConfig build() { throw new IllegalArgumentException("If endpoint is set, you must also set the events endpoint"); } - if (_numThreadsForSegmentFetch <= 0) { - throw new IllegalArgumentException("Number of threads for fetching segments MUST be greater than zero"); - } - - if (_authRetryBackoffBase <= 0) { - throw new IllegalArgumentException("authRetryBackoffBase: must be >= 1"); - } - - if (_streamingReconnectBackoffBase <= 0) { - throw new IllegalArgumentException("streamingReconnectBackoffBase: must be >= 1"); - } - if (_authServiceURL == null) { throw new IllegalArgumentException("authServiceURL must not be null"); } @@ -1044,22 +1032,26 @@ public SplitClientConfig build() { if (_telemetryURl == null) { throw new IllegalArgumentException("telemetryURl must not be null"); } + } - if (_onDemandFetchRetryDelayMs <= 0) { - throw new IllegalStateException("streamingRetryDelay must be > 0"); + private void verifyAllModes() { + switch (_impressionsMode) { + case OPTIMIZED: + _impressionsRefreshRate = (_impressionsRefreshRate <= 0) ? 300 : Math.max(60, _impressionsRefreshRate); + break; + case DEBUG: + _impressionsRefreshRate = (_impressionsRefreshRate <= 0) ? 60 : _impressionsRefreshRate; + break; + case NONE: + break; } - if(_onDemandFetchMaxRetries <= 0) { - throw new IllegalStateException("_onDemandFetchMaxRetries must be > 0"); + if (_impressionsQueueSize <=0 ) { + throw new IllegalArgumentException("impressionsQueueSize must be > 0: " + _impressionsQueueSize); } - if(_storageMode == null) { _storageMode = StorageMode.MEMORY; } - - if(_telemetryRefreshRate < 60) { - throw new IllegalStateException("_telemetryRefreshRate must be >= 60"); - } if(OperationMode.CONSUMER.equals(_operationMode)){ if(_customStorageWrapper == null) { @@ -1067,8 +1059,56 @@ public SplitClientConfig build() { } _storageMode = StorageMode.PLUGGABLE; } + } + + private void verifyNetworkParams() { + if (_connectionTimeout <= 0) { + throw new IllegalArgumentException("connectionTimeOutInMs must be > 0: " + _connectionTimeout); + } + + if (_readTimeout <= 0) { + throw new IllegalArgumentException("readTimeout must be > 0: " + _readTimeout); + } + if (_authRetryBackoffBase <= 0) { + throw new IllegalArgumentException("authRetryBackoffBase: must be >= 1"); + } + + if (_streamingReconnectBackoffBase <= 0) { + throw new IllegalArgumentException("streamingReconnectBackoffBase: must be >= 1"); + } + + if (_onDemandFetchRetryDelayMs <= 0) { + throw new IllegalStateException("streamingRetryDelay must be > 0"); + } + + if(_onDemandFetchMaxRetries <= 0) { + throw new IllegalStateException("_onDemandFetchMaxRetries must be > 0"); + } + } + + private void verifyAlternativeClient() { + if (_alternativeHTTPModule != null && _streamingEnabled) { + throw new IllegalArgumentException("Streaming feature is not supported with Alternative HTTP Client"); + } + } + + public SplitClientConfig build() { + + verifyRates(); + + verifyAllModes(); + + verifyEndPoints(); + + verifyNetworkParams(); + + verifyAlternativeClient(); + + if (_numThreadsForSegmentFetch <= 0) { + throw new IllegalArgumentException("Number of threads for fetching segments MUST be greater than zero"); + } - return new SplitClientConfig( + return new SplitClientConfig( _endpoint, _eventsEndpoint, _featuresRefreshRate, @@ -1120,7 +1160,8 @@ public SplitClientConfig build() { _threadFactory, _flagSetsFilter, _invalidSetsCount, - _customHeaderDecorator); + _customHeaderDecorator, + _alternativeHTTPModule); } } } \ No newline at end of file diff --git a/client/src/main/java/io/split/client/SplitFactoryBuilder.java b/client/src/main/java/io/split/client/SplitFactoryBuilder.java index c2271ec4f..2b48fb0d3 100644 --- a/client/src/main/java/io/split/client/SplitFactoryBuilder.java +++ b/client/src/main/java/io/split/client/SplitFactoryBuilder.java @@ -2,6 +2,7 @@ import io.split.inputValidation.ApiKeyValidator; import io.split.grammar.Treatments; +import io.split.service.SplitHttpClient; import io.split.storages.enums.StorageMode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/client/src/main/java/io/split/client/SplitFactoryImpl.java b/client/src/main/java/io/split/client/SplitFactoryImpl.java index a783b1445..3102d3e17 100644 --- a/client/src/main/java/io/split/client/SplitFactoryImpl.java +++ b/client/src/main/java/io/split/client/SplitFactoryImpl.java @@ -57,8 +57,9 @@ import io.split.engine.segments.SegmentChangeFetcher; import io.split.engine.segments.SegmentSynchronizationTaskImp; import io.split.integrations.IntegrationsConfig; -import io.split.service.SplitHttpClient; import io.split.service.SplitHttpClientImpl; +import io.split.service.SplitHttpClient; + import io.split.storages.SegmentCache; import io.split.storages.SegmentCacheConsumer; import io.split.storages.SegmentCacheProducer; @@ -83,6 +84,7 @@ import io.split.telemetry.synchronizer.TelemetryInMemorySubmitter; import io.split.telemetry.synchronizer.TelemetrySyncTask; import io.split.telemetry.synchronizer.TelemetrySynchronizer; + import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.Credentials; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; @@ -102,7 +104,6 @@ import org.apache.hc.core5.ssl.SSLContexts; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; -import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pluggable.CustomStorageWrapper; @@ -111,16 +112,16 @@ import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; +import java.util.HashSet; +import java.util.List; +import java.util.ArrayList; import static io.split.client.utils.SplitExecutorFactory.buildExecutorService; public class SplitFactoryImpl implements SplitFactory { - private static final Logger _log = LoggerFactory.getLogger(SplitFactory.class); + private static final org.slf4j.Logger _log = LoggerFactory.getLogger(SplitFactoryImpl.class); private static final String LEGACY_LOG_MESSAGE = "The sdk initialize in localhost mode using Legacy file. The splitFile or " + "inputStream doesn't add it to the config."; @@ -155,15 +156,16 @@ public class SplitFactoryImpl implements SplitFactory { private final SplitSynchronizationTask _splitSynchronizationTask; private final EventsTask _eventsTask; private final SyncManager _syncManager; - private final SplitHttpClient _splitHttpClient; + private SplitHttpClient _splitHttpClient; private final UserStorageWrapper _userStorageWrapper; private final ImpressionsSender _impressionsSender; private final URI _rootTarget; private final URI _eventsRootTarget; private final UniqueKeysTracker _uniqueKeysTracker; + private RequestDecorator _requestDecorator; // Constructor for standalone mode - public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyntaxException { + public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyntaxException, IOException { _userStorageWrapper = null; _operationMode = config.operationMode(); _startTime = System.currentTimeMillis(); @@ -186,9 +188,13 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn // SDKReadinessGates _gates = new SDKReadinessGates(); + _requestDecorator = new RequestDecorator(config.customHeaderDecorator()); // HttpClient - RequestDecorator requestDecorator = new RequestDecorator(config.customHeaderDecorator()); - _splitHttpClient = buildSplitHttpClient(apiToken, config, _sdkMetadata, requestDecorator); + if (config.alternativeHTTPModule() == null) { + _splitHttpClient = buildSplitHttpClient(apiToken, config, _sdkMetadata, _requestDecorator); + } else { + _splitHttpClient = config.alternativeHTTPModule().createClient(apiToken, _sdkMetadata, _requestDecorator); + } // Roots _rootTarget = URI.create(config.endpoint()); @@ -234,7 +240,8 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn EventsSender eventsSender = EventsSender.create(_splitHttpClient, _eventsRootTarget, _telemetryStorageProducer); _eventsTask = EventsTask.create(config.eventSendIntervalInMillis(), eventsStorage, eventsSender, config.getThreadFactory()); - _telemetrySyncTask = new TelemetrySyncTask(config.getTelemetryRefreshRate(), _telemetrySynchronizer, config.getThreadFactory()); + _telemetrySyncTask = new TelemetrySyncTask(config.getTelemetryRefreshRate(), _telemetrySynchronizer, + config.getThreadFactory()); // Evaluator _evaluator = new EvaluatorImp(splitCache, segmentCache); @@ -257,7 +264,8 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn // SyncManager SplitTasks splitTasks = SplitTasks.build(_splitSynchronizationTask, _segmentSynchronizationTaskImp, _impressionsManager, _eventsTask, _telemetrySyncTask, _uniqueKeysTracker); - SplitAPI splitAPI = SplitAPI.build(_splitHttpClient, buildSSEdHttpClient(apiToken, config, _sdkMetadata), requestDecorator); + SplitAPI splitAPI = SplitAPI.build(_splitHttpClient, buildSSEdHttpClient(apiToken, config, _sdkMetadata), + _requestDecorator); _syncManager = SyncManagerImp.build(splitTasks, _splitFetcher, splitCache, splitAPI, segmentCache, _gates, _telemetryStorageProducer, _telemetrySynchronizer, config, splitParser, @@ -328,8 +336,10 @@ protected SplitFactoryImpl(String apiToken, SplitClientConfig config, CustomStor _evaluator = new EvaluatorImp(userCustomSplitAdapterConsumer, userCustomSegmentAdapterConsumer); _impressionsSender = PluggableImpressionSender.create(customStorageWrapper); _uniqueKeysTracker = createUniqueKeysTracker(config); - _impressionsManager = buildImpressionsManager(config, userCustomImpressionAdapterConsumer, userCustomImpressionAdapterProducer); - _telemetrySyncTask = new TelemetrySyncTask(config.getTelemetryRefreshRate(), _telemetrySynchronizer, config.getThreadFactory()); + _impressionsManager = buildImpressionsManager(config, userCustomImpressionAdapterConsumer, + userCustomImpressionAdapterProducer); + _telemetrySyncTask = new TelemetrySyncTask(config.getTelemetryRefreshRate(), _telemetrySynchronizer, + config.getThreadFactory()); SplitTasks splitTasks = SplitTasks.build(null, null, _impressionsManager, null, _telemetrySyncTask, _uniqueKeysTracker); @@ -491,7 +501,7 @@ public boolean isDestroyed() { return isTerminated; } - private static SplitHttpClient buildSplitHttpClient(String apiToken, SplitClientConfig config, + protected static SplitHttpClient buildSplitHttpClient(String apiToken, SplitClientConfig config, SDKMetadata sdkMetadata, RequestDecorator requestDecorator) throws URISyntaxException { SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create() diff --git a/client/src/main/java/io/split/client/dtos/SplitHttpResponse.java b/client/src/main/java/io/split/client/dtos/SplitHttpResponse.java index a5474cf5b..259ed0794 100644 --- a/client/src/main/java/io/split/client/dtos/SplitHttpResponse.java +++ b/client/src/main/java/io/split/client/dtos/SplitHttpResponse.java @@ -1,7 +1,7 @@ package io.split.client.dtos; -import java.util.Map; -import org.apache.hc.core5.http.Header; +import java.util.List; + /** * A structure for returning http call results information */ @@ -11,15 +11,42 @@ public class SplitHttpResponse { private final String _body; private final Header[] _responseHeaders; + public static class Header { + private String _name; + private List<String> _values; + + public Header(String name, List<String> values) { + _name = name; + _values = values; + } + + public String getName() { + return _name; + } + + public List<String> getValues() { + return _values; + } + }; + public SplitHttpResponse(Integer statusCode, String statusMessage, String body, Header[] headers) { _statusCode = statusCode; _statusMessage = statusMessage; _body = body; _responseHeaders = headers; } + + public SplitHttpResponse(Integer statusCode, String statusMessage, String body, List<Header> headers) { + _statusCode = statusCode; + _statusMessage = statusMessage; + _body = body; + _responseHeaders = headers.toArray(new Header[0]); + } + public Integer statusCode() { return _statusCode; } + public String statusMessage() { return _statusMessage; } diff --git a/client/src/main/java/io/split/client/impressions/HttpImpressionsSender.java b/client/src/main/java/io/split/client/impressions/HttpImpressionsSender.java index 06df64cc4..35c0f57f2 100644 --- a/client/src/main/java/io/split/client/impressions/HttpImpressionsSender.java +++ b/client/src/main/java/io/split/client/impressions/HttpImpressionsSender.java @@ -4,6 +4,7 @@ import io.split.client.dtos.ImpressionCount; import io.split.client.dtos.SplitHttpResponse; import io.split.client.dtos.TestImpressions; +import io.split.client.utils.Json; import io.split.client.utils.Utils; import io.split.service.SplitHttpClient; @@ -11,7 +12,6 @@ import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; import io.split.telemetry.domain.enums.ResourceEnum; import io.split.telemetry.storage.TelemetryRuntimeProducer; -import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,10 +67,12 @@ private HttpImpressionsSender(SplitHttpClient client, URI impressionBulkTarget, public void postImpressionsBulk(List<TestImpressions> impressions) { long initTime = System.currentTimeMillis(); try { - HttpEntity entity = Utils.toJsonEntity(impressions); - Map<String, List<String>> additionalHeaders = Collections.singletonMap(IMPRESSIONS_MODE_HEADER, - Collections.singletonList(_mode.toString())); - SplitHttpResponse response = _client.post(_impressionBulkTarget, entity, additionalHeaders); + Map<String, List<String>> additionalHeaders = new HashMap<>(); + additionalHeaders.put(IMPRESSIONS_MODE_HEADER, Collections.singletonList(_mode.toString())); + additionalHeaders.put("Content-Type", Collections.singletonList("application/json")); + + SplitHttpResponse response = _client.post(_impressionBulkTarget, Json.toJson(impressions), + additionalHeaders); if (response.statusCode() < HttpStatus.SC_OK || response.statusCode() >= HttpStatus.SC_MULTIPLE_CHOICES) { _telemetryRuntimeProducer.recordSyncError(ResourceEnum.IMPRESSION_SYNC, response.statusCode()); @@ -95,8 +97,12 @@ public void postCounters(HashMap<ImpressionCounter.Key, Integer> raw) { } try { + + Map<String, List<String>> additionalHeaders = new HashMap<>(); + additionalHeaders.put("Content-Type", Collections.singletonList("application/json")); + SplitHttpResponse response = _client.post(_impressionCountTarget, - Utils.toJsonEntity(ImpressionCount.fromImpressionCounterData(raw)), + Json.toJson(ImpressionCount.fromImpressionCounterData(raw)), null); if (response.statusCode() < HttpStatus.SC_OK || response.statusCode() >= HttpStatus.SC_MULTIPLE_CHOICES) { diff --git a/client/src/main/java/io/split/client/utils/ApacheRequestDecorator.java b/client/src/main/java/io/split/client/utils/ApacheRequestDecorator.java new file mode 100644 index 000000000..c64d9d46c --- /dev/null +++ b/client/src/main/java/io/split/client/utils/ApacheRequestDecorator.java @@ -0,0 +1,43 @@ +package io.split.client.utils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpRequest; + +import io.split.client.RequestDecorator; +import io.split.client.dtos.RequestContext; + +public class ApacheRequestDecorator { + + public static HttpRequest decorate(HttpRequest request, RequestDecorator decorator) { + + RequestContext ctx = new RequestContext(convertToMap(request.getHeaders())); + for (Map.Entry<String, List<String>> entry : decorator.decorateHeaders(ctx).headers().entrySet()) { + List<String> values = entry.getValue(); + for (int i = 0; i < values.size(); i++) { + if (i == 0) { + request.setHeader(entry.getKey(), values.get(i)); + } else { + request.addHeader(entry.getKey(), values.get(i)); + } + } + } + + return request; + } + + private static Map<String, List<String>> convertToMap(Header[] to_convert) { + Map<String, List<String>> to_return = new HashMap<String, List<String>>(); + for (Integer i = 0; i < to_convert.length; i++) { + if (!to_return.containsKey(to_convert[i].getName())) { + to_return.put(to_convert[i].getName(), new ArrayList<String>()); + } + to_return.get(to_convert[i].getName()).add(to_convert[i].getValue()); + } + return to_return; + } +} diff --git a/client/src/main/java/io/split/engine/common/PushManagerImp.java b/client/src/main/java/io/split/engine/common/PushManagerImp.java index 3c15481fd..653249308 100644 --- a/client/src/main/java/io/split/engine/common/PushManagerImp.java +++ b/client/src/main/java/io/split/engine/common/PushManagerImp.java @@ -84,6 +84,7 @@ public static PushManagerImp build(Synchronizer synchronizer, telemetryRuntimeProducer, flagSetsFilter); Worker<SegmentQueueDto> segmentWorker = new SegmentsWorkerImp(synchronizer); PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(statusMessages, telemetryRuntimeProducer); + return new PushManagerImp(new AuthApiClientImp(authUrl, splitAPI.getHttpClient(), telemetryRuntimeProducer), EventSourceClientImp.build(streamingUrl, featureFlagsWorker, segmentWorker, pushStatusTracker, splitAPI.getSseHttpClient(), telemetryRuntimeProducer, threadFactory, splitAPI.getRequestDecorator()), diff --git a/client/src/main/java/io/split/engine/sse/client/SSEClient.java b/client/src/main/java/io/split/engine/sse/client/SSEClient.java index 37cc6dac9..aac6f5566 100644 --- a/client/src/main/java/io/split/engine/sse/client/SSEClient.java +++ b/client/src/main/java/io/split/engine/sse/client/SSEClient.java @@ -2,6 +2,7 @@ import com.google.common.base.Strings; import io.split.client.RequestDecorator; +import io.split.client.utils.ApacheRequestDecorator; import io.split.telemetry.domain.StreamingEvent; import io.split.telemetry.domain.enums.StreamEventsEnum; import io.split.telemetry.storage.TelemetryRuntimeProducer; @@ -64,11 +65,11 @@ private enum ConnectionState { private final TelemetryRuntimeProducer _telemetryRuntimeProducer; public SSEClient(Function<RawEvent, Void> eventCallback, - Function<StatusMessage, Void> statusCallback, - CloseableHttpClient client, - TelemetryRuntimeProducer telemetryRuntimeProducer, - ThreadFactory threadFactory, - RequestDecorator requestDecorator) { + Function<StatusMessage, Void> statusCallback, + CloseableHttpClient client, + TelemetryRuntimeProducer telemetryRuntimeProducer, + ThreadFactory threadFactory, + RequestDecorator requestDecorator) { _eventCallback = eventCallback; _statusCallback = statusCallback; _client = client; @@ -96,7 +97,7 @@ public boolean open(URI uri) { } } catch (InterruptedException e) { Thread.currentThread().interrupt(); - if(e.getMessage() == null){ + if (e.getMessage() == null) { _log.info("The thread was interrupted while opening SSEClient"); return false; } @@ -152,31 +153,41 @@ private void connectAndLoop(URI uri, CountDownLatch signal) { _log.debug(exc.getMessage()); if (SOCKET_CLOSED_MESSAGE.equals(exc.getMessage())) { // Connection closed by us _statusCallback.apply(StatusMessage.FORCED_STOP); - _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), - StreamEventsEnum.SseConnectionErrorValues.REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis())); + _telemetryRuntimeProducer.recordStreamingEvents( + new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), + StreamEventsEnum.SseConnectionErrorValues.REQUESTED_CONNECTION_ERROR.getValue(), + System.currentTimeMillis())); return; } // Connection closed by server _statusCallback.apply(StatusMessage.RETRYABLE_ERROR); - _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), - StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis())); + _telemetryRuntimeProducer + .recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), + StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(), + System.currentTimeMillis())); return; } catch (IOException exc) { // Other type of connection error - if(!_forcedStop.get()) { + if (!_forcedStop.get()) { _log.debug(String.format("SSE connection ended abruptly: %s. Retying", exc.getMessage())); - _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), - StreamEventsEnum.SseConnectionErrorValues.REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis())); + _telemetryRuntimeProducer.recordStreamingEvents( + new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), + StreamEventsEnum.SseConnectionErrorValues.REQUESTED_CONNECTION_ERROR.getValue(), + System.currentTimeMillis())); _statusCallback.apply(StatusMessage.RETRYABLE_ERROR); return; } - _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), - StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis())); + _telemetryRuntimeProducer + .recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), + StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(), + System.currentTimeMillis())); } } } catch (Exception e) { // Any other error non related to the connection disables streaming altogether - _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), - StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis())); + _telemetryRuntimeProducer + .recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), + StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(), + System.currentTimeMillis())); _log.warn(e.getMessage(), e); _statusCallback.apply(StatusMessage.NONRETRYABLE_ERROR); } finally { @@ -194,12 +205,13 @@ private void connectAndLoop(URI uri, CountDownLatch signal) { private boolean establishConnection(URI uri, CountDownLatch signal) { HttpGet request = new HttpGet(uri); - request = (HttpGet) _requestDecorator.decorateHeaders(request); + request = (HttpGet) ApacheRequestDecorator.decorate(request, _requestDecorator); _ongoingRequest.set(request); try { _ongoingResponse.set(_client.execute(_ongoingRequest.get())); if (_ongoingResponse.get().getCode() != 200) { - _log.error(String.format("Establishing connection, code error: %s. The url is %s", _ongoingResponse.get().getCode(), uri.toURL())); + _log.error(String.format("Establishing connection, code error: %s. The url is %s", + _ongoingResponse.get().getCode(), uri.toURL())); return false; } _state.set(ConnectionState.OPEN); @@ -236,4 +248,4 @@ private void handleMessage(String message) { RawEvent e = RawEvent.fromString(message); _eventCallback.apply(e); } -} \ No newline at end of file +} diff --git a/client/src/main/java/io/split/service/CustomHttpModule.java b/client/src/main/java/io/split/service/CustomHttpModule.java new file mode 100644 index 000000000..001648fb3 --- /dev/null +++ b/client/src/main/java/io/split/service/CustomHttpModule.java @@ -0,0 +1,11 @@ +package io.split.service; + +import io.split.client.RequestDecorator; +import io.split.client.utils.SDKMetadata; + +import java.io.IOException; + +public interface CustomHttpModule { + public SplitHttpClient createClient(String apiToken, SDKMetadata sdkMetadata, RequestDecorator decorator) + throws IOException; +} diff --git a/client/src/main/java/io/split/service/HttpPostImp.java b/client/src/main/java/io/split/service/HttpPostImp.java index e5baa001b..b33bf2103 100644 --- a/client/src/main/java/io/split/service/HttpPostImp.java +++ b/client/src/main/java/io/split/service/HttpPostImp.java @@ -1,15 +1,18 @@ package io.split.service; import io.split.client.dtos.SplitHttpResponse; -import io.split.client.utils.Utils; +import io.split.client.utils.Json; import io.split.telemetry.domain.enums.HttpParamsWrapper; import io.split.telemetry.storage.TelemetryRuntimeProducer; -import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static com.google.common.base.Preconditions.checkNotNull; @@ -25,16 +28,19 @@ public HttpPostImp(SplitHttpClient client, TelemetryRuntimeProducer telemetryRun public void post(URI uri, Object object, String posted, HttpParamsWrapper httpParamsWrapper) { long initTime = System.currentTimeMillis(); - HttpEntity entity = Utils.toJsonEntity(object); try { - SplitHttpResponse response = _client.post(uri, entity, null); + Map<String, List<String>> headers = new HashMap<>(); + headers.put("Content-Type", Collections.singletonList("application/json")); + SplitHttpResponse response = _client.post(uri, Json.toJson(object), headers); if (response.statusCode() < HttpStatus.SC_OK || response.statusCode() >= HttpStatus.SC_MULTIPLE_CHOICES) { _telemetryRuntimeProducer.recordSyncError(httpParamsWrapper.getResourceEnum(), response.statusCode()); return; } - _telemetryRuntimeProducer.recordSyncLatency(httpParamsWrapper.getHttpLatenciesEnum(), System.currentTimeMillis() - initTime); - _telemetryRuntimeProducer.recordSuccessfulSync(httpParamsWrapper.getLastSynchronizationRecordsEnum(), System.currentTimeMillis()); + _telemetryRuntimeProducer.recordSyncLatency(httpParamsWrapper.getHttpLatenciesEnum(), + System.currentTimeMillis() - initTime); + _telemetryRuntimeProducer.recordSuccessfulSync(httpParamsWrapper.getLastSynchronizationRecordsEnum(), + System.currentTimeMillis()); } catch (Throwable t) { _logger.warn("Exception when posting " + posted + object, t); } diff --git a/client/src/main/java/io/split/service/SplitHttpClient.java b/client/src/main/java/io/split/service/SplitHttpClient.java index 1c88bcd4e..899fcf56b 100644 --- a/client/src/main/java/io/split/service/SplitHttpClient.java +++ b/client/src/main/java/io/split/service/SplitHttpClient.java @@ -3,8 +3,6 @@ import io.split.engine.common.FetchOptions; import io.split.client.dtos.SplitHttpResponse; -import org.apache.hc.core5.http.HttpEntity; - import java.io.Closeable; import java.io.IOException; import java.net.URI; @@ -30,6 +28,6 @@ public interface SplitHttpClient extends Closeable { * @return The response structure SplitHttpResponse */ public SplitHttpResponse post(URI uri, - HttpEntity entity, + String entity, Map<String, List<String>> additionalHeaders) throws IOException; } diff --git a/client/src/main/java/io/split/service/SplitHttpClientImpl.java b/client/src/main/java/io/split/service/SplitHttpClientImpl.java index 64ca3a55c..7f0674411 100644 --- a/client/src/main/java/io/split/service/SplitHttpClientImpl.java +++ b/client/src/main/java/io/split/service/SplitHttpClientImpl.java @@ -1,6 +1,7 @@ package io.split.service; import io.split.client.RequestDecorator; +import io.split.client.utils.ApacheRequestDecorator; import io.split.client.utils.SDKMetadata; import io.split.client.utils.Utils; import io.split.engine.common.FetchOptions; @@ -9,9 +10,10 @@ import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.HttpEntities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; @@ -19,8 +21,11 @@ import java.net.URISyntaxException; import org.apache.hc.core5.http.HttpRequest; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; public final class SplitHttpClientImpl implements SplitHttpClient { @@ -72,7 +77,7 @@ public SplitHttpResponse get(URI uri, FetchOptions options, Map<String, List<Str request.setHeader(HEADER_CACHE_CONTROL_NAME, HEADER_CACHE_CONTROL_VALUE); } - _requestDecorator.decorateHeaders(request); + request = (HttpGet) ApacheRequestDecorator.decorate(request, _requestDecorator); response = _client.execute(request); @@ -87,10 +92,14 @@ public SplitHttpResponse get(URI uri, FetchOptions options, Map<String, List<Str response.getReasonPhrase())); statusMessage = response.getReasonPhrase(); } + return new SplitHttpResponse(response.getCode(), statusMessage, EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8), - response.getHeaders()); + Arrays.stream(response.getHeaders()).map( + h -> new SplitHttpResponse.Header(h.getName(), Collections.singletonList(h.getValue()))) + .collect(Collectors.toList())); + // response.getHeaders()); } catch (Exception e) { throw new IllegalStateException(String.format("Problem in http get operation: %s", e), e); } finally { @@ -98,7 +107,7 @@ public SplitHttpResponse get(URI uri, FetchOptions options, Map<String, List<Str } } - public SplitHttpResponse post(URI uri, HttpEntity entity, Map<String, List<String>> additionalHeaders) + public SplitHttpResponse post(URI uri, String body, Map<String, List<String>> additionalHeaders) throws IOException { CloseableHttpResponse response = null; @@ -112,8 +121,8 @@ public SplitHttpResponse post(URI uri, HttpEntity entity, Map<String, List<Strin } } } - request.setEntity(entity); - request = (HttpPost) _requestDecorator.decorateHeaders(request); + request.setEntity(HttpEntities.create(body, ContentType.APPLICATION_JSON)); + request = (HttpPost) ApacheRequestDecorator.decorate(request, _requestDecorator); response = _client.execute(request); @@ -123,7 +132,10 @@ public SplitHttpResponse post(URI uri, HttpEntity entity, Map<String, List<Strin _log.warn(String.format("Response status was: %s. Reason: %s", response.getCode(), response.getReasonPhrase())); } - return new SplitHttpResponse(response.getCode(), statusMessage, "", response.getHeaders()); + return new SplitHttpResponse(response.getCode(), statusMessage, "", + Arrays.stream(response.getHeaders()).map( + h -> new SplitHttpResponse.Header(h.getName(), Collections.singletonList(h.getValue()))) + .collect(Collectors.toList())); } catch (Exception e) { throw new IOException(String.format("Problem in http post operation: %s", e), e); } finally { diff --git a/client/src/test/java/io/split/client/LocalhostSplitFactoryYamlTest.java b/client/src/test/java/io/split/client/LocalhostSplitFactoryYamlTest.java index abcc551fe..0a154f7d4 100644 --- a/client/src/test/java/io/split/client/LocalhostSplitFactoryYamlTest.java +++ b/client/src/test/java/io/split/client/LocalhostSplitFactoryYamlTest.java @@ -2,6 +2,7 @@ import io.split.client.utils.LocalhostUtils; import io.split.grammar.Treatments; +import io.split.service.SplitHttpClient; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; diff --git a/client/src/test/java/io/split/client/SplitFactoryImplTest.java b/client/src/test/java/io/split/client/SplitFactoryImplTest.java index 2d548f9e6..f2f7e3efc 100644 --- a/client/src/test/java/io/split/client/SplitFactoryImplTest.java +++ b/client/src/test/java/io/split/client/SplitFactoryImplTest.java @@ -11,10 +11,11 @@ import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; +import static org.mockito.Mockito.when; import pluggable.CustomStorageWrapper; import java.io.FileInputStream; -import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -22,6 +23,7 @@ import java.lang.reflect.Modifier; import java.net.URISyntaxException; + public class SplitFactoryImplTest extends TestCase { public static final String API_KEY ="29013ionasdasd09u"; public static final String ENDPOINT = "https://sdk.split-stage.io"; @@ -135,7 +137,7 @@ public void testFactoryConsumerInstantiation() throws Exception { CustomStorageWrapper customStorageWrapper = Mockito.mock(CustomStorageWrapper.class); UserStorageWrapper userStorageWrapper = Mockito.mock(UserStorageWrapper.class); TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetrySynchronizer.class); - Mockito.when(userStorageWrapper.connect()).thenReturn(true); + when(userStorageWrapper.connect()).thenReturn(true); SplitClientConfig splitClientConfig = SplitClientConfig.builder() .enableDebug() @@ -173,7 +175,7 @@ public void testFactoryConsumerInstantiation() throws Exception { public void testFactoryConsumerInstantiationRetryReadiness() throws Exception { CustomStorageWrapper customStorageWrapper = Mockito.mock(CustomStorageWrapper.class); UserStorageWrapper userStorageWrapper = Mockito.mock(UserStorageWrapper.class); - Mockito.when(userStorageWrapper.connect()).thenReturn(false).thenReturn(true); + when(userStorageWrapper.connect()).thenReturn(false).thenReturn(true); SplitClientConfig splitClientConfig = SplitClientConfig.builder() .enableDebug() .impressionsMode(ImpressionsManager.Mode.DEBUG) @@ -202,7 +204,7 @@ public void testFactoryConsumerInstantiationRetryReadiness() throws Exception { public void testFactoryConsumerDestroy() throws NoSuchFieldException, URISyntaxException, IllegalAccessException { CustomStorageWrapper customStorageWrapper = Mockito.mock(CustomStorageWrapper.class); UserStorageWrapper userStorageWrapper = Mockito.mock(UserStorageWrapper.class); - Mockito.when(userStorageWrapper.connect()).thenReturn(false).thenReturn(true); + when(userStorageWrapper.connect()).thenReturn(false).thenReturn(true); SplitClientConfig splitClientConfig = SplitClientConfig.builder() .enableDebug() .impressionsMode(ImpressionsManager.Mode.DEBUG) @@ -228,7 +230,7 @@ public void testFactoryConsumerDestroy() throws NoSuchFieldException, URISyntaxE } @Test - public void testLocalhostLegacy() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + public void testLocalhostLegacy() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException { SplitClientConfig splitClientConfig = SplitClientConfig.builder() .setBlockUntilReadyTimeout(10000) .build(); @@ -241,7 +243,7 @@ public void testLocalhostLegacy() throws URISyntaxException, NoSuchMethodExcepti } @Test - public void testLocalhostYaml() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + public void testLocalhostYaml() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException { SplitClientConfig splitClientConfig = SplitClientConfig.builder() .splitFile("src/test/resources/split.yaml") .setBlockUntilReadyTimeout(10000) @@ -255,7 +257,7 @@ public void testLocalhostYaml() throws URISyntaxException, NoSuchMethodException } @Test - public void testLocalhosJson() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + public void testLocalhosJson() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException { SplitClientConfig splitClientConfig = SplitClientConfig.builder() .splitFile("src/test/resources/split_init.json") .setBlockUntilReadyTimeout(10000) @@ -270,7 +272,7 @@ public void testLocalhosJson() throws URISyntaxException, NoSuchMethodException, @Test public void testLocalhostYamlInputStream() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, - IllegalAccessException, FileNotFoundException { + IllegalAccessException, IOException { InputStream inputStream = new FileInputStream("src/test/resources/split.yaml"); SplitClientConfig splitClientConfig = SplitClientConfig.builder() .splitFile(inputStream, FileTypeEnum.YAML) @@ -286,7 +288,7 @@ public void testLocalhostYamlInputStream() throws URISyntaxException, NoSuchMeth @Test public void testLocalhosJsonInputStream() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, - IllegalAccessException, FileNotFoundException { + IllegalAccessException, IOException { InputStream inputStream = new FileInputStream("src/test/resources/split_init.json"); SplitClientConfig splitClientConfig = SplitClientConfig.builder() .splitFile(inputStream, FileTypeEnum.JSON) @@ -301,7 +303,7 @@ public void testLocalhosJsonInputStream() throws URISyntaxException, NoSuchMetho } @Test - public void testLocalhosJsonInputStreamNull() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + public void testLocalhosJsonInputStreamNull() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException { SplitClientConfig splitClientConfig = SplitClientConfig.builder() .splitFile(null, FileTypeEnum.JSON) .setBlockUntilReadyTimeout(10000) @@ -316,7 +318,7 @@ public void testLocalhosJsonInputStreamNull() throws URISyntaxException, NoSuchM @Test public void testLocalhosJsonInputStreamAndFileTypeNull() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, - IllegalAccessException, FileNotFoundException { + IllegalAccessException, IOException { InputStream inputStream = new FileInputStream("src/test/resources/split_init.json"); SplitClientConfig splitClientConfig = SplitClientConfig.builder() .splitFile(inputStream, null) @@ -332,7 +334,7 @@ public void testLocalhosJsonInputStreamAndFileTypeNull() throws URISyntaxExcepti @Test public void testLocalhosJsonInputStreamNullAndFileTypeNull() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, - IllegalAccessException { + IllegalAccessException, IOException { SplitClientConfig splitClientConfig = SplitClientConfig.builder() .splitFile(null, null) .setBlockUntilReadyTimeout(10000) diff --git a/client/src/test/java/io/split/client/RequestDecoratorTest.java b/client/src/test/java/io/split/client/utils/ApacheRequestDecoratorTest.java similarity index 80% rename from client/src/test/java/io/split/client/RequestDecoratorTest.java rename to client/src/test/java/io/split/client/utils/ApacheRequestDecoratorTest.java index 62868eb40..5d5971bb8 100644 --- a/client/src/test/java/io/split/client/RequestDecoratorTest.java +++ b/client/src/test/java/io/split/client/utils/ApacheRequestDecoratorTest.java @@ -1,32 +1,31 @@ -package io.split.client; +package io.split.client.utils; +import io.split.client.CustomHeaderDecorator; +import io.split.client.RequestDecorator; +import io.split.client.dtos.RequestContext; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.ProtocolException; import org.junit.Assert; import org.junit.Test; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; - -import io.split.client.dtos.RequestContext; import java.util.List; import java.util.Arrays; -import java.util.HashMap; import java.util.Map; -public class RequestDecoratorTest { +public class ApacheRequestDecoratorTest { @Test public void testNoOp() { - RequestDecorator decorator = new RequestDecorator(null); + ApacheRequestDecorator apacheRequestDecorator = new ApacheRequestDecorator(); + RequestDecorator requestDecorator = new RequestDecorator(null); HttpGet request = new HttpGet("http://anyhost"); - request = (HttpGet) decorator.decorateHeaders(request); + + request = (HttpGet) apacheRequestDecorator.decorate(request, requestDecorator); Assert.assertEquals(0, request.getHeaders().length); request.addHeader("myheader", "value"); - request = (HttpGet) decorator.decorateHeaders(request); + request = (HttpGet) apacheRequestDecorator.decorate(request, requestDecorator); Assert.assertEquals(1, request.getHeaders().length); } @@ -45,9 +44,11 @@ public Map<String, List<String>> getHeaderOverrides(RequestContext context) { } MyCustomHeaders myHeaders = new MyCustomHeaders(); RequestDecorator decorator = new RequestDecorator(myHeaders); + ApacheRequestDecorator apacheRequestDecorator = new ApacheRequestDecorator(); + HttpGet request = new HttpGet("http://anyhost"); request.addHeader("first", "myfirstheader"); - request = (HttpGet) decorator.decorateHeaders(request); + request = (HttpGet) apacheRequestDecorator.decorate(request, decorator); Assert.assertEquals(4, request.getHeaders().length); Assert.assertEquals("1", request.getHeader("first").getValue()); @@ -59,7 +60,7 @@ public Map<String, List<String>> getHeaderOverrides(RequestContext context) { HttpPost request2 = new HttpPost("http://anyhost"); request2.addHeader("myheader", "value"); - request2 = (HttpPost) decorator.decorateHeaders(request2); + request2 = (HttpPost) apacheRequestDecorator.decorate(request2, decorator); Assert.assertEquals(5, request2.getHeaders().length); } @@ -88,8 +89,9 @@ public Map<String, List<String>> getHeaderOverrides(RequestContext context) { } MyCustomHeaders myHeaders = new MyCustomHeaders(); RequestDecorator decorator = new RequestDecorator(myHeaders); + ApacheRequestDecorator apacheRequestDecorator = new ApacheRequestDecorator(); HttpGet request = new HttpGet("http://anyhost"); - request = (HttpGet) decorator.decorateHeaders(request); + request = (HttpGet) apacheRequestDecorator.decorate(request, decorator); Assert.assertEquals(1, request.getHeaders().length); Assert.assertEquals(null, request.getHeader("SplitSDKVersion")); } @@ -105,7 +107,8 @@ public Map<String, List<String>> getHeaderOverrides(RequestContext context) { } MyCustomHeaders myHeaders = new MyCustomHeaders(); RequestDecorator decorator = new RequestDecorator(myHeaders); + ApacheRequestDecorator apacheRequestDecorator = new ApacheRequestDecorator(); HttpGet request = new HttpGet("http://anyhost"); - request = (HttpGet) decorator.decorateHeaders(request); + request = (HttpGet) apacheRequestDecorator.decorate(request, decorator); } } \ No newline at end of file diff --git a/client/src/test/java/io/split/service/HttpSplitClientTest.java b/client/src/test/java/io/split/service/HttpSplitClientTest.java index 946775f39..4d18a080d 100644 --- a/client/src/test/java/io/split/service/HttpSplitClientTest.java +++ b/client/src/test/java/io/split/service/HttpSplitClientTest.java @@ -16,7 +16,7 @@ import org.apache.hc.client5.http.classic.methods.HttpUriRequest; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.Header; +//import org.apache.hc.core5.http.Header; import org.junit.Assert; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -57,9 +57,9 @@ public void testGetWithSpecialCharacters() throws URISyntaxException, Invocation HttpUriRequest request = captor.getValue(); assertThat(request.getFirstHeader("AdditionalHeader").getValue(), is(equalTo("add"))); - Header[] headers = splitHttpResponse.responseHeaders(); + SplitHttpResponse.Header[] headers = splitHttpResponse.responseHeaders(); assertThat(headers[0].getName(), is(equalTo("Via"))); - assertThat(headers[0].getValue(), is(equalTo("HTTP/1.1 m_proxy_rio1"))); + assertThat(headers[0].getValues().get(0), is(equalTo("HTTP/1.1 m_proxy_rio1"))); Assert.assertNotNull(change); Assert.assertEquals(1, change.splits.size()); Assert.assertNotNull(change.splits.get(0)); @@ -122,7 +122,7 @@ public void testPost() throws URISyntaxException, IOException, IllegalAccessExce Map<String, List<String>> additionalHeaders = Collections.singletonMap("SplitSDKImpressionsMode", Collections.singletonList("OPTIMIZED")); - SplitHttpResponse splitHttpResponse = splitHtpClient.post(rootTarget, Utils.toJsonEntity(toSend), + SplitHttpResponse splitHttpResponse = splitHtpClient.post(rootTarget, Json.toJson(toSend), additionalHeaders); // Capture outgoing request and validate it @@ -152,7 +152,7 @@ public void testPosttNoExceptionOnHttpErrorCode() throws URISyntaxException, Inv SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClientMock, decorator, "qwerty", metadata()); SplitHttpResponse splitHttpResponse = splitHtpClient.post(rootTarget, - Utils.toJsonEntity(Arrays.asList(new String[] { "A", "B", "C", "D" })), null); + Json.toJson(Arrays.asList(new String[] { "A", "B", "C", "D" })), null); Assert.assertEquals(500, (long) splitHttpResponse.statusCode()); } @@ -165,7 +165,7 @@ public void testPosttException() throws URISyntaxException, InvocationTargetExce HttpStatus.SC_INTERNAL_SERVER_ERROR); SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClientMock, null, "qwerty", metadata()); - splitHtpClient.post(rootTarget, Utils.toJsonEntity(Arrays.asList(new String[] { "A", "B", "C", "D" })), null); + splitHtpClient.post(rootTarget, Json.toJson(Arrays.asList(new String[] { "A", "B", "C", "D" })), null); } private SDKMetadata metadata() { diff --git a/client/src/test/resources/org/powermock/extensions/configuration.properties b/client/src/test/resources/org/powermock/extensions/configuration.properties new file mode 100644 index 000000000..a8ebaeba3 --- /dev/null +++ b/client/src/test/resources/org/powermock/extensions/configuration.properties @@ -0,0 +1 @@ +powermock.global-ignore=jdk.internal.reflect.*,javax.net.ssl.* \ No newline at end of file diff --git a/okhttp-modules/pom.xml b/okhttp-modules/pom.xml new file mode 100644 index 000000000..529869307 --- /dev/null +++ b/okhttp-modules/pom.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <artifactId>java-client-parent</artifactId> + <groupId>io.split.client</groupId> + <version>4.13.0</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <version>4.13.0</version> + <artifactId>okhttp-modules</artifactId> + <packaging>jar</packaging> + <name>http-modules</name> + <description>Alternative Http Modules</description> + + <profiles> + <profile> + <id>release</id> + <build> + <plugins> + <plugin> + <groupId>org.sonatype.plugins</groupId> + <artifactId>nexus-staging-maven-plugin</artifactId> + <version>1.6.3</version> + <extensions>true</extensions> + <configuration> + <skipNexusStagingDeployMojo>false</skipNexusStagingDeployMojo> + </configuration> + </plugin> + </plugins> + </build> + </profile> + </profiles> + <dependencies> + <dependency> + <groupId>com.squareup.okhttp3</groupId> + <artifactId>okhttp</artifactId> + <version>4.12.0</version> + </dependency> + <dependency> + <groupId>com.squareup.okhttp3</groupId> + <artifactId>logging-interceptor</artifactId> + <version>4.12.0</version> + </dependency> + <dependency> + <groupId>io.split.client</groupId> + <artifactId>java-client</artifactId> + <version>4.13.0</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents.client5</groupId> + <artifactId>httpclient5</artifactId> + <version>5.0.3</version> + </dependency> + <!-- Test deps --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>1.10.19</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-module-junit4</artifactId> + <version>1.7.4</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-api-mockito</artifactId> + <version>1.7.4</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.squareup.okhttp3</groupId> + <artifactId>mockwebserver</artifactId> + <version>4.8.0</version> + <scope>test</scope> + </dependency> + + </dependencies> +</project> diff --git a/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/HTTPKerberosAuthInterceptor.java b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/HTTPKerberosAuthInterceptor.java new file mode 100644 index 000000000..26bd23ea5 --- /dev/null +++ b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/HTTPKerberosAuthInterceptor.java @@ -0,0 +1,273 @@ +package io.split.httpmodules.okhttp; + +import java.io.IOException; +import java.util.Map; +import java.util.Date; +import java.util.Set; +import java.util.Base64; + +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.security.Principal; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.security.auth.Subject; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.kerberos.KerberosTicket; + +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; + +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.Authenticator; +import okhttp3.Route; + +/** + * + * An HTTP Request interceptor that modifies the request headers to enable + * Kerberos authentication. It appends the Kerberos authentication token to the + * 'Authorization' request header for Kerberos authentication + * + * Copyright 2024 MarkLogic Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +public class HTTPKerberosAuthInterceptor implements Authenticator { + String host; + Map<String,String> krbOptions; + LoginContext loginContext; + public HTTPKerberosAuthInterceptor(String host, Map<String,String> krbOptions) throws IOException { + this.host = host; + this.krbOptions = krbOptions; + try { + buildSubjectCredentials(); + } catch (LoginException e) { + throw new IOException(e.getMessage(), e); + } + } + + /** + * Class to create Kerberos Configuration object which specifies the Kerberos + * Login Module to be used for authentication. + * + */ + protected static class KerberosLoginConfiguration extends Configuration { + Map<String,String> krbOptions = null; + + public KerberosLoginConfiguration() {} + + KerberosLoginConfiguration(Map<String,String> krbOptions) { + + this.krbOptions = krbOptions; + } + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + if (krbOptions == null) { + throw new IllegalStateException("Cannot create AppConfigurationEntry without Kerberos Options"); + } + return new AppConfigurationEntry[] { new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, krbOptions) }; + } + } + + /** + * This method checks the validity of the TGT in the cache and build the + * Subject inside the LoginContext using Krb5LoginModule and the TGT cached by + * the Kerberos client. It assumes that a valid TGT is already present in the + * kerberos client's cache. + * + * @throws LoginException + */ + protected void buildSubjectCredentials() throws LoginException { + Subject subject = new Subject(); + /** + * We are not getting the TGT from KDC here. The actual TGT is got from the + * KDC using kinit or equivalent but we use the cached TGT in order to build + * the LoginContext and populate the TGT inside the Subject using + * Krb5LoginModule + */ + + LoginContext lc = getLoginContext(subject); + lc.login(); + loginContext = lc; + } + + protected LoginContext getLoginContext(Subject subject) throws LoginException { + return new LoginContext("Krb5LoginContext", subject, null, + (krbOptions != null) ? new KerberosLoginConfiguration(krbOptions) : new KerberosLoginConfiguration()); + } + /** + * This method is responsible for getting the client principal name from the + * subject's principal set + * + * @return String the Kerberos principal name populated in the subject + * @throws IllegalStateException if there is more than 0 or more than 1 + * principal is present + */ + protected String getClientPrincipalName() { + final Set<Principal> principalSet = getContextSubject().getPrincipals(); + if (principalSet.size() != 1) + throw new IllegalStateException( + "Only one principal is expected. Found 0 or more than one principals :" + principalSet); + return principalSet.iterator().next().getName(); + } + + protected Subject getContextSubject() { + Subject subject = loginContext.getSubject(); + if (subject == null) + throw new IllegalStateException("Kerberos login context without subject"); + return subject; + } + + protected CreateAuthorizationHeaderAction getAuthorizationHeaderAction(String clientPrincipal, + String serverPrincipalName) { + return new CreateAuthorizationHeaderAction(clientPrincipal, + serverPrincipalName); + } + + /** + * This method builds the Authorization header for Kerberos. It + * generates a request token based on the service ticket, client principal name and + * time-stamp + * + * @param serverPrincipalName + * the name registered with the KDC of the service for which we + * need to authenticate + * @return the HTTP Authorization header token + */ + protected String buildAuthorizationHeader(String serverPrincipalName) throws LoginException, PrivilegedActionException { + /* + * Get the principal from the Subject's private credentials and populate the + * client and server principal name for the GSS API + */ + final String clientPrincipal = getClientPrincipalName(); + final CreateAuthorizationHeaderAction action = getAuthorizationHeaderAction(clientPrincipal, + serverPrincipalName); + + /* + * Check if the TGT in the Subject's private credentials are valid. If + * valid, then we use the TGT in the Subject's private credentials. If not, + * we build the Subject's private credentials again from valid TGT in the + * Kerberos client cache. + */ + Set<Object> privateCreds = getContextSubject().getPrivateCredentials(); + for (Object privateCred : privateCreds) { + if (privateCred instanceof KerberosTicket) { + String serverPrincipalTicketName = ((KerberosTicket) privateCred).getServer().getName(); + if ((serverPrincipalTicketName.startsWith("krbtgt")) + && ((KerberosTicket) privateCred).getEndTime().compareTo(new Date()) < 0) { + buildSubjectCredentials(); + break; + } + } + } + + /* + * Subject.doAs takes in the Subject context and the action to be run as + * arguments. This method executes the action as the Subject given in the + * argument. We do this in order to provide the Subject's context so that we + * reuse the service ticket which will be populated in the Subject rather + * than getting the service ticket from the KDC for each request. The GSS + * API populates the service ticket in the Subject and reuses it + * + */ + Subject.doAs(loginContext.getSubject(), action); + return action.getNegotiateToken(); + } + + /** + * Creates a privileged action which will be executed as the Subject using + * Subject.doAs() method. We do this in order to create a context of the user + * who has the service ticket and reuse this context for subsequent requests + */ + protected static class CreateAuthorizationHeaderAction implements PrivilegedExceptionAction { + String clientPrincipalName; + String serverPrincipalName; + + private StringBuilder outputToken = new StringBuilder(); + + protected CreateAuthorizationHeaderAction(final String clientPrincipalName, final String serverPrincipalName) { + this.clientPrincipalName = clientPrincipalName; + this.serverPrincipalName = serverPrincipalName; + } + + protected String getNegotiateToken() { + return outputToken.toString(); + } + + /* + * Here GSS API takes care of getting the service ticket from the Subject + * cache or by using the TGT information populated in the subject which is + * done by buildSubjectCredentials method. The service ticket received is + * populated in the subject's private credentials along with the TGT + * information since we will be executing this method as the Subject. For + * subsequent requests, the cached service ticket will be re-used. For this + * to work the System property javax.security.auth.useSubjectCredsOnly must + * be set to true. + */ + @Override + public Object run() throws KerberosAuthException { + try { + Oid krb5Mechanism = new Oid("1.2.840.113554.1.2.2"); + Oid krb5PrincipalNameType = new Oid("1.2.840.113554.1.2.2.1"); + final GSSManager manager = GSSManager.getInstance(); + final GSSName clientName = manager.createName(clientPrincipalName, krb5PrincipalNameType); + final GSSCredential clientCred = manager.createCredential(clientName, 8 * 3600, krb5Mechanism, + GSSCredential.INITIATE_ONLY); + final GSSName serverName = manager.createName(serverPrincipalName, krb5PrincipalNameType); + + final GSSContext context = manager.createContext(serverName, krb5Mechanism, clientCred, + GSSContext.DEFAULT_LIFETIME); + byte[] inToken = new byte[0]; + byte[] outToken = context.initSecContext(inToken, 0, inToken.length); + if (outToken == null) { + throw new IOException("could not initialize the security context"); + } + context.requestMutualAuth(true); + outputToken.append(new String(Base64.getEncoder().encode(outToken))); + context.dispose(); + } catch (GSSException | IOException exception) { + throw new KerberosAuthException(exception.getMessage(), exception); + } + return null; + } + } + + /* + * The server principal name which we pass as an argument to + * buildAuthorizationHeader method would always start with 'HTTP/' because we + * create the principal name for the Marklogic server starting with 'HTTP/' + * followed by the host name as mentioned in the <a href= + * "http://docs.marklogic.com/guide/security/external-auth#id_13835"> External + * Security Guide</a>. + */ + @Override public Request authenticate(Route route, Response response) throws IOException { + String authValue; + try { + authValue = "Negotiate " + buildAuthorizationHeader("HTTP/" + host); + } catch (Exception e) { + throw new IOException(e.getMessage(), e); + } + + return response.request().newBuilder() + .header("Proxy-authorization", authValue) + .build(); + } +} diff --git a/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/KerberosAuthException.java b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/KerberosAuthException.java new file mode 100644 index 000000000..06fa2672f --- /dev/null +++ b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/KerberosAuthException.java @@ -0,0 +1,10 @@ +package io.split.httpmodules.okhttp; + +public class KerberosAuthException extends Exception { + public KerberosAuthException(String message) { + super(message); + } + public KerberosAuthException(String message, Throwable exception) { + super(message, exception); + } +} diff --git a/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/OkHttpClientImpl.java b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/OkHttpClientImpl.java new file mode 100644 index 000000000..65a59921f --- /dev/null +++ b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/OkHttpClientImpl.java @@ -0,0 +1,247 @@ +package io.split.httpmodules.okhttp; + +import io.split.client.RequestDecorator; +import io.split.client.dtos.SplitHttpResponse; +import io.split.client.utils.SDKMetadata; +import io.split.engine.common.FetchOptions; +import io.split.service.SplitHttpClient; + +import okhttp3.Authenticator; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class OkHttpClientImpl implements SplitHttpClient { + protected OkHttpClient httpClient; + private static final Logger _log = LoggerFactory.getLogger(OkHttpClientImpl.class); + private static final String HEADER_CACHE_CONTROL_NAME = "Cache-Control"; + private static final String HEADER_CACHE_CONTROL_VALUE = "no-cache"; + private static final String HEADER_API_KEY = "Authorization"; + private static final String HEADER_CLIENT_KEY = "SplitSDKClientKey"; + private static final String HEADER_CLIENT_MACHINE_NAME = "SplitSDKMachineName"; + private static final String HEADER_CLIENT_MACHINE_IP = "SplitSDKMachineIP"; + private static final String HEADER_CLIENT_VERSION = "SplitSDKVersion"; + private String _apikey; + protected SDKMetadata _metadata; + private final RequestDecorator _decorator; + + public OkHttpClientImpl(String apiToken, SDKMetadata sdkMetadata, + Proxy proxy, String proxyAuthKerberosPrincipalName, boolean debugEnabled, + int readTimeout, int connectionTimeout, RequestDecorator decorator) throws IOException { + _apikey = apiToken; + _metadata = sdkMetadata; + _decorator = decorator; + setHttpClient(proxy, proxyAuthKerberosPrincipalName, debugEnabled, + readTimeout, connectionTimeout); + + } + protected void setHttpClient(Proxy proxy, String proxyAuthKerberosPrincipalName, boolean debugEnabled, + int readTimeout, int connectionTimeout) throws IOException { + httpClient = initializeClient(proxy, proxyAuthKerberosPrincipalName, debugEnabled, + readTimeout, connectionTimeout); + } + protected OkHttpClient initializeClient(Proxy proxy, String proxyAuthKerberosPrincipalName, boolean debugEnabled, + int readTimeout, int connectionTimeout) throws IOException { + HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); + if (debugEnabled) { + logging.setLevel(HttpLoggingInterceptor.Level.HEADERS); + } else { + logging.setLevel(HttpLoggingInterceptor.Level.NONE); + } + + Map<String, String> kerberosOptions = new HashMap<>(); + kerberosOptions.put("com.sun.security.auth.module.Krb5LoginModule", "required"); + kerberosOptions.put("refreshKrb5Config", "false"); + kerberosOptions.put("doNotPrompt", "false"); + kerberosOptions.put("useTicketCache", "true"); + + Authenticator proxyAuthenticator = getProxyAuthenticator(proxyAuthKerberosPrincipalName, kerberosOptions); + + return new okhttp3.OkHttpClient.Builder() + .proxy(proxy) + .readTimeout(readTimeout, TimeUnit.MILLISECONDS) + .connectTimeout(connectionTimeout, TimeUnit.MILLISECONDS) + .addInterceptor(logging) + .proxyAuthenticator(proxyAuthenticator) + .build(); + } + + public HTTPKerberosAuthInterceptor getProxyAuthenticator(String proxyKerberosPrincipalName, + Map<String, String> kerberosOptions) throws IOException { + return new HTTPKerberosAuthInterceptor(proxyKerberosPrincipalName, kerberosOptions); + } + + @Override + public SplitHttpResponse get(URI uri, FetchOptions options, Map<String, List<String>> additionalHeaders) { + try { + okhttp3.Request.Builder requestBuilder = getRequestBuilder(); + requestBuilder.url(uri.toString()); + Map<String, List<String>> headers = mergeHeaders(buildBasicHeaders(), additionalHeaders); + Map<String, List<String>> decorateHeaders = OkHttpRequestDecorator.decorate(headers, _decorator); + Map<String, List<String>> finalHeaders; + if (decorateHeaders.isEmpty()) { + finalHeaders = headers; + } else { + finalHeaders = decorateHeaders; + } + for (Map.Entry<String, List<String>> e : finalHeaders.entrySet()) { + for (String headerValue : e.getValue()) { + requestBuilder.addHeader(e.getKey(), headerValue); + } + } + + if (options.cacheControlHeadersEnabled()) { + requestBuilder.addHeader(HEADER_CACHE_CONTROL_NAME, HEADER_CACHE_CONTROL_VALUE); + } + + Request request = requestBuilder.build(); + _log.debug(String.format("Request Headers: %s", request.headers())); + + Response response = httpClient.newCall(request).execute(); + + int responseCode = response.code(); + + _log.debug(String.format("[GET] %s. Status code: %s", + request.url().toString(), + responseCode)); + + String statusMessage = ""; + if (responseCode < HttpURLConnection.HTTP_OK || responseCode >= HttpURLConnection.HTTP_MULT_CHOICE) { + _log.warn(String.format("Response status was: %s. Reason: %s", responseCode, + response.message())); + statusMessage = response.message(); + } + + String responseBody = response.body().string(); + response.close(); + + return new SplitHttpResponse(responseCode, + statusMessage, + responseBody, + getResponseHeaders(response)); + } catch (Exception e) { + throw new IllegalStateException(String.format("Problem in http get operation: %s", e), e); + } + } + + @Override + public SplitHttpResponse post(URI url, String entity, + Map<String, List<String>> additionalHeaders) { + try { + okhttp3.Request.Builder requestBuilder = getRequestBuilder(); + requestBuilder.url(url.toString()); + Map<String, List<String>> headers = mergeHeaders(buildBasicHeaders(), additionalHeaders); + Map<String, List<String>> decorateHeaders = OkHttpRequestDecorator.decorate(headers, _decorator); + Map<String, List<String>> finalHeaders; + if (decorateHeaders.isEmpty()) { + finalHeaders = headers; + } else { + finalHeaders = decorateHeaders; + } + for (Map.Entry<String, List<String>> e : finalHeaders.entrySet()) { + for (String headerValue : e.getValue()) { + requestBuilder.addHeader(e.getKey(), headerValue); + } + } + requestBuilder.addHeader("Accept-Encoding", "gzip"); + requestBuilder.addHeader("Content-Type", "application/json"); + RequestBody postBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), entity); + requestBuilder.post(postBody); + + Request request = requestBuilder.build(); + _log.debug(String.format("Request Headers: %s", request.headers())); + + Response response = httpClient.newCall(request).execute(); + + int responseCode = response.code(); + + _log.debug(String.format("[GET] %s. Status code: %s", + request.url().toString(), + responseCode)); + + String statusMessage = ""; + if (responseCode < HttpURLConnection.HTTP_OK || responseCode >= HttpURLConnection.HTTP_MULT_CHOICE) { + _log.warn(String.format("Response status was: %s. Reason: %s", responseCode, + response.message())); + statusMessage = response.message(); + } + response.close(); + + return new SplitHttpResponse(responseCode, statusMessage, "", getResponseHeaders(response)); + } catch (Exception e) { + throw new IllegalStateException(String.format("Problem in http post operation: %s", e), e); + } + } + + protected okhttp3.Request.Builder getRequestBuilder() { + return new okhttp3.Request.Builder(); + } + + protected Request getRequest(okhttp3.Request.Builder requestBuilder) { + return requestBuilder.build(); + } + + protected Map<String, List<String>> buildBasicHeaders() { + Map<String, List<String>> h = new HashMap<>(); + h.put(HEADER_API_KEY, Collections.singletonList("Bearer " + _apikey)); + h.put(HEADER_CLIENT_VERSION, Collections.singletonList(_metadata.getSdkVersion())); + h.put(HEADER_CLIENT_MACHINE_IP, Collections.singletonList(_metadata.getMachineIp())); + h.put(HEADER_CLIENT_MACHINE_NAME, Collections.singletonList(_metadata.getMachineName())); + h.put(HEADER_CLIENT_KEY, Collections.singletonList(_apikey.length() > 4 + ? _apikey.substring(_apikey.length() - 4) + : _apikey)); + return h; + } + + protected Map<String, List<String>> mergeHeaders(Map<String, List<String>> headers, + Map<String, List<String>> toAdd) { + if (toAdd == null || toAdd.size() == 0) { + return headers; + } + + for (Map.Entry<String, List<String>> entry : toAdd.entrySet()) { + headers.put(entry.getKey(), entry.getValue()); +// headers.computeIfPresent(entry.getKey(), +// (k, oldValue) -> Stream.concat(oldValue.stream(), entry.getValue().stream()) +// .collect(Collectors.toList())); + } + + return headers; + } + + protected SplitHttpResponse.Header[] getResponseHeaders(Response response) { + List<SplitHttpResponse.Header> responseHeaders = new ArrayList<>(); + Map<String, List<String>> map = response.headers().toMultimap(); + for (Map.Entry<String, List<String>> entry : map.entrySet()) { + if (entry.getKey() != null) { + responseHeaders.add(new SplitHttpResponse.Header(entry.getKey(), entry.getValue())); + } + } + return responseHeaders.toArray(new SplitHttpResponse.Header[0]); + } + + @Override + public void close() throws IOException { + httpClient.dispatcher().executorService().shutdown(); + } + +} diff --git a/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/OkHttpModule.java b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/OkHttpModule.java new file mode 100644 index 000000000..9f512874d --- /dev/null +++ b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/OkHttpModule.java @@ -0,0 +1,195 @@ +package io.split.httpmodules.okhttp; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; + +import io.split.client.RequestDecorator; +import io.split.client.utils.SDKMetadata; +import io.split.service.CustomHttpModule; + +public class OkHttpModule implements CustomHttpModule { + private static final int DEFAULT_CONNECTION_TIMEOUT = 15000; + private static final int DEFAULT_READ_TIMEOUT = 15000; + private final Boolean _debugEnabled; + private final Integer _connectionTimeout; + private final Integer _readTimeout; + private final Proxy _proxy; + private final ProxyAuthScheme _proxyAuthScheme; + private final String _proxyAuthKerberosPrincipalName; + + public static Builder builder() { + return new Builder(); + } + + private OkHttpModule(ProxyAuthScheme proxyAuthScheme, + String proxyAuthKerberosPrincipalName, + Proxy proxy, + Integer connectionTimeout, + Integer readTimeout, + Boolean debugEnabled) { + _proxyAuthScheme = proxyAuthScheme; + _proxyAuthKerberosPrincipalName = proxyAuthKerberosPrincipalName; + _proxy = proxy; + _connectionTimeout = connectionTimeout; + _readTimeout = readTimeout; + _debugEnabled = debugEnabled; + } + + @Override + public OkHttpClientImpl createClient(String apiToken, SDKMetadata sdkMetadata, RequestDecorator decorator) + throws IOException { + return new OkHttpClientImpl(apiToken, sdkMetadata, + _proxy, _proxyAuthKerberosPrincipalName, _debugEnabled, + _readTimeout, _connectionTimeout, decorator); + } + + public Proxy proxy() { + return _proxy; + } + + public ProxyAuthScheme proxyAuthScheme() { + return _proxyAuthScheme; + } + + public String proxyKerberosPrincipalName() { + return _proxyAuthKerberosPrincipalName; + } + + public Integer connectionTimeout() { + return _connectionTimeout; + } + + public Boolean debugEnabled() { + return _debugEnabled; + } + + public Integer readTimeout() { + return _readTimeout; + } + + public static final class Builder { + private Integer _connectionTimeout = DEFAULT_CONNECTION_TIMEOUT; + private Integer _readTimeout = DEFAULT_READ_TIMEOUT; + private String _proxyHost = "localhost"; + private int _proxyPort = -1; + private ProxyAuthScheme _proxyAuthScheme = null; + private String _proxyAuthKerberosPrincipalName = null; + private Boolean _debugEnabled = false; + + public Builder() { + } + + public Builder debugEnabled() { + _debugEnabled = true; + return this; + } + + /** + * The host location of the proxy. Default is localhost. + * + * @param proxyHost location of the proxy + * @return this builder + */ + public Builder proxyHost(String proxyHost) { + _proxyHost = proxyHost; + return this; + } + + /** + * The port of the proxy. Default is -1. + * + * @param proxyPort port for the proxy + * @return this builder + */ + public Builder proxyPort(int proxyPort) { + _proxyPort = proxyPort; + return this; + } + + Proxy proxy() { + if (_proxyPort != -1) { + return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(_proxyHost, _proxyPort)); + } + // Default is no proxy. + return null; + } + + /** + * Authentication Scheme + * + * @param proxyAuthScheme + * @return this builder + */ + public Builder proxyAuthScheme(ProxyAuthScheme proxyAuthScheme) { + _proxyAuthScheme = proxyAuthScheme; + return this; + } + + /** + * Kerberos Principal Account Name + * + * @param proxyAuthKerberosPrincipalName + * @return this builder + */ + public Builder proxyAuthKerberosPrincipalName(String proxyAuthKerberosPrincipalName) { + _proxyAuthKerberosPrincipalName = proxyAuthKerberosPrincipalName; + return this; + } + + /** + * HTTP Connection Timeout + * + * @param connectionTimeout + * @return this builder + */ + public Builder connectionTimeout(int connectionTimeout) { + _connectionTimeout = connectionTimeout; + return this; + } + + /** + * HTTP Read Timeout + * + * @param readTimeout + * @return this builder + */ + public Builder readTimeout(int readTimeout) { + _readTimeout = readTimeout; + return this; + } + + private void verifyAuthScheme() { + if (_proxyAuthScheme == ProxyAuthScheme.KERBEROS) { + if (proxy() == null) { + throw new IllegalStateException("Kerberos mode require Proxy parameters."); + } + if (_proxyAuthKerberosPrincipalName == null) { + throw new IllegalStateException("Kerberos mode require Kerberos Principal Name."); + } + } + } + + private void verifyTimeouts() { + if (_connectionTimeout <= 0) { + _connectionTimeout = DEFAULT_CONNECTION_TIMEOUT; + } + if (_readTimeout <= 0) { + _readTimeout = DEFAULT_READ_TIMEOUT; + } + } + + public OkHttpModule build() { + verifyTimeouts(); + verifyAuthScheme(); + + return new OkHttpModule( + _proxyAuthScheme, + _proxyAuthKerberosPrincipalName, + proxy(), + _connectionTimeout, + _readTimeout, + _debugEnabled); + } + } +} diff --git a/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/OkHttpRequestDecorator.java b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/OkHttpRequestDecorator.java new file mode 100644 index 000000000..efe9b8077 --- /dev/null +++ b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/OkHttpRequestDecorator.java @@ -0,0 +1,15 @@ +package io.split.httpmodules.okhttp; + +import java.util.List; +import java.util.Map; + +import io.split.client.RequestDecorator; +import io.split.client.dtos.RequestContext; + +class OkHttpRequestDecorator { + + public static Map<String, List<String>> decorate(Map<String, List<String>> headers, + RequestDecorator decorator) { + return decorator.decorateHeaders(new RequestContext(headers)).headers(); + } +} diff --git a/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/ProxyAuthScheme.java b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/ProxyAuthScheme.java new file mode 100644 index 000000000..4340829a2 --- /dev/null +++ b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/ProxyAuthScheme.java @@ -0,0 +1,5 @@ +package io.split.httpmodules.okhttp; + +public enum ProxyAuthScheme { + KERBEROS +} diff --git a/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/HTTPKerberosAuthIntercepterTest.java b/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/HTTPKerberosAuthIntercepterTest.java new file mode 100644 index 000000000..2103abd1c --- /dev/null +++ b/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/HTTPKerberosAuthIntercepterTest.java @@ -0,0 +1,111 @@ +package io.split.httpmodules.okhttp; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosPrincipal; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.powermock.api.mockito.PowerMockito.*; + +import java.security.PrivilegedActionException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(HTTPKerberosAuthInterceptor.class) +public class HTTPKerberosAuthIntercepterTest { + @Test + public void testBasicFlow() throws Exception { + System.setProperty("java.security.krb5.conf", "src/test/resources/krb5.conf"); + + HTTPKerberosAuthInterceptor kerberosAuthInterceptor = mock(HTTPKerberosAuthInterceptor.class); + LoginContext loginContext = PowerMockito.mock(LoginContext.class); + when(kerberosAuthInterceptor.getLoginContext(any())).thenReturn((loginContext)); + + doCallRealMethod().when(kerberosAuthInterceptor).buildSubjectCredentials(); + kerberosAuthInterceptor.buildSubjectCredentials(); + verify(loginContext, times(1)).login(); + + Subject subject = new Subject(); + when(loginContext.getSubject()).thenReturn(subject); + doCallRealMethod().when(kerberosAuthInterceptor).getContextSubject(); + kerberosAuthInterceptor.getContextSubject(); + verify(loginContext, times(1)).getSubject(); + + subject.getPrincipals().add(new KerberosPrincipal("bilal")); + subject.getPublicCredentials().add(new KerberosPrincipal("name")); + subject.getPrivateCredentials().add(new KerberosPrincipal("name")); + + doCallRealMethod().when(kerberosAuthInterceptor).getClientPrincipalName(); + assertThat(kerberosAuthInterceptor.getClientPrincipalName(), is(equalTo("bilal@ATHENA.MIT.EDU"))) ; + verify(loginContext, times(2)).getSubject(); + + when(kerberosAuthInterceptor.buildAuthorizationHeader(any())).thenReturn("secured-token"); + okhttp3.Request originalRequest = new okhttp3.Request.Builder().url("http://somthing").build(); + okhttp3.Response response = new okhttp3.Response.Builder().code(200).request(originalRequest). + protocol(okhttp3.Protocol.HTTP_1_1).message("ok").build(); + doCallRealMethod().when(kerberosAuthInterceptor).authenticate(null, response); + okhttp3.Request request = kerberosAuthInterceptor.authenticate(null, response); + assertThat(request.headers("Proxy-authorization"), is(equalTo(Arrays.asList("Negotiate secured-token")))); + } + + @Test + public void testKerberosLoginConfiguration() { + Map<String, String> kerberosOptions = new HashMap<String, String>(); + kerberosOptions.put("com.sun.security.auth.module.Krb5LoginModule", "required"); + kerberosOptions.put("refreshKrb5Config", "false"); + kerberosOptions.put("doNotPrompt", "false"); + kerberosOptions.put("useTicketCache", "true"); + + HTTPKerberosAuthInterceptor.KerberosLoginConfiguration kerberosConfig = new HTTPKerberosAuthInterceptor.KerberosLoginConfiguration(kerberosOptions); + AppConfigurationEntry[] appConfig = kerberosConfig.getAppConfigurationEntry(""); + assertThat("com.sun.security.auth.module.Krb5LoginModule", is(equalTo(appConfig[0].getLoginModuleName()))); + assertThat(AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, is(equalTo(appConfig[0].getControlFlag()))); + } + + @Test(expected = IllegalStateException.class) + public void testKerberosLoginConfigurationException() { + HTTPKerberosAuthInterceptor.KerberosLoginConfiguration kerberosConfig = new HTTPKerberosAuthInterceptor.KerberosLoginConfiguration(); + AppConfigurationEntry[] appConfig = kerberosConfig.getAppConfigurationEntry(""); + } + + @Test + public void testBuildAuthorizationHeader() throws LoginException, PrivilegedActionException { + System.setProperty("java.security.krb5.conf", "src/test/resources/krb5.conf"); + + HTTPKerberosAuthInterceptor kerberosAuthInterceptor = mock(HTTPKerberosAuthInterceptor.class); + HTTPKerberosAuthInterceptor.CreateAuthorizationHeaderAction ahh = mock(HTTPKerberosAuthInterceptor.CreateAuthorizationHeaderAction.class); + when(ahh.getNegotiateToken()).thenReturn("secret-token"); + when(kerberosAuthInterceptor.getAuthorizationHeaderAction(any(), any())).thenReturn(ahh); + + LoginContext loginContext = PowerMockito.mock(LoginContext.class); + doCallRealMethod().when(kerberosAuthInterceptor).buildAuthorizationHeader("bilal"); + Subject subject = new Subject(); + when(loginContext.getSubject()).thenReturn(subject); + when(kerberosAuthInterceptor.getContextSubject()).thenReturn(subject); + when(kerberosAuthInterceptor.getLoginContext(subject)).thenReturn((loginContext)); + doCallRealMethod().when(kerberosAuthInterceptor).buildSubjectCredentials(); + kerberosAuthInterceptor.buildSubjectCredentials(); + + subject.getPrincipals().add(new KerberosPrincipal("bilal")); + subject.getPublicCredentials().add(new KerberosPrincipal("name")); + subject.getPrivateCredentials().add(new KerberosPrincipal("name")); + doCallRealMethod().when(kerberosAuthInterceptor).getClientPrincipalName(); + + assertThat("secret-token", is(equalTo(kerberosAuthInterceptor.buildAuthorizationHeader("bilal")))); + } +} diff --git a/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/OkHttpClientImplTest.java b/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/OkHttpClientImplTest.java new file mode 100644 index 000000000..88d93333a --- /dev/null +++ b/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/OkHttpClientImplTest.java @@ -0,0 +1,429 @@ +package io.split.httpmodules.okhttp; + +import org.powermock.api.mockito.PowerMockito; +import org.powermock.reflect.Whitebox; + +import io.split.client.CustomHeaderDecorator; +import io.split.client.RequestDecorator; +import io.split.client.dtos.*; +import io.split.client.impressions.Impression; +import io.split.client.utils.Json; +import io.split.client.utils.SDKMetadata; +import io.split.client.dtos.SplitHttpResponse.Header; +import io.split.engine.common.FetchOptions; + +import okhttp3.OkHttpClient; +import okhttp3.OkHttpClient.*; +import okhttp3.HttpUrl; +import okhttp3.Headers; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.Assert; +import org.junit.Test; + +import java.io.*; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.HttpURLConnection; +import java.util.*; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.Matchers.any; +import static org.powermock.api.mockito.PowerMockito.mock; + +public class OkHttpClientImplTest { + + @Test + public void testGetWithSpecialCharacters() throws IOException, InterruptedException { + MockWebServer server = new MockWebServer(); + BufferedReader br = new BufferedReader(new FileReader("src/test/resources/split-change-special-characters.json")); + String body; + try { + StringBuilder sb = new StringBuilder(); + String line = br.readLine(); + + while (line != null) { + sb.append(line); + sb.append(System.lineSeparator()); + line = br.readLine(); + } + body = sb.toString(); + } finally { + br.close(); + } + + server.enqueue(new MockResponse().setBody(body).addHeader("via", "HTTP/1.1 s_proxy_rio1")); + server.start(); + HttpUrl baseUrl = server.url("/v1/"); + URI rootTarget = baseUrl.uri(); + + OkHttpClientImpl okHttpClientImpl = mock(OkHttpClientImpl.class); + OkHttpClient client = new OkHttpClient.Builder().build(); + PowerMockito.doReturn(client).when(okHttpClientImpl).initializeClient(null, "bilal", false, + 0, 0); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).setHttpClient(null, "bilal", false, + 0, 0); + okHttpClientImpl.setHttpClient(null, "bilal", false, + 0, 0); + + Map<String, List<String>> additionalHeaders = Collections.singletonMap("AdditionalHeader", + Collections.singletonList("add")); + FetchOptions fetchOptions = new FetchOptions.Builder().cacheControlHeaders(true).build(); + RequestDecorator requestDecorator = new RequestDecorator(null); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).get(rootTarget, fetchOptions, additionalHeaders); + okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder(); + requestBuilder.url(rootTarget.toString()); + PowerMockito.doReturn(requestBuilder).when(okHttpClientImpl).getRequestBuilder(); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).buildBasicHeaders(); + Whitebox.setInternalState(okHttpClientImpl, "_metadata", metadata()); + Whitebox.setInternalState(okHttpClientImpl, "_apikey", "qwerty"); + Whitebox.setInternalState(okHttpClientImpl, "_decorator", requestDecorator); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).mergeHeaders(buildBasicHeaders(), additionalHeaders); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).getResponseHeaders(any()); + PowerMockito.doReturn(requestBuilder.build()).when(okHttpClientImpl).getRequest(requestBuilder); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).getRequest(requestBuilder); + + SplitHttpResponse splitHttpResponse = okHttpClientImpl.get(rootTarget, fetchOptions, additionalHeaders); + + RecordedRequest request = server.takeRequest(); + server.shutdown(); + Headers requestHeaders = request.getHeaders(); + + assertThat(splitHttpResponse.statusCode(), is(equalTo(HttpURLConnection.HTTP_OK))); + Assert.assertEquals("/v1/", request.getPath()); + assertThat(requestHeaders.get("Authorization"), is(equalTo("Bearer qwerty"))) ; + assertThat(requestHeaders.get("SplitSDKClientKey"), is(equalTo("erty"))); + assertThat(requestHeaders.get("SplitSDKVersion"), is(equalTo("java-1.2.3"))); + assertThat(requestHeaders.get("SplitSDKMachineIP"), is(equalTo("1.2.3.4"))); + assertThat(requestHeaders.get("SplitSDKMachineName"), is(equalTo("someIP"))); + assertThat(requestHeaders.get("AdditionalHeader"), is(equalTo("add"))); + + SplitChange change = Json.fromJson(splitHttpResponse.body(), SplitChange.class); + + Header[] headers = splitHttpResponse.responseHeaders(); + assertThat(headers[1].getName(), is(equalTo("via"))); + assertThat(headers[1].getValues().get(0), is(equalTo("HTTP/1.1 s_proxy_rio1"))); + assertThat(splitHttpResponse.statusCode(), is(equalTo(200))); + Assert.assertNotNull(change); + Assert.assertEquals(1, change.splits.size()); + Assert.assertNotNull(change.splits.get(0)); + + Split split = change.splits.get(0); + Map<String, String> configs = split.configurations; + Assert.assertEquals(2, configs.size()); + Assert.assertEquals("{\"test\": \"blue\",\"grüne Straße\": 13}", configs.get("on")); + Assert.assertEquals("{\"test\": \"blue\",\"size\": 15}", configs.get("off")); + Assert.assertEquals(2, split.sets.size()); + okHttpClientImpl.close(); + } + + @Test + public void testGetErrors() throws IOException, InterruptedException { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("").setResponseCode(HttpURLConnection.HTTP_INTERNAL_ERROR)); + server.start(); + HttpUrl baseUrl = server.url("/v1/"); + URI rootTarget = baseUrl.uri(); + + OkHttpClientImpl okHttpClientImpl = mock(OkHttpClientImpl.class); + OkHttpClient client = new OkHttpClient.Builder().build(); + PowerMockito.doReturn(client).when(okHttpClientImpl).initializeClient(null, "bilal", false, + 0, 0); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).setHttpClient(null, "bilal", false, + 0, 0); + okHttpClientImpl.setHttpClient(null, "bilal", false, + 0, 0); + + Map<String, List<String>> additionalHeaders = Collections.singletonMap("AdditionalHeader", + Collections.singletonList("add")); + FetchOptions fetchOptions = new FetchOptions.Builder().cacheControlHeaders(true).build(); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).get(rootTarget, fetchOptions, additionalHeaders); + okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder(); + requestBuilder.url(rootTarget.toString()); + PowerMockito.doReturn(requestBuilder).when(okHttpClientImpl).getRequestBuilder(); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).buildBasicHeaders(); + Whitebox.setInternalState(okHttpClientImpl, "_metadata", metadata()); + Whitebox.setInternalState(okHttpClientImpl, "_apikey", "qwerty"); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).mergeHeaders(buildBasicHeaders(), additionalHeaders); + RequestDecorator requestDecorator = new RequestDecorator(null); + Whitebox.setInternalState(okHttpClientImpl, "_decorator", requestDecorator); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).getResponseHeaders(any()); + + SplitHttpResponse splitHttpResponse = okHttpClientImpl.get(rootTarget, + fetchOptions, additionalHeaders); + + RecordedRequest request = server.takeRequest(); + server.shutdown(); + assertThat(splitHttpResponse.statusCode(), is(equalTo(HttpURLConnection.HTTP_INTERNAL_ERROR))); + okHttpClientImpl.close(); + } + + @Test + public void testGetParameters() throws IOException, InterruptedException { + class MyCustomHeaders implements CustomHeaderDecorator { + public MyCustomHeaders() {} + @Override + public Map<String, List<String>> getHeaderOverrides(RequestContext context) { + Map<String, List<String>> additionalHeaders = context.headers(); + additionalHeaders.put("first", Arrays.asList("1")); + additionalHeaders.put("second", Arrays.asList("2.1", "2.2")); + additionalHeaders.put("third", Arrays.asList("3")); + return additionalHeaders; + } + } + MockWebServer server = new MockWebServer(); + BufferedReader br = new BufferedReader(new FileReader("src/test/resources/split-change-special-characters.json")); + String body; + try { + StringBuilder sb = new StringBuilder(); + String line = br.readLine(); + + while (line != null) { + sb.append(line); + sb.append(System.lineSeparator()); + line = br.readLine(); + } + body = sb.toString(); + } finally { + br.close(); + } + + server.enqueue(new MockResponse().setBody(body).addHeader("via", "HTTP/1.1 s_proxy_rio1")); + server.start(); + HttpUrl baseUrl = server.url("/splitChanges?since=1234567"); + URI rootTarget = baseUrl.uri(); + RequestDecorator requestDecorator = new RequestDecorator(new MyCustomHeaders()); + + OkHttpClientImpl okHttpClientImpl = mock(OkHttpClientImpl.class); + OkHttpClient client = new OkHttpClient.Builder().build(); + PowerMockito.doReturn(client).when(okHttpClientImpl).initializeClient(null, "bilal", false, + 0, 0); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).setHttpClient(null, "bilal", false, + 0, 0); + okHttpClientImpl.setHttpClient(null, "bilal", false, + 0, 0); + + FetchOptions fetchOptions = new FetchOptions.Builder().cacheControlHeaders(true).build(); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).get(rootTarget, fetchOptions, null); + okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder(); + requestBuilder.url(rootTarget.toString()); + PowerMockito.doReturn(requestBuilder).when(okHttpClientImpl).getRequestBuilder(); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).buildBasicHeaders(); + Whitebox.setInternalState(okHttpClientImpl, "_metadata", metadata()); + Whitebox.setInternalState(okHttpClientImpl, "_apikey", "qwerty"); + Whitebox.setInternalState(okHttpClientImpl, "_decorator", requestDecorator); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).mergeHeaders(buildBasicHeaders(), null); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).getResponseHeaders(any()); + FetchOptions options = new FetchOptions.Builder().cacheControlHeaders(true).build(); + + SplitHttpResponse splitHttpResponse = okHttpClientImpl.get(rootTarget, options, null); + + RecordedRequest request = server.takeRequest(); + server.shutdown(); + Headers requestHeaders = request.getHeaders(); + + assertThat(requestHeaders.get("Cache-Control"), is(equalTo("no-cache"))); + assertThat(requestHeaders.get("first"), is(equalTo("1"))); + assertThat(requestHeaders.values("second"), is(equalTo(Arrays.asList("2.1","2.2")))); + assertThat(requestHeaders.get("third"), is(equalTo("3"))); + Assert.assertEquals("/splitChanges?since=1234567", request.getPath()); + assertThat(request.getMethod(), is(equalTo("GET"))); + } + + @Test(expected = IllegalStateException.class) + public void testException() throws URISyntaxException, IOException { + URI rootTarget = new URI("https://api.split.io/splitChanges?since=1234567"); + RequestDecorator requestDecorator = null; + + OkHttpClientImpl okHttpClientImpl = mock(OkHttpClientImpl.class); + OkHttpClient client = new OkHttpClient.Builder().build(); + PowerMockito.doReturn(client).when(okHttpClientImpl).initializeClient(null, "bilal", false, + 0, 0); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).setHttpClient(null, "bilal", false, + 0, 0); + okHttpClientImpl.setHttpClient(null, "bilal", false, + 0, 0); + + FetchOptions fetchOptions = new FetchOptions.Builder().cacheControlHeaders(true).build(); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).get(rootTarget, fetchOptions, null); + okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder(); + requestBuilder.url(rootTarget.toString()); + PowerMockito.doReturn(requestBuilder).when(okHttpClientImpl).getRequestBuilder(); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).buildBasicHeaders(); + Whitebox.setInternalState(okHttpClientImpl, "_metadata", metadata()); + Whitebox.setInternalState(okHttpClientImpl, "_apikey", "qwerty"); + Whitebox.setInternalState(okHttpClientImpl, "_decorator", requestDecorator); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).mergeHeaders(buildBasicHeaders(), null); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).getResponseHeaders(any()); + FetchOptions options = new FetchOptions.Builder().cacheControlHeaders(true).build(); + + SplitHttpResponse splitHttpResponse = okHttpClientImpl.get(rootTarget, + new FetchOptions.Builder().cacheControlHeaders(true).build(), null); + } + + + + @Test + public void testPost() throws IOException, InterruptedException { + MockWebServer server = new MockWebServer(); + + server.enqueue(new MockResponse().addHeader("via", "HTTP/1.1 s_proxy_rio1")); + server.start(); + HttpUrl baseUrl = server.url("/impressions"); + URI rootTarget = baseUrl.uri(); + RequestDecorator requestDecorator = new RequestDecorator(null); + + OkHttpClientImpl okHttpClientImpl = mock(OkHttpClientImpl.class); + OkHttpClient client = new OkHttpClient.Builder().build(); + PowerMockito.doReturn(client).when(okHttpClientImpl).initializeClient(null, "bilal", false, + 0, 0); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).setHttpClient(null, "bilal", false, + 0, 0); + okHttpClientImpl.setHttpClient(null, "bilal", false, + 0, 0); + Map<String, List<String>> additionalHeaders = Collections.singletonMap("SplitSDKImpressionsMode", + Collections.singletonList("OPTIMIZED")); + + okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder(); + requestBuilder.url(rootTarget.toString()); + PowerMockito.doReturn(requestBuilder).when(okHttpClientImpl).getRequestBuilder(); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).buildBasicHeaders(); + Whitebox.setInternalState(okHttpClientImpl, "_metadata", metadata()); + Whitebox.setInternalState(okHttpClientImpl, "_apikey", "qwerty"); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).mergeHeaders(buildBasicHeaders(), additionalHeaders); + Whitebox.setInternalState(okHttpClientImpl, "_decorator", requestDecorator); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).getResponseHeaders(any()); + // Send impressions + List<TestImpressions> toSend = Arrays.asList(new TestImpressions("t1", Arrays.asList( + KeyImpression.fromImpression(new Impression("k1", null, "t1", "on", 123L, "r1", 456L, null)), + KeyImpression.fromImpression(new Impression("k2", null, "t1", "on", 123L, "r1", 456L, null)), + KeyImpression.fromImpression(new Impression("k3", null, "t1", "on", 123L, "r1", 456L, null)))), + new TestImpressions("t2", Arrays.asList( + KeyImpression.fromImpression(new Impression("k1", null, "t2", "on", 123L, "r1", 456L, null)), + KeyImpression.fromImpression(new Impression("k2", null, "t2", "on", 123L, "r1", 456L, null)), + KeyImpression.fromImpression(new Impression("k3", null, "t2", "on", 123L, "r1", 456L, null))))); + String data = Json.toJson(toSend); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).post(rootTarget, data, + additionalHeaders); + + SplitHttpResponse splitHttpResponse = okHttpClientImpl.post(rootTarget, data, + additionalHeaders); + + RecordedRequest request = server.takeRequest(); + server.shutdown(); + Headers requestHeaders = request.getHeaders(); + + Assert.assertEquals("POST /impressions HTTP/1.1", request.getRequestLine()); + Assert.assertEquals(data, request.getBody().readUtf8()); + assertThat(requestHeaders.get("Authorization"), is(equalTo("Bearer qwerty"))) ; + assertThat(requestHeaders.get("SplitSDKClientKey"), is(equalTo("erty"))); + assertThat(requestHeaders.get("SplitSDKVersion"), is(equalTo("java-1.2.3"))); + assertThat(requestHeaders.get("SplitSDKMachineIP"), is(equalTo("1.2.3.4"))); + assertThat(requestHeaders.get("SplitSDKMachineName"), is(equalTo("someIP"))); + assertThat(requestHeaders.get("SplitSDKImpressionsMode"), is(equalTo("OPTIMIZED"))); + + Header[] headers = splitHttpResponse.responseHeaders(); + assertThat(headers[1].getName(), is(equalTo("via"))); + assertThat(headers[1].getValues().get(0), is(equalTo("HTTP/1.1 s_proxy_rio1"))); + assertThat(splitHttpResponse.statusCode(), is(equalTo(200))); + } + + @Test + public void testPostErrors() throws IOException, InterruptedException { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("").setResponseCode(HttpURLConnection.HTTP_INTERNAL_ERROR)); + server.start(); + HttpUrl baseUrl = server.url("/v1/"); + URI rootTarget = baseUrl.uri(); + RequestDecorator requestDecorator = new RequestDecorator(null); + + OkHttpClientImpl okHttpClientImpl = mock(OkHttpClientImpl.class); + OkHttpClient client = new OkHttpClient.Builder().build(); + PowerMockito.doReturn(client).when(okHttpClientImpl).initializeClient(null, "bilal", false, + 0, 0); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).setHttpClient(null, "bilal", false, + 0, 0); + okHttpClientImpl.setHttpClient(null, "bilal", false, + 0, 0); + Map<String, List<String>> additionalHeaders = Collections.singletonMap("SplitSDKImpressionsMode", + Collections.singletonList("OPTIMIZED")); + + okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder(); + requestBuilder.url(rootTarget.toString()); + PowerMockito.doReturn(requestBuilder).when(okHttpClientImpl).getRequestBuilder(); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).buildBasicHeaders(); + Whitebox.setInternalState(okHttpClientImpl, "_metadata", metadata()); + Whitebox.setInternalState(okHttpClientImpl, "_apikey", "qwerty"); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).mergeHeaders(buildBasicHeaders(), additionalHeaders); + Whitebox.setInternalState(okHttpClientImpl, "_decorator", requestDecorator); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).getResponseHeaders(any()); + + String data = Json.toJson("<>"); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).post(rootTarget, data, + additionalHeaders); + + SplitHttpResponse splitHttpResponse = okHttpClientImpl.post(rootTarget, data, + additionalHeaders); + + RecordedRequest request = server.takeRequest(); + server.shutdown(); + assertThat(splitHttpResponse.statusCode(), is(equalTo(HttpURLConnection.HTTP_INTERNAL_ERROR))); + okHttpClientImpl.close(); + } + + @Test(expected = IllegalStateException.class) + public void testPosttException() throws URISyntaxException, IOException { + URI rootTarget = new URI("https://kubernetesturl.com/split/api/testImpressions/bulk"); + + OkHttpClientImpl okHttpClientImpl = mock(OkHttpClientImpl.class); + OkHttpClient client = new OkHttpClient.Builder().build(); + PowerMockito.doReturn(client).when(okHttpClientImpl).initializeClient(null, "bilal", false, + 0, 0); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).setHttpClient(null, "bilal", false, + 0, 0); + okHttpClientImpl.setHttpClient(null, "bilal", false, + 0, 0); + Map<String, List<String>> additionalHeaders = Collections.singletonMap("SplitSDKImpressionsMode", + Collections.singletonList("OPTIMIZED")); + + okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder(); + RequestDecorator requestDecorator = new RequestDecorator(null); + requestBuilder.url(rootTarget.toString()); + PowerMockito.doReturn(requestBuilder).when(okHttpClientImpl).getRequestBuilder(); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).buildBasicHeaders(); + Whitebox.setInternalState(okHttpClientImpl, "_metadata", metadata()); + Whitebox.setInternalState(okHttpClientImpl, "_apikey", "qwerty"); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).mergeHeaders(buildBasicHeaders(), additionalHeaders); + Whitebox.setInternalState(okHttpClientImpl, "_decorator", requestDecorator); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).getResponseHeaders(any()); + + String data = Json.toJson("<>"); + PowerMockito.doCallRealMethod().when(okHttpClientImpl).post(rootTarget, data, + additionalHeaders); + + SplitHttpResponse splitHttpResponse = okHttpClientImpl.post(rootTarget, data, + additionalHeaders); + } + + private SDKMetadata metadata() { + return new SDKMetadata("java-1.2.3", "1.2.3.4", "someIP"); + } + + private Map<String, List<String>> buildBasicHeaders() { + Map<String, List<String>> h = new HashMap<>(); + h.put("Authorization", Collections.singletonList("Bearer qwerty")); + h.put("SplitSDKVersion", Collections.singletonList(metadata().getSdkVersion())); + h.put("SplitSDKMachineIP", Collections.singletonList(metadata().getMachineIp())); + h.put("SplitSDKMachineName", Collections.singletonList(metadata().getMachineName())); + h.put("SplitSDKClientKey", Collections.singletonList("qwerty".length() > 4 + ? "qwerty".substring("qwerty".length() - 4) + : "qwerty")); + return h; + } + +} diff --git a/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/OkHttpModuleTests.java b/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/OkHttpModuleTests.java new file mode 100644 index 000000000..d8c5b5242 --- /dev/null +++ b/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/OkHttpModuleTests.java @@ -0,0 +1,111 @@ +package io.split.httpmodules.okhttp; + +import io.split.client.RequestDecorator; +import io.split.client.utils.SDKMetadata; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.mockito.stubbing.Answer; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(OkHttpModule.class) +public class OkHttpModuleTests { + @Test + public void checkProxySettings() { + OkHttpModule module = OkHttpModule.builder() + .proxyAuthScheme(ProxyAuthScheme.KERBEROS) + .proxyAuthKerberosPrincipalName("bilal@bilal") + .proxyHost("some-proxy") + .proxyPort(3128) + .build(); + Assert.assertEquals(ProxyAuthScheme.KERBEROS, module.proxyAuthScheme()); + Assert.assertEquals("bilal@bilal", module.proxyKerberosPrincipalName()); + Assert.assertEquals("HTTP @ some-proxy:3128", module.proxy().toString()); + } + + @Test + public void checkDebugLog() { + OkHttpModule module = OkHttpModule.builder() + .debugEnabled() + .build(); + Assert.assertEquals(true, module.debugEnabled()); + + module = OkHttpModule.builder() + .build(); + Assert.assertEquals(false, module.debugEnabled()); + } + + @Test + public void checkTimeouts() { + OkHttpModule module = OkHttpModule.builder() + .build(); + Assert.assertEquals(15000, (int) module.connectionTimeout()); + Assert.assertEquals(15000, (int) module.readTimeout()); + + module = OkHttpModule.builder() + .connectionTimeout(13000) + .readTimeout(14000) + .build(); + Assert.assertEquals(13000, (int) module.connectionTimeout()); + Assert.assertEquals(14000, (int) module.readTimeout()); + + module = OkHttpModule.builder() + .connectionTimeout(-1) + .readTimeout(-10) + .build(); + Assert.assertEquals(15000, (int) module.connectionTimeout()); + Assert.assertEquals(15000, (int) module.readTimeout()); + } + + @Test + public void testCreateClient() throws Exception { + OkHttpClientImpl mockclient = mock(OkHttpClientImpl.class); + AtomicBoolean argsCaptured = new AtomicBoolean(false); + + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("some-proxy", 3128)); + String apiToken = "qwerty"; + SDKMetadata sdkMetadata = new SDKMetadata("1.1.1", "ip", "name"); + RequestDecorator requestDecorator = new RequestDecorator(null); + + whenNew(OkHttpClientImpl.class).withAnyArguments() + .then((Answer<OkHttpClientImpl>) invocationOnMock -> { + assertThat("qwerty", is(equalTo((String) invocationOnMock.getArguments()[0]))); + assertThat(sdkMetadata, is(equalTo((SDKMetadata) invocationOnMock.getArguments()[1]))); + assertThat(proxy, is(equalTo((Proxy) invocationOnMock.getArguments()[2]))); + assertThat("bilal@bilal", is(equalTo((String) invocationOnMock.getArguments()[3]))); + assertThat(false, is(equalTo((Boolean) invocationOnMock.getArguments()[4]))); + assertThat(11000, is(equalTo((Integer) invocationOnMock.getArguments()[5]))); + assertThat(12000, is(equalTo((Integer) invocationOnMock.getArguments()[6]))); + assertThat(requestDecorator, is(equalTo((RequestDecorator) invocationOnMock.getArguments()[7]))); + argsCaptured.set(true); + return mockclient; + } + ); + + OkHttpModule module = OkHttpModule.builder() + .proxyAuthScheme(ProxyAuthScheme.KERBEROS) + .proxyAuthKerberosPrincipalName("bilal@bilal") + .proxyHost("some-proxy") + .proxyPort(3128) + .connectionTimeout(12000) + .readTimeout(11000) + .build(); + + module.createClient(apiToken, sdkMetadata, requestDecorator); + assertThat(true, is(equalTo(argsCaptured.get()))); + } +} diff --git a/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/SplitConfigTests.java b/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/SplitConfigTests.java new file mode 100644 index 000000000..20feddb38 --- /dev/null +++ b/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/SplitConfigTests.java @@ -0,0 +1,45 @@ +package io.split.httpmodules.okhttp; + +import io.split.client.SplitClientConfig; +import org.junit.Assert; +import org.junit.Test; + +public class SplitConfigTests { + + @Test + public void checkExpectedAuthScheme() { + SplitClientConfig cfg = SplitClientConfig.builder() + .alternativeHTTPModule(OkHttpModule.builder() + .proxyAuthScheme(ProxyAuthScheme.KERBEROS) + .proxyAuthKerberosPrincipalName("bilal@bilal") + .proxyHost("some-proxy") + .proxyPort(3128) + .debugEnabled() + .build() + ) + .streamingEnabled(false) + .build(); + OkHttpModule module = (OkHttpModule) cfg.alternativeHTTPModule(); + Assert.assertEquals(ProxyAuthScheme.KERBEROS, module.proxyAuthScheme()); + Assert.assertEquals("bilal@bilal", module.proxyKerberosPrincipalName()); + Assert.assertEquals("HTTP @ some-proxy:3128", module.proxy().toString()); + + cfg = SplitClientConfig.builder() + .build(); + Assert.assertEquals(null, cfg.alternativeHTTPModule()); + } + + @Test(expected = IllegalArgumentException.class) + public void checkStreamingEnabled() { + SplitClientConfig cfg = SplitClientConfig.builder() + .alternativeHTTPModule(OkHttpModule.builder() + .proxyAuthScheme(ProxyAuthScheme.KERBEROS) + .proxyAuthKerberosPrincipalName("bilal@bilal") + .proxyHost("some-proxy") + .proxyPort(3128) + .debugEnabled() + .build()) + .streamingEnabled(true) + .build(); + } +} diff --git a/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/SplitFactoryTests.java b/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/SplitFactoryTests.java new file mode 100644 index 000000000..23cf3cb53 --- /dev/null +++ b/okhttp-modules/src/test/java/io/split/httpmodules/okhttp/SplitFactoryTests.java @@ -0,0 +1,68 @@ +package io.split.httpmodules.okhttp; + +import io.split.client.*; +import io.split.client.utils.SDKMetadata; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(OkHttpModule.class) +public class SplitFactoryTests { + @Test + public void testFactoryCreatingClient() throws Exception { + OkHttpClientImpl mockclient = mock(OkHttpClientImpl.class); + AtomicBoolean argsCaptured = new AtomicBoolean(false); + + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("some-proxy", 3128)); + String apiToken = "qwerty"; + + whenNew(OkHttpClientImpl.class).withAnyArguments() + .then((Answer<OkHttpClientImpl>) invocationOnMock -> { + assertThat("qwerty", is(equalTo((String) invocationOnMock.getArguments()[0]))); + assertThat((SDKMetadata) invocationOnMock.getArguments()[1], instanceOf(SDKMetadata.class)); + assertThat(proxy, is(equalTo((Proxy) invocationOnMock.getArguments()[2]))); + assertThat("bilal@bilal", is(equalTo((String) invocationOnMock.getArguments()[3]))); + assertThat(false, is(equalTo((Boolean) invocationOnMock.getArguments()[4]))); + assertThat(11000, is(equalTo((Integer) invocationOnMock.getArguments()[5]))); + assertThat(12000, is(equalTo((Integer) invocationOnMock.getArguments()[6]))); + assertThat((RequestDecorator) invocationOnMock.getArguments()[7], instanceOf(RequestDecorator.class)); + argsCaptured.set(true); + return mockclient; + } + ); + + OkHttpModule module = OkHttpModule.builder() + .proxyAuthScheme(ProxyAuthScheme.KERBEROS) + .proxyAuthKerberosPrincipalName("bilal@bilal") + .proxyHost("some-proxy") + .proxyPort(3128) + .connectionTimeout(12000) + .readTimeout(11000) + .build(); + + SplitClientConfig cfg = SplitClientConfig.builder() + .alternativeHTTPModule(module) + .streamingEnabled(false) + .build(); + + SplitFactoryImpl factory = (SplitFactoryImpl) SplitFactoryBuilder.build(apiToken, cfg); + +// module.createClient(apiToken, sdkMetadata, requestDecorator); + assertThat(true, is(equalTo(argsCaptured.get()))); + } +} diff --git a/okhttp-modules/src/test/resources/krb5.conf b/okhttp-modules/src/test/resources/krb5.conf new file mode 100644 index 000000000..78d63ba8f --- /dev/null +++ b/okhttp-modules/src/test/resources/krb5.conf @@ -0,0 +1,37 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +[libdefaults] + kdc_realm = ATHENA.MIT.EDU + default_realm = ATHENA.MIT.EDU + kdc_tcp_port = 88 + kdc_udp_port = 88 + dns_lookup_realm = false + dns_lookup_kdc = false + udp_preference_limit = 1 + +[logging] + default = FILE:/var/logs/krb5kdc.log + +[realms] + ATHENA.MIT.EDU = { +# kdc = 10.12.4.76:88 +# kdc = tcp/10.12.4.76:88 +# kdc = tcp/192.168.1.19:88 + kdc = 192.168.1.19:88 + } \ No newline at end of file diff --git a/okhttp-modules/src/test/resources/org/powermock/extensions/configuration.properties b/okhttp-modules/src/test/resources/org/powermock/extensions/configuration.properties new file mode 100644 index 000000000..a8ebaeba3 --- /dev/null +++ b/okhttp-modules/src/test/resources/org/powermock/extensions/configuration.properties @@ -0,0 +1 @@ +powermock.global-ignore=jdk.internal.reflect.*,javax.net.ssl.* \ No newline at end of file diff --git a/okhttp-modules/src/test/resources/split-change-special-characters.json b/okhttp-modules/src/test/resources/split-change-special-characters.json new file mode 100644 index 000000000..9fd55904e --- /dev/null +++ b/okhttp-modules/src/test/resources/split-change-special-characters.json @@ -0,0 +1,56 @@ +{ + "splits": [ + { + "trafficTypeName": "user", + "name": "DEMO_MURMUR2", + "trafficAllocation": 100, + "trafficAllocationSeed": 1314112417, + "seed": -2059033614, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "of", + "changeNumber": 1491244291288, + "sets": [ "set1", "set2" ], + "algo": 2, + "configurations": { + "on": "{\"test\": \"blue\",\"grüne Straße\": 13}", + "off": "{\"test\": \"blue\",\"size\": 15}" + }, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "of", + "size": 100 + } + ], + "label": "in segment all" + } + ] + } + ], + "since": 1491244291288, + "till": 1491244291288 +} diff --git a/pluggable-storage/pom.xml b/pluggable-storage/pom.xml index f643564f9..2e502e35c 100644 --- a/pluggable-storage/pom.xml +++ b/pluggable-storage/pom.xml @@ -6,7 +6,7 @@ <parent> <artifactId>java-client-parent</artifactId> <groupId>io.split.client</groupId> - <version>4.12.1</version> + <version>4.13.0</version> </parent> <version>2.1.0</version> @@ -29,7 +29,7 @@ <serverId>ossrh</serverId> <nexusUrl>https://oss.sonatype.org/</nexusUrl> <autoReleaseAfterClose>true</autoReleaseAfterClose> - <skipNexusStagingDeployMojo>false</skipNexusStagingDeployMojo> + <skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo> </configuration> </plugin> </plugins> diff --git a/pom.xml b/pom.xml index 3dc7d33f9..a7c51ff30 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>io.split.client</groupId> <artifactId>java-client-parent</artifactId> - <version>4.12.1</version> + <version>4.13.0</version> <dependencyManagement> <dependencies> <dependency> @@ -81,10 +81,11 @@ <maven.compiler.target>1.8</maven.compiler.target> </properties> <modules> - <module>testing</module> - <module>client</module> <module>pluggable-storage</module> <module>redis-wrapper</module> + <module>testing</module> + <module>okhttp-modules</module> + <module>client</module> </modules> <build> <plugins> diff --git a/redis-wrapper/pom.xml b/redis-wrapper/pom.xml index a8ce195f5..6a25062ed 100644 --- a/redis-wrapper/pom.xml +++ b/redis-wrapper/pom.xml @@ -6,7 +6,7 @@ <parent> <artifactId>java-client-parent</artifactId> <groupId>io.split.client</groupId> - <version>4.12.1</version> + <version>4.13.0</version> </parent> <artifactId>redis-wrapper</artifactId> <version>3.1.0</version> @@ -51,7 +51,7 @@ <serverId>ossrh</serverId> <nexusUrl>https://oss.sonatype.org/</nexusUrl> <autoReleaseAfterClose>true</autoReleaseAfterClose> - <skipNexusStagingDeployMojo>false</skipNexusStagingDeployMojo> + <skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo> </configuration> </plugin> </plugins> diff --git a/testing/pom.xml b/testing/pom.xml index 2240c94db..adbffc998 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>io.split.client</groupId> <artifactId>java-client-parent</artifactId> - <version>4.12.1</version> + <version>4.13.0</version> </parent> <artifactId>java-client-testing</artifactId> <packaging>jar</packaging>