From 5c6220823656b0089df2b504c1667eeb8c6f436c Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Wed, 3 Aug 2016 11:37:01 -0700 Subject: [PATCH 1/3] Add secure mode hash method --- .../com/launchdarkly/client/LDClient.java | 31 ++++++++++++++++++- .../com/launchdarkly/client/LDClientTest.java | 10 ++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/launchdarkly/client/LDClient.java b/src/main/java/com/launchdarkly/client/LDClient.java index 5eca9bd7c..8d4690a42 100644 --- a/src/main/java/com/launchdarkly/client/LDClient.java +++ b/src/main/java/com/launchdarkly/client/LDClient.java @@ -4,13 +4,19 @@ import com.google.common.annotations.VisibleForTesting; import com.google.gson.JsonElement; import com.google.gson.JsonPrimitive; +import org.apache.commons.codec.binary.Hex; import org.apache.http.annotation.ThreadSafe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; import java.io.Closeable; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URL; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Future; @@ -26,11 +32,14 @@ @ThreadSafe public class LDClient implements Closeable { private static final Logger logger = LoggerFactory.getLogger(LDClient.class); + private static final String HMAC_ALGORITHM = "HmacSHA256"; + protected static final String CLIENT_VERSION = getClientVersion(); + private final LDConfig config; + private final String apiKey; private final FeatureRequestor requestor; private final EventProcessor eventProcessor; private UpdateProcessor updateProcessor; - protected static final String CLIENT_VERSION = getClientVersion(); /** * Creates a new client instance that connects to LaunchDarkly with the default configuration. In most @@ -51,6 +60,7 @@ public LDClient(String apiKey) { */ public LDClient(String apiKey, LDConfig config) { this.config = config; + this.apiKey = apiKey; this.requestor = createFeatureRequestor(apiKey, config); this.eventProcessor = createEventProcessor(apiKey, config); @@ -368,6 +378,25 @@ public boolean isOffline() { return config.offline; } + /** + * For more info: https://github.com/launchdarkly/js-client#secure-mode + * @param user The User to be hashed along with the api key + * @return the hash, or null if the hash could not be calculated. + */ + public String secureModeHash(LDUser user) { + if (user == null || user.getKeyAsString().isEmpty()) { + return null; + } + try { + Mac mac = Mac.getInstance(HMAC_ALGORITHM); + mac.init(new SecretKeySpec(apiKey.getBytes(), HMAC_ALGORITHM)); + return Hex.encodeHexString(mac.doFinal(user.getKeyAsString().getBytes("UTF8"))); + } catch (InvalidKeyException | UnsupportedEncodingException | NoSuchAlgorithmException e) { + logger.error("Could not generate secure mode hash", e); + } + return null; + } + private static String getClientVersion() { Class clazz = LDConfig.class; String className = clazz.getSimpleName() + ".class"; diff --git a/src/test/java/com/launchdarkly/client/LDClientTest.java b/src/test/java/com/launchdarkly/client/LDClientTest.java index 521762c61..179a98cc9 100644 --- a/src/test/java/com/launchdarkly/client/LDClientTest.java +++ b/src/test/java/com/launchdarkly/client/LDClientTest.java @@ -341,6 +341,16 @@ public void testPollingWait() throws Exception { verifyAll(); } + @Test + public void testSecureModeHash() { + LDConfig config = new LDConfig.Builder() + .offline(true) + .build(); + LDClient client = new LDClient("secret", config); + LDUser user = new LDUser.Builder("Message").build(); + assertEquals("aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597", client.secureModeHash(user)); + } + private void assertDefaultValueIsReturned() { boolean result = client.boolVariation("test", new LDUser("test.key"), true); assertEquals(true, result); From 16e2cdc8cdf28bfe0de291c6bfcb6f0924610bc9 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Wed, 3 Aug 2016 16:00:17 -0700 Subject: [PATCH 2/3] Add allFlags() method --- .../com/launchdarkly/client/FeatureFlag.java | 14 ++++- .../com/launchdarkly/client/LDClient.java | 52 ++++++++++--------- .../launchdarkly/client/FeatureFlagTest.java | 2 +- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/launchdarkly/client/FeatureFlag.java b/src/main/java/com/launchdarkly/client/FeatureFlag.java index c622f24e2..67f1dc728 100644 --- a/src/main/java/com/launchdarkly/client/FeatureFlag.java +++ b/src/main/java/com/launchdarkly/client/FeatureFlag.java @@ -53,9 +53,19 @@ static Map fromJsonMap(String json) { } EvalResult evaluate(LDUser user, FeatureStore featureStore) throws EvaluationException { + if (user == null || user.getKeyAsString().isEmpty()) { + logger.warn("Null user or null/empty user key when evaluating flag: " + key + "; returning null"); + return null; + } List prereqEvents = new ArrayList<>(); - JsonElement value = evaluate(user, featureStore, prereqEvents); - return new EvalResult(value, prereqEvents); + if (isOn()) { + JsonElement value = evaluate(user, featureStore, prereqEvents); + if (value != null) { + return new EvalResult(value, prereqEvents); + } + } + JsonElement offVariation = getOffVariationValue(); + return new EvalResult(offVariation, prereqEvents); } // Returning either a JsonElement or null indicating prereq failure/error. diff --git a/src/main/java/com/launchdarkly/client/LDClient.java b/src/main/java/com/launchdarkly/client/LDClient.java index 8d4690a42..89d5de247 100644 --- a/src/main/java/com/launchdarkly/client/LDClient.java +++ b/src/main/java/com/launchdarkly/client/LDClient.java @@ -184,33 +184,42 @@ private void sendFlagRequestEvent(String featureKey, LDUser user, JsonElement va } /** - * Returns a map from feature flag keys to Boolean feature flag values for a given user. The map will contain {@code null} - * entries for any flags that are off or for any feature flags with non-boolean variations. If the client is offline or - * has not been initialized, a {@code null} map will be returned. + * Returns a map from feature flag keys to {@code JsonElement} feature flag values for a given user. + * If the result of a flag's evaluation would have returned the default variation, it will have a null entry + * in the map. If the client is offline, has not been initialized, or a null user or user with null/empty user key a {@code null} map will be returned. * This method will not send analytics events back to LaunchDarkly. *

* The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service. * * @param user the end user requesting the feature flags - * @return a map from feature flag keys to JsonElement values for the specified user + * @return a map from feature flag keys to {@code JsonElement} for the specified user */ - public Map allFlags(LDUser user) { + public Map allFlags(LDUser user) { if (isOffline()) { + logger.warn("allFlags() was called when client is in offline mode! Returning null."); return null; } if (!initialized()) { + logger.warn("allFlags() was called before Client has been initialized! Returning null."); + return null; + } + + if (user == null || user.getKeyAsString().isEmpty()) { + logger.warn("allFlags() was called with null user or null/empty user key! returning null"); return null; } Map flags = this.config.featureStore.all(); - Map result = new HashMap<>(); + Map result = new HashMap<>(); - for (String key : flags.keySet()) { - JsonElement evalResult = evaluate(key, user, null); - if (evalResult.isJsonPrimitive() && evalResult.getAsJsonPrimitive().isBoolean()) { - result.put(key, evalResult.getAsBoolean()); + for (Map.Entry entry : flags.entrySet()) { + try { + JsonElement evalResult = entry.getValue().evaluate(user, config.featureStore).getValue(); + result.put(entry.getKey(), evalResult); + } catch (EvaluationException e) { + logger.error("Exception caught when evaluating all flags:", e); } } return result; @@ -325,22 +334,15 @@ private JsonElement evaluate(String featureKey, LDUser user, JsonElement default sendFlagRequestEvent(featureKey, user, defaultValue, defaultValue, null); return defaultValue; } - if (featureFlag.isOn()) { - FeatureFlag.EvalResult evalResult = featureFlag.evaluate(user, config.featureStore); - if (!isOffline()) { - for (FeatureRequestEvent event : evalResult.getPrerequisiteEvents()) { - eventProcessor.sendEvent(event); - } - } - if (evalResult.getValue() != null) { - sendFlagRequestEvent(featureKey, user, evalResult.getValue(), defaultValue, featureFlag.getVersion()); - return evalResult.getValue(); - } + FeatureFlag.EvalResult evalResult = featureFlag.evaluate(user, config.featureStore); + if (!isOffline()) { + for (FeatureRequestEvent event : evalResult.getPrerequisiteEvents()) { + eventProcessor.sendEvent(event); + } } - JsonElement offVariation = featureFlag.getOffVariationValue(); - if (offVariation != null) { - sendFlagRequestEvent(featureKey, user, offVariation, defaultValue, featureFlag.getVersion()); - return offVariation; + if (evalResult.getValue() != null) { + sendFlagRequestEvent(featureKey, user, evalResult.getValue(), defaultValue, featureFlag.getVersion()); + return evalResult.getValue(); } } catch (Exception e) { logger.error("Encountered exception in LaunchDarkly client", e); diff --git a/src/test/java/com/launchdarkly/client/FeatureFlagTest.java b/src/test/java/com/launchdarkly/client/FeatureFlagTest.java index 372591ddd..4c27d6f1d 100644 --- a/src/test/java/com/launchdarkly/client/FeatureFlagTest.java +++ b/src/test/java/com/launchdarkly/client/FeatureFlagTest.java @@ -62,7 +62,7 @@ public void testPrereqCollectsEventsForPrereqs() throws EvaluationException { FeatureFlag.EvalResult flagCResult = flagC.evaluate(user, featureStore); Assert.assertNotNull(flagCResult); - Assert.assertEquals(new JsonPrimitive(0), flagCResult.getValue()); + Assert.assertEquals(null, flagCResult.getValue()); Assert.assertEquals(0, flagCResult.getPrerequisiteEvents().size()); } From fdc35436b942f2a5509439ac5829ce9a06b352dc Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Wed, 3 Aug 2016 17:52:33 -0700 Subject: [PATCH 3/3] api_key->sdk_key. Change logging when stream is reconnecting to be less spammy. --- README.md | 4 +-- .../launchdarkly/client/EventProcessor.java | 8 ++--- .../launchdarkly/client/FeatureRequestor.java | 10 +++--- .../com/launchdarkly/client/LDClient.java | 36 +++++++++---------- .../com/launchdarkly/client/LDConfig.java | 23 +++--------- .../launchdarkly/client/StreamProcessor.java | 11 +++--- .../java/com/launchdarkly/client/Util.java | 2 +- .../com/launchdarkly/client/LDClientTest.java | 8 ++--- 8 files changed, 44 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index d6df3c3da..4358ae0c8 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ Quick setup import com.launchdarkly.client.*; -2. Create a new LDClient with your API key: +2. Create a new LDClient with your SDK key: - LDClient ldClient = new LDClient("YOUR_API_KEY"); + LDClient ldClient = new LDClient("YOUR_SDK_KEY"); Your first feature flag ----------------------- diff --git a/src/main/java/com/launchdarkly/client/EventProcessor.java b/src/main/java/com/launchdarkly/client/EventProcessor.java index a5e5ba0dc..c7ae5e84d 100644 --- a/src/main/java/com/launchdarkly/client/EventProcessor.java +++ b/src/main/java/com/launchdarkly/client/EventProcessor.java @@ -21,12 +21,12 @@ class EventProcessor implements Closeable { private final ScheduledExecutorService scheduler; private final Random random = new Random(); private final BlockingQueue queue; - private final String apiKey; + private final String sdkKey; private final LDConfig config; private final Consumer consumer; - EventProcessor(String apiKey, LDConfig config) { - this.apiKey = apiKey; + EventProcessor(String sdkKey, LDConfig config) { + this.sdkKey = sdkKey; this.queue = new ArrayBlockingQueue<>(config.capacity); this.consumer = new Consumer(config); this.config = config; @@ -85,7 +85,7 @@ private void postEvents(List events) { Gson gson = new Gson(); String json = gson.toJson(events); - HttpPost request = config.postEventsRequest(apiKey, "/bulk"); + HttpPost request = config.postEventsRequest(sdkKey, "/bulk"); StringEntity entity = new StringEntity(json, "UTF-8"); entity.setContentType("application/json"); request.setEntity(entity); diff --git a/src/main/java/com/launchdarkly/client/FeatureRequestor.java b/src/main/java/com/launchdarkly/client/FeatureRequestor.java index 56f814865..54f535e2f 100644 --- a/src/main/java/com/launchdarkly/client/FeatureRequestor.java +++ b/src/main/java/com/launchdarkly/client/FeatureRequestor.java @@ -19,13 +19,13 @@ class FeatureRequestor { public static final String GET_LATEST_FLAGS_PATH = "/sdk/latest-flags"; - private final String apiKey; + private final String sdkKey; private final LDConfig config; private final CloseableHttpClient client; private static final Logger logger = LoggerFactory.getLogger(FeatureRequestor.class); - FeatureRequestor(String apiKey, LDConfig config) { - this.apiKey = apiKey; + FeatureRequestor(String sdkKey, LDConfig config) { + this.sdkKey = sdkKey; this.config = config; this.client = createClient(); } @@ -58,7 +58,7 @@ protected CloseableHttpClient createClient() { Map makeAllRequest() throws IOException { HttpCacheContext context = HttpCacheContext.create(); - HttpGet request = config.getRequest(apiKey, GET_LATEST_FLAGS_PATH); + HttpGet request = config.getRequest(sdkKey, GET_LATEST_FLAGS_PATH); CloseableHttpResponse response = null; try { @@ -107,7 +107,7 @@ FeatureFlag makeRequest(String featureKey, boolean latest) throws IOException { String resource = latest ? "/api/eval/latest-features/" : "/api/eval/features/"; - HttpGet request = config.getRequest(apiKey,resource + featureKey); + HttpGet request = config.getRequest(sdkKey,resource + featureKey); CloseableHttpResponse response = null; try { diff --git a/src/main/java/com/launchdarkly/client/LDClient.java b/src/main/java/com/launchdarkly/client/LDClient.java index 89d5de247..37533a89a 100644 --- a/src/main/java/com/launchdarkly/client/LDClient.java +++ b/src/main/java/com/launchdarkly/client/LDClient.java @@ -36,7 +36,7 @@ public class LDClient implements Closeable { protected static final String CLIENT_VERSION = getClientVersion(); private final LDConfig config; - private final String apiKey; + private final String sdkKey; private final FeatureRequestor requestor; private final EventProcessor eventProcessor; private UpdateProcessor updateProcessor; @@ -45,24 +45,24 @@ public class LDClient implements Closeable { * Creates a new client instance that connects to LaunchDarkly with the default configuration. In most * cases, you should use this constructor. * - * @param apiKey the API key for your account + * @param sdkKey the SDK key for your LaunchDarkly environment */ - public LDClient(String apiKey) { - this(apiKey, LDConfig.DEFAULT); + public LDClient(String sdkKey) { + this(sdkKey, LDConfig.DEFAULT); } /** * Creates a new client to connect to LaunchDarkly with a custom configuration. This constructor * can be used to configure advanced client features, such as customizing the LaunchDarkly base URL. * - * @param apiKey the API key for your account + * @param sdkKey the SDK key for your LaunchDarkly environment * @param config a client configuration object */ - public LDClient(String apiKey, LDConfig config) { + public LDClient(String sdkKey, LDConfig config) { this.config = config; - this.apiKey = apiKey; - this.requestor = createFeatureRequestor(apiKey, config); - this.eventProcessor = createEventProcessor(apiKey, config); + this.sdkKey = sdkKey; + this.requestor = createFeatureRequestor(sdkKey, config); + this.eventProcessor = createEventProcessor(sdkKey, config); if (config.offline) { logger.info("Starting LaunchDarkly client in offline mode"); @@ -76,7 +76,7 @@ public LDClient(String apiKey, LDConfig config) { if (config.stream) { logger.info("Enabling streaming API"); - this.updateProcessor = createStreamProcessor(apiKey, config, requestor); + this.updateProcessor = createStreamProcessor(sdkKey, config, requestor); } else { logger.info("Disabling streaming API"); this.updateProcessor = createPollingProcessor(config); @@ -101,18 +101,18 @@ public boolean initialized() { } @VisibleForTesting - protected FeatureRequestor createFeatureRequestor(String apiKey, LDConfig config) { - return new FeatureRequestor(apiKey, config); + protected FeatureRequestor createFeatureRequestor(String sdkKey, LDConfig config) { + return new FeatureRequestor(sdkKey, config); } @VisibleForTesting - protected EventProcessor createEventProcessor(String apiKey, LDConfig config) { - return new EventProcessor(apiKey, config); + protected EventProcessor createEventProcessor(String sdkKey, LDConfig config) { + return new EventProcessor(sdkKey, config); } @VisibleForTesting - protected StreamProcessor createStreamProcessor(String apiKey, LDConfig config, FeatureRequestor requestor) { - return new StreamProcessor(apiKey, config, requestor); + protected StreamProcessor createStreamProcessor(String sdkKey, LDConfig config, FeatureRequestor requestor) { + return new StreamProcessor(sdkKey, config, requestor); } @VisibleForTesting @@ -382,7 +382,7 @@ public boolean isOffline() { /** * For more info: https://github.com/launchdarkly/js-client#secure-mode - * @param user The User to be hashed along with the api key + * @param user The User to be hashed along with the sdk key * @return the hash, or null if the hash could not be calculated. */ public String secureModeHash(LDUser user) { @@ -391,7 +391,7 @@ public String secureModeHash(LDUser user) { } try { Mac mac = Mac.getInstance(HMAC_ALGORITHM); - mac.init(new SecretKeySpec(apiKey.getBytes(), HMAC_ALGORITHM)); + mac.init(new SecretKeySpec(sdkKey.getBytes(), HMAC_ALGORITHM)); return Hex.encodeHexString(mac.doFinal(user.getKeyAsString().getBytes("UTF8"))); } catch (InvalidKeyException | UnsupportedEncodingException | NoSuchAlgorithmException e) { logger.error("Could not generate secure mode hash", e); diff --git a/src/main/java/com/launchdarkly/client/LDConfig.java b/src/main/java/com/launchdarkly/client/LDConfig.java index 1dc0a959b..1f9fff861 100644 --- a/src/main/java/com/launchdarkly/client/LDConfig.java +++ b/src/main/java/com/launchdarkly/client/LDConfig.java @@ -376,12 +376,12 @@ private URIBuilder getEventsBuilder() { .setPort(eventsURI.getPort()); } - HttpGet getRequest(String apiKey, String path) { + HttpGet getRequest(String sdkKey, String path) { URIBuilder builder = this.getBuilder().setPath(path); try { HttpGet request = new HttpGet(builder.build()); - request.addHeader("Authorization", "api_key " + apiKey); + request.addHeader("Authorization", sdkKey); request.addHeader("User-Agent", "JavaClient/" + LDClient.CLIENT_VERSION); return request; @@ -391,27 +391,12 @@ HttpGet getRequest(String apiKey, String path) { } } - HttpPost postRequest(String apiKey, String path) { - URIBuilder builder = this.getBuilder().setPath(path); - - try { - HttpPost request = new HttpPost(builder.build()); - request.addHeader("Authorization", "api_key " + apiKey); - request.addHeader("User-Agent", "JavaClient/" + LDClient.CLIENT_VERSION); - - return request; - } catch (Exception e) { - logger.error("Unhandled exception in LaunchDarkly client", e); - return null; - } - } - - HttpPost postEventsRequest(String apiKey, String path) { + HttpPost postEventsRequest(String sdkKey, String path) { URIBuilder builder = this.getEventsBuilder().setPath(eventsURI.getPath() + path); try { HttpPost request = new HttpPost(builder.build()); - request.addHeader("Authorization", "api_key " + apiKey); + request.addHeader("Authorization", sdkKey); request.addHeader("User-Agent", "JavaClient/" + LDClient.CLIENT_VERSION); return request; diff --git a/src/main/java/com/launchdarkly/client/StreamProcessor.java b/src/main/java/com/launchdarkly/client/StreamProcessor.java index 9f29536b1..35e55dece 100644 --- a/src/main/java/com/launchdarkly/client/StreamProcessor.java +++ b/src/main/java/com/launchdarkly/client/StreamProcessor.java @@ -23,16 +23,16 @@ class StreamProcessor implements UpdateProcessor { private final FeatureStore store; private final LDConfig config; - private final String apiKey; + private final String sdkKey; private final FeatureRequestor requestor; private EventSource es; private AtomicBoolean initialized = new AtomicBoolean(false); - StreamProcessor(String apiKey, LDConfig config, FeatureRequestor requestor) { + StreamProcessor(String sdkKey, LDConfig config, FeatureRequestor requestor) { this.store = config.featureStore; this.config = config; - this.apiKey = apiKey; + this.sdkKey = sdkKey; this.requestor = requestor; } @@ -41,7 +41,7 @@ public Future start() { final VeryBasicFuture initFuture = new VeryBasicFuture(); Headers headers = new Headers.Builder() - .add("Authorization", "api_key " + this.apiKey) + .add("Authorization", this.sdkKey) .add("User-Agent", "JavaClient/" + LDClient.CLIENT_VERSION) .add("Accept", "text/event-stream") .build(); @@ -102,7 +102,8 @@ public void onMessage(String name, MessageEvent event) throws Exception { @Override public void onError(Throwable throwable) { - logger.warn("Encountered EventSource error", throwable); + logger.error("Encountered EventSource error: " + throwable.getMessage()); + logger.debug("", throwable); } }; diff --git a/src/main/java/com/launchdarkly/client/Util.java b/src/main/java/com/launchdarkly/client/Util.java index b7f8571c8..2de223022 100644 --- a/src/main/java/com/launchdarkly/client/Util.java +++ b/src/main/java/com/launchdarkly/client/Util.java @@ -43,7 +43,7 @@ static boolean handleResponse(Logger logger, HttpRequestBase request, CloseableH return true; } if (statusCode == HttpStatus.SC_UNAUTHORIZED) { - logger.error("[401] Invalid API key when accessing URI: " + request.getURI().toString()); + logger.error("[401] Invalid SDK key when accessing URI: " + request.getURI().toString()); } else { logger.error("[" + statusCode + "] " + response.getStatusLine().getReasonPhrase() + " When accessing URI: " + request.getURI().toString()); } diff --git a/src/test/java/com/launchdarkly/client/LDClientTest.java b/src/test/java/com/launchdarkly/client/LDClientTest.java index 179a98cc9..92439bbe0 100644 --- a/src/test/java/com/launchdarkly/client/LDClientTest.java +++ b/src/test/java/com/launchdarkly/client/LDClientTest.java @@ -357,14 +357,14 @@ private void assertDefaultValueIsReturned() { } private LDClient createMockClient(LDConfig config) { - return new LDClient("API_KEY", config) { + return new LDClient("SDK_KEY", config) { @Override - protected FeatureRequestor createFeatureRequestor(String apiKey, LDConfig config) { + protected FeatureRequestor createFeatureRequestor(String sdkKey, LDConfig config) { return requestor; } @Override - protected StreamProcessor createStreamProcessor(String apiKey, LDConfig config, FeatureRequestor requestor) { + protected StreamProcessor createStreamProcessor(String sdkKey, LDConfig config, FeatureRequestor requestor) { return streamProcessor; } @@ -374,7 +374,7 @@ protected PollingProcessor createPollingProcessor(LDConfig config) { } @Override - protected EventProcessor createEventProcessor(String apiKey, LDConfig config) { + protected EventProcessor createEventProcessor(String sdkKey, LDConfig config) { return eventProcessor; } };