diff --git a/bazel/envoy_mobile_test_extensions.bzl b/bazel/envoy_mobile_test_extensions.bzl index 17db8ce6fa..d7edf2bb58 100644 --- a/bazel/envoy_mobile_test_extensions.bzl +++ b/bazel/envoy_mobile_test_extensions.bzl @@ -5,4 +5,5 @@ TEST_EXTENSIONS = [ "//library/common/extensions/filters/http/test_logger:config", "//library/common/extensions/filters/http/test_accessor:config", "//library/common/extensions/filters/http/test_event_tracker:config", + "//library/common/extensions/filters/http/test_kv_store:config", ] diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index 25d05e8dbd..ece69bf8ca 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -18,6 +18,7 @@ Bugfixes: Features: +- android: add support for registering a platform KV store (:issue: `#2134 <2134>`) - api: add option to extend the keepalive timeout when any frame is received on the owning HTTP/2 connection. (:issue:`#2229 <2229>`) - api: add option to control whether Envoy should drain connections after a soft DNS refresh completes. (:issue:`#2225 <2225>`, :issue:`#2242 <2242>`) - configuration: enable h2 ping by default. (:issue: `#2270 <2270>`) diff --git a/library/common/extensions/filters/http/test_kv_store/BUILD b/library/common/extensions/filters/http/test_kv_store/BUILD new file mode 100644 index 0000000000..418364d8c9 --- /dev/null +++ b/library/common/extensions/filters/http/test_kv_store/BUILD @@ -0,0 +1,45 @@ +load( + "@envoy//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", + "envoy_proto_library", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_proto_library( + name = "filter", + srcs = ["filter.proto"], + deps = [ + "@envoy_api//envoy/config/common/matcher/v3:pkg", + ], +) + +envoy_cc_extension( + name = "test_kv_store_filter_lib", + srcs = ["filter.cc"], + hdrs = ["filter.h"], + repository = "@envoy", + deps = [ + "filter_cc_proto", + "//library/common/api:c_types", + "//library/common/api:external_api_lib", + "//library/common/data:utility_lib", + "//library/common/extensions/key_value/platform:config", + "@envoy//source/common/common:assert_lib", + "@envoy//source/extensions/filters/http/common:pass_through_filter_lib", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + repository = "@envoy", + deps = [ + ":test_kv_store_filter_lib", + "@envoy//source/extensions/filters/http/common:factory_base_lib", + ], +) diff --git a/library/common/extensions/filters/http/test_kv_store/config.cc b/library/common/extensions/filters/http/test_kv_store/config.cc new file mode 100644 index 0000000000..54bfd918d6 --- /dev/null +++ b/library/common/extensions/filters/http/test_kv_store/config.cc @@ -0,0 +1,29 @@ +#include "library/common/extensions/filters/http/test_kv_store/config.h" + +#include "library/common/extensions/filters/http/test_kv_store/filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace TestKeyValueStore { + +Http::FilterFactoryCb TestKeyValueStoreFilterFactory::createFilterFactoryFromProtoTyped( + const envoymobile::extensions::filters::http::test_kv_store::TestKeyValueStore& proto_config, + const std::string&, Server::Configuration::FactoryContext&) { + + auto config = std::make_shared(proto_config); + return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamFilter(std::make_shared(config)); + }; +} + +/** + * Static registration for the TestKeyValueStore filter. @see NamedHttpFilterConfigFactory. + */ +REGISTER_FACTORY(TestKeyValueStoreFilterFactory, + Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace TestKeyValueStore +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/extensions/filters/http/test_kv_store/config.h b/library/common/extensions/filters/http/test_kv_store/config.h new file mode 100644 index 0000000000..67b84091ba --- /dev/null +++ b/library/common/extensions/filters/http/test_kv_store/config.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "source/extensions/filters/http/common/factory_base.h" + +#include "library/common/extensions/filters/http/test_kv_store/filter.h" +#include "library/common/extensions/filters/http/test_kv_store/filter.pb.h" +#include "library/common/extensions/filters/http/test_kv_store/filter.pb.validate.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace TestKeyValueStore { + +/** + * Config registration for the TestKeyValueStore filter. @see NamedHttpFilterConfigFactory. + */ +class TestKeyValueStoreFilterFactory + : public Common::FactoryBase< + envoymobile::extensions::filters::http::test_kv_store::TestKeyValueStore> { +public: + TestKeyValueStoreFilterFactory() : FactoryBase("test_kv_store") {} + +private: + ::Envoy::Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoymobile::extensions::filters::http::test_kv_store::TestKeyValueStore& config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; +}; + +DECLARE_FACTORY(TestKeyValueStoreFilterFactory); + +} // namespace TestKeyValueStore +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/extensions/filters/http/test_kv_store/filter.cc b/library/common/extensions/filters/http/test_kv_store/filter.cc new file mode 100644 index 0000000000..d20a0ecf80 --- /dev/null +++ b/library/common/extensions/filters/http/test_kv_store/filter.cc @@ -0,0 +1,46 @@ +#include "library/common/extensions/filters/http/test_kv_store/filter.h" + +#include "envoy/server/filter_config.h" + +#include "source/common/common/assert.h" + +#include "library/common/data/utility.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace TestKeyValueStore { + +TestKeyValueStoreFilterConfig::TestKeyValueStoreFilterConfig( + const envoymobile::extensions::filters::http::test_kv_store::TestKeyValueStore& proto_config) + : kv_store_( + static_cast(Api::External::retrieveApi(proto_config.kv_store_name()))) {} + +Http::FilterHeadersStatus TestKeyValueStoreFilter::decodeHeaders(Http::RequestHeaderMap&, bool) { + const auto store = config_->keyValueStore(); + auto key = Data::Utility::copyToBridgeData(config_->testKey()); + RELEASE_ASSERT(Data::Utility::copyToString(store->read(key, store->context)).empty(), + "store should be empty"); + + envoy_data value = Data::Utility::copyToBridgeData(config_->testValue()); + store->save(key, value, store->context); + return Http::FilterHeadersStatus::Continue; +} + +Http::FilterHeadersStatus TestKeyValueStoreFilter::encodeHeaders(Http::ResponseHeaderMap&, bool) { + const auto store = config_->keyValueStore(); + auto key = Data::Utility::copyToBridgeData(config_->testKey()); + RELEASE_ASSERT(Data::Utility::copyToString(store->read(key, store->context)) == + config_->testValue(), + "store did not contain expected value"); + + store->remove(key, store->context); + RELEASE_ASSERT(Data::Utility::copyToString(store->read(key, store->context)).empty(), + "store should be empty"); + + return Http::FilterHeadersStatus::Continue; +} +} // namespace TestKeyValueStore +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/extensions/filters/http/test_kv_store/filter.h b/library/common/extensions/filters/http/test_kv_store/filter.h new file mode 100644 index 0000000000..6dfc29bd6e --- /dev/null +++ b/library/common/extensions/filters/http/test_kv_store/filter.h @@ -0,0 +1,59 @@ +#pragma once + +#include "envoy/http/filter.h" + +#include "source/extensions/filters/http/common/pass_through_filter.h" + +#include "library/common/api/c_types.h" +#include "library/common/api/external.h" +#include "library/common/extensions/filters/http/test_kv_store/filter.pb.h" +#include "library/common/extensions/key_value/platform/c_types.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace TestKeyValueStore { + +/** + * This is a test-only filter used for validating PlatformKeyValueStore integrations. It retrieves + * a known key-value store implementation and issues a series of fixed calls to it, allowing for + * validation to be performed from platform code. + * + * TODO(goaway): Move to a location for test components outside of the main source tree. + */ +class TestKeyValueStoreFilterConfig { +public: + TestKeyValueStoreFilterConfig( + const envoymobile::extensions::filters::http::test_kv_store::TestKeyValueStore& proto_config); + + const envoy_kv_store* keyValueStore() const { return kv_store_; } + const std::string& testKey() const { return test_key_; } + const std::string& testValue() const { return test_value_; } + +private: + const envoy_kv_store* kv_store_; + const std::string test_key_; + const std::string test_value_; +}; + +using TestKeyValueStoreFilterConfigSharedPtr = std::shared_ptr; + +class TestKeyValueStoreFilter final : public ::Envoy::Http::PassThroughFilter { +public: + TestKeyValueStoreFilter(TestKeyValueStoreFilterConfigSharedPtr config) : config_(config) {} + + // StreamDecoderFilter + ::Envoy::Http::FilterHeadersStatus decodeHeaders(::Envoy::Http::RequestHeaderMap& headers, + bool end_stream) override; + // StreamEncoderFilter + ::Envoy::Http::FilterHeadersStatus encodeHeaders(::Envoy::Http::ResponseHeaderMap& headers, + bool end_stream) override; + +private: + const TestKeyValueStoreFilterConfigSharedPtr config_; +}; + +} // namespace TestKeyValueStore +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/extensions/filters/http/test_kv_store/filter.proto b/library/common/extensions/filters/http/test_kv_store/filter.proto new file mode 100644 index 0000000000..feea1e015d --- /dev/null +++ b/library/common/extensions/filters/http/test_kv_store/filter.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package envoymobile.extensions.filters.http.test_kv_store; + +import "validate/validate.proto"; + +message TestKeyValueStore { + string kv_store_name = 1 [(validate.rules).string.min_len = 1]; + string test_key = 2 [(validate.rules).string.min_len = 1]; + string test_value = 3 [(validate.rules).string.min_len = 1]; +} diff --git a/library/common/extensions/key_value/platform/BUILD b/library/common/extensions/key_value/platform/BUILD index 4ffc2b6a74..4b38c7afd2 100644 --- a/library/common/extensions/key_value/platform/BUILD +++ b/library/common/extensions/key_value/platform/BUILD @@ -17,10 +17,15 @@ envoy_proto_library( envoy_cc_extension( name = "config", srcs = ["config.cc"], - hdrs = ["config.h"], + hdrs = [ + "c_types.h", + "config.h", + ], repository = "@envoy", deps = [ ":platform_cc_proto", + "//library/common/api:external_api_lib", + "//library/common/data:utility_lib", "@envoy//envoy/common:key_value_store_interface", "@envoy//envoy/event:dispatcher_interface", "@envoy//envoy/filesystem:filesystem_interface", diff --git a/library/common/extensions/key_value/platform/c_types.h b/library/common/extensions/key_value/platform/c_types.h new file mode 100644 index 0000000000..3ff34193a3 --- /dev/null +++ b/library/common/extensions/key_value/platform/c_types.h @@ -0,0 +1,35 @@ +#pragma once + +#include "library/common/types/c_types.h" + +// NOLINT(namespace-envoy) + +#ifdef __cplusplus +extern "C" { // function pointers +#endif + +/** + * Function signature for reading value from implementation. + */ +typedef envoy_data (*envoy_kv_store_read_f)(envoy_data key, const void* context); + +/** + * Function signature for saving value to implementation. + */ +typedef void (*envoy_kv_store_save_f)(envoy_data key, envoy_data value, const void* context); + +/** + * Function signature for removing value from implementation. + */ +typedef void (*envoy_kv_store_remove_f)(envoy_data key, const void* context); + +#ifdef __cplusplus +} // function pointers +#endif + +typedef struct { + envoy_kv_store_read_f read; + envoy_kv_store_save_f save; + envoy_kv_store_remove_f remove; + const void* context; +} envoy_kv_store; diff --git a/library/common/extensions/key_value/platform/config.cc b/library/common/extensions/key_value/platform/config.cc index f47a9152f6..ff56be84ad 100644 --- a/library/common/extensions/key_value/platform/config.cc +++ b/library/common/extensions/key_value/platform/config.cc @@ -4,10 +4,37 @@ #include "envoy/config/common/key_value/v3/config.pb.validate.h" #include "envoy/registry/registry.h" +#include "library/common/api/external.h" +#include "library/common/data/utility.h" +#include "library/common/extensions/key_value/platform/c_types.h" + namespace Envoy { namespace Extensions { namespace KeyValue { +class PlatformInterfaceImpl : PlatformInterface, public Logger::Loggable { +public: + PlatformInterfaceImpl(const std::string& name) + : bridged_store_(*static_cast(Api::External::retrieveApi(name))) {} + + ~PlatformInterfaceImpl() override {} + + std::string read(const std::string& key) const override { + envoy_data bridged_key = Data::Utility::copyToBridgeData(key); + envoy_data bridged_value = bridged_store_.read(bridged_key, bridged_store_.context); + return Data::Utility::copyToString(bridged_value); + } + + void save(const std::string& key, const std::string& contents) override { + envoy_data bridged_key = Data::Utility::copyToBridgeData(key); + envoy_data bridged_value = Data::Utility::copyToBridgeData(contents); + bridged_store_.save(bridged_key, bridged_value, bridged_store_.context); + } + +private: + envoy_kv_store bridged_store_; +}; + PlatformKeyValueStore::PlatformKeyValueStore(Event::Dispatcher& dispatcher, std::chrono::milliseconds save_interval, PlatformInterface& platform_interface, diff --git a/library/common/jni/jni_interface.cc b/library/common/jni/jni_interface.cc index 811087dac6..d2e19955ae 100644 --- a/library/common/jni/jni_interface.cc +++ b/library/common/jni/jni_interface.cc @@ -4,6 +4,7 @@ #include "library/common/api/c_types.h" #include "library/common/extensions/filters/http/platform_bridge/c_types.h" +#include "library/common/extensions/key_value/platform/c_types.h" #include "library/common/jni/import/jni_import.h" #include "library/common/jni/jni_support.h" #include "library/common/jni/jni_utility.h" @@ -787,6 +788,58 @@ static void* jvm_on_send_window_available(envoy_stream_intel stream_intel, void* return result; } +// JvmKeyValueStoreContext +static envoy_data jvm_kv_store_read(envoy_data key, const void* context) { + jni_log("[Envoy]", "jvm_kv_store_read"); + JNIEnv* env = get_env(); + + jobject j_context = static_cast(const_cast(context)); + + jclass jcls_JvmKeyValueStoreContext = env->GetObjectClass(j_context); + jmethodID jmid_read = env->GetMethodID(jcls_JvmKeyValueStoreContext, "read", "([B)[B"); + jbyteArray j_key = native_data_to_array(env, key); + jbyteArray j_value = (jbyteArray)env->CallObjectMethod(j_context, jmid_read, j_key); + envoy_data native_data = array_to_native_data(env, j_value); + + env->DeleteLocalRef(j_value); + env->DeleteLocalRef(j_key); + env->DeleteLocalRef(jcls_JvmKeyValueStoreContext); + + return native_data; +} + +static void jvm_kv_store_remove(envoy_data key, const void* context) { + jni_log("[Envoy]", "jvm_kv_store_remove"); + JNIEnv* env = get_env(); + + jobject j_context = static_cast(const_cast(context)); + + jclass jcls_JvmKeyValueStoreContext = env->GetObjectClass(j_context); + jmethodID jmid_remove = env->GetMethodID(jcls_JvmKeyValueStoreContext, "remove", "([B)V"); + jbyteArray j_key = native_data_to_array(env, key); + env->CallVoidMethod(j_context, jmid_remove, j_key); + + env->DeleteLocalRef(j_key); + env->DeleteLocalRef(jcls_JvmKeyValueStoreContext); +} + +static void jvm_kv_store_save(envoy_data key, envoy_data value, const void* context) { + jni_log("[Envoy]", "jvm_kv_store_save"); + JNIEnv* env = get_env(); + + jobject j_context = static_cast(const_cast(context)); + + jclass jcls_JvmKeyValueStoreContext = env->GetObjectClass(j_context); + jmethodID jmid_save = env->GetMethodID(jcls_JvmKeyValueStoreContext, "save", "([B[B)V"); + jbyteArray j_key = native_data_to_array(env, key); + jbyteArray j_value = native_data_to_array(env, value); + env->CallVoidMethod(j_context, jmid_save, j_key, j_value); + + env->DeleteLocalRef(j_value); + env->DeleteLocalRef(j_key); + env->DeleteLocalRef(jcls_JvmKeyValueStoreContext); +} + // JvmFilterFactoryContext static const void* jvm_http_filter_init(const void* context) { @@ -862,6 +915,29 @@ extern "C" JNIEXPORT jint JNICALL Java_io_envoyproxy_envoymobile_engine_JniLibra return result; } +// EnvoyKeyValueStore + +extern "C" JNIEXPORT jint JNICALL +Java_io_envoyproxy_envoymobile_engine_JniLibrary_registerKeyValueStore(JNIEnv* env, jclass, + jstring name, + jobject j_context) { + + // TODO(goaway): The java context here leaks, but it's tied to the life of the engine. + // This will need to be updated for https://github.com/envoyproxy/envoy-mobile/issues/332 + jni_log("[Envoy]", "registerKeyValueStore"); + jni_log_fmt("[Envoy]", "j_context: %p", j_context); + jobject retained_context = env->NewGlobalRef(j_context); + jni_log_fmt("[Envoy]", "retained_context: %p", retained_context); + envoy_kv_store* api = (envoy_kv_store*)safe_malloc(sizeof(envoy_kv_store)); + api->save = jvm_kv_store_save; + api->read = jvm_kv_store_read; + api->remove = jvm_kv_store_remove; + api->context = retained_context; + + envoy_status_t result = register_platform_api(env->GetStringUTFChars(name, nullptr), api); + return result; +} + // EnvoyHTTPFilter extern "C" JNIEXPORT jint JNICALL diff --git a/library/common/types/c_types.h b/library/common/types/c_types.h index 22f1fc4dba..82f6d6a742 100644 --- a/library/common/types/c_types.h +++ b/library/common/types/c_types.h @@ -7,6 +7,16 @@ // NOLINT(namespace-envoy) +/** + * Throughout this file one may note that most callbacks take a void* context parameter, and most + * callback structs have a void* context field. In typical practice, the value for context on the + * struct is the one passed through in every call made to a callback. This allows platform + * callbacks to propagate state when supplying the callbacks and later, receiving them. Common code + * will not attempt to use or modify this state - it's purely for the platform implementation to + * leverage. Often that might mean it contains references to platform-native objects and/or thread + * dispatch mechanisms that can be used to dispatch the callback as appropriate to platform code. + */ + /** * Handle to an Envoy engine instance. Valid only for the lifetime of the engine and not intended * for any external interpretation or use. diff --git a/library/java/io/envoyproxy/envoymobile/engine/BUILD b/library/java/io/envoyproxy/envoymobile/engine/BUILD index 3cd8171616..b7ec90603b 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/BUILD +++ b/library/java/io/envoyproxy/envoymobile/engine/BUILD @@ -40,6 +40,7 @@ java_library( "JvmCallbackContext.java", "JvmFilterContext.java", "JvmFilterFactoryContext.java", + "JvmKeyValueStoreContext.java", "JvmStringAccessorContext.java", ], visibility = ["//visibility:public"], diff --git a/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java b/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java index 5d8f973e74..3ca2a1c153 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java +++ b/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java @@ -9,6 +9,7 @@ import io.envoyproxy.envoymobile.engine.types.EnvoyHTTPFilterFactory; import io.envoyproxy.envoymobile.engine.types.EnvoyStringAccessor; +import io.envoyproxy.envoymobile.engine.types.EnvoyKeyValueStore; /* Typed configuration that may be used for starting Envoy. */ public class EnvoyConfiguration { @@ -54,6 +55,7 @@ public enum TrustChainVerification { public final String virtualClusters; public final List nativeFilterChain; public final Map stringAccessors; + public final Map keyValueStores; private static final Pattern UNRESOLVED_KEY_PATTERN = Pattern.compile("\\{\\{ (.+) \\}\\}"); @@ -93,6 +95,7 @@ public enum TrustChainVerification { * @param nativeFilterChain the configuration for native filters. * @param httpPlatformFilterFactories the configuration for platform filters. * @param stringAccessors platform string accessors to register. + * @param keyValueStores platform key-value store implementations. */ public EnvoyConfiguration( Boolean adminInterfaceEnabled, String grpcStatsDomain, @Nullable Integer statsdPort, @@ -107,7 +110,8 @@ public EnvoyConfiguration( String appVersion, String appId, TrustChainVerification trustChainVerification, String virtualClusters, List nativeFilterChain, List httpPlatformFilterFactories, - Map stringAccessors) { + Map stringAccessors, + Map keyValueStores) { this.adminInterfaceEnabled = adminInterfaceEnabled; this.grpcStatsDomain = grpcStatsDomain; this.statsdPort = statsdPort; @@ -140,6 +144,7 @@ public EnvoyConfiguration( this.nativeFilterChain = nativeFilterChain; this.httpPlatformFilterFactories = httpPlatformFilterFactories; this.stringAccessors = stringAccessors; + this.keyValueStores = keyValueStores; } /** diff --git a/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java b/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java index 6f1e7e94c2..fa387ad086 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java +++ b/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java @@ -3,6 +3,7 @@ import io.envoyproxy.envoymobile.engine.types.EnvoyEventTracker; import io.envoyproxy.envoymobile.engine.types.EnvoyHTTPCallbacks; import io.envoyproxy.envoymobile.engine.types.EnvoyHTTPFilterFactory; +import io.envoyproxy.envoymobile.engine.types.EnvoyKeyValueStore; import io.envoyproxy.envoymobile.engine.types.EnvoyLogger; import io.envoyproxy.envoymobile.engine.types.EnvoyNetworkType; import io.envoyproxy.envoymobile.engine.types.EnvoyOnEngineRunning; @@ -87,6 +88,12 @@ public int runWithTemplate(String configurationYAML, EnvoyConfiguration envoyCon new JvmStringAccessorContext(entry.getValue())); } + for (Map.Entry entry : + envoyConfiguration.keyValueStores.entrySet()) { + JniLibrary.registerKeyValueStore(entry.getKey(), + new JvmKeyValueStoreContext(entry.getValue())); + } + return runWithResolvedYAML( envoyConfiguration.resolveTemplate(configurationYAML, JniLibrary.platformFilterTemplate(), JniLibrary.nativeFilterTemplate(), diff --git a/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java b/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java index cac0daae03..3c09b8cb8c 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java +++ b/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java @@ -305,6 +305,15 @@ protected static native int recordHistogramValue(long engine, String elements, b */ public static native String altProtocolCacheFilterInsert(); + /** + * Register a platform-provided key-value store implementation. + * + * @param name, unique name identifying this key-value store. + * @param context, context containing logic necessary to invoke the key-value store. + * @return int, the resulting status of the operation. + */ + protected static native int registerKeyValueStore(String name, JvmKeyValueStoreContext context); + /** * Register a string accessor to get strings from the platform. * diff --git a/library/java/io/envoyproxy/envoymobile/engine/JvmKeyValueStoreContext.java b/library/java/io/envoyproxy/envoymobile/engine/JvmKeyValueStoreContext.java new file mode 100644 index 0000000000..18fe9b0250 --- /dev/null +++ b/library/java/io/envoyproxy/envoymobile/engine/JvmKeyValueStoreContext.java @@ -0,0 +1,33 @@ +package io.envoyproxy.envoymobile.engine; + +import io.envoyproxy.envoymobile.engine.types.EnvoyKeyValueStore; +import java.nio.charset.StandardCharsets; + +/** + * JNI compatibility class to translate calls to EnvoyKeyValueStore implementations. + * + * Dealing with Java Strings directly in the JNI is awkward due to how Java encodes them. + */ +class JvmKeyValueStoreContext { + private static final byte[] EMPTY_BYTES = {}; + private final EnvoyKeyValueStore keyValueStore; + + public JvmKeyValueStoreContext(EnvoyKeyValueStore keyValueStore) { + this.keyValueStore = keyValueStore; + } + + public byte[] read(byte[] key) { + final String value = keyValueStore.read(new String(key, StandardCharsets.UTF_8)); + if (value == null) { + return EMPTY_BYTES; + } + return value.getBytes(StandardCharsets.UTF_8); + } + + public void remove(byte[] key) { keyValueStore.remove(new String(key, StandardCharsets.UTF_8)); } + + public void save(byte[] key, byte[] value) { + keyValueStore.save(new String(key, StandardCharsets.UTF_8), + new String(value, StandardCharsets.UTF_8)); + } +} diff --git a/library/java/io/envoyproxy/envoymobile/engine/types/BUILD b/library/java/io/envoyproxy/envoymobile/engine/types/BUILD index 6ada97b118..34ed3a103c 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/types/BUILD +++ b/library/java/io/envoyproxy/envoymobile/engine/types/BUILD @@ -11,6 +11,7 @@ java_library( "EnvoyHTTPFilter.java", "EnvoyHTTPFilterCallbacks.java", "EnvoyHTTPFilterFactory.java", + "EnvoyKeyValueStore.java", "EnvoyLogger.java", "EnvoyNetworkType.java", "EnvoyOnEngineRunning.java", diff --git a/library/java/io/envoyproxy/envoymobile/engine/types/EnvoyKeyValueStore.java b/library/java/io/envoyproxy/envoymobile/engine/types/EnvoyKeyValueStore.java new file mode 100644 index 0000000000..74db46d3d8 --- /dev/null +++ b/library/java/io/envoyproxy/envoymobile/engine/types/EnvoyKeyValueStore.java @@ -0,0 +1,26 @@ +package io.envoyproxy.envoymobile.engine.types; + +public interface EnvoyKeyValueStore { + /** + * Read a value from the key value store implementation. + * + * @param key, key identifying the value to be returned. + * @return String, value mapped to the key, or null if not present. + */ + String read(String key); + + /** + * Remove a value from the key value store implementation. + * + * @param key, key identifying the value to be removed. + */ + void remove(String key); + + /** + * Save a value to the key value store implementation. + * + * @param key, key identifying the value to be saved. + * @param value, the value to be saved. + */ + void save(String key, String value); +} diff --git a/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java b/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java index 0cbd6ef679..df39b291ad 100644 --- a/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java +++ b/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java @@ -16,6 +16,7 @@ import io.envoyproxy.envoymobile.engine.types.EnvoyLogger; import io.envoyproxy.envoymobile.engine.types.EnvoyOnEngineRunning; import io.envoyproxy.envoymobile.engine.types.EnvoyStringAccessor; +import io.envoyproxy.envoymobile.engine.types.EnvoyKeyValueStore; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -116,6 +117,7 @@ private EnvoyConfiguration createEnvoyConfiguration() { List platformFilterChain = Collections.emptyList(); List nativeFilterChain = new ArrayList<>(); Map stringAccessors = Collections.emptyMap(); + Map keyValueStores = Collections.emptyMap(); if (brotliEnabled()) { nativeFilterChain.add( new EnvoyNativeFilterConfig("envoy.filters.http.decompressor", BROTLI_CONFIG)); @@ -130,6 +132,6 @@ private EnvoyConfiguration createEnvoyConfiguration() { mH2ExtendKeepaliveTimeout, mH2RawDomains, mMaxConnectionsPerHost, mStatsFlushSeconds, mStreamIdleTimeoutSeconds, mPerTryIdleTimeoutSeconds, mAppVersion, mAppId, mTrustChainVerification, mVirtualClusters, nativeFilterChain, platformFilterChain, - stringAccessors); + stringAccessors, keyValueStores); } } diff --git a/library/kotlin/io/envoyproxy/envoymobile/BUILD b/library/kotlin/io/envoyproxy/envoymobile/BUILD index 1b762f0455..faf074edd6 100644 --- a/library/kotlin/io/envoyproxy/envoymobile/BUILD +++ b/library/kotlin/io/envoyproxy/envoymobile/BUILD @@ -43,6 +43,7 @@ envoy_mobile_kt_library( "EnvoyError.kt", "Headers.kt", "HeadersBuilder.kt", + "KeyValueStore.kt", "LogLevel.kt", "PulseClient.kt", "PulseClientImpl.kt", diff --git a/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt b/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt index 277c531d26..ce67373e8f 100644 --- a/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt +++ b/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt @@ -6,6 +6,7 @@ import io.envoyproxy.envoymobile.engine.EnvoyEngine import io.envoyproxy.envoymobile.engine.EnvoyEngineImpl import io.envoyproxy.envoymobile.engine.EnvoyNativeFilterConfig import io.envoyproxy.envoymobile.engine.types.EnvoyHTTPFilterFactory +import io.envoyproxy.envoymobile.engine.types.EnvoyKeyValueStore import io.envoyproxy.envoymobile.engine.types.EnvoyStringAccessor import java.util.UUID @@ -58,6 +59,7 @@ open class EngineBuilder( private var platformFilterChain = mutableListOf() private var nativeFilterChain = mutableListOf() private var stringAccessors = mutableMapOf() + private var keyValueStores = mutableMapOf() /** * Add a log level to use with Envoy. @@ -430,6 +432,7 @@ open class EngineBuilder( this.eventTracker = eventTracker return this } + /** * Add a string accessor to this Envoy Client. * @@ -443,6 +446,19 @@ open class EngineBuilder( return this } + /** + * Register a key-value store implementation for internal use. + * + * @param name the name of the KV store. + * @param keyValueStore the KV store implementation. + * + * @return this builder. + */ + fun addKeyValueStore(name: String, keyValueStore: KeyValueStore): EngineBuilder { + this.keyValueStores.put(name, EnvoyKeyValueStoreAdapter(keyValueStore)) + return this + } + /** * Add the App Version of the App using this Envoy Client. * @@ -541,7 +557,8 @@ open class EngineBuilder( virtualClusters, nativeFilterChain, platformFilterChain, - stringAccessors + stringAccessors, + keyValueStores ) return when (configuration) { diff --git a/library/kotlin/io/envoyproxy/envoymobile/KeyValueStore.kt b/library/kotlin/io/envoyproxy/envoymobile/KeyValueStore.kt new file mode 100644 index 0000000000..50bc19d991 --- /dev/null +++ b/library/kotlin/io/envoyproxy/envoymobile/KeyValueStore.kt @@ -0,0 +1,32 @@ +package io.envoyproxy.envoymobile + +import io.envoyproxy.envoymobile.engine.types.EnvoyKeyValueStore + +/** + * `KeyValueStore` is bridged through to `EnvoyKeyValueStore` to communicate with the engine. + */ +class KeyValueStore constructor ( + val read: ((key: String) -> String?), + val remove: ((key: String) -> Unit), + val save: ((key: String, value: String) -> Unit) +) + +/** + * Class responsible for bridging between the platform-level `KeyValueStore` and the + * engine's `EnvoyKeyValueStore`. + */ +internal class EnvoyKeyValueStoreAdapter( + private val callbacks: KeyValueStore +) : EnvoyKeyValueStore { + override fun read(key: String): String? { + return callbacks.read(key) + } + + override fun remove(key: String) { + callbacks.remove(key) + } + + override fun save(key: String, value: String) { + callbacks.save(key, value) + } +} diff --git a/library/proguard.txt b/library/proguard.txt index 0cee86089e..23b5f364b0 100644 --- a/library/proguard.txt +++ b/library/proguard.txt @@ -39,6 +39,10 @@ ; } +-keep, includedescriptorclasses class io.envoyproxy.envoymobile.engine.JvmKeyValueStoreContext { + ; +} + -keep, includedescriptorclasses class io.envoyproxy.envoymobile.engine.JvmStringAccessorContext { ; } diff --git a/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt b/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt index d7b729f401..879a1c0bd2 100644 --- a/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt +++ b/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt @@ -92,6 +92,7 @@ class EnvoyConfigurationTest { virtualClusters, listOf(EnvoyNativeFilterConfig("filter_name", "test_config")), emptyList(), + emptyMap(), emptyMap() ) } diff --git a/test/kotlin/integration/BUILD b/test/kotlin/integration/BUILD index 01a75e7a9f..a6ce4f629f 100644 --- a/test/kotlin/integration/BUILD +++ b/test/kotlin/integration/BUILD @@ -28,6 +28,20 @@ envoy_mobile_jni_kt_test( ], ) +envoy_mobile_jni_kt_test( + name = "key_value_store_test", + srcs = [ + "KeyValueStoreTest.kt", + ], + native_deps = [ + "//library/common/jni:libjava_jni_lib.so", + "//library/common/jni:java_jni_lib.jnilib", + ], + deps = [ + "//library/kotlin/io/envoyproxy/envoymobile:envoy_interfaces_lib", + ], +) + envoy_mobile_jni_kt_test( name = "set_event_tracker_test", srcs = [ diff --git a/test/kotlin/integration/KeyValueStoreTest.kt b/test/kotlin/integration/KeyValueStoreTest.kt new file mode 100644 index 0000000000..a4ffee9fbb --- /dev/null +++ b/test/kotlin/integration/KeyValueStoreTest.kt @@ -0,0 +1,103 @@ +package test.kotlin.integration + +import io.envoyproxy.envoymobile.Custom +import io.envoyproxy.envoymobile.EngineBuilder +import io.envoyproxy.envoymobile.KeyValueStore +import io.envoyproxy.envoymobile.RequestHeadersBuilder +import io.envoyproxy.envoymobile.RequestMethod +import io.envoyproxy.envoymobile.UpstreamHttpProtocol +import io.envoyproxy.envoymobile.engine.JniLibrary +import java.nio.ByteBuffer +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.fail +import org.junit.Test + +private const val apiListenerType = + "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.EnvoyMobileHttpConnectionManager" +private const val assertionFilterType = "type.googleapis.com/envoymobile.extensions.filters.http.assertion.Assertion" +private const val testKey = "foo" +private const val testValue = "bar" +private const val config = +""" +static_resources: + listeners: + - name: base_api_listener + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 10000 + api_listener: + api_listener: + "@type": $apiListenerType + config: + stat_prefix: hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + http_filters: + - name: envoy.filters.http.test_kv_store + typed_config: + "@type": type.googleapis.com/envoymobile.extensions.filters.http.test_kv_store.TestKeyValueStore + kv_store_name: envoy.key_value.platform_test + test_key: $testKey + test_value: $testValue + - name: envoy.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router +""" + +class KeyValueStoreTest { + + init { + JniLibrary.loadTestLibrary() + } + + @Test + fun `a registered KeyValueStore implementation handles calls from a TestKeyValueStore filter`() { + + val readExpectation = CountDownLatch(3) + val saveExpectation = CountDownLatch(1) + val testKeyValueStore = KeyValueStore( + read = { _ -> readExpectation.countDown(); null }, + remove = { _ -> {}}, + save = { _, _ -> saveExpectation.countDown() } + ) + + val engine = EngineBuilder(Custom(config)) + .addKeyValueStore("envoy.key_value.platform_test", testKeyValueStore) + .build() + val client = engine.streamClient() + + val requestHeaders = RequestHeadersBuilder( + method = RequestMethod.GET, + scheme = "https", + authority = "example.com", + path = "/test" + ) + .addUpstreamHttpProtocol(UpstreamHttpProtocol.HTTP2) + .build() + + client.newStreamPrototype() + .setOnError { _, _ -> fail("Unexpected error") } + .start() + .sendHeaders(requestHeaders, true) + + readExpectation.await(10, TimeUnit.SECONDS) + saveExpectation.await(10, TimeUnit.SECONDS) + engine.terminate() + + assertThat(readExpectation.count).isEqualTo(0) + assertThat(saveExpectation.count).isEqualTo(0) + } +}