Google Cloud Spanner C++ Client
A C++ Client Library for Google Cloud Spanner
validate_metadata.cc
Go to the documentation of this file.
1 // Copyright 2019 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
16 #include "google/cloud/status_or.h"
17 #include <google/api/annotations.pb.h>
18 #include <google/protobuf/descriptor.h>
19 #include <grpcpp/channel.h>
20 #include <grpcpp/completion_queue.h>
21 #include <grpcpp/generic/async_generic_service.h>
22 #include <grpcpp/generic/generic_stub.h>
23 #include <grpcpp/server.h>
24 #include <grpcpp/server_builder.h>
25 #include <grpcpp/server_context.h>
26 #include <regex>
27 
28 namespace google {
29 namespace cloud {
30 namespace spanner_testing {
31 inline namespace SPANNER_CLIENT_NS {
32 
33 #if !defined(__clang__) && \
34  (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 9))
35 
36 // gcc-4.8 and earlier have broken regexes - ignore the tests there.
37 Status IsContextMDValid(grpc::ClientContext&, std::string const&,
38  std::string const&) {
39  return Status();
40 }
41 
42 #else
43 
44 namespace {
45 
53 std::multimap<std::string, std::string> GetMetadata(
54  grpc::ClientContext& context) {
55  // Start the generic server.
56  grpc::ServerBuilder builder;
57  grpc::AsyncGenericService generic_service;
58  builder.RegisterAsyncGenericService(&generic_service);
59  auto srv_cq = builder.AddCompletionQueue();
60  auto server = builder.BuildAndStart();
61 
62  // Send some garbage with the supplied context.
63  grpc::GenericStub generic_stub(
64  server->InProcessChannel(grpc::ChannelArguments()));
65  grpc::CompletionQueue cli_cq;
66  auto cli_stream =
67  generic_stub.PrepareCall(&context, "made_up_method", &cli_cq);
68  cli_stream->StartCall(nullptr);
69  bool ok;
70  void* dummy;
71  cli_cq.Next(&dummy, &ok); // actually start the client call
72 
73  // Receive the garbage with the supplied context.
74  grpc::GenericServerContext server_context;
75  grpc::GenericServerAsyncReaderWriter reader_writer(&server_context);
76  generic_service.RequestCall(&server_context, &reader_writer, srv_cq.get(),
77  srv_cq.get(), nullptr);
78  srv_cq->Next(&dummy, &ok); // actually receive the data
79 
80  // Now we've got the data - save it before cleaning up.
81  std::multimap<std::string, std::string> res;
82  auto const& cli_md = server_context.client_metadata();
83  std::transform(cli_md.begin(), cli_md.end(), std::inserter(res, res.begin()),
84  [](std::pair<grpc::string_ref, grpc::string_ref> const& md) {
85  return std::make_pair(
86  std::string(md.first.data(), md.first.length()),
87  std::string(md.second.data(), md.second.length()));
88  });
89 
90  // Shut everything down.
91  server->Shutdown(std::chrono::system_clock::now());
92  srv_cq->Shutdown();
93  cli_cq.Shutdown();
94  // Drain completion queues.
95  while (srv_cq->Next(&dummy, &ok))
96  ;
97  while (cli_cq.Next(&dummy, &ok))
98  ;
99 
100  return res;
101 }
102 
107 StatusOr<std::map<std::string, std::string> > ExtractMDFromHeader(
108  std::string header) {
109  std::map<std::string, std::string> res;
110  std::regex pair_re("[^&]+");
111  for (std::sregex_iterator i =
112  std::sregex_iterator(header.begin(), header.end(), pair_re);
113  i != std::sregex_iterator(); ++i) {
114  std::regex assign_re("([^=]+)=([^=]+)");
115  std::smatch match_res;
116  std::string s = i->str();
117  bool const matched = std::regex_match(s, match_res, assign_re);
118  if (!matched) {
119  return Status(
120  StatusCode::kInvalidArgument,
121  "Bad header format. The header should be a series of \"a=b\" "
122  "delimited with \"&\", but is \"" +
123  s + "\"");
124  }
125  bool const inserted =
126  res.insert(std::make_pair(match_res[1].str(), match_res[2].str()))
127  .second;
128  if (!inserted) {
129  return Status(
130  StatusCode::kInvalidArgument,
131  "Param " + match_res[1].str() + " is listed more then once");
132  }
133  }
134  return res;
135 }
136 
137 StatusOr<std::map<std::string, std::string> > ExtractMDFromHeaders(
138  std::multimap<std::string, std::string> const& headers) {
139  auto param_header = headers.equal_range("x-goog-request-params");
140  if (param_header.first == param_header.second) {
141  return Status(StatusCode::kInvalidArgument, "Expected header not found");
142  }
143  if (std::distance(param_header.first, param_header.second) > 1U) {
144  return Status(StatusCode::kInvalidArgument, "Multiple headers found");
145  }
146  return ExtractMDFromHeader(param_header.first->second);
147 }
148 
150 bool ValueMatchesPattern(std::string val, std::string pattern) {
151  std::string regexified_pattern =
152  regex_replace(pattern, std::regex("\\*"), std::string("[^/]+"));
153  return std::regex_match(val, std::regex(regexified_pattern));
154 }
155 
163 StatusOr<std::map<std::string, std::string> > ExtractParamsFromMethod(
164  std::string const& method) {
165  auto method_desc =
166  google::protobuf::DescriptorPool::generated_pool()->FindMethodByName(
167  method);
168 
169  if (method_desc == nullptr) {
170  return Status(StatusCode::kInvalidArgument,
171  "Method " + method + " is unknown.");
172  }
173  auto options = method_desc->options();
174  if (!options.HasExtension(google::api::http)) {
175  return Status(StatusCode::kInvalidArgument,
176  "Method " + method + " doesn't have a http option.");
177  }
178  auto const& http = options.GetExtension(google::api::http);
179  std::string pattern;
180  if (!http.get().empty()) {
181  pattern = http.get();
182  }
183  if (!http.put().empty()) {
184  pattern = http.put();
185  }
186  if (!http.post().empty()) {
187  pattern = http.post();
188  }
189  if (!http.delete_().empty()) {
190  pattern = http.delete_();
191  }
192  if (!http.patch().empty()) {
193  pattern = http.patch();
194  }
195  if (http.has_custom()) {
196  pattern = http.custom().path();
197  }
198 
199  if (pattern.empty()) {
200  return Status(
201  StatusCode::kInvalidArgument,
202  "Method " + method + " has a http option with an empty pattern.");
203  }
204 
205  std::regex subst_re("\\{([^{}=]+)=([^{}=]+)\\}");
206  std::map<std::string, std::string> res;
207  for (std::sregex_iterator i =
208  std::sregex_iterator(pattern.begin(), pattern.end(), subst_re);
209  i != std::sregex_iterator(); ++i) {
210  std::string const& param = (*i)[1].str();
211  std::string const& expected_pattern = (*i)[2].str();
212  res.insert(std::make_pair(param, expected_pattern));
213  }
214  return res;
215 }
216 
217 } // namespace
218 
225 Status IsContextMDValid(grpc::ClientContext& context, std::string const& method,
226  std::string const& api_client_header) {
227  auto headers = GetMetadata(context);
228 
229  // Extract the metadata from `x-goog-request-params` header in context.
230  auto md = ExtractMDFromHeaders(headers);
231  if (!md) {
232  return md.status();
233  }
234 
235  // Extract expectations on `x-goog-request-params` from the `google.api.http`
236  // annotation on the specified method.
237  auto params = ExtractParamsFromMethod(method);
238  if (!params) {
239  return params.status();
240  }
241  // Check if the metadata in the context satisfied the expectations.
242  for (auto const& param_pattern : *params) {
243  auto const& param = param_pattern.first;
244  auto const& expected_pattern = param_pattern.second;
245  auto found_it = md->find(param);
246  if (found_it == md->end()) {
247  return Status(StatusCode::kInvalidArgument,
248  "Expected param \"" + param + "\" not found in metadata");
249  }
250  if (!ValueMatchesPattern(found_it->second, expected_pattern)) {
251  return Status(StatusCode::kInvalidArgument,
252  "Expected param \"" + param +
253  "\" found, but its value (\"" + found_it->second +
254  "\") does not satisfy the pattern (\"" +
255  expected_pattern + "\").");
256  }
257  }
258 
259  auto found_api_client_header = headers.find("x-goog-api-client");
260  if (found_api_client_header == headers.end()) {
261  return Status(StatusCode::kInvalidArgument,
262  "Expected x-goog-api-client metadata");
263  }
264  if (found_api_client_header->second != api_client_header) {
265  return Status(StatusCode::kInvalidArgument,
266  "Expected x-goog-api-client to be " + api_client_header +
267  ", was " + found_api_client_header->second);
268  }
269  return Status();
270 }
271 
272 #endif
273 
274 } // namespace SPANNER_CLIENT_NS
275 } // namespace spanner_testing
276 } // namespace cloud
277 } // namespace google
Status IsContextMDValid(grpc::ClientContext &, std::string const &, std::string const &)
Verify that the metadata in the context is appropriate for a gRPC method.
#define SPANNER_CLIENT_NS
Definition: version.h:22