| // Copyright 2018 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/runners/cast/cast_runner.h" |
| |
| #include <fuchsia/web/cpp/fidl.h> |
| #include <lib/fit/function.h> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/command_line.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/fuchsia/file_utils.h" |
| #include "base/fuchsia/fuchsia_logging.h" |
| #include "base/fuchsia/process_context.h" |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "base/process/process.h" |
| #include "base/strings/strcat.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "components/cast/common/constants.h" |
| #include "components/fuchsia_component_support/config_reader.h" |
| #include "fuchsia_web/cast_streaming/cast_streaming.h" |
| #include "fuchsia_web/runners/cast/cast_streaming.h" |
| #include "fuchsia_web/runners/cast/pending_cast_component.h" |
| #include "fuchsia_web/runners/common/web_content_runner.h" |
| #include "fuchsia_web/webinstance_host/web_instance_host.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| |
| // Use a constexpr instead of the existing switch, because of the additional |
| // dependencies required. |
| constexpr char kAudioCapturerWithEchoCancellationSwitch[] = |
| "audio-capturer-with-echo-cancellation"; |
| |
| // Names used to partition the Runner's persistent storage for different uses. |
| constexpr char kCdmDataSubdirectoryName[] = "cdm_data"; |
| constexpr char kProfileSubdirectoryName[] = "web_profile"; |
| |
| // Name of the file used to detect cache erasure. |
| // TODO(crbug.com/1188780): Remove once an explicit cache flush signal exists. |
| constexpr char kSentinelFileName[] = ".sentinel"; |
| |
| // Ephemeral remote debugging port used by child contexts. |
| const uint16_t kEphemeralRemoteDebuggingPort = 0; |
| |
| // Subdirectory used to stage persistent directories to be deleted upon next |
| // startup. |
| const char kStagedForDeletionSubdirectory[] = "staged_for_deletion"; |
| |
| base::FilePath GetStagedForDeletionDirectoryPath() { |
| base::FilePath cache_directory(base::kPersistedCacheDirectoryPath); |
| return cache_directory.Append(kStagedForDeletionSubdirectory); |
| } |
| |
| // Deletes files/directories staged for deletion during the previous run. |
| // We delete synchronously on main thread for simplicity. Note that this |
| // overall mechanism is a temporary solution. TODO(crbug.com/1146480): migrate |
| // to the framework mechanism of clearing session data when available. |
| void DeleteStagedForDeletionDirectoryIfExists() { |
| const base::FilePath staged_for_deletion_directory = |
| GetStagedForDeletionDirectoryPath(); |
| |
| if (!PathExists(staged_for_deletion_directory)) |
| return; |
| |
| const base::TimeTicks started_at = base::TimeTicks::Now(); |
| bool result = base::DeletePathRecursively(staged_for_deletion_directory); |
| if (!result) { |
| LOG(ERROR) << "Failed to delete the staging directory"; |
| return; |
| } |
| |
| LOG(WARNING) << "Deleting old persistent data took " |
| << (base::TimeTicks::Now() - started_at).InMillisecondsF() |
| << " ms"; |
| } |
| |
| // TODO(crbug.com/1134719): Consider removing this flag once Media Capabilities |
| // is supported. |
| void EnsureSoftwareVideoDecodersAreDisabled( |
| ::fuchsia::web::ContextFeatureFlags* features) { |
| if ((*features & fuchsia::web::ContextFeatureFlags::HARDWARE_VIDEO_DECODER) == |
| fuchsia::web::ContextFeatureFlags::HARDWARE_VIDEO_DECODER) { |
| *features |= fuchsia::web::ContextFeatureFlags::HARDWARE_VIDEO_DECODER_ONLY; |
| } |
| } |
| |
| // Populates |params| with web data settings. Web data persistence is only |
| // enabled if a soft quota is explicitly specified via config-data. |
| // Exits the Runner process if creation of data storage fails for any reason. |
| void SetDataParamsForMainContext(fuchsia::web::CreateContextParams* params) { |
| // Set the web data quota based on the CastRunner configuration. |
| const absl::optional<base::Value::Dict>& config = |
| fuchsia_component_support::LoadPackageConfig(); |
| if (!config) |
| return; |
| |
| constexpr char kDataQuotaBytesSwitch[] = "data-quota-bytes"; |
| const absl::optional<int> data_quota_bytes = |
| config->FindInt(kDataQuotaBytesSwitch); |
| if (!data_quota_bytes) |
| return; |
| |
| // Allow best-effort persistent of Cast application data. |
| // TODO(crbug.com/1148334): Remove the need for an explicit quota to be |
| // configured, once the platform provides storage quotas. |
| const auto profile_path = base::FilePath(base::kPersistedCacheDirectoryPath) |
| .Append(kProfileSubdirectoryName); |
| auto error_code = base::File::Error::FILE_OK; |
| if (!base::CreateDirectoryAndGetError(profile_path, &error_code)) { |
| LOG(ERROR) << "Failed to create profile directory: " << error_code; |
| base::Process::TerminateCurrentProcessImmediately(1); |
| } |
| params->set_data_directory(base::OpenDirectoryHandle(profile_path)); |
| if (!params->data_directory()) { |
| LOG(ERROR) << "Unable to open data to transfer"; |
| base::Process::TerminateCurrentProcessImmediately(1); |
| } |
| params->set_data_quota_bytes(*data_quota_bytes); |
| } |
| |
| // Populates |params| with settings to enable Widevine & PlayReady CDMs. |
| // CDM data persistence is always enabled, with an optional soft quota. |
| // Exits the Runner if creation of CDM storage fails for any reason. |
| void SetCdmParamsForMainContext(fuchsia::web::CreateContextParams* params) { |
| const absl::optional<base::Value::Dict>& config = |
| fuchsia_component_support::LoadPackageConfig(); |
| if (config) { |
| constexpr char kCdmDataQuotaBytesSwitch[] = "cdm-data-quota-bytes"; |
| const absl::optional<int> cdm_data_quota_bytes = |
| config->FindInt(kCdmDataQuotaBytesSwitch); |
| if (cdm_data_quota_bytes) |
| params->set_cdm_data_quota_bytes(*cdm_data_quota_bytes); |
| } |
| |
| // TODO(b/154204041): Consider using isolated-persistent-storage for CDM data. |
| // Create an isolated-cache-storage sub-directory for CDM data. |
| const auto cdm_data_path = base::FilePath(base::kPersistedCacheDirectoryPath) |
| .Append(kCdmDataSubdirectoryName); |
| auto error_code = base::File::Error::FILE_OK; |
| if (!base::CreateDirectoryAndGetError(cdm_data_path, &error_code)) { |
| LOG(ERROR) << "Failed to create cache directory: " << error_code; |
| base::Process::TerminateCurrentProcessImmediately(1); |
| } |
| params->set_cdm_data_directory(base::OpenDirectoryHandle(cdm_data_path)); |
| if (!params->cdm_data_directory()) { |
| LOG(ERROR) << "Unable to open cdm_data to transfer"; |
| base::Process::TerminateCurrentProcessImmediately(1); |
| } |
| |
| // Enable the Widevine and Playready CDMs. |
| *params->mutable_features() |= |
| fuchsia::web::ContextFeatureFlags::WIDEVINE_CDM; |
| const char kCastPlayreadyKeySystem[] = "com.chromecast.playready"; |
| params->set_playready_key_system(kCastPlayreadyKeySystem); |
| } |
| |
| } // namespace |
| |
| CastRunner::CastRunner(WebInstanceHost& web_instance_host, Options options) |
| : web_instance_host_(web_instance_host), |
| is_headless_(options.headless), |
| disable_codegen_(options.disable_codegen), |
| main_context_(std::make_unique<WebContentRunner>( |
| base::BindRepeating( |
| &WebInstanceHost::CreateInstanceForContextWithCopiedArgs, |
| base::Unretained(&web_instance_host_.get())), |
| base::BindRepeating(&CastRunner::GetMainWebInstanceConfig, |
| base::Unretained(this)))) { |
| // Delete persisted data staged for deletion during the previous run. |
| DeleteStagedForDeletionDirectoryIfExists(); |
| |
| // Fetch the list of CORS-exempt headers to apply for all components launched |
| // under this Runner. |
| zx_status_t status = base::ComponentContextForProcess()->svc()->Connect( |
| cors_exempt_headers_provider_.NewRequest()); |
| ZX_CHECK(status == ZX_OK, status) |
| << "Connect CorsExemptHeaderProvider failed"; |
| cors_exempt_headers_provider_.set_error_handler( |
| base::LogFidlErrorAndExitProcess(FROM_HERE, "CorsExemptHeaderProvider")); |
| cors_exempt_headers_provider_->GetCorsExemptHeaderNames( |
| [this](std::vector<std::vector<uint8_t>> header_names) { |
| cors_exempt_headers_provider_.Unbind(); |
| cors_exempt_headers_ = std::move(header_names); |
| for (auto& callback : on_have_cors_exempt_headers_) |
| std::move(callback).Run(); |
| on_have_cors_exempt_headers_.clear(); |
| }); |
| } |
| |
| CastRunner::~CastRunner() = default; |
| |
| void CastRunner::Start( |
| fuchsia::component::runner::ComponentStartInfo start_info, |
| fidl::InterfaceRequest<fuchsia::component::runner::ComponentController> |
| controller_request) { |
| // Verify that |package| specifies a Cast URI, and pull the app-Id from it. |
| constexpr char kCastPresentationUrlScheme[] = "cast"; |
| constexpr char kCastSecurePresentationUrlScheme[] = "casts"; |
| |
| GURL cast_url(start_info.has_resolved_url() ? start_info.resolved_url() : ""); |
| if (!cast_url.is_valid() || |
| (!cast_url.SchemeIs(kCastPresentationUrlScheme) && |
| !cast_url.SchemeIs(kCastSecurePresentationUrlScheme)) || |
| cast_url.GetContent().empty()) { |
| LOG(ERROR) << "Rejected invalid URL: " << cast_url; |
| return; |
| } |
| |
| auto startup_context = |
| std::make_unique<base::StartupContext>(std::move(start_info)); |
| |
| // If the persistent cache directory was erased then re-create the main Cast |
| // app Context. |
| if (WasPersistedCacheErased()) { |
| LOG(WARNING) << "Cache erased. Restarting web.Context."; |
| // The sentinel file will be re-created the next time CreateContextParams |
| // are request for the main web.Context. |
| was_cache_sentinel_created_ = false; |
| main_context_->DestroyWebContext(); |
| } |
| |
| if (cors_exempt_headers_) { |
| StartComponentInternal(cast_url, std::move(startup_context), |
| std::move(controller_request)); |
| } else { |
| // Queue the component launch to be resumed once the header list is |
| // available. |
| on_have_cors_exempt_headers_.push_back(base::BindOnce( |
| &CastRunner::StartComponentInternal, base::Unretained(this), cast_url, |
| std::move(startup_context), std::move(controller_request))); |
| } |
| } |
| |
| void CastRunner::DeletePersistentData(DeletePersistentDataCallback callback) { |
| if (data_reset_in_progress_) { |
| // Repeated requests to DeletePersistentData are not supported. |
| // It is expected that the Runner be requested to terminate shortly after |
| // data-reset is received, in any case. |
| callback(false); |
| return; |
| } |
| |
| callback(DeletePersistentDataInternal()); |
| } |
| |
| void CastRunner::LaunchPendingComponent(PendingCastComponent* pending_component, |
| CastComponent::Params params) { |
| DCHECK(cors_exempt_headers_); |
| |
| // TODO(crbug.com/1082821): Remove |web_content_url| once the Cast Streaming |
| // Receiver component has been implemented. |
| GURL web_content_url(params.application_config.web_url()); |
| if (IsAppConfigForCastStreaming(params.application_config)) |
| web_content_url = GURL(kCastStreamingWebUrl); |
| |
| auto web_instance_config = |
| GetWebInstanceConfigForAppConfig(¶ms.application_config); |
| |
| WebContentRunner* component_owner = main_context_.get(); |
| if (web_instance_config) { |
| component_owner = |
| CreateIsolatedRunner(std::move(web_instance_config.value())); |
| } |
| |
| auto cast_component = std::make_unique<CastComponent>( |
| base::StrCat({"cast:", pending_component->app_id()}), component_owner, |
| std::move(params), is_headless_); |
| |
| // Start the component, which creates and configures the web.Frame, and load |
| // the specified web content into it. |
| cast_component->StartComponent(); |
| cast_component->LoadUrl(std::move(web_content_url), |
| std::vector<fuchsia::net::http::Header>()); |
| |
| if (component_owner == main_context_.get()) { |
| // For components in the main Context the cache sentinel file should have |
| // been created as a side-effect of |CastComponent::StartComponent()|. |
| DCHECK(was_cache_sentinel_created_); |
| } |
| |
| // Do not launch new main context components while data reset is in progress, |
| // so that they don't create new persisted state. We expect the session |
| // to be restarted shortly after data reset completes. |
| if (data_reset_in_progress_ && component_owner == main_context_.get()) { |
| pending_components_.erase(pending_component); |
| return; |
| } |
| |
| // Register the new component and clean up the |pending_component|. |
| component_owner->RegisterComponent(std::move(cast_component)); |
| pending_components_.erase(pending_component); |
| } |
| |
| void CastRunner::CancelPendingComponent( |
| PendingCastComponent* pending_component) { |
| size_t count = pending_components_.erase(pending_component); |
| DCHECK_EQ(count, 1u); |
| } |
| |
| WebContentRunner::WebInstanceConfig CastRunner::GetCommonWebInstanceConfig() { |
| DCHECK(cors_exempt_headers_); |
| |
| WebContentRunner::WebInstanceConfig config; |
| |
| static constexpr char const* kSwitchesToCopy[] = { |
| // Must match the value in `content/public/common/content_switches.cc`. |
| "enable-logging", |
| // Must match the value in `ui/ozone/public/ozone_switches.cc`. |
| "ozone-platform", |
| }; |
| config.extra_args.CopySwitchesFrom(*base::CommandLine::ForCurrentProcess(), |
| kSwitchesToCopy); |
| |
| config.params.set_features(fuchsia::web::ContextFeatureFlags::AUDIO); |
| |
| if (is_headless_) { |
| LOG(WARNING) << "Running in headless mode."; |
| *config.params.mutable_features() |= |
| fuchsia::web::ContextFeatureFlags::HEADLESS; |
| } else { |
| *config.params.mutable_features() |= |
| fuchsia::web::ContextFeatureFlags::HARDWARE_VIDEO_DECODER | |
| fuchsia::web::ContextFeatureFlags::VULKAN; |
| } |
| |
| if (disable_codegen_) { |
| *config.params.mutable_features() |= |
| fuchsia::web::ContextFeatureFlags::DISABLE_DYNAMIC_CODE_GENERATION; |
| } |
| |
| // When tests require that VULKAN be disabled, DRM must also be disabled. |
| if (disable_vulkan_for_test_) { |
| *config.params.mutable_features() &= |
| ~(fuchsia::web::ContextFeatureFlags::VULKAN | |
| fuchsia::web::ContextFeatureFlags::HARDWARE_VIDEO_DECODER); |
| } |
| |
| // If there is a list of headers to exempt from CORS checks, pass the list |
| // along to the Context. |
| if (!cors_exempt_headers_->empty()) |
| config.params.set_cors_exempt_headers(*cors_exempt_headers_); |
| |
| return config; |
| } |
| |
| WebContentRunner::WebInstanceConfig CastRunner::GetMainWebInstanceConfig() { |
| auto config = GetCommonWebInstanceConfig(); |
| |
| // The fuchsia.media.Audio implementation provided to the Runner in existing |
| // integrations always supports echo cancellation. |
| // |
| // TODO(crbug.com/852834): Remove once AudioManagerFuchsia is updated to |
| // get this information from AudioCapturerFactory. |
| config.extra_args.AppendSwitch(kAudioCapturerWithEchoCancellationSwitch); |
| |
| *config.params.mutable_features() |= |
| fuchsia::web::ContextFeatureFlags::NETWORK | |
| fuchsia::web::ContextFeatureFlags::LEGACYMETRICS | |
| fuchsia::web::ContextFeatureFlags::KEYBOARD | |
| fuchsia::web::ContextFeatureFlags::VIRTUAL_KEYBOARD; |
| EnsureSoftwareVideoDecodersAreDisabled(config.params.mutable_features()); |
| config.params.set_remote_debugging_port(CastRunner::kRemoteDebuggingPort); |
| |
| config.params.set_user_agent_product("CrKey"); |
| config.params.set_user_agent_version(chromecast::kFrozenCrKeyValue); |
| |
| if (!disable_vulkan_for_test_) { |
| SetCdmParamsForMainContext(&config.params); |
| } |
| |
| SetDataParamsForMainContext(&config.params); |
| |
| // Create a sentinel file to detect if the cache is erased. |
| // TODO(crbug.com/1188780): Remove once an explicit cache flush signal exists. |
| CreatePersistedCacheSentinel(); |
| |
| // TODO(crbug.com/1023514): Remove this switch when it is no longer |
| // necessary. |
| config.params.set_unsafely_treat_insecure_origins_as_secure( |
| {"allow-running-insecure-content", "disable-mixed-content-autoupgrade"}); |
| |
| return config; |
| } |
| |
| WebContentRunner::WebInstanceConfig |
| CastRunner::GetIsolatedWebInstanceConfigWithFuchsiaDirs( |
| std::vector<fuchsia::web::ContentDirectoryProvider> content_directories) { |
| auto config = GetCommonWebInstanceConfig(); |
| |
| EnsureSoftwareVideoDecodersAreDisabled(config.params.mutable_features()); |
| *config.params.mutable_features() |= |
| fuchsia::web::ContextFeatureFlags::NETWORK; |
| config.params.set_remote_debugging_port(kEphemeralRemoteDebuggingPort); |
| config.params.set_content_directories(std::move(content_directories)); |
| |
| return config; |
| } |
| |
| WebContentRunner::WebInstanceConfig |
| CastRunner::GetIsolatedWebInstanceConfigForCastStreaming() { |
| auto config = GetCommonWebInstanceConfig(); |
| |
| ApplyCastStreamingContextParams(&config.params); |
| config.params.set_remote_debugging_port(kEphemeralRemoteDebuggingPort); |
| |
| return config; |
| } |
| |
| absl::optional<WebContentRunner::WebInstanceConfig> |
| CastRunner::GetWebInstanceConfigForAppConfig( |
| chromium::cast::ApplicationConfig* app_config) { |
| if (IsAppConfigForCastStreaming(*app_config)) { |
| // TODO(crbug.com/1082821): Remove this once the CastStreamingReceiver |
| // Component has been implemented. |
| return absl::make_optional(GetIsolatedWebInstanceConfigForCastStreaming()); |
| } |
| |
| const bool is_isolated_app = |
| app_config->has_content_directories_for_isolated_application(); |
| if (is_isolated_app) { |
| return absl::make_optional( |
| GetIsolatedWebInstanceConfigWithFuchsiaDirs(std::move( |
| *app_config |
| ->mutable_content_directories_for_isolated_application()))); |
| } |
| |
| // No need to create an isolated context in other cases. |
| return absl::nullopt; |
| } |
| |
| WebContentRunner* CastRunner::CreateIsolatedRunner( |
| WebContentRunner::WebInstanceConfig config) { |
| // Create an isolated context which will own the CastComponent. |
| auto context = std::make_unique<WebContentRunner>( |
| base::BindRepeating( |
| &WebInstanceHost::CreateInstanceForContextWithCopiedArgs, |
| base::Unretained(&web_instance_host_.get())), |
| std::move(config)); |
| context->SetOnEmptyCallback( |
| base::BindOnce(&CastRunner::OnIsolatedContextEmpty, |
| base::Unretained(this), base::Unretained(context.get()))); |
| WebContentRunner* raw_context = context.get(); |
| isolated_contexts_.insert(std::move(context)); |
| return raw_context; |
| } |
| |
| void CastRunner::OnIsolatedContextEmpty(WebContentRunner* context) { |
| auto it = isolated_contexts_.find(context); |
| DCHECK(it != isolated_contexts_.end()); |
| isolated_contexts_.erase(it); |
| } |
| |
| void CastRunner::StartComponentInternal( |
| const GURL& url, |
| std::unique_ptr<base::StartupContext> startup_context, |
| fidl::InterfaceRequest<fuchsia::component::runner::ComponentController> |
| controller_request) { |
| pending_components_.emplace(std::make_unique<PendingCastComponent>( |
| this, std::move(startup_context), std::move(controller_request), |
| url.GetContent())); |
| } |
| |
| static base::FilePath SentinelFilePath() { |
| return base::FilePath(base::kPersistedCacheDirectoryPath) |
| .Append(kSentinelFileName); |
| } |
| |
| bool CastRunner::DeletePersistentDataInternal() { |
| // Set data reset flag so that new components are not being started. |
| data_reset_in_progress_ = true; |
| |
| // Create the staging directory. |
| base::FilePath staged_for_deletion_directory = |
| GetStagedForDeletionDirectoryPath(); |
| base::File::Error file_error; |
| bool result = base::CreateDirectoryAndGetError(staged_for_deletion_directory, |
| &file_error); |
| if (!result) { |
| LOG(ERROR) << "Failed to create the staging directory, error: " |
| << file_error; |
| return false; |
| } |
| |
| // Stage everything under `/cache` for deletion. |
| const base::FilePath cache_directory(base::kPersistedCacheDirectoryPath); |
| base::FileEnumerator enumerator( |
| cache_directory, /*recursive=*/false, |
| base::FileEnumerator::FileType::FILES | |
| base::FileEnumerator::FileType::DIRECTORIES); |
| for (base::FilePath current = enumerator.Next(); !current.empty(); |
| current = enumerator.Next()) { |
| // Skip the staging directory itself. |
| if (current == staged_for_deletion_directory) { |
| continue; |
| } |
| |
| base::FilePath destination = |
| staged_for_deletion_directory.Append(current.BaseName()); |
| result = base::Move(current, destination); |
| if (!result) { |
| LOG(ERROR) << "Failed to move " << current << " to " << destination; |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| fidl::InterfaceRequestHandler<fuchsia::web::FrameHost> |
| CastRunner::GetFrameHostRequestHandler() { |
| return [this](fidl::InterfaceRequest<fuchsia::web::FrameHost> request) { |
| if (!cors_exempt_headers_) { |
| on_have_cors_exempt_headers_.push_back(base::BindOnce( |
| [](fidl::InterfaceRequestHandler<fuchsia::web::FrameHost> |
| request_handler, |
| fidl::InterfaceRequest<fuchsia::web::FrameHost> request) { |
| request_handler(std::move(request)); |
| }, |
| main_context_->GetFrameHostRequestHandler(), std::move(request))); |
| return; |
| } |
| main_context_->GetFrameHostRequestHandler()(std::move(request)); |
| }; |
| } |
| |
| void CastRunner::CreatePersistedCacheSentinel() { |
| base::WriteFile(SentinelFilePath(), ""); |
| was_cache_sentinel_created_ = true; |
| } |
| |
| bool CastRunner::WasPersistedCacheErased() { |
| if (!was_cache_sentinel_created_) |
| return false; |
| return !base::PathExists(SentinelFilePath()); |
| } |