| // Copyright 2024 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/extensions/api/messaging/launch_context.h" |
| |
| #include <inttypes.h> |
| |
| #include <utility> |
| |
| #include "base/base64.h" |
| #include "base/base_paths.h" |
| #include "base/base_switches.h" |
| #include "base/check.h" |
| #include "base/command_line.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/json/json_writer.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/path_service.h" |
| #include "base/process/kill.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/current_thread.h" |
| #include "base/task/task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/extensions/api/messaging/native_messaging_host_manifest.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "components/app_launch_prefetch/app_launch_prefetch.h" |
| #include "net/base/file_stream.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| void TerminateNativeProcess(base::Process native_process) { |
| // Kill the host process if necessary to make sure we don't leave zombies. |
| // TODO(crbug.com/41367359): On OSX EnsureProcessTerminated() may |
| // block, so we have to post a task on the blocking pool. |
| #if BUILDFLAG(IS_MAC) |
| base::ThreadPool::PostTask(FROM_HERE, |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}, |
| base::BindOnce(&base::EnsureProcessTerminated, |
| std::move(native_process))); |
| #else |
| base::EnsureProcessTerminated(std::move(native_process)); |
| #endif |
| } |
| |
| } // namespace |
| |
| // static |
| std::unique_ptr<LaunchContext> LaunchContext::Start( |
| bool allow_user_level_hosts, |
| bool require_native_initiated_connections, |
| bool native_hosts_executables_launch_directly, |
| intptr_t window_handle, |
| base::FilePath profile_directory, |
| std::string connect_id, |
| std::string error_arg, |
| GURL origin, |
| std::string native_host_name, |
| scoped_refptr<base::TaskRunner> background_task_runner, |
| NativeProcessLauncher::LaunchedCallback callback) { |
| // Must run on the IO thread. |
| CHECK(base::CurrentIOThread::IsSet()); |
| |
| auto instance = base::WrapUnique( |
| new LaunchContext(background_task_runner, std::move(callback))); |
| background_task_runner->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&LaunchInBackground, allow_user_level_hosts, |
| require_native_initiated_connections, |
| native_hosts_executables_launch_directly, window_handle, |
| std::move(profile_directory), std::move(connect_id), |
| std::move(error_arg), std::move(origin), |
| std::move(native_host_name)), |
| base::BindOnce(&OnProcessLaunched, instance->GetWeakPtr())); |
| return instance; |
| } |
| |
| LaunchContext::~LaunchContext() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| LaunchContext::LaunchContext( |
| scoped_refptr<base::TaskRunner> background_task_runner, |
| NativeProcessLauncher::LaunchedCallback callback) |
| : background_task_runner_(std::move(background_task_runner)), |
| callback_(std::move(callback)) {} |
| |
| LaunchContext::ProcessState::ProcessState() = default; |
| LaunchContext::ProcessState::ProcessState(base::Process process, |
| base::ScopedPlatformFile read_file, |
| base::ScopedPlatformFile write_file) |
| : process(std::move(process)), |
| read_file(std::move(read_file)), |
| write_file(std::move(write_file)) {} |
| LaunchContext::ProcessState::ProcessState(ProcessState&& other) noexcept = |
| default; |
| LaunchContext::ProcessState& LaunchContext::ProcessState::operator=( |
| ProcessState&& other) noexcept = default; |
| LaunchContext::ProcessState::~ProcessState() = default; |
| |
| LaunchContext::BackgroundLaunchResult::BackgroundLaunchResult( |
| NativeProcessLauncher::LaunchResult result) |
| : result(result), process_state(std::nullopt) {} |
| LaunchContext::BackgroundLaunchResult::BackgroundLaunchResult( |
| ProcessState process_state) |
| : result(NativeProcessLauncher::RESULT_SUCCESS), |
| process_state(std::move(process_state)) {} |
| LaunchContext::BackgroundLaunchResult::BackgroundLaunchResult( |
| BackgroundLaunchResult&& other) noexcept = default; |
| LaunchContext::BackgroundLaunchResult& |
| LaunchContext::BackgroundLaunchResult::operator=( |
| BackgroundLaunchResult&& other) noexcept = default; |
| LaunchContext::BackgroundLaunchResult::~BackgroundLaunchResult() = default; |
| |
| // static |
| LaunchContext::BackgroundLaunchResult LaunchContext::LaunchInBackground( |
| bool allow_user_level_hosts, |
| bool require_native_initiated_connections, |
| bool native_hosts_executables_launch_directly, |
| intptr_t window_handle, |
| const base::FilePath& profile_directory, |
| const std::string& connect_id, |
| const std::string& error_arg, |
| const GURL& origin, |
| const std::string& native_host_name) { |
| if (!NativeMessagingHostManifest::IsValidName(native_host_name)) { |
| return BackgroundLaunchResult(NativeProcessLauncher::RESULT_INVALID_NAME); |
| } |
| |
| std::string error_message; |
| base::FilePath manifest_path = |
| FindManifest(native_host_name, allow_user_level_hosts, error_message); |
| |
| if (manifest_path.empty()) { |
| LOG(WARNING) << "Can't find manifest for native messaging host " |
| << native_host_name; |
| return BackgroundLaunchResult(NativeProcessLauncher::RESULT_NOT_FOUND); |
| } |
| |
| std::unique_ptr<NativeMessagingHostManifest> manifest = |
| NativeMessagingHostManifest::Load(manifest_path, &error_message); |
| |
| if (!manifest) { |
| LOG(WARNING) << "Failed to load manifest for native messaging host " |
| << native_host_name << ": " << error_message; |
| return BackgroundLaunchResult(NativeProcessLauncher::RESULT_NOT_FOUND); |
| } |
| |
| if (manifest->name() != native_host_name) { |
| LOG(WARNING) << "Failed to load manifest for native messaging host " |
| << native_host_name |
| << ": Invalid name specified in the manifest."; |
| return BackgroundLaunchResult(NativeProcessLauncher::RESULT_NOT_FOUND); |
| } |
| |
| if (!manifest->allowed_origins().MatchesSecurityOrigin(origin)) { |
| // Not an allowed origin. |
| return BackgroundLaunchResult(NativeProcessLauncher::RESULT_FORBIDDEN); |
| } |
| |
| if (require_native_initiated_connections && |
| !manifest->supports_native_initiated_connections()) { |
| return BackgroundLaunchResult(NativeProcessLauncher::RESULT_FORBIDDEN); |
| } |
| |
| base::FilePath host_path = manifest->path(); |
| if (!host_path.IsAbsolute()) { |
| // On Windows host path is allowed to be relative to the location of the |
| // manifest file. On all other platforms the path must be absolute. |
| #if BUILDFLAG(IS_WIN) |
| host_path = manifest_path.DirName().Append(host_path); |
| #else // BUILDFLAG(IS_WIN) |
| LOG(WARNING) << "Native messaging host path must be absolute for " |
| << native_host_name; |
| return BackgroundLaunchResult(NativeProcessLauncher::RESULT_NOT_FOUND); |
| #endif // BUILDFLAG(IS_WIN) |
| } |
| |
| // In case when the manifest file is there, but the host binary doesn't exist |
| // report the NOT_FOUND error. |
| if (!base::PathExists(host_path)) { |
| LOG(WARNING) |
| << "Found manifest, but not the binary for native messaging host " |
| << native_host_name << ". Host path specified in the manifest: " |
| << host_path.AsUTF8Unsafe(); |
| return BackgroundLaunchResult(NativeProcessLauncher::RESULT_NOT_FOUND); |
| } |
| |
| base::CommandLine command_line(host_path); |
| // Note: The origin must be the first argument, so do not use AppendSwitch* |
| // hereafter because CommandLine inserts these switches before the other |
| // arguments. |
| command_line.AppendArg(origin.spec()); |
| |
| // Pass handle of the native view window to the native messaging host. This |
| // way the host will be able to create properly focused UI windows. |
| #if BUILDFLAG(IS_WIN) |
| command_line.AppendArg( |
| base::StringPrintf("--parent-window=%" PRIdPTR, window_handle)); |
| #endif // !BUILDFLAG(IS_WIN) |
| |
| bool send_connect_id = false; |
| if (!error_arg.empty()) { |
| send_connect_id = true; |
| command_line.AppendArg(error_arg); |
| } else if (manifest->supports_native_initiated_connections() && |
| !profile_directory.empty()) { |
| send_connect_id = true; |
| base::FilePath exe_path; |
| base::PathService::Get(base::FILE_EXE, &exe_path); |
| |
| base::CommandLine reconnect_command_line(exe_path); |
| reconnect_command_line.AppendSwitch(::switches::kNoStartupWindow); |
| reconnect_command_line.AppendSwitchASCII( |
| ::switches::kNativeMessagingConnectHost, native_host_name); |
| reconnect_command_line.AppendSwitchASCII( |
| ::switches::kNativeMessagingConnectExtension, origin.host()); |
| reconnect_command_line.AppendSwitchASCII(::switches::kEnableFeatures, |
| features::kOnConnectNative.name); |
| reconnect_command_line.AppendSwitchPath(::switches::kProfileDirectory, |
| profile_directory.BaseName()); |
| reconnect_command_line.AppendSwitchPath(::switches::kUserDataDir, |
| profile_directory.DirName()); |
| #if BUILDFLAG(IS_WIN) |
| reconnect_command_line.AppendArgNative( |
| app_launch_prefetch::GetPrefetchSwitch( |
| app_launch_prefetch::SubprocessType::kBrowserBackground)); |
| #endif |
| base::Value::List args; |
| for (const auto& arg : reconnect_command_line.argv()) { |
| #if BUILDFLAG(IS_WIN) |
| args.Append(base::WideToUTF8(arg)); |
| #else |
| args.Append(arg); |
| #endif |
| } |
| std::string encoded_reconnect_command; |
| bool success = |
| base::JSONWriter::Write(std::move(args), &encoded_reconnect_command); |
| DCHECK(success); |
| command_line.AppendArg( |
| base::StrCat({"--reconnect-command=", |
| base::Base64Encode(encoded_reconnect_command)})); |
| } |
| |
| if (send_connect_id && !connect_id.empty()) { |
| command_line.AppendArg(base::StrCat( |
| {"--", switches::kNativeMessagingConnectId, "=", connect_id})); |
| } |
| |
| if (auto state = LaunchNativeProcess( |
| command_line, native_hosts_executables_launch_directly)) { |
| return BackgroundLaunchResult(*std::move(state)); |
| } |
| return BackgroundLaunchResult(NativeProcessLauncher::RESULT_FAILED_TO_START); |
| } |
| |
| // static |
| void LaunchContext::OnProcessLaunched(base::WeakPtr<LaunchContext> weak_this, |
| BackgroundLaunchResult result) { |
| if (!weak_this) { |
| // The launch was cancelled. Terminate the host process and close the pipes. |
| if (result.process_state.has_value()) { |
| auto& process_state = result.process_state.value(); |
| TerminateNativeProcess(std::move(process_state.process)); |
| // Close the pipes on a background thread since doing so may block. |
| // Ownership of the pipes is passed to a callback that is run in the |
| // thread pool. They will be closed automatically when they go out of |
| // scope when the callback is run. |
| base::ThreadPool::PostTask( |
| FROM_HERE, |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}, |
| base::BindOnce( |
| [](base::ScopedPlatformFile read_file, |
| base::ScopedPlatformFile write_file) { |
| // Execution of this lambda will lead to destruction of |
| // `read_file` and `write_file`. |
| }, |
| std::move(process_state.read_file), |
| std::move(process_state.write_file))); |
| } |
| return; |
| } |
| DCHECK_CALLED_ON_VALID_SEQUENCE(weak_this->sequence_checker_); |
| if (result.result == NativeProcessLauncher::RESULT_SUCCESS) { |
| weak_this->native_process_ = std::move(result.process_state->process); |
| weak_this->ConnectPipes(std::move(result.process_state->read_file), |
| std::move(result.process_state->write_file)); |
| } else { |
| weak_this->OnFailure(result.result); |
| } |
| } |
| |
| void LaunchContext::OnSuccess(base::PlatformFile read_file, |
| std::unique_ptr<net::FileStream> read_stream, |
| std::unique_ptr<net::FileStream> write_stream) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| std::move(callback_).Run(NativeProcessLauncher::RESULT_SUCCESS, |
| std::move(native_process_), read_file, |
| std::move(read_stream), std::move(write_stream)); |
| } |
| |
| void LaunchContext::OnFailure( |
| NativeProcessLauncher::LaunchResult launch_result) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (native_process_.IsValid()) { |
| TerminateNativeProcess(std::move(native_process_)); |
| } |
| std::move(callback_).Run(launch_result, base::Process(), |
| base::kInvalidPlatformFile, nullptr, nullptr); |
| } |
| |
| } // namespace extensions |