diff --git a/generator/integration_tests/tests/golden_thing_admin_rest_metadata_decorator_test.cc b/generator/integration_tests/tests/golden_thing_admin_rest_metadata_decorator_test.cc index b666614589891..514ed4e722197 100644 --- a/generator/integration_tests/tests/golden_thing_admin_rest_metadata_decorator_test.cc +++ b/generator/integration_tests/tests/golden_thing_admin_rest_metadata_decorator_test.cc @@ -312,7 +312,7 @@ TEST(ThingAdminRestMetadataDecoratorTest, DropDatabaseExplicitRoutingMatch) { EXPECT_THAT(context.GetHeader("x-goog-quota-user"), IsEmpty()); EXPECT_THAT(context.GetHeader("x-server-timeout"), IsEmpty()); EXPECT_THAT( - context.GetHeader("x-goog-request-params")[0], + context.GetHeader("x-goog-request-params").values().front(), AllOf( HasSubstr(std::string("project=projects%2Fmy_project")), HasSubstr(std::string("instance=instances%2Fmy_instance")), diff --git a/google/cloud/BUILD.bazel b/google/cloud/BUILD.bazel index 68bd685293f5f..dd4feee33dcf0 100644 --- a/google/cloud/BUILD.bazel +++ b/google/cloud/BUILD.bazel @@ -256,6 +256,7 @@ cc_library( visibility = ["//:__subpackages__"], deps = [ ":google_cloud_cpp_common", + "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/functional:function_ref", "@abseil-cpp//absl/types:span", "@curl", diff --git a/google/cloud/bigquery/v2/minimal/internal/log_wrapper.h b/google/cloud/bigquery/v2/minimal/internal/log_wrapper.h index 2337435bbf05a..f689cb0aef151 100644 --- a/google/cloud/bigquery/v2/minimal/internal/log_wrapper.h +++ b/google/cloud/bigquery/v2/minimal/internal/log_wrapper.h @@ -38,10 +38,11 @@ Result LogWrapper(Functor&& functor, rest_internal::RestContext& context, TracingOptions const& options) { auto formatter = [options](std::string* out, auto const& header) { auto const* delim = options.single_line_mode() ? "&" : "\n"; - absl::StrAppend( - out, " { name: \"", header.first, "\" value: \"", - internal::DebugString(absl::StrJoin(header.second, delim), options), - "\" }"); + absl::StrAppend(out, " { name: \"", std::string_view{header.first}, + "\" value: \"", + internal::DebugString( + absl::StrJoin(header.second.values(), delim), options), + "\" }"); }; GCP_LOG(DEBUG) << where << "() << " << request.DebugString(request_name, options) << ", Context {" diff --git a/google/cloud/bigquery/v2/minimal/internal/rest_stub_utils.h b/google/cloud/bigquery/v2/minimal/internal/rest_stub_utils.h index ca0762f1a05b1..34dd83a79a44f 100644 --- a/google/cloud/bigquery/v2/minimal/internal/rest_stub_utils.h +++ b/google/cloud/bigquery/v2/minimal/internal/rest_stub_utils.h @@ -48,7 +48,7 @@ StatusOr PrepareRestRequest( if (!rest_context.headers().empty()) { for (auto const& h : rest_context.headers()) { if (!h.second.empty()) { - rest_request->AddHeader(h.first, absl::StrJoin(h.second, "&")); + rest_request->AddHeader(h.first, absl::StrJoin(h.second.values(), "&")); } } } diff --git a/google/cloud/bigquery/v2/minimal/testing/metadata_test_utils.cc b/google/cloud/bigquery/v2/minimal/testing/metadata_test_utils.cc index 9116da715a518..4efa4545f6f36 100644 --- a/google/cloud/bigquery/v2/minimal/testing/metadata_test_utils.cc +++ b/google/cloud/bigquery/v2/minimal/testing/metadata_test_utils.cc @@ -30,13 +30,15 @@ static auto const kUserProject = "test-only-project"; static auto const kQuotaUser = "test-quota-user"; void VerifyMetadataContext(rest_internal::RestContext& context) { - EXPECT_THAT(context.GetHeader("x-goog-api-client"), + EXPECT_THAT(context.GetHeader("x-goog-api-client").values(), ElementsAre(internal::HandCraftedLibClientHeader())); - EXPECT_THAT(context.GetHeader("x-goog-request-params"), IsEmpty()); - EXPECT_THAT(context.GetHeader("x-goog-user-project"), + EXPECT_THAT(context.GetHeader("x-goog-request-params").values(), IsEmpty()); + EXPECT_THAT(context.GetHeader("x-goog-user-project").values(), ElementsAre(kUserProject)); - EXPECT_THAT(context.GetHeader("x-goog-quota-user"), ElementsAre(kQuotaUser)); - EXPECT_THAT(context.GetHeader("x-server-timeout"), ElementsAre("3.141")); + EXPECT_THAT(context.GetHeader("x-goog-quota-user").values(), + ElementsAre(kQuotaUser)); + EXPECT_THAT(context.GetHeader("x-server-timeout").values(), + ElementsAre("3.141")); } Options GetMetadataOptions() { diff --git a/google/cloud/google_cloud_cpp_rest_internal.bzl b/google/cloud/google_cloud_cpp_rest_internal.bzl index 1506bb22a18cd..50cb7a0d042a4 100644 --- a/google/cloud/google_cloud_cpp_rest_internal.bzl +++ b/google/cloud/google_cloud_cpp_rest_internal.bzl @@ -17,6 +17,7 @@ """Automatically generated source lists for google_cloud_cpp_rest_internal - DO NOT EDIT.""" google_cloud_cpp_rest_internal_hdrs = [ + "internal/async_rest_retry_loop.h", "internal/binary_data_as_debug_string.h", "internal/curl_handle.h", "internal/curl_handle_factory.h", @@ -54,6 +55,7 @@ google_cloud_cpp_rest_internal_hdrs = [ "internal/oauth2_logging_credentials.h", "internal/oauth2_minimal_iam_credentials_rest.h", "internal/oauth2_refreshing_credentials_wrapper.h", + "internal/oauth2_regional_access_boundary_token_manager.h", "internal/oauth2_service_account_credentials.h", "internal/oauth2_universe_domain.h", "internal/parse_service_account_p12_file.h", @@ -65,6 +67,8 @@ google_cloud_cpp_rest_internal_hdrs = [ "internal/rest_opentelemetry.h", "internal/rest_options.h", "internal/rest_parse_json_error.h", + "internal/rest_pure_background_threads_impl.h", + "internal/rest_pure_completion_queue_impl.h", "internal/rest_request.h", "internal/rest_response.h", "internal/rest_retry_loop.h", @@ -111,6 +115,7 @@ google_cloud_cpp_rest_internal_srcs = [ "internal/oauth2_logging_credentials.cc", "internal/oauth2_minimal_iam_credentials_rest.cc", "internal/oauth2_refreshing_credentials_wrapper.cc", + "internal/oauth2_regional_access_boundary_token_manager.cc", "internal/oauth2_service_account_credentials.cc", "internal/oauth2_universe_domain.cc", "internal/openssl/parse_service_account_p12_file.cc", @@ -121,6 +126,7 @@ google_cloud_cpp_rest_internal_srcs = [ "internal/rest_lro_helpers.cc", "internal/rest_opentelemetry.cc", "internal/rest_parse_json_error.cc", + "internal/rest_pure_completion_queue_impl.cc", "internal/rest_request.cc", "internal/rest_response.cc", "internal/rest_set_metadata.cc", diff --git a/google/cloud/google_cloud_cpp_rest_internal.cmake b/google/cloud/google_cloud_cpp_rest_internal.cmake index 3d38991523870..7a3b97995e301 100644 --- a/google/cloud/google_cloud_cpp_rest_internal.cmake +++ b/google/cloud/google_cloud_cpp_rest_internal.cmake @@ -23,6 +23,7 @@ endif () # the library add_library( google_cloud_cpp_rest_internal # cmake-format: sort + internal/async_rest_retry_loop.h internal/binary_data_as_debug_string.cc internal/binary_data_as_debug_string.h internal/curl_handle.cc @@ -92,6 +93,8 @@ add_library( internal/oauth2_minimal_iam_credentials_rest.h internal/oauth2_refreshing_credentials_wrapper.cc internal/oauth2_refreshing_credentials_wrapper.h + internal/oauth2_regional_access_boundary_token_manager.cc + internal/oauth2_regional_access_boundary_token_manager.h internal/oauth2_service_account_credentials.cc internal/oauth2_service_account_credentials.h internal/oauth2_universe_domain.cc @@ -113,6 +116,9 @@ add_library( internal/rest_options.h internal/rest_parse_json_error.cc internal/rest_parse_json_error.h + internal/rest_pure_background_threads_impl.h + internal/rest_pure_completion_queue_impl.cc + internal/rest_pure_completion_queue_impl.h internal/rest_request.cc internal/rest_request.h internal/rest_response.cc @@ -137,7 +143,7 @@ add_library( target_link_libraries( google_cloud_cpp_rest_internal PUBLIC absl::span google-cloud-cpp::common CURL::libcurl - nlohmann_json::nlohmann_json) + absl::flat_hash_map nlohmann_json::nlohmann_json) if (WIN32) target_compile_definitions(google_cloud_cpp_rest_internal PRIVATE WIN32_LEAN_AND_MEAN) @@ -197,6 +203,7 @@ google_cloud_cpp_add_pkgconfig( "Provides REST Transport for the Google Cloud C++ Client Library." "google_cloud_cpp_common" "libcurl" + "absl_flat_hash_map" NON_WIN32_REQUIRES openssl WIN32_LIBS @@ -279,6 +286,7 @@ if (BUILD_TESTING) internal/oauth2_logging_credentials_test.cc internal/oauth2_minimal_iam_credentials_rest_test.cc internal/oauth2_refreshing_credentials_wrapper_test.cc + internal/oauth2_regional_access_boundary_token_manager_test.cc internal/oauth2_service_account_credentials_test.cc internal/oauth2_universe_domain_test.cc internal/populate_rest_options_test.cc diff --git a/google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl b/google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl index b80f9ce502207..97315183034dc 100644 --- a/google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl +++ b/google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl @@ -51,6 +51,7 @@ google_cloud_cpp_rest_internal_unit_tests = [ "internal/oauth2_logging_credentials_test.cc", "internal/oauth2_minimal_iam_credentials_rest_test.cc", "internal/oauth2_refreshing_credentials_wrapper_test.cc", + "internal/oauth2_regional_access_boundary_token_manager_test.cc", "internal/oauth2_service_account_credentials_test.cc", "internal/oauth2_universe_domain_test.cc", "internal/populate_rest_options_test.cc", diff --git a/google/cloud/google_cloud_cpp_rest_protobuf_internal.bzl b/google/cloud/google_cloud_cpp_rest_protobuf_internal.bzl index ef8329acde2e4..6581cf0fc3469 100644 --- a/google/cloud/google_cloud_cpp_rest_protobuf_internal.bzl +++ b/google/cloud/google_cloud_cpp_rest_protobuf_internal.bzl @@ -22,7 +22,6 @@ google_cloud_cpp_rest_protobuf_internal_hdrs = [ "internal/async_rest_polling_loop.h", "internal/async_rest_polling_loop_custom.h", "internal/async_rest_polling_loop_impl.h", - "internal/async_rest_retry_loop.h", "internal/rest_background_threads_impl.h", "internal/rest_completion_queue_impl.h", "internal/rest_stub_helpers.h", diff --git a/google/cloud/google_cloud_cpp_rest_protobuf_internal.cmake b/google/cloud/google_cloud_cpp_rest_protobuf_internal.cmake index d77363ad99cd3..bd35043de18de 100644 --- a/google/cloud/google_cloud_cpp_rest_protobuf_internal.cmake +++ b/google/cloud/google_cloud_cpp_rest_protobuf_internal.cmake @@ -23,7 +23,6 @@ add_library( internal/async_rest_polling_loop.h internal/async_rest_polling_loop_custom.h internal/async_rest_polling_loop_impl.h - internal/async_rest_retry_loop.h internal/rest_background_threads_impl.cc internal/rest_background_threads_impl.h internal/rest_completion_queue_impl.cc diff --git a/google/cloud/internal/async_rest_retry_loop.h b/google/cloud/internal/async_rest_retry_loop.h index 968ee2c8cf8f8..3b3365505d05e 100644 --- a/google/cloud/internal/async_rest_retry_loop.h +++ b/google/cloud/internal/async_rest_retry_loop.h @@ -16,12 +16,12 @@ #define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_ASYNC_REST_RETRY_LOOP_H #include "google/cloud/backoff_policy.h" -#include "google/cloud/completion_queue.h" +// #include "google/cloud/completion_queue.h" #include "google/cloud/future.h" #include "google/cloud/idempotency.h" #include "google/cloud/internal/call_context.h" -#include "google/cloud/internal/grpc_opentelemetry.h" #include "google/cloud/internal/invoke_result.h" +#include "google/cloud/internal/opentelemetry.h" #include "google/cloud/internal/rest_context.h" #include "google/cloud/internal/retry_loop_helpers.h" #include "google/cloud/options.h" @@ -167,14 +167,15 @@ struct FutureValueType> { * functions. If the value is visible, the retry loop will stop on the next * callback and/or before the next request or timer is issued. */ -template +template class AsyncRestRetryLoopImpl - : public std::enable_shared_from_this< - AsyncRestRetryLoopImpl> { + : public std::enable_shared_from_this> { public: AsyncRestRetryLoopImpl(std::unique_ptr retry_policy, std::unique_ptr backoff_policy, - Idempotency idempotency, CompletionQueue cq, + Idempotency idempotency, CompletionQueueType cq, Functor&& functor, internal::ImmutableOptions options, Request request, char const* location) : retry_policy_(std::move(retry_policy)), @@ -194,7 +195,7 @@ class AsyncRestRetryLoopImpl ~AsyncRestRetryLoopImpl() = default; using ReturnType = ::google::cloud::internal::invoke_result_t< - Functor, CompletionQueue&, std::unique_ptr, + Functor, CompletionQueueType&, std::unique_ptr, internal::ImmutableOptions, Request const&>; using T = typename FutureValueType::value_type; @@ -329,7 +330,7 @@ class AsyncRestRetryLoopImpl std::unique_ptr retry_policy_; std::unique_ptr backoff_policy_; Idempotency idempotency_ = Idempotency::kNonIdempotent; - CompletionQueue cq_; + CompletionQueueType cq_; std::decay_t functor_; Request request_; char const* location_ = "unknown"; @@ -352,24 +353,26 @@ class AsyncRestRetryLoopImpl */ template < typename Functor, typename Request, typename RetryPolicyType, + typename CompletionQueueType, std::enable_if_t< google::cloud::internal::is_invocable< - Functor, CompletionQueue&, std::unique_ptr, + Functor, CompletionQueueType&, std::unique_ptr, google::cloud::internal::ImmutableOptions, Request const&>::value, int> = 0> auto AsyncRestRetryLoop(std::unique_ptr retry_policy, std::unique_ptr backoff_policy, - Idempotency idempotency, CompletionQueue cq, + Idempotency idempotency, CompletionQueueType cq, Functor&& functor, internal::ImmutableOptions options, Request request, char const* location) -> google::cloud::internal::invoke_result_t< - Functor, CompletionQueue&, std::unique_ptr, + Functor, CompletionQueueType&, std::unique_ptr, google::cloud::internal::ImmutableOptions, Request const&> { - auto loop = std::make_shared< - AsyncRestRetryLoopImpl>( - std::move(retry_policy), std::move(backoff_policy), idempotency, - std::move(cq), std::forward(functor), std::move(options), - std::move(request), location); + auto loop = + std::make_shared>( + std::move(retry_policy), std::move(backoff_policy), idempotency, + std::move(cq), std::forward(functor), std::move(options), + std::move(request), location); return loop->Start(); } diff --git a/google/cloud/internal/curl_impl.cc b/google/cloud/internal/curl_impl.cc index c20667a4b5b3d..9d897bfe59909 100644 --- a/google/cloud/internal/curl_impl.cc +++ b/google/cloud/internal/curl_impl.cc @@ -297,10 +297,9 @@ void CurlImpl::MergeAndWriteHeaders( } } -void CurlImpl::SetHeaders( - std::unordered_map> const& headers) { +void CurlImpl::SetHeaders(HttpHeaders const& headers) { for (auto const& header : headers) { - SetHeader(HttpHeader(header.first, header.second)); + SetHeader(header.second); } } diff --git a/google/cloud/internal/curl_impl.h b/google/cloud/internal/curl_impl.h index 266be569501e3..c71078aeeacd4 100644 --- a/google/cloud/internal/curl_impl.h +++ b/google/cloud/internal/curl_impl.h @@ -35,7 +35,6 @@ #include #include #include -#include #include namespace google { @@ -84,8 +83,7 @@ class CurlImpl { CurlImpl& operator=(CurlImpl&&) = default; void SetHeader(HttpHeader header); - void SetHeaders( - std::unordered_map> const& headers); + void SetHeaders(HttpHeaders const& headers); std::string MakeEscapedString(std::string const& s); diff --git a/google/cloud/internal/curl_rest_client.cc b/google/cloud/internal/curl_rest_client.cc index 04f5ec98cea6b..0a993ff7c2a86 100644 --- a/google/cloud/internal/curl_rest_client.cc +++ b/google/cloud/internal/curl_rest_client.cc @@ -124,10 +124,12 @@ StatusOr> CurlRestClient::CreateCurlImpl( auto impl = std::make_unique(std::move(handle), handle_factory_, options); if (credentials_) { - auto auth_header = - credentials_->AuthenticationHeader(std::chrono::system_clock::now()); - if (!auth_header.ok()) return std::move(auth_header).status(); - impl->SetHeader(HttpHeader(auth_header->first, auth_header->second)); + auto auth_headers = credentials_->AuthenticationHeaders( + std::chrono::system_clock::now(), endpoint_address_); + if (!auth_headers.ok()) return std::move(auth_headers).status(); + for (auto& header : *auth_headers) { + impl->SetHeader(std::move(header)); + } } impl->SetHeader(HostHeader(options, endpoint_address_)); impl->SetHeaders(context.headers()); diff --git a/google/cloud/internal/curl_rest_client_test.cc b/google/cloud/internal/curl_rest_client_test.cc index 59f8339d7b28d..487be9ed6bac0 100644 --- a/google/cloud/internal/curl_rest_client_test.cc +++ b/google/cloud/internal/curl_rest_client_test.cc @@ -29,21 +29,21 @@ TEST(CurlRestClientStandaloneFunctions, HostHeader) { std::string expected; } cases[] = { {"https://storage.googleapis.com", "storage.googleapis.com", - "Host: storage.googleapis.com"}, - {"https://storage.googleapis.com", "", "Host: storage.googleapis.com"}, - {"https://storage.googleapis.com", "auth", "Host: auth"}, + "host: storage.googleapis.com"}, + {"https://storage.googleapis.com", "", "host: storage.googleapis.com"}, + {"https://storage.googleapis.com", "auth", "host: auth"}, {"https://storage.googleapis.com:443", "storage.googleapis.com", - "Host: storage.googleapis.com"}, + "host: storage.googleapis.com"}, {"https://restricted.googleapis.com", "storage.googleapis.com", - "Host: storage.googleapis.com"}, + "host: storage.googleapis.com"}, {"https://private.googleapis.com", "storage.googleapis.com", - "Host: storage.googleapis.com"}, + "host: storage.googleapis.com"}, {"https://restricted.googleapis.com", "iamcredentials.googleapis.com", - "Host: iamcredentials.googleapis.com"}, + "host: iamcredentials.googleapis.com"}, {"https://private.googleapis.com", "iamcredentials.googleapis.com", - "Host: iamcredentials.googleapis.com"}, + "host: iamcredentials.googleapis.com"}, {"http://localhost:8080", "", ""}, - {"http://localhost:8080", "auth", "Host: auth"}, + {"http://localhost:8080", "auth", "host: auth"}, {"http://[::1]", "", ""}, {"http://[::1]/", "", ""}, {"http://[::1]/foo/bar", "", ""}, diff --git a/google/cloud/internal/grpc_opentelemetry.h b/google/cloud/internal/grpc_opentelemetry.h index 9ea8bc86a80e2..1ed5be9c488ee 100644 --- a/google/cloud/internal/grpc_opentelemetry.h +++ b/google/cloud/internal/grpc_opentelemetry.h @@ -108,23 +108,23 @@ future> EndSpan( }); } -/** - * Returns a traced timer, if OpenTelemetry tracing is enabled. - */ -template -future> TracedAsyncBackoff( - CompletionQueue& cq, Options const& options, - std::chrono::duration duration, std::string const& name) { - if (TracingEnabled(options)) { - auto span = MakeSpan(name); - OTelScope scope(span); - auto timer = cq.MakeRelativeTimer(duration); - return EndSpan(std::move(span), std::move(timer)); - } - (void)options; - (void)name; - return cq.MakeRelativeTimer(duration); -} +// /** +// * Returns a traced timer, if OpenTelemetry tracing is enabled. +// */ +// template +// future> TracedAsyncBackoff( +// CompletionQueue& cq, Options const& options, +// std::chrono::duration duration, std::string const& name) { +// if (TracingEnabled(options)) { +// auto span = MakeSpan(name); +// OTelScope scope(span); +// auto timer = cq.MakeRelativeTimer(duration); +// return EndSpan(std::move(span), std::move(timer)); +// } +// (void)options; +// (void)name; +// return cq.MakeRelativeTimer(duration); +// } } // namespace internal GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END diff --git a/google/cloud/internal/http_header.cc b/google/cloud/internal/http_header.cc index 32d0220797bd9..fbe8820eec808 100644 --- a/google/cloud/internal/http_header.cc +++ b/google/cloud/internal/http_header.cc @@ -22,48 +22,50 @@ namespace cloud { namespace rest_internal { GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN -HttpHeader::HttpHeader(std::string key) : key_(std::move(key)) {} +HttpHeader::HttpHeader(HttpHeaderName key) : name_(std::move(key)) {} -HttpHeader::HttpHeader(std::string key, std::string value) - : key_(std::move(key)), values_({std::move(value)}) {} +HttpHeader::HttpHeader(std::pair header) + : HttpHeader(std::move(header.first), std::move(header.second)) {} -HttpHeader::HttpHeader(std::string key, std::vector values) - : key_(std::move(key)), values_(std::move(values)) {} +HttpHeader::HttpHeader(HttpHeaderName key, std::string value) + : name_(std::move(key)), values_({std::move(value)}) {} -HttpHeader::HttpHeader(std::string key, +HttpHeader::HttpHeader(HttpHeaderName key, std::vector values) + : name_(std::move(key)), values_(std::move(values)) {} + +HttpHeader::HttpHeader(HttpHeaderName key, std::initializer_list values) - : key_(std::move(key)) { + : name_(std::move(key)) { for (auto&& v : values) values_.emplace_back(v); } bool operator==(HttpHeader const& lhs, HttpHeader const& rhs) { - return absl::AsciiStrToLower(lhs.key_) == absl::AsciiStrToLower(rhs.key_) && - lhs.values_ == rhs.values_; + return lhs.name_ == rhs.name_ && lhs.values_ == rhs.values_; } bool operator<(HttpHeader const& lhs, HttpHeader const& rhs) { - return absl::AsciiStrToLower(lhs.key_) < absl::AsciiStrToLower(rhs.key_); + return lhs.name_ < rhs.name_; } -bool HttpHeader::IsSameKey(std::string const& key) const { - return absl::AsciiStrToLower(key_) == absl::AsciiStrToLower(key); +bool HttpHeader::IsSameKey(HttpHeaderName const& name) const { + return name_ == name.name(); } bool HttpHeader::IsSameKey(HttpHeader const& other) const { - return IsSameKey(other.key_); + return name_ == other.name_; } HttpHeader::operator std::string() const { - if (key_.empty()) return {}; - if (values_.empty()) return absl::StrCat(key_, ":"); - return absl::StrCat(key_, ": ", absl::StrJoin(values_, ",")); + if (name_.empty()) return {}; + if (values_.empty()) return absl::StrCat(name_.name(), ":"); + return absl::StrCat(name_.name(), ": ", absl::StrJoin(values_, ",")); } std::string HttpHeader::DebugString() const { - if (key_.empty()) return {}; - if (values_.empty()) return absl::StrCat(key_, ":"); + if (name_.empty()) return {}; + if (values_.empty()) return absl::StrCat(name_.name(), ":"); return absl::StrCat( - key_, ": ", + name_.name(), ": ", absl::StrJoin(values_, ",", [](std::string* out, std::string const& v) { absl::StrAppend(out, v.substr(0, 10)); })); diff --git a/google/cloud/internal/http_header.h b/google/cloud/internal/http_header.h index 54799eaf2fb83..5a64f42d8a887 100644 --- a/google/cloud/internal/http_header.h +++ b/google/cloud/internal/http_header.h @@ -16,6 +16,13 @@ #define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_HTTP_HEADER_H #include "google/cloud/version.h" +#if UINTPTR_MAX == UINT64_MAX +#include "absl/container/flat_hash_map.h" +#else +#include +#endif +#include "absl/strings/ascii.h" +#include #include #include @@ -24,17 +31,67 @@ namespace cloud { namespace rest_internal { GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +// This class represents a case-insensitive HTTP header name by storing all +// strings in lower-case. +class HttpHeaderName { + public: + HttpHeaderName() = default; + HttpHeaderName(std::string name) // NOLINT(google-explicit-constructor) + : name_(std::move(name)) { + absl::AsciiStrToLower(&name_); + } + HttpHeaderName(std::string_view name) // NOLINT(google-explicit-constructor) + : HttpHeaderName(std::string{name}) {} + HttpHeaderName(char const* name) // NOLINT(google-explicit-constructor) + : HttpHeaderName(std::string{name}) {} + + operator std::string() const { // NOLINT(google-explicit-constructor) + return name_; + } + operator std::string_view() const { // NOLINT(google-explicit-constructor) + return name_; + } + operator char const*() const { // NOLINT(google-explicit-constructor) + return name_.c_str(); + } + + bool empty() const { return name_.empty(); } + std::string const& name() const { return name_; } + + friend bool operator==(HttpHeaderName const& lhs, HttpHeaderName const& rhs) { + return lhs.name_ == rhs.name_; + } + friend bool operator<(HttpHeaderName const& lhs, HttpHeaderName const& rhs) { + return lhs.name_ < rhs.name_; + } + friend bool operator!=(HttpHeaderName const& lhs, HttpHeaderName const& rhs) { + return !(lhs == rhs); + } + friend bool operator>(HttpHeaderName const& lhs, HttpHeaderName const& rhs) { + return !(lhs < rhs) && (lhs != rhs); + } + friend bool operator>=(HttpHeaderName const& lhs, HttpHeaderName const& rhs) { + return !(lhs < rhs); + } + friend bool operator<=(HttpHeaderName const& lhs, HttpHeaderName const& rhs) { + return !(lhs > rhs); + } + + private: + std::string name_; +}; + /** * This class represents an HTTP header field. */ class HttpHeader { public: HttpHeader() = default; - explicit HttpHeader(std::string key); - HttpHeader(std::string key, std::string value); - HttpHeader(std::string key, std::initializer_list values); - - HttpHeader(std::string key, std::vector values); + explicit HttpHeader(HttpHeaderName key); + explicit HttpHeader(std::pair header); + HttpHeader(HttpHeaderName key, std::string value); + HttpHeader(HttpHeaderName key, std::initializer_list values); + HttpHeader(HttpHeaderName key, std::vector values); HttpHeader(HttpHeader&&) = default; HttpHeader& operator=(HttpHeader&&) = default; @@ -57,19 +114,23 @@ class HttpHeader { friend bool operator<(HttpHeader const& lhs, HttpHeader const& rhs); // If the key is empty, the entire HttpHeader is considered empty. - bool empty() const { return key_.empty(); } + bool empty() const { return name_.empty(); } + + // Number of values. + std::size_t size() const { return values_.size(); } // Checks to see if the values are empty. Does not inspect the key field. bool EmptyValues() const { return values_.empty(); } // Performs a case-insensitive comparison of the key. bool IsSameKey(HttpHeader const& other) const; - bool IsSameKey(std::string const& key) const; + bool IsSameKey(HttpHeaderName const& name) const; - // While the RFCs indicate that header keys are case-insensitive, no attempt - // to convert them to all lowercase is made. Header keys are printed in the - // case they were constructed with. We rely on libcurl to encode them per the - // HTTP version used. + std::string name() const { return name_; } + std::vector const& values() const { return values_; } + + // The RFCs indicate that header names are case-insensitive. The + // HttpHeaderName type converts them to all lowercase. // // HTTP/1.1 https://www.rfc-editor.org/rfc/rfc7230#section-3.2 // HTTP/2 https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2 @@ -83,14 +144,44 @@ class HttpHeader { HttpHeader& MergeHeader(HttpHeader const& other); HttpHeader& MergeHeader(HttpHeader&& other); + using value_type = std::string; + using const_iterator = std::vector::const_iterator; + const_iterator begin() const { return values_.begin(); } + const_iterator end() const { return values_.end(); } + const_iterator cbegin() const { return begin(); } + const_iterator cend() const { return end(); } + private: - std::string key_; + HttpHeaderName name_; std::vector values_; }; +// Abseil does not guarantee compatibility with 32-bit platforms that they do +// not test with. Support for such platforms is a community effort. Using +// std::unordered_map on 32-bit platforms reduces the likelihood of issues +// arising due to this arrangement. +#if UINTPTR_MAX == UINT64_MAX +// 64-bit architecture +using HttpHeaders = absl::flat_hash_map; +#else +// 32-bit architecture +using HttpHeaders = std::unordered_map; +#endif // UINTPTR_MAX == UINT64_MAX + GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace rest_internal } // namespace cloud } // namespace google +#if UINTPTR_MAX != UINT64_MAX +// This specialization has to be in the global namespace. +template <> +struct std::hash { + std::size_t operator()( + google::cloud::rest_internal::HttpHeaderName const& k) const noexcept { + return std::hash()(k.name()); + } +}; +#endif // UINTPTR_MAX != UINT64_MAX + #endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_HTTP_HEADER_H diff --git a/google/cloud/internal/http_header_test.cc b/google/cloud/internal/http_header_test.cc index a7fd60520a101..4e8441d385fee 100644 --- a/google/cloud/internal/http_header_test.cc +++ b/google/cloud/internal/http_header_test.cc @@ -23,6 +23,48 @@ namespace { using ::testing::Eq; +TEST(HttpHeaderName, ConstructorsAndConversions) { + auto constexpr kHeaderName = "header-name"; + std::string header(kHeaderName); + std::string_view header_view(header); + + HttpHeaderName h1(header); + HttpHeaderName h2(header_view); + HttpHeaderName h3(kHeaderName); + + EXPECT_THAT(std::string(h2), Eq(header)); + EXPECT_THAT(std::string_view(h3), Eq(header)); + EXPECT_THAT(static_cast(h1), Eq(header)); +} + +TEST(HttpHeaderName, Empty) { + HttpHeaderName empty; + EXPECT_TRUE(empty.empty()); + EXPECT_THAT(std::string(empty), Eq("")); + + HttpHeaderName not_empty("header-name"); + EXPECT_FALSE(not_empty.empty()); + EXPECT_THAT(std::string(not_empty), Eq("header-name")); +} + +TEST(HttpHeaderName, LogicalOperators) { + HttpHeaderName h1("aa"); + HttpHeaderName h2("bb"); + EXPECT_TRUE(h1 < h2); + EXPECT_FALSE(h2 < h1); + EXPECT_FALSE(h1 == h2); + EXPECT_FALSE(h2 == h1); + EXPECT_TRUE(h1 != h2); + EXPECT_TRUE(h2 != h1); + EXPECT_FALSE(h1 > h2); + EXPECT_TRUE(h2 > h1); + EXPECT_TRUE(h1 >= h1); + EXPECT_TRUE(h2 >= h1); + EXPECT_TRUE(h1 <= h2); + EXPECT_TRUE(h1 <= h1); + EXPECT_TRUE(h2 >= h1); +} + TEST(HttpHeader, ConstructionAndStringFormatting) { HttpHeader empty; EXPECT_THAT(std::string(empty), Eq("")); diff --git a/google/cloud/internal/json_parsing.cc b/google/cloud/internal/json_parsing.cc index b1e42321debe5..6ef260fe38c35 100644 --- a/google/cloud/internal/json_parsing.cc +++ b/google/cloud/internal/json_parsing.cc @@ -63,6 +63,19 @@ StatusOr ValidateIntField(nlohmann::json const& json, return it->get(); } +StatusOr> ValidateStringArrayField( + nlohmann::json const& json, absl::string_view name, + absl::string_view object_name, internal::ErrorContext const& ec) { + auto it = json.find(std::string{name}); + if (it == json.end()) return MissingFieldError(name, object_name, ec); + if (!it->is_array()) return InvalidTypeError(name, object_name, ec); + if (!std::all_of(it->begin(), it->end(), + [](nlohmann::json const& e) { return e.is_string(); })) { + return InvalidTypeError(name, object_name, ec); + } + return it->get>(); +} + Status MissingFieldError(absl::string_view name, absl::string_view object_name, internal::ErrorContext const& ec) { return InvalidArgumentError( diff --git a/google/cloud/internal/json_parsing.h b/google/cloud/internal/json_parsing.h index 3549d73326f92..66676f6811f4d 100644 --- a/google/cloud/internal/json_parsing.h +++ b/google/cloud/internal/json_parsing.h @@ -57,6 +57,12 @@ StatusOr ValidateIntField(nlohmann::json const& json, std::int32_t default_value, internal::ErrorContext const& ec); +/// Returns the string values for `json[name]` (which must exist) or a +/// descriptive error. +StatusOr> ValidateStringArrayField( + nlohmann::json const& json, absl::string_view name, + absl::string_view object_name, internal::ErrorContext const& ec); + /// Use when a JSON field cannot be found but is required. Status MissingFieldError(absl::string_view name, absl::string_view object_name, internal::ErrorContext const& ec); diff --git a/google/cloud/internal/oauth2_api_key_credentials.cc b/google/cloud/internal/oauth2_api_key_credentials.cc index 245a9a33ed08d..aee0f3386f16c 100644 --- a/google/cloud/internal/oauth2_api_key_credentials.cc +++ b/google/cloud/internal/oauth2_api_key_credentials.cc @@ -27,9 +27,12 @@ StatusOr ApiKeyCredentials::GetToken( return AccessToken{std::string{}, tp}; } -StatusOr> -ApiKeyCredentials::AuthenticationHeader(std::chrono::system_clock::time_point) { - return std::make_pair(std::string{"x-goog-api-key"}, api_key_); +StatusOr> +ApiKeyCredentials::AuthenticationHeaders(std::chrono::system_clock::time_point, + std::string_view) { + std::vector headers; + headers.emplace_back("x-goog-api-key", api_key_); + return headers; } GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END diff --git a/google/cloud/internal/oauth2_api_key_credentials.h b/google/cloud/internal/oauth2_api_key_credentials.h index aec219a29cba0..1ab7141d46ece 100644 --- a/google/cloud/internal/oauth2_api_key_credentials.h +++ b/google/cloud/internal/oauth2_api_key_credentials.h @@ -37,8 +37,9 @@ class ApiKeyCredentials : public oauth2_internal::Credentials { StatusOr GetToken( std::chrono::system_clock::time_point tp) override; - StatusOr> AuthenticationHeader( - std::chrono::system_clock::time_point) override; + StatusOr> AuthenticationHeaders( + std::chrono::system_clock::time_point, + std::string_view endpoint) override; private: std::string api_key_; diff --git a/google/cloud/internal/oauth2_api_key_credentials_test.cc b/google/cloud/internal/oauth2_api_key_credentials_test.cc index 12f51a2257b50..3aaaab21199ae 100644 --- a/google/cloud/internal/oauth2_api_key_credentials_test.cc +++ b/google/cloud/internal/oauth2_api_key_credentials_test.cc @@ -13,6 +13,7 @@ // limitations under the License. #include "google/cloud/internal/oauth2_api_key_credentials.h" +#include "google/cloud/internal/http_header.h" #include "google/cloud/testing_util/status_matchers.h" #include #include @@ -24,8 +25,8 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN namespace { using ::google::cloud::testing_util::IsOkAndHolds; +using ::testing::Contains; using ::testing::IsEmpty; -using ::testing::Pair; TEST(ApiKeyCredentials, EmptyToken) { ApiKeyCredentials creds("api-key"); @@ -38,8 +39,9 @@ TEST(ApiKeyCredentials, EmptyToken) { TEST(ApiKeyCredentials, SetsXGoogApiKeyHeader) { ApiKeyCredentials creds("api-key"); auto const now = std::chrono::system_clock::now(); - EXPECT_THAT(creds.AuthenticationHeader(now), - IsOkAndHolds(Pair("x-goog-api-key", "api-key"))); + EXPECT_THAT(creds.AuthenticationHeaders(now, ""), + IsOkAndHolds(Contains( + rest_internal::HttpHeader("x-goog-api-key", "api-key")))); } } // namespace diff --git a/google/cloud/internal/oauth2_credentials.cc b/google/cloud/internal/oauth2_credentials.cc index b2f4d53c57d63..6533d960eb3a8 100644 --- a/google/cloud/internal/oauth2_credentials.cc +++ b/google/cloud/internal/oauth2_credentials.cc @@ -16,6 +16,7 @@ #include "google/cloud/internal/make_status.h" #include "google/cloud/internal/oauth2_universe_domain.h" #include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" namespace google { namespace cloud { @@ -46,21 +47,29 @@ StatusOr Credentials::project_id( return project_id(); } -StatusOr> Credentials::AuthenticationHeader( - std::chrono::system_clock::time_point tp) { +StatusOr Credentials::AllowedLocations( + std::chrono::system_clock::time_point tp, std::string_view) { + return {}; +} + +StatusOr> +Credentials::AuthenticationHeaders(std::chrono::system_clock::time_point tp, + std::string_view endpoint) { + std::vector headers; auto token = GetToken(tp); if (!token) return std::move(token).status(); - if (token->token.empty()) return std::make_pair(std::string{}, std::string{}); - return std::make_pair(std::string{"Authorization"}, - absl::StrCat("Bearer ", token->token)); -} + if (!token->token.empty()) { + headers.emplace_back("Authorization", + absl::StrCat("Bearer ", token->token)); + } -StatusOr AuthenticationHeaderJoined( - Credentials& credentials, std::chrono::system_clock::time_point tp) { - auto header = credentials.AuthenticationHeader(tp); - if (!header) return std::move(header).status(); - if (header->first.empty()) return std::string{}; - return absl::StrCat(header->first, ": ", header->second); + auto allowed_locations = AllowedLocations(tp, endpoint); + // Not all credential types support the x-allowed-locations header. For those + // that do, if there is a problem retrieving the header, omit the header. + if (allowed_locations.ok() && !allowed_locations->empty()) { + headers.push_back(*std::move(allowed_locations)); + } + return headers; } GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END diff --git a/google/cloud/internal/oauth2_credentials.h b/google/cloud/internal/oauth2_credentials.h index 7c050f229370c..8044ddcf31c3c 100644 --- a/google/cloud/internal/oauth2_credentials.h +++ b/google/cloud/internal/oauth2_credentials.h @@ -16,10 +16,12 @@ #define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_OAUTH2_CREDENTIALS_H #include "google/cloud/access_token.h" +#include "google/cloud/internal/http_header.h" #include "google/cloud/options.h" #include "google/cloud/status.h" #include "google/cloud/status_or.h" #include "google/cloud/version.h" +#include #include #include #include @@ -109,10 +111,18 @@ class Credentials { virtual StatusOr project_id(Options const&) const; /** - * Returns a header pair used for authentication. + * Returns the "x-allowed-locations" header if applicable for the credential + * type. + */ + virtual StatusOr AllowedLocations( + std::chrono::system_clock::time_point tp, std::string_view endpoint); + + /** + * Returns header pairs used for authentication. * * In most cases, this is the "Authorization" HTTP header. For API key - * credentials, it is the "X-Goog-Api-Key" header. + * credentials, it is the "X-Goog-Api-Key" header. It may also include the + * "x-allowed-locations" header if applicable. * * If unable to obtain a value for the header, which could happen for * `Credentials` that need to be periodically refreshed, the underlying @@ -120,25 +130,11 @@ class Credentials { * Otherwise, the returned value will contain the header pair to be used in * HTTP requests. */ - virtual StatusOr> AuthenticationHeader( - std::chrono::system_clock::time_point tp); + virtual StatusOr> + AuthenticationHeaders(std::chrono::system_clock::time_point tp, + std::string_view endpoint); }; -/** - * Returns a header pair as a single string to be used for authentication. - * - * In most cases, this is the "Authorization" HTTP header. For API key - * credentials, it is the "X-Goog-Api-Key" header. - * - * If unable to obtain a value for the header, which could happen for - * `Credentials` that need to be periodically refreshed, the underlying `Status` - * will indicate failure details from the refresh HTTP request. Otherwise, the - * returned value will contain the header pair to be used in HTTP requests. - */ -StatusOr AuthenticationHeaderJoined( - Credentials& credentials, std::chrono::system_clock::time_point tp = - std::chrono::system_clock::now()); - GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace oauth2_internal } // namespace cloud diff --git a/google/cloud/internal/oauth2_credentials_test.cc b/google/cloud/internal/oauth2_credentials_test.cc index 7bdadaac90fff..47679dcdac9d3 100644 --- a/google/cloud/internal/oauth2_credentials_test.cc +++ b/google/cloud/internal/oauth2_credentials_test.cc @@ -26,15 +26,17 @@ namespace { using ::google::cloud::internal::UnavailableError; using ::google::cloud::testing_util::IsOk; using ::google::cloud::testing_util::IsOkAndHolds; -using ::testing::IsEmpty; +using ::testing::Contains; using ::testing::Not; -using ::testing::Pair; using ::testing::Return; class MockCredentials : public Credentials { public: MOCK_METHOD(StatusOr, GetToken, (std::chrono::system_clock::time_point), (override)); + MOCK_METHOD(StatusOr, AllowedLocations, + (std::chrono::system_clock::time_point, std::string_view), + (override)); }; TEST(Credentials, AuthorizationHeaderSuccess) { @@ -43,48 +45,60 @@ TEST(Credentials, AuthorizationHeaderSuccess) { auto const expiration = now + std::chrono::seconds(3600); EXPECT_CALL(mock, GetToken(now)) .WillOnce(Return(AccessToken{"test-token", expiration})); - auto actual = mock.AuthenticationHeader(now); - EXPECT_THAT(actual, IsOkAndHolds(Pair("Authorization", "Bearer test-token"))); + EXPECT_CALL(mock, AllowedLocations) + .WillOnce(Return(rest_internal::HttpHeader{})); + auto actual = mock.AuthenticationHeaders(now, "my-endpoint"); + EXPECT_THAT(actual, IsOkAndHolds(Contains(rest_internal::HttpHeader( + "authorization", "Bearer test-token")))); } -TEST(Credentials, AuthenticationHeaderJoinedSuccess) { +TEST(Credentials, AuthenticationHeaderError) { MockCredentials mock; - auto const now = std::chrono::system_clock::now(); - auto const expiration = now + std::chrono::seconds(3600); - EXPECT_CALL(mock, GetToken(now)) - .WillOnce(Return(AccessToken{"test-token", expiration})); - auto actual = AuthenticationHeaderJoined(mock, now); - EXPECT_THAT(actual, IsOkAndHolds("Authorization: Bearer test-token")); + EXPECT_CALL(mock, GetToken).WillOnce(Return(UnavailableError("try-again"))); + auto actual = mock.AuthenticationHeaders(std::chrono::system_clock::now(), + "my-endpoint"); + EXPECT_EQ(actual.status(), UnavailableError("try-again")); } -TEST(Credentials, AuthenticationHeaderJoinedEmpty) { +TEST(Credentials, ProjectId) { MockCredentials mock; - auto const now = std::chrono::system_clock::now(); - auto const expiration = now + std::chrono::seconds(3600); - EXPECT_CALL(mock, GetToken(now)) - .WillOnce(Return(AccessToken{"", expiration})); - auto actual = AuthenticationHeaderJoined(mock, now); - EXPECT_THAT(actual, IsOkAndHolds(IsEmpty())); + EXPECT_THAT(mock.project_id(), Not(IsOk())); + EXPECT_THAT(mock.project_id({}), Not(IsOk())); } -TEST(Credentials, AuthenticationHeaderError) { +TEST(Credentials, AllowedLocationsSuccess) { MockCredentials mock; - EXPECT_CALL(mock, GetToken).WillOnce(Return(UnavailableError("try-again"))); - auto actual = mock.AuthenticationHeader(std::chrono::system_clock::now()); - EXPECT_EQ(actual.status(), UnavailableError("try-again")); -} + auto const now = std::chrono::system_clock::now(); + auto const expiration = now + std::chrono::seconds(3600); + EXPECT_CALL(mock, GetToken) + .WillOnce(Return(AccessToken{"test-token", expiration})); + EXPECT_CALL(mock, AllowedLocations) + .WillOnce(Return( + rest_internal::HttpHeader("x-allowed-locations", "my-location"))); -TEST(Credentials, AuthenticationHeaderJoinedError) { - MockCredentials mock; - EXPECT_CALL(mock, GetToken).WillOnce(Return(UnavailableError("try-again"))); - auto actual = AuthenticationHeaderJoined(mock); - EXPECT_EQ(actual.status(), UnavailableError("try-again")); + auto auth_headers = mock.AuthenticationHeaders( + std::chrono::system_clock::now(), "my-endpoint"); + EXPECT_THAT( + auth_headers, + IsOkAndHolds(::testing::ElementsAre( + rest_internal::HttpHeader("authorization", "Bearer test-token"), + rest_internal::HttpHeader("x-allowed-locations", "my-location")))); } -TEST(Credentials, ProjectId) { +TEST(Credentials, AllowedLocationsFailure) { MockCredentials mock; - EXPECT_THAT(mock.project_id(), Not(IsOk())); - EXPECT_THAT(mock.project_id({}), Not(IsOk())); + auto const now = std::chrono::system_clock::now(); + auto const expiration = now + std::chrono::seconds(3600); + EXPECT_CALL(mock, GetToken) + .WillOnce(Return(AccessToken{"test-token", expiration})); + EXPECT_CALL(mock, AllowedLocations) + .WillOnce(Return(internal::DeadlineExceededError("RPC took too long"))); + + auto auth_headers = mock.AuthenticationHeaders( + std::chrono::system_clock::now(), "my-endpoint"); + EXPECT_THAT(auth_headers, + IsOkAndHolds(::testing::ElementsAre(rest_internal::HttpHeader( + "authorization", "Bearer test-token")))); } } // namespace diff --git a/google/cloud/internal/oauth2_minimal_iam_credentials_rest.cc b/google/cloud/internal/oauth2_minimal_iam_credentials_rest.cc index 46dd95d2d0fa2..38dfc2eeb59b7 100644 --- a/google/cloud/internal/oauth2_minimal_iam_credentials_rest.cc +++ b/google/cloud/internal/oauth2_minimal_iam_credentials_rest.cc @@ -16,6 +16,7 @@ #include "google/cloud/common_options.h" #include "google/cloud/internal/api_client_header.h" #include "google/cloud/internal/format_time_point.h" +#include "google/cloud/internal/http_header.h" #include "google/cloud/internal/json_parsing.h" #include "google/cloud/internal/make_status.h" #include "google/cloud/internal/oauth2_credentials.h" @@ -32,6 +33,15 @@ namespace google { namespace cloud { namespace oauth2_internal { GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +std::string IamCredentialsEndpoint( + StatusOr const& universe_domain) { + return absl::StrCat("https://iamcredentials.", + universe_domain ? *universe_domain : "googleapis.com"); +} + +} // namespace using ::google::cloud::internal::InvalidArgumentError; @@ -45,12 +55,14 @@ MinimalIamCredentialsRestStub::MinimalIamCredentialsRestStub( StatusOr MinimalIamCredentialsRestStub::GenerateAccessToken( GenerateAccessTokenRequest const& request) { - auto auth_header = - credentials_->AuthenticationHeader(std::chrono::system_clock::now()); - if (!auth_header) return std::move(auth_header).status(); - + auto auth_headers = credentials_->AuthenticationHeaders( + std::chrono::system_clock::now(), + IamCredentialsEndpoint(universe_domain(Options{}))); + if (!auth_headers) return std::move(auth_headers).status(); rest_internal::RestRequest rest_request; - rest_request.AddHeader(auth_header.value()); + for (auto const& auth_header : *auth_headers) { + rest_request.AddHeader(auth_header); + } rest_request.AddHeader("Content-Type", "application/json"); rest_request.SetPath(MakeRequestPath(request)); nlohmann::json payload{ @@ -73,12 +85,44 @@ MinimalIamCredentialsRestStub::GenerateAccessToken( std::string MinimalIamCredentialsRestStub::MakeRequestPath( GenerateAccessTokenRequest const& request) const { - auto ud = universe_domain(Options{}); - return absl::StrCat("https://iamcredentials.", ud ? *ud : "googleapis.com", + return absl::StrCat(IamCredentialsEndpoint(universe_domain(Options{})), "/v1/projects/-/serviceAccounts/", request.service_account, ":generateAccessToken"); } +StatusOr +MinimalIamCredentialsRestStub::AllowedLocations( + AllowedLocationsRequest const& request) { + auto auth_headers = credentials_->AuthenticationHeaders( + std::chrono::system_clock::now(), + IamCredentialsEndpoint(universe_domain(Options{}))); + if (!auth_headers) return std::move(auth_headers).status(); + rest_internal::RestRequest rest_request; + for (auto const& auth_header : *auth_headers) { + rest_request.AddHeader(auth_header); + } + rest_request.AddHeader("Content-Type", "application/json"); + rest_request.SetPath(MakeRequestPath(request)); + nlohmann::json payload{}; + + auto client = client_factory_(options_); + rest_internal::RestContext context; + auto response = client->Post(context, rest_request, {payload.dump()}); + if (!response) return std::move(response).status(); + return ParseAllowedLocationsResponse( + **response, + internal::ErrorContext( + {{"gcloud-cpp.root.class", "MinimalIamCredentialsRestStub"}, + {"gcloud-cpp.root.function", __func__}, + {"path", rest_request.path()}})); +} + +std::string MinimalIamCredentialsRestStub::MakeRequestPath( + AllowedLocationsRequest const& request) const { + return absl::StrCat(IamCredentialsEndpoint(universe_domain(Options{})), + "/v1/", request.path); +} + MinimalIamCredentialsRestLogging::MinimalIamCredentialsRestLogging( std::shared_ptr child) : child_(std::move(child)) {} @@ -132,6 +176,29 @@ StatusOr ParseGenerateAccessTokenResponse( return google::cloud::AccessToken{*std::move(token), *expire_time}; } +StatusOr ParseAllowedLocationsResponse( + rest_internal::RestResponse& response, + google::cloud::internal::ErrorContext const& ec) { + if (IsHttpError(response)) return AsStatus(std::move(response)); + auto response_payload = + rest_internal::ReadAll(std::move(response).ExtractPayload()); + if (!response_payload) return std::move(response_payload).status(); + auto parsed = nlohmann::json::parse(*response_payload, nullptr, false); + if (!parsed.is_object()) { + return InvalidArgumentError("cannot parse response as a JSON object", + GCP_ERROR_INFO().WithContext(ec)); + } + auto locations = ValidateStringArrayField(parsed, "locations", + "AllowedLocations() response", ec); + if (!locations) return std::move(locations).status(); + + auto encoded_locations = ValidateStringField( + parsed, "encodedLocations", "AllowedLocations() response", ec); + if (!encoded_locations) return std::move(encoded_locations).status(); + return AllowedLocationsResponse{*std::move(locations), + *std::move(encoded_locations)}; +} + std::shared_ptr MakeMinimalIamCredentialsRestStub( std::shared_ptr credentials, Options options, HttpClientFactory client_factory) { diff --git a/google/cloud/internal/oauth2_minimal_iam_credentials_rest.h b/google/cloud/internal/oauth2_minimal_iam_credentials_rest.h index 0c1a8e241c5f9..7e2424e5c7a45 100644 --- a/google/cloud/internal/oauth2_minimal_iam_credentials_rest.h +++ b/google/cloud/internal/oauth2_minimal_iam_credentials_rest.h @@ -39,11 +39,24 @@ struct GenerateAccessTokenRequest { std::vector delegates; }; +struct AllowedLocationsRequest { + std::string path; +}; + +struct AllowedLocationsResponse { + std::vector locations; + std::string encoded_locations; +}; + /// Parse the HTTP response from a `GenerateAccessToken()` call. StatusOr ParseGenerateAccessTokenResponse( rest_internal::RestResponse& response, google::cloud::internal::ErrorContext const& ec); +StatusOr ParseAllowedLocationsResponse( + rest_internal::RestResponse& response, + google::cloud::internal::ErrorContext const& ec); + /** * Wrapper for IAM Credentials intended for use with * `ImpersonateServiceAccountCredentials`. @@ -55,6 +68,11 @@ class MinimalIamCredentialsRest { virtual StatusOr GenerateAccessToken( GenerateAccessTokenRequest const& request) = 0; + virtual StatusOr AllowedLocations( + AllowedLocationsRequest const& request) { + return StatusOr{}; + } + virtual StatusOr universe_domain( Options const& options) const = 0; }; @@ -78,12 +96,16 @@ class MinimalIamCredentialsRestStub : public MinimalIamCredentialsRest { StatusOr GenerateAccessToken( GenerateAccessTokenRequest const& request) override; + StatusOr AllowedLocations( + AllowedLocationsRequest const& request) override; + StatusOr universe_domain(Options const& options) const override { return credentials_->universe_domain(options); } private: std::string MakeRequestPath(GenerateAccessTokenRequest const& request) const; + std::string MakeRequestPath(AllowedLocationsRequest const& request) const; std::shared_ptr credentials_; Options options_; diff --git a/google/cloud/internal/oauth2_regional_access_boundary_token_manager.cc b/google/cloud/internal/oauth2_regional_access_boundary_token_manager.cc new file mode 100644 index 0000000000000..1133a32a24c8b --- /dev/null +++ b/google/cloud/internal/oauth2_regional_access_boundary_token_manager.cc @@ -0,0 +1,126 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "google/cloud/internal/oauth2_regional_access_boundary_token_manager.h" +#include "google/cloud/internal/async_rest_retry_loop.h" +// #include "google/cloud/internal/make_status.h" +// #include "google/cloud/internal/oauth2_universe_domain.h" +// #include "absl/strings/str_cat.h" +// #include "absl/strings/str_join.h" + +namespace google { +namespace cloud { +namespace oauth2_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +auto constexpr kTokenTtl = std::chrono::seconds(6 * 3600); +auto constexpr kTtlGracePeriod = std::chrono::seconds(3600); +auto constexpr kFailedLookupCooldown = std::chrono::seconds(900); +auto constexpr kMaximumRetryDuration = std::chrono::seconds(60); +auto constexpr kInitialBackoffDelay = std::chrono::seconds(1); +auto constexpr kMaximumBackoffDelay = std::chrono::seconds(5); +auto constexpr kBackoffScaling = 2.0; + +} // namespace + +RegionalAccessBoundaryTokenManager::RegionalAccessBoundaryTokenManager( + std::shared_ptr credentials, + rest_internal::RestPureCompletionQueue cq, HttpClientFactory client_factory, + std::shared_ptr clock) + : cq_(std::move(cq)), + clock_(std::move(clock)), + retry_policy_(std::make_unique>( + kMaximumRetryDuration)), + backoff_policy_(std::make_unique( + kInitialBackoffDelay, kMaximumBackoffDelay, kBackoffScaling)), + iam_stub_(MakeMinimalIamCredentialsRestStub(std::move(credentials), {}, + std::move(client_factory))) {} + +bool RegionalAccessBoundaryTokenManager::DoesEndpointRequireToken( + std::string_view endpoint) { + return absl::EndsWithIgnoreCase(endpoint, ".googleapis.com") && + !absl::EndsWithIgnoreCase(endpoint, ".rep.googleapis.com") && + !absl::EndsWithIgnoreCase(endpoint, (".rep.sandbox.googleapis.com")); +} + +bool RegionalAccessBoundaryTokenManager::IsTokenValid( + std::chrono::system_clock::time_point tp) const { + return !token_.empty() && tp < expire_time_; +} + +StatusOr +RegionalAccessBoundaryTokenManager::GetServiceAccountToken( + std::string_view sa_email, std::chrono::system_clock::time_point tp, + std::string_view endpoint) { + // If the endpoint does not need a token, return immediately. + if (!DoesEndpointRequireToken(endpoint)) return Token{}; + + std::scoped_lock lock(mu_); + // check to see if we're near expiry and if so, start refresh process + if (tp > expire_time_ - kTtlGracePeriod) { + // do refresh + } + + if (IsTokenValid(tp)) return token_; + + // do refresh + + return Token{}; +} + +void RegionalAccessBoundaryTokenManager::RefreshToken( + std::string_view iam_path_suffix) { + std::scoped_lock lock(mu_); + if (failed_lookup_cooldown_.valid() && !failed_lookup_cooldown_.is_ready()) { + return; + } + std::string iam_path{iam_path_suffix}; + auto fn = [iam_stub = iam_stub_, iam_path = std::move(iam_path)]( + rest_internal::RestPureCompletionQueue&, + std::unique_ptr, + google::cloud::internal::ImmutableOptions, + TokenRefreshRequest const&) -> future> { + auto response = iam_stub->AllowedLocations({iam_path}); + if (!response) + return make_ready_future(StatusOr(std::move(response).status())); + return make_ready_future( + StatusOr{Token{response->encoded_locations}}); + }; + + TokenRefreshRequest request; + auto r = rest_internal::AsyncRestRetryLoop( + retry_policy_->clone(), backoff_policy_->clone(), + Idempotency::kIdempotent, cq_, fn, + google::cloud::internal::ImmutableOptions{}, request, __func__) + .then([weak = weak_from_this()](auto f) -> void { + auto new_token = f.get(); + auto manager = weak.lock(); + if (!manager) return; + if (new_token.ok()) { + manager->token_ = *new_token; + manager->expire_time_ = manager->clock_->Now() + kTokenTtl; + } else { + manager->token_ = Token{}; + manager->failed_lookup_cooldown_ = + manager->cq_.MakeRelativeTimer(kFailedLookupCooldown); + } + }); +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace oauth2_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/internal/oauth2_regional_access_boundary_token_manager.h b/google/cloud/internal/oauth2_regional_access_boundary_token_manager.h new file mode 100644 index 0000000000000..766d655c3a40e --- /dev/null +++ b/google/cloud/internal/oauth2_regional_access_boundary_token_manager.h @@ -0,0 +1,115 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_OAUTH2_REGIONAL_ACCESS_BOUNDARY_TOKEN_MANAGER_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_OAUTH2_REGIONAL_ACCESS_BOUNDARY_TOKEN_MANAGER_H + +#include "google/cloud/backoff_policy.h" +#include "google/cloud/internal/clock.h" +#include "google/cloud/internal/oauth2_minimal_iam_credentials_rest.h" +#include "google/cloud/internal/rest_pure_completion_queue_impl.h" +#include "google/cloud/internal/retry_policy_impl.h" +// #include "google/cloud/access_token.h" +#include "google/cloud/internal/http_header.h" +#include "google/cloud/options.h" +// #include "google/cloud/status.h" +#include "google/cloud/status_or.h" +#include "google/cloud/version.h" +#include "absl/strings/match.h" +#include +// #include +#include +// #include + +namespace google { +namespace cloud { +namespace oauth2_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +/** + * Manages fetching and caching routing tokens for RAB. + * + * Regional endpoints (those ending with ".rep.googleapis.com" or + * ".rep.sandbox.googleapis.com") do not require a routing token. Non-GDU + * endpoints, likewise do not require a routing token. + * + * The routing tokens are refreshed asynchronously by a background thread. + * + * Supported credential types: + * - Service Account + * - Impersonated Service Account + * - Workload Identity Federation + * - Workforce Identity Federation + * - Self Signed JWT + * + * Supported environments: + * - GDU only + */ +class RegionalAccessBoundaryTokenManager + : public std::enable_shared_from_this { + public: + using Clock = ::google::cloud::internal::SystemClock; + + struct Token { + Token() = default; + explicit Token(std::string token) : token(std::move(token)) {} + bool empty() const { return token.empty(); } + std::string token; + }; + + struct RetryTraits { + static bool IsPermanentFailure(Status const& s) { + // Retry only on 500, 502, 503, and 504. + return false; + } + }; + + struct TokenRefreshRequest {}; + + RegionalAccessBoundaryTokenManager( + std::shared_ptr credentials, + rest_internal::RestPureCompletionQueue cq, + HttpClientFactory client_factory, + std::shared_ptr clock = std::make_shared()); + + static bool DoesEndpointRequireToken(std::string_view endpoint); + + bool IsTokenValid(std::chrono::system_clock::time_point tp) const; + + StatusOr GetServiceAccountToken( + std::string_view sa_email, std::chrono::system_clock::time_point tp, + std::string_view endpoint); + + void RefreshToken(std::string_view iam_path_suffix); + + private: + std::mutex mu_; + rest_internal::RestPureCompletionQueue cq_; + std::shared_ptr clock_; + std::unique_ptr> retry_policy_; + std::unique_ptr backoff_policy_; + std::shared_ptr iam_stub_; + + future> + failed_lookup_cooldown_; + Token token_; + std::chrono::system_clock::time_point expire_time_; +}; + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace oauth2_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_OAUTH2_REGIONAL_ACCESS_BOUNDARY_TOKEN_MANAGER_H diff --git a/google/cloud/internal/oauth2_regional_access_boundary_token_manager_test.cc b/google/cloud/internal/oauth2_regional_access_boundary_token_manager_test.cc new file mode 100644 index 0000000000000..409efcdacffc5 --- /dev/null +++ b/google/cloud/internal/oauth2_regional_access_boundary_token_manager_test.cc @@ -0,0 +1,28 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "google/cloud/internal/oauth2_regional_access_boundary_token_manager.h" +// #include "google/cloud/internal/make_status.h" +// #include "google/cloud/testing_util/status_matchers.h" +#include + +namespace google { +namespace cloud { +namespace oauth2_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace {} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace oauth2_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/internal/oauth2_service_account_credentials.cc b/google/cloud/internal/oauth2_service_account_credentials.cc index 849f572211bfe..8cade50fc04e0 100644 --- a/google/cloud/internal/oauth2_service_account_credentials.cc +++ b/google/cloud/internal/oauth2_service_account_credentials.cc @@ -370,6 +370,15 @@ StatusOr ServiceAccountCredentials::project_id( return project_id(); } +StatusOr ServiceAccountCredentials::AllowedLocations( + std::chrono::system_clock::time_point tp, std::string_view endpoint) { + auto token = rab_token_manager_->GetServiceAccountToken(info_.client_email, + tp, endpoint); + if (!token.ok()) return std::move(token.status()); + if (token->empty()) return rest_internal::HttpHeader{}; + return rest_internal::HttpHeader{"x-allowed-locations", token->token}; +} + bool ServiceAccountUseOAuth(ServiceAccountCredentialsInfo const& info) { // Custom universe domains are only supported with JWT, not OAuth tokens. if (info.universe_domain.has_value() && diff --git a/google/cloud/internal/oauth2_service_account_credentials.h b/google/cloud/internal/oauth2_service_account_credentials.h index 6a62388c18ec9..1510a90e9275c 100644 --- a/google/cloud/internal/oauth2_service_account_credentials.h +++ b/google/cloud/internal/oauth2_service_account_credentials.h @@ -18,6 +18,7 @@ #include "google/cloud/internal/oauth2_credential_constants.h" #include "google/cloud/internal/oauth2_credentials.h" #include "google/cloud/internal/oauth2_http_client_factory.h" +#include "google/cloud/internal/oauth2_regional_access_boundary_token_manager.h" #include "google/cloud/optional.h" #include "google/cloud/status_or.h" #include "google/cloud/version.h" @@ -293,6 +294,10 @@ class ServiceAccountCredentials : public oauth2_internal::Credentials { StatusOr project_id() const override; StatusOr project_id(Options const&) const override; + StatusOr AllowedLocations( + std::chrono::system_clock::time_point tp, + std::string_view endpoint) override; + private: bool UseOAuth(); StatusOr GetTokenOAuth( @@ -303,6 +308,7 @@ class ServiceAccountCredentials : public oauth2_internal::Credentials { ServiceAccountCredentialsInfo info_; Options options_; HttpClientFactory client_factory_; + std::shared_ptr rab_token_manager_; }; GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END diff --git a/google/cloud/internal/opentelemetry.h b/google/cloud/internal/opentelemetry.h index bd4ffe84ee42f..738cec11ef222 100644 --- a/google/cloud/internal/opentelemetry.h +++ b/google/cloud/internal/opentelemetry.h @@ -257,6 +257,24 @@ std::function)> MakeTracedSleeper( void AddSpanAttribute(Options const& options, std::string const& key, std::string const& value); +/** + * Returns a traced timer, if OpenTelemetry tracing is enabled. + */ +template +future> TracedAsyncBackoff( + CompletionQueueType& cq, Options const& options, + std::chrono::duration duration, std::string const& name) { + if (TracingEnabled(options)) { + auto span = MakeSpan(name); + OTelScope scope(span); + auto timer = cq.MakeRelativeTimer(duration); + return EndSpan(std::move(span), std::move(timer)); + } + (void)options; + (void)name; + return cq.MakeRelativeTimer(duration); +} + } // namespace internal GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace cloud diff --git a/google/cloud/internal/rest_context.cc b/google/cloud/internal/rest_context.cc index 4c128564aa9f4..7d8eb3acdaa78 100644 --- a/google/cloud/internal/rest_context.cc +++ b/google/cloud/internal/rest_context.cc @@ -13,35 +13,29 @@ // limitations under the License. #include "google/cloud/internal/rest_context.h" +#include "absl/strings/strip.h" #include -#include namespace google { namespace cloud { namespace rest_internal { GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN -RestContext& RestContext::AddHeader(std::string header, std::string value) & { - std::transform(header.begin(), header.end(), header.begin(), - [](unsigned char c) { return std::tolower(c); }); - auto iter = headers_.find(header); +RestContext& RestContext::AddHeader(HttpHeader header) & { + auto iter = headers_.find(header.name()); if (iter == headers_.end()) { - std::vector v = {std::move(value)}; - headers_.emplace(std::move(header), std::move(v)); + headers_.emplace(header.name(), std::move(header)); } else { - iter->second.push_back(value); + iter->second.MergeHeader(std::move(header)); } return *this; } -RestContext& RestContext::AddHeader( - std::pair header) & { - return AddHeader(std::move(header.first), std::move(header.second)); +RestContext& RestContext::AddHeader(std::string header, std::string value) & { + return AddHeader(HttpHeader(std::move(header), std::move(value))); } -std::vector RestContext::GetHeader(std::string header) const { - std::transform(header.begin(), header.end(), header.begin(), - [](unsigned char c) { return std::tolower(c); }); +HttpHeader RestContext::GetHeader(HttpHeaderName const& header) const { auto iter = headers_.find(header); if (iter == headers_.end()) { return {}; diff --git a/google/cloud/internal/rest_context.h b/google/cloud/internal/rest_context.h index 5f19a9e191d36..780207cb48113 100644 --- a/google/cloud/internal/rest_context.h +++ b/google/cloud/internal/rest_context.h @@ -15,15 +15,13 @@ #ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_REST_CONTEXT_H #define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_REST_CONTEXT_H +#include "google/cloud/internal/http_header.h" #include "google/cloud/options.h" #include "google/cloud/version.h" #include "absl/types/optional.h" #include -#include #include -#include #include -#include namespace google { namespace cloud { @@ -36,7 +34,6 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN */ class RestContext { public: - using HttpHeaders = std::unordered_map>; RestContext() = default; explicit RestContext(Options options, HttpHeaders headers) : options_(std::move(options)), headers_(std::move(headers)) {} @@ -50,18 +47,17 @@ class RestContext { // Adding a header/value pair that already exists results in the new value // appended to the list of values for the existing header. + RestContext& AddHeader(HttpHeader header) &; + RestContext&& AddHeader(HttpHeader header) && { + return std::move(AddHeader(std::move(header))); + } RestContext& AddHeader(std::string header, std::string value) &; RestContext&& AddHeader(std::string header, std::string value) && { return std::move(AddHeader(std::move(header), std::move(value))); } - RestContext& AddHeader(std::pair header) &; - RestContext&& AddHeader(std::pair header) && { - return std::move(AddHeader(std::move(header))); - } - // Vector is empty if header name is not found. // Header names are case-insensitive; header values are case-sensitive. - std::vector GetHeader(std::string header) const; + HttpHeader GetHeader(HttpHeaderName const& header) const; absl::optional local_ip_address() const { return local_ip_address_; diff --git a/google/cloud/internal/rest_context_test.cc b/google/cloud/internal/rest_context_test.cc index edc46bc91972c..82fd3c854b866 100644 --- a/google/cloud/internal/rest_context_test.cc +++ b/google/cloud/internal/rest_context_test.cc @@ -30,17 +30,17 @@ using ::testing::Pair; class RestContextTest : public ::testing::Test { protected: void SetUp() override { - headers_["header1"] = {"value1"}; - headers_["header2"] = {"value2a", "value2b"}; + headers_["header1"] = HttpHeader{"header1", "value1"}; + headers_["header2"] = HttpHeader{"header2", {"value2a", "value2b"}}; } - RestContext::HttpHeaders headers_; + HttpHeaders headers_; }; TEST_F(RestContextTest, RvalueBuilder) { auto context = RestContext() .AddHeader("header1", "value1") - .AddHeader(std::make_pair("header2", "value2a")) + .AddHeader(HttpHeader("header2", "value2a")) .AddHeader("header2", "value2b"); EXPECT_THAT(context.headers(), Contains(Pair("header1", ElementsAre("value1")))); diff --git a/google/cloud/internal/rest_opentelemetry.cc b/google/cloud/internal/rest_opentelemetry.cc index aaf2034dcd2e8..4d5d1d839b194 100644 --- a/google/cloud/internal/rest_opentelemetry.cc +++ b/google/cloud/internal/rest_opentelemetry.cc @@ -81,16 +81,16 @@ opentelemetry::nostd::shared_ptr MakeSpanHttp( {/*sc::kUrlFull=*/"url.full", request.path()}}, options); for (auto const& kv : request.headers()) { - auto const name = "http.request.header." + kv.first; - if (kv.second.empty()) { + auto const name = "http.request.header." + std::string{kv.first}; + if (kv.second.EmptyValues()) { span->SetAttribute(name, ""); continue; } if (absl::EqualsIgnoreCase(kv.first, "authorization")) { - span->SetAttribute(name, kv.second.front().substr(0, 32)); + span->SetAttribute(name, kv.second.values().front().substr(0, 32)); continue; } - span->SetAttribute(name, kv.second.front()); + span->SetAttribute(name, kv.second.values().front()); } return span; } diff --git a/google/cloud/internal/rest_opentelemetry_test.cc b/google/cloud/internal/rest_opentelemetry_test.cc index 672e60ab86f03..114a731871a76 100644 --- a/google/cloud/internal/rest_opentelemetry_test.cc +++ b/google/cloud/internal/rest_opentelemetry_test.cc @@ -42,7 +42,7 @@ TEST(RestOpentelemetry, MakeSpanHttp) { auto span_catcher = InstallSpanCatcher(); auto constexpr kUrl = "https://storage.googleapis.com/storage/v1/b/my-bucket"; - RestRequest request(kUrl, RestRequest::HttpHeaders{{"empty", {}}}); + RestRequest request(kUrl, HttpHeaders{{"empty", {}}}); auto const secret = std::string{"Bearer secret-0123456789aaaaabbbbbcccccddddd"}; ASSERT_THAT(secret.size(), Gt(static_cast(32))); @@ -81,7 +81,7 @@ TEST(RestOpentelemetry, MakeSpanHttp) { TEST(RestOpentelemetry, InjectTraceContext) { auto span_catcher = InstallSpanCatcher(); auto constexpr kUrl = "https://storage.googleapis.com/storage/v1/b/my-bucket"; - RestRequest request(kUrl, RestRequest::HttpHeaders{{"empty", {}}}); + RestRequest request(kUrl, HttpHeaders{{"empty", {}}}); auto span = MakeSpanHttp(request, "GET"); auto scope = opentelemetry::trace::Scope(span); diff --git a/google/cloud/internal/rest_pure_background_threads_impl.h b/google/cloud/internal/rest_pure_background_threads_impl.h new file mode 100644 index 0000000000000..0254e5a89f1fd --- /dev/null +++ b/google/cloud/internal/rest_pure_background_threads_impl.h @@ -0,0 +1,56 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_REST_PURE_BACKGROUND_THREADS_IMPL_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_REST_PURE_BACKGROUND_THREADS_IMPL_H + +#include "google/cloud/internal/rest_pure_completion_queue_impl.h" +#include "google/cloud/version.h" +#include +#include + +namespace google { +namespace cloud { +namespace rest_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +class RestPureBackgroundThreads { + virtual ~RestPureBackgroundThreads() = default; + + /// The completion queue used for the background operations. + virtual RestPureCompletionQueue cq() const = 0; +}; + +class RestPureAutomaticallyCreatedBackgroundThreads + : public RestPureBackgroundThreads { + public: + explicit RestPureAutomaticallyCreatedBackgroundThreads( + std::size_t thread_count = 1U); + ~RestPureAutomaticallyCreatedBackgroundThreads() override; + + RestPureCompletionQueue cq() const override { return cq_; } + void Shutdown(); + std::size_t pool_size() const { return pool_.size(); } + + private: + RestPureCompletionQueue cq_; + std::vector pool_; +}; + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace rest_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_REST_PURE_BACKGROUND_THREADS_IMPL_H diff --git a/google/cloud/internal/rest_pure_completion_queue_impl.cc b/google/cloud/internal/rest_pure_completion_queue_impl.cc new file mode 100644 index 0000000000000..0a2d9fc324d01 --- /dev/null +++ b/google/cloud/internal/rest_pure_completion_queue_impl.cc @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "google/cloud/internal/rest_pure_completion_queue_impl.h" +#include "google/cloud/internal/timer_queue.h" + +namespace google { +namespace cloud { +namespace rest_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +RestPureCompletionQueueImpl::RestPureCompletionQueueImpl() + : tq_(internal::TimerQueue::Create()) {} + +void RestPureCompletionQueueImpl::Run() { tq_->Service(); } + +void RestPureCompletionQueueImpl::Shutdown() { tq_->Shutdown(); } + +void RestPureCompletionQueueImpl::CancelAll() {} + +future> +RestPureCompletionQueueImpl::MakeDeadlineTimer( + std::chrono::system_clock::time_point deadline) { + return tq_->Schedule(deadline); +} + +future> +RestPureCompletionQueueImpl::MakeRelativeTimer( + std::chrono::nanoseconds duration) { + using std::chrono::system_clock; + auto d = std::chrono::duration_cast(duration); + if (d < duration) d += system_clock::duration{1}; + return MakeDeadlineTimer(system_clock::now() + d); +} + +// Use an "immediately" expiring timer in order to get the thread(s) servicing +// the TimerQueue to execute the function. However, if the timer +// expires before .then() is invoked, the lambda will be immediately called and +// the passing of execution to the queue servicing thread will not occur. +void RestPureCompletionQueueImpl::RunAsync( + std::unique_ptr function) { + ++run_async_counter_; + tq_->Schedule([f = std::move(function)](auto) { f->exec(); }); +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace rest_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/internal/rest_pure_completion_queue_impl.h b/google/cloud/internal/rest_pure_completion_queue_impl.h new file mode 100644 index 0000000000000..ed666d413a3e9 --- /dev/null +++ b/google/cloud/internal/rest_pure_completion_queue_impl.h @@ -0,0 +1,262 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_REST_PURE_COMPLETION_QUEUE_IMPL_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_REST_PURE_COMPLETION_QUEUE_IMPL_H + +#include "google/cloud/future.h" +#include "google/cloud/internal/timer_queue.h" +#include "google/cloud/log.h" +#include "google/cloud/status_or.h" +#include "google/cloud/version.h" +#include +#include +#include + +namespace google { +namespace cloud { +namespace rest_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +struct RunAsyncBase { + virtual ~RunAsyncBase() = default; + virtual void exec() = 0; +}; + +/** + * Implementation for CompletionQueue that does NOT use a grpc::CompletionQueue. + * + * Due to the lack of a completion queue that can manage multiple, simultaneous + * REST requests, asynchronous calls should be launched on a thread of their own + * and RunAsync should only be called with a function to join that thread after + * it completes its work. + */ +class RestPureCompletionQueueInterface { + public: + virtual ~RestPureCompletionQueueInterface() = default; + + /// Run the event loop until Shutdown() is called. + virtual void Run() = 0; + + /// Terminate the event loop. + virtual void Shutdown() = 0; + + /// Cancel all existing operations. + virtual void CancelAll() = 0; + + /// Create a new timer. + virtual future> + MakeDeadlineTimer(std::chrono::system_clock::time_point deadline) = 0; + + /// Create a new timer. + virtual future> + MakeRelativeTimer(std::chrono::nanoseconds duration) = 0; + + /// Enqueue a new asynchronous function. + virtual void RunAsync(std::unique_ptr function) = 0; +}; + +class RestPureCompletionQueue; + +std::shared_ptr +GetRestPureCompletionQueueImpl(RestPureCompletionQueue const& cq); +std::shared_ptr +GetRestPureCompletionQueueImpl(RestPureCompletionQueue&& cq); + +template +using CheckRunAsyncCallback = + google::cloud::internal::is_invocable; + +class RestPureCompletionQueue { + public: + RestPureCompletionQueue(); + explicit RestPureCompletionQueue( + std::shared_ptr impl) + : impl_(std::move(impl)) {} + + /** + * Run the completion queue event loop. + * + * Note that more than one thread can call this member function, to create a + * pool of threads completing asynchronous operations. + */ + void Run() { impl_->Run(); } + + /// Terminate the completion queue event loop. + void Shutdown() { impl_->Shutdown(); } + + /// Cancel all pending operations. + void CancelAll() { impl_->CancelAll(); } + + /** + * Create a timer that fires at @p deadline. + * + * @param deadline when should the timer expire. + * + * @return a future that becomes satisfied after @p deadline. + * The result of the future is the time at which it expired, or an error + * Status if the timer did not run to expiration (e.g. it was cancelled). + */ + google::cloud::future> + MakeDeadlineTimer(std::chrono::system_clock::time_point deadline) { + return impl_->MakeDeadlineTimer(deadline); + } + + /** + * Create a timer that fires after the @p duration. + * + * @tparam Rep a placeholder to match the Rep tparam for @p duration type, + * the semantics of this template parameter are documented in + * `std::chrono::duration<>` (in brief, the underlying arithmetic type + * used to store the number of ticks), for our purposes it is simply a + * formal parameter. + * @tparam Period a placeholder to match the Period tparam for @p duration + * type, the semantics of this template parameter are documented in + * `std::chrono::duration<>` (in brief, the length of the tick in seconds, + * expressed as a `std::ratio<>`), for our purposes it is simply a formal + * parameter. + * + * @param duration when should the timer expire relative to the current time. + * + * @return a future that becomes satisfied after @p duration time has elapsed. + * The result of the future is the time at which it expired, or an error + * Status if the timer did not run to expiration (e.g. it was cancelled). + */ + template + future> MakeRelativeTimer( + std::chrono::duration duration) { + return impl_->MakeRelativeTimer( + std::chrono::duration_cast(duration)); + } + + /** + * Asynchronously run a functor on a thread `Run()`ning the `CompletionQueue`. + * + * @param functor the functor to invoke in one of the CompletionQueue's + * threads. + * + * @tparam Functor the type of @p functor. It must satisfy + * `std::is_invocable` + */ + template ::value, int> = 0 + /// @endcond + > + void RunAsync(Functor&& functor) { + class Wrapper : public RunAsyncBase { + public: + Wrapper(std::weak_ptr impl, Functor&& f) + : impl_(std::move(impl)), fun_(std::forward(f)) {} + ~Wrapper() override = default; + void exec() override { + auto impl = impl_.lock(); + if (!impl) return; + RestPureCompletionQueue cq(std::move(impl)); + fun_(cq); + } + + private: + std::weak_ptr impl_; + std::decay_t fun_; + }; + impl_->RunAsync( + std::make_unique(impl_, std::forward(functor))); + } + + /** + * Asynchronously run a functor on a thread `Run()`ning the `CompletionQueue`. + * + * @param functor the functor to call in one of the CompletionQueue's threads. + * @tparam Functor the type of @p functor. It must satisfy + * `std::is_invocable`. + */ + template ::value, int> = 0 + /// @endcond + > + void RunAsync(Functor&& functor) { + class Wrapper : public RunAsyncBase { + public: + explicit Wrapper(Functor&& f) : fun_(std::forward(f)) {} + ~Wrapper() override = default; + void exec() override { fun_(); } + + private: + std::decay_t fun_; + }; + impl_->RunAsync(std::make_unique(std::forward(functor))); + } + + private: + friend std::shared_ptr + GetRestPureCompletionQueueImpl(RestPureCompletionQueue const& cq); + friend std::shared_ptr + GetRestPureCompletionQueueImpl(RestPureCompletionQueue&& cq); + std::shared_ptr impl_; +}; + +inline std::shared_ptr +GetRestPureCompletionQueueImpl(RestPureCompletionQueue const& cq) { + return cq.impl_; +} + +inline std::shared_ptr +GetRestPureCompletionQueueImpl(RestPureCompletionQueue&& cq) { + return std::move(cq.impl_); +} + +class RestPureCompletionQueueImpl final + : public RestPureCompletionQueueInterface, + public std::enable_shared_from_this { + public: + ~RestPureCompletionQueueImpl() override = default; + RestPureCompletionQueueImpl(); + + /// Run the event loop until Shutdown() is called. + void Run() override; + + /// Terminate the event loop. + void Shutdown() override; + + /// Cancel all existing operations. + void CancelAll() override; + + /// Create a new timer. + future> MakeDeadlineTimer( + std::chrono::system_clock::time_point deadline) override; + + /// Create a new timer. + future> MakeRelativeTimer( + std::chrono::nanoseconds duration) override; + + /// Enqueue a new asynchronous function. + void RunAsync(std::unique_ptr function) override; + + /// Some counters for testing and debugging. + std::int64_t run_async_counter() const { return run_async_counter_.load(); } + + private: + std::shared_ptr tq_; + // These are metrics used in testing. + std::atomic run_async_counter_{0}; +}; + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace rest_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_REST_PURE_COMPLETION_QUEUE_IMPL_H diff --git a/google/cloud/internal/rest_request.cc b/google/cloud/internal/rest_request.cc index 778fc673b6d82..cee621e5f83b5 100644 --- a/google/cloud/internal/rest_request.cc +++ b/google/cloud/internal/rest_request.cc @@ -23,33 +23,19 @@ namespace google { namespace cloud { namespace rest_internal { GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN -namespace { - -// NOLINTNEXTLINE(performance-unnecessary-value-param) -RestRequest::HttpHeaders NormalizeHeaders(RestRequest::HttpHeaders headers) { - RestRequest::HttpHeaders result; - for (auto& kv : headers) { - auto& entry = result[absl::AsciiStrToLower(kv.first)]; - entry.insert(entry.end(), std::make_move_iterator(kv.second.begin()), - std::make_move_iterator(kv.second.end())); - } - return result; -} - -} // namespace RestRequest::RestRequest() = default; RestRequest::RestRequest(std::string path) : path_(std::move(path)) {} RestRequest::RestRequest(RestContext const& rest_context) - : headers_(NormalizeHeaders(rest_context.headers())) {} + : headers_(rest_context.headers()) {} RestRequest::RestRequest(std::string path, HttpHeaders headers) - : path_(std::move(path)), headers_(NormalizeHeaders(std::move(headers))) {} + : path_(std::move(path)), headers_(std::move(headers)) {} RestRequest::RestRequest(std::string path, HttpParameters parameters) : path_(std::move(path)), parameters_(std::move(parameters)) {} RestRequest::RestRequest(std::string path, HttpHeaders headers, HttpParameters parameters) : path_(std::move(path)), - headers_(NormalizeHeaders(std::move(headers))), + headers_(std::move(headers)), parameters_(std::move(parameters)) {} RestRequest& RestRequest::SetPath(std::string path) & { @@ -64,21 +50,18 @@ RestRequest& RestRequest::AppendPath(std::string path) & { return *this; } -RestRequest& RestRequest::AddHeader(std::string header, std::string value) & { - absl::AsciiStrToLower(&header); - auto iter = headers_.find(header); +RestRequest& RestRequest::AddHeader(HttpHeader header) & { + auto iter = headers_.find(header.name()); if (iter == headers_.end()) { - std::vector v = {std::move(value)}; - headers_.emplace(std::move(header), std::move(v)); + headers_.emplace(header.name(), std::move(header)); } else { - iter->second.push_back(value); + iter->second.MergeHeader(std::move(header)); } return *this; } -RestRequest& RestRequest::AddHeader( - std::pair header) & { - return AddHeader(std::move(header.first), std::move(header.second)); +RestRequest& RestRequest::AddHeader(std::string header, std::string value) & { + return AddHeader(HttpHeader(std::move(header), std::move(value))); } RestRequest& RestRequest::AddQueryParameter(std::string parameter, @@ -93,8 +76,7 @@ RestRequest& RestRequest::AddQueryParameter( std::move(parameter.second)); } -std::vector RestRequest::GetHeader(std::string header) const { - absl::AsciiStrToLower(&header); +HttpHeader RestRequest::GetHeader(HttpHeaderName const& header) const { auto iter = headers_.find(header); if (iter == headers_.end()) { return {}; diff --git a/google/cloud/internal/rest_request.h b/google/cloud/internal/rest_request.h index 4fb4ad052bb76..44150be1928b6 100644 --- a/google/cloud/internal/rest_request.h +++ b/google/cloud/internal/rest_request.h @@ -15,10 +15,9 @@ #ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_REST_REQUEST_H #define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_REST_REQUEST_H +#include "google/cloud/internal/http_header.h" #include "google/cloud/internal/rest_context.h" -#include "google/cloud/status_or.h" #include "google/cloud/version.h" -#include #include namespace google { @@ -34,7 +33,6 @@ class RestClient; // payload if required. class RestRequest { public: - using HttpHeaders = std::unordered_map>; using HttpParameters = std::vector>; RestRequest(); @@ -60,14 +58,15 @@ class RestRequest { // Adding a header/value pair that already exists results in the new value // appended to the list of values for the existing header. + RestRequest& AddHeader(HttpHeader header) &; + RestRequest&& AddHeader(HttpHeader header) && { + return std::move(AddHeader(std::move(header))); + } + RestRequest& AddHeader(std::string header, std::string value) &; RestRequest&& AddHeader(std::string header, std::string value) && { return std::move(AddHeader(std::move(header), std::move(value))); } - RestRequest& AddHeader(std::pair header) &; - RestRequest&& AddHeader(std::pair header) && { - return std::move(AddHeader(std::move(header))); - } // Adding a duplicate param and or value results in both the new and original // pairs stored in order of addition. @@ -84,7 +83,7 @@ class RestRequest { // Vector is empty if header name is not found. // Header names are case-insensitive; header values are case-sensitive. - std::vector GetHeader(std::string header) const; + HttpHeader GetHeader(HttpHeaderName const& header) const; // Returns all values associated with parameter name. // Parameter names and values are case-sensitive. diff --git a/google/cloud/internal/rest_request_test.cc b/google/cloud/internal/rest_request_test.cc index 6c5ff0620e419..a498a913a0363 100644 --- a/google/cloud/internal/rest_request_test.cc +++ b/google/cloud/internal/rest_request_test.cc @@ -27,12 +27,12 @@ using ::testing::Eq; class RestRequestTest : public ::testing::Test { protected: void SetUp() override { - headers_["Header1"] = {"value1"}; - headers_["header2"] = {"value2a", "value2b"}; + headers_["header1"] = {"Header1", "value1"}; + headers_["header2"] = {"header2", {"value2a", "value2b"}}; parameters_.emplace_back("param1", "value1"); } - RestRequest::HttpHeaders headers_; + HttpHeaders headers_; RestRequest::HttpParameters parameters_; }; @@ -49,10 +49,10 @@ TEST_F(RestRequestTest, ConstructorPathHeaders) { EXPECT_TRUE(request.parameters().empty()); EXPECT_THAT( request.headers(), - Contains(std::make_pair("header1", std::vector{"value1"}))); + Contains(std::make_pair("header1", HttpHeader("header1", "value1")))); EXPECT_THAT(request.headers(), Contains(std::make_pair( - "header2", std::vector{"value2a", "value2b"}))); + "header2", HttpHeader("header2", {"value2a", "value2b"})))); } TEST_F(RestRequestTest, ConstructorPathParameters) { @@ -68,10 +68,10 @@ TEST_F(RestRequestTest, ConstructorPathHeadersParameters) { EXPECT_THAT(request.path(), Eq("foo/bar")); EXPECT_THAT( request.headers(), - Contains(std::make_pair("header1", std::vector{"value1"}))); + Contains(std::make_pair("header1", HttpHeader{"header1", "value1"}))); EXPECT_THAT(request.headers(), Contains(std::make_pair( - "header2", std::vector{"value2a", "value2b"}))); + "header2", HttpHeader{"header2", {"value2a", "value2b"}}))); EXPECT_THAT(request.parameters(), Contains(std::make_pair("param1", "value1"))); } @@ -80,17 +80,17 @@ TEST_F(RestRequestTest, RvalueBuilder) { auto request = RestRequest() .SetPath("foo/bar") .AddHeader("header1", "value1") - .AddHeader(std::make_pair("header2", "value2a")) + .AddHeader(HttpHeader("header2", "value2a")) .AddHeader("header2", "value2b") .AddQueryParameter("param1", "value1") .AddQueryParameter(std::make_pair("param2", "value2")); EXPECT_THAT(request.path(), Eq("foo/bar")); EXPECT_THAT( request.headers(), - Contains(std::make_pair("header1", std::vector{"value1"}))); + Contains(std::make_pair("header1", HttpHeader{"header1", "value1"}))); EXPECT_THAT(request.headers(), Contains(std::make_pair( - "header2", std::vector{"value2a", "value2b"}))); + "header2", HttpHeader{"header2", {"value2a", "value2b"}}))); ASSERT_THAT(request.parameters().size(), Eq(2)); EXPECT_THAT(request.parameters()[0], Eq(std::make_pair(std::string("param1"), std::string("value1")))); @@ -120,11 +120,11 @@ TEST_F(RestRequestTest, GetHeaderNotFound) { TEST_F(RestRequestTest, GetHeaderFound) { RestRequest request("foo/bar", headers_); auto result = request.GetHeader("Header1"); - EXPECT_THAT(result.size(), Eq(1)); - EXPECT_THAT(result, Contains("value1")); + EXPECT_THAT(result.values().size(), Eq(1)); + EXPECT_THAT(result.values(), Contains("value1")); result = request.GetHeader("header1"); - EXPECT_THAT(result.size(), Eq(1)); - EXPECT_THAT(result, Contains("value1")); + EXPECT_THAT(result.values().size(), Eq(1)); + EXPECT_THAT(result.values(), Contains("value1")); } TEST_F(RestRequestTest, GetQueryParameterNotFound) { diff --git a/google/cloud/internal/tracing_rest_client.cc b/google/cloud/internal/tracing_rest_client.cc index 84139c2ef3dd5..0e92b092919e7 100644 --- a/google/cloud/internal/tracing_rest_client.cc +++ b/google/cloud/internal/tracing_rest_client.cc @@ -71,21 +71,22 @@ StatusOr> EndResponseSpan( *context.local_port()); } for (auto const& kv : context.headers()) { - auto const name = "http.request.header." + kv.first; - if (kv.second.empty()) { + auto const name = "http.request.header." + std::string{kv.first}; + if (kv.second.EmptyValues()) { span->SetAttribute(name, ""); continue; } if (absl::EqualsIgnoreCase(kv.first, "authorization")) { - span->SetAttribute(name, kv.second.front().substr(0, 32)); + span->SetAttribute(name, kv.second.values().front().substr(0, 32)); continue; } if (absl::EqualsIgnoreCase(kv.first, "x-goog-api-key")) { span->SetAttribute( - name, kv.second.front().substr(0, kApiKeyHintLength) + "..."); + name, + kv.second.values().front().substr(0, kApiKeyHintLength) + "..."); continue; } - span->SetAttribute(name, kv.second.front()); + span->SetAttribute(name, kv.second.values().front()); } if (!request_result || !(*request_result)) { return internal::EndSpan(*span, std::move(request_result)); diff --git a/google/cloud/internal/unified_rest_credentials_test.cc b/google/cloud/internal/unified_rest_credentials_test.cc index 48760cfa8f0ca..b3d769bf462f0 100644 --- a/google/cloud/internal/unified_rest_credentials_test.cc +++ b/google/cloud/internal/unified_rest_credentials_test.cc @@ -470,8 +470,9 @@ TEST(UnifiedRestCredentialsTest, ApiKey) { ASSERT_THAT(oauth2_creds, NotNull()); auto header = - oauth2_creds->AuthenticationHeader(std::chrono::system_clock::now()); - EXPECT_THAT(header, IsOkAndHolds(Pair("x-goog-api-key", "api-key"))); + oauth2_creds->AuthenticationHeaders(std::chrono::system_clock::now(), ""); + EXPECT_THAT(header, + IsOkAndHolds(Contains(HttpHeader("x-goog-api-key", "api-key")))); } TEST(UnifiedRestCredentialsTest, LoadError) { diff --git a/google/cloud/storage/client.cc b/google/cloud/storage/client.cc index 22db63e9346b6..559d2f87fca7c 100644 --- a/google/cloud/storage/client.cc +++ b/google/cloud/storage/client.cc @@ -58,10 +58,6 @@ class WrapRestCredentials { std::shared_ptr impl) : impl_(std::move(impl)) {} - StatusOr AuthorizationHeader() { - return oauth2_internal::AuthenticationHeaderJoined(*impl_); - } - StatusOr> SignBlob( SigningAccount const& signing_account, std::string const& blob) const { return impl_->SignBlob(signing_account.value_or(impl_->AccountEmail()), diff --git a/google/cloud/storage/internal/grpc/configure_client_context.cc b/google/cloud/storage/internal/grpc/configure_client_context.cc index 4507f6d2af721..ff716764e8ece 100644 --- a/google/cloud/storage/internal/grpc/configure_client_context.cc +++ b/google/cloud/storage/internal/grpc/configure_client_context.cc @@ -28,7 +28,7 @@ void AddIdempotencyToken(grpc::ClientContext& ctx, auto const& headers = context.headers(); auto const l = headers.find(kIdempotencyTokenHeader); if (l != headers.end()) { - for (auto const& v : l->second) { + for (auto const& v : l->second.values()) { ctx.AddMetadata(kIdempotencyTokenHeader, v); } } diff --git a/google/cloud/storage/internal/rest/stub_test.cc b/google/cloud/storage/internal/rest/stub_test.cc index 05202648577d2..51540b8824f6e 100644 --- a/google/cloud/storage/internal/rest/stub_test.cc +++ b/google/cloud/storage/internal/rest/stub_test.cc @@ -178,12 +178,9 @@ TEST(RestStubTest, GlobalCustomHeadersAppearInRequestTest) { google::cloud::rest_internal::RestRequest const& request) { auto const& headers = request.headers(); EXPECT_THAT(headers, - Contains(Pair("custom-header-1", - std::vector{"value1"}))); + Contains(Pair("custom-header-1", ElementsAre("value1")))); EXPECT_THAT(headers, - Contains(Pair("custom-header-2", - std::vector{"value2"}))); - + Contains(Pair("custom-header-2", ElementsAre("value2")))); return PermanentError(); }); auto stub = std::make_unique(global_opts, mock_client, mock_client); diff --git a/google/cloud/storage/testing/retry_tests.cc b/google/cloud/storage/testing/retry_tests.cc index b635d67fbcbcf..509925bafef6d 100644 --- a/google/cloud/storage/testing/retry_tests.cc +++ b/google/cloud/storage/testing/retry_tests.cc @@ -121,7 +121,8 @@ void CaptureIdempotencyToken(std::vector& tokens, auto const& headers = context.headers(); auto l = headers.find(kIdempotencyTokenHeader); if (l == headers.end()) return; - tokens.insert(tokens.end(), l->second.begin(), l->second.end()); + auto values = l->second.values(); + tokens.insert(tokens.end(), values.begin(), values.end()); } void CaptureAuthorityOption(std::vector& authority, diff --git a/google/cloud/storage/tests/service_account_credentials_integration_test.cc b/google/cloud/storage/tests/service_account_credentials_integration_test.cc index 08fc4c0d3670b..13ffc19d34626 100644 --- a/google/cloud/storage/tests/service_account_credentials_integration_test.cc +++ b/google/cloud/storage/tests/service_account_credentials_integration_test.cc @@ -56,8 +56,8 @@ TEST_F(ServiceAccountCredentialsTest, UserInfoOAuth2) { auto factory = [c = sa_creds]() { auto authorization = c->GetToken(std::chrono::system_clock::now()); if (!authorization) return rest_internal::RestRequest(); - return rest_internal::RestRequest().AddHeader( - std::make_pair("Authorization", "Bearer " + authorization->token)); + return rest_internal::RestRequest().AddHeader(rest_internal::HttpHeader( + "Authorization", "Bearer " + authorization->token)); }; auto response = RetryHttpGet(kUrl, factory); diff --git a/google/cloud/storage/tests/unified_credentials_integration_test.cc b/google/cloud/storage/tests/unified_credentials_integration_test.cc index 58ad784cc953f..2d5407f17f567 100644 --- a/google/cloud/storage/tests/unified_credentials_integration_test.cc +++ b/google/cloud/storage/tests/unified_credentials_integration_test.cc @@ -21,6 +21,7 @@ #include "google/cloud/internal/unified_rest_credentials.h" #include "google/cloud/testing_util/scoped_environment.h" #include "google/cloud/testing_util/status_matchers.h" +#include "absl/strings/match.h" #include #ifndef _WIN32 #include @@ -43,9 +44,9 @@ using ::google::cloud::UnifiedCredentialsOption; using ::google::cloud::internal::GetEnv; using ::google::cloud::storage::testing::TempFile; using ::google::cloud::testing_util::IsOk; +using ::testing::Contains; using ::testing::IsEmpty; using ::testing::Not; -using ::testing::StartsWith; // This is a properly formatted, but invalid, CA Certificate. We will use this // as the *only* root of trust and try to contact *.google.com. This will @@ -88,6 +89,10 @@ KlXA1yQW/ClmnHVg57SN1g1rvOJCcnHBnSbT7kGFqUol constexpr int kCurleAbortedByCallback = 42; constexpr int kCurleOk = 0; +MATCHER_P(HeaderStartsWith, prefix, "header start with") { + return absl::StartsWith(std::string{arg}, prefix); +} + class UnifiedCredentialsIntegrationTest : public ::google::cloud::storage::testing::StorageIntegrationTest { protected: @@ -375,13 +380,18 @@ TEST_F(UnifiedCredentialsIntegrationTest, AccessToken) { auto default_credentials = rest_internal::MapCredentials((*MakeGoogleDefaultCredentials())); auto expiration = std::chrono::system_clock::now() + std::chrono::hours(1); - auto header = - oauth2_internal::AuthenticationHeaderJoined(*default_credentials); - ASSERT_THAT(header, IsOk()); - - auto constexpr kPrefix = "Authorization: Bearer "; - ASSERT_THAT(*header, StartsWith(kPrefix)); - auto token = header->substr(std::strlen(kPrefix)); + auto headers = default_credentials->AuthenticationHeaders( + std::chrono::system_clock::now(), ""); + ASSERT_THAT(headers, IsOk()); + + auto constexpr kPrefix = "authorization: Bearer "; + ASSERT_THAT(*headers, Contains(HeaderStartsWith(kPrefix))); + std::string authorization; + for (auto const& h : *headers) { + authorization = std::string{h}; + if (absl::StartsWith(authorization, kPrefix)) break; + } + auto token = authorization.substr(std::strlen(kPrefix)); auto client = MakeTestClient(Options{}.set( MakeAccessTokenCredentials(token, expiration))); @@ -401,13 +411,18 @@ TEST_F(UnifiedCredentialsIntegrationTest, AccessTokenCustomTrustStore) { auto default_credentials = rest_internal::MapCredentials((*MakeGoogleDefaultCredentials())); auto expiration = std::chrono::system_clock::now() + std::chrono::hours(1); - auto header = - oauth2_internal::AuthenticationHeaderJoined(*default_credentials); - ASSERT_THAT(header, IsOk()); - - auto constexpr kPrefix = "Authorization: Bearer "; - ASSERT_THAT(*header, StartsWith(kPrefix)); - auto token = header->substr(std::strlen(kPrefix)); + auto headers = default_credentials->AuthenticationHeaders( + std::chrono::system_clock::now(), ""); + ASSERT_THAT(headers, IsOk()); + + auto constexpr kPrefix = "authorization: Bearer "; + ASSERT_THAT(*headers, Contains(HeaderStartsWith(kPrefix))); + std::string authorization; + for (auto const& h : *headers) { + authorization = std::string{h}; + if (absl::StartsWith(authorization, kPrefix)) break; + } + auto token = authorization.substr(std::strlen(kPrefix)); testing_util::ScopedEnvironment grpc_roots_pem( "GRPC_DEFAULT_SSL_ROOTS_FILE_PATH", absl::nullopt); @@ -430,13 +445,18 @@ TEST_F(UnifiedCredentialsIntegrationTest, AccessTokenEmptyTrustStore) { auto default_credentials = rest_internal::MapCredentials((*MakeGoogleDefaultCredentials())); auto expiration = std::chrono::system_clock::now() + std::chrono::hours(1); - auto header = - oauth2_internal::AuthenticationHeaderJoined(*default_credentials); - ASSERT_THAT(header, IsOk()); - - auto constexpr kPrefix = "Authorization: Bearer "; - ASSERT_THAT(*header, StartsWith(kPrefix)); - auto token = header->substr(std::strlen(kPrefix)); + auto headers = default_credentials->AuthenticationHeaders( + std::chrono::system_clock::now(), ""); + ASSERT_THAT(headers, IsOk()); + + auto constexpr kPrefix = "authorization: Bearer "; + ASSERT_THAT(*headers, Contains(HeaderStartsWith(kPrefix))); + std::string authorization; + for (auto const& h : *headers) { + authorization = std::string{h}; + if (absl::StartsWith(authorization, kPrefix)) break; + } + auto token = authorization.substr(std::strlen(kPrefix)); auto client = MakeTestClient( EmptyTrustStoreOptions()