feat: add net module to utility process (#40017)
* chore: initial prototype of net api from utility process * chore: update url loader to work on both browser and utility processes * chore: add net files to utility process bundle * chore: re-add app ready check but only on main process * chore: replace browser thread dcheck's with sequence checker * refactor: move url loader from browser to common * refactor: move net-client-request.ts from browser to common * docs: add utility process to net api docs * refactor: move net module app ready check to browser only * refactor: switch import from main to common after moving to common * test: add basic net module test for utility process * refactor: switch browser pid with utility pid * refactor: move electron_api_net from browser to common * chore: add fetch to utility net module * chore: add isOnline and online to utility net module * refactor: move net spec helpers into helper file * refactor: break apart net module tests Adds two additional net module test files: `api-net-session-spec.ts` for tests that depend on a session being available (aka depend on running on the main process) and `api-net-custom-protocols-spec.ts` for custom protocol tests. This enables running `api-net-spec.ts` in the utility process. * test: add utility process mocha runner to run net module tests * docs: add utility process to net module classes * refactor: update imports in lib/utility to use electron/utility * chore: check browser context before using in main process Since the browser context supplied to the SimpleURLLoaderWrapper can now be null for use in the UtilityProcess, adding a null check for the main process before use to get a more sensible error if something goes wrong. Co-authored-by: Cheng Zhao <github@zcbenz.com> * chore: remove test debugging * chore: remove unnecessary header include * docs: add utility process net module limitations * test: run net module tests in utility process individually * refactor: clean up prior utility process net tests * chore: add resolveHost to utility process net module * chore: replace resolve host dcheck with sequence checker * test: add net module tests for net.resolveHost * docs: remove utility process limitation for resolveHost --------- Co-authored-by: deepak1556 <hop2deep@gmail.com> Co-authored-by: Cheng Zhao <github@zcbenz.com>
This commit is contained in:
parent
f229201f41
commit
8c71e2adc9
29 changed files with 2642 additions and 2290 deletions
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> Make HTTP/HTTPS requests.
|
> Make HTTP/HTTPS requests.
|
||||||
|
|
||||||
Process: [Main](../glossary.md#main-process)<br />
|
Process: [Main](../glossary.md#main-process), [Utility](../glossary.md#utility-process)<br />
|
||||||
_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._
|
_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._
|
||||||
|
|
||||||
`ClientRequest` implements the [Writable Stream](https://nodejs.org/api/stream.html#stream_writable_streams)
|
`ClientRequest` implements the [Writable Stream](https://nodejs.org/api/stream.html#stream_writable_streams)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> Handle responses to HTTP/HTTPS requests.
|
> Handle responses to HTTP/HTTPS requests.
|
||||||
|
|
||||||
Process: [Main](../glossary.md#main-process)<br />
|
Process: [Main](../glossary.md#main-process), [Utility](../glossary.md#utility-process)<br />
|
||||||
_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._
|
_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._
|
||||||
|
|
||||||
`IncomingMessage` implements the [Readable Stream](https://nodejs.org/api/stream.html#stream_readable_streams)
|
`IncomingMessage` implements the [Readable Stream](https://nodejs.org/api/stream.html#stream_readable_streams)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> Issue HTTP/HTTPS requests using Chromium's native networking library
|
> Issue HTTP/HTTPS requests using Chromium's native networking library
|
||||||
|
|
||||||
Process: [Main](../glossary.md#main-process)
|
Process: [Main](../glossary.md#main-process), [Utility](../glossary.md#utility-process)
|
||||||
|
|
||||||
The `net` module is a client-side API for issuing HTTP(S) requests. It is
|
The `net` module is a client-side API for issuing HTTP(S) requests. It is
|
||||||
similar to the [HTTP](https://nodejs.org/api/http.html) and
|
similar to the [HTTP](https://nodejs.org/api/http.html) and
|
||||||
|
@ -119,6 +119,9 @@ protocol.handle('https', (req) => {
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: in the [utility process](../glossary.md#utility-process) custom protocols
|
||||||
|
are not supported.
|
||||||
|
|
||||||
### `net.isOnline()`
|
### `net.isOnline()`
|
||||||
|
|
||||||
Returns `boolean` - Whether there is currently internet connection.
|
Returns `boolean` - Whether there is currently internet connection.
|
||||||
|
|
|
@ -219,7 +219,6 @@ auto_filenames = {
|
||||||
"lib/browser/api/message-channel.ts",
|
"lib/browser/api/message-channel.ts",
|
||||||
"lib/browser/api/module-list.ts",
|
"lib/browser/api/module-list.ts",
|
||||||
"lib/browser/api/native-theme.ts",
|
"lib/browser/api/native-theme.ts",
|
||||||
"lib/browser/api/net-client-request.ts",
|
|
||||||
"lib/browser/api/net-fetch.ts",
|
"lib/browser/api/net-fetch.ts",
|
||||||
"lib/browser/api/net-log.ts",
|
"lib/browser/api/net-log.ts",
|
||||||
"lib/browser/api/net.ts",
|
"lib/browser/api/net.ts",
|
||||||
|
@ -255,6 +254,7 @@ auto_filenames = {
|
||||||
"lib/browser/web-view-events.ts",
|
"lib/browser/web-view-events.ts",
|
||||||
"lib/common/api/module-list.ts",
|
"lib/common/api/module-list.ts",
|
||||||
"lib/common/api/native-image.ts",
|
"lib/common/api/native-image.ts",
|
||||||
|
"lib/common/api/net-client-request.ts",
|
||||||
"lib/common/api/shell.ts",
|
"lib/common/api/shell.ts",
|
||||||
"lib/common/define-properties.ts",
|
"lib/common/define-properties.ts",
|
||||||
"lib/common/deprecate.ts",
|
"lib/common/deprecate.ts",
|
||||||
|
@ -348,12 +348,16 @@ auto_filenames = {
|
||||||
]
|
]
|
||||||
|
|
||||||
utility_bundle_deps = [
|
utility_bundle_deps = [
|
||||||
|
"lib/browser/api/net-fetch.ts",
|
||||||
"lib/browser/message-port-main.ts",
|
"lib/browser/message-port-main.ts",
|
||||||
|
"lib/common/api/net-client-request.ts",
|
||||||
"lib/common/define-properties.ts",
|
"lib/common/define-properties.ts",
|
||||||
"lib/common/init.ts",
|
"lib/common/init.ts",
|
||||||
"lib/common/reset-search-paths.ts",
|
"lib/common/reset-search-paths.ts",
|
||||||
|
"lib/common/webpack-globals-provider.ts",
|
||||||
"lib/utility/api/exports/electron.ts",
|
"lib/utility/api/exports/electron.ts",
|
||||||
"lib/utility/api/module-list.ts",
|
"lib/utility/api/module-list.ts",
|
||||||
|
"lib/utility/api/net.ts",
|
||||||
"lib/utility/init.ts",
|
"lib/utility/init.ts",
|
||||||
"lib/utility/parent-port.ts",
|
"lib/utility/parent-port.ts",
|
||||||
"package.json",
|
"package.json",
|
||||||
|
|
|
@ -279,7 +279,6 @@ filenames = {
|
||||||
"shell/browser/api/electron_api_menu.h",
|
"shell/browser/api/electron_api_menu.h",
|
||||||
"shell/browser/api/electron_api_native_theme.cc",
|
"shell/browser/api/electron_api_native_theme.cc",
|
||||||
"shell/browser/api/electron_api_native_theme.h",
|
"shell/browser/api/electron_api_native_theme.h",
|
||||||
"shell/browser/api/electron_api_net.cc",
|
|
||||||
"shell/browser/api/electron_api_net_log.cc",
|
"shell/browser/api/electron_api_net_log.cc",
|
||||||
"shell/browser/api/electron_api_net_log.h",
|
"shell/browser/api/electron_api_net_log.h",
|
||||||
"shell/browser/api/electron_api_notification.cc",
|
"shell/browser/api/electron_api_notification.cc",
|
||||||
|
@ -305,8 +304,6 @@ filenames = {
|
||||||
"shell/browser/api/electron_api_system_preferences.h",
|
"shell/browser/api/electron_api_system_preferences.h",
|
||||||
"shell/browser/api/electron_api_tray.cc",
|
"shell/browser/api/electron_api_tray.cc",
|
||||||
"shell/browser/api/electron_api_tray.h",
|
"shell/browser/api/electron_api_tray.h",
|
||||||
"shell/browser/api/electron_api_url_loader.cc",
|
|
||||||
"shell/browser/api/electron_api_url_loader.h",
|
|
||||||
"shell/browser/api/electron_api_utility_process.cc",
|
"shell/browser/api/electron_api_utility_process.cc",
|
||||||
"shell/browser/api/electron_api_utility_process.h",
|
"shell/browser/api/electron_api_utility_process.h",
|
||||||
"shell/browser/api/electron_api_view.cc",
|
"shell/browser/api/electron_api_view.cc",
|
||||||
|
@ -544,8 +541,11 @@ filenames = {
|
||||||
"shell/common/api/electron_api_key_weak_map.h",
|
"shell/common/api/electron_api_key_weak_map.h",
|
||||||
"shell/common/api/electron_api_native_image.cc",
|
"shell/common/api/electron_api_native_image.cc",
|
||||||
"shell/common/api/electron_api_native_image.h",
|
"shell/common/api/electron_api_native_image.h",
|
||||||
|
"shell/common/api/electron_api_net.cc",
|
||||||
"shell/common/api/electron_api_shell.cc",
|
"shell/common/api/electron_api_shell.cc",
|
||||||
"shell/common/api/electron_api_testing.cc",
|
"shell/common/api/electron_api_testing.cc",
|
||||||
|
"shell/common/api/electron_api_url_loader.cc",
|
||||||
|
"shell/common/api/electron_api_url_loader.h",
|
||||||
"shell/common/api/electron_api_v8_util.cc",
|
"shell/common/api/electron_api_v8_util.cc",
|
||||||
"shell/common/api/electron_bindings.cc",
|
"shell/common/api/electron_bindings.cc",
|
||||||
"shell/common/api/electron_bindings.h",
|
"shell/common/api/electron_bindings.h",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { net, IncomingMessage, Session as SessionT } from 'electron/main';
|
import { ClientRequestConstructorOptions, ClientRequest, IncomingMessage, Session as SessionT } from 'electron/main';
|
||||||
import { Readable, Writable, isReadable } from 'stream';
|
import { Readable, Writable, isReadable } from 'stream';
|
||||||
import { allowAnyProtocol } from '@electron/internal/browser/api/net-client-request';
|
import { allowAnyProtocol } from '@electron/internal/common/api/net-client-request';
|
||||||
|
|
||||||
function createDeferredPromise<T, E extends Error = Error> (): { promise: Promise<T>; resolve: (x: T) => void; reject: (e: E) => void; } {
|
function createDeferredPromise<T, E extends Error = Error> (): { promise: Promise<T>; resolve: (x: T) => void; reject: (e: E) => void; } {
|
||||||
let res: (x: T) => void;
|
let res: (x: T) => void;
|
||||||
|
@ -13,7 +13,8 @@ function createDeferredPromise<T, E extends Error = Error> (): { promise: Promis
|
||||||
return { promise, resolve: res!, reject: rej! };
|
return { promise, resolve: res!, reject: rej! };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchWithSession (input: RequestInfo, init: (RequestInit & {bypassCustomProtocolHandlers?: boolean}) | undefined, session: SessionT): Promise<Response> {
|
export function fetchWithSession (input: RequestInfo, init: (RequestInit & {bypassCustomProtocolHandlers?: boolean}) | undefined, session: SessionT | undefined,
|
||||||
|
request: (options: ClientRequestConstructorOptions | string) => ClientRequest) {
|
||||||
const p = createDeferredPromise<Response>();
|
const p = createDeferredPromise<Response>();
|
||||||
let req: Request;
|
let req: Request;
|
||||||
try {
|
try {
|
||||||
|
@ -73,7 +74,7 @@ export function fetchWithSession (input: RequestInfo, init: (RequestInit & {bypa
|
||||||
// We can't set credentials to same-origin unless there's an origin set.
|
// We can't set credentials to same-origin unless there's an origin set.
|
||||||
const credentials = req.credentials === 'same-origin' && !origin ? 'include' : req.credentials;
|
const credentials = req.credentials === 'same-origin' && !origin ? 'include' : req.credentials;
|
||||||
|
|
||||||
const r = net.request(allowAnyProtocol({
|
const r = request(allowAnyProtocol({
|
||||||
session,
|
session,
|
||||||
method: req.method,
|
method: req.method,
|
||||||
url: req.url,
|
url: req.url,
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import { IncomingMessage, session } from 'electron/main';
|
import { app, IncomingMessage, session } from 'electron/main';
|
||||||
import type { ClientRequestConstructorOptions } from 'electron/main';
|
import type { ClientRequestConstructorOptions } from 'electron/main';
|
||||||
import { ClientRequest } from '@electron/internal/browser/api/net-client-request';
|
import { ClientRequest } from '@electron/internal/common/api/net-client-request';
|
||||||
|
|
||||||
const { isOnline } = process._linkedBinding('electron_browser_net');
|
const { isOnline } = process._linkedBinding('electron_common_net');
|
||||||
|
|
||||||
export function request (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
|
export function request (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
|
||||||
|
if (!app.isReady()) {
|
||||||
|
throw new Error('net module can only be used after app is ready');
|
||||||
|
}
|
||||||
return new ClientRequest(options, callback);
|
return new ClientRequest(options, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { fetchWithSession } from '@electron/internal/browser/api/net-fetch';
|
import { fetchWithSession } from '@electron/internal/browser/api/net-fetch';
|
||||||
|
import { net } from 'electron/main';
|
||||||
const { fromPartition, fromPath, Session } = process._linkedBinding('electron_browser_session');
|
const { fromPartition, fromPath, Session } = process._linkedBinding('electron_browser_session');
|
||||||
|
|
||||||
Session.prototype.fetch = function (input: RequestInfo, init?: RequestInit) {
|
Session.prototype.fetch = function (input: RequestInfo, init?: RequestInit) {
|
||||||
return fetchWithSession(input, init, this);
|
return fetchWithSession(input, init, this, net.request);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -14,7 +14,7 @@ interface GuestInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
const webViewManager = process._linkedBinding('electron_browser_web_view_manager');
|
const webViewManager = process._linkedBinding('electron_browser_web_view_manager');
|
||||||
const netBinding = process._linkedBinding('electron_browser_net');
|
const netBinding = process._linkedBinding('electron_common_net');
|
||||||
|
|
||||||
const supportedWebViewEvents = Object.keys(webViewEvents);
|
const supportedWebViewEvents = Object.keys(webViewEvents);
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
import { Readable, Writable } from 'stream';
|
import { Readable, Writable } from 'stream';
|
||||||
import { app } from 'electron/main';
|
import type {
|
||||||
import type { ClientRequestConstructorOptions, UploadProgress } from 'electron/main';
|
ClientRequestConstructorOptions,
|
||||||
|
UploadProgress
|
||||||
|
} from 'electron/common';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isValidHeaderName,
|
isValidHeaderName,
|
||||||
isValidHeaderValue,
|
isValidHeaderValue,
|
||||||
createURLLoader
|
createURLLoader
|
||||||
} = process._linkedBinding('electron_browser_net');
|
} = process._linkedBinding('electron_common_net');
|
||||||
const { Session } = process._linkedBinding('electron_browser_session');
|
|
||||||
|
|
||||||
const kHttpProtocols = new Set(['http:', 'https:']);
|
const kHttpProtocols = new Set(['http:', 'https:']);
|
||||||
|
|
||||||
|
@ -283,14 +284,17 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
|
||||||
const key = name.toLowerCase();
|
const key = name.toLowerCase();
|
||||||
urlLoaderOptions.headers[key] = { name, value };
|
urlLoaderOptions.headers[key] = { name, value };
|
||||||
}
|
}
|
||||||
if (options.session) {
|
if (process.type !== 'utility') {
|
||||||
if (!(options.session instanceof Session)) { throw new TypeError('`session` should be an instance of the Session class'); }
|
const { Session } = process._linkedBinding('electron_browser_session');
|
||||||
urlLoaderOptions.session = options.session;
|
if (options.session) {
|
||||||
} else if (options.partition) {
|
if (!(options.session instanceof Session)) { throw new TypeError('`session` should be an instance of the Session class'); }
|
||||||
if (typeof options.partition === 'string') {
|
urlLoaderOptions.session = options.session;
|
||||||
urlLoaderOptions.partition = options.partition;
|
} else if (options.partition) {
|
||||||
} else {
|
if (typeof options.partition === 'string') {
|
||||||
throw new TypeError('`partition` should be a string');
|
urlLoaderOptions.partition = options.partition;
|
||||||
|
} else {
|
||||||
|
throw new TypeError('`partition` should be a string');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return urlLoaderOptions;
|
return urlLoaderOptions;
|
||||||
|
@ -312,10 +316,6 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
|
||||||
constructor (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
|
constructor (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
|
||||||
super({ autoDestroy: true });
|
super({ autoDestroy: true });
|
||||||
|
|
||||||
if (!app.isReady()) {
|
|
||||||
throw new Error('net module can only be used after app is ready');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
this.once('response', callback);
|
this.once('response', callback);
|
||||||
}
|
}
|
|
@ -1,2 +1,4 @@
|
||||||
// Utility side modules, please sort alphabetically.
|
// Utility side modules, please sort alphabetically.
|
||||||
export const utilityNodeModuleList: ElectronInternal.ModuleEntry[] = [];
|
export const utilityNodeModuleList: ElectronInternal.ModuleEntry[] = [
|
||||||
|
{ name: 'net', loader: () => require('./net') }
|
||||||
|
];
|
||||||
|
|
22
lib/utility/api/net.ts
Normal file
22
lib/utility/api/net.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { IncomingMessage } from 'electron/utility';
|
||||||
|
import type { ClientRequestConstructorOptions } from 'electron/utility';
|
||||||
|
import { ClientRequest } from '@electron/internal/common/api/net-client-request';
|
||||||
|
import { fetchWithSession } from '@electron/internal/browser/api/net-fetch';
|
||||||
|
|
||||||
|
const { isOnline, resolveHost } = process._linkedBinding('electron_common_net');
|
||||||
|
|
||||||
|
export function request (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
|
||||||
|
return new ClientRequest(options, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetch (input: RequestInfo, init?: RequestInit): Promise<Response> {
|
||||||
|
return fetchWithSession(input, init, undefined, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.resolveHost = resolveHost;
|
||||||
|
|
||||||
|
exports.isOnline = isOnline;
|
||||||
|
|
||||||
|
Object.defineProperty(exports, 'online', {
|
||||||
|
get: () => isOnline()
|
||||||
|
});
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
import { pathToFileURL } from 'url';
|
import { pathToFileURL } from 'url';
|
||||||
|
|
||||||
import { ParentPort } from '@electron/internal/utility/parent-port';
|
import { ParentPort } from '@electron/internal/utility/parent-port';
|
||||||
|
@ -15,6 +16,8 @@ require('../common/reset-search-paths');
|
||||||
// Import common settings.
|
// Import common settings.
|
||||||
require('@electron/internal/common/init');
|
require('@electron/internal/common/init');
|
||||||
|
|
||||||
|
process._linkedBinding('electron_browser_event_emitter').setEventEmitterPrototype(EventEmitter.prototype);
|
||||||
|
|
||||||
const parentPort: ParentPort = new ParentPort();
|
const parentPort: ParentPort = new ParentPort();
|
||||||
Object.defineProperty(process, 'parentPort', {
|
Object.defineProperty(process, 'parentPort', {
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
#include "base/process/kill.h"
|
#include "base/process/kill.h"
|
||||||
#include "base/process/launch.h"
|
#include "base/process/launch.h"
|
||||||
#include "base/process/process.h"
|
#include "base/process/process.h"
|
||||||
|
#include "chrome/browser/browser_process.h"
|
||||||
#include "content/public/browser/child_process_host.h"
|
#include "content/public/browser/child_process_host.h"
|
||||||
#include "content/public/browser/service_process_host.h"
|
#include "content/public/browser/service_process_host.h"
|
||||||
#include "content/public/common/result_codes.h"
|
#include "content/public/common/result_codes.h"
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
#include "mojo/public/cpp/bindings/pending_receiver.h"
|
#include "mojo/public/cpp/bindings/pending_receiver.h"
|
||||||
#include "shell/browser/api/message_port.h"
|
#include "shell/browser/api/message_port.h"
|
||||||
#include "shell/browser/javascript_environment.h"
|
#include "shell/browser/javascript_environment.h"
|
||||||
|
#include "shell/browser/net/system_network_context_manager.h"
|
||||||
#include "shell/common/gin_converters/callback_converter.h"
|
#include "shell/common/gin_converters/callback_converter.h"
|
||||||
#include "shell/common/gin_converters/file_path_converter.h"
|
#include "shell/common/gin_converters/file_path_converter.h"
|
||||||
#include "shell/common/gin_helper/dictionary.h"
|
#include "shell/common/gin_helper/dictionary.h"
|
||||||
|
@ -192,6 +194,22 @@ UtilityProcessWrapper::UtilityProcessWrapper(
|
||||||
connector_->set_connection_error_handler(base::BindOnce(
|
connector_->set_connection_error_handler(base::BindOnce(
|
||||||
&UtilityProcessWrapper::CloseConnectorPort, weak_factory_.GetWeakPtr()));
|
&UtilityProcessWrapper::CloseConnectorPort, weak_factory_.GetWeakPtr()));
|
||||||
|
|
||||||
|
mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory;
|
||||||
|
network::mojom::URLLoaderFactoryParamsPtr loader_params =
|
||||||
|
network::mojom::URLLoaderFactoryParams::New();
|
||||||
|
loader_params->process_id = pid_;
|
||||||
|
loader_params->is_corb_enabled = false;
|
||||||
|
loader_params->is_trusted = true;
|
||||||
|
network::mojom::NetworkContext* network_context =
|
||||||
|
g_browser_process->system_network_context_manager()->GetContext();
|
||||||
|
network_context->CreateURLLoaderFactory(
|
||||||
|
url_loader_factory.InitWithNewPipeAndPassReceiver(),
|
||||||
|
std::move(loader_params));
|
||||||
|
params->url_loader_factory = std::move(url_loader_factory);
|
||||||
|
mojo::PendingRemote<network::mojom::HostResolver> host_resolver;
|
||||||
|
network_context->CreateHostResolver(
|
||||||
|
{}, host_resolver.InitWithNewPipeAndPassReceiver());
|
||||||
|
params->host_resolver = std::move(host_resolver);
|
||||||
node_service_remote_->Initialize(std::move(params));
|
node_service_remote_->Initialize(std::move(params));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,10 @@
|
||||||
#include "net/base/net_errors.h"
|
#include "net/base/net_errors.h"
|
||||||
#include "net/base/network_isolation_key.h"
|
#include "net/base/network_isolation_key.h"
|
||||||
#include "net/dns/public/resolve_error_info.h"
|
#include "net/dns/public/resolve_error_info.h"
|
||||||
|
#include "services/network/public/mojom/network_context.mojom.h"
|
||||||
#include "shell/browser/electron_browser_context.h"
|
#include "shell/browser/electron_browser_context.h"
|
||||||
|
#include "shell/common/process_util.h"
|
||||||
|
#include "shell/services/node/node_service.h"
|
||||||
#include "url/origin.h"
|
#include "url/origin.h"
|
||||||
|
|
||||||
using content::BrowserThread;
|
using content::BrowserThread;
|
||||||
|
@ -30,15 +33,17 @@ ResolveHostFunction::ResolveHostFunction(
|
||||||
: browser_context_(browser_context),
|
: browser_context_(browser_context),
|
||||||
host_(std::move(host)),
|
host_(std::move(host)),
|
||||||
params_(std::move(params)),
|
params_(std::move(params)),
|
||||||
callback_(std::move(callback)) {}
|
callback_(std::move(callback)) {
|
||||||
|
DETACH_FROM_SEQUENCE(sequence_checker_);
|
||||||
|
}
|
||||||
|
|
||||||
ResolveHostFunction::~ResolveHostFunction() {
|
ResolveHostFunction::~ResolveHostFunction() {
|
||||||
DCHECK_CURRENTLY_ON(BrowserThread::UI);
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||||
DCHECK(!receiver_.is_bound());
|
DCHECK(!receiver_.is_bound());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResolveHostFunction::Run() {
|
void ResolveHostFunction::Run() {
|
||||||
DCHECK_CURRENTLY_ON(BrowserThread::UI);
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||||
DCHECK(!receiver_.is_bound());
|
DCHECK(!receiver_.is_bound());
|
||||||
|
|
||||||
// Start the request.
|
// Start the request.
|
||||||
|
@ -50,12 +55,21 @@ void ResolveHostFunction::Run() {
|
||||||
net::ResolveErrorInfo(net::ERR_FAILED),
|
net::ResolveErrorInfo(net::ERR_FAILED),
|
||||||
/*resolved_addresses=*/absl::nullopt,
|
/*resolved_addresses=*/absl::nullopt,
|
||||||
/*endpoint_results_with_metadata=*/absl::nullopt));
|
/*endpoint_results_with_metadata=*/absl::nullopt));
|
||||||
browser_context_->GetDefaultStoragePartition()
|
if (electron::IsUtilityProcess()) {
|
||||||
->GetNetworkContext()
|
URLLoaderBundle::GetInstance()->GetHostResolver()->ResolveHost(
|
||||||
->ResolveHost(network::mojom::HostResolverHost::NewHostPortPair(
|
network::mojom::HostResolverHost::NewHostPortPair(
|
||||||
std::move(host_port_pair)),
|
std::move(host_port_pair)),
|
||||||
net::NetworkAnonymizationKey(), std::move(params_),
|
net::NetworkAnonymizationKey(), std::move(params_),
|
||||||
std::move(resolve_host_client));
|
std::move(resolve_host_client));
|
||||||
|
} else {
|
||||||
|
DCHECK_CURRENTLY_ON(BrowserThread::UI);
|
||||||
|
browser_context_->GetDefaultStoragePartition()
|
||||||
|
->GetNetworkContext()
|
||||||
|
->ResolveHost(network::mojom::HostResolverHost::NewHostPortPair(
|
||||||
|
std::move(host_port_pair)),
|
||||||
|
net::NetworkAnonymizationKey(), std::move(params_),
|
||||||
|
std::move(resolve_host_client));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResolveHostFunction::OnComplete(
|
void ResolveHostFunction::OnComplete(
|
||||||
|
@ -64,7 +78,7 @@ void ResolveHostFunction::OnComplete(
|
||||||
const absl::optional<net::AddressList>& resolved_addresses,
|
const absl::optional<net::AddressList>& resolved_addresses,
|
||||||
const absl::optional<net::HostResolverEndpointResults>&
|
const absl::optional<net::HostResolverEndpointResults>&
|
||||||
endpoint_results_with_metadata) {
|
endpoint_results_with_metadata) {
|
||||||
DCHECK_CURRENTLY_ON(BrowserThread::UI);
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||||
|
|
||||||
// Ensure that we outlive the `receiver_.reset()` call.
|
// Ensure that we outlive the `receiver_.reset()` call.
|
||||||
scoped_refptr<ResolveHostFunction> self(this);
|
scoped_refptr<ResolveHostFunction> self(this);
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
#include "base/memory/raw_ptr.h"
|
#include "base/memory/raw_ptr.h"
|
||||||
#include "base/memory/ref_counted.h"
|
#include "base/memory/ref_counted.h"
|
||||||
|
#include "base/sequence_checker.h"
|
||||||
#include "mojo/public/cpp/bindings/receiver.h"
|
#include "mojo/public/cpp/bindings/receiver.h"
|
||||||
#include "net/base/address_list.h"
|
#include "net/base/address_list.h"
|
||||||
#include "net/dns/public/host_resolver_results.h"
|
#include "net/dns/public/host_resolver_results.h"
|
||||||
|
@ -53,6 +54,8 @@ class ResolveHostFunction
|
||||||
const absl::optional<net::HostResolverEndpointResults>&
|
const absl::optional<net::HostResolverEndpointResults>&
|
||||||
endpoint_results_with_metadata) override;
|
endpoint_results_with_metadata) override;
|
||||||
|
|
||||||
|
SEQUENCE_CHECKER(sequence_checker_);
|
||||||
|
|
||||||
// Receiver for the currently in-progress request, if any.
|
// Receiver for the currently in-progress request, if any.
|
||||||
mojo::Receiver<network::mojom::ResolveHostClient> receiver_{this};
|
mojo::Receiver<network::mojom::ResolveHostClient> receiver_{this};
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,16 @@
|
||||||
#include "net/base/network_change_notifier.h"
|
#include "net/base/network_change_notifier.h"
|
||||||
#include "net/http/http_util.h"
|
#include "net/http/http_util.h"
|
||||||
#include "services/network/public/cpp/features.h"
|
#include "services/network/public/cpp/features.h"
|
||||||
#include "shell/browser/api/electron_api_url_loader.h"
|
#include "services/network/public/mojom/host_resolver.mojom.h"
|
||||||
|
#include "shell/browser/net/resolve_host_function.h"
|
||||||
|
#include "shell/common/api/electron_api_url_loader.h"
|
||||||
#include "shell/common/gin_converters/file_path_converter.h"
|
#include "shell/common/gin_converters/file_path_converter.h"
|
||||||
#include "shell/common/gin_converters/gurl_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"
|
#include "shell/common/gin_helper/dictionary.h"
|
||||||
#include "shell/common/gin_helper/error_thrower.h"
|
#include "shell/common/gin_helper/error_thrower.h"
|
||||||
#include "shell/common/gin_helper/object_template_builder.h"
|
#include "shell/common/gin_helper/object_template_builder.h"
|
||||||
|
#include "shell/common/gin_helper/promise.h"
|
||||||
#include "shell/common/node_includes.h"
|
#include "shell/common/node_includes.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
@ -40,6 +43,37 @@ base::FilePath FileURLToFilePath(v8::Isolate* isolate, const GURL& url) {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v8::Local<v8::Promise> ResolveHost(
|
||||||
|
v8::Isolate* isolate,
|
||||||
|
std::string host,
|
||||||
|
absl::optional<network::mojom::ResolveHostParametersPtr> params) {
|
||||||
|
gin_helper::Promise<gin_helper::Dictionary> promise(isolate);
|
||||||
|
v8::Local<v8::Promise> handle = promise.GetHandle();
|
||||||
|
|
||||||
|
auto fn = base::MakeRefCounted<electron::ResolveHostFunction>(
|
||||||
|
nullptr, std::move(host), params ? std::move(params.value()) : nullptr,
|
||||||
|
base::BindOnce(
|
||||||
|
[](gin_helper::Promise<gin_helper::Dictionary> promise,
|
||||||
|
int64_t net_error, const absl::optional<net::AddressList>& addrs) {
|
||||||
|
if (net_error < 0) {
|
||||||
|
promise.RejectWithErrorMessage(net::ErrorToString(net_error));
|
||||||
|
} else {
|
||||||
|
DCHECK(addrs.has_value() && !addrs->empty());
|
||||||
|
|
||||||
|
v8::HandleScope handle_scope(promise.isolate());
|
||||||
|
auto dict =
|
||||||
|
gin_helper::Dictionary::CreateEmpty(promise.isolate());
|
||||||
|
dict.Set("endpoints", addrs->endpoints());
|
||||||
|
promise.Resolve(dict);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
std::move(promise)));
|
||||||
|
|
||||||
|
fn->Run();
|
||||||
|
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
using electron::api::SimpleURLLoaderWrapper;
|
using electron::api::SimpleURLLoaderWrapper;
|
||||||
|
|
||||||
void Initialize(v8::Local<v8::Object> exports,
|
void Initialize(v8::Local<v8::Object> exports,
|
||||||
|
@ -54,8 +88,9 @@ void Initialize(v8::Local<v8::Object> exports,
|
||||||
dict.SetMethod("isValidHeaderValue", &IsValidHeaderValue);
|
dict.SetMethod("isValidHeaderValue", &IsValidHeaderValue);
|
||||||
dict.SetMethod("createURLLoader", &SimpleURLLoaderWrapper::Create);
|
dict.SetMethod("createURLLoader", &SimpleURLLoaderWrapper::Create);
|
||||||
dict.SetMethod("fileURLToFilePath", &FileURLToFilePath);
|
dict.SetMethod("fileURLToFilePath", &FileURLToFilePath);
|
||||||
|
dict.SetMethod("resolveHost", &ResolveHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_net, Initialize)
|
NODE_LINKED_BINDING_CONTEXT_AWARE(electron_common_net, Initialize)
|
|
@ -2,7 +2,7 @@
|
||||||
// Use of this source code is governed by the MIT license that can be
|
// Use of this source code is governed by the MIT license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
#include "shell/browser/api/electron_api_url_loader.h"
|
#include "shell/common/api/electron_api_url_loader.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
@ -13,6 +13,7 @@
|
||||||
#include "base/containers/fixed_flat_map.h"
|
#include "base/containers/fixed_flat_map.h"
|
||||||
#include "base/memory/raw_ptr.h"
|
#include "base/memory/raw_ptr.h"
|
||||||
#include "base/no_destructor.h"
|
#include "base/no_destructor.h"
|
||||||
|
#include "base/sequence_checker.h"
|
||||||
#include "gin/handle.h"
|
#include "gin/handle.h"
|
||||||
#include "gin/object_template_builder.h"
|
#include "gin/object_template_builder.h"
|
||||||
#include "gin/wrappable.h"
|
#include "gin/wrappable.h"
|
||||||
|
@ -39,7 +40,10 @@
|
||||||
#include "shell/common/gin_converters/net_converter.h"
|
#include "shell/common/gin_converters/net_converter.h"
|
||||||
#include "shell/common/gin_helper/dictionary.h"
|
#include "shell/common/gin_helper/dictionary.h"
|
||||||
#include "shell/common/gin_helper/object_template_builder.h"
|
#include "shell/common/gin_helper/object_template_builder.h"
|
||||||
|
#include "shell/common/gin_helper/promise.h"
|
||||||
#include "shell/common/node_includes.h"
|
#include "shell/common/node_includes.h"
|
||||||
|
#include "shell/common/process_util.h"
|
||||||
|
#include "shell/services/node/node_service.h"
|
||||||
#include "third_party/blink/public/common/loader/referrer_utils.h"
|
#include "third_party/blink/public/common/loader/referrer_utils.h"
|
||||||
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom.h"
|
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom.h"
|
||||||
|
|
||||||
|
@ -186,6 +190,7 @@ class JSChunkedDataPipeGetter : public gin::Wrappable<JSChunkedDataPipeGetter>,
|
||||||
mojo::PendingReceiver<network::mojom::ChunkedDataPipeGetter>
|
mojo::PendingReceiver<network::mojom::ChunkedDataPipeGetter>
|
||||||
chunked_data_pipe_getter)
|
chunked_data_pipe_getter)
|
||||||
: isolate_(isolate), body_func_(isolate, body_func) {
|
: isolate_(isolate), body_func_(isolate, body_func) {
|
||||||
|
DETACH_FROM_SEQUENCE(sequence_checker_);
|
||||||
receiver_.Bind(std::move(chunked_data_pipe_getter));
|
receiver_.Bind(std::move(chunked_data_pipe_getter));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +200,7 @@ class JSChunkedDataPipeGetter : public gin::Wrappable<JSChunkedDataPipeGetter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
void StartReading(mojo::ScopedDataPipeProducerHandle pipe) override {
|
void StartReading(mojo::ScopedDataPipeProducerHandle pipe) override {
|
||||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||||
|
|
||||||
if (body_func_.IsEmpty()) {
|
if (body_func_.IsEmpty()) {
|
||||||
LOG(ERROR) << "Tried to read twice from a JSChunkedDataPipeGetter";
|
LOG(ERROR) << "Tried to read twice from a JSChunkedDataPipeGetter";
|
||||||
|
@ -251,7 +256,7 @@ class JSChunkedDataPipeGetter : public gin::Wrappable<JSChunkedDataPipeGetter>,
|
||||||
|
|
||||||
void OnWriteChunkComplete(gin_helper::Promise<void> promise,
|
void OnWriteChunkComplete(gin_helper::Promise<void> promise,
|
||||||
MojoResult result) {
|
MojoResult result) {
|
||||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||||
is_writing_ = false;
|
is_writing_ = false;
|
||||||
if (result == MOJO_RESULT_OK) {
|
if (result == MOJO_RESULT_OK) {
|
||||||
promise.Resolve();
|
promise.Resolve();
|
||||||
|
@ -278,6 +283,7 @@ class JSChunkedDataPipeGetter : public gin::Wrappable<JSChunkedDataPipeGetter>,
|
||||||
size_callback_.Reset();
|
size_callback_.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SEQUENCE_CHECKER(sequence_checker_);
|
||||||
GetSizeCallback size_callback_;
|
GetSizeCallback size_callback_;
|
||||||
mojo::Receiver<network::mojom::ChunkedDataPipeGetter> receiver_{this};
|
mojo::Receiver<network::mojom::ChunkedDataPipeGetter> receiver_{this};
|
||||||
std::unique_ptr<mojo::DataPipeProducer> data_producer_;
|
std::unique_ptr<mojo::DataPipeProducer> data_producer_;
|
||||||
|
@ -320,6 +326,7 @@ SimpleURLLoaderWrapper::SimpleURLLoaderWrapper(
|
||||||
: browser_context_(browser_context),
|
: browser_context_(browser_context),
|
||||||
request_options_(options),
|
request_options_(options),
|
||||||
request_(std::move(request)) {
|
request_(std::move(request)) {
|
||||||
|
DETACH_FROM_SEQUENCE(sequence_checker_);
|
||||||
if (!request_->trusted_params)
|
if (!request_->trusted_params)
|
||||||
request_->trusted_params = network::ResourceRequest::TrustedParams();
|
request_->trusted_params = network::ResourceRequest::TrustedParams();
|
||||||
mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver>
|
mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver>
|
||||||
|
@ -393,7 +400,7 @@ void SimpleURLLoaderWrapper::OnAuthRequired(
|
||||||
const scoped_refptr<net::HttpResponseHeaders>& head_headers,
|
const scoped_refptr<net::HttpResponseHeaders>& head_headers,
|
||||||
mojo::PendingRemote<network::mojom::AuthChallengeResponder>
|
mojo::PendingRemote<network::mojom::AuthChallengeResponder>
|
||||||
auth_challenge_responder) {
|
auth_challenge_responder) {
|
||||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||||
mojo::Remote<network::mojom::AuthChallengeResponder> auth_responder(
|
mojo::Remote<network::mojom::AuthChallengeResponder> auth_responder(
|
||||||
std::move(auth_challenge_responder));
|
std::move(auth_challenge_responder));
|
||||||
// WeakPtr because if we're Cancel()ed while waiting for auth, and the
|
// WeakPtr because if we're Cancel()ed while waiting for auth, and the
|
||||||
|
@ -462,6 +469,10 @@ void SimpleURLLoaderWrapper::Cancel() {
|
||||||
}
|
}
|
||||||
scoped_refptr<network::SharedURLLoaderFactory>
|
scoped_refptr<network::SharedURLLoaderFactory>
|
||||||
SimpleURLLoaderWrapper::GetURLLoaderFactoryForURL(const GURL& url) {
|
SimpleURLLoaderWrapper::GetURLLoaderFactoryForURL(const GURL& url) {
|
||||||
|
if (electron::IsUtilityProcess()) {
|
||||||
|
return URLLoaderBundle::GetInstance()->GetSharedURLLoaderFactory();
|
||||||
|
}
|
||||||
|
CHECK(browser_context_);
|
||||||
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory;
|
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory;
|
||||||
auto* protocol_registry =
|
auto* protocol_registry =
|
||||||
ProtocolRegistry::FromBrowserContext(browser_context_);
|
ProtocolRegistry::FromBrowserContext(browser_context_);
|
||||||
|
@ -660,18 +671,22 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string partition;
|
ElectronBrowserContext* browser_context = nullptr;
|
||||||
gin::Handle<Session> session;
|
if (electron::IsBrowserProcess()) {
|
||||||
if (!opts.Get("session", &session)) {
|
std::string partition;
|
||||||
if (opts.Get("partition", &partition))
|
gin::Handle<Session> session;
|
||||||
session = Session::FromPartition(args->isolate(), partition);
|
if (!opts.Get("session", &session)) {
|
||||||
else // default session
|
if (opts.Get("partition", &partition))
|
||||||
session = Session::FromPartition(args->isolate(), "");
|
session = Session::FromPartition(args->isolate(), partition);
|
||||||
|
else // default session
|
||||||
|
session = Session::FromPartition(args->isolate(), "");
|
||||||
|
}
|
||||||
|
browser_context = session->browser_context();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ret = gin::CreateHandle(
|
auto ret = gin::CreateHandle(
|
||||||
args->isolate(), new SimpleURLLoaderWrapper(session->browser_context(),
|
args->isolate(),
|
||||||
std::move(request), options));
|
new SimpleURLLoaderWrapper(browser_context, std::move(request), options));
|
||||||
ret->Pin();
|
ret->Pin();
|
||||||
if (!chunk_pipe_getter.IsEmpty()) {
|
if (!chunk_pipe_getter.IsEmpty()) {
|
||||||
ret->PinBodyGetter(chunk_pipe_getter);
|
ret->PinBodyGetter(chunk_pipe_getter);
|
||||||
|
@ -681,7 +696,7 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
||||||
|
|
||||||
void SimpleURLLoaderWrapper::OnDataReceived(base::StringPiece string_piece,
|
void SimpleURLLoaderWrapper::OnDataReceived(base::StringPiece string_piece,
|
||||||
base::OnceClosure resume) {
|
base::OnceClosure resume) {
|
||||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||||
v8::HandleScope handle_scope(isolate);
|
v8::HandleScope handle_scope(isolate);
|
||||||
auto array_buffer = v8::ArrayBuffer::New(isolate, string_piece.size());
|
auto array_buffer = v8::ArrayBuffer::New(isolate, string_piece.size());
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
#include "base/memory/raw_ptr.h"
|
#include "base/memory/raw_ptr.h"
|
||||||
#include "base/memory/weak_ptr.h"
|
#include "base/memory/weak_ptr.h"
|
||||||
|
#include "base/sequence_checker.h"
|
||||||
#include "gin/wrappable.h"
|
#include "gin/wrappable.h"
|
||||||
#include "mojo/public/cpp/bindings/receiver_set.h"
|
#include "mojo/public/cpp/bindings/receiver_set.h"
|
||||||
#include "net/base/auth.h"
|
#include "net/base/auth.h"
|
||||||
|
@ -133,6 +134,7 @@ class SimpleURLLoaderWrapper
|
||||||
void Pin();
|
void Pin();
|
||||||
void PinBodyGetter(v8::Local<v8::Value>);
|
void PinBodyGetter(v8::Local<v8::Value>);
|
||||||
|
|
||||||
|
SEQUENCE_CHECKER(sequence_checker_);
|
||||||
raw_ptr<ElectronBrowserContext> browser_context_;
|
raw_ptr<ElectronBrowserContext> browser_context_;
|
||||||
int request_options_;
|
int request_options_;
|
||||||
std::unique_ptr<network::ResourceRequest> request_;
|
std::unique_ptr<network::ResourceRequest> request_;
|
|
@ -57,7 +57,6 @@
|
||||||
V(electron_browser_menu) \
|
V(electron_browser_menu) \
|
||||||
V(electron_browser_message_port) \
|
V(electron_browser_message_port) \
|
||||||
V(electron_browser_native_theme) \
|
V(electron_browser_native_theme) \
|
||||||
V(electron_browser_net) \
|
|
||||||
V(electron_browser_notification) \
|
V(electron_browser_notification) \
|
||||||
V(electron_browser_power_monitor) \
|
V(electron_browser_power_monitor) \
|
||||||
V(electron_browser_power_save_blocker) \
|
V(electron_browser_power_save_blocker) \
|
||||||
|
@ -76,7 +75,8 @@
|
||||||
V(electron_browser_web_contents_view) \
|
V(electron_browser_web_contents_view) \
|
||||||
V(electron_browser_web_frame_main) \
|
V(electron_browser_web_frame_main) \
|
||||||
V(electron_browser_web_view_manager) \
|
V(electron_browser_web_view_manager) \
|
||||||
V(electron_browser_window)
|
V(electron_browser_window) \
|
||||||
|
V(electron_common_net)
|
||||||
|
|
||||||
#define ELECTRON_COMMON_BINDINGS(V) \
|
#define ELECTRON_COMMON_BINDINGS(V) \
|
||||||
V(electron_common_asar) \
|
V(electron_common_asar) \
|
||||||
|
@ -96,7 +96,10 @@
|
||||||
V(electron_renderer_ipc) \
|
V(electron_renderer_ipc) \
|
||||||
V(electron_renderer_web_frame)
|
V(electron_renderer_web_frame)
|
||||||
|
|
||||||
#define ELECTRON_UTILITY_BINDINGS(V) V(electron_utility_parent_port)
|
#define ELECTRON_UTILITY_BINDINGS(V) \
|
||||||
|
V(electron_browser_event_emitter) \
|
||||||
|
V(electron_common_net) \
|
||||||
|
V(electron_utility_parent_port)
|
||||||
|
|
||||||
#define ELECTRON_TESTING_BINDINGS(V) V(electron_common_testing)
|
#define ELECTRON_TESTING_BINDINGS(V) V(electron_common_testing)
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,11 @@
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "base/command_line.h"
|
#include "base/command_line.h"
|
||||||
|
#include "base/no_destructor.h"
|
||||||
#include "base/strings/utf_string_conversions.h"
|
#include "base/strings/utf_string_conversions.h"
|
||||||
|
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
|
||||||
|
#include "services/network/public/mojom/host_resolver.mojom.h"
|
||||||
|
#include "services/network/public/mojom/network_context.mojom.h"
|
||||||
#include "shell/browser/javascript_environment.h"
|
#include "shell/browser/javascript_environment.h"
|
||||||
#include "shell/common/api/electron_bindings.h"
|
#include "shell/common/api/electron_bindings.h"
|
||||||
#include "shell/common/gin_converters/file_path_converter.h"
|
#include "shell/common/gin_converters/file_path_converter.h"
|
||||||
|
@ -18,6 +22,34 @@
|
||||||
|
|
||||||
namespace electron {
|
namespace electron {
|
||||||
|
|
||||||
|
URLLoaderBundle::URLLoaderBundle() = default;
|
||||||
|
|
||||||
|
URLLoaderBundle::~URLLoaderBundle() = default;
|
||||||
|
|
||||||
|
URLLoaderBundle* URLLoaderBundle::GetInstance() {
|
||||||
|
static base::NoDestructor<URLLoaderBundle> instance;
|
||||||
|
return instance.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
void URLLoaderBundle::SetURLLoaderFactory(
|
||||||
|
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_factory,
|
||||||
|
mojo::Remote<network::mojom::HostResolver> host_resolver) {
|
||||||
|
factory_ = network::SharedURLLoaderFactory::Create(
|
||||||
|
std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
|
||||||
|
std::move(pending_factory)));
|
||||||
|
host_resolver_ = std::move(host_resolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
scoped_refptr<network::SharedURLLoaderFactory>
|
||||||
|
URLLoaderBundle::GetSharedURLLoaderFactory() {
|
||||||
|
return factory_;
|
||||||
|
}
|
||||||
|
|
||||||
|
network::mojom::HostResolver* URLLoaderBundle::GetHostResolver() {
|
||||||
|
DCHECK(host_resolver_);
|
||||||
|
return host_resolver_.get();
|
||||||
|
}
|
||||||
|
|
||||||
NodeService::NodeService(
|
NodeService::NodeService(
|
||||||
mojo::PendingReceiver<node::mojom::NodeService> receiver)
|
mojo::PendingReceiver<node::mojom::NodeService> receiver)
|
||||||
: node_bindings_{NodeBindings::Create(
|
: node_bindings_{NodeBindings::Create(
|
||||||
|
@ -42,6 +74,10 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
|
||||||
|
|
||||||
ParentPort::GetInstance()->Initialize(std::move(params->port));
|
ParentPort::GetInstance()->Initialize(std::move(params->port));
|
||||||
|
|
||||||
|
URLLoaderBundle::GetInstance()->SetURLLoaderFactory(
|
||||||
|
std::move(params->url_loader_factory),
|
||||||
|
mojo::Remote(std::move(params->host_resolver)));
|
||||||
|
|
||||||
js_env_ = std::make_unique<JavascriptEnvironment>(node_bindings_->uv_loop());
|
js_env_ = std::make_unique<JavascriptEnvironment>(node_bindings_->uv_loop());
|
||||||
|
|
||||||
v8::HandleScope scope(js_env_->isolate());
|
v8::HandleScope scope(js_env_->isolate());
|
||||||
|
|
|
@ -8,7 +8,12 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "mojo/public/cpp/bindings/pending_receiver.h"
|
#include "mojo/public/cpp/bindings/pending_receiver.h"
|
||||||
|
#include "mojo/public/cpp/bindings/pending_remote.h"
|
||||||
#include "mojo/public/cpp/bindings/receiver.h"
|
#include "mojo/public/cpp/bindings/receiver.h"
|
||||||
|
#include "mojo/public/cpp/bindings/remote.h"
|
||||||
|
#include "services/network/public/cpp/shared_url_loader_factory.h"
|
||||||
|
#include "services/network/public/mojom/host_resolver.mojom.h"
|
||||||
|
#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
|
||||||
#include "shell/services/node/public/mojom/node_service.mojom.h"
|
#include "shell/services/node/public/mojom/node_service.mojom.h"
|
||||||
|
|
||||||
namespace node {
|
namespace node {
|
||||||
|
@ -23,6 +28,26 @@ class ElectronBindings;
|
||||||
class JavascriptEnvironment;
|
class JavascriptEnvironment;
|
||||||
class NodeBindings;
|
class NodeBindings;
|
||||||
|
|
||||||
|
class URLLoaderBundle {
|
||||||
|
public:
|
||||||
|
URLLoaderBundle();
|
||||||
|
~URLLoaderBundle();
|
||||||
|
|
||||||
|
URLLoaderBundle(const URLLoaderBundle&) = delete;
|
||||||
|
URLLoaderBundle& operator=(const URLLoaderBundle&) = delete;
|
||||||
|
|
||||||
|
static URLLoaderBundle* GetInstance();
|
||||||
|
void SetURLLoaderFactory(
|
||||||
|
mojo::PendingRemote<network::mojom::URLLoaderFactory> factory,
|
||||||
|
mojo::Remote<network::mojom::HostResolver> host_resolver);
|
||||||
|
scoped_refptr<network::SharedURLLoaderFactory> GetSharedURLLoaderFactory();
|
||||||
|
network::mojom::HostResolver* GetHostResolver();
|
||||||
|
|
||||||
|
private:
|
||||||
|
scoped_refptr<network::SharedURLLoaderFactory> factory_;
|
||||||
|
mojo::Remote<network::mojom::HostResolver> host_resolver_;
|
||||||
|
};
|
||||||
|
|
||||||
class NodeService : public node::mojom::NodeService {
|
class NodeService : public node::mojom::NodeService {
|
||||||
public:
|
public:
|
||||||
explicit NodeService(
|
explicit NodeService(
|
||||||
|
|
|
@ -6,6 +6,8 @@ module node.mojom;
|
||||||
|
|
||||||
import "mojo/public/mojom/base/file_path.mojom";
|
import "mojo/public/mojom/base/file_path.mojom";
|
||||||
import "sandbox/policy/mojom/sandbox.mojom";
|
import "sandbox/policy/mojom/sandbox.mojom";
|
||||||
|
import "services/network/public/mojom/host_resolver.mojom";
|
||||||
|
import "services/network/public/mojom/url_loader_factory.mojom";
|
||||||
import "third_party/blink/public/mojom/messaging/message_port_descriptor.mojom";
|
import "third_party/blink/public/mojom/messaging/message_port_descriptor.mojom";
|
||||||
|
|
||||||
struct NodeServiceParams {
|
struct NodeServiceParams {
|
||||||
|
@ -13,6 +15,8 @@ struct NodeServiceParams {
|
||||||
array<string> args;
|
array<string> args;
|
||||||
array<string> exec_args;
|
array<string> exec_args;
|
||||||
blink.mojom.MessagePortDescriptor port;
|
blink.mojom.MessagePortDescriptor port;
|
||||||
|
pending_remote<network.mojom.URLLoaderFactory> url_loader_factory;
|
||||||
|
pending_remote<network.mojom.HostResolver> host_resolver;
|
||||||
};
|
};
|
||||||
|
|
||||||
[ServiceSandbox=sandbox.mojom.Sandbox.kNoSandbox]
|
[ServiceSandbox=sandbox.mojom.Sandbox.kNoSandbox]
|
||||||
|
|
85
spec/api-net-custom-protocols-spec.ts
Normal file
85
spec/api-net-custom-protocols-spec.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { net, protocol } from 'electron/main';
|
||||||
|
import * as url from 'node:url';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { defer } from './lib/spec-helpers';
|
||||||
|
|
||||||
|
describe('net module custom protocols', () => {
|
||||||
|
it('can request file:// URLs', async () => {
|
||||||
|
const resp = await net.fetch(url.pathToFileURL(path.join(__dirname, 'fixtures', 'hello.txt')).toString());
|
||||||
|
expect(resp.ok).to.be.true();
|
||||||
|
// trimRight instead of asserting the whole string to avoid line ending shenanigans on WOA
|
||||||
|
expect((await resp.text()).trimRight()).to.equal('hello world');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can make requests to custom protocols', async () => {
|
||||||
|
protocol.registerStringProtocol('electron-test', (req, cb) => { cb('hello ' + req.url); });
|
||||||
|
defer(() => {
|
||||||
|
protocol.unregisterProtocol('electron-test');
|
||||||
|
});
|
||||||
|
const body = await net.fetch('electron-test://foo').then(r => r.text());
|
||||||
|
expect(body).to.equal('hello electron-test://foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs through intercept handlers', async () => {
|
||||||
|
protocol.interceptStringProtocol('http', (req, cb) => { cb('hello ' + req.url); });
|
||||||
|
defer(() => {
|
||||||
|
protocol.uninterceptProtocol('http');
|
||||||
|
});
|
||||||
|
const body = await net.fetch('http://foo').then(r => r.text());
|
||||||
|
expect(body).to.equal('hello http://foo/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('file: runs through intercept handlers', async () => {
|
||||||
|
protocol.interceptStringProtocol('file', (req, cb) => { cb('hello ' + req.url); });
|
||||||
|
defer(() => {
|
||||||
|
protocol.uninterceptProtocol('file');
|
||||||
|
});
|
||||||
|
const body = await net.fetch('file://foo').then(r => r.text());
|
||||||
|
expect(body).to.equal('hello file://foo/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be redirected', async () => {
|
||||||
|
protocol.interceptStringProtocol('file', (req, cb) => { cb({ statusCode: 302, headers: { location: 'electron-test://bar' } }); });
|
||||||
|
defer(() => {
|
||||||
|
protocol.uninterceptProtocol('file');
|
||||||
|
});
|
||||||
|
protocol.registerStringProtocol('electron-test', (req, cb) => { cb('hello ' + req.url); });
|
||||||
|
defer(() => {
|
||||||
|
protocol.unregisterProtocol('electron-test');
|
||||||
|
});
|
||||||
|
const body = await net.fetch('file://foo').then(r => r.text());
|
||||||
|
expect(body).to.equal('hello electron-test://bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not follow redirect when redirect: error', async () => {
|
||||||
|
protocol.registerStringProtocol('electron-test', (req, cb) => {
|
||||||
|
if (/redirect/.test(req.url)) return cb({ statusCode: 302, headers: { location: 'electron-test://bar' } });
|
||||||
|
cb('hello ' + req.url);
|
||||||
|
});
|
||||||
|
defer(() => {
|
||||||
|
protocol.unregisterProtocol('electron-test');
|
||||||
|
});
|
||||||
|
await expect(net.fetch('electron-test://redirect', { redirect: 'error' })).to.eventually.be.rejectedWith('Attempted to redirect, but redirect policy was \'error\'');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a 307 redirected POST request preserves the body', async () => {
|
||||||
|
const bodyData = 'Hello World!';
|
||||||
|
let postedBodyData: any;
|
||||||
|
protocol.registerStringProtocol('electron-test', async (req, cb) => {
|
||||||
|
if (/redirect/.test(req.url)) return cb({ statusCode: 307, headers: { location: 'electron-test://bar' } });
|
||||||
|
postedBodyData = req.uploadData![0].bytes.toString();
|
||||||
|
cb('hello ' + req.url);
|
||||||
|
});
|
||||||
|
defer(() => {
|
||||||
|
protocol.unregisterProtocol('electron-test');
|
||||||
|
});
|
||||||
|
const response = await net.fetch('electron-test://redirect', {
|
||||||
|
method: 'POST',
|
||||||
|
body: bodyData
|
||||||
|
});
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
await response.text();
|
||||||
|
expect(postedBodyData).to.equal(bodyData);
|
||||||
|
});
|
||||||
|
});
|
642
spec/api-net-session-spec.ts
Normal file
642
spec/api-net-session-spec.ts
Normal file
|
@ -0,0 +1,642 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import * as dns from 'node:dns';
|
||||||
|
import { net, session, BrowserWindow, ClientRequestConstructorOptions } from 'electron/main';
|
||||||
|
import { collectStreamBody, getResponse, respondNTimes, respondOnce } from './lib/net-helpers';
|
||||||
|
|
||||||
|
// See https://github.com/nodejs/node/issues/40702.
|
||||||
|
dns.setDefaultResultOrder('ipv4first');
|
||||||
|
|
||||||
|
describe('net module (session)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
respondNTimes.routeFailure = false;
|
||||||
|
});
|
||||||
|
afterEach(async function () {
|
||||||
|
await session.defaultSession.clearCache();
|
||||||
|
if (respondNTimes.routeFailure && this.test) {
|
||||||
|
if (!this.test.isFailed()) {
|
||||||
|
throw new Error('Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('HTTP basics', () => {
|
||||||
|
for (const extraOptions of [{}, { credentials: 'include' }, { useSessionCookies: false, credentials: 'include' }] as ClientRequestConstructorOptions[]) {
|
||||||
|
describe(`authentication when ${JSON.stringify(extraOptions)}`, () => {
|
||||||
|
it('should share credentials with WebContents', async () => {
|
||||||
|
const [user, pass] = ['user', 'pass'];
|
||||||
|
const serverUrl = await respondNTimes.toSingleURL((request, response) => {
|
||||||
|
if (!request.headers.authorization) {
|
||||||
|
return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end();
|
||||||
|
}
|
||||||
|
return response.writeHead(200).end('ok');
|
||||||
|
}, 2);
|
||||||
|
const bw = new BrowserWindow({ show: false });
|
||||||
|
bw.webContents.on('login', (event, details, authInfo, cb) => {
|
||||||
|
event.preventDefault();
|
||||||
|
cb(user, pass);
|
||||||
|
});
|
||||||
|
await bw.loadURL(serverUrl);
|
||||||
|
bw.close();
|
||||||
|
const request = net.request({ method: 'GET', url: serverUrl, ...extraOptions });
|
||||||
|
let logInCount = 0;
|
||||||
|
request.on('login', () => {
|
||||||
|
logInCount++;
|
||||||
|
});
|
||||||
|
const response = await getResponse(request);
|
||||||
|
await collectStreamBody(response);
|
||||||
|
expect(logInCount).to.equal(0, 'should not receive a login event, credentials should be cached');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should share proxy credentials with WebContents', async () => {
|
||||||
|
const [user, pass] = ['user', 'pass'];
|
||||||
|
const proxyUrl = await respondNTimes((request, response) => {
|
||||||
|
if (!request.headers['proxy-authorization']) {
|
||||||
|
return response.writeHead(407, { 'Proxy-Authenticate': 'Basic realm="Foo"' }).end();
|
||||||
|
}
|
||||||
|
return response.writeHead(200).end('ok');
|
||||||
|
}, 2);
|
||||||
|
const customSession = session.fromPartition(`net-proxy-test-${Math.random()}`);
|
||||||
|
await customSession.setProxy({ proxyRules: proxyUrl.replace('http://', ''), proxyBypassRules: '<-loopback>' });
|
||||||
|
const bw = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
|
||||||
|
bw.webContents.on('login', (event, details, authInfo, cb) => {
|
||||||
|
event.preventDefault();
|
||||||
|
cb(user, pass);
|
||||||
|
});
|
||||||
|
await bw.loadURL('http://127.0.0.1:9999');
|
||||||
|
bw.close();
|
||||||
|
const request = net.request({ method: 'GET', url: 'http://127.0.0.1:9999', session: customSession, ...extraOptions });
|
||||||
|
let logInCount = 0;
|
||||||
|
request.on('login', () => {
|
||||||
|
logInCount++;
|
||||||
|
});
|
||||||
|
const response = await getResponse(request);
|
||||||
|
const body = await collectStreamBody(response);
|
||||||
|
expect(response.statusCode).to.equal(200);
|
||||||
|
expect(body).to.equal('ok');
|
||||||
|
expect(logInCount).to.equal(0, 'should not receive a login event, credentials should be cached');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('authentication when {"credentials":"omit"}', () => {
|
||||||
|
it('should not share credentials with WebContents', async () => {
|
||||||
|
const [user, pass] = ['user', 'pass'];
|
||||||
|
const serverUrl = await respondNTimes.toSingleURL((request, response) => {
|
||||||
|
if (!request.headers.authorization) {
|
||||||
|
return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end();
|
||||||
|
}
|
||||||
|
return response.writeHead(200).end('ok');
|
||||||
|
}, 2);
|
||||||
|
const bw = new BrowserWindow({ show: false });
|
||||||
|
bw.webContents.on('login', (event, details, authInfo, cb) => {
|
||||||
|
event.preventDefault();
|
||||||
|
cb(user, pass);
|
||||||
|
});
|
||||||
|
await bw.loadURL(serverUrl);
|
||||||
|
bw.close();
|
||||||
|
const request = net.request({ method: 'GET', url: serverUrl, credentials: 'omit' });
|
||||||
|
request.on('login', () => {
|
||||||
|
expect.fail();
|
||||||
|
});
|
||||||
|
const response = await getResponse(request);
|
||||||
|
expect(response.statusCode).to.equal(401);
|
||||||
|
expect(response.headers['www-authenticate']).to.equal('Basic realm="Foo"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should share proxy credentials with WebContents', async () => {
|
||||||
|
const [user, pass] = ['user', 'pass'];
|
||||||
|
const proxyUrl = await respondNTimes((request, response) => {
|
||||||
|
if (!request.headers['proxy-authorization']) {
|
||||||
|
return response.writeHead(407, { 'Proxy-Authenticate': 'Basic realm="Foo"' }).end();
|
||||||
|
}
|
||||||
|
return response.writeHead(200).end('ok');
|
||||||
|
}, 2);
|
||||||
|
const customSession = session.fromPartition(`net-proxy-test-${Math.random()}`);
|
||||||
|
await customSession.setProxy({ proxyRules: proxyUrl.replace('http://', ''), proxyBypassRules: '<-loopback>' });
|
||||||
|
const bw = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
|
||||||
|
bw.webContents.on('login', (event, details, authInfo, cb) => {
|
||||||
|
event.preventDefault();
|
||||||
|
cb(user, pass);
|
||||||
|
});
|
||||||
|
await bw.loadURL('http://127.0.0.1:9999');
|
||||||
|
bw.close();
|
||||||
|
const request = net.request({ method: 'GET', url: 'http://127.0.0.1:9999', session: customSession, credentials: 'omit' });
|
||||||
|
request.on('login', () => {
|
||||||
|
expect.fail();
|
||||||
|
});
|
||||||
|
const response = await getResponse(request);
|
||||||
|
const body = await collectStreamBody(response);
|
||||||
|
expect(response.statusCode).to.equal(200);
|
||||||
|
expect(body).to.equal('ok');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ClientRequest API', () => {
|
||||||
|
it('should be able to set cookie header line', async () => {
|
||||||
|
const cookieHeaderName = 'Cookie';
|
||||||
|
const cookieHeaderValue = 'test=12345';
|
||||||
|
const customSession = session.fromPartition(`test-cookie-header-${Math.random()}`);
|
||||||
|
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||||
|
expect(request.headers[cookieHeaderName.toLowerCase()]).to.equal(cookieHeaderValue);
|
||||||
|
response.statusCode = 200;
|
||||||
|
response.statusMessage = 'OK';
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
await customSession.cookies.set({
|
||||||
|
url: `${serverUrl}`,
|
||||||
|
name: 'test',
|
||||||
|
value: '11111',
|
||||||
|
expirationDate: 0
|
||||||
|
});
|
||||||
|
const urlRequest = net.request({
|
||||||
|
method: 'GET',
|
||||||
|
url: serverUrl,
|
||||||
|
session: customSession
|
||||||
|
});
|
||||||
|
urlRequest.setHeader(cookieHeaderName, cookieHeaderValue);
|
||||||
|
expect(urlRequest.getHeader(cookieHeaderName)).to.equal(cookieHeaderValue);
|
||||||
|
const response = await getResponse(urlRequest);
|
||||||
|
expect(response.statusCode).to.equal(200);
|
||||||
|
await collectStreamBody(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not use the sessions cookie store by default', async () => {
|
||||||
|
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||||
|
response.statusCode = 200;
|
||||||
|
response.statusMessage = 'OK';
|
||||||
|
response.setHeader('x-cookie', `${request.headers.cookie!}`);
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||||
|
const cookieVal = `${Date.now()}`;
|
||||||
|
await sess.cookies.set({
|
||||||
|
url: serverUrl,
|
||||||
|
name: 'wild_cookie',
|
||||||
|
value: cookieVal
|
||||||
|
});
|
||||||
|
const urlRequest = net.request({
|
||||||
|
url: serverUrl,
|
||||||
|
session: sess
|
||||||
|
});
|
||||||
|
const response = await getResponse(urlRequest);
|
||||||
|
expect(response.headers['x-cookie']).to.equal('undefined');
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const extraOptions of [{ useSessionCookies: true }, { credentials: 'include' }] as ClientRequestConstructorOptions[]) {
|
||||||
|
describe(`when ${JSON.stringify(extraOptions)}`, () => {
|
||||||
|
it('should be able to use the sessions cookie store', async () => {
|
||||||
|
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||||
|
response.statusCode = 200;
|
||||||
|
response.statusMessage = 'OK';
|
||||||
|
response.setHeader('x-cookie', request.headers.cookie!);
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||||
|
const cookieVal = `${Date.now()}`;
|
||||||
|
await sess.cookies.set({
|
||||||
|
url: serverUrl,
|
||||||
|
name: 'wild_cookie',
|
||||||
|
value: cookieVal
|
||||||
|
});
|
||||||
|
const urlRequest = net.request({
|
||||||
|
url: serverUrl,
|
||||||
|
session: sess,
|
||||||
|
...extraOptions
|
||||||
|
});
|
||||||
|
const response = await getResponse(urlRequest);
|
||||||
|
expect(response.headers['x-cookie']).to.equal(`wild_cookie=${cookieVal}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to use the sessions cookie store with set-cookie', async () => {
|
||||||
|
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||||
|
response.statusCode = 200;
|
||||||
|
response.statusMessage = 'OK';
|
||||||
|
response.setHeader('set-cookie', 'foo=bar');
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||||
|
let cookies = await sess.cookies.get({});
|
||||||
|
expect(cookies).to.have.lengthOf(0);
|
||||||
|
const urlRequest = net.request({
|
||||||
|
url: serverUrl,
|
||||||
|
session: sess,
|
||||||
|
...extraOptions
|
||||||
|
});
|
||||||
|
await collectStreamBody(await getResponse(urlRequest));
|
||||||
|
cookies = await sess.cookies.get({});
|
||||||
|
expect(cookies).to.have.lengthOf(1);
|
||||||
|
expect(cookies[0]).to.deep.equal({
|
||||||
|
name: 'foo',
|
||||||
|
value: 'bar',
|
||||||
|
domain: '127.0.0.1',
|
||||||
|
hostOnly: true,
|
||||||
|
path: '/',
|
||||||
|
secure: false,
|
||||||
|
httpOnly: false,
|
||||||
|
session: true,
|
||||||
|
sameSite: 'unspecified'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const mode of ['Lax', 'Strict']) {
|
||||||
|
it(`should be able to use the sessions cookie store with same-site ${mode} cookies`, async () => {
|
||||||
|
const serverUrl = await respondNTimes.toSingleURL((request, response) => {
|
||||||
|
response.statusCode = 200;
|
||||||
|
response.statusMessage = 'OK';
|
||||||
|
response.setHeader('set-cookie', `same=site; SameSite=${mode}`);
|
||||||
|
response.setHeader('x-cookie', `${request.headers.cookie}`);
|
||||||
|
response.end();
|
||||||
|
}, 2);
|
||||||
|
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||||
|
let cookies = await sess.cookies.get({});
|
||||||
|
expect(cookies).to.have.lengthOf(0);
|
||||||
|
const urlRequest = net.request({
|
||||||
|
url: serverUrl,
|
||||||
|
session: sess,
|
||||||
|
...extraOptions
|
||||||
|
});
|
||||||
|
const response = await getResponse(urlRequest);
|
||||||
|
expect(response.headers['x-cookie']).to.equal('undefined');
|
||||||
|
await collectStreamBody(response);
|
||||||
|
cookies = await sess.cookies.get({});
|
||||||
|
expect(cookies).to.have.lengthOf(1);
|
||||||
|
expect(cookies[0]).to.deep.equal({
|
||||||
|
name: 'same',
|
||||||
|
value: 'site',
|
||||||
|
domain: '127.0.0.1',
|
||||||
|
hostOnly: true,
|
||||||
|
path: '/',
|
||||||
|
secure: false,
|
||||||
|
httpOnly: false,
|
||||||
|
session: true,
|
||||||
|
sameSite: mode.toLowerCase()
|
||||||
|
});
|
||||||
|
const urlRequest2 = net.request({
|
||||||
|
url: serverUrl,
|
||||||
|
session: sess,
|
||||||
|
...extraOptions
|
||||||
|
});
|
||||||
|
const response2 = await getResponse(urlRequest2);
|
||||||
|
expect(response2.headers['x-cookie']).to.equal('same=site');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should be able to use the sessions cookie store safely across redirects', async () => {
|
||||||
|
const serverUrl = await respondOnce.toSingleURL(async (request, response) => {
|
||||||
|
response.statusCode = 302;
|
||||||
|
response.statusMessage = 'Moved';
|
||||||
|
const newUrl = await respondOnce.toSingleURL((req, res) => {
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.statusMessage = 'OK';
|
||||||
|
res.setHeader('x-cookie', req.headers.cookie!);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
response.setHeader('x-cookie', request.headers.cookie!);
|
||||||
|
response.setHeader('location', newUrl.replace('127.0.0.1', 'localhost'));
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||||
|
const cookie127Val = `${Date.now()}-127`;
|
||||||
|
const cookieLocalVal = `${Date.now()}-local`;
|
||||||
|
const localhostUrl = serverUrl.replace('127.0.0.1', 'localhost');
|
||||||
|
expect(localhostUrl).to.not.equal(serverUrl);
|
||||||
|
// cookies with lax or strict same-site settings will not
|
||||||
|
// persist after redirects. no_restriction must be used
|
||||||
|
await Promise.all([
|
||||||
|
sess.cookies.set({
|
||||||
|
url: serverUrl,
|
||||||
|
name: 'wild_cookie',
|
||||||
|
sameSite: 'no_restriction',
|
||||||
|
value: cookie127Val
|
||||||
|
}), sess.cookies.set({
|
||||||
|
url: localhostUrl,
|
||||||
|
name: 'wild_cookie',
|
||||||
|
sameSite: 'no_restriction',
|
||||||
|
value: cookieLocalVal
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
const urlRequest = net.request({
|
||||||
|
url: serverUrl,
|
||||||
|
session: sess,
|
||||||
|
...extraOptions
|
||||||
|
});
|
||||||
|
urlRequest.on('redirect', (status, method, url, headers) => {
|
||||||
|
// The initial redirect response should have received the 127 value here
|
||||||
|
expect(headers['x-cookie'][0]).to.equal(`wild_cookie=${cookie127Val}`);
|
||||||
|
urlRequest.followRedirect();
|
||||||
|
});
|
||||||
|
const response = await getResponse(urlRequest);
|
||||||
|
// We expect the server to have received the localhost value here
|
||||||
|
// The original request was to a 127.0.0.1 URL
|
||||||
|
// That request would have the cookie127Val cookie attached
|
||||||
|
// The request is then redirect to a localhost URL (different site)
|
||||||
|
// Because we are using the session cookie store it should do the safe / secure thing
|
||||||
|
// and attach the cookies for the new target domain
|
||||||
|
expect(response.headers['x-cookie']).to.equal(`wild_cookie=${cookieLocalVal}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should be able correctly filter out cookies that are secure', async () => {
|
||||||
|
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
sess.cookies.set({
|
||||||
|
url: 'https://electronjs.org',
|
||||||
|
domain: 'electronjs.org',
|
||||||
|
name: 'cookie1',
|
||||||
|
value: '1',
|
||||||
|
secure: true
|
||||||
|
}),
|
||||||
|
sess.cookies.set({
|
||||||
|
url: 'https://electronjs.org',
|
||||||
|
domain: 'electronjs.org',
|
||||||
|
name: 'cookie2',
|
||||||
|
value: '2',
|
||||||
|
secure: false
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const secureCookies = await sess.cookies.get({
|
||||||
|
secure: true
|
||||||
|
});
|
||||||
|
expect(secureCookies).to.have.lengthOf(1);
|
||||||
|
expect(secureCookies[0].name).to.equal('cookie1');
|
||||||
|
|
||||||
|
const cookies = await sess.cookies.get({
|
||||||
|
secure: false
|
||||||
|
});
|
||||||
|
expect(cookies).to.have.lengthOf(1);
|
||||||
|
expect(cookies[0].name).to.equal('cookie2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when an invalid domain is passed', async () => {
|
||||||
|
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||||
|
|
||||||
|
await expect(sess.cookies.set({
|
||||||
|
url: 'https://electronjs.org',
|
||||||
|
domain: 'wssss.iamabaddomain.fun',
|
||||||
|
name: 'cookie1'
|
||||||
|
})).to.eventually.be.rejectedWith(/Failed to set cookie with an invalid domain attribute/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able correctly filter out cookies that are session', async () => {
|
||||||
|
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
sess.cookies.set({
|
||||||
|
url: 'https://electronjs.org',
|
||||||
|
domain: 'electronjs.org',
|
||||||
|
name: 'cookie1',
|
||||||
|
value: '1'
|
||||||
|
}),
|
||||||
|
sess.cookies.set({
|
||||||
|
url: 'https://electronjs.org',
|
||||||
|
domain: 'electronjs.org',
|
||||||
|
name: 'cookie2',
|
||||||
|
value: '2',
|
||||||
|
expirationDate: Math.round(Date.now() / 1000) + 10000
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sessionCookies = await sess.cookies.get({
|
||||||
|
session: true
|
||||||
|
});
|
||||||
|
expect(sessionCookies).to.have.lengthOf(1);
|
||||||
|
expect(sessionCookies[0].name).to.equal('cookie1');
|
||||||
|
|
||||||
|
const cookies = await sess.cookies.get({
|
||||||
|
session: false
|
||||||
|
});
|
||||||
|
expect(cookies).to.have.lengthOf(1);
|
||||||
|
expect(cookies[0].name).to.equal('cookie2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able correctly filter out cookies that are httpOnly', async () => {
|
||||||
|
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
sess.cookies.set({
|
||||||
|
url: 'https://electronjs.org',
|
||||||
|
domain: 'electronjs.org',
|
||||||
|
name: 'cookie1',
|
||||||
|
value: '1',
|
||||||
|
httpOnly: true
|
||||||
|
}),
|
||||||
|
sess.cookies.set({
|
||||||
|
url: 'https://electronjs.org',
|
||||||
|
domain: 'electronjs.org',
|
||||||
|
name: 'cookie2',
|
||||||
|
value: '2',
|
||||||
|
httpOnly: false
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const httpOnlyCookies = await sess.cookies.get({
|
||||||
|
httpOnly: true
|
||||||
|
});
|
||||||
|
expect(httpOnlyCookies).to.have.lengthOf(1);
|
||||||
|
expect(httpOnlyCookies[0].name).to.equal('cookie1');
|
||||||
|
|
||||||
|
const cookies = await sess.cookies.get({
|
||||||
|
httpOnly: false
|
||||||
|
});
|
||||||
|
expect(cookies).to.have.lengthOf(1);
|
||||||
|
expect(cookies[0].name).to.equal('cookie2');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('webRequest', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
session.defaultSession.webRequest.onBeforeRequest(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should throw when invalid filters are passed', () => {
|
||||||
|
expect(() => {
|
||||||
|
session.defaultSession.webRequest.onBeforeRequest(
|
||||||
|
{ urls: ['*://www.googleapis.com'] },
|
||||||
|
(details, callback) => { callback({ cancel: false }); }
|
||||||
|
);
|
||||||
|
}).to.throw('Invalid url pattern *://www.googleapis.com: Empty path.');
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
session.defaultSession.webRequest.onBeforeRequest(
|
||||||
|
{ urls: ['*://www.googleapis.com/', '*://blahblah.dev'] },
|
||||||
|
(details, callback) => { callback({ cancel: false }); }
|
||||||
|
);
|
||||||
|
}).to.throw('Invalid url pattern *://blahblah.dev: Empty path.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not throw when valid filters are passed', () => {
|
||||||
|
expect(() => {
|
||||||
|
session.defaultSession.webRequest.onBeforeRequest(
|
||||||
|
{ urls: ['*://www.googleapis.com/'] },
|
||||||
|
(details, callback) => { callback({ cancel: false }); }
|
||||||
|
);
|
||||||
|
}).to.not.throw();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Requests should be intercepted by webRequest module', async () => {
|
||||||
|
const requestUrl = '/requestUrl';
|
||||||
|
const redirectUrl = '/redirectUrl';
|
||||||
|
let requestIsRedirected = false;
|
||||||
|
const serverUrl = await respondOnce.toURL(redirectUrl, (request, response) => {
|
||||||
|
requestIsRedirected = true;
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
let requestIsIntercepted = false;
|
||||||
|
session.defaultSession.webRequest.onBeforeRequest(
|
||||||
|
(details, callback) => {
|
||||||
|
if (details.url === `${serverUrl}${requestUrl}`) {
|
||||||
|
requestIsIntercepted = true;
|
||||||
|
callback({
|
||||||
|
redirectURL: `${serverUrl}${redirectUrl}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback({
|
||||||
|
cancel: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const urlRequest = net.request(`${serverUrl}${requestUrl}`);
|
||||||
|
const response = await getResponse(urlRequest);
|
||||||
|
|
||||||
|
expect(response.statusCode).to.equal(200);
|
||||||
|
await collectStreamBody(response);
|
||||||
|
expect(requestIsRedirected).to.be.true('The server should receive a request to the forward URL');
|
||||||
|
expect(requestIsIntercepted).to.be.true('The request should be intercepted by the webRequest module');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should to able to create and intercept a request using a custom session object', async () => {
|
||||||
|
const requestUrl = '/requestUrl';
|
||||||
|
const redirectUrl = '/redirectUrl';
|
||||||
|
const customPartitionName = `custom-partition-${Math.random()}`;
|
||||||
|
let requestIsRedirected = false;
|
||||||
|
const serverUrl = await respondOnce.toURL(redirectUrl, (request, response) => {
|
||||||
|
requestIsRedirected = true;
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
session.defaultSession.webRequest.onBeforeRequest(() => {
|
||||||
|
expect.fail('Request should not be intercepted by the default session');
|
||||||
|
});
|
||||||
|
|
||||||
|
const customSession = session.fromPartition(customPartitionName, { cache: false });
|
||||||
|
let requestIsIntercepted = false;
|
||||||
|
customSession.webRequest.onBeforeRequest((details, callback) => {
|
||||||
|
if (details.url === `${serverUrl}${requestUrl}`) {
|
||||||
|
requestIsIntercepted = true;
|
||||||
|
callback({
|
||||||
|
redirectURL: `${serverUrl}${redirectUrl}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback({
|
||||||
|
cancel: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const urlRequest = net.request({
|
||||||
|
url: `${serverUrl}${requestUrl}`,
|
||||||
|
session: customSession
|
||||||
|
});
|
||||||
|
const response = await getResponse(urlRequest);
|
||||||
|
expect(response.statusCode).to.equal(200);
|
||||||
|
await collectStreamBody(response);
|
||||||
|
expect(requestIsRedirected).to.be.true('The server should receive a request to the forward URL');
|
||||||
|
expect(requestIsIntercepted).to.be.true('The request should be intercepted by the webRequest module');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should to able to create and intercept a request using a custom partition name', async () => {
|
||||||
|
const requestUrl = '/requestUrl';
|
||||||
|
const redirectUrl = '/redirectUrl';
|
||||||
|
const customPartitionName = `custom-partition-${Math.random()}`;
|
||||||
|
let requestIsRedirected = false;
|
||||||
|
const serverUrl = await respondOnce.toURL(redirectUrl, (request, response) => {
|
||||||
|
requestIsRedirected = true;
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
session.defaultSession.webRequest.onBeforeRequest(() => {
|
||||||
|
expect.fail('Request should not be intercepted by the default session');
|
||||||
|
});
|
||||||
|
|
||||||
|
const customSession = session.fromPartition(customPartitionName, { cache: false });
|
||||||
|
let requestIsIntercepted = false;
|
||||||
|
customSession.webRequest.onBeforeRequest((details, callback) => {
|
||||||
|
if (details.url === `${serverUrl}${requestUrl}`) {
|
||||||
|
requestIsIntercepted = true;
|
||||||
|
callback({
|
||||||
|
redirectURL: `${serverUrl}${redirectUrl}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback({
|
||||||
|
cancel: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const urlRequest = net.request({
|
||||||
|
url: `${serverUrl}${requestUrl}`,
|
||||||
|
partition: customPartitionName
|
||||||
|
});
|
||||||
|
const response = await getResponse(urlRequest);
|
||||||
|
expect(response.statusCode).to.equal(200);
|
||||||
|
await collectStreamBody(response);
|
||||||
|
expect(requestIsRedirected).to.be.true('The server should receive a request to the forward URL');
|
||||||
|
expect(requestIsIntercepted).to.be.true('The request should be intercepted by the webRequest module');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers webRequest handlers when bypassCustomProtocolHandlers', async () => {
|
||||||
|
let webRequestDetails: Electron.OnBeforeRequestListenerDetails | null = null;
|
||||||
|
const serverUrl = await respondOnce.toSingleURL((req, res) => res.end('hi'));
|
||||||
|
session.defaultSession.webRequest.onBeforeRequest((details, cb) => {
|
||||||
|
webRequestDetails = details;
|
||||||
|
cb({});
|
||||||
|
});
|
||||||
|
const body = await net.fetch(serverUrl, { bypassCustomProtocolHandlers: true }).then(r => r.text());
|
||||||
|
expect(body).to.equal('hi');
|
||||||
|
expect(webRequestDetails).to.have.property('url', serverUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if given an invalid session option', () => {
|
||||||
|
expect(() => {
|
||||||
|
net.request({
|
||||||
|
url: 'https://foo',
|
||||||
|
session: 1 as any
|
||||||
|
});
|
||||||
|
}).to.throw('`session` should be an instance of the Session class');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if given an invalid partition option', () => {
|
||||||
|
expect(() => {
|
||||||
|
net.request({
|
||||||
|
url: 'https://foo',
|
||||||
|
partition: 1 as any
|
||||||
|
});
|
||||||
|
}).to.throw('`partition` should be a string');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('net.fetch', () => {
|
||||||
|
it('should be able to use a session cookie store', async () => {
|
||||||
|
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||||
|
response.statusCode = 200;
|
||||||
|
response.statusMessage = 'OK';
|
||||||
|
response.setHeader('x-cookie', request.headers.cookie!);
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||||
|
const cookieVal = `${Date.now()}`;
|
||||||
|
await sess.cookies.set({
|
||||||
|
url: serverUrl,
|
||||||
|
name: 'wild_cookie',
|
||||||
|
value: cookieVal
|
||||||
|
});
|
||||||
|
const response = await sess.fetch(serverUrl, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
expect(response.headers.get('x-cookie')).to.equal(`wild_cookie=${cookieVal}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
3725
spec/api-net-spec.ts
3725
spec/api-net-spec.ts
File diff suppressed because it is too large
Load diff
49
spec/fixtures/api/utility-process/api-net-spec.js
vendored
Normal file
49
spec/fixtures/api/utility-process/api-net-spec.js
vendored
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
require('ts-node/register');
|
||||||
|
|
||||||
|
const chai_1 = require('chai');
|
||||||
|
const main_1 = require('electron/main');
|
||||||
|
const http = require('node:http');
|
||||||
|
const url = require('node:url');
|
||||||
|
const node_events_1 = require('node:events');
|
||||||
|
const promises_1 = require('node:timers/promises');
|
||||||
|
const net_helpers_1 = require('../../../lib/net-helpers');
|
||||||
|
const v8 = require('node:v8');
|
||||||
|
|
||||||
|
v8.setFlagsFromString('--expose_gc');
|
||||||
|
chai_1.use(require('chai-as-promised'));
|
||||||
|
chai_1.use(require('dirty-chai'));
|
||||||
|
|
||||||
|
function fail (message) {
|
||||||
|
process.parentPort.postMessage({ ok: false, message });
|
||||||
|
}
|
||||||
|
|
||||||
|
process.parentPort.on('message', async (e) => {
|
||||||
|
// Equivalent of beforeEach in spec/api-net-spec.ts
|
||||||
|
net_helpers_1.respondNTimes.routeFailure = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (e.data.args) {
|
||||||
|
for (const [key, value] of Object.entries(e.data.args)) {
|
||||||
|
// eslint-disable-next-line no-eval
|
||||||
|
eval(`var ${key} = value;`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-eval
|
||||||
|
await eval(e.data.fn);
|
||||||
|
} catch (err) {
|
||||||
|
fail(`${err}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equivalent of afterEach in spec/api-net-spec.ts
|
||||||
|
if (net_helpers_1.respondNTimes.routeFailure) {
|
||||||
|
fail('Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test passed
|
||||||
|
process.parentPort.postMessage({ ok: true });
|
||||||
|
process.exit(0);
|
||||||
|
});
|
108
spec/lib/net-helpers.ts
Normal file
108
spec/lib/net-helpers.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import * as dns from 'node:dns';
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import { Socket } from 'node:net';
|
||||||
|
import { defer, listen } from './spec-helpers';
|
||||||
|
|
||||||
|
// See https://github.com/nodejs/node/issues/40702.
|
||||||
|
dns.setDefaultResultOrder('ipv4first');
|
||||||
|
|
||||||
|
export const kOneKiloByte = 1024;
|
||||||
|
export const kOneMegaByte = kOneKiloByte * kOneKiloByte;
|
||||||
|
|
||||||
|
export function randomBuffer (size: number, start: number = 0, end: number = 255) {
|
||||||
|
const range = 1 + end - start;
|
||||||
|
const buffer = Buffer.allocUnsafe(size);
|
||||||
|
for (let i = 0; i < size; ++i) {
|
||||||
|
buffer[i] = start + Math.floor(Math.random() * range);
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randomString (length: number) {
|
||||||
|
const buffer = randomBuffer(length, '0'.charCodeAt(0), 'z'.charCodeAt(0));
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getResponse (urlRequest: Electron.ClientRequest) {
|
||||||
|
return new Promise<Electron.IncomingMessage>((resolve, reject) => {
|
||||||
|
urlRequest.on('error', reject);
|
||||||
|
urlRequest.on('abort', reject);
|
||||||
|
urlRequest.on('response', (response) => resolve(response));
|
||||||
|
urlRequest.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function collectStreamBody (response: Electron.IncomingMessage | http.IncomingMessage) {
|
||||||
|
return (await collectStreamBodyBuffer(response)).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectStreamBodyBuffer (response: Electron.IncomingMessage | http.IncomingMessage) {
|
||||||
|
return new Promise<Buffer>((resolve, reject) => {
|
||||||
|
response.on('error', reject);
|
||||||
|
(response as NodeJS.EventEmitter).on('aborted', reject);
|
||||||
|
const data: Buffer[] = [];
|
||||||
|
response.on('data', (chunk) => data.push(chunk));
|
||||||
|
response.on('end', (chunk?: Buffer) => {
|
||||||
|
if (chunk) data.push(chunk);
|
||||||
|
resolve(Buffer.concat(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function respondNTimes (fn: http.RequestListener, n: number): Promise<string> {
|
||||||
|
const server = http.createServer((request, response) => {
|
||||||
|
fn(request, response);
|
||||||
|
// don't close if a redirect was returned
|
||||||
|
if ((response.statusCode < 300 || response.statusCode >= 399) && n <= 0) {
|
||||||
|
n--;
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sockets: Socket[] = [];
|
||||||
|
server.on('connection', s => sockets.push(s));
|
||||||
|
defer(() => {
|
||||||
|
server.close();
|
||||||
|
for (const socket of sockets) {
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return (await listen(server)).url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function respondOnce (fn: http.RequestListener) {
|
||||||
|
return respondNTimes(fn, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
respondNTimes.routeFailure = false;
|
||||||
|
|
||||||
|
respondNTimes.toRoutes = (routes: Record<string, http.RequestListener>, n: number) => {
|
||||||
|
return respondNTimes((request, response) => {
|
||||||
|
if (Object.hasOwn(routes, request.url || '')) {
|
||||||
|
(async () => {
|
||||||
|
await Promise.resolve(routes[request.url || ''](request, response));
|
||||||
|
})().catch((err) => {
|
||||||
|
respondNTimes.routeFailure = true;
|
||||||
|
console.error('Route handler failed, this is probably why your test failed', err);
|
||||||
|
response.statusCode = 500;
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
response.statusCode = 500;
|
||||||
|
response.end();
|
||||||
|
expect.fail(`Unexpected URL: ${request.url}`);
|
||||||
|
}
|
||||||
|
}, n);
|
||||||
|
};
|
||||||
|
respondOnce.toRoutes = (routes: Record<string, http.RequestListener>) => respondNTimes.toRoutes(routes, 1);
|
||||||
|
|
||||||
|
respondNTimes.toURL = (url: string, fn: http.RequestListener, n: number) => {
|
||||||
|
return respondNTimes.toRoutes({ [url]: fn }, n);
|
||||||
|
};
|
||||||
|
respondOnce.toURL = (url: string, fn: http.RequestListener) => respondNTimes.toURL(url, fn, 1);
|
||||||
|
|
||||||
|
respondNTimes.toSingleURL = (fn: http.RequestListener, n: number) => {
|
||||||
|
const requestUrl = '/requestUrl';
|
||||||
|
return respondNTimes.toURL(requestUrl, fn, n).then(url => `${url}${requestUrl}`);
|
||||||
|
};
|
||||||
|
respondOnce.toSingleURL = (fn: http.RequestListener) => respondNTimes.toSingleURL(fn, 1);
|
3
typings/internal-ambient.d.ts
vendored
3
typings/internal-ambient.d.ts
vendored
|
@ -101,6 +101,7 @@ declare namespace NodeJS {
|
||||||
Net: any;
|
Net: any;
|
||||||
net: any;
|
net: any;
|
||||||
createURLLoader(options: CreateURLLoaderOptions): URLLoader;
|
createURLLoader(options: CreateURLLoaderOptions): URLLoader;
|
||||||
|
resolveHost(host: string, options?: Electron.ResolveHostOptions): Promise<Electron.ResolvedHost>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotificationBinding {
|
interface NotificationBinding {
|
||||||
|
@ -209,6 +210,7 @@ declare namespace NodeJS {
|
||||||
_linkedBinding(name: 'electron_common_environment'): EnvironmentBinding;
|
_linkedBinding(name: 'electron_common_environment'): EnvironmentBinding;
|
||||||
_linkedBinding(name: 'electron_common_features'): FeaturesBinding;
|
_linkedBinding(name: 'electron_common_features'): FeaturesBinding;
|
||||||
_linkedBinding(name: 'electron_common_native_image'): { nativeImage: typeof Electron.NativeImage };
|
_linkedBinding(name: 'electron_common_native_image'): { nativeImage: typeof Electron.NativeImage };
|
||||||
|
_linkedBinding(name: 'electron_common_net'): NetBinding;
|
||||||
_linkedBinding(name: 'electron_common_shell'): Electron.Shell;
|
_linkedBinding(name: 'electron_common_shell'): Electron.Shell;
|
||||||
_linkedBinding(name: 'electron_common_v8_util'): V8UtilBinding;
|
_linkedBinding(name: 'electron_common_v8_util'): V8UtilBinding;
|
||||||
_linkedBinding(name: 'electron_browser_app'): { app: Electron.App, App: Function };
|
_linkedBinding(name: 'electron_browser_app'): { app: Electron.App, App: Function };
|
||||||
|
@ -221,7 +223,6 @@ declare namespace NodeJS {
|
||||||
_linkedBinding(name: 'electron_browser_in_app_purchase'): { inAppPurchase: Electron.InAppPurchase };
|
_linkedBinding(name: 'electron_browser_in_app_purchase'): { inAppPurchase: Electron.InAppPurchase };
|
||||||
_linkedBinding(name: 'electron_browser_message_port'): { createPair(): { port1: Electron.MessagePortMain, port2: Electron.MessagePortMain }; };
|
_linkedBinding(name: 'electron_browser_message_port'): { createPair(): { port1: Electron.MessagePortMain, port2: Electron.MessagePortMain }; };
|
||||||
_linkedBinding(name: 'electron_browser_native_theme'): { nativeTheme: Electron.NativeTheme };
|
_linkedBinding(name: 'electron_browser_native_theme'): { nativeTheme: Electron.NativeTheme };
|
||||||
_linkedBinding(name: 'electron_browser_net'): NetBinding;
|
|
||||||
_linkedBinding(name: 'electron_browser_notification'): NotificationBinding;
|
_linkedBinding(name: 'electron_browser_notification'): NotificationBinding;
|
||||||
_linkedBinding(name: 'electron_browser_power_monitor'): PowerMonitorBinding;
|
_linkedBinding(name: 'electron_browser_power_monitor'): PowerMonitorBinding;
|
||||||
_linkedBinding(name: 'electron_browser_power_save_blocker'): { powerSaveBlocker: Electron.PowerSaveBlocker };
|
_linkedBinding(name: 'electron_browser_power_save_blocker'): { powerSaveBlocker: Electron.PowerSaveBlocker };
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue