feat: support app#login event for utility process net requests (#42631)

* feat: support app#login event for utility process net requests

* chore: address review feedback

* GlobalRequestID: Avoid unwanted inlining and narrowing int conversions

Refs https://chromium-review.googlesource.com/c/chromium/src/+/5702737
This commit is contained in:
Robo 2024-08-14 11:36:47 +09:00 committed by GitHub
parent 62406708cd
commit 9b166b3ed4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 536 additions and 37 deletions

View file

@ -345,9 +345,10 @@ app.on('select-client-certificate', (event, webContents, url, list, callback) =>
Returns: Returns:
* `event` Event * `event` Event
* `webContents` [WebContents](web-contents.md) * `webContents` [WebContents](web-contents.md) (optional)
* `authenticationResponseDetails` Object * `authenticationResponseDetails` Object
* `url` URL * `url` URL
* `pid` number
* `authInfo` Object * `authInfo` Object
* `isProxy` boolean * `isProxy` boolean
* `scheme` string * `scheme` string
@ -358,7 +359,7 @@ Returns:
* `username` string (optional) * `username` string (optional)
* `password` 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 The default behavior is to cancel all authentications. To override this you
should prevent the default behavior with `event.preventDefault()` and call should prevent the default behavior with `event.preventDefault()` and call

View file

@ -36,6 +36,8 @@ Process: [Main](../glossary.md#main-process)<br />
`com.apple.security.cs.allow-unsigned-executable-memory` entitlements. This will allow the utility 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. to load unsigned libraries. Unless you specifically need this capability, it is best to leave this disabled.
Default is `false`. 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) Returns [`UtilityProcess`](utility-process.md#class-utilityprocess)

View file

@ -14,6 +14,12 @@ This document uses the following convention to categorize breaking changes:
## Planned Breaking API Changes (33.0) ## 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` ### 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. 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.

View file

@ -453,6 +453,8 @@ filenames = {
"shell/browser/net/resolve_proxy_helper.h", "shell/browser/net/resolve_proxy_helper.h",
"shell/browser/net/system_network_context_manager.cc", "shell/browser/net/system_network_context_manager.cc",
"shell/browser/net/system_network_context_manager.h", "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.cc",
"shell/browser/net/url_pipe_loader.h", "shell/browser/net/url_pipe_loader.h",
"shell/browser/net/web_request_api_interface.h", "shell/browser/net/web_request_api_interface.h",

View file

@ -63,7 +63,8 @@ UtilityProcessWrapper::UtilityProcessWrapper(
std::map<IOHandle, IOType> stdio, std::map<IOHandle, IOType> stdio,
base::EnvironmentMap env_map, base::EnvironmentMap env_map,
base::FilePath current_working_directory, base::FilePath current_working_directory,
bool use_plugin_helper) { bool use_plugin_helper,
bool create_network_observer) {
#if BUILDFLAG(IS_WIN) #if BUILDFLAG(IS_WIN)
base::win::ScopedHandle stdout_write(nullptr); base::win::ScopedHandle stdout_write(nullptr);
base::win::ScopedHandle stderr_write(nullptr); base::win::ScopedHandle stderr_write(nullptr);
@ -203,6 +204,11 @@ UtilityProcessWrapper::UtilityProcessWrapper(
loader_params->process_id = pid_; loader_params->process_id = pid_;
loader_params->is_orb_enabled = false; loader_params->is_orb_enabled = false;
loader_params->is_trusted = true; 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 = network::mojom::NetworkContext* network_context =
g_browser_process->system_network_context_manager()->GetContext(); g_browser_process->system_network_context_manager()->GetContext();
network_context->CreateURLLoaderFactory( network_context->CreateURLLoaderFactory(
@ -213,6 +219,8 @@ UtilityProcessWrapper::UtilityProcessWrapper(
network_context->CreateHostResolver( network_context->CreateHostResolver(
{}, host_resolver.InitWithNewPipeAndPassReceiver()); {}, host_resolver.InitWithNewPipeAndPassReceiver());
params->host_resolver = std::move(host_resolver); 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)); node_service_remote_->Initialize(std::move(params));
} }
@ -230,6 +238,9 @@ void UtilityProcessWrapper::OnServiceProcessLaunch(
EmitWithoutEvent("stdout", stdout_read_fd_); EmitWithoutEvent("stdout", stdout_read_fd_);
if (stderr_read_fd_ != -1) if (stderr_read_fd_ != -1)
EmitWithoutEvent("stderr", stderr_read_fd_); EmitWithoutEvent("stderr", stderr_read_fd_);
if (url_loader_network_observer_.has_value()) {
url_loader_network_observer_->set_process_id(pid_);
}
EmitWithoutEvent("spawn"); EmitWithoutEvent("spawn");
} }
@ -378,6 +389,7 @@ gin::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
std::u16string display_name; std::u16string display_name;
bool use_plugin_helper = false; bool use_plugin_helper = false;
bool create_network_observer = false;
std::map<IOHandle, IOType> stdio; std::map<IOHandle, IOType> stdio;
base::FilePath current_working_directory; base::FilePath current_working_directory;
base::EnvironmentMap env_map; base::EnvironmentMap env_map;
@ -403,6 +415,7 @@ gin::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
opts.Get("serviceName", &display_name); opts.Get("serviceName", &display_name);
opts.Get("cwd", &current_working_directory); opts.Get("cwd", &current_working_directory);
opts.Get("respondToAuthRequestsFromMainProcess", &create_network_observer);
std::vector<std::string> stdio_arr{"ignore", "inherit", "inherit"}; std::vector<std::string> stdio_arr{"ignore", "inherit", "inherit"};
opts.Get("stdio", &stdio_arr); opts.Get("stdio", &stdio_arr);
@ -423,10 +436,10 @@ gin::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
#endif #endif
} }
auto handle = gin::CreateHandle( auto handle = gin::CreateHandle(
args->isolate(), args->isolate(), new UtilityProcessWrapper(
new UtilityProcessWrapper(std::move(params), display_name, std::move(params), display_name, std::move(stdio),
std::move(stdio), env_map, env_map, current_working_directory,
current_working_directory, use_plugin_helper)); use_plugin_helper, create_network_observer));
handle->Pin(args->isolate()); handle->Pin(args->isolate());
return handle; return handle;
} }

View file

@ -18,6 +18,7 @@
#include "mojo/public/cpp/bindings/message.h" #include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/remote.h" #include "mojo/public/cpp/bindings/remote.h"
#include "shell/browser/event_emitter_mixin.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/common/gin_helper/pinnable.h"
#include "shell/services/node/public/mojom/node_service.mojom.h" #include "shell/services/node/public/mojom/node_service.mojom.h"
#include "v8/include/v8-forward.h" #include "v8/include/v8-forward.h"
@ -66,7 +67,8 @@ class UtilityProcessWrapper
std::map<IOHandle, IOType> stdio, std::map<IOHandle, IOType> stdio,
base::EnvironmentMap env_map, base::EnvironmentMap env_map,
base::FilePath current_working_directory, base::FilePath current_working_directory,
bool use_plugin_helper); bool use_plugin_helper,
bool create_network_observer);
void OnServiceProcessLaunch(const base::Process& process); void OnServiceProcessLaunch(const base::Process& process);
void CloseConnectorPort(); void CloseConnectorPort();
@ -101,6 +103,8 @@ class UtilityProcessWrapper
std::unique_ptr<mojo::Connector> connector_; std::unique_ptr<mojo::Connector> connector_;
blink::MessagePortDescriptor host_port_; blink::MessagePortDescriptor host_port_;
mojo::Remote<node::mojom::NodeService> node_service_remote_; mojo::Remote<node::mojom::NodeService> node_service_remote_;
std::optional<electron::URLLoaderNetworkObserver>
url_loader_network_observer_;
base::WeakPtrFactory<UtilityProcessWrapper> weak_factory_{this}; base::WeakPtrFactory<UtilityProcessWrapper> weak_factory_{this};
}; };

View file

@ -1649,8 +1649,8 @@ ElectronBrowserClient::CreateLoginDelegate(
bool first_auth_attempt, bool first_auth_attempt,
LoginAuthRequiredCallback auth_required_callback) { LoginAuthRequiredCallback auth_required_callback) {
return std::make_unique<LoginHandler>( return std::make_unique<LoginHandler>(
auth_info, web_contents, is_main_frame, url, response_headers, auth_info, web_contents, is_main_frame, base::kNullProcessId, url,
first_auth_attempt, std::move(auth_required_callback)); response_headers, first_auth_attempt, std::move(auth_required_callback));
} }
std::vector<std::unique_ptr<blink::URLLoaderThrottle>> std::vector<std::unique_ptr<blink::URLLoaderThrottle>>

View file

@ -9,6 +9,7 @@
#include "base/task/sequenced_task_runner.h" #include "base/task/sequenced_task_runner.h"
#include "gin/arguments.h" #include "gin/arguments.h"
#include "gin/dictionary.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/api/electron_api_web_contents.h"
#include "shell/browser/javascript_environment.h" #include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/callback_converter.h" #include "shell/common/gin_converters/callback_converter.h"
@ -24,39 +25,44 @@ LoginHandler::LoginHandler(
const net::AuthChallengeInfo& auth_info, const net::AuthChallengeInfo& auth_info,
content::WebContents* web_contents, content::WebContents* web_contents,
bool is_main_frame, bool is_main_frame,
base::ProcessId process_id,
const GURL& url, const GURL& url,
scoped_refptr<net::HttpResponseHeaders> response_headers, scoped_refptr<net::HttpResponseHeaders> response_headers,
bool first_auth_attempt, bool first_auth_attempt,
LoginAuthRequiredCallback auth_required_callback) LoginAuthRequiredCallback auth_required_callback)
: auth_required_callback_(std::move(auth_required_callback)) {
: WebContentsObserver(web_contents),
auth_required_callback_(std::move(auth_required_callback)) {
DCHECK_CURRENTLY_ON(BrowserThread::UI); DCHECK_CURRENTLY_ON(BrowserThread::UI);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask( base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, FROM_HERE,
base::BindOnce(&LoginHandler::EmitEvent, weak_factory_.GetWeakPtr(), base::BindOnce(&LoginHandler::EmitEvent, weak_factory_.GetWeakPtr(),
auth_info, is_main_frame, url, response_headers, auth_info, web_contents, is_main_frame, process_id, url,
first_auth_attempt)); response_headers, first_auth_attempt));
} }
void LoginHandler::EmitEvent( void LoginHandler::EmitEvent(
net::AuthChallengeInfo auth_info, net::AuthChallengeInfo auth_info,
content::WebContents* web_contents,
bool is_main_frame, bool is_main_frame,
base::ProcessId process_id,
const GURL& url, const GURL& url,
scoped_refptr<net::HttpResponseHeaders> response_headers, scoped_refptr<net::HttpResponseHeaders> response_headers,
bool first_auth_attempt) { bool first_auth_attempt) {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate(); v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope scope(isolate); v8::HandleScope scope(isolate);
api::WebContents* api_web_contents = api::WebContents::From(web_contents()); raw_ptr<api::WebContents> api_web_contents = nullptr;
if (!api_web_contents) { if (web_contents) {
std::move(auth_required_callback_).Run(std::nullopt); api_web_contents = api::WebContents::From(web_contents);
return; if (!api_web_contents) {
std::move(auth_required_callback_).Run(std::nullopt);
return;
}
} }
auto details = gin::Dictionary::CreateEmpty(isolate); auto details = gin::Dictionary::CreateEmpty(isolate);
details.Set("url", url); details.Set("url", url);
details.Set("pid", process_id);
// These parameters aren't documented, and I'm not sure that they're useful, // 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 // 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()); details.Set("responseHeaders", response_headers.get());
auto weak_this = weak_factory_.GetWeakPtr(); auto weak_this = weak_factory_.GetWeakPtr();
bool default_prevented = bool default_prevented = false;
api_web_contents->Emit("login", std::move(details), auth_info, if (api_web_contents) {
base::BindOnce(&LoginHandler::CallbackFromJS, default_prevented =
weak_factory_.GetWeakPtr())); 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 // ⚠️ NB, if CallbackFromJS is called during Emit(), |this| will have been
// deleted. Check the weak ptr before accessing any member variables to // deleted. Check the weak ptr before accessing any member variables to
// prevent UAF. // prevent UAF.

View file

@ -5,9 +5,9 @@
#ifndef ELECTRON_SHELL_BROWSER_LOGIN_HANDLER_H_ #ifndef ELECTRON_SHELL_BROWSER_LOGIN_HANDLER_H_
#define 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/content_browser_client.h"
#include "content/public/browser/login_delegate.h" #include "content/public/browser/login_delegate.h"
#include "content/public/browser/web_contents_observer.h"
namespace content { namespace content {
class WebContents; class WebContents;
@ -20,12 +20,12 @@ class Arguments;
namespace electron { namespace electron {
// Handles HTTP basic auth. // Handles HTTP basic auth.
class LoginHandler : public content::LoginDelegate, class LoginHandler : public content::LoginDelegate {
private content::WebContentsObserver {
public: public:
LoginHandler(const net::AuthChallengeInfo& auth_info, LoginHandler(const net::AuthChallengeInfo& auth_info,
content::WebContents* web_contents, content::WebContents* web_contents,
bool is_main_frame, bool is_main_frame,
base::ProcessId process_id,
const GURL& url, const GURL& url,
scoped_refptr<net::HttpResponseHeaders> response_headers, scoped_refptr<net::HttpResponseHeaders> response_headers,
bool first_auth_attempt, bool first_auth_attempt,
@ -38,7 +38,9 @@ class LoginHandler : public content::LoginDelegate,
private: private:
void EmitEvent(net::AuthChallengeInfo auth_info, void EmitEvent(net::AuthChallengeInfo auth_info,
content::WebContents* web_contents,
bool is_main_frame, bool is_main_frame,
base::ProcessId process_id,
const GURL& url, const GURL& url,
scoped_refptr<net::HttpResponseHeaders> response_headers, scoped_refptr<net::HttpResponseHeaders> response_headers,
bool first_auth_attempt); bool first_auth_attempt);

View file

@ -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<network::mojom::AuthChallengeResponder>
auth_challenge_responder,
const net::AuthChallengeInfo& auth_info,
const GURL& url,
scoped_refptr<net::HttpResponseHeaders> 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<LoginHandler>(
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<net::AuthCredentials>& auth_credentials) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auth_challenge_responder_->OnAuthCredentials(auth_credentials);
delete this;
}
mojo::Remote<network::mojom::AuthChallengeResponder>
auth_challenge_responder_;
std::unique_ptr<LoginHandler> login_handler_;
base::WeakPtrFactory<LoginHandlerDelegate> weak_factory_{this};
};
} // namespace
URLLoaderNetworkObserver::URLLoaderNetworkObserver() = default;
URLLoaderNetworkObserver::~URLLoaderNetworkObserver() = default;
mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver>
URLLoaderNetworkObserver::Bind() {
mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver>
pending_remote;
receivers_.Add(this, pending_remote.InitWithNewPipeAndPassReceiver());
return pending_remote;
}
void URLLoaderNetworkObserver::OnAuthRequired(
const std::optional<base::UnguessableToken>& window_id,
int32_t request_id,
const GURL& url,
bool first_auth_attempt,
const net::AuthChallengeInfo& auth_info,
const scoped_refptr<net::HttpResponseHeaders>& head_headers,
mojo::PendingRemote<network::mojom::AuthChallengeResponder>
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<net::CookiePartitionKey>& 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<network::mojom::SharedStorageOperationPtr> operations,
OnSharedStorageHeaderReceivedCallback callback) {
std::move(callback).Run();
}
void URLLoaderNetworkObserver::Clone(
mojo::PendingReceiver<network::mojom::URLLoaderNetworkServiceObserver>
observer) {
receivers_.Add(this, std::move(observer));
}
} // namespace electron

View file

@ -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<network::mojom::URLLoaderNetworkServiceObserver> Bind();
void set_process_id(base::ProcessId pid) { process_id_ = pid; }
private:
void OnAuthRequired(
const std::optional<base::UnguessableToken>& window_id,
int32_t request_id,
const GURL& url,
bool first_auth_attempt,
const net::AuthChallengeInfo& auth_info,
const scoped_refptr<net::HttpResponseHeaders>& head_headers,
mojo::PendingRemote<network::mojom::AuthChallengeResponder>
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<net::CookiePartitionKey>& 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<network::mojom::SharedStorageOperationPtr> 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<base::UnguessableToken>& window_id,
const scoped_refptr<net::SSLCertRequestInfo>& cert_info,
mojo::PendingRemote<network::mojom::ClientCertificateResponder>
client_cert_responder) override {}
void OnPrivateNetworkAccessPermissionRequired(
const GURL& url,
const net::IPAddress& ip_address,
const std::optional<std::string>& private_network_device_id,
const std::optional<std::string>& private_network_device_name,
OnPrivateNetworkAccessPermissionRequiredCallback callback) override {}
void Clone(
mojo::PendingReceiver<network::mojom::URLLoaderNetworkServiceObserver>
observer) override;
mojo::ReceiverSet<network::mojom::URLLoaderNetworkServiceObserver> receivers_;
base::ProcessId process_id_ = base::kNullProcessId;
base::WeakPtrFactory<URLLoaderNetworkObserver> weak_factory_{this};
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_NET_URL_LOADER_NETWORK_OBSERVER_H_

View file

@ -335,13 +335,21 @@ SimpleURLLoaderWrapper::SimpleURLLoaderWrapper(
DETACH_FROM_SEQUENCE(sequence_checker_); DETACH_FROM_SEQUENCE(sequence_checker_);
if (!request_->trusted_params) if (!request_->trusted_params)
request_->trusted_params = network::ResourceRequest::TrustedParams(); request_->trusted_params = network::ResourceRequest::TrustedParams();
mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver> bool create_network_observer = true;
url_loader_network_observer_remote; if (electron::IsUtilityProcess()) {
url_loader_network_observer_receivers_.Add( create_network_observer =
this, !URLLoaderBundle::GetInstance()
url_loader_network_observer_remote.InitWithNewPipeAndPassReceiver()); ->ShouldUseNetworkObserverfromURLLoaderFactory();
request_->trusted_params->url_loader_network_observer = }
std::move(url_loader_network_observer_remote); if (create_network_observer) {
mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver>
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 // Chromium filters headers using browser rules, while for net module we have
// every header passed. The following setting will allow us to capture the // every header passed. The following setting will allow us to capture the
// raw headers in the URLLoader. // raw headers in the URLLoader.

View file

@ -33,11 +33,14 @@ URLLoaderBundle* URLLoaderBundle::GetInstance() {
void URLLoaderBundle::SetURLLoaderFactory( void URLLoaderBundle::SetURLLoaderFactory(
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_factory, mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_factory,
mojo::Remote<network::mojom::HostResolver> host_resolver) { mojo::Remote<network::mojom::HostResolver> host_resolver,
bool use_network_observer_from_url_loader_factory) {
factory_ = network::SharedURLLoaderFactory::Create( factory_ = network::SharedURLLoaderFactory::Create(
std::make_unique<network::WrapperPendingSharedURLLoaderFactory>( std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
std::move(pending_factory))); std::move(pending_factory)));
host_resolver_ = std::move(host_resolver); host_resolver_ = std::move(host_resolver);
should_use_network_observer_from_url_loader_factory_ =
use_network_observer_from_url_loader_factory;
} }
scoped_refptr<network::SharedURLLoaderFactory> scoped_refptr<network::SharedURLLoaderFactory>
@ -50,6 +53,10 @@ network::mojom::HostResolver* URLLoaderBundle::GetHostResolver() {
return host_resolver_.get(); return host_resolver_.get();
} }
bool URLLoaderBundle::ShouldUseNetworkObserverfromURLLoaderFactory() const {
return should_use_network_observer_from_url_loader_factory_;
}
NodeService::NodeService( NodeService::NodeService(
mojo::PendingReceiver<node::mojom::NodeService> receiver) mojo::PendingReceiver<node::mojom::NodeService> receiver)
: node_bindings_{NodeBindings::Create( : node_bindings_{NodeBindings::Create(
@ -76,7 +83,8 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
URLLoaderBundle::GetInstance()->SetURLLoaderFactory( URLLoaderBundle::GetInstance()->SetURLLoaderFactory(
std::move(params->url_loader_factory), 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<JavascriptEnvironment>(node_bindings_->uv_loop()); js_env_ = std::make_unique<JavascriptEnvironment>(node_bindings_->uv_loop());

View file

@ -39,13 +39,16 @@ class URLLoaderBundle {
static URLLoaderBundle* GetInstance(); static URLLoaderBundle* GetInstance();
void SetURLLoaderFactory( void SetURLLoaderFactory(
mojo::PendingRemote<network::mojom::URLLoaderFactory> factory, mojo::PendingRemote<network::mojom::URLLoaderFactory> factory,
mojo::Remote<network::mojom::HostResolver> host_resolver); mojo::Remote<network::mojom::HostResolver> host_resolver,
bool use_network_observer_from_url_loader_factory);
scoped_refptr<network::SharedURLLoaderFactory> GetSharedURLLoaderFactory(); scoped_refptr<network::SharedURLLoaderFactory> GetSharedURLLoaderFactory();
network::mojom::HostResolver* GetHostResolver(); network::mojom::HostResolver* GetHostResolver();
bool ShouldUseNetworkObserverfromURLLoaderFactory() const;
private: private:
scoped_refptr<network::SharedURLLoaderFactory> factory_; scoped_refptr<network::SharedURLLoaderFactory> factory_;
mojo::Remote<network::mojom::HostResolver> host_resolver_; mojo::Remote<network::mojom::HostResolver> host_resolver_;
bool should_use_network_observer_from_url_loader_factory_ = false;
}; };
class NodeService : public node::mojom::NodeService { class NodeService : public node::mojom::NodeService {

View file

@ -17,6 +17,7 @@ struct NodeServiceParams {
blink.mojom.MessagePortDescriptor port; blink.mojom.MessagePortDescriptor port;
pending_remote<network.mojom.URLLoaderFactory> url_loader_factory; pending_remote<network.mojom.URLLoaderFactory> url_loader_factory;
pending_remote<network.mojom.HostResolver> host_resolver; pending_remote<network.mojom.HostResolver> host_resolver;
bool use_network_observer_from_url_loader_factory = false;
}; };
[ServiceSandbox=sandbox.mojom.Sandbox.kNoSandbox] [ServiceSandbox=sandbox.mojom.Sandbox.kNoSandbox]

View file

@ -2,8 +2,9 @@ import { expect } from 'chai';
import * as childProcess from 'node:child_process'; import * as childProcess from 'node:child_process';
import * as path from 'node:path'; import * as path from 'node:path';
import { BrowserWindow, MessageChannelMain, utilityProcess, app } from 'electron/main'; 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 { closeWindow } from './lib/window-helpers';
import { respondOnce, randomString, kOneKiloByte } from './lib/net-helpers';
import { once } from 'node:events'; import { once } from 'node:events';
import { pathToFileURL } from 'node:url'; import { pathToFileURL } from 'node:url';
import { setImmediate } from 'node:timers/promises'; import { setImmediate } from 'node:timers/promises';
@ -508,5 +509,193 @@ describe('utilityProcess module', () => {
expect(child.kill()).to.be.true(); expect(child.kill()).to.be.true();
await exit; 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');
});
}); });
}); });

View file

@ -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();
}
}