diff --git a/CODEOWNERS b/CODEOWNERS index d08360957e56..2c69a6f5adcb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -324,6 +324,8 @@ extensions/filters/http/oauth2 @derekargueta @mattklein123 /*/extensions/http/early_header_mutation/header_mutation @wbpcode @UNOWNED # Network matching extensions /*/extensions/matching/network/ @kyessenov @mattklein123 +# String matching extensions +/*/extensions/string_matcher/ @ggreenway @UNOWNED # Header mutation /*/extensions/filters/http/header_mutation @wbpcode @htuch @soulxu # Health checkers diff --git a/api/BUILD b/api/BUILD index 33de22a6811c..e0efeebce4f7 100644 --- a/api/BUILD +++ b/api/BUILD @@ -317,6 +317,7 @@ proto_library( "//envoy/extensions/stat_sinks/graphite_statsd/v3:pkg", "//envoy/extensions/stat_sinks/open_telemetry/v3:pkg", "//envoy/extensions/stat_sinks/wasm/v3:pkg", + "//envoy/extensions/string_matcher/lua/v3:pkg", "//envoy/extensions/tracers/opentelemetry/resource_detectors/v3:pkg", "//envoy/extensions/tracers/opentelemetry/samplers/v3:pkg", "//envoy/extensions/transport_sockets/alts/v3:pkg", diff --git a/api/envoy/extensions/string_matcher/lua/v3/BUILD b/api/envoy/extensions/string_matcher/lua/v3/BUILD new file mode 100644 index 000000000000..09a37ad16b83 --- /dev/null +++ b/api/envoy/extensions/string_matcher/lua/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/core/v3:pkg", + "@com_github_cncf_xds//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/string_matcher/lua/v3/lua.proto b/api/envoy/extensions/string_matcher/lua/v3/lua.proto new file mode 100644 index 000000000000..04c5b36f4543 --- /dev/null +++ b/api/envoy/extensions/string_matcher/lua/v3/lua.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package envoy.extensions.string_matcher.lua.v3; + +import "envoy/config/core/v3/base.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.string_matcher.lua.v3"; +option java_outer_classname = "LuaProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/string_matcher/lua/v3;luav3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Lua StringMatcher] +// A Lua StringMatcher allows executing a Lua script to determine if a string is a match. The configured source +// code must define a function named `envoy_match`. If the function returns true, the string is considered a match. +// Any other result, including an execution error, is considered a non-match. +// +// Example: +// +// .. code-block:: yaml +// +// source_code: +// inline_string: | +// function envoy_match(str) +// -- Do something. +// return true +// end +// +// [#extension: envoy.string_matcher.lua] + +message Lua { + // The Lua code that Envoy will execute + config.core.v3.DataSource source_code = 1 [(validate.rules).message = {required: true}]; +} diff --git a/api/envoy/type/matcher/v3/BUILD b/api/envoy/type/matcher/v3/BUILD index 320b988b1a53..bdb648a2dd2a 100644 --- a/api/envoy/type/matcher/v3/BUILD +++ b/api/envoy/type/matcher/v3/BUILD @@ -9,5 +9,6 @@ api_proto_package( "//envoy/annotations:pkg", "//envoy/type/v3:pkg", "@com_github_cncf_xds//udpa/annotations:pkg", + "@com_github_cncf_xds//xds/core/v3:pkg", ], ) diff --git a/api/envoy/type/matcher/v3/string.proto b/api/envoy/type/matcher/v3/string.proto index 2df1bd37a6a3..10033749acd3 100644 --- a/api/envoy/type/matcher/v3/string.proto +++ b/api/envoy/type/matcher/v3/string.proto @@ -4,6 +4,8 @@ package envoy.type.matcher.v3; import "envoy/type/matcher/v3/regex.proto"; +import "xds/core/v3/extension.proto"; + import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; import "validate/validate.proto"; @@ -17,7 +19,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: String matcher] // Specifies the way to match a string. -// [#next-free-field: 8] +// [#next-free-field: 9] message StringMatcher { option (udpa.annotations.versioning).previous_message_type = "envoy.type.matcher.StringMatcher"; @@ -61,6 +63,10 @@ message StringMatcher { // // * ``abc`` matches the value ``xyz.abc.def`` string contains = 7 [(validate.rules).string = {min_len: 1}]; + + // Use an extension as the matcher type. + // [#extension-category: envoy.string_matcher] + xds.core.v3.TypedExtensionConfig custom = 8; } // If true, indicates the exact/prefix/suffix/contains matching should be case insensitive. This diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 86061fda4ab7..eb4569353709 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -256,6 +256,7 @@ proto_library( "//envoy/extensions/stat_sinks/graphite_statsd/v3:pkg", "//envoy/extensions/stat_sinks/open_telemetry/v3:pkg", "//envoy/extensions/stat_sinks/wasm/v3:pkg", + "//envoy/extensions/string_matcher/lua/v3:pkg", "//envoy/extensions/tracers/opentelemetry/resource_detectors/v3:pkg", "//envoy/extensions/tracers/opentelemetry/samplers/v3:pkg", "//envoy/extensions/transport_sockets/alts/v3:pkg", diff --git a/bazel/repository_locations.bzl b/bazel/repository_locations.bzl index 6e6043e8452b..0f17fac8af96 100644 --- a/bazel/repository_locations.bzl +++ b/bazel/repository_locations.bzl @@ -480,6 +480,7 @@ REPOSITORY_LOCATIONS_SPEC = dict( extensions = [ "envoy.filters.http.lua", "envoy.router.cluster_specifier_plugin.lua", + "envoy.string_matcher.lua", ], cpe = "cpe:2.3:a:luajit:luajit:*", license = "MIT", diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 33a8bd26ac12..6b0e2d97e781 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -259,6 +259,10 @@ new_features: change: | Added load shed point ``envoy.load_shed_points.hcm_ondata_creating_codec`` that closes connections before creating codec if Envoy is under pressure, typically memory. +- area: string matcher + change: | + Added an :ref:`extension point for custom string matcher implementations `. + An implementation for :ref:`running a Lua script ` is included. - area: overload change: | added a :ref:`configuration option diff --git a/docs/root/api-v3/config/config.rst b/docs/root/api-v3/config/config.rst index 12d216da67c8..5c81eb5ff5d9 100644 --- a/docs/root/api-v3/config/config.rst +++ b/docs/root/api-v3/config/config.rst @@ -40,6 +40,7 @@ Extensions resource_monitor/resource_monitor retry/retry stat_sinks/stat_sinks + string_matcher/string_matcher transport_socket/transport_socket upstream/upstream wasm/wasm diff --git a/docs/root/api-v3/config/string_matcher/string_matcher.rst b/docs/root/api-v3/config/string_matcher/string_matcher.rst new file mode 100644 index 000000000000..ee1fe552ec7d --- /dev/null +++ b/docs/root/api-v3/config/string_matcher/string_matcher.rst @@ -0,0 +1,8 @@ +String Matcher +============== + +.. toctree:: + :glob: + :maxdepth: 2 + + ../../extensions/string_matcher/*/v3/* diff --git a/docs/root/configuration/other_features/other_features.rst b/docs/root/configuration/other_features/other_features.rst index b5ba3fd18aeb..bd54f4e7a129 100644 --- a/docs/root/configuration/other_features/other_features.rst +++ b/docs/root/configuration/other_features/other_features.rst @@ -12,3 +12,4 @@ Other features wasm wasm_service qatzip + string_matcher diff --git a/docs/root/configuration/other_features/string_matcher.rst b/docs/root/configuration/other_features/string_matcher.rst new file mode 100644 index 000000000000..69cf5afd6021 --- /dev/null +++ b/docs/root/configuration/other_features/string_matcher.rst @@ -0,0 +1,10 @@ +String Matcher +============== + +:ref:`StringMatcher ` can be configured +for one of the commonly used modes, or :ref:`extended for custom usecases +`. + +Envoy includes an :ref:`extension for running a Lua script +` to determine +whether a string matches. diff --git a/source/common/common/matchers.cc b/source/common/common/matchers.cc index ac623b85ad8d..d69fe3c72822 100644 --- a/source/common/common/matchers.cc +++ b/source/common/common/matchers.cc @@ -9,6 +9,7 @@ #include "source/common/common/macros.h" #include "source/common/common/regex.h" #include "source/common/config/metadata.h" +#include "source/common/config/utility.h" #include "source/common/http/path_utility.h" #include "absl/strings/match.h" @@ -200,5 +201,10 @@ bool PathMatcher::match(const absl::string_view path) const { return matcher_.match(Http::PathUtil::removeQueryAndFragment(path)); } +StringMatcherPtr getExtensionStringMatcher(const ::xds::core::v3::TypedExtensionConfig& config) { + auto factory = Config::Utility::getAndCheckFactory(config, false); + return factory->createStringMatcher(config.typed_config()); +} + } // namespace Matchers } // namespace Envoy diff --git a/source/common/common/matchers.h b/source/common/common/matchers.h index e96f3689322f..ae66633b6660 100644 --- a/source/common/common/matchers.h +++ b/source/common/common/matchers.h @@ -86,6 +86,8 @@ class UniversalStringMatcher : public StringMatcher { bool match(absl::string_view) const override { return true; } }; +StringMatcherPtr getExtensionStringMatcher(const ::xds::core::v3::TypedExtensionConfig& config); + template class StringMatcherImpl : public ValueMatcher, public StringMatcher { public: @@ -100,32 +102,14 @@ class StringMatcherImpl : public ValueMatcher, public StringMatcher { // Cache the lowercase conversion of the Contains matcher for future use lowercase_contains_match_ = absl::AsciiStrToLower(matcher_.contains()); } + } else { + initialize(matcher); } } // StringMatcher - bool match(const absl::string_view value) const override { - switch (matcher_.match_pattern_case()) { - case StringMatcherType::MatchPatternCase::kExact: - return matcher_.ignore_case() ? absl::EqualsIgnoreCase(value, matcher_.exact()) - : value == matcher_.exact(); - case StringMatcherType::MatchPatternCase::kPrefix: - return matcher_.ignore_case() ? absl::StartsWithIgnoreCase(value, matcher_.prefix()) - : absl::StartsWith(value, matcher_.prefix()); - case StringMatcherType::MatchPatternCase::kSuffix: - return matcher_.ignore_case() ? absl::EndsWithIgnoreCase(value, matcher_.suffix()) - : absl::EndsWith(value, matcher_.suffix()); - case StringMatcherType::MatchPatternCase::kContains: - return matcher_.ignore_case() - ? absl::StrContains(absl::AsciiStrToLower(value), lowercase_contains_match_) - : absl::StrContains(value, matcher_.contains()); - case StringMatcherType::MatchPatternCase::kSafeRegex: - return regex_->match(value); - case StringMatcherType::MatchPatternCase::MATCH_PATTERN_NOT_SET: - break; - } - PANIC("unexpected"); - } + bool match(const absl::string_view value) const override { return match(value, matcher_); } + bool match(const ProtobufWkt::Value& value) const override { if (value.kind_case() != ProtobufWkt::Value::kStringValue) { @@ -155,9 +139,65 @@ class StringMatcherImpl : public ValueMatcher, public StringMatcher { } private: + // Type `xds::type::matcher::v3::StringMatcher` doesn't have an extension type, so use function + // overloading to only handle that case for type `envoy::type::matcher::v3::StringMatcher` to + // prevent compilation errors on use of `kCustom`. + + void initialize(const xds::type::matcher::v3::StringMatcher&) {} + + void initialize(const envoy::type::matcher::v3::StringMatcher& matcher) { + if (matcher.has_custom()) { + custom_ = getExtensionStringMatcher(matcher.custom()); + } + } + + bool match(const absl::string_view value, const xds::type::matcher::v3::StringMatcher&) const { + return matchCommon(value); + } + + bool match(const absl::string_view value, + const envoy::type::matcher::v3::StringMatcher& matcher) const { + if (matcher.match_pattern_case() == + envoy::type::matcher::v3::StringMatcher::MatchPatternCase::kCustom) { + return custom_->match(value); + } + return matchCommon(value); + } + + // StringMatcher + bool matchCommon(const absl::string_view value) const { + switch (matcher_.match_pattern_case()) { + case StringMatcherType::MatchPatternCase::kExact: + return matcher_.ignore_case() ? absl::EqualsIgnoreCase(value, matcher_.exact()) + : value == matcher_.exact(); + case StringMatcherType::MatchPatternCase::kPrefix: + return matcher_.ignore_case() ? absl::StartsWithIgnoreCase(value, matcher_.prefix()) + : absl::StartsWith(value, matcher_.prefix()); + case StringMatcherType::MatchPatternCase::kSuffix: + return matcher_.ignore_case() ? absl::EndsWithIgnoreCase(value, matcher_.suffix()) + : absl::EndsWith(value, matcher_.suffix()); + case StringMatcherType::MatchPatternCase::kContains: + return matcher_.ignore_case() + ? absl::StrContains(absl::AsciiStrToLower(value), lowercase_contains_match_) + : absl::StrContains(value, matcher_.contains()); + case StringMatcherType::MatchPatternCase::kSafeRegex: + return regex_->match(value); + default: + PANIC("unexpected"); + } + } + const StringMatcherType matcher_; Regex::CompiledMatcherPtr regex_; std::string lowercase_contains_match_; + StringMatcherPtr custom_; +}; + +class StringMatcherExtensionFactory : public Config::TypedFactory { +public: + virtual StringMatcherPtr createStringMatcher(const ProtobufWkt::Any& config) PURE; + + std::string category() const override { return "envoy.string_matcher"; } }; class ListMatcher : public ValueMatcher { diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 4b7ed2228f47..a3fa5c9831e2 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -115,6 +115,11 @@ EXTENSIONS = { "envoy.matching.actions.format_string": "//source/extensions/matching/actions/format_string:config", + # + # StringMatchers + # + "envoy.string_matcher.lua": "//source/extensions/string_matcher/lua:config", + # # HTTP filters # diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 2822b8e21329..771ae0f999c3 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -1140,6 +1140,13 @@ envoy.stat_sinks.wasm: status: alpha type_urls: - envoy.extensions.stat_sinks.wasm.v3.Wasm +envoy.string_matcher.lua: + categories: + - envoy.string_matcher + security_posture: robust_to_untrusted_downstream_and_upstream + status: alpha + type_urls: + - envoy.extensions.string_matcher.lua.v3.Lua envoy.tls.cert_validator.spiffe: categories: - envoy.tls.cert_validator diff --git a/source/extensions/string_matcher/lua/BUILD b/source/extensions/string_matcher/lua/BUILD new file mode 100644 index 000000000000..550c67b8b1b4 --- /dev/null +++ b/source/extensions/string_matcher/lua/BUILD @@ -0,0 +1,21 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_extension( + name = "config", + srcs = ["match.cc"], + hdrs = ["match.h"], + deps = [ + "//source/common/common:matchers_lib", + "//source/common/config:datasource_lib", + "//source/extensions/filters/common/lua:lua_lib", + "@envoy_api//envoy/extensions/string_matcher/lua/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/string_matcher/lua/match.cc b/source/extensions/string_matcher/lua/match.cc new file mode 100644 index 000000000000..fd24f17f81f0 --- /dev/null +++ b/source/extensions/string_matcher/lua/match.cc @@ -0,0 +1,121 @@ +#include "source/extensions/string_matcher/lua/match.h" + +#include "envoy/extensions/string_matcher/lua/v3/lua.pb.h" + +#include "source/common/config/datasource.h" +#include "source/common/config/utility.h" +#include "source/common/protobuf/message_validator_impl.h" + +namespace Envoy { +namespace Extensions { +namespace StringMatcher { +namespace Lua { + +LuaStringMatcher::LuaStringMatcher(const std::string& code) : state_(luaL_newstate()) { + RELEASE_ASSERT(state_.get() != nullptr, "unable to create new Lua state object"); + luaL_openlibs(state_.get()); + int rc = luaL_dostring(state_.get(), code.c_str()); + if (rc != 0) { + absl::string_view error("unknown"); + if (lua_isstring(state_.get(), -1)) { + size_t len = 0; + const char* err = lua_tolstring(state_.get(), -1, &len); + error = absl::string_view(err, len); + } + throw EnvoyException(absl::StrCat("Failed to load lua code in Lua StringMatcher:", error)); + } + + lua_getglobal(state_.get(), "envoy_match"); + bool is_function = lua_isfunction(state_.get(), -1); + if (!is_function) { + throw EnvoyException("Lua code did not contain a global function named 'envoy_match'"); + } + matcher_func_ref_ = luaL_ref(state_.get(), LUA_REGISTRYINDEX); +} + +bool LuaStringMatcher::match(const absl::string_view value) const { + const int initial_depth = lua_gettop(state_.get()); + + bool ret = [&]() { + lua_rawgeti(state_.get(), LUA_REGISTRYINDEX, matcher_func_ref_); + ASSERT(lua_isfunction(state_.get(), -1)); // Validated in constructor + + lua_pushlstring(state_.get(), value.data(), value.size()); + int rc = lua_pcall(state_.get(), 1, 1, 0); + if (rc != 0) { + // Runtime error + absl::string_view error("unknown"); + if (lua_isstring(state_.get(), -1)) { + size_t len = 0; + const char* err = lua_tolstring(state_.get(), -1, &len); + error = absl::string_view(err, len); + } + ENVOY_LOG_PERIODIC_MISC(error, std::chrono::seconds(5), + "Lua StringMatcher error running script: {}", error); + lua_pop(state_.get(), 1); + + return false; + } + + bool ret = false; + if (lua_isboolean(state_.get(), -1)) { + ret = lua_toboolean(state_.get(), -1) != 0; + } else { + ENVOY_LOG_PERIODIC_MISC(error, std::chrono::seconds(5), + "Lua StringMatcher match function did not return a boolean"); + } + + lua_pop(state_.get(), 1); + return ret; + }(); + + // Validate that the stack is restored to it's original state; nothing added or removed. + ASSERT(lua_gettop(state_.get()) == initial_depth); + return ret; +} + +// Lua state is not thread safe, so a state needs to be stored in thread local storage. +class LuaStringMatcherThreadWrapper : public Matchers::StringMatcher { +public: + LuaStringMatcherThreadWrapper(const std::string& code) { + // Validate that there are no errors while creating on the main thread. + LuaStringMatcher validator(code); + + tls_slot_ = ThreadLocal::TypedSlot::makeUnique( + *InjectableSingleton::getExisting()); + tls_slot_->set([code](Event::Dispatcher&) -> std::shared_ptr { + return std::make_shared(code); + }); + } + + bool match(const absl::string_view value) const override { return (*tls_slot_)->match(value); } + +private: + ThreadLocal::TypedSlotPtr tls_slot_; +}; + +Matchers::StringMatcherPtr +LuaStringMatcherFactory::createStringMatcher(const ProtobufWkt::Any& message) { + ::envoy::extensions::string_matcher::lua::v3::Lua config; + Config::Utility::translateOpaqueConfig(message, ProtobufMessage::getStrictValidationVisitor(), + config); + Api::Api* api = InjectableSingleton::getExisting(); + absl::StatusOr result = Config::DataSource::read( + config.source_code(), false /* allow_empty */, *api, 0 /* max_size */); + if (!result.ok()) { + throw EnvoyException( + fmt::format("Failed to get lua string matcher code from source: {}", result.status())); + } + return std::make_unique(*result); +} + +ProtobufTypes::MessagePtr LuaStringMatcherFactory::createEmptyConfigProto() { + return std::make_unique<::envoy::extensions::string_matcher::lua::v3::Lua>(); +} + +REGISTER_FACTORY(LuaStringMatcherFactory, Matchers::StringMatcherExtensionFactory); + +} // namespace Lua +} // namespace StringMatcher +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/string_matcher/lua/match.h b/source/extensions/string_matcher/lua/match.h new file mode 100644 index 000000000000..8452a24b0832 --- /dev/null +++ b/source/extensions/string_matcher/lua/match.h @@ -0,0 +1,40 @@ +#pragma once + +#include "envoy/thread_local/thread_local.h" + +#include "source/common/common/matchers.h" +#include "source/extensions/filters/common/lua/lua.h" + +namespace Envoy { +namespace Extensions { +namespace StringMatcher { +namespace Lua { + +// This class should not be used directly. It is exposed here for use in tests. +// Correct use requires use of a thread local slot. +class LuaStringMatcher : public Matchers::StringMatcher, public ThreadLocal::ThreadLocalObject { +public: + LuaStringMatcher(const std::string& code); + + // ThreadLocal::ThreadLocalObject + ~LuaStringMatcher() override = default; + + // Matchers::StringMatcher + bool match(const absl::string_view value) const override; + +private: + CSmartPtr state_; + int matcher_func_ref_{LUA_NOREF}; +}; + +class LuaStringMatcherFactory : public Matchers::StringMatcherExtensionFactory { +public: + Matchers::StringMatcherPtr createStringMatcher(const ProtobufWkt::Any& message) override; + std::string name() const override { return "envoy.string_matcher.lua"; } + ProtobufTypes::MessagePtr createEmptyConfigProto() override; +}; + +} // namespace Lua +} // namespace StringMatcher +} // namespace Extensions +} // namespace Envoy diff --git a/source/server/server.cc b/source/server/server.cc index 376ebb48e829..5573fd3727a9 100644 --- a/source/server/server.cc +++ b/source/server/server.cc @@ -106,7 +106,20 @@ InstanceBase::InstanceBase(Init::Manager& init_manager, const Options& options, grpc_context_(store.symbolTable()), http_context_(store.symbolTable()), router_context_(store.symbolTable()), process_context_(std::move(process_context)), hooks_(hooks), quic_stat_names_(store.symbolTable()), server_contexts_(*this), - enable_reuse_port_default_(true), stats_flush_in_progress_(false) {} + enable_reuse_port_default_(true), stats_flush_in_progress_(false) { + + // These are needed for string matcher extensions. It is too painful to pass these objects through + // all call chains that construct a `StringMatcherImpl`, so these are singletons. + // + // They must be cleared before being set to make the multi-envoy integration test pass. Note that + // this means that extensions relying on these singletons probably will not function correctly in + // some Envoy mobile setups where multiple Envoy engines are used in the same process. The same + // caveat also applies to a few other singletons, such as the global regex engine. + InjectableSingleton::clear(); + InjectableSingleton::initialize(&thread_local_); + InjectableSingleton::clear(); + InjectableSingleton::initialize(api_.get()); +} InstanceBase::~InstanceBase() { terminate(); @@ -138,6 +151,9 @@ InstanceBase::~InstanceBase() { close(tracing_fd_); } #endif + + InjectableSingleton::clear(); + InjectableSingleton::clear(); } Upstream::ClusterManager& InstanceBase::clusterManager() { diff --git a/test/extensions/string_matcher/lua/BUILD b/test/extensions/string_matcher/lua/BUILD new file mode 100644 index 000000000000..b8688ea5de38 --- /dev/null +++ b/test/extensions/string_matcher/lua/BUILD @@ -0,0 +1,37 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "lua_test", + srcs = ["lua_test.cc"], + extension_names = ["envoy.string_matcher.lua"], + deps = [ + "//source/extensions/string_matcher/lua:config", + "//test/mocks/api:api_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/test_common:logging_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/string_matcher/lua/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "lua_integration_test", + srcs = ["lua_integration_test.cc"], + extension_names = ["envoy.string_matcher.lua"], + deps = [ + "//source/extensions/string_matcher/lua:config", + "//test/integration:http_integration_lib", + "@envoy_api//envoy/extensions/string_matcher/lua/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/string_matcher/lua/lua_integration_test.cc b/test/extensions/string_matcher/lua/lua_integration_test.cc new file mode 100644 index 000000000000..eec35e4fd304 --- /dev/null +++ b/test/extensions/string_matcher/lua/lua_integration_test.cc @@ -0,0 +1,85 @@ +#include "envoy/extensions/string_matcher/lua/v3/lua.pb.h" + +#include "source/common/http/utility.h" +#include "source/common/protobuf/protobuf.h" + +#include "test/integration/http_integration.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Matching { +namespace String { +namespace Lua { + +class LuaIntegrationTest : public testing::TestWithParam, + public HttpIntegrationTest { +public: + LuaIntegrationTest() : HttpIntegrationTest(Http::CodecType::HTTP1, GetParam()) { + autonomous_upstream_ = true; + config_helper_.addConfigModifier( + [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) { + ::envoy::extensions::string_matcher::lua::v3::Lua config; + config.mutable_source_code()->set_inline_string( + R"( + function envoy_match(str) + return str == "good" or str == "acceptable" + end + )"); + + auto* header_match = hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_match() + ->add_headers(); + + header_match->set_name("lua-header"); + + auto* string_match_extension = header_match->mutable_string_match()->mutable_custom(); + string_match_extension->set_name("unused but must be set"); + string_match_extension->mutable_typed_config()->PackFrom(config); + }); + HttpIntegrationTest::initialize(); + } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, LuaIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(LuaIntegrationTest, HeaderMatcher) { + Http::TestRequestHeaderMapImpl matching_request_headers{ + {":method", "GET"}, {":path", "/"}, + {":scheme", "http"}, {":authority", "example.com"}, + {"lua-header", "acceptable"}, + }; + codec_client_ = makeHttpConnection(lookupPort("http")); + { + auto response = codec_client_->makeHeaderOnlyRequest(matching_request_headers); + ASSERT_TRUE(response->waitForEndStream()); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + } + + Http::TestRequestHeaderMapImpl non_matching_request_headers{ + {":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {"lua-header", "unacceptable"}, + }; + { + auto response = codec_client_->makeHeaderOnlyRequest(non_matching_request_headers); + ASSERT_TRUE(response->waitForEndStream()); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("404", response->headers().Status()->value().getStringView()); + } +} + +} // namespace Lua +} // namespace String +} // namespace Matching +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/string_matcher/lua/lua_test.cc b/test/extensions/string_matcher/lua/lua_test.cc new file mode 100644 index 000000000000..4e834e0d6d71 --- /dev/null +++ b/test/extensions/string_matcher/lua/lua_test.cc @@ -0,0 +1,113 @@ +#include "envoy/extensions/string_matcher/lua/v3/lua.pb.h" + +#include "source/extensions/string_matcher/lua/match.h" + +#include "test/mocks/api/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/test_common/logging.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace StringMatcher { +namespace Lua { + +namespace { +bool test(const std::string& code, const std::string& str) { + LuaStringMatcher matcher(code); + return matcher.match(str); +} + +const std::string program = R"( + -- Test that these locals are properly captured in the state. + local good_val = "match" + local bad_val = "nomatch" + local error_val = "error" + + function envoy_match(str) + if str == good_val then + return true + elseif str == bad_val then + return false + elseif str == error_val then + error("intentional error") + end + end + + -- Test that no error is raised for this un-called code. + function not_called(blah) + panic("foo") + end + )"; + +const std::string no_match_function_program = R"( + function wrong() + return false + end + )"; + +const std::string invalid_lua_program = R"( + if + )"; +} // namespace + +TEST(LuaStringMatcher, LuaBehavior) { + EXPECT_THROW_WITH_MESSAGE(test(no_match_function_program, ""), EnvoyException, + "Lua code did not contain a global function named 'envoy_match'"); + + EXPECT_THROW_WITH_REGEX( + test(invalid_lua_program, ""), EnvoyException, + "Failed to load lua code in Lua StringMatcher:.*unexpected symbol near ''"); + + EXPECT_TRUE(test(program, "match")); + + EXPECT_LOG_NOT_CONTAINS("error", "Lua StringMatcher", + { EXPECT_FALSE(test(program, "nomatch")); }); + + EXPECT_LOG_CONTAINS("error", "function did not return a boolean", + { EXPECT_FALSE(test(program, "unknown")); }); + + EXPECT_LOG_CONTAINS( + "error", "Lua StringMatcher error running script: [string \"...\"]:13: intentional error", + { EXPECT_FALSE(test(program, "error")); }); +} + +// Ensure that the code runs in a context that the standard library is loaded into. +TEST(LuaStringMatcher, LuaStdLib) { + const std::string code = R"( + function envoy_match(str) + -- Requires the string library to be present. + return string.find(str, "text") ~= nil + end + )"; + + EXPECT_TRUE(test(code, "contains text!")); + EXPECT_FALSE(test(code, "nope")); +} + +TEST(LuaStringMatcher, NoCode) { + ScopedInjectableLoader api_inject(std::make_unique()); + ScopedInjectableLoader tls_inject( + std::make_unique()); + + LuaStringMatcherFactory factory; + ::envoy::extensions::string_matcher::lua::v3::Lua empty_config; + ProtobufWkt::Any any; + any.PackFrom(empty_config); + EXPECT_THROW_WITH_MESSAGE(factory.createStringMatcher(any), EnvoyException, + "Failed to get lua string matcher code from source: INVALID_ARGUMENT: " + "Unexpected DataSource::specifier_case(): 0"); + + empty_config.mutable_source_code()->set_inline_string(""); + any.PackFrom(empty_config); + EXPECT_THROW_WITH_MESSAGE(factory.createStringMatcher(any), EnvoyException, + "Failed to get lua string matcher code from source: INVALID_ARGUMENT: " + "DataSource cannot be empty"); +} + +} // namespace Lua +} // namespace StringMatcher +} // namespace Extensions +} // namespace Envoy diff --git a/test/tools/router_check/router.cc b/test/tools/router_check/router.cc index b2ad4182ed79..c14106bf7c08 100644 --- a/test/tools/router_check/router.cc +++ b/test/tools/router_check/router.cc @@ -36,6 +36,8 @@ const std::string toString(envoy::type::matcher::v3::StringMatcher::MatchPattern return "safe_regex"; case envoy::type::matcher::v3::StringMatcher::MatchPatternCase::kContains: return "contains"; + case envoy::type::matcher::v3::StringMatcher::MatchPatternCase::kCustom: + return "custom"; case envoy::type::matcher::v3::StringMatcher::MatchPatternCase::MATCH_PATTERN_NOT_SET: return "match_pattern_not_set"; } diff --git a/tools/extensions/extensions_schema.yaml b/tools/extensions/extensions_schema.yaml index 19ee79c513b4..81755662fd64 100644 --- a/tools/extensions/extensions_schema.yaml +++ b/tools/extensions/extensions_schema.yaml @@ -127,6 +127,7 @@ categories: - envoy.matching.http.custom_matchers - envoy.matching.network.input - envoy.matching.network.custom_matchers +- envoy.string_matcher - envoy.filters.http.upstream - envoy.path.match - envoy.path.rewrite