diff --git a/docs/api/app.md b/docs/api/app.md
index 5c84730313c9..2a137c2983da 100755
--- a/docs/api/app.md
+++ b/docs/api/app.md
@@ -345,9 +345,10 @@ app.on('select-client-certificate', (event, webContents, url, list, callback) =>
Returns:
* `event` Event
-* `webContents` [WebContents](web-contents.md)
+* `webContents` [WebContents](web-contents.md) (optional)
* `authenticationResponseDetails` Object
* `url` URL
+ * `pid` number
* `authInfo` Object
* `isProxy` boolean
* `scheme` string
@@ -358,7 +359,7 @@ Returns:
* `username` string (optional)
* `password` string (optional)
-Emitted when `webContents` wants to do basic auth.
+Emitted when `webContents` or [Utility process](../glossary.md#utility-process) wants to do basic auth.
The default behavior is to cancel all authentications. To override this you
should prevent the default behavior with `event.preventDefault()` and call
diff --git a/docs/api/utility-process.md b/docs/api/utility-process.md
index f628f4164d0e..100c3ead6d19 100644
--- a/docs/api/utility-process.md
+++ b/docs/api/utility-process.md
@@ -36,6 +36,8 @@ Process: [Main](../glossary.md#main-process)
`com.apple.security.cs.allow-unsigned-executable-memory` entitlements. This will allow the utility process
to load unsigned libraries. Unless you specifically need this capability, it is best to leave this disabled.
Default is `false`.
+ * `respondToAuthRequestsFromMainProcess` boolean (optional) - With this flag, all HTTP 401 and 407 network
+ requests created via the [net module](net.md) will allow responding to them via the [`app#login`](app.md#event-login) event in the main process instead of the default [`login`](client-request.md#event-login) event on the [`ClientRequest`](client-request.md) object.
Returns [`UtilityProcess`](utility-process.md#class-utilityprocess)
diff --git a/docs/breaking-changes.md b/docs/breaking-changes.md
index 7a6e0df45f76..58a39a99e53e 100644
--- a/docs/breaking-changes.md
+++ b/docs/breaking-changes.md
@@ -14,6 +14,12 @@ This document uses the following convention to categorize breaking changes:
## Planned Breaking API Changes (33.0)
+### Behavior Changed: `webContents` property on `login` on `app`
+
+The `webContents` property in the `login` event from `app` will be `null`
+when the event is triggered for requests from the [utility process](api/utility-process.md)
+created with `respondToAuthRequestsFromMainProcess` option.
+
### Deprecated: `textured` option in `BrowserWindowConstructorOption.type`
The `textured` option of `type` in `BrowserWindowConstructorOptions` has been deprecated with no replacement. This option relied on the [`NSWindowStyleMaskTexturedBackground`](https://developer.apple.com/documentation/appkit/nswindowstylemask/nswindowstylemasktexturedbackground) style mask on macOS, which has been deprecated with no alternative.
diff --git a/filenames.gni b/filenames.gni
index 8766b716f6d5..6ae9f9e70f08 100644
--- a/filenames.gni
+++ b/filenames.gni
@@ -453,6 +453,8 @@ filenames = {
"shell/browser/net/resolve_proxy_helper.h",
"shell/browser/net/system_network_context_manager.cc",
"shell/browser/net/system_network_context_manager.h",
+ "shell/browser/net/url_loader_network_observer.cc",
+ "shell/browser/net/url_loader_network_observer.h",
"shell/browser/net/url_pipe_loader.cc",
"shell/browser/net/url_pipe_loader.h",
"shell/browser/net/web_request_api_interface.h",
diff --git a/shell/browser/api/electron_api_utility_process.cc b/shell/browser/api/electron_api_utility_process.cc
index 0d02fd3adad4..10050210e2b4 100644
--- a/shell/browser/api/electron_api_utility_process.cc
+++ b/shell/browser/api/electron_api_utility_process.cc
@@ -63,7 +63,8 @@ UtilityProcessWrapper::UtilityProcessWrapper(
std::map stdio,
base::EnvironmentMap env_map,
base::FilePath current_working_directory,
- bool use_plugin_helper) {
+ bool use_plugin_helper,
+ bool create_network_observer) {
#if BUILDFLAG(IS_WIN)
base::win::ScopedHandle stdout_write(nullptr);
base::win::ScopedHandle stderr_write(nullptr);
@@ -203,6 +204,11 @@ UtilityProcessWrapper::UtilityProcessWrapper(
loader_params->process_id = pid_;
loader_params->is_orb_enabled = false;
loader_params->is_trusted = true;
+ if (create_network_observer) {
+ url_loader_network_observer_.emplace();
+ loader_params->url_loader_network_observer =
+ url_loader_network_observer_->Bind();
+ }
network::mojom::NetworkContext* network_context =
g_browser_process->system_network_context_manager()->GetContext();
network_context->CreateURLLoaderFactory(
@@ -213,6 +219,8 @@ UtilityProcessWrapper::UtilityProcessWrapper(
network_context->CreateHostResolver(
{}, host_resolver.InitWithNewPipeAndPassReceiver());
params->host_resolver = std::move(host_resolver);
+ params->use_network_observer_from_url_loader_factory =
+ create_network_observer;
node_service_remote_->Initialize(std::move(params));
}
@@ -230,6 +238,9 @@ void UtilityProcessWrapper::OnServiceProcessLaunch(
EmitWithoutEvent("stdout", stdout_read_fd_);
if (stderr_read_fd_ != -1)
EmitWithoutEvent("stderr", stderr_read_fd_);
+ if (url_loader_network_observer_.has_value()) {
+ url_loader_network_observer_->set_process_id(pid_);
+ }
EmitWithoutEvent("spawn");
}
@@ -378,6 +389,7 @@ gin::Handle UtilityProcessWrapper::Create(
std::u16string display_name;
bool use_plugin_helper = false;
+ bool create_network_observer = false;
std::map stdio;
base::FilePath current_working_directory;
base::EnvironmentMap env_map;
@@ -403,6 +415,7 @@ gin::Handle UtilityProcessWrapper::Create(
opts.Get("serviceName", &display_name);
opts.Get("cwd", ¤t_working_directory);
+ opts.Get("respondToAuthRequestsFromMainProcess", &create_network_observer);
std::vector stdio_arr{"ignore", "inherit", "inherit"};
opts.Get("stdio", &stdio_arr);
@@ -423,10 +436,10 @@ gin::Handle UtilityProcessWrapper::Create(
#endif
}
auto handle = gin::CreateHandle(
- args->isolate(),
- new UtilityProcessWrapper(std::move(params), display_name,
- std::move(stdio), env_map,
- current_working_directory, use_plugin_helper));
+ args->isolate(), new UtilityProcessWrapper(
+ std::move(params), display_name, std::move(stdio),
+ env_map, current_working_directory,
+ use_plugin_helper, create_network_observer));
handle->Pin(args->isolate());
return handle;
}
diff --git a/shell/browser/api/electron_api_utility_process.h b/shell/browser/api/electron_api_utility_process.h
index 9e5d40ea1ca0..767252d54fc7 100644
--- a/shell/browser/api/electron_api_utility_process.h
+++ b/shell/browser/api/electron_api_utility_process.h
@@ -18,6 +18,7 @@
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "shell/browser/event_emitter_mixin.h"
+#include "shell/browser/net/url_loader_network_observer.h"
#include "shell/common/gin_helper/pinnable.h"
#include "shell/services/node/public/mojom/node_service.mojom.h"
#include "v8/include/v8-forward.h"
@@ -66,7 +67,8 @@ class UtilityProcessWrapper
std::map stdio,
base::EnvironmentMap env_map,
base::FilePath current_working_directory,
- bool use_plugin_helper);
+ bool use_plugin_helper,
+ bool create_network_observer);
void OnServiceProcessLaunch(const base::Process& process);
void CloseConnectorPort();
@@ -101,6 +103,8 @@ class UtilityProcessWrapper
std::unique_ptr connector_;
blink::MessagePortDescriptor host_port_;
mojo::Remote node_service_remote_;
+ std::optional
+ url_loader_network_observer_;
base::WeakPtrFactory weak_factory_{this};
};
diff --git a/shell/browser/electron_browser_client.cc b/shell/browser/electron_browser_client.cc
index 91dc148f672f..9c04627f4ccc 100644
--- a/shell/browser/electron_browser_client.cc
+++ b/shell/browser/electron_browser_client.cc
@@ -1649,8 +1649,8 @@ ElectronBrowserClient::CreateLoginDelegate(
bool first_auth_attempt,
LoginAuthRequiredCallback auth_required_callback) {
return std::make_unique(
- auth_info, web_contents, is_main_frame, url, response_headers,
- first_auth_attempt, std::move(auth_required_callback));
+ auth_info, web_contents, is_main_frame, base::kNullProcessId, url,
+ response_headers, first_auth_attempt, std::move(auth_required_callback));
}
std::vector>
diff --git a/shell/browser/login_handler.cc b/shell/browser/login_handler.cc
index 10005077314c..3b92625ff4c5 100644
--- a/shell/browser/login_handler.cc
+++ b/shell/browser/login_handler.cc
@@ -9,6 +9,7 @@
#include "base/task/sequenced_task_runner.h"
#include "gin/arguments.h"
#include "gin/dictionary.h"
+#include "shell/browser/api/electron_api_app.h"
#include "shell/browser/api/electron_api_web_contents.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/callback_converter.h"
@@ -24,39 +25,44 @@ LoginHandler::LoginHandler(
const net::AuthChallengeInfo& auth_info,
content::WebContents* web_contents,
bool is_main_frame,
+ base::ProcessId process_id,
const GURL& url,
scoped_refptr response_headers,
bool first_auth_attempt,
LoginAuthRequiredCallback auth_required_callback)
-
- : WebContentsObserver(web_contents),
- auth_required_callback_(std::move(auth_required_callback)) {
+ : auth_required_callback_(std::move(auth_required_callback)) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&LoginHandler::EmitEvent, weak_factory_.GetWeakPtr(),
- auth_info, is_main_frame, url, response_headers,
- first_auth_attempt));
+ auth_info, web_contents, is_main_frame, process_id, url,
+ response_headers, first_auth_attempt));
}
void LoginHandler::EmitEvent(
net::AuthChallengeInfo auth_info,
+ content::WebContents* web_contents,
bool is_main_frame,
+ base::ProcessId process_id,
const GURL& url,
scoped_refptr response_headers,
bool first_auth_attempt) {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope scope(isolate);
- api::WebContents* api_web_contents = api::WebContents::From(web_contents());
- if (!api_web_contents) {
- std::move(auth_required_callback_).Run(std::nullopt);
- return;
+ raw_ptr api_web_contents = nullptr;
+ if (web_contents) {
+ api_web_contents = api::WebContents::From(web_contents);
+ if (!api_web_contents) {
+ std::move(auth_required_callback_).Run(std::nullopt);
+ return;
+ }
}
auto details = gin::Dictionary::CreateEmpty(isolate);
details.Set("url", url);
+ details.Set("pid", process_id);
// These parameters aren't documented, and I'm not sure that they're useful,
// but we might as well stick 'em on the details object. If it turns out they
@@ -66,10 +72,18 @@ void LoginHandler::EmitEvent(
details.Set("responseHeaders", response_headers.get());
auto weak_this = weak_factory_.GetWeakPtr();
- bool default_prevented =
- api_web_contents->Emit("login", std::move(details), auth_info,
- base::BindOnce(&LoginHandler::CallbackFromJS,
- weak_factory_.GetWeakPtr()));
+ bool default_prevented = false;
+ if (api_web_contents) {
+ default_prevented =
+ api_web_contents->Emit("login", std::move(details), auth_info,
+ base::BindOnce(&LoginHandler::CallbackFromJS,
+ weak_factory_.GetWeakPtr()));
+ } else {
+ default_prevented =
+ api::App::Get()->Emit("login", nullptr, std::move(details), auth_info,
+ base::BindOnce(&LoginHandler::CallbackFromJS,
+ weak_factory_.GetWeakPtr()));
+ }
// ⚠️ NB, if CallbackFromJS is called during Emit(), |this| will have been
// deleted. Check the weak ptr before accessing any member variables to
// prevent UAF.
diff --git a/shell/browser/login_handler.h b/shell/browser/login_handler.h
index b4456703fffa..507483a63aaa 100644
--- a/shell/browser/login_handler.h
+++ b/shell/browser/login_handler.h
@@ -5,9 +5,9 @@
#ifndef ELECTRON_SHELL_BROWSER_LOGIN_HANDLER_H_
#define ELECTRON_SHELL_BROWSER_LOGIN_HANDLER_H_
+#include "base/process/process_handle.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/login_delegate.h"
-#include "content/public/browser/web_contents_observer.h"
namespace content {
class WebContents;
@@ -20,12 +20,12 @@ class Arguments;
namespace electron {
// Handles HTTP basic auth.
-class LoginHandler : public content::LoginDelegate,
- private content::WebContentsObserver {
+class LoginHandler : public content::LoginDelegate {
public:
LoginHandler(const net::AuthChallengeInfo& auth_info,
content::WebContents* web_contents,
bool is_main_frame,
+ base::ProcessId process_id,
const GURL& url,
scoped_refptr response_headers,
bool first_auth_attempt,
@@ -38,7 +38,9 @@ class LoginHandler : public content::LoginDelegate,
private:
void EmitEvent(net::AuthChallengeInfo auth_info,
+ content::WebContents* web_contents,
bool is_main_frame,
+ base::ProcessId process_id,
const GURL& url,
scoped_refptr response_headers,
bool first_auth_attempt);
diff --git a/shell/browser/net/url_loader_network_observer.cc b/shell/browser/net/url_loader_network_observer.cc
new file mode 100644
index 000000000000..0243dec1867a
--- /dev/null
+++ b/shell/browser/net/url_loader_network_observer.cc
@@ -0,0 +1,120 @@
+// Copyright (c) 2024 Microsoft, GmbH
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/net/url_loader_network_observer.h"
+
+#include "base/functional/bind.h"
+#include "content/public/browser/browser_thread.h"
+#include "shell/browser/login_handler.h"
+
+namespace electron {
+
+namespace {
+
+class LoginHandlerDelegate {
+ public:
+ LoginHandlerDelegate(
+ mojo::PendingRemote
+ auth_challenge_responder,
+ const net::AuthChallengeInfo& auth_info,
+ const GURL& url,
+ scoped_refptr response_headers,
+ base::ProcessId process_id,
+ bool first_auth_attempt)
+ : auth_challenge_responder_(std::move(auth_challenge_responder)) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ auth_challenge_responder_.set_disconnect_handler(base::BindOnce(
+ &LoginHandlerDelegate::OnRequestCancelled, base::Unretained(this)));
+
+ login_handler_ = std::make_unique(
+ auth_info, nullptr, false, process_id, url, response_headers,
+ first_auth_attempt,
+ base::BindOnce(&LoginHandlerDelegate::OnAuthCredentials,
+ weak_factory_.GetWeakPtr()));
+ }
+
+ private:
+ void OnRequestCancelled() {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ delete this;
+ }
+
+ void OnAuthCredentials(
+ const std::optional& auth_credentials) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ auth_challenge_responder_->OnAuthCredentials(auth_credentials);
+ delete this;
+ }
+
+ mojo::Remote
+ auth_challenge_responder_;
+ std::unique_ptr login_handler_;
+ base::WeakPtrFactory weak_factory_{this};
+};
+
+} // namespace
+
+URLLoaderNetworkObserver::URLLoaderNetworkObserver() = default;
+URLLoaderNetworkObserver::~URLLoaderNetworkObserver() = default;
+
+mojo::PendingRemote
+URLLoaderNetworkObserver::Bind() {
+ mojo::PendingRemote
+ pending_remote;
+ receivers_.Add(this, pending_remote.InitWithNewPipeAndPassReceiver());
+ return pending_remote;
+}
+
+void URLLoaderNetworkObserver::OnAuthRequired(
+ const std::optional& window_id,
+ int32_t request_id,
+ const GURL& url,
+ bool first_auth_attempt,
+ const net::AuthChallengeInfo& auth_info,
+ const scoped_refptr& head_headers,
+ mojo::PendingRemote
+ auth_challenge_responder) {
+ new LoginHandlerDelegate(std::move(auth_challenge_responder), auth_info, url,
+ head_headers, process_id_, first_auth_attempt);
+}
+
+void URLLoaderNetworkObserver::OnSSLCertificateError(
+ const GURL& url,
+ int net_error,
+ const net::SSLInfo& ssl_info,
+ bool fatal,
+ OnSSLCertificateErrorCallback response) {
+ std::move(response).Run(net_error);
+}
+
+void URLLoaderNetworkObserver::OnClearSiteData(
+ const GURL& url,
+ const std::string& header_value,
+ int32_t load_flags,
+ const std::optional& cookie_partition_key,
+ bool partitioned_state_allowed_only,
+ OnClearSiteDataCallback callback) {
+ std::move(callback).Run();
+}
+
+void URLLoaderNetworkObserver::OnLoadingStateUpdate(
+ network::mojom::LoadInfoPtr info,
+ OnLoadingStateUpdateCallback callback) {
+ std::move(callback).Run();
+}
+
+void URLLoaderNetworkObserver::OnSharedStorageHeaderReceived(
+ const url::Origin& request_origin,
+ std::vector operations,
+ OnSharedStorageHeaderReceivedCallback callback) {
+ std::move(callback).Run();
+}
+
+void URLLoaderNetworkObserver::Clone(
+ mojo::PendingReceiver
+ observer) {
+ receivers_.Add(this, std::move(observer));
+}
+
+} // namespace electron
diff --git a/shell/browser/net/url_loader_network_observer.h b/shell/browser/net/url_loader_network_observer.h
new file mode 100644
index 000000000000..93b3b4e35279
--- /dev/null
+++ b/shell/browser/net/url_loader_network_observer.h
@@ -0,0 +1,82 @@
+// Copyright (c) 2024 Microsoft, GmbH
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_BROWSER_NET_URL_LOADER_NETWORK_OBSERVER_H_
+#define ELECTRON_SHELL_BROWSER_NET_URL_LOADER_NETWORK_OBSERVER_H_
+
+#include "base/memory/weak_ptr.h"
+#include "base/process/process_handle.h"
+#include "mojo/public/cpp/bindings/receiver_set.h"
+#include "services/network/public/mojom/url_loader_network_service_observer.mojom.h"
+
+namespace electron {
+
+class URLLoaderNetworkObserver
+ : public network::mojom::URLLoaderNetworkServiceObserver {
+ public:
+ URLLoaderNetworkObserver();
+ ~URLLoaderNetworkObserver() override;
+
+ URLLoaderNetworkObserver(const URLLoaderNetworkObserver&) = delete;
+ URLLoaderNetworkObserver& operator=(const URLLoaderNetworkObserver&) = delete;
+
+ mojo::PendingRemote Bind();
+ void set_process_id(base::ProcessId pid) { process_id_ = pid; }
+
+ private:
+ void OnAuthRequired(
+ const std::optional& window_id,
+ int32_t request_id,
+ const GURL& url,
+ bool first_auth_attempt,
+ const net::AuthChallengeInfo& auth_info,
+ const scoped_refptr& head_headers,
+ mojo::PendingRemote
+ auth_challenge_responder) override;
+ void OnSSLCertificateError(const GURL& url,
+ int net_error,
+ const net::SSLInfo& ssl_info,
+ bool fatal,
+ OnSSLCertificateErrorCallback response) override;
+ void OnClearSiteData(
+ const GURL& url,
+ const std::string& header_value,
+ int32_t load_flags,
+ const std::optional& cookie_partition_key,
+ bool partitioned_state_allowed_only,
+ OnClearSiteDataCallback callback) override;
+ void OnLoadingStateUpdate(network::mojom::LoadInfoPtr info,
+ OnLoadingStateUpdateCallback callback) override;
+ void OnSharedStorageHeaderReceived(
+ const url::Origin& request_origin,
+ std::vector operations,
+ OnSharedStorageHeaderReceivedCallback callback) override;
+ void OnDataUseUpdate(int32_t network_traffic_annotation_id_hash,
+ int64_t recv_bytes,
+ int64_t sent_bytes) override {}
+ void OnWebSocketConnectedToPrivateNetwork(
+ network::mojom::IPAddressSpace ip_address_space) override {}
+ void OnCertificateRequested(
+ const std::optional& window_id,
+ const scoped_refptr& cert_info,
+ mojo::PendingRemote
+ client_cert_responder) override {}
+ void OnPrivateNetworkAccessPermissionRequired(
+ const GURL& url,
+ const net::IPAddress& ip_address,
+ const std::optional& private_network_device_id,
+ const std::optional& private_network_device_name,
+ OnPrivateNetworkAccessPermissionRequiredCallback callback) override {}
+ void Clone(
+ mojo::PendingReceiver
+ observer) override;
+
+ mojo::ReceiverSet receivers_;
+ base::ProcessId process_id_ = base::kNullProcessId;
+ base::WeakPtrFactory weak_factory_{this};
+};
+
+} // namespace electron
+
+#endif // ELECTRON_SHELL_BROWSER_NET_URL_LOADER_NETWORK_OBSERVER_H_
diff --git a/shell/common/api/electron_api_url_loader.cc b/shell/common/api/electron_api_url_loader.cc
index 32ce00934277..212d133d6e4d 100644
--- a/shell/common/api/electron_api_url_loader.cc
+++ b/shell/common/api/electron_api_url_loader.cc
@@ -335,13 +335,21 @@ SimpleURLLoaderWrapper::SimpleURLLoaderWrapper(
DETACH_FROM_SEQUENCE(sequence_checker_);
if (!request_->trusted_params)
request_->trusted_params = network::ResourceRequest::TrustedParams();
- mojo::PendingRemote
- url_loader_network_observer_remote;
- url_loader_network_observer_receivers_.Add(
- this,
- url_loader_network_observer_remote.InitWithNewPipeAndPassReceiver());
- request_->trusted_params->url_loader_network_observer =
- std::move(url_loader_network_observer_remote);
+ bool create_network_observer = true;
+ if (electron::IsUtilityProcess()) {
+ create_network_observer =
+ !URLLoaderBundle::GetInstance()
+ ->ShouldUseNetworkObserverfromURLLoaderFactory();
+ }
+ if (create_network_observer) {
+ mojo::PendingRemote
+ url_loader_network_observer_remote;
+ url_loader_network_observer_receivers_.Add(
+ this,
+ url_loader_network_observer_remote.InitWithNewPipeAndPassReceiver());
+ request_->trusted_params->url_loader_network_observer =
+ std::move(url_loader_network_observer_remote);
+ }
// Chromium filters headers using browser rules, while for net module we have
// every header passed. The following setting will allow us to capture the
// raw headers in the URLLoader.
diff --git a/shell/services/node/node_service.cc b/shell/services/node/node_service.cc
index 771ce3550896..6d273119d02f 100644
--- a/shell/services/node/node_service.cc
+++ b/shell/services/node/node_service.cc
@@ -33,11 +33,14 @@ URLLoaderBundle* URLLoaderBundle::GetInstance() {
void URLLoaderBundle::SetURLLoaderFactory(
mojo::PendingRemote pending_factory,
- mojo::Remote host_resolver) {
+ mojo::Remote host_resolver,
+ bool use_network_observer_from_url_loader_factory) {
factory_ = network::SharedURLLoaderFactory::Create(
std::make_unique(
std::move(pending_factory)));
host_resolver_ = std::move(host_resolver);
+ should_use_network_observer_from_url_loader_factory_ =
+ use_network_observer_from_url_loader_factory;
}
scoped_refptr
@@ -50,6 +53,10 @@ network::mojom::HostResolver* URLLoaderBundle::GetHostResolver() {
return host_resolver_.get();
}
+bool URLLoaderBundle::ShouldUseNetworkObserverfromURLLoaderFactory() const {
+ return should_use_network_observer_from_url_loader_factory_;
+}
+
NodeService::NodeService(
mojo::PendingReceiver receiver)
: node_bindings_{NodeBindings::Create(
@@ -76,7 +83,8 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
URLLoaderBundle::GetInstance()->SetURLLoaderFactory(
std::move(params->url_loader_factory),
- mojo::Remote(std::move(params->host_resolver)));
+ mojo::Remote(std::move(params->host_resolver)),
+ params->use_network_observer_from_url_loader_factory);
js_env_ = std::make_unique(node_bindings_->uv_loop());
diff --git a/shell/services/node/node_service.h b/shell/services/node/node_service.h
index a7b87da8041c..b025ad233a26 100644
--- a/shell/services/node/node_service.h
+++ b/shell/services/node/node_service.h
@@ -39,13 +39,16 @@ class URLLoaderBundle {
static URLLoaderBundle* GetInstance();
void SetURLLoaderFactory(
mojo::PendingRemote factory,
- mojo::Remote host_resolver);
+ mojo::Remote host_resolver,
+ bool use_network_observer_from_url_loader_factory);
scoped_refptr GetSharedURLLoaderFactory();
network::mojom::HostResolver* GetHostResolver();
+ bool ShouldUseNetworkObserverfromURLLoaderFactory() const;
private:
scoped_refptr factory_;
mojo::Remote host_resolver_;
+ bool should_use_network_observer_from_url_loader_factory_ = false;
};
class NodeService : public node::mojom::NodeService {
diff --git a/shell/services/node/public/mojom/node_service.mojom b/shell/services/node/public/mojom/node_service.mojom
index 30ebcae92c21..be9a0cb0aba5 100644
--- a/shell/services/node/public/mojom/node_service.mojom
+++ b/shell/services/node/public/mojom/node_service.mojom
@@ -17,6 +17,7 @@ struct NodeServiceParams {
blink.mojom.MessagePortDescriptor port;
pending_remote url_loader_factory;
pending_remote host_resolver;
+ bool use_network_observer_from_url_loader_factory = false;
};
[ServiceSandbox=sandbox.mojom.Sandbox.kNoSandbox]
diff --git a/spec/api-utility-process-spec.ts b/spec/api-utility-process-spec.ts
index 9ee9a3595ad0..76d0e2d6e30f 100644
--- a/spec/api-utility-process-spec.ts
+++ b/spec/api-utility-process-spec.ts
@@ -2,8 +2,9 @@ import { expect } from 'chai';
import * as childProcess from 'node:child_process';
import * as path from 'node:path';
import { BrowserWindow, MessageChannelMain, utilityProcess, app } from 'electron/main';
-import { ifit } from './lib/spec-helpers';
+import { ifit, startRemoteControlApp } from './lib/spec-helpers';
import { closeWindow } from './lib/window-helpers';
+import { respondOnce, randomString, kOneKiloByte } from './lib/net-helpers';
import { once } from 'node:events';
import { pathToFileURL } from 'node:url';
import { setImmediate } from 'node:timers/promises';
@@ -508,5 +509,193 @@ describe('utilityProcess module', () => {
expect(child.kill()).to.be.true();
await exit;
});
+
+ it('should emit the app#login event when 401', async () => {
+ const { remotely } = await startRemoteControlApp();
+ const serverUrl = await respondOnce.toSingleURL((request, response) => {
+ if (!request.headers.authorization) {
+ return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end();
+ }
+ response.writeHead(200).end('ok');
+ });
+ const [loginAuthInfo, statusCode] = await remotely(async (serverUrl: string, fixture: string) => {
+ const { app, utilityProcess } = require('electron');
+ const { once } = require('node:events');
+ const child = utilityProcess.fork(fixture, [`--server-url=${serverUrl}`], {
+ stdio: 'ignore',
+ respondToAuthRequestsFromMainProcess: true
+ });
+ await once(child, 'spawn');
+ const [ev,,, authInfo, cb] = await once(app, 'login');
+ ev.preventDefault();
+ cb('dummy', 'pass');
+ const [result] = await once(child, 'message');
+ return [authInfo, ...result];
+ }, serverUrl, path.join(fixturesPath, 'net.js'));
+ expect(statusCode).to.equal(200);
+ expect(loginAuthInfo!.realm).to.equal('Foo');
+ expect(loginAuthInfo!.scheme).to.equal('basic');
+ });
+
+ it('should receive 401 response when cancelling authentication via app#login event', async () => {
+ const { remotely } = await startRemoteControlApp();
+ const serverUrl = await respondOnce.toSingleURL((request, response) => {
+ if (!request.headers.authorization) {
+ response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' });
+ response.end('unauthenticated');
+ } else {
+ response.writeHead(200).end('ok');
+ }
+ });
+ const [authDetails, responseBody, statusCode] = await remotely(async (serverUrl: string, fixture: string) => {
+ const { app, utilityProcess } = require('electron');
+ const { once } = require('node:events');
+ const child = utilityProcess.fork(fixture, [`--server-url=${serverUrl}`], {
+ stdio: 'ignore',
+ respondToAuthRequestsFromMainProcess: true
+ });
+ await once(child, 'spawn');
+ const [,, details,, cb] = await once(app, 'login');
+ cb();
+ const [response] = await once(child, 'message');
+ const [responseBody] = await once(child, 'message');
+ return [details, responseBody, ...response];
+ }, serverUrl, path.join(fixturesPath, 'net.js'));
+ expect(authDetails.url).to.equal(serverUrl);
+ expect(statusCode).to.equal(401);
+ expect(responseBody).to.equal('unauthenticated');
+ });
+
+ it('should upload body when 401', async () => {
+ const { remotely } = await startRemoteControlApp();
+ const serverUrl = await respondOnce.toSingleURL((request, response) => {
+ if (!request.headers.authorization) {
+ return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end();
+ }
+ response.writeHead(200);
+ request.on('data', (chunk) => response.write(chunk));
+ request.on('end', () => response.end());
+ });
+ const requestData = randomString(kOneKiloByte);
+ const [authDetails, responseBody, statusCode] = await remotely(async (serverUrl: string, requestData: string, fixture: string) => {
+ const { app, utilityProcess } = require('electron');
+ const { once } = require('node:events');
+ const child = utilityProcess.fork(fixture, [`--server-url=${serverUrl}`, '--request-data'], {
+ stdio: 'ignore',
+ respondToAuthRequestsFromMainProcess: true
+ });
+ await once(child, 'spawn');
+ await once(child, 'message');
+ child.postMessage(requestData);
+ const [,, details,, cb] = await once(app, 'login');
+ cb('user', 'pass');
+ const [response] = await once(child, 'message');
+ const [responseBody] = await once(child, 'message');
+ return [details, responseBody, ...response];
+ }, serverUrl, requestData, path.join(fixturesPath, 'net.js'));
+ expect(authDetails.url).to.equal(serverUrl);
+ expect(statusCode).to.equal(200);
+ expect(responseBody).to.equal(requestData);
+ });
+
+ it('should not emit the app#login event when 401 with {"credentials":"omit"}', async () => {
+ const rc = await startRemoteControlApp();
+ const serverUrl = await respondOnce.toSingleURL((request, response) => {
+ if (!request.headers.authorization) {
+ return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end();
+ }
+ response.writeHead(200).end('ok');
+ });
+ const [statusCode, responseHeaders] = await rc.remotely(async (serverUrl: string, fixture: string) => {
+ const { app, utilityProcess } = require('electron');
+ const { once } = require('node:events');
+ let gracefulExit = true;
+ const child = utilityProcess.fork(fixture, [`--server-url=${serverUrl}`, '--omit-credentials'], {
+ stdio: 'ignore',
+ respondToAuthRequestsFromMainProcess: true
+ });
+ await once(child, 'spawn');
+ app.on('login', () => {
+ gracefulExit = false;
+ });
+ const [result] = await once(child, 'message');
+ setTimeout(() => {
+ if (gracefulExit) {
+ app.quit();
+ } else {
+ process.exit(1);
+ }
+ });
+ return result;
+ }, serverUrl, path.join(fixturesPath, 'net.js'));
+ const [code] = await once(rc.process, 'exit');
+ expect(code).to.equal(0);
+ expect(statusCode).to.equal(401);
+ expect(responseHeaders['www-authenticate']).to.equal('Basic realm="Foo"');
+ });
+
+ it('should not emit the app#login event with default respondToAuthRequestsFromMainProcess', async () => {
+ const rc = await startRemoteControlApp();
+ const serverUrl = await respondOnce.toSingleURL((request, response) => {
+ if (!request.headers.authorization) {
+ return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end();
+ }
+ response.writeHead(200).end('ok');
+ });
+ const [loginAuthInfo, statusCode] = await rc.remotely(async (serverUrl: string, fixture: string) => {
+ const { app, utilityProcess } = require('electron');
+ const { once } = require('node:events');
+ let gracefulExit = true;
+ const child = utilityProcess.fork(fixture, [`--server-url=${serverUrl}`, '--use-net-login-event'], {
+ stdio: 'ignore'
+ });
+ await once(child, 'spawn');
+ app.on('login', () => {
+ gracefulExit = false;
+ });
+ const [authInfo] = await once(child, 'message');
+ const [result] = await once(child, 'message');
+ setTimeout(() => {
+ if (gracefulExit) {
+ app.quit();
+ } else {
+ process.exit(1);
+ }
+ });
+ return [authInfo, ...result];
+ }, serverUrl, path.join(fixturesPath, 'net.js'));
+ const [code] = await once(rc.process, 'exit');
+ expect(code).to.equal(0);
+ expect(statusCode).to.equal(200);
+ expect(loginAuthInfo!.realm).to.equal('Foo');
+ expect(loginAuthInfo!.scheme).to.equal('basic');
+ });
+
+ it('should emit the app#login event when creating requests with fetch API', async () => {
+ const { remotely } = await startRemoteControlApp();
+ const serverUrl = await respondOnce.toSingleURL((request, response) => {
+ if (!request.headers.authorization) {
+ return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end();
+ }
+ response.writeHead(200).end('ok');
+ });
+ const [loginAuthInfo, statusCode] = await remotely(async (serverUrl: string, fixture: string) => {
+ const { app, utilityProcess } = require('electron');
+ const { once } = require('node:events');
+ const child = utilityProcess.fork(fixture, [`--server-url=${serverUrl}`, '--use-fetch-api'], {
+ stdio: 'ignore',
+ respondToAuthRequestsFromMainProcess: true
+ });
+ await once(child, 'spawn');
+ const [ev,,, authInfo, cb] = await once(app, 'login');
+ ev.preventDefault();
+ cb('dummy', 'pass');
+ const [response] = await once(child, 'message');
+ return [authInfo, ...response];
+ }, serverUrl, path.join(fixturesPath, 'net.js'));
+ expect(statusCode).to.equal(200);
+ expect(loginAuthInfo!.realm).to.equal('Foo');
+ expect(loginAuthInfo!.scheme).to.equal('basic');
+ });
});
});
diff --git a/spec/fixtures/api/utility-process/net.js b/spec/fixtures/api/utility-process/net.js
new file mode 100644
index 000000000000..697a3572b92b
--- /dev/null
+++ b/spec/fixtures/api/utility-process/net.js
@@ -0,0 +1,44 @@
+const { net } = require('electron');
+const serverUrl = process.argv[2].split('=')[1];
+let configurableArg = null;
+if (process.argv[3]) {
+ configurableArg = process.argv[3].split('=')[0];
+}
+const data = [];
+
+let request = null;
+if (configurableArg === '--omit-credentials') {
+ request = net.request({ method: 'GET', url: serverUrl, credentials: 'omit' });
+} else if (configurableArg === '--use-fetch-api') {
+ net.fetch(serverUrl).then((response) => {
+ process.parentPort.postMessage([response.status, response.headers]);
+ });
+} else {
+ request = net.request({ method: 'GET', url: serverUrl });
+}
+
+if (request) {
+ if (configurableArg === '--use-net-login-event') {
+ request.on('login', (authInfo, cb) => {
+ process.parentPort.postMessage(authInfo);
+ cb('user', 'pass');
+ });
+ }
+ request.on('response', (response) => {
+ process.parentPort.postMessage([response.statusCode, response.headers]);
+ response.on('data', (chunk) => data.push(chunk));
+ response.on('end', (chunk) => {
+ if (chunk) data.push(chunk);
+ process.parentPort.postMessage(Buffer.concat(data).toString());
+ });
+ });
+ if (configurableArg === '--request-data') {
+ process.parentPort.on('message', (e) => {
+ request.write(e.data);
+ request.end();
+ });
+ process.parentPort.postMessage('get-request-data');
+ } else {
+ request.end();
+ }
+}