feat: support app#login event for utility process net requests (#43317)
* feat: support app#login event for utility process net requests * 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 * chore: fix lint
This commit is contained in:
parent
7123b313cf
commit
8c1be5ade2
17 changed files with 536 additions and 37 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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)
|
||||
|
||||
|
|
|
@ -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: `systemPreferences.accessibilityDisplayShouldReduceTransparency`
|
||||
|
||||
The `systemPreferences.accessibilityDisplayShouldReduceTransparency` property is now deprecated in favor of the new `nativeTheme.prefersReducedTransparency`, which provides identical information and works cross-platform.
|
||||
|
|
|
@ -449,6 +449,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",
|
||||
|
|
|
@ -64,7 +64,8 @@ UtilityProcessWrapper::UtilityProcessWrapper(
|
|||
std::map<IOHandle, IOType> 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);
|
||||
|
@ -204,6 +205,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(
|
||||
|
@ -214,6 +220,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));
|
||||
}
|
||||
|
@ -231,6 +239,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");
|
||||
}
|
||||
|
||||
|
@ -379,6 +390,7 @@ gin::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
|
|||
|
||||
std::u16string display_name;
|
||||
bool use_plugin_helper = false;
|
||||
bool create_network_observer = false;
|
||||
std::map<IOHandle, IOType> stdio;
|
||||
base::FilePath current_working_directory;
|
||||
base::EnvironmentMap env_map;
|
||||
|
@ -404,6 +416,7 @@ gin::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
|
|||
|
||||
opts.Get("serviceName", &display_name);
|
||||
opts.Get("cwd", ¤t_working_directory);
|
||||
opts.Get("respondToAuthRequestsFromMainProcess", &create_network_observer);
|
||||
|
||||
std::vector<std::string> stdio_arr{"ignore", "inherit", "inherit"};
|
||||
opts.Get("stdio", &stdio_arr);
|
||||
|
@ -424,10 +437,10 @@ gin::Handle<UtilityProcessWrapper> 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;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#include "mojo/public/cpp/bindings/receiver.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.h"
|
||||
|
@ -64,7 +65,8 @@ class UtilityProcessWrapper
|
|||
std::map<IOHandle, IOType> 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();
|
||||
|
||||
|
@ -99,6 +101,8 @@ class UtilityProcessWrapper
|
|||
std::unique_ptr<mojo::Connector> connector_;
|
||||
blink::MessagePortDescriptor host_port_;
|
||||
mojo::Remote<node::mojom::NodeService> node_service_remote_;
|
||||
std::optional<electron::URLLoaderNetworkObserver>
|
||||
url_loader_network_observer_;
|
||||
base::WeakPtrFactory<UtilityProcessWrapper> weak_factory_{this};
|
||||
};
|
||||
|
||||
|
|
|
@ -1635,8 +1635,8 @@ ElectronBrowserClient::CreateLoginDelegate(
|
|||
bool first_auth_attempt,
|
||||
LoginAuthRequiredCallback auth_required_callback) {
|
||||
return std::make_unique<LoginHandler>(
|
||||
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<std::unique_ptr<blink::URLLoaderThrottle>>
|
||||
|
|
|
@ -10,6 +10,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"
|
||||
|
@ -25,39 +26,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<net::HttpResponseHeaders> 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<net::HttpResponseHeaders> 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::WebContents> 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
|
||||
|
@ -67,10 +73,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.
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
#ifndef ELECTRON_SHELL_BROWSER_LOGIN_HANDLER_H_
|
||||
#define ELECTRON_SHELL_BROWSER_LOGIN_HANDLER_H_
|
||||
|
||||
#include "base/process/process_handle.h"
|
||||
#include "base/values.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;
|
||||
|
@ -21,12 +21,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<net::HttpResponseHeaders> response_headers,
|
||||
bool first_auth_attempt,
|
||||
|
@ -39,7 +39,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<net::HttpResponseHeaders> response_headers,
|
||||
bool first_auth_attempt);
|
||||
|
|
120
shell/browser/net/url_loader_network_observer.cc
Normal file
120
shell/browser/net/url_loader_network_observer.cc
Normal 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
|
82
shell/browser/net/url_loader_network_observer.h
Normal file
82
shell/browser/net/url_loader_network_observer.h
Normal 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_
|
|
@ -334,13 +334,21 @@ SimpleURLLoaderWrapper::SimpleURLLoaderWrapper(
|
|||
DETACH_FROM_SEQUENCE(sequence_checker_);
|
||||
if (!request_->trusted_params)
|
||||
request_->trusted_params = network::ResourceRequest::TrustedParams();
|
||||
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);
|
||||
bool create_network_observer = true;
|
||||
if (electron::IsUtilityProcess()) {
|
||||
create_network_observer =
|
||||
!URLLoaderBundle::GetInstance()
|
||||
->ShouldUseNetworkObserverfromURLLoaderFactory();
|
||||
}
|
||||
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
|
||||
// every header passed. The following setting will allow us to capture the
|
||||
// raw headers in the URLLoader.
|
||||
|
|
|
@ -33,11 +33,14 @@ URLLoaderBundle* URLLoaderBundle::GetInstance() {
|
|||
|
||||
void URLLoaderBundle::SetURLLoaderFactory(
|
||||
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(
|
||||
std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
|
||||
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<network::SharedURLLoaderFactory>
|
||||
|
@ -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<node::mojom::NodeService> 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<JavascriptEnvironment>(node_bindings_->uv_loop());
|
||||
|
||||
|
|
|
@ -39,13 +39,16 @@ class URLLoaderBundle {
|
|||
static URLLoaderBundle* GetInstance();
|
||||
void SetURLLoaderFactory(
|
||||
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();
|
||||
network::mojom::HostResolver* GetHostResolver();
|
||||
bool ShouldUseNetworkObserverfromURLLoaderFactory() const;
|
||||
|
||||
private:
|
||||
scoped_refptr<network::SharedURLLoaderFactory> factory_;
|
||||
mojo::Remote<network::mojom::HostResolver> host_resolver_;
|
||||
bool should_use_network_observer_from_url_loader_factory_ = false;
|
||||
};
|
||||
|
||||
class NodeService : public node::mojom::NodeService {
|
||||
|
|
|
@ -17,6 +17,7 @@ struct NodeServiceParams {
|
|||
blink.mojom.MessagePortDescriptor port;
|
||||
pending_remote<network.mojom.URLLoaderFactory> url_loader_factory;
|
||||
pending_remote<network.mojom.HostResolver> host_resolver;
|
||||
bool use_network_observer_from_url_loader_factory = false;
|
||||
};
|
||||
|
||||
[ServiceSandbox=sandbox.mojom.Sandbox.kNoSandbox]
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
44
spec/fixtures/api/utility-process/net.js
vendored
Normal file
44
spec/fixtures/api/utility-process/net.js
vendored
Normal 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();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue