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