Skip to content

Commit

Permalink
extensions: add webAuthenticationProxy.onRemoteSessionStateChange event
Browse files Browse the repository at this point in the history
CRD's webAuthenticationProxy extension needs to be kept informed by the
native host app whenever a remote session is attached or detached. To
accomplish this, the extension ServiceWorker communicates via the native
messaging host with the CRD host process. But the CRD host needs to be
able to signal session state changes even when the ServiceWorker and NMH
have been terminated.

This change introduces an event that the CRD host process can trigger in
response to a session attachment change by writing to a file at
`WebAuthenticationRemoteSessionStateChange/<extension id>` inside the
default user data directory. (*Default* UDD because we don't want the
extension/NMH/app having to discover the correct location.)

Writing to that file raises the event to the extension with that ID. The
contents of the file are ignored, and the event has no parameters. This
is simply a mechanism to wake up the extension and instruct it to fetch
the current session attachment state from the CRD host.

(This change also restricts the API to Win, Mac and Linux, because those
are the only platforms we're targeting for this API. So we don't need to
pick file paths for other OSes.)

Bug: 1231802
Change-Id: I23409d6f23fd76c1a27ea96ad96ee7699bdeaa8c
Cq-Do-Not-Cancel-Tryjobs: true
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3514116
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Commit-Queue: Martin Kreichgauer <martinkr@google.com>
Cr-Commit-Position: refs/heads/main@{#989094}
  • Loading branch information
kreichgauer authored and Chromium LUCI CQ committed Apr 5, 2022
1 parent ace8322 commit cdbdd06
Show file tree
Hide file tree
Showing 19 changed files with 392 additions and 45 deletions.
2 changes: 2 additions & 0 deletions chrome/browser/extensions/BUILD.gn
Expand Up @@ -392,6 +392,8 @@ static_library("extensions") {
"api/tabs/windows_util.h",
"api/top_sites/top_sites_api.cc",
"api/top_sites/top_sites_api.h",
"api/web_authentication_proxy/remote_session_state_change.cc",
"api/web_authentication_proxy/remote_session_state_change.h",
"api/web_authentication_proxy/value_conversions.cc",
"api/web_authentication_proxy/value_conversions.h",
"api/web_authentication_proxy/web_authentication_proxy_api.cc",
Expand Down
Expand Up @@ -12,11 +12,11 @@
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/common/chrome_paths.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/service_worker_test_helpers.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.h"
#include "extensions/browser/browsertest_util.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/test/result_catcher.h"

Expand Down Expand Up @@ -152,16 +152,8 @@ IN_PROC_BROWSER_TEST_F(DeveloperPrivateApiTest,
ASSERT_TRUE(result_catcher.GetNextResult());

// Stop the service worker.
{
base::RunLoop run_loop;
content::StoragePartition* storage_partition =
profile()->GetDefaultStoragePartition();
content::ServiceWorkerContext* context =
storage_partition->GetServiceWorkerContext();
content::StopServiceWorkerForScope(context, extension->url(),
run_loop.QuitClosure());
run_loop.Run();
}
browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(),
extension->id());

// Get the info about the extension, including the inspectable views.
auto get_info_function =
Expand Down
Expand Up @@ -7,11 +7,10 @@
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/service_worker_test_helpers.h"
#include "extensions/browser/api/messaging/message_service.h"
#include "extensions/browser/browsertest_util.h"
#include "extensions/browser/service_worker/service_worker_test_utils.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
Expand Down Expand Up @@ -82,18 +81,11 @@ class ServiceWorkerMessagingTest : public ExtensionApiTest {
}

protected:
// TODO(lazyboy): Move this to a common place so it can be shared with other
// tests.
void StopServiceWorker(const Extension& extension) {
content::StoragePartition* storage_partition =
browser()->profile()->GetDefaultStoragePartition();
content::ServiceWorkerContext* context =
storage_partition->GetServiceWorkerContext();
base::RunLoop run_loop;
// The service worker is registered at the root scope.
content::StopServiceWorkerForScope(context, extension.url(),
run_loop.QuitClosure());
run_loop.Run();
// TODO(lazyboy): Ideally we'd want to test worker shutdown on idle, do that
// once //content API allows to override test timeouts for Service Workers.
browsertest_util::StopServiceWorkerForExtensionGlobalScope(
browser()->profile(), extension.id());
}

extensions::ScopedTestNativeMessagingHost test_host_;
Expand Down
@@ -0,0 +1,110 @@
// Copyright 2022 The Chromium 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 "chrome/browser/extensions/api/web_authentication_proxy/remote_session_state_change.h"

#include <memory>

#include "base/base_paths.h"
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/task/bind_post_task.h"
#include "chrome/common/chrome_paths_internal.h"
#include "chrome/common/extensions/api/web_authentication_proxy.h"

namespace extensions {

namespace {

void OnSessionStateChangeFileUpdate(
const base::FilePath& watched_path,
const base::RepeatingCallback<void()>& update_callback,
const base::FilePath& path,
bool is_error) {
DCHECK_EQ(path, watched_path);
if (is_error) {
DLOG(ERROR) << "OnRemoteSessionStateFileUpdate() error";
return;
}
update_callback.Run();
}

void WatchSessionStateChangeFile(
base::FilePathWatcher* watcher,
const ExtensionId& extension_id,
base::RepeatingCallback<void()> update_callback) {
base::FilePath dir;
if (!WebAuthenticationProxyRemoteSessionStateChangeNotifier::
GetSessionStateChangeDir(&dir)) {
DLOG(ERROR) << "GetSessionStateChangeDir failed";
return;
}

if (!base::PathExists(dir)) {
base::CreateDirectory(dir);
}
const base::FilePath path = dir.AppendASCII(extension_id);
if (!watcher->Watch(path, base::FilePathWatcher::Type::kNonRecursive,
base::BindRepeating(&OnSessionStateChangeFileUpdate, path,
update_callback))) {
DLOG(ERROR) << "FilePathWatcher::Watch() failed";
}
DVLOG(1) << "WebAuthenticationProxyRemoteSessionStateChangeNotifier at "
<< path;
}
} // namespace

bool WebAuthenticationProxyRemoteSessionStateChangeNotifier::
GetSessionStateChangeDir(base::FilePath* out) {
// The path must be stable, i.e. the remote desktop app should not need to do
// any sort of discovery, which rules out the User Data Directory. It also has
// to be user-writable, because the app isn't expected to run as root.
base::FilePath default_udd;
if (!chrome::GetDefaultUserDataDirectory(&default_udd)) {
return false;
}
*out = default_udd.Append(
FILE_PATH_LITERAL("WebAuthenticationProxyRemoteSessionStateChange"));
return true;
}

WebAuthenticationProxyRemoteSessionStateChangeNotifier::
WebAuthenticationProxyRemoteSessionStateChangeNotifier(
EventRouter* event_router,
ExtensionId extension_id)
: event_router_(event_router), extension_id_(std::move(extension_id)) {
DCHECK(event_router_);
auto broadcast_event_on_change = base::BindPostTask(
base::SequencedTaskRunnerHandle::Get(),
base::BindRepeating(
&WebAuthenticationProxyRemoteSessionStateChangeNotifier::
BroadcastRemoteSessionStateChangeEvent,
weak_ptr_factory_.GetWeakPtr()));
// This task could run after `this` has been deleted. But `watcher_` is
// getting destroyed on `io_runner_`, so it will still be alive.
io_runner_->PostTask(
FROM_HERE,
base::BindOnce(&WatchSessionStateChangeFile, watcher_.get(),
extension_id_, std::move(broadcast_event_on_change)));
}

WebAuthenticationProxyRemoteSessionStateChangeNotifier::
~WebAuthenticationProxyRemoteSessionStateChangeNotifier() = default;

void WebAuthenticationProxyRemoteSessionStateChangeNotifier::
BroadcastRemoteSessionStateChangeEvent() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
event_router_->DispatchEventToExtension(
extension_id_,
std::make_unique<Event>(
events::WEB_AUTHENTICATION_PROXY_ON_REMOTE_SESSION_STATE_CHANGE,
api::web_authentication_proxy::OnRemoteSessionStateChange::kEventName,
api::web_authentication_proxy::OnRemoteSessionStateChange::Create()));
}

} // namespace extensions
@@ -0,0 +1,67 @@
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CHROME_BROWSER_EXTENSIONS_API_WEB_AUTHENTICATION_PROXY_REMOTE_SESSION_STATE_CHANGE_H_
#define CHROME_BROWSER_EXTENSIONS_API_WEB_AUTHENTICATION_PROXY_REMOTE_SESSION_STATE_CHANGE_H_

#include <memory>

#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/files/file_path_watcher.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/event_router.h"

namespace extensions {

// WebAuthenticationProxyRemoteSessionStateChangeNotifier watches for changes to
// a per-extension file in the well-known directory path returned by
// `GetSessionStateChangeDir()`, and raises
// `webAuthentcationProxy.onRemoteSessionStateChange` events in response.
class WebAuthenticationProxyRemoteSessionStateChangeNotifier {
public:
// Returns the directory in which the extension is expected to write its file.
static bool GetSessionStateChangeDir(base::FilePath* out);

WebAuthenticationProxyRemoteSessionStateChangeNotifier(
EventRouter* event_router,
ExtensionId extension_id);
WebAuthenticationProxyRemoteSessionStateChangeNotifier(
const WebAuthenticationProxyRemoteSessionStateChangeNotifier&) = delete;
WebAuthenticationProxyRemoteSessionStateChangeNotifier& operator=(
WebAuthenticationProxyRemoteSessionStateChangeNotifier&) = delete;
virtual ~WebAuthenticationProxyRemoteSessionStateChangeNotifier();

private:
void OnRemoteSessionStateFileUpdate(
base::RepeatingCallback<void()> on_file_change,
const base::FilePath& path,
bool error);
void BroadcastRemoteSessionStateChangeEvent();

const raw_ptr<EventRouter> event_router_;
const ExtensionId extension_id_;

// FilePathWatcher::Watch() may block, and must be called on the same sequence
// as the destructor.
scoped_refptr<base::SequencedTaskRunner> io_runner_ =
base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()});

std::unique_ptr<base::FilePathWatcher, base::OnTaskRunnerDeleter> watcher_{
new base::FilePathWatcher(), base::OnTaskRunnerDeleter(io_runner_)};

SEQUENCE_CHECKER(sequence_checker_);

base::WeakPtrFactory<WebAuthenticationProxyRemoteSessionStateChangeNotifier>
weak_ptr_factory_{this};
};

} // namespace extensions

#endif // CHROME_BROWSER_EXTENSIONS_API_WEB_AUTHENTICATION_PROXY_REMOTE_SESSION_STATE_CHANGE_H_
Expand Up @@ -9,6 +9,55 @@

namespace extensions {

BrowserContextKeyedAPIFactory<WebAuthenticationProxyAPI>*
WebAuthenticationProxyAPI::GetFactoryInstance() {
static base::NoDestructor<
BrowserContextKeyedAPIFactory<WebAuthenticationProxyAPI>>
instance;
return instance.get();
}

WebAuthenticationProxyAPI::WebAuthenticationProxyAPI(
content::BrowserContext* context)
: context_(context) {
EventRouter::Get(context_)->RegisterObserver(
this,
api::web_authentication_proxy::OnRemoteSessionStateChange::kEventName);
}

WebAuthenticationProxyAPI::~WebAuthenticationProxyAPI() = default;

void WebAuthenticationProxyAPI::Shutdown() {
EventRouter::Get(context_)->UnregisterObserver(this);
}

void WebAuthenticationProxyAPI::OnListenerAdded(
const EventListenerInfo& details) {
DCHECK_EQ(
details.event_name,
api::web_authentication_proxy::OnRemoteSessionStateChange::kEventName);
// This may be called multiple times for the same extension, but we only need
// to instantiate a notifier once.
session_state_change_notifiers_.try_emplace(
details.extension_id, EventRouter::Get(context_), details.extension_id);
}

void WebAuthenticationProxyAPI::OnListenerRemoved(
const EventListenerInfo& details) {
DCHECK_EQ(
details.event_name,
api::web_authentication_proxy::OnRemoteSessionStateChange::kEventName);
if (EventRouter::Get(context_)->ExtensionHasEventListener(
details.extension_id, api::web_authentication_proxy::
OnRemoteSessionStateChange::kEventName)) {
// This wasn't necessarily the last remaining listener for this extension.
return;
}
auto it = session_state_change_notifiers_.find(details.extension_id);
DCHECK(it != session_state_change_notifiers_.end());
session_state_change_notifiers_.erase(it);
}

WebAuthenticationProxyAttachFunction::WebAuthenticationProxyAttachFunction() =
default;
WebAuthenticationProxyAttachFunction::~WebAuthenticationProxyAttachFunction() =
Expand Down
Expand Up @@ -5,11 +5,54 @@
#ifndef CHROME_BROWSER_EXTENSIONS_API_WEB_AUTHENTICATION_PROXY_WEB_AUTHENTICATION_PROXY_API_H_
#define CHROME_BROWSER_EXTENSIONS_API_WEB_AUTHENTICATION_PROXY_WEB_AUTHENTICATION_PROXY_API_H_

#include "chrome/browser/extensions/api/web_authentication_proxy/remote_session_state_change.h"
#include "extensions/browser/browser_context_keyed_api_factory.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/event_router_factory.h"
#include "extensions/browser/extension_function.h"
#include "extensions/browser/extension_function_histogram_value.h"

namespace extensions {

class WebAuthenticationProxyAPI : public BrowserContextKeyedAPI,
public EventRouter::Observer {
public:
static BrowserContextKeyedAPIFactory<WebAuthenticationProxyAPI>*
GetFactoryInstance();

explicit WebAuthenticationProxyAPI(content::BrowserContext* context);
WebAuthenticationProxyAPI(const WebAuthenticationProxyAPI&) = delete;
WebAuthenticationProxyAPI& operator=(const WebAuthenticationProxyAPI&) =
delete;
~WebAuthenticationProxyAPI() override;

private:
friend class BrowserContextKeyedAPIFactory<WebAuthenticationProxyAPI>;

// BrowserContextKeyedAPI:
static const bool kServiceIsNULLWhileTesting = true;
static const char* service_name() { return "WebAuthenticationProxyAPI"; }
void Shutdown() override;

// EventRouter::Observer:
void OnListenerAdded(const EventListenerInfo& details) override;
void OnListenerRemoved(const EventListenerInfo& details) override;

content::BrowserContext* const context_;
std::map<ExtensionId, WebAuthenticationProxyRemoteSessionStateChangeNotifier>
session_state_change_notifiers_;
};

template <>
struct BrowserContextFactoryDependencies<WebAuthenticationProxyAPI> {
static void DeclareFactoryDependencies(
BrowserContextKeyedAPIFactory<WebAuthenticationProxyAPI>* factory) {
factory->DependsOn(
ExtensionsBrowserClient::Get()->GetExtensionSystemFactory());
factory->DependsOn(EventRouterFactory::GetInstance());
}
};

class WebAuthenticationProxyAttachFunction : public ExtensionFunction {
public:
WebAuthenticationProxyAttachFunction();
Expand Down

0 comments on commit cdbdd06

Please sign in to comment.