Support loading build api credentials from GCE.

This allows using credentials to get more builds on higher-privileged
GCE instances.

Based on https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances

See "Authenticating applications directly with access tokens"

Bug: 137304531
Test: ./fetch_cvd
Change-Id: Ied9813f0c9264e0b6d3c77ee30c5ecd1ccd37628
diff --git a/host/commands/fetcher/Android.bp b/host/commands/fetcher/Android.bp
index 1897cda..0c88171 100644
--- a/host/commands/fetcher/Android.bp
+++ b/host/commands/fetcher/Android.bp
@@ -17,6 +17,7 @@
     name: "fetch_cvd",
     srcs: [
         "build_api.cc",
+        "credential_source.cc",
         "curl_wrapper.cc",
         "main.cc",
     ],
diff --git a/host/commands/fetcher/build_api.cc b/host/commands/fetcher/build_api.cc
index 231f805..7823cab 100644
--- a/host/commands/fetcher/build_api.cc
+++ b/host/commands/fetcher/build_api.cc
@@ -37,11 +37,22 @@
   crc32 = json_artifact["crc32"].asUInt();
 }
 
+BuildApi::BuildApi(std::unique_ptr<CredentialSource> credential_source)
+    : credential_source(std::move(credential_source)) {}
+
+std::vector<std::string> BuildApi::Headers() {
+  std::vector<std::string> headers;
+  if (credential_source) {
+    headers.push_back("Authorization:Bearer " + credential_source->Credential());
+  }
+  return headers;
+}
+
 std::string BuildApi::LatestBuildId(const std::string& branch,
                                     const std::string& target) {
   std::string url = BUILD_API + "/builds?branch=" + branch
       + "&buildType=submitted&maxResults=1&successful=true&target=" + target;
-  auto response = curl.DownloadToJson(url);
+  auto response = curl.DownloadToJson(url, Headers());
   if (response["builds"].size() != 1) {
     LOG(ERROR) << "invalid number of builds\n";
     return "";
@@ -54,7 +65,7 @@
                                           const std::string& attempt_id) {
   std::string url = BUILD_API + "/builds/" + build_id + "/" + target
       + "/attempts/" + attempt_id + "/artifacts?maxResults=1000";
-  auto artifacts_json = curl.DownloadToJson(url);
+  auto artifacts_json = curl.DownloadToJson(url, Headers());
   std::vector<Artifact> artifacts;
   for (const auto& artifact_json : artifacts_json["artifacts"]) {
     artifacts.emplace_back(artifact_json);
@@ -69,5 +80,5 @@
                               const std::string& path) {
   std::string url = BUILD_API + "/builds/" + build_id + "/" + target
       + "/attempts/" + attempt_id + "/artifacts/" + artifact + "?alt=media";
-  return curl.DownloadToFile(url, path);
+  return curl.DownloadToFile(url, path, Headers());
 }
diff --git a/host/commands/fetcher/build_api.h b/host/commands/fetcher/build_api.h
index d3fb887..be5596a 100644
--- a/host/commands/fetcher/build_api.h
+++ b/host/commands/fetcher/build_api.h
@@ -15,8 +15,11 @@
 
 #pragma once
 
+#include <functional>
+#include <memory>
 #include <string>
 
+#include "credential_source.h"
 #include "curl_wrapper.h"
 
 class Artifact {
@@ -43,9 +46,11 @@
 
 class BuildApi {
   CurlWrapper curl;
-  // TODO credential fetcher
+  std::unique_ptr<CredentialSource> credential_source;
+
+  std::vector<std::string> Headers();
 public:
-  BuildApi() = default;
+  BuildApi(std::unique_ptr<CredentialSource> credential_source);
   ~BuildApi() = default;
 
   std::string LatestBuildId(const std::string& branch,
diff --git a/host/commands/fetcher/credential_source.cc b/host/commands/fetcher/credential_source.cc
new file mode 100644
index 0000000..6133149
--- /dev/null
+++ b/host/commands/fetcher/credential_source.cc
@@ -0,0 +1,49 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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 "credential_source.h"
+
+namespace {
+
+std::chrono::steady_clock::duration REFRESH_WINDOW =
+    std::chrono::minutes(2);
+std::string REFRESH_URL = "http://metadata.google.internal/computeMetadata/"
+    "v1/instance/service-accounts/default/token";
+
+}
+
+GceMetadataCredentialSource::GceMetadataCredentialSource() {
+  latest_credential = "";
+  expiration = std::chrono::steady_clock::now();
+}
+
+std::string GceMetadataCredentialSource::Credential() {
+  if (expiration - std::chrono::steady_clock::now() < REFRESH_WINDOW) {
+    RefreshCredential();
+  }
+  return latest_credential;
+}
+
+void GceMetadataCredentialSource::RefreshCredential() {
+  Json::Value credential_json =
+      curl.DownloadToJson(REFRESH_URL, {"Metadata-Flavor: Google"});
+  expiration = std::chrono::steady_clock::now()
+      + std::chrono::seconds(credential_json["expires_in"].asInt());
+  latest_credential = credential_json["access_token"].asString();
+}
+
+std::unique_ptr<CredentialSource> GceMetadataCredentialSource::make() {
+  return std::unique_ptr<CredentialSource>(new GceMetadataCredentialSource());
+}
diff --git a/host/commands/fetcher/credential_source.h b/host/commands/fetcher/credential_source.h
new file mode 100644
index 0000000..f50bd12
--- /dev/null
+++ b/host/commands/fetcher/credential_source.h
@@ -0,0 +1,42 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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.
+
+#pragma once
+
+#include <chrono>
+#include <memory>
+
+#include "curl_wrapper.h"
+
+class CredentialSource {
+public:
+  virtual ~CredentialSource() = default;
+  virtual std::string Credential() = 0;
+};
+
+class GceMetadataCredentialSource : public CredentialSource {
+  CurlWrapper curl;
+  std::string latest_credential;
+  std::chrono::steady_clock::time_point expiration;
+
+  void RefreshCredential();
+public:
+  GceMetadataCredentialSource();
+  GceMetadataCredentialSource(GceMetadataCredentialSource&&) = default;
+
+  virtual std::string Credential();
+
+  static std::unique_ptr<CredentialSource> make();
+};
diff --git a/host/commands/fetcher/curl_wrapper.cc b/host/commands/fetcher/curl_wrapper.cc
index 4a59c47..93dca5e 100644
--- a/host/commands/fetcher/curl_wrapper.cc
+++ b/host/commands/fetcher/curl_wrapper.cc
@@ -32,6 +32,22 @@
   return nmemb;
 }
 
+curl_slist* build_slist(const std::vector<std::string>& strings) {
+  curl_slist* curl_headers = nullptr;
+  for (const auto& str : strings) {
+    curl_slist* temp = curl_slist_append(curl_headers, str.c_str());
+    if (temp == nullptr) {
+      LOG(ERROR) << "curl_slist_append failed to add " << str;
+      if (curl_headers) {
+        curl_slist_free_all(curl_headers);
+        return nullptr;
+      }
+    }
+    curl_headers = temp;
+  }
+  return curl_headers;
+}
+
 } // namespace
 
 CurlWrapper::CurlWrapper() {
@@ -40,7 +56,6 @@
     LOG(ERROR) << "failed to initialize curl";
     return;
   }
-  curl_easy_setopt(curl, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt");
 }
 
 CurlWrapper::~CurlWrapper() {
@@ -48,12 +63,20 @@
 }
 
 bool CurlWrapper::DownloadToFile(const std::string& url, const std::string& path) {
+  return CurlWrapper::DownloadToFile(url, path, {});
+}
+
+bool CurlWrapper::DownloadToFile(const std::string& url, const std::string& path,
+                                 const std::vector<std::string>& headers) {
   LOG(INFO) << "Attempting to save \"" << url << "\" to \"" << path << "\"";
   if (!curl) {
     LOG(ERROR) << "curl was not initialized\n";
     return false;
   }
+  curl_slist* curl_headers = build_slist(headers);
   curl_easy_reset(curl);
+  curl_easy_setopt(curl, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt");
+  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curl_headers);
   curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
   FILE* file = fopen(path.c_str(), "w");
   if (!file) {
@@ -62,26 +85,40 @@
   }
   curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*) file);
   CURLcode res = curl_easy_perform(curl);
+  if (curl_headers) {
+    curl_slist_free_all(curl_headers);
+  }
+  fclose(file);
   if(res != CURLE_OK) {
     LOG(ERROR) << "curl_easy_perform() failed: " << curl_easy_strerror(res);
     return false;
   }
-  fclose(file);
   return true;
 }
 
 std::string CurlWrapper::DownloadToString(const std::string& url) {
+  return DownloadToString(url, {});
+}
+
+std::string CurlWrapper::DownloadToString(const std::string& url,
+                                          const std::vector<std::string>& headers) {
   LOG(INFO) << "Attempting to download \"" << url << "\"";
   if (!curl) {
     LOG(ERROR) << "curl was not initialized\n";
     return "";
   }
+  curl_slist* curl_headers = build_slist(headers);
   curl_easy_reset(curl);
+  curl_easy_setopt(curl, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt");
+  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curl_headers);
   curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
   std::stringstream data;
   curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, file_write_callback);
   curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
   CURLcode res = curl_easy_perform(curl);
+  if (curl_headers) {
+    curl_slist_free_all(curl_headers);
+  }
   if(res != CURLE_OK) {
     LOG(ERROR) << "curl_easy_perform() failed: " << curl_easy_strerror(res);
     return "";
@@ -90,7 +127,12 @@
 }
 
 Json::Value CurlWrapper::DownloadToJson(const std::string& url) {
-  std::string contents = DownloadToString(url);
+  return DownloadToJson(url, {});
+}
+
+Json::Value CurlWrapper::DownloadToJson(const std::string& url,
+                                        const std::vector<std::string>& headers) {
+  std::string contents = DownloadToString(url, headers);
   Json::Reader reader;
   Json::Value json;
   if (!reader.parse(contents, json)) {
diff --git a/host/commands/fetcher/curl_wrapper.h b/host/commands/fetcher/curl_wrapper.h
index 40ab5a8..5a5ba9c 100644
--- a/host/commands/fetcher/curl_wrapper.h
+++ b/host/commands/fetcher/curl_wrapper.h
@@ -27,9 +27,15 @@
   ~CurlWrapper();
   CurlWrapper(const CurlWrapper&) = delete;
   CurlWrapper& operator=(const CurlWrapper*) = delete;
-  CurlWrapper(const CurlWrapper&&) = delete;
+  CurlWrapper(CurlWrapper&&) = default;
 
   bool DownloadToFile(const std::string& url, const std::string& path);
+  bool DownloadToFile(const std::string& url, const std::string& path,
+                      const std::vector<std::string>& headers);
   std::string DownloadToString(const std::string& url);
+  std::string DownloadToString(const std::string& url,
+                               const std::vector<std::string>& headers);
   Json::Value DownloadToJson(const std::string& url);
+  Json::Value DownloadToJson(const std::string& url,
+                             const std::vector<std::string>& headers);
 };
diff --git a/host/commands/fetcher/main.cc b/host/commands/fetcher/main.cc
index a323083..c209e01 100644
--- a/host/commands/fetcher/main.cc
+++ b/host/commands/fetcher/main.cc
@@ -24,6 +24,7 @@
 #include "common/libs/utils/subprocess.h"
 
 #include "build_api.h"
+#include "credential_source.h"
 
 namespace {
 
@@ -35,6 +36,7 @@
 DEFINE_string(build_id, "latest", "Build ID for all artifacts");
 DEFINE_string(branch, "aosp-master", "Branch when build_id=\"latest\"");
 DEFINE_string(target, "aosp_cf_x86_phone-userdebug", "Build target");
+DEFINE_string(credential_source, "", "Build API credential source");
 
 int main(int argc, char** argv) {
   ::android::base::InitLogging(argv, android::base::StderrLogger);
@@ -42,7 +44,11 @@
 
   curl_global_init(CURL_GLOBAL_DEFAULT);
   {
-    BuildApi build_api;
+    std::unique_ptr<CredentialSource> credential_source;
+    if (FLAGS_credential_source == "gce") {
+      credential_source = GceMetadataCredentialSource::make();
+    }
+    BuildApi build_api(std::move(credential_source));
     std::string build_id = FLAGS_build_id;
     if (build_id == "latest") {
       build_id = build_api.LatestBuildId(FLAGS_branch, FLAGS_target);
@@ -51,7 +57,7 @@
     auto artifacts = build_api.Artifacts(build_id, FLAGS_target, "latest");
     bool has_host_package = false;
     bool has_image_zip = false;
-    const std::string img_zip_name = "aosp_cf_x86_phone-img-" + build_id + ".zip";
+    const std::string img_zip_name = FLAGS_target + "-img-" + build_id + ".zip";
     for (const auto& artifact : artifacts) {
       has_host_package |= artifact.Name() == HOST_TOOLS;
       has_image_zip |= artifact.Name() == img_zip_name;
@@ -60,7 +66,7 @@
       LOG(FATAL) << "Target build " << build_id << " did not have " << HOST_TOOLS;
     }
     if (!has_image_zip) {
-      LOG(FATAL) << "Target build " << build_id << " did not have" << img_zip_name;
+      LOG(FATAL) << "Target build " << build_id << " did not have " << img_zip_name;
     }
 
     build_api.ArtifactToFile(build_id, FLAGS_target, "latest",