// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "fuchsia_web/webinstance_host/web_instance_host_internal.h"
#include <fuchsia/web/cpp/fidl.h>
#include <string_view>
#include <utility>
#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "build/build_config.h"
#include "build/chromecast_buildflags.h"
#include "components/fuchsia_component_support/feedback_registration.h"
#include "fuchsia_web/common/string_util.h"
#include "fuchsia_web/webengine/features.h"
#include "fuchsia_web/webengine/switches.h"
#include "gpu/command_buffer/service/gpu_switches.h"
#include "gpu/config/gpu_finch_features.h"
#include "net/http/http_util.h"
#include "services/network/public/cpp/network_switches.h"
#include "third_party/widevine/cdm/buildflags.h"
#include "ui/gfx/switches.h"
#include "ui/gl/gl_switches.h"
#include "ui/ozone/public/ozone_switches.h"
#if BUILDFLAG(ENABLE_WIDEVINE) && BUILDFLAG(ENABLE_CAST_RECEIVER)
#include "third_party/widevine/cdm/widevine_cdm_common.h" // nogncheck
#endif
namespace {
// Returns true if DRM is supported in current configuration. Currently we
// assume that it is supported on ARM64, but not on x64.
//
// TODO(crbug.com/42050020): Detect support for all features required for
// FuchsiaCdm. Specifically we need to verify that protected memory is supported
// and that mediacodec API provides hardware video decoders.
bool IsFuchsiaCdmSupported() {
#if BUILDFLAG(ENABLE_WIDEVINE) && defined(ARCH_CPU_ARM64)
return true;
#else
return false;
#endif
}
// Appends |value| to the value of |switch_name| in the |command_line|.
// The switch is assumed to consist of comma-separated values. If |switch_name|
// is already set in |command_line| then a comma will be appended, followed by
// |value|, otherwise the switch will be set to |value|.
void AppendToSwitch(std::string_view switch_name,
std::string_view value,
base::CommandLine& command_line) {
if (!command_line.HasSwitch(switch_name)) {
command_line.AppendSwitchNative(switch_name, value);
return;
}
std::string new_value =
base::StrCat({command_line.GetSwitchValueASCII(switch_name), ",", value});
command_line.RemoveSwitch(switch_name);
command_line.AppendSwitchNative(switch_name, new_value);
}
bool HandleUserAgentParams(fuchsia::web::CreateContextParams& params,
base::CommandLine& launch_args) {
if (!params.has_user_agent_product()) {
if (params.has_user_agent_version()) {
LOG(ERROR) << "Embedder version without product.";
return false;
}
return true;
}
if (!net::HttpUtil::IsToken(params.user_agent_product())) {
LOG(ERROR) << "Invalid embedder product.";
return false;
}
std::string product_and_version(params.user_agent_product());
if (params.has_user_agent_version()) {
if (!net::HttpUtil::IsToken(params.user_agent_version())) {
LOG(ERROR) << "Invalid embedder version.";
return false;
}
base::StrAppend(&product_and_version, {"/", params.user_agent_version()});
}
launch_args.AppendSwitchNative(switches::kUserAgentProductAndVersion,
std::move(product_and_version));
return true;
}
bool HandleKeyboardFeatureFlags(fuchsia::web::ContextFeatureFlags features,
base::CommandLine& launch_args) {
const bool enable_keyboard =
(features & fuchsia::web::ContextFeatureFlags::KEYBOARD) ==
fuchsia::web::ContextFeatureFlags::KEYBOARD;
const bool enable_virtual_keyboard =
(features & fuchsia::web::ContextFeatureFlags::VIRTUAL_KEYBOARD) ==
fuchsia::web::ContextFeatureFlags::VIRTUAL_KEYBOARD;
if (enable_keyboard) {
AppendToSwitch(switches::kEnableFeatures, features::kKeyboardInput.name,
launch_args);
if (enable_virtual_keyboard) {
AppendToSwitch(switches::kEnableFeatures, features::kVirtualKeyboard.name,
launch_args);
}
} else if (enable_virtual_keyboard) {
LOG(ERROR) << "VIRTUAL_KEYBOARD feature requires KEYBOARD.";
return false;
}
return true;
}
void HandleUnsafelyTreatInsecureOriginsAsSecureParam(
fuchsia::web::CreateContextParams& params,
base::CommandLine& launch_args) {
if (!params.has_unsafely_treat_insecure_origins_as_secure()) {
return;
}
const std::vector<std::string>& insecure_origins =
params.unsafely_treat_insecure_origins_as_secure();
for (const auto& origin : insecure_origins) {
#if BUILDFLAG(ENABLE_CAST_RECEIVER)
static constexpr char kDisableMixedContentAutoupgradeOrigin[] =
"disable-mixed-content-autoupgrade";
if (origin == switches::kAllowRunningInsecureContent) {
launch_args.AppendSwitch(switches::kAllowRunningInsecureContent);
continue;
}
if (origin == kDisableMixedContentAutoupgradeOrigin) {
// Constant from //third_party/blink/common/features.cc:
static constexpr char kMixedContentAutoupgradeFeatureName[] =
"AutoupgradeMixedContent";
AppendToSwitch(switches::kDisableFeatures,
kMixedContentAutoupgradeFeatureName, launch_args);
continue;
}
#endif
// Pass the list to the Context process.
AppendToSwitch(network::switches::kUnsafelyTreatInsecureOriginAsSecure,
origin, launch_args);
}
}
void HandleCorsExemptHeadersParam(fuchsia::web::CreateContextParams& params,
base::CommandLine& launch_args) {
if (!params.has_cors_exempt_headers()) {
return;
}
std::vector<std::string_view> cors_exempt_headers;
cors_exempt_headers.reserve(params.cors_exempt_headers().size());
for (const auto& header : params.cors_exempt_headers()) {
cors_exempt_headers.push_back(BytesAsString(header));
}
launch_args.AppendSwitchNative(switches::kCorsExemptHeaders,
base::JoinString(cors_exempt_headers, ","));
}
void HandleDisableCodeGenerationParam(
fuchsia::web::ContextFeatureFlags features,
base::CommandLine& launch_args) {
if ((features &
fuchsia::web::ContextFeatureFlags::DISABLE_DYNAMIC_CODE_GENERATION) !=
fuchsia::web::ContextFeatureFlags::DISABLE_DYNAMIC_CODE_GENERATION) {
return;
}
// These flag constants must match the values defined in Blink and V8,
// respectively. They are duplicated here rather than creating dependencies
// of `WebInstanceHost` uses on those sub-projects.
static constexpr char kJavaScriptFlags[] = "js-flags";
static constexpr char kV8JitlessFlag[] = "--jitless";
// Add the JIT-less option to the comma-separated set of V8 flags passed to
// Blink.
AppendToSwitch(kJavaScriptFlags, kV8JitlessFlag, launch_args);
// TODO(crbug.com/42050417): Disable use of VmexResource in this case, once
// migrated off of ambient VMEX.
}
} // namespace
void RegisterWebInstanceProductData(std::string_view absolute_component_url) {
// LINT.IfChange(web_engine_crash_product_name)
static constexpr char kCrashProductName[] = "FuchsiaWebEngine";
// LINT.ThenChange(//fuchsia_web/webengine/context_provider_main.cc:web_engine_crash_product_name)
static constexpr char kFeedbackAnnotationsNamespace[] = "web-engine";
fuchsia_component_support::RegisterProductDataForCrashReporting(
absolute_component_url, kCrashProductName);
fuchsia_component_support::RegisterProductDataForFeedback(
kFeedbackAnnotationsNamespace);
}
bool IsValidContentDirectoryName(std::string_view file_name) {
if (file_name.find_first_of(base::FilePath::kSeparators, 0,
base::FilePath::kSeparatorsLength - 1) !=
std::string_view::npos) {
return false;
}
if (file_name == base::FilePath::kCurrentDirectory ||
file_name == base::FilePath::kParentDirectory) {
return false;
}
return true;
}
zx_status_t AppendLaunchArgs(fuchsia::web::CreateContextParams& params,
base::CommandLine& launch_args) {
// Arguments to be stripped rather than propagated.
launch_args.RemoveSwitch(switches::kContextProvider);
const fuchsia::web::ContextFeatureFlags features =
params.has_features() ? params.features()
: fuchsia::web::ContextFeatureFlags();
if (params.has_remote_debugging_port()) {
if ((features & fuchsia::web::ContextFeatureFlags::NETWORK) !=
fuchsia::web::ContextFeatureFlags::NETWORK) {
LOG(ERROR) << "Enabling remote debugging port requires NETWORK feature.";
return ZX_ERR_INVALID_ARGS;
}
// Constant copied from //content/public/common/content_switches.cc:
static constexpr char kRemoteDebuggingPort[] = "remote-debugging-port";
launch_args.AppendSwitchNative(
kRemoteDebuggingPort,
base::NumberToString(params.remote_debugging_port()));
}
const bool is_headless =
(features & fuchsia::web::ContextFeatureFlags::HEADLESS) ==
fuchsia::web::ContextFeatureFlags::HEADLESS;
if (is_headless) {
launch_args.AppendSwitchNative(switches::kOzonePlatform,
switches::kHeadless);
launch_args.AppendSwitch(switches::kHeadless);
}
if ((features & fuchsia::web::ContextFeatureFlags::LEGACYMETRICS) ==
fuchsia::web::ContextFeatureFlags::LEGACYMETRICS) {
#if BUILDFLAG(ENABLE_CAST_RECEIVER)
launch_args.AppendSwitch(switches::kUseLegacyMetricsService);
#else
LOG(WARNING) << "LEGACYMETRICS is not supported.";
#endif
}
const bool enable_vulkan =
(features & fuchsia::web::ContextFeatureFlags::VULKAN) ==
fuchsia::web::ContextFeatureFlags::VULKAN;
bool enable_widevine =
(features & fuchsia::web::ContextFeatureFlags::WIDEVINE_CDM) ==
fuchsia::web::ContextFeatureFlags::WIDEVINE_CDM;
bool enable_playready = params.has_playready_key_system();
// Verify that the configuration is compatible with DRM, if requested.
if (enable_widevine || enable_playready) {
// VULKAN is required for DRM-protected video playback. Allow DRM to also be
// enabled for HEADLESS Contexts, since Vulkan is never required for audio.
if (!enable_vulkan && !is_headless) {
LOG(ERROR) << "WIDEVINE_CDM and PLAYREADY_CDM features require VULKAN "
" or HEADLESS.";
return ZX_ERR_NOT_SUPPORTED;
}
if (!params.has_cdm_data_directory()) {
LOG(ERROR) << "WIDEVINE_CDM and PLAYREADY_CDM features require a "
"|cdm_data_directory|.";
return ZX_ERR_NOT_SUPPORTED;
}
// |cdm_data_directory| will be handled later.
}
// If the system doesn't actually support DRM then disable it. This may result
// in the Context being able to run without using protected buffers.
if (enable_playready && !IsFuchsiaCdmSupported()) {
LOG(WARNING) << "PlayReady is not supported on this device.";
enable_playready = false;
params.clear_playready_key_system();
}
if (enable_widevine && !IsFuchsiaCdmSupported()) {
LOG(WARNING) << "Widevine is not supported on this device.";
enable_widevine = false;
}
if (enable_vulkan) {
if (is_headless) {
DLOG(ERROR) << "VULKAN and HEADLESS features cannot be used together.";
return ZX_ERR_NOT_SUPPORTED;
}
VLOG(1) << "Enabling Vulkan GPU acceleration.";
// Vulkan requires use of SkiaRenderer, configured to a use Vulkan context.
launch_args.AppendSwitch(switches::kUseVulkan);
AppendToSwitch(switches::kEnableFeatures, features::kVulkan.name,
launch_args);
launch_args.AppendSwitchASCII(switches::kUseGL,
gl::kGLImplementationANGLEName);
} else {
VLOG(1) << "Disabling GPU acceleration.";
// Disable use of Vulkan GPU, and use of the software-GL rasterizer. The
// Context will still run a GPU process, but will not support WebGL.
// Constants copied from //content/public/common/content_switches.cc:
static constexpr char kDisableGpu[] = "disable-gpu";
static constexpr char kDisableSoftwareRasterizer[] =
"disable-software-rasterizer";
launch_args.AppendSwitch(kDisableGpu);
launch_args.AppendSwitch(kDisableSoftwareRasterizer);
}
#if BUILDFLAG(ENABLE_WIDEVINE)
if (enable_widevine) {
launch_args.AppendSwitch(switches::kEnableWidevine);
}
#if BUILDFLAG(ENABLE_CAST_RECEIVER)
// Use a constexpr instead of the media::IsClearKey() helper, because of the
// additional dependencies required.
static constexpr char kClearKeyKeySystem[] = "org.w3.clearkey";
if (enable_playready) {
const std::string& key_system = params.playready_key_system();
if (key_system == kWidevineKeySystem || key_system == kClearKeyKeySystem) {
LOG(ERROR)
<< "Invalid value for CreateContextParams/playready_key_system: "
<< key_system;
return ZX_ERR_INVALID_ARGS;
}
launch_args.AppendSwitchNative(switches::kPlayreadyKeySystem, key_system);
}
#endif // BUILDFLAG(ENABLE_CAST_RECEIVER)
#endif // BUILDFLAG(ENABLE_WIDEVINE)
bool enable_audio = (features & fuchsia::web::ContextFeatureFlags::AUDIO) ==
fuchsia::web::ContextFeatureFlags::AUDIO;
if (!enable_audio) {
// TODO(fxbug.dev/58902): Split up audio input and output in
// ContextFeatureFlags.
// Constants copied from //media/base/media_switches.cc:
static constexpr char kDisableAudioInput[] = "disable-audio-input";
static constexpr char kDisableAudioOutput[] = "disable-audio-output";
launch_args.AppendSwitch(kDisableAudioOutput);
launch_args.AppendSwitch(kDisableAudioInput);
}
bool enable_hardware_video_decoder =
(features & fuchsia::web::ContextFeatureFlags::HARDWARE_VIDEO_DECODER) ==
fuchsia::web::ContextFeatureFlags::HARDWARE_VIDEO_DECODER;
if (!enable_hardware_video_decoder) {
// Constant copied from //content/public/common/content_switches.cc:
static constexpr char kDisableAcceleratedVideoDecode[] =
"disable-accelerated-video-decode";
launch_args.AppendSwitch(kDisableAcceleratedVideoDecode);
}
if (enable_hardware_video_decoder && !enable_vulkan) {
DLOG(ERROR) << "HARDWARE_VIDEO_DECODER requires VULKAN.";
return ZX_ERR_NOT_SUPPORTED;
}
bool disable_software_video_decoder =
(features &
fuchsia::web::ContextFeatureFlags::HARDWARE_VIDEO_DECODER_ONLY) ==
fuchsia::web::ContextFeatureFlags::HARDWARE_VIDEO_DECODER_ONLY;
if (disable_software_video_decoder) {
if (!enable_hardware_video_decoder) {
LOG(ERROR) << "Software video decoding may only be disabled if hardware "
"video decoding is enabled.";
return ZX_ERR_INVALID_ARGS;
}
AppendToSwitch(switches::kDisableFeatures,
features::kEnableSoftwareOnlyVideoCodecs.name, launch_args);
}
if (!HandleUserAgentParams(params, launch_args)) {
return ZX_ERR_INVALID_ARGS;
}
if (!HandleKeyboardFeatureFlags(features, launch_args)) {
return ZX_ERR_INVALID_ARGS;
}
HandleUnsafelyTreatInsecureOriginsAsSecureParam(params, launch_args);
HandleCorsExemptHeadersParam(params, launch_args);
HandleDisableCodeGenerationParam(features, launch_args);
return ZX_OK;
}
void AppendDynamicServices(fuchsia::web::ContextFeatureFlags features,
bool enable_playready,
std::vector<std::string>& services) {
using ::fuchsia::web::ContextFeatureFlags;
// Result of bitwise AND when no specified flag(s) are present.
const ContextFeatureFlags kNoFeaturesRequested =
static_cast<ContextFeatureFlags>(0);
// Features are listed here in order of their enum value.
static constexpr struct {
ContextFeatureFlags flag;
ContextFeatureFlags value;
std::string_view service;
} kServices[] = {
{ContextFeatureFlags::NETWORK, ContextFeatureFlags::NETWORK,
"fuchsia.net.interfaces.State"},
{ContextFeatureFlags::NETWORK, ContextFeatureFlags::NETWORK,
"fuchsia.net.name.Lookup"},
{ContextFeatureFlags::NETWORK, ContextFeatureFlags::NETWORK,
"fuchsia.posix.socket.Provider"},
{ContextFeatureFlags::AUDIO, ContextFeatureFlags::AUDIO,
"fuchsia.media.Audio"},
{ContextFeatureFlags::AUDIO, ContextFeatureFlags::AUDIO,
"fuchsia.media.AudioDeviceEnumerator"},
{ContextFeatureFlags::AUDIO, ContextFeatureFlags::AUDIO,
"fuchsia.media.SessionAudioConsumerFactory"},
{ContextFeatureFlags::VULKAN, ContextFeatureFlags::VULKAN,
"fuchsia.tracing.provider.Registry"},
{ContextFeatureFlags::VULKAN, ContextFeatureFlags::VULKAN,
"fuchsia.vulkan.loader.Loader"},
{ContextFeatureFlags::HARDWARE_VIDEO_DECODER,
ContextFeatureFlags::HARDWARE_VIDEO_DECODER,
"fuchsia.mediacodec.CodecFactory"},
// HARDWARE_VIDEO_DECODER_ONLY does not require any additional services.
#if BUILDFLAG(ENABLE_WIDEVINE)
{ContextFeatureFlags::WIDEVINE_CDM, ContextFeatureFlags::WIDEVINE_CDM,
"fuchsia.media.drm.Widevine"},
#endif
{ContextFeatureFlags::HEADLESS, kNoFeaturesRequested,
"fuchsia.accessibility.semantics.SemanticsManager"},
{ContextFeatureFlags::HEADLESS, kNoFeaturesRequested,
"fuchsia.ui.composition.Allocator"},
{ContextFeatureFlags::HEADLESS, kNoFeaturesRequested,
"fuchsia.ui.composition.Flatland"},
#if BUILDFLAG(ENABLE_CAST_RECEIVER)
{ContextFeatureFlags::LEGACYMETRICS, ContextFeatureFlags::LEGACYMETRICS,
"fuchsia.legacymetrics.MetricsRecorder"},
#endif
{ContextFeatureFlags::KEYBOARD, ContextFeatureFlags::KEYBOARD,
"fuchsia.ui.input3.Keyboard"},
{ContextFeatureFlags::VIRTUAL_KEYBOARD,
ContextFeatureFlags::VIRTUAL_KEYBOARD,
"fuchsia.input.virtualkeyboard.ControllerCreator"},
{ContextFeatureFlags::DISABLE_DYNAMIC_CODE_GENERATION, kNoFeaturesRequested,
"fuchsia.kernel.VmexResource"},
};
for (const auto& [flag, value, service] : kServices) {
if ((features & flag) == value) {
services.push_back(std::string(service));
}
}
#if BUILDFLAG(ENABLE_WIDEVINE) && BUILDFLAG(ENABLE_CAST_RECEIVER)
if (enable_playready) {
services.emplace_back("fuchsia.media.drm.PlayReady");
}
#endif
}