diff --git a/source/extensions/filters/http/common/aws/BUILD b/source/extensions/filters/http/common/aws/BUILD index 0a0c543fb426c..50727d36a96ae 100644 --- a/source/extensions/filters/http/common/aws/BUILD +++ b/source/extensions/filters/http/common/aws/BUILD @@ -44,6 +44,7 @@ envoy_cc_library( name = "utility_lib", srcs = ["utility.cc"], hdrs = ["utility.h"], + external_deps = ["curl"], deps = [ "//source/common/common:utility_lib", "//source/common/http:headers_lib", diff --git a/source/extensions/filters/http/common/aws/utility.cc b/source/extensions/filters/http/common/aws/utility.cc index 88836f7b7721b..0410ef55df921 100644 --- a/source/extensions/filters/http/common/aws/utility.cc +++ b/source/extensions/filters/http/common/aws/utility.cc @@ -4,6 +4,7 @@ #include "common/common/utility.h" #include "absl/strings/str_join.h" +#include "curl/curl.h" namespace Envoy { namespace Extensions { @@ -87,6 +88,56 @@ Utility::joinCanonicalHeaderNames(const std::map& cano }); } +static size_t curlCallback(char* ptr, size_t, size_t nmemb, void* data) { + auto buf = static_cast(data); + buf->append(ptr, nmemb); + return nmemb; +} + +absl::optional Utility::metadataFetcher(const std::string& host, + const std::string& path, + const std::string& auth_token) { + static const size_t MAX_RETRIES = 4; + static const std::chrono::milliseconds RETRY_DELAY{1000}; + static const std::chrono::seconds TIMEOUT{5}; + + CURL* const curl = curl_easy_init(); + if (!curl) { + return absl::nullopt; + }; + + const std::string url = fmt::format("http://{}/{}", host, path); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, TIMEOUT.count()); + curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); + + std::string buffer; + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlCallback); + + struct curl_slist* headers = nullptr; + if (!auth_token.empty()) { + const std::string auth = fmt::format("Authorization: {}", auth_token); + headers = curl_slist_append(headers, auth.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + } + + for (size_t retry = 0; retry < MAX_RETRIES; retry++) { + const CURLcode res = curl_easy_perform(curl); + if (res == CURLE_OK) { + break; + } + ENVOY_LOG_MISC(debug, "Could not fetch AWS metadata: {}", curl_easy_strerror(res)); + buffer.clear(); + std::this_thread::sleep_for(RETRY_DELAY); + } + + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + + return buffer.empty() ? absl::nullopt : absl::optional(buffer); +} + } // namespace Aws } // namespace Common } // namespace HttpFilters diff --git a/source/extensions/filters/http/common/aws/utility.h b/source/extensions/filters/http/common/aws/utility.h index ead9339fb826b..c43236ceb2425 100644 --- a/source/extensions/filters/http/common/aws/utility.h +++ b/source/extensions/filters/http/common/aws/utility.h @@ -39,10 +39,26 @@ class Utility { */ static std::string joinCanonicalHeaderNames(const std::map& canonical_headers); + + /** + * Fetch AWS instance or task metadata. + * + * @param host host or ip address of the metadata endpoint. + * @param path path of the metadata document. + * @auth_token authentication token to pass in the request, empty string indicates no auth. + * @return Metadata document or nullopt in case if unable to fetch it. + * + * @note In case of an error, function will log ENVOY_LOG_MISC(debug) message. + * + * @note This is not main loop safe method as it is blocking. It is intended to be used from the + * gRPC auth plugins that are able to schedule blocking plugins on a different thread. + */ + static absl::optional + metadataFetcher(const std::string& host, const std::string& path, const std::string& auth_token); }; } // namespace Aws } // namespace Common } // namespace HttpFilters } // namespace Extensions -} // namespace Envoy \ No newline at end of file +} // namespace Envoy diff --git a/test/integration/BUILD b/test/integration/BUILD index ce5cb1e554589..6f1891a4066f2 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -795,3 +795,23 @@ envoy_cc_test( "//test/test_common:utility_lib", ], ) + +envoy_cc_test( + name = "aws_metadata_fetcher_integration_test", + srcs = [ + "aws_metadata_fetcher_integration_test.cc", + ], + tags = ["exclusive"], + deps = [ + ":integration_lib", + "//source/common/common:fmt_lib", + "//source/extensions/filters/http/common/aws:utility_lib", + "//source/extensions/filters/http/fault:config", + "//source/extensions/filters/http/fault:fault_filter_lib", + "//source/extensions/filters/http/router:config", + "//source/extensions/filters/network/echo:config", + "//source/extensions/filters/network/http_connection_manager:config", + "//test/server:utility_lib", + "//test/test_common:utility_lib", + ], +) diff --git a/test/integration/aws_metadata_fetcher_integration_test.cc b/test/integration/aws_metadata_fetcher_integration_test.cc new file mode 100644 index 0000000000000..eb08642bc51a3 --- /dev/null +++ b/test/integration/aws_metadata_fetcher_integration_test.cc @@ -0,0 +1,152 @@ +#include "common/common/fmt.h" + +#include "extensions/filters/http/common/aws/utility.h" + +#include "test/integration/integration.h" +#include "test/integration/utility.h" +#include "test/server/utility.h" +#include "test/test_common/utility.h" + +namespace Envoy { + +using Envoy::Extensions::HttpFilters::Common::Aws::Utility; + +class AwsMetadataIntegrationTestBase : public ::testing::Test, public BaseIntegrationTest { +public: + AwsMetadataIntegrationTestBase(int status_code, int delay_s) + : BaseIntegrationTest(Network::Address::IpVersion::v4, renderConfig(status_code, delay_s)) {} + + static std::string renderConfig(int status_code, int delay_s) { + return fmt::format(ConfigHelper::BASE_CONFIG + R"EOF( + filter_chains: + filters: + name: envoy.http_connection_manager + config: + stat_prefix: metadata_test + http_filters: + - name: envoy.fault + config: + delay: + fixed_delay: + seconds: {} + nanos: {} + percentage: + numerator: 100 + denominator: HUNDRED + - name: envoy.router + codec_type: HTTP1 + route_config: + virtual_hosts: + name: metadata_endpoint + routes: + - name: auth_route + direct_response: + status: {} + body: + inline_string: METADATA_VALUE_WITH_AUTH + match: + prefix: "/" + headers: + - name: Authorization + exact_match: AUTH_TOKEN + - name: no_auth_route + direct_response: + status: {} + body: + inline_string: METADATA_VALUE + match: + prefix: "/" + domains: "*" + name: route_config_0 + )EOF", + delay_s, delay_s > 0 ? 0 : 1000, status_code, status_code); + } + + void SetUp() override { BaseIntegrationTest::initialize(); } + + void TearDown() override { + test_server_.reset(); + fake_upstreams_.clear(); + } +}; + +class AwsMetadataIntegrationTestSuccess : public AwsMetadataIntegrationTestBase { +public: + AwsMetadataIntegrationTestSuccess() : AwsMetadataIntegrationTestBase(200, 0) {} +}; + +TEST_F(AwsMetadataIntegrationTestSuccess, Success) { + const auto endpoint = fmt::format("{}:{}", Network::Test::getLoopbackAddressUrlString(version_), + lookupPort("listener_0")); + const auto response = Utility::metadataFetcher(endpoint, "", ""); + + ASSERT_TRUE(response.has_value()); + EXPECT_EQ("METADATA_VALUE", *response); + + ASSERT_NE(nullptr, test_server_->counter("http.metadata_test.downstream_rq_completed")); + EXPECT_EQ(1, test_server_->counter("http.metadata_test.downstream_rq_completed")->value()); +} + +TEST_F(AwsMetadataIntegrationTestSuccess, AuthToken) { + const auto endpoint = fmt::format("{}:{}", Network::Test::getLoopbackAddressUrlString(version_), + lookupPort("listener_0")); + const auto response = Utility::metadataFetcher(endpoint, "", "AUTH_TOKEN"); + + ASSERT_TRUE(response.has_value()); + EXPECT_EQ("METADATA_VALUE_WITH_AUTH", *response); + + ASSERT_NE(nullptr, test_server_->counter("http.metadata_test.downstream_rq_completed")); + EXPECT_EQ(1, test_server_->counter("http.metadata_test.downstream_rq_completed")->value()); +} + +class AwsMetadataIntegrationTestFailure : public AwsMetadataIntegrationTestBase { +public: + AwsMetadataIntegrationTestFailure() : AwsMetadataIntegrationTestBase(503, 0) {} +}; + +TEST_F(AwsMetadataIntegrationTestFailure, Failure) { + const auto endpoint = fmt::format("{}:{}", Network::Test::getLoopbackAddressUrlString(version_), + lookupPort("listener_0")); + + const auto start_time = timeSystem().monotonicTime(); + const auto response = Utility::metadataFetcher(endpoint, "", ""); + const auto end_time = timeSystem().monotonicTime(); + + EXPECT_FALSE(response.has_value()); + + // Verify correct number of retries + ASSERT_NE(nullptr, test_server_->counter("http.metadata_test.downstream_rq_completed")); + EXPECT_EQ(4, test_server_->counter("http.metadata_test.downstream_rq_completed")->value()); + + // Verify correct sleep time between retries: 4 * 1000 = 4000 + EXPECT_LE(4000, + std::chrono::duration_cast(end_time - start_time).count()); +} + +class AwsMetadataIntegrationTestTimeout : public AwsMetadataIntegrationTestBase { +public: + AwsMetadataIntegrationTestTimeout() : AwsMetadataIntegrationTestBase(200, 10) {} +}; + +TEST_F(AwsMetadataIntegrationTestTimeout, Timeout) { + const auto endpoint = fmt::format("{}:{}", Network::Test::getLoopbackAddressUrlString(version_), + lookupPort("listener_0")); + + const auto start_time = timeSystem().monotonicTime(); + const auto response = Utility::metadataFetcher(endpoint, "", ""); + const auto end_time = timeSystem().monotonicTime(); + + EXPECT_FALSE(response.has_value()); + + // We do now check http.metadata_test.downstream_rq_completed value here because it's + // behavior is different between Linux and Mac when Curl disconnects on timeout. On Mac it is + // incremented, while on Linux it is not. + + // Verify correct sleep time between retries: 4 * 5000 = 20000 + EXPECT_LE(20000, + std::chrono::duration_cast(end_time - start_time).count()); + EXPECT_GT(40000, + std::chrono::duration_cast(end_time - start_time).count()); +} + +} // namespace Envoy diff --git a/tools/check_format.py b/tools/check_format.py index ec748006c4a0b..769c21add1d44 100755 --- a/tools/check_format.py +++ b/tools/check_format.py @@ -29,6 +29,7 @@ # definitions for real-world time, the construction of them in main(), and perf annotation. # For now it includes the validation server but that really should be injected too. REAL_TIME_WHITELIST = ("./source/common/common/utility.h", + "./source/extensions/filters/http/common/aws/utility.cc", "./source/common/event/real_time_system.cc", "./source/common/event/real_time_system.h", "./source/exe/main_common.cc", "./source/exe/main_common.h", "./source/server/config_validation/server.cc",