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:
Robo 2024-08-16 18:24:19 +09:00 committed by GitHub
parent 7123b313cf
commit 8c1be5ade2
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:
* `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

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
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)

View file

@ -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.

View file

@ -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",

View file

@ -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", &current_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;
}

View file

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

View file

@ -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>>

View file

@ -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.

View file

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

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

@ -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.

View file

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

View file

@ -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 {

View file

@ -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]

View file

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

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