blob: 31f49be928f2763c96ef5f7fd24d54f140617bcd [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/developer/debug/zxdb/client/cloud_storage_symbol_server.h"
#include <lib/syslog/cpp/macros.h>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <memory>
#include "lib/fit/function.h"
#include "rapidjson/document.h"
#include "rapidjson/filereadstream.h"
#include "rapidjson/filewritestream.h"
#include "rapidjson/istreamwrapper.h"
#include "rapidjson/writer.h"
#include "src/developer/debug/shared/message_loop.h"
#include "src/developer/debug/zxdb/client/session.h"
#include "src/developer/debug/zxdb/client/setting_schema_definition.h"
#include "src/developer/debug/zxdb/common/err_or.h"
#include "src/developer/debug/zxdb/common/string_util.h"
namespace zxdb {
namespace {
constexpr char kClientId[] =
"446450136466-2hr92jrq8e6i4tnsa56b52vacp7t3936"
".apps.googleusercontent.com";
constexpr char kClientSecret[] = "uBfbay2KCy9t4QveJ-dOqHtp";
constexpr char kAuthServer[] = "https://accounts.google.com/o/oauth2/v2/auth";
constexpr char kScope[] = "https://www.googleapis.com/auth/devstorage.read_only";
constexpr char kTokenServer[] = "https://www.googleapis.com/oauth2/v4/token";
bool DocIsAuthInfo(const rapidjson::Document& document) {
return !document.HasParseError() && document.IsObject() && document.HasMember("access_token");
}
std::string ToDebugFileName(const std::string& name, DebugSymbolFileType file_type) {
if (file_type == DebugSymbolFileType::kDebugInfo) {
return name + ".debug";
}
return name;
}
FILE* GetGoogleApiAuthCache(const char* mode) {
static std::filesystem::path path;
if (path.empty()) {
path = std::filesystem::path(std::getenv("HOME")) / ".fuchsia" / "debug";
std::error_code ec;
std::filesystem::create_directories(path, ec);
if (ec) {
path.clear();
return nullptr;
}
}
return fopen((path / "googleapi_auth").c_str(), mode);
}
class CloudStorageSymbolServerImpl : public CloudStorageSymbolServer {
public:
CloudStorageSymbolServerImpl(Session* session, const std::string& url,
bool require_authentication)
: CloudStorageSymbolServer(session, url), weak_factory_(this) {
if (require_authentication) {
DoInit();
} else {
ChangeState(SymbolServer::State::kReady);
}
}
void Fetch(const std::string& build_id, DebugSymbolFileType file_type,
SymbolServer::FetchCallback cb) override;
void CheckFetch(const std::string& build_id, DebugSymbolFileType file_type,
SymbolServer::CheckFetchCallback cb) override;
private:
void DoAuthenticate(const std::map<std::string, std::string>& data,
fit::callback<void(const Err&)> cb) override;
void OnAuthenticationResponse(Curl::Error result, fit::callback<void(const Err&)> cb,
const std::string& response);
fxl::RefPtr<Curl> PrepareCurl(const std::string& build_id, DebugSymbolFileType file_type);
void FetchWithCurl(const std::string& build_id, DebugSymbolFileType file_type,
fxl::RefPtr<Curl> curl, SymbolServer::FetchCallback cb);
fxl::WeakPtrFactory<CloudStorageSymbolServerImpl> weak_factory_;
};
} // namespace
CloudStorageSymbolServer::CloudStorageSymbolServer(Session* session, const std::string& url)
: SymbolServer(session, url) {
if (url.size() >= 6) {
// Strip off the protocol identifier.
path_ = url.substr(5);
if (path_.back() != '/') {
path_ += "/";
}
}
}
std::unique_ptr<CloudStorageSymbolServer> CloudStorageSymbolServer::Impl(
Session* session, const std::string& url, bool require_authentication) {
return std::make_unique<CloudStorageSymbolServerImpl>(session, url, require_authentication);
}
Err CloudStorageSymbolServer::HandleRequestResult(Curl::Error result, long response_code,
size_t previous_ready_count) {
if (!result && response_code == 200) {
return Err();
}
if (state() != SymbolServer::State::kReady || previous_ready_count != ready_count_) {
return Err("Internal error.");
}
Err out_err;
if (result) {
out_err = Err("Could not contact server: " + result.ToString());
// Fall through to retry.
} else if (response_code == 401) {
return Err("Authentication expired.");
} else if (response_code == 404 || response_code == 410) {
return Err("Server responded with code " + std::to_string(response_code));
} else {
out_err = Err("Unexpected response: " + std::to_string(response_code));
// Fall through to retry.
}
error_log_.push_back(out_err.msg());
IncrementRetries();
return out_err;
}
std::string CloudStorageSymbolServer::AuthInfo() const {
static std::string result;
if (state() != SymbolServer::State::kAuth) {
return "";
}
if (result.empty()) {
result = kAuthServer;
result += "?client_id=";
result += Curl::Escape(kClientId);
result += "&redirect_uri=urn:ietf:wg:oauth:2.0:oob";
result += "&response_type=code";
result += "&scope=";
result += Curl::Escape(kScope);
}
return result;
}
void CloudStorageSymbolServerImpl::DoAuthenticate(
const std::map<std::string, std::string>& post_data, fit::callback<void(const Err&)> cb) {
ChangeState(SymbolServer::State::kBusy);
auto curl = fxl::MakeRefCounted<Curl>();
// When running on GCE, metadata server is used to get the access token and post_data is unused.
// https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#applications
if (auto metadata_server = std::getenv("GCE_METADATA_HOST")) {
curl->SetURL("http://" + std::string(metadata_server) +
"/computeMetadata/v1/instance/service-accounts/default/token");
curl->headers().push_back("Metadata-Flavor: Google");
} else {
curl->SetURL(kTokenServer);
curl->set_post_data(post_data);
}
auto response = std::make_shared<std::string>();
curl->set_data_callback([response](const std::string& data) {
response->append(data);
return data.size();
});
curl->Perform([weak_this = weak_factory_.GetWeakPtr(), cb = std::move(cb), response](
Curl*, Curl::Error result) mutable {
if (weak_this)
weak_this->OnAuthenticationResponse(std::move(result), std::move(cb), *response);
});
}
void CloudStorageSymbolServerImpl::OnAuthenticationResponse(Curl::Error result,
fit::callback<void(const Err&)> cb,
const std::string& response) {
if (result) {
std::string error = "Could not contact authentication server: ";
error += result.ToString();
error_log_.push_back(error);
ChangeState(SymbolServer::State::kAuth);
cb(Err(error));
return;
}
rapidjson::Document document;
document.Parse(response);
if (!DocIsAuthInfo(document)) {
error_log_.push_back("Authentication failed");
ChangeState(SymbolServer::State::kAuth);
cb(Err("Authentication failed"));
return;
}
access_token_ = document["access_token"].GetString();
bool new_refresh = false;
if (document.HasMember("refresh_token")) {
new_refresh = true;
refresh_token_ = document["refresh_token"].GetString();
}
if (document.HasMember("expires_in")) {
constexpr int kMilli = 1000;
int time = document["expires_in"].GetInt();
if (time > 1000) {
time -= 100;
}
time *= kMilli;
debug::MessageLoop::Current()->PostTimer(FROM_HERE, time,
[weak_this = weak_factory_.GetWeakPtr()]() {
if (weak_this)
weak_this->AuthRefresh();
});
}
ChangeState(SymbolServer::State::kReady);
cb(Err());
if (!new_refresh) {
return;
}
if (FILE* fp = GetGoogleApiAuthCache("wb")) {
fwrite(refresh_token_.data(), 1, refresh_token_.size(), fp);
fclose(fp);
}
}
void CloudStorageSymbolServer::Authenticate(const std::string& data,
fit::callback<void(const Err&)> cb) {
if (state() != SymbolServer::State::kAuth) {
debug::MessageLoop::Current()->PostTask(
FROM_HERE, [cb = std::move(cb)]() mutable { cb(Err("Authentication not required.")); });
return;
}
// Authenciate using zxdb's own client ID.
client_id_ = kClientId;
client_secret_ = kClientSecret;
std::map<std::string, std::string> post_data;
post_data["code"] = data;
post_data["client_id"] = kClientId;
post_data["client_secret"] = kClientSecret;
post_data["redirect_uri"] = "urn:ietf:wg:oauth:2.0:oob";
post_data["grant_type"] = "authorization_code";
DoAuthenticate(post_data, std::move(cb));
}
void CloudStorageSymbolServer::AuthRefresh() {
std::map<std::string, std::string> post_data;
post_data["refresh_token"] = refresh_token_;
post_data["client_id"] = client_id_;
post_data["client_secret"] = client_secret_;
post_data["grant_type"] = "refresh_token";
DoAuthenticate(post_data, [](const Err& err) {});
}
void CloudStorageSymbolServer::DoInit() {
if (state() != SymbolServer::State::kAuth && state() != SymbolServer::State::kInitializing) {
return;
}
if (std::getenv("GCE_METADATA_HOST") || LoadCachedAuth() || LoadGCloudAuth()) {
ChangeState(SymbolServer::State::kBusy);
AuthRefresh();
} else {
ChangeState(SymbolServer::State::kAuth);
}
}
bool CloudStorageSymbolServer::LoadCachedAuth() {
FILE* fp = GetGoogleApiAuthCache("rb");
if (!fp) {
return false;
}
std::vector<char> buf(65536);
buf.resize(fread(buf.data(), 1, buf.size(), fp));
bool success = feof(fp);
fclose(fp);
if (!success) {
return false;
}
client_id_ = kClientId;
client_secret_ = kClientSecret;
refresh_token_ = std::string(buf.data(), buf.data() + buf.size());
return true;
}
bool CloudStorageSymbolServer::LoadGCloudAuth() {
std::string gcloud_config;
if (auto cloudsdk_config = std::getenv("CLOUDSDK_CONFIG")) {
gcloud_config = cloudsdk_config;
} else if (auto home = std::getenv("HOME")) {
gcloud_config = std::string(home) + "/.config/gcloud";
} else {
return false;
}
std::ifstream credential_file(gcloud_config + "/application_default_credentials.json");
if (!credential_file) {
return false;
}
rapidjson::IStreamWrapper input_stream(credential_file);
rapidjson::Document credentials;
credentials.ParseStream(input_stream);
if (credentials.HasParseError() || !credentials.IsObject() ||
!credentials.HasMember("client_id") || !credentials.HasMember("client_secret") ||
!credentials.HasMember("refresh_token")) {
return false;
}
client_id_ = credentials["client_id"].GetString();
client_secret_ = credentials["client_secret"].GetString();
refresh_token_ = credentials["refresh_token"].GetString();
return true;
}
fxl::RefPtr<Curl> CloudStorageSymbolServerImpl::PrepareCurl(const std::string& build_id,
DebugSymbolFileType file_type) {
if (state() != SymbolServer::State::kReady) {
return nullptr;
}
std::string url = "https://storage.googleapis.com/";
url += path_ + ToDebugFileName(build_id, file_type);
auto curl = fxl::MakeRefCounted<Curl>();
FX_DCHECK(curl);
curl->SetURL(url);
// When require_authentication is true.
if (!access_token_.empty()) {
curl->headers().push_back("Authorization: Bearer " + access_token_);
}
return curl;
}
void CloudStorageSymbolServerImpl::CheckFetch(const std::string& build_id,
DebugSymbolFileType file_type,
SymbolServer::CheckFetchCallback cb) {
auto curl = PrepareCurl(build_id, file_type);
if (!curl) {
debug::MessageLoop::Current()->PostTask(
FROM_HERE, [cb = std::move(cb)]() mutable { cb(Err("Server not ready."), nullptr); });
return;
}
curl->get_body() = false;
size_t previous_ready_count = ready_count_;
curl->Perform([weak_this = weak_factory_.GetWeakPtr(), build_id, file_type, curl,
cb = std::move(cb), previous_ready_count](Curl*, Curl::Error result) mutable {
if (!weak_this)
return;
auto code = curl->ResponseCode();
Err err = weak_this->HandleRequestResult(result, code, previous_ready_count);
if (err.ok()) {
curl->get_body() = true;
cb(Err(), [weak_this, build_id, file_type, curl](SymbolServer::FetchCallback fcb) {
if (weak_this)
weak_this->FetchWithCurl(build_id, file_type, curl, std::move(fcb));
});
return;
}
cb(err, nullptr);
});
}
void CloudStorageSymbolServerImpl::Fetch(const std::string& build_id, DebugSymbolFileType file_type,
SymbolServer::FetchCallback cb) {
auto curl = PrepareCurl(build_id, file_type);
if (!curl) {
debug::MessageLoop::Current()->PostTask(
FROM_HERE, [cb = std::move(cb)]() mutable { cb(Err("Server not ready."), ""); });
return;
}
FetchWithCurl(build_id, file_type, curl, std::move(cb));
}
void CloudStorageSymbolServerImpl::FetchWithCurl(const std::string& build_id,
DebugSymbolFileType file_type,
fxl::RefPtr<Curl> curl, FetchCallback cb) {
auto cache_path = session()->system().settings().GetString(ClientSettings::System::kSymbolCache);
std::string path;
if (!cache_path.empty()) {
std::error_code ec;
auto path_obj = std::filesystem::path(cache_path);
if (std::filesystem::is_directory(path_obj, ec)) {
// Download to a temporary file, so if we get cancelled (or we get sent a 404 page instead of
// the real symbols) we don't pollute the build ID folder.
std::string name = ToDebugFileName(build_id, file_type) + ".part";
path = path_obj / name;
}
}
// Compute the destination file from the build ID.
if (build_id.size() <= 2) {
debug::MessageLoop::Current()->PostTask(FROM_HERE, [build_id, cb = std::move(cb)]() mutable {
cb(Err("Invalid build ID \"" + build_id + "\" for symbol fetch."), "");
});
return;
}
auto target_path = std::filesystem::path(cache_path) / build_id.substr(0, 2);
auto target_name = ToDebugFileName(build_id.substr(2), file_type);
FILE* file = nullptr;
// We don't have a folder specified where downloaded symbols go. We'll just drop it in tmp and at
// least you'll be able to use them for this session.
if (path.empty()) {
path = "/tmp/zxdb_downloaded_symbolsXXXXXX\0";
int fd = mkstemp(path.data());
path.pop_back();
if (fd >= 0) {
// Ownership of the fd is absorbed by fdopen.
file = fdopen(fd, "wb");
}
} else {
file = std::fopen(path.c_str(), "wb");
}
if (!file) {
debug::MessageLoop::Current()->PostTask(FROM_HERE, [cb = std::move(cb)]() mutable {
cb(Err("Error opening temporary file."), "");
});
return;
}
auto cleanup = [file, path, cache_path, target_path,
target_name](const Err& in_err) -> ErrOr<std::string> {
fclose(file);
std::error_code ec;
if (in_err.has_error()) {
// Need to cleanup the file in the error case.
std::filesystem::remove(path, ec);
return in_err; // Just forward the same error to the caller.
}
if (cache_path.empty()) {
return Err("No symbol cache specified.");
}
std::filesystem::create_directory(target_path, ec);
if (std::filesystem::is_directory(target_path, ec)) {
std::filesystem::rename(path, target_path / target_name, ec);
return std::string(target_path / target_name);
} else {
return Err("Could not move file in to cache.");
}
};
curl->set_data_callback(
[file](const std::string& data) { return std::fwrite(data.data(), 1, data.size(), file); });
size_t previous_ready_count = ready_count_;
curl->Perform([weak_this = weak_factory_.GetWeakPtr(), cleanup, cb = std::move(cb),
previous_ready_count](Curl* curl, Curl::Error result) mutable {
if (!weak_this)
return;
Err request_err =
weak_this->HandleRequestResult(result, curl->ResponseCode(), previous_ready_count);
ErrOr<std::string> path_or = cleanup(request_err);
cb(path_or.err_or_empty(), path_or.value_or_empty());
});
}
} // namespace zxdb