// Copyright 2019 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 // // http://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/testing_util/validate_metadata.h" #include "google/cloud/status_or.h" #include #include #include #include #include #include #include #include #include #include namespace google { namespace cloud { inline namespace GOOGLE_CLOUD_CPP_NS { namespace testing_util { namespace { /** * GetMetadata from `ClientContext`. * * `ClientContext` doesn't give access to the metadata, but `ServerContext` * does. In order to transform the `ClientContext` into `ServerContext` * we spin up a server and a client and send some garbage with this context. */ std::multimap GetMetadata( grpc::ClientContext& context) { // Set the deadline to far in the future. If the deadline is in the past, gRPC // doesn't send the initial metadata at all (which makes sense, given that the // context is already expired). The `context` is destroyed by this function // anyway, so we're not making things worse by changing the deadline. context.set_deadline(std::chrono::system_clock::now() + std::chrono::hours(24)); // Start the generic server. grpc::ServerBuilder builder; grpc::AsyncGenericService generic_service; builder.RegisterAsyncGenericService(&generic_service); auto srv_cq = builder.AddCompletionQueue(); auto server = builder.BuildAndStart(); // Send some garbage with the supplied context. grpc::GenericStub generic_stub( server->InProcessChannel(grpc::ChannelArguments())); grpc::CompletionQueue cli_cq; auto cli_stream = generic_stub.PrepareCall(&context, "made_up_method", &cli_cq); cli_stream->StartCall(nullptr); bool ok; void* dummy; cli_cq.Next(&dummy, &ok); // actually start the client call // Receive the garbage with the supplied context. grpc::GenericServerContext server_context; grpc::GenericServerAsyncReaderWriter reader_writer(&server_context); generic_service.RequestCall(&server_context, &reader_writer, srv_cq.get(), srv_cq.get(), nullptr); srv_cq->Next(&dummy, &ok); // actually receive the data // Now we've got the data - save it before cleaning up. std::multimap res; auto const& cli_md = server_context.client_metadata(); std::transform(cli_md.begin(), cli_md.end(), std::inserter(res, res.begin()), [](std::pair const& md) { return std::make_pair( std::string(md.first.data(), md.first.length()), std::string(md.second.data(), md.second.length())); }); // Shut everything down. server->Shutdown(std::chrono::system_clock::now()); srv_cq->Shutdown(); cli_cq.Shutdown(); // Drain completion queues. while (srv_cq->Next(&dummy, &ok)) ; while (cli_cq.Next(&dummy, &ok)) ; return res; } /** * Check if the `header` is of "foo=bar&baz=rab&..." and if it is, return a * `map` containing `"foo"->"bar", "baz"->"rab"`. */ StatusOr > ExtractMDFromHeader( std::string header) { std::map res; std::regex pair_re("[^&]+"); for (std::sregex_iterator i = std::sregex_iterator(header.begin(), header.end(), pair_re); i != std::sregex_iterator(); ++i) { std::regex assign_re("([^=]+)=([^=]+)"); std::smatch match_res; std::string s = i->str(); bool const matched = std::regex_match(s, match_res, assign_re); if (!matched) { return Status( StatusCode::kInvalidArgument, "Bad header format. The header should be a series of \"a=b\" " "delimited with \"&\", but is \"" + s + "\""); } bool const inserted = res.insert(std::make_pair(match_res[1].str(), match_res[2].str())) .second; if (!inserted) { return Status( StatusCode::kInvalidArgument, "Param " + match_res[1].str() + " is listed more then once"); } } return res; } StatusOr > ExtractMDFromHeaders( std::multimap const& headers) { auto param_header = headers.equal_range("x-goog-request-params"); if (param_header.first == param_header.second) { return Status(StatusCode::kInvalidArgument, "Expected header not found"); } if (std::distance(param_header.first, param_header.second) > 1U) { return Status(StatusCode::kInvalidArgument, "Multiple headers found"); } return ExtractMDFromHeader(param_header.first->second); } /// A poor man's check if a value matches a glob used in URL patterns. bool ValueMatchesPattern(std::string const& val, std::string const& pattern) { std::string regexified_pattern = regex_replace(pattern, std::regex("\\*"), std::string("[^/]+")); return std::regex_match(val, std::regex(regexified_pattern)); } /** * Given a `method`, extract its `google.api.http` option and parse it. * * The expected format of the option is * `something{foo=bar}something_else{baz=rab}`. For such a content, a `map` * containing `"foo"->"bar", "baz"->"rab"` is returned. */ StatusOr > ExtractParamsFromMethod( std::string const& method) { auto const* method_desc = google::protobuf::DescriptorPool::generated_pool()->FindMethodByName( method); if (method_desc == nullptr) { return Status(StatusCode::kInvalidArgument, "Method " + method + " is unknown."); } auto options = method_desc->options(); if (!options.HasExtension(google::api::http)) { return std::map{}; } auto const& http = options.GetExtension(google::api::http); std::string pattern; if (!http.get().empty()) { pattern = http.get(); } if (!http.put().empty()) { pattern = http.put(); } if (!http.post().empty()) { pattern = http.post(); } if (!http.delete_().empty()) { pattern = http.delete_(); } if (!http.patch().empty()) { pattern = http.patch(); } if (http.has_custom()) { pattern = http.custom().path(); } if (pattern.empty()) { return Status( StatusCode::kInvalidArgument, "Method " + method + " has a http option with an empty pattern."); } std::regex subst_re("\\{([^{}=]+)=([^{}=]+)\\}"); std::map res; for (std::sregex_iterator i = std::sregex_iterator(pattern.begin(), pattern.end(), subst_re); i != std::sregex_iterator(); ++i) { std::string const& param = (*i)[1].str(); std::string const& expected_pattern = (*i)[2].str(); res.insert(std::make_pair(param, expected_pattern)); } return res; } } // namespace /** * We use reflection to extract the `google.api.http` option from the given * `method`. We then parse it and check whether the contents of the * `x-goog-request-params` header in `context` set all the parameters listed in * the curly braces. */ Status IsContextMDValid(grpc::ClientContext& context, std::string const& method, std::string const& api_client_header, absl::optional resource_prefix_header) { auto headers = GetMetadata(context); // Extract the metadata from `x-goog-request-params` header in context. auto md = ExtractMDFromHeaders(headers); if (!md) { return md.status(); } // Extract expectations on `x-goog-request-params` from the `google.api.http` // annotation on the specified method. auto params = ExtractParamsFromMethod(method); if (!params) { return params.status(); } // Check if the metadata in the context satisfied the expectations. for (auto const& param_pattern : *params) { auto const& param = param_pattern.first; auto const& expected_pattern = param_pattern.second; auto found_it = md->find(param); if (found_it == md->end()) { return Status(StatusCode::kInvalidArgument, "Expected param \"" + param + "\" not found in metadata"); } if (!ValueMatchesPattern(found_it->second, expected_pattern)) { return Status( StatusCode::kInvalidArgument, "Expected param \"" + param + "\" found, but its value (\"" + // NOLINTNEXTLINE(performance-inefficient-string-concatenation) found_it->second + "\") does not satisfy the pattern (\"" + expected_pattern + "\")."); } } auto found_api_client_header = headers.find("x-goog-api-client"); if (found_api_client_header == headers.end()) { return Status(StatusCode::kInvalidArgument, "Expected x-goog-api-client metadata"); } if (found_api_client_header->second != api_client_header) { return Status(StatusCode::kInvalidArgument, "Expected x-goog-api-client to be " + api_client_header + ", was " + found_api_client_header->second); } if (resource_prefix_header) { std::string const header = "google-cloud-resource-prefix"; auto it = headers.find(header); if (it == headers.end()) { return Status(StatusCode::kInvalidArgument, header + " not found"); } if (it->second != *resource_prefix_header) { return Status(StatusCode::kInvalidArgument, header + " expected to be " + *resource_prefix_header + ", but was " + it->second); } } return Status(); } } // namespace testing_util } // namespace GOOGLE_CLOUD_CPP_NS } // namespace cloud } // namespace google