fix: implement 'login' event for net.ClientRequest (#21096)

This commit is contained in:
Jeremy Apthorp 2019-11-14 10:01:18 -08:00 committed by GitHub
parent 878ab916d2
commit 4f1536479e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 445 additions and 38 deletions

View file

@ -323,8 +323,8 @@ Returns:
* `port` Integer
* `realm` String
* `callback` Function
* `username` String
* `password` String
* `username` String (optional)
* `password` String (optional)
Emitted when `webContents` wants to do basic auth.
@ -341,6 +341,10 @@ app.on('login', (event, webContents, details, authInfo, callback) => {
})
```
If `callback` is called without a username or password, the authentication
request will be cancelled and the authentication error will be returned to the
page.
### Event: 'gpu-info-update'
Emitted whenever there is a GPU info update.

View file

@ -70,8 +70,8 @@ Returns:
* `port` Integer
* `realm` String
* `callback` Function
* `username` String
* `password` String
* `username` String (optional)
* `password` String (optional)
Emitted when an authenticating proxy is asking for user credentials.

View file

@ -463,8 +463,8 @@ Returns:
* `port` Integer
* `realm` String
* `callback` Function
* `username` String
* `password` String
* `username` String (optional)
* `password` String (optional)
Emitted when `webContents` wants to do basic auth.

View file

@ -247,22 +247,11 @@ class ClientRequest extends EventEmitter {
})
urlRequest.on('login', (event, authInfo, callback) => {
this.emit('login', authInfo, (username, password) => {
// If null or undefined username/password, force to empty string.
if (username === null || username === undefined) {
username = ''
const handled = this.emit('login', authInfo, callback)
if (!handled) {
// If there were no listeners, cancel the authentication request.
callback()
}
if (typeof username !== 'string') {
throw new Error('username must be a string')
}
if (password === null || password === undefined) {
password = ''
}
if (typeof password !== 'string') {
throw new Error('password must be a string')
}
callback(username, password)
})
})
if (callback) {

View file

@ -86,3 +86,4 @@ fix_ambiguous_reference_to_data.patch
backport_fix_msstl_compat_in_ui_events.patch
build_win_fix_msstl_compatibility_for_pdf.patch
fix_missing_algorithm_include.patch
add_trustedauthclient_to_urlloaderfactory.patch

View file

@ -0,0 +1,158 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jeremy Apthorp <nornagon@nornagon.net>
Date: Tue, 12 Nov 2019 11:50:16 -0800
Subject: add TrustedAuthClient to URLLoaderFactory
This allows intercepting authentication requests for the 'net' module.
Without this, the 'login' event for electron.net.ClientRequest can't be
implemented, because the existing path checks for the presence of a
WebContents, and cancels the authentication if there's no WebContents
available, which there isn't in the case of the 'net' module.
diff --git a/services/network/public/mojom/network_context.mojom b/services/network/public/mojom/network_context.mojom
index 6b14d8354375377526e141ee499a7583be3f22b0..eeb9e19c0ecdf4631e596e7c0927693f2239f293 100644
--- a/services/network/public/mojom/network_context.mojom
+++ b/services/network/public/mojom/network_context.mojom
@@ -181,6 +181,25 @@ interface TrustedURLLoaderHeaderClient {
pending_receiver<TrustedHeaderClient> header_client);
};
+interface TrustedAuthClient {
+ OnAuthRequired(
+ mojo_base.mojom.UnguessableToken? window_id,
+ uint32 process_id,
+ uint32 routing_id,
+ uint32 request_id,
+ url.mojom.Url url,
+ bool first_auth_attempt,
+ AuthChallengeInfo auth_info,
+ URLResponseHead? head,
+ pending_remote<AuthChallengeResponder> auth_challenge_responder);
+};
+interface TrustedURLLoaderAuthClient {
+ // When a new URLLoader is created, this will be called to pass a
+ // corresponding |auth_client|.
+ OnLoaderCreated(int32 request_id,
+ pending_receiver<TrustedAuthClient> auth_client);
+};
+
interface CertVerifierClient {
Verify(
int32 default_error,
@@ -559,6 +578,8 @@ struct URLLoaderFactoryParams {
// impact because of the extra process hops, so use should be minimized.
pending_remote<TrustedURLLoaderHeaderClient>? header_client;
+ pending_remote<TrustedURLLoaderAuthClient>? auth_client;
+
// If non-empty array is given, |factory_bound_allow_patterns| is used for
// CORS checks in addition to the per-context allow patterns that is managed
// via NetworkContext interface. This still respects the per-context block
diff --git a/services/network/url_loader.cc b/services/network/url_loader.cc
index d4e13ffaed76847b00cf98b248ba17ad70a9884c..33ab3ea9c60e097d8525f1066f3890a5bccd754a 100644
--- a/services/network/url_loader.cc
+++ b/services/network/url_loader.cc
@@ -335,6 +335,7 @@ URLLoader::URLLoader(
base::WeakPtr<KeepaliveStatisticsRecorder> keepalive_statistics_recorder,
base::WeakPtr<NetworkUsageAccumulator> network_usage_accumulator,
mojom::TrustedURLLoaderHeaderClient* url_loader_header_client,
+ mojom::TrustedURLLoaderAuthClient* url_loader_auth_client,
mojom::OriginPolicyManager* origin_policy_manager)
: url_request_context_(url_request_context),
network_service_client_(network_service_client),
@@ -391,6 +392,11 @@ URLLoader::URLLoader(
header_client_.set_disconnect_handler(
base::BindOnce(&URLLoader::OnConnectionError, base::Unretained(this)));
}
+ if (url_loader_auth_client) {
+ url_loader_auth_client->OnLoaderCreated(request_id_, auth_client_.BindNewPipeAndPassReceiver());
+ auth_client_.set_disconnect_handler(
+ base::BindOnce(&URLLoader::OnConnectionError, base::Unretained(this)));
+ }
if (want_raw_headers_) {
options_ |= mojom::kURLLoadOptionSendSSLInfoWithResponse |
mojom::kURLLoadOptionSendSSLInfoForCertificateError;
@@ -818,7 +824,7 @@ void URLLoader::OnReceivedRedirect(net::URLRequest* url_request,
void URLLoader::OnAuthRequired(net::URLRequest* url_request,
const net::AuthChallengeInfo& auth_info) {
- if (!network_context_client_) {
+ if (!network_context_client_ && !auth_client_) {
OnAuthCredentials(base::nullopt);
return;
}
@@ -834,10 +840,18 @@ void URLLoader::OnAuthRequired(net::URLRequest* url_request,
if (url_request->response_headers())
head.headers = url_request->response_headers();
head.auth_challenge_info = auth_info;
- network_context_client_->OnAuthRequired(
- fetch_window_id_, factory_params_->process_id, render_frame_id_,
- request_id_, url_request_->url(), first_auth_attempt_, auth_info, head,
- auth_challenge_responder_receiver_.BindNewPipeAndPassRemote());
+
+ if (auth_client_) {
+ auth_client_->OnAuthRequired(
+ fetch_window_id_, factory_params_->process_id, render_frame_id_,
+ request_id_, url_request_->url(), first_auth_attempt_, auth_info, head,
+ auth_challenge_responder_receiver_.BindNewPipeAndPassRemote());
+ } else {
+ network_context_client_->OnAuthRequired(
+ fetch_window_id_, factory_params_->process_id, render_frame_id_,
+ request_id_, url_request_->url(), first_auth_attempt_, auth_info, head,
+ auth_challenge_responder_receiver_.BindNewPipeAndPassRemote());
+ }
auth_challenge_responder_receiver_.set_disconnect_handler(
base::BindOnce(&URLLoader::DeleteSelf, base::Unretained(this)));
diff --git a/services/network/url_loader.h b/services/network/url_loader.h
index 0a47148a52a46f8a6f12f503731623f87e15b173..db8ca018c7e99a1a1acea156b4d49a755b93cc09 100644
--- a/services/network/url_loader.h
+++ b/services/network/url_loader.h
@@ -85,6 +85,7 @@ class COMPONENT_EXPORT(NETWORK_SERVICE) URLLoader
base::WeakPtr<KeepaliveStatisticsRecorder> keepalive_statistics_recorder,
base::WeakPtr<NetworkUsageAccumulator> network_usage_accumulator,
mojom::TrustedURLLoaderHeaderClient* url_loader_header_client,
+ mojom::TrustedURLLoaderAuthClient* url_loader_auth_client,
mojom::OriginPolicyManager* origin_policy_manager);
~URLLoader() override;
@@ -362,6 +363,7 @@ class COMPONENT_EXPORT(NETWORK_SERVICE) URLLoader
base::Optional<base::UnguessableToken> fetch_window_id_;
mojo::Remote<mojom::TrustedHeaderClient> header_client_;
+ mojo::Remote<mojom::TrustedAuthClient> auth_client_;
std::unique_ptr<FileOpenerForUpload> file_opener_for_upload_;
diff --git a/services/network/url_loader_factory.cc b/services/network/url_loader_factory.cc
index 7145e0e96550d554bb1df85bd79818ec9a45f7b1..53225eb1b0b7f1aa2498cecc8222f9f897ac364f 100644
--- a/services/network/url_loader_factory.cc
+++ b/services/network/url_loader_factory.cc
@@ -65,6 +65,7 @@ URLLoaderFactory::URLLoaderFactory(
params_(std::move(params)),
resource_scheduler_client_(std::move(resource_scheduler_client)),
header_client_(std::move(params_->header_client)),
+ auth_client_(std::move(params_->auth_client)),
cors_url_loader_factory_(cors_url_loader_factory) {
DCHECK(context);
DCHECK_NE(mojom::kInvalidProcessId, params_->process_id);
@@ -209,6 +210,7 @@ void URLLoaderFactory::CreateLoaderAndStart(
resource_scheduler_client_, std::move(keepalive_statistics_recorder),
std::move(network_usage_accumulator),
header_client_.is_bound() ? header_client_.get() : nullptr,
+ auth_client_.is_bound() ? auth_client_.get() : nullptr,
context_->origin_policy_manager());
cors_url_loader_factory_->OnLoaderCreated(std::move(loader));
}
diff --git a/services/network/url_loader_factory.h b/services/network/url_loader_factory.h
index 7b143aa49be833ddf05b7b99bea19ee0b674b79c..6d1fbca87e3827c953fdac2cfb96806114d8aea9 100644
--- a/services/network/url_loader_factory.h
+++ b/services/network/url_loader_factory.h
@@ -71,6 +71,7 @@ class URLLoaderFactory : public mojom::URLLoaderFactory {
mojom::URLLoaderFactoryParamsPtr params_;
scoped_refptr<ResourceSchedulerClient> resource_scheduler_client_;
mojo::Remote<mojom::TrustedURLLoaderHeaderClient> header_client_;
+ mojo::Remote<mojom::TrustedURLLoaderAuthClient> auth_client_;
// |cors_url_loader_factory_| owns this.
cors::CorsURLLoaderFactory* cors_url_loader_factory_;

View file

@ -6,6 +6,7 @@
#include <utility>
#include "base/containers/id_map.h"
#include "gin/handle.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/system/string_data_source.h"
@ -13,6 +14,7 @@
#include "services/network/public/mojom/chunked_data_pipe_getter.mojom.h"
#include "shell/browser/api/atom_api_session.h"
#include "shell/browser/atom_browser_context.h"
#include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/net_converter.h"
#include "shell/common/gin_helper/dictionary.h"
@ -51,6 +53,11 @@ namespace api {
namespace {
base::IDMap<URLRequest*>& GetAllRequests() {
static base::NoDestructor<base::IDMap<URLRequest*>> s_all_requests;
return *s_all_requests;
}
// Network state for request and response.
enum State {
STATE_STARTED = 1 << 0,
@ -166,7 +173,9 @@ class ChunkedDataPipeGetter : public UploadDataPipeGetter,
mojo::ReceiverSet<network::mojom::ChunkedDataPipeGetter> receiver_set_;
};
URLRequest::URLRequest(gin::Arguments* args) : weak_factory_(this) {
URLRequest::URLRequest(gin::Arguments* args)
: id_(GetAllRequests().Add(this)), weak_factory_(this) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
request_ = std::make_unique<network::ResourceRequest>();
gin_helper::Dictionary dict;
if (args->GetNext(&dict)) {
@ -176,6 +185,8 @@ URLRequest::URLRequest(gin::Arguments* args) : weak_factory_(this) {
request_->redirect_mode = redirect_mode_;
}
request_->render_frame_id = id_;
std::string partition;
gin::Handle<api::Session> session;
if (!dict.Get("session", &session)) {
@ -192,6 +203,38 @@ URLRequest::URLRequest(gin::Arguments* args) : weak_factory_(this) {
URLRequest::~URLRequest() = default;
URLRequest* URLRequest::FromID(uint32_t id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return GetAllRequests().Lookup(id);
}
void URLRequest::OnAuthRequired(
const GURL& url,
bool first_auth_attempt,
net::AuthChallengeInfo auth_info,
network::mojom::URLResponseHeadPtr head,
mojo::PendingRemote<network::mojom::AuthChallengeResponder>
auth_challenge_responder) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
mojo::Remote<network::mojom::AuthChallengeResponder> auth_responder(
std::move(auth_challenge_responder));
auth_responder.set_disconnect_handler(
base::BindOnce(&URLRequest::Cancel, weak_factory_.GetWeakPtr()));
auto cb = base::BindOnce(
[](mojo::Remote<network::mojom::AuthChallengeResponder> auth_responder,
gin::Arguments* args) {
base::string16 username_str, password_str;
if (!args->GetNext(&username_str) || !args->GetNext(&password_str)) {
auth_responder->OnAuthCredentials(base::nullopt);
return;
}
auth_responder->OnAuthCredentials(
net::AuthCredentials(username_str, password_str));
},
std::move(auth_responder));
Emit("login", auth_info, base::AdaptCallbackForRepeating(std::move(cb)));
}
bool URLRequest::NotStarted() const {
return request_state_ == 0;
}
@ -512,11 +555,12 @@ void URLRequest::EmitError(EventType type, base::StringPiece message) {
}
template <typename... Args>
void URLRequest::EmitEvent(EventType type, Args... args) {
void URLRequest::EmitEvent(EventType type, Args&&... args) {
const char* method =
type == EventType::kRequest ? "_emitRequestEvent" : "_emitResponseEvent";
v8::HandleScope handle_scope(isolate());
gin_helper::CustomEmit(isolate(), GetWrapper(), method, args...);
gin_helper::CustomEmit(isolate(), GetWrapper(), method,
std::forward<Args>(args)...);
}
// static

View file

@ -17,6 +17,7 @@
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/cpp/simple_url_loader_stream_consumer.h"
#include "services/network/public/mojom/data_pipe_getter.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "shell/common/gin_helper/event_emitter.h"
namespace electron {
@ -32,6 +33,15 @@ class URLRequest : public gin_helper::EventEmitter<URLRequest>,
static void BuildPrototype(v8::Isolate* isolate,
v8::Local<v8::FunctionTemplate> prototype);
static URLRequest* FromID(uint32_t id);
void OnAuthRequired(
const GURL& url,
bool first_auth_attempt,
net::AuthChallengeInfo auth_info,
network::mojom::URLResponseHeadPtr head,
mojo::PendingRemote<network::mojom::AuthChallengeResponder>
auth_challenge_responder);
protected:
explicit URLRequest(gin::Arguments* args);
@ -89,7 +99,7 @@ class URLRequest : public gin_helper::EventEmitter<URLRequest>,
};
void EmitError(EventType type, base::StringPiece error);
template <typename... Args>
void EmitEvent(EventType type, Args... args);
void EmitEvent(EventType type, Args&&... args);
std::unique_ptr<network::ResourceRequest> request_;
std::unique_ptr<network::SimpleURLLoader> loader_;
@ -132,6 +142,8 @@ class URLRequest : public gin_helper::EventEmitter<URLRequest>,
// Used by pin/unpin to manage lifetime.
v8::Global<v8::Object> wrapper_;
uint32_t id_;
base::WeakPtrFactory<URLRequest> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(URLRequest);

View file

@ -28,10 +28,12 @@
#include "content/browser/blob_storage/chrome_blob_storage_context.h" // nogncheck
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/base/escape.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "shell/browser/api/atom_api_url_request.h"
#include "shell/browser/atom_browser_client.h"
#include "shell/browser/atom_browser_main_parts.h"
#include "shell/browser/atom_download_manager_delegate.h"
@ -325,6 +327,7 @@ AtomBrowserContext::GetURLLoaderFactory() {
network::mojom::URLLoaderFactoryParamsPtr params =
network::mojom::URLLoaderFactoryParams::New();
params->header_client = std::move(header_client);
params->auth_client = auth_client_.BindNewPipeAndPassRemote();
params->process_id = network::mojom::kBrowserProcessId;
params->is_trusted = true;
params->is_corb_enabled = false;
@ -342,6 +345,39 @@ AtomBrowserContext::GetURLLoaderFactory() {
return url_loader_factory_;
}
class AuthResponder : public network::mojom::TrustedAuthClient {
public:
AuthResponder() {}
~AuthResponder() override = default;
private:
void OnAuthRequired(
const base::Optional<::base::UnguessableToken>& window_id,
uint32_t process_id,
uint32_t routing_id,
uint32_t request_id,
const ::GURL& url,
bool first_auth_attempt,
const ::net::AuthChallengeInfo& auth_info,
::network::mojom::URLResponseHeadPtr head,
mojo::PendingRemote<network::mojom::AuthChallengeResponder>
auth_challenge_responder) override {
api::URLRequest* url_request = api::URLRequest::FromID(routing_id);
if (url_request) {
url_request->OnAuthRequired(url, first_auth_attempt, auth_info,
std::move(head),
std::move(auth_challenge_responder));
}
}
};
void AtomBrowserContext::OnLoaderCreated(
int32_t request_id,
mojo::PendingReceiver<network::mojom::TrustedAuthClient> auth_client) {
mojo::MakeSelfOwnedReceiver(std::make_unique<AuthResponder>(),
std::move(auth_client));
}
content::PushMessagingService* AtomBrowserContext::GetPushMessagingService() {
return nullptr;
}

View file

@ -17,6 +17,7 @@
#include "content/public/browser/browser_context.h"
#include "content/public/browser/resource_context.h"
#include "electron/buildflags/buildflags.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "shell/browser/media/media_device_id_salt.h"
@ -50,7 +51,8 @@ class WebViewManager;
class AtomBrowserContext
: public base::RefCountedDeleteOnSequence<AtomBrowserContext>,
public content::BrowserContext {
public content::BrowserContext,
public network::mojom::TrustedURLLoaderAuthClient {
public:
// partition_id => browser_context
struct PartitionKey {
@ -149,6 +151,10 @@ class AtomBrowserContext
friend class base::RefCountedDeleteOnSequence<AtomBrowserContext>;
friend class base::DeleteHelper<AtomBrowserContext>;
void OnLoaderCreated(int32_t request_id,
mojo::PendingReceiver<network::mojom::TrustedAuthClient>
header_client) override;
// Initialize pref registry.
void InitPrefs();
@ -185,6 +191,7 @@ class AtomBrowserContext
// Shared URLLoaderFactory.
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
mojo::Receiver<network::mojom::TrustedURLLoaderAuthClient> auth_client_{this};
base::WeakPtrFactory<AtomBrowserContext> weak_factory_;

View file

@ -10,9 +10,12 @@
#include <vector>
#include "base/callback.h"
#include "base/strings/string16.h"
#include "gin/arguments.h"
#include "gin/dictionary.h"
#include "shell/browser/api/atom_api_web_contents.h"
#include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/net_converter.h"
#include "shell/common/gin_converters/value_converter.h"
@ -57,7 +60,7 @@ void LoginHandler::EmitEvent(
v8::HandleScope scope(isolate);
auto details = gin::Dictionary::CreateEmpty(isolate);
details.Set("url", url.spec());
details.Set("url", url);
// 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
@ -70,17 +73,23 @@ void LoginHandler::EmitEvent(
api_web_contents->Emit("login", std::move(details), auth_info,
base::BindOnce(&LoginHandler::CallbackFromJS,
weak_factory_.GetWeakPtr()));
if (!default_prevented) {
if (!default_prevented && auth_required_callback_) {
std::move(auth_required_callback_).Run(base::nullopt);
}
}
LoginHandler::~LoginHandler() = default;
void LoginHandler::CallbackFromJS(base::string16 username,
base::string16 password) {
void LoginHandler::CallbackFromJS(gin::Arguments* args) {
if (auth_required_callback_) {
base::string16 username, password;
if (!args->GetNext(&username) || !args->GetNext(&password)) {
std::move(auth_required_callback_).Run(base::nullopt);
return;
}
std::move(auth_required_callback_)
.Run(net::AuthCredentials(username, password));
}
}
} // namespace electron

View file

@ -5,7 +5,6 @@
#ifndef SHELL_BROWSER_LOGIN_HANDLER_H_
#define SHELL_BROWSER_LOGIN_HANDLER_H_
#include "base/strings/string16.h"
#include "base/values.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/login_delegate.h"
@ -15,6 +14,10 @@ namespace content {
class WebContents;
}
namespace gin {
class Arguments;
}
namespace electron {
// Handles HTTP basic auth.
@ -36,7 +39,7 @@ class LoginHandler : public content::LoginDelegate,
const GURL& url,
scoped_refptr<net::HttpResponseHeaders> response_headers,
bool first_auth_attempt);
void CallbackFromJS(base::string16 username, base::string16 password);
void CallbackFromJS(gin::Arguments* args);
LoginAuthRequiredCallback auth_required_callback_;

View file

@ -13,7 +13,6 @@
#include "base/task/post_task.h"
#include "content/public/browser/file_url_loader.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/strong_binding.h"
#include "mojo/public/cpp/system/data_pipe_producer.h"
#include "mojo/public/cpp/system/file_data_source.h"
#include "net/base/filename_util.h"

View file

@ -1,5 +1,5 @@
import { expect } from 'chai'
import { net as originalNet, session, ClientRequest } from 'electron'
import { net as originalNet, session, ClientRequest, BrowserWindow } from 'electron'
import * as http from 'http'
import * as url from 'url'
import { AddressInfo } from 'net'
@ -206,6 +206,135 @@ describe('net module', () => {
urlRequest.end()
})
})
it('should emit the login event when 401', async () => {
const [user, pass] = ['user', 'pass']
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')
})
let loginAuthInfo: any
await new Promise((resolve, reject) => {
const request = net.request({ method: 'GET', url: serverUrl })
request.on('response', (response) => {
response.on('error', reject)
response.on('data', () => {})
response.on('end', () => resolve())
})
request.on('login', (authInfo, cb) => {
loginAuthInfo = authInfo
cb(user, pass)
})
request.on('error', reject)
request.end()
})
expect(loginAuthInfo.realm).to.equal('Foo')
expect(loginAuthInfo.scheme).to.equal('basic')
})
it('should produce an error on the response object when cancelling authentication', async () => {
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')
})
await expect(new Promise((resolve, reject) => {
const request = net.request({ method: 'GET', url: serverUrl })
request.on('response', (response) => {
response.on('error', reject)
response.on('data', () => {})
response.on('end', () => resolve())
})
request.on('login', (authInfo, cb) => {
cb()
})
request.on('error', reject)
request.end()
})).to.eventually.be.rejectedWith('net::ERR_HTTP_RESPONSE_CODE_FAILURE')
})
it('should share credentials with WebContents', async () => {
const [user, pass] = ['user', 'pass']
const serverUrl = await new Promise<string>((resolve) => {
const server = http.createServer((request, response) => {
if (!request.headers.authorization) {
return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end()
}
return response.writeHead(200).end('ok')
})
server.listen(0, '127.0.0.1', () => {
resolve(`http://127.0.0.1:${(server.address() as AddressInfo).port}`)
})
after(() => { server.close() })
})
const bw = new BrowserWindow({ show: false })
const loaded = bw.loadURL(serverUrl)
bw.webContents.on('login', (event, details, authInfo, cb) => {
event.preventDefault()
cb(user, pass)
})
await loaded
bw.close()
await new Promise((resolve, reject) => {
const request = net.request({ method: 'GET', url: serverUrl })
request.on('response', (response) => {
response.on('error', reject)
response.on('data', () => {})
response.on('end', () => resolve())
})
request.on('login', () => {
// we shouldn't receive a login event, because the credentials should
// be cached.
reject(new Error('unexpected login event'))
})
request.on('error', reject)
request.end()
})
})
it('should share proxy credentials with WebContents', async () => {
const [user, pass] = ['user', 'pass']
const proxyPort = await new Promise<number>((resolve) => {
const server = http.createServer((request, response) => {
if (!request.headers['proxy-authorization']) {
return response.writeHead(407, { 'Proxy-Authenticate': 'Basic realm="Foo"' }).end()
}
return response.writeHead(200).end('ok')
})
server.listen(0, '127.0.0.1', () => {
resolve((server.address() as AddressInfo).port)
})
after(() => { server.close() })
})
const customSession = session.fromPartition(`net-proxy-test-${Math.random()}`)
await customSession.setProxy({ proxyRules: `127.0.0.1:${proxyPort}`, proxyBypassRules: '<-loopback>' })
const bw = new BrowserWindow({ show: false, webPreferences: { session: customSession } })
const loaded = bw.loadURL('http://127.0.0.1:9999')
bw.webContents.on('login', (event, details, authInfo, cb) => {
event.preventDefault()
cb(user, pass)
})
await loaded
bw.close()
await new Promise((resolve, reject) => {
const request = net.request({ method: 'GET', url: 'http://127.0.0.1:9999', session: customSession })
request.on('response', (response) => {
response.on('error', reject)
response.on('data', () => {})
response.on('end', () => resolve())
})
request.on('login', () => {
// we shouldn't receive a login event, because the credentials should
// be cached.
reject(new Error('unexpected login event'))
})
request.on('error', reject)
request.end()
})
})
})
describe('ClientRequest API', () => {

View file

@ -520,6 +520,7 @@ describe('session module', () => {
})
response.on('error', (error: any) => { reject(new Error(error)) })
})
request.on('error', (error: any) => { reject(new Error(error)) })
request.end()
})
// the first time should throw due to unauthenticated

View file

@ -1534,7 +1534,7 @@ describe('webContents module', () => {
}
response
.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' })
.end()
.end('401')
}).listen(0, '127.0.0.1', () => {
serverPort = (server.address() as AddressInfo).port
serverUrl = `http://127.0.0.1:${serverPort}`
@ -1557,6 +1557,10 @@ describe('webContents module', () => {
})
})
afterEach(async () => {
await session.defaultSession.clearAuthCache({ type: 'password' })
})
after(() => {
server.close()
proxyServer.close()
@ -1607,5 +1611,16 @@ describe('webContents module', () => {
expect(eventAuthInfo.port).to.equal(proxyServerPort)
expect(eventAuthInfo.realm).to.equal('Foo')
})
it('cancels authentication when callback is called with no arguments', async () => {
const w = new BrowserWindow({ show: false })
w.webContents.on('login', (event, request, authInfo, cb) => {
event.preventDefault()
cb()
})
await w.loadURL(serverUrl)
const body = await w.webContents.executeJavaScript(`document.documentElement.textContent`)
expect(body).to.equal('401')
})
})
})