blob: b92b8e8f7800c8f2dcee576c325b4632c2a05ec9 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/borealis/borealis_util.h"
#include "base/base64.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/process/launch.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/system/sys_info.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "components/crx_file/id_util.h"
#include "components/exo/shell_surface_util.h"
#include "net/base/url_util.h"
#include "third_party/re2/src/re2/re2.h"
#include "ui/display/screen.h"
namespace borealis {
const char kInstallerAppId[] = "dkecggknbdokeipkgnhifhiokailichf";
const char kClientAppId[] = "epfhbkiklgmlkhfpbcdleadnhcfdjfmo";
const char kLauncherSearchAppId[] = "ceoplblcdaffnnflkkcagjpomjgedmdl";
const char kIgnoredAppIdPrefix[] = "org.chromium.borealis.xid.";
const char kBorealisDlcName[] = "borealis-dlc";
const char kAllowedScheme[] = "steam";
const base::StringPiece kURLAllowlist[] = {"//store/", "//run/"};
const char kBorealisAppIdRegex[] = "(?:steam:\\/\\/rungameid\\/)(\\d+)";
const char kCompatToolVersionGameMismatch[] = "UNKNOWN (GameID mismatch)";
const char kDeviceInformationKey[] = "entry.1613887985";
const char kInsertCoinSuccessMessage[] = "Success";
const char kInsertCoinRejectMessage[] = "Coin Invalid";
namespace {
// Base feedback form URL, without query parameters for prefilling.
static constexpr char kFeedbackUrl[] =
"https://docs.google.com/forms/d/e/"
"1FAIpQLScGvT2BIwYJe9g15OINX2pvw6TgK8e2ihvSq3hHZudAneRmuA/"
"viewform?usp=pp_url";
// Query parameter keys for prefilling form data.
static constexpr char kAppNameKey[] = "entry.504707995";
// JSON keys for prefilling JSON section.
static constexpr char kJSONAppIdKey[] = "steam_appid";
static constexpr char kJSONBoardKey[] = "board";
static constexpr char kJSONMonitorsExternal[] = "external_monitors";
static constexpr char kJSONMonitorsInternal[] = "internal_monitors";
static constexpr char kJSONPlatformKey[] = "platform_version";
static constexpr char kJSONProtonKey[] = "proton_version";
static constexpr char kJSONSpecsKey[] = "specs";
static constexpr char kJSONSteamKey[] = "steam_runtime_version";
// App IDs prefixed with this are identified with a numeric "Borealis ID".
const base::StringPiece kBorealisWindowWithIdPrefix(
"org.chromium.borealis.xprop.");
// Windows with these app IDs are not games. Don't prompt for feedback for them.
// Hashed by crx_file::id_util::GenerateId().
static constexpr char kNonGameIdHash1[] = "hnfpbccfbbbjkmcalgjofgokpgjjppon";
static constexpr char kNonGameIdHash2[] = "kooplpnkalpdpoohnhmlmfebokjkgnlb";
static constexpr char kNonGameIdHash3[] = "bmhgcnboebpgmobfgfjcfplecleopefa";
GURL AssembleUrlAsync(std::string owner_id,
absl::optional<int> game_id,
std::string window_title) {
GURL url(kFeedbackUrl);
url = net::AppendQueryParameter(url, kAppNameKey, window_title);
base::Value::Dict json_root;
// System specs
json_root.Set(kJSONBoardKey, base::SysInfo::HardwareModelName());
json_root.Set(
kJSONSpecsKey,
base::StringPrintf("%ldGB; %s",
(long)(base::SysInfo::AmountOfPhysicalMemory() /
(1000 * 1000 * 1000)),
base::SysInfo::CPUModelName().c_str()));
json_root.Set(kJSONPlatformKey, base::SysInfo::OperatingSystemVersion());
// Number of monitors
int internal_displays = 0;
int external_displays = 0;
for (const display::Display& d :
display::Screen::GetScreen()->GetAllDisplays()) {
if (d.IsInternal()) {
internal_displays++;
} else {
external_displays++;
}
}
json_root.Set(kJSONMonitorsInternal, internal_displays);
json_root.Set(kJSONMonitorsExternal, external_displays);
// Proton/SLR versions
borealis::CompatToolInfo compat_tool_info;
std::string output;
if (borealis::GetCompatToolInfo(owner_id, &output)) {
compat_tool_info = borealis::ParseCompatToolInfo(game_id, output);
} else {
LOG(WARNING) << "Failed to get compat tool version info:";
LOG(WARNING) << output;
}
json_root.Set(kJSONProtonKey, compat_tool_info.proton);
json_root.Set(kJSONSteamKey, compat_tool_info.slr);
// Steam GameID
if (!game_id.has_value() && compat_tool_info.game_id.has_value()) {
game_id = compat_tool_info.game_id.value();
}
if (game_id.has_value()) {
json_root.Set(kJSONAppIdKey, base::StringPrintf("%d", game_id.value()));
}
std::string device_info;
base::JSONWriter::Write(json_root, &device_info);
url = net::AppendQueryParameter(url, kDeviceInformationKey, device_info);
return url;
}
} // namespace
absl::optional<int> GetBorealisAppId(std::string exec) {
int app_id;
if (RE2::PartialMatch(exec, kBorealisAppIdRegex, &app_id)) {
return app_id;
} else {
return absl::nullopt;
}
}
absl::optional<int> GetBorealisAppId(const aura::Window* window) {
const std::string* id = exo::GetShellApplicationId(window);
if (id && base::StartsWith(*id, kBorealisWindowWithIdPrefix)) {
int borealis_id;
if (base::StringToInt(id->substr(kBorealisWindowWithIdPrefix.size()),
&borealis_id)) {
return borealis_id;
}
}
return absl::nullopt;
}
void FeedbackFormUrl(Profile* const profile,
const std::string& app_id,
const std::string& window_title,
base::OnceCallback<void(GURL)> url_callback) {
const guest_os::GuestOsRegistryService* registry_service =
guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile);
// Exclude windows that aren't games.
if (app_id.find(kIgnoredAppIdPrefix) != std::string::npos ||
app_id == kClientAppId) {
std::move(url_callback).Run(GURL());
return;
}
std::string hash = crx_file::id_util::GenerateId(app_id);
if (hash == kNonGameIdHash1 || hash == kNonGameIdHash2 ||
hash == kNonGameIdHash3) {
std::move(url_callback).Run(GURL());
return;
}
// Attempt to get the Borealis app ID.
// TODO(b/173977876): Implement this in a more reliable way.
absl::optional<int> game_id;
absl::optional<guest_os::GuestOsRegistryService::Registration> registration =
registry_service->GetRegistration(app_id);
if (registration.has_value()) {
game_id = GetBorealisAppId(registration->Exec());
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, base::MayBlock(),
base::BindOnce(&AssembleUrlAsync,
ash::ProfileHelper::GetUserIdHashFromProfile(profile),
std::move(game_id), std::move(window_title)),
std::move(url_callback));
}
bool IsExternalURLAllowed(const GURL& url) {
if (url.scheme() != kAllowedScheme) {
return false;
}
for (auto& allowed_url : kURLAllowlist) {
if (base::StartsWith(url.GetContent(), allowed_url)) {
return true;
}
}
return false;
}
bool GetCompatToolInfo(const std::string& owner_id, std::string* output) {
std::vector<std::string> command = {"/usr/bin/vsh", "--owner_id=" + owner_id,
"--vm_name=borealis", "--",
"/usr/bin/get_compat_tool_versions.py"};
return base::GetAppOutputAndError(command, output);
}
CompatToolInfo ParseCompatToolInfo(absl::optional<int> game_id,
const std::string& output) {
// Expected stdout of get_compat_tool_versions.py:
// GameID: <game_id>, Proton:<proton_version>, SLR: <slr_version>, Timestamp: <timestamp>
// GameID: <game_id>, Proton:<proton_version>, SLR: <slr_version>, Timestamp: <timestamp>
// ...
// Only grab the first line, which is for the last game played.
std::string raw_info = output.substr(0, output.find("\n"));
CompatToolInfo compat_tool_info;
base::StringPairs tokenized_info;
base::SplitStringIntoKeyValuePairs(raw_info, ':', ',', &tokenized_info);
for (const auto& key_val_pair : tokenized_info) {
std::string key;
TrimWhitespaceASCII(key_val_pair.first, base::TRIM_ALL, &key);
std::string val;
TrimWhitespaceASCII(key_val_pair.second, base::TRIM_ALL, &val);
if (key == "GameID") {
int parsed_val;
bool ret = base::StringToInt(val, &parsed_val);
if (ret) {
compat_tool_info.game_id = parsed_val;
}
} else if (key == "Proton") {
compat_tool_info.proton = val;
} else if (key == "SLR") {
compat_tool_info.slr = val;
}
}
// If the app id is known and doesn't match, return the version "UNKNOWN"
if (game_id.has_value() && compat_tool_info.game_id.has_value() &&
game_id.value() != compat_tool_info.game_id.value()) {
LOG(WARNING) << "Expected GameID " << game_id.value() << " got "
<< compat_tool_info.game_id.value();
compat_tool_info.proton = kCompatToolVersionGameMismatch;
compat_tool_info.slr = kCompatToolVersionGameMismatch;
}
return compat_tool_info;
}
} // namespace borealis