gce.cc
1 // Copyright 2019, Beeri 15. All rights reserved.
2 // Author: Roman Gershman (romange@gmail.com)
3 //
4 
5 #include "util/gce/gce.h"
6 
7 #include <boost/asio/connect.hpp>
8 #include <boost/asio/ip/tcp.hpp>
9 
10 #include <boost/beast/core/buffers_to_string.hpp>
11 #include <boost/beast/core/flat_buffer.hpp>
12 #include <boost/beast/http/dynamic_body.hpp>
13 #include <boost/beast/http/empty_body.hpp>
14 #include <boost/beast/http/string_body.hpp>
15 #include <boost/beast/http/write.hpp> // for operator<<
16 
17 #include <rapidjson/document.h>
18 #include <rapidjson/error/en.h>
19 
20 #include "absl/strings/str_cat.h"
21 #include "absl/strings/str_split.h"
22 #include "absl/strings/strip.h"
23 #include "base/logging.h"
24 #include "file/file_util.h"
25 #include "file/filesource.h"
26 #include "util/asio/fiber_socket.h"
27 #include "util/http/https_client.h"
28 
29 namespace util {
30 using namespace std;
31 using namespace boost;
32 
33 namespace h2 = beast::http;
34 namespace rj = rapidjson;
35 using tcp = asio::ip::tcp;
36 
37 static const char kMetaDataHost[] = "metadata.google.internal";
38 
39 static Status ToStatus(const system::error_code& ec) {
40  return Status(StatusCode::IO_ERROR, absl::StrCat(ec.value(), ": ", ec.message()));
41 }
42 
43 #define RETURN_ON_ERROR \
44  if (ec) \
45  return ToStatus(ec)
46 
47 static StatusObject<string> GetHttp(const h2::request<h2::empty_body>& req, tcp::socket* sock) {
48  system::error_code ec;
49  boost::beast::flat_buffer buffer;
50  h2::response<h2::string_body> resp;
51 
52  h2::write(*sock, req, ec);
53  RETURN_ON_ERROR;
54 
55  h2::read(*sock, buffer, resp, ec);
56  RETURN_ON_ERROR;
57 
58  return std::move(resp).body();
59 }
60 
61 const char* GCE::GoogleCert() {
62  return R"(
63 -----BEGIN CERTIFICATE-----
64 MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
65 A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
66 Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
67 MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
68 A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
69 hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
70 v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
71 eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
72 tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
73 C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
74 zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
75 mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
76 V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
77 bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
78 3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
79 J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
80 291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
81 ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
82 AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
83 TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
84 -----END CERTIFICATE-----
85 )";
86 }
87 
88 const char* GCE::kApiDomain = "www.googleapis.com";
89 
90 asio::ssl::context GCE::CheckedSslContext() {
91  auto res = http::CreateClientSslContext(GCE::GoogleCert());
92  asio::ssl::context* ssl_cntx = absl::get_if<asio::ssl::context>(&res);
93  CHECK(ssl_cntx) << absl::get<system::error_code>(res);
94  return std::move(*ssl_cntx);
95 }
96 
97 Status GCE::ParseDefaultConfig() {
98  string config = file_util::ExpandPath("~/.config/gcloud/configurations/config_default");
99  if (!file::Exists(config)) {
100  return Status("Could not find config_default");
101  }
102  file::LineReader reader(config);
103  string scratch;
104  StringPiece line;
105 
106  while (reader.Next(&line, &scratch)) {
107  vector<StringPiece> vals = absl::StrSplit(line, "=");
108  if (vals.size() != 2)
109  continue;
110  for (auto& v : vals) {
111  v = absl::StripAsciiWhitespace(v);
112  }
113  if (vals[0] == "account") {
114  account_id_ = string(vals[1]);
115  } else if (vals[0] == "project") {
116  project_id_ = string(vals[1]);
117  }
118  }
119 
120  if (account_id_.empty() || project_id_.empty()) {
121  return Status("Could not find required fields in config_default");
122  }
123  return Status::OK;
124 }
125 
126 Status GCE::Init() {
127  system::error_code ec;
128  asio::ssl::context ssl_context = CheckedSslContext();
129  ssl_ctx_.reset(new SslContext{std::move(ssl_context)});
130 
131  string root_path = file_util::ExpandPath("~/.config/gcloud/");
132  string gce_file = absl::StrCat(root_path, "gce");
133 
134  asio::io_context io_context;
135  tcp::socket socket(io_context);
136 
137  auto connect = [&] {
138  tcp::resolver resolver{io_context};
139  auto const results = resolver.resolve(kMetaDataHost, "80", ec);
140  if (!ec) {
141  asio::connect(socket, results.begin(), results.end(), ec);
142  }
143  };
144 
145  if (file::Exists(gce_file)) {
146  string tmp_str;
147  CHECK(file_util::ReadFileToString(gce_file, &tmp_str));
148  is_prod_env_ = (tmp_str == "True");
149  } else {
150  connect();
151  is_prod_env_ = !ec;
152  }
153 
154  if (is_prod_env_) {
155  if (!socket.is_open()) {
156  connect();
157  RETURN_ON_ERROR;
158  }
159 
160  h2::request<h2::empty_body> req{
161  h2::verb::get, "/computeMetadata/v1/instance/service-accounts/default/email", 11};
162  req.set("Metadata-Flavor", "Google");
163  req.set(h2::field::host, kMetaDataHost);
164 
165  VLOG(1) << "Req: " << req;
166  auto str_res = GetHttp(req, &socket);
167  if (!str_res.ok())
168  return str_res.status;
169  account_id_ = std::move(str_res.obj);
170 
171  req.target("/computeMetadata/v1/project/project-id");
172  str_res = GetHttp(req, &socket);
173  if (!str_res.ok())
174  return str_res.status;
175  project_id_ = std::move(str_res.obj);
176  } else {
177  return ReadDevCreds(root_path);
178  }
179  return Status::OK;
180 }
181 
182 util::Status GCE::ReadDevCreds(const std::string& root_path) {
183  RETURN_IF_ERROR(ParseDefaultConfig());
184  LOG(INFO) << "Found account " << account_id_ << "/" << project_id_;
185 
186  string adc_file = absl::StrCat(root_path, "legacy_credentials/", account_id_, "/adc.json");
187 
188  string adc;
189  if (!file_util::ReadFileToString(adc_file, &adc)) {
190  return Status("Could not find " + adc_file);
191  }
192 
193  rj::Document adc_doc;
194  constexpr unsigned kFlags = rj::kParseTrailingCommasFlag | rj::kParseCommentsFlag;
195  adc_doc.ParseInsitu<kFlags>(&adc.front());
196 
197  if (adc_doc.HasParseError()) {
198  return Status(rj::GetParseError_En(adc_doc.GetParseError()));
199  }
200  for (auto it = adc_doc.MemberBegin(); it != adc_doc.MemberEnd(); ++it) {
201  if (it->name == "client_id") {
202  client_id_ = it->value.GetString();
203  } else if (it->name == "client_secret") {
204  client_secret_ = it->value.GetString();
205  } else if (it->name == "refresh_token") {
206  refresh_token_ = it->value.GetString();
207  }
208  }
209  if (client_id_.empty() || client_secret_.empty() || refresh_token_.empty()) {
210  return Status("Did not find secret tokens");
211  }
212  return Status::OK;
213 }
214 
215 std::string GCE::access_token() const {
216  std::lock_guard<fibers::mutex> lk(mu_);
217  return access_token_;
218 }
219 
220 StatusObject<std::string> GCE::RefreshAccessToken(IoContext* context) const {
221  h2::response<h2::string_body> resp;
222  error_code ec;
223 
224  if (is_prod_env_) {
225  h2::request<h2::empty_body> req{
226  h2::verb::get, "/computeMetadata/v1/instance/service-accounts/default/token", 11};
227  req.set("Metadata-Flavor", "Google");
228  req.set(h2::field::host, kMetaDataHost);
229 
230  FiberSyncSocket socket{kMetaDataHost, "80", context};
231  ec = socket.ClientWaitToConnect(2000);
232  RETURN_ON_ERROR;
233 
234  h2::write(socket, req, ec);
235  RETURN_ON_ERROR;
236 
237  beast::flat_buffer buffer;
238  h2::read(socket, buffer, resp, ec);
239  RETURN_ON_ERROR;
240  } else {
241  constexpr char kDomain[] = "oauth2.googleapis.com";
242 
243  http::HttpsClient https_client(kDomain, context, ssl_ctx_.get());
244 
245  ec = https_client.Connect(2000);
246  RETURN_ON_ERROR;
247 
248  h2::request<h2::string_body> req{h2::verb::post, "/token", 11};
249  req.set(h2::field::host, kDomain);
250  req.set(h2::field::content_type, "application/x-www-form-urlencoded");
251 
252  string& body = req.body();
253  body = absl::StrCat("grant_type=refresh_token&client_secret=", client_secret(),
254  "&refresh_token=", refresh_token());
255  absl::StrAppend(&body, "&client_id=", client_id());
256  req.prepare_payload();
257  VLOG(1) << "Req: " << req;
258 
259  ec = https_client.Send(req, &resp);
260  if (ec) {
261  return Status(absl::StrCat("Error sending access token request: ", ec.message()));
262  }
263  }
264 
265  if (resp.result() != h2::status::ok) {
266  return Status(StatusCode::IO_ERROR,
267  absl::StrCat("Http error ", string(resp.reason()), "Body: ", resp.body()));
268  }
269  VLOG(1) << "Resp: " << resp;
270 
271  return ParseTokenResponse(std::move(resp.body()));
272 }
273 
274 util::StatusObject<std::string> GCE::ParseTokenResponse(std::string&& response) const {
275  rj::Document doc;
276  constexpr unsigned kFlags = rj::kParseTrailingCommasFlag | rj::kParseCommentsFlag;
277  doc.ParseInsitu<kFlags>(&response.front());
278 
279  if (doc.HasParseError()) {
280  return Status(rj::GetParseError_En(doc.GetParseError()));
281  }
282 
283  string access_token, token_type;
284  for (auto it = doc.MemberBegin(); it != doc.MemberEnd(); ++it) {
285  if (it->name == "access_token") {
286  access_token = it->value.GetString();
287  } else if (it->name == "token_type") {
288  token_type = it->value.GetString();
289  }
290  }
291  if (token_type != "Bearer" || access_token.empty()) {
292  return Status(absl::StrCat("Bad json response: ", doc.GetString()));
293  }
294 
295  std::lock_guard<fibers::mutex> lk(mu_);
296  access_token_ = access_token;
297 
298  return access_token;
299 }
300 
301 void GCE::Test_InjectAcessToken(std::string access_token) {
302  std::lock_guard<fibers::mutex> lk(mu_);
303  access_token_.swap(access_token);
304 }
305 
306 } // namespace util
std::string access_token() const
Definition: gce.cc:215