Browse Source

feat: add net module to utility process (#40967)

feat: add net module to utility process (#40017)

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
trop[bot] 1 year ago
parent
commit
8f72f9bcb5

+ 1 - 1
docs/api/client-request.md

@@ -2,7 +2,7 @@
 
 > 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._
 
 `ClientRequest` implements the [Writable Stream](https://nodejs.org/api/stream.html#stream_writable_streams)

+ 1 - 1
docs/api/incoming-message.md

@@ -2,7 +2,7 @@
 
 > 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._
 
 `IncomingMessage` implements the [Readable Stream](https://nodejs.org/api/stream.html#stream_readable_streams)

+ 4 - 1
docs/api/net.md

@@ -2,7 +2,7 @@
 
 > 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
 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()`
 
 Returns `boolean` - Whether there is currently internet connection.

+ 5 - 1
filenames.auto.gni

@@ -213,7 +213,6 @@ auto_filenames = {
     "lib/browser/api/message-channel.ts",
     "lib/browser/api/module-list.ts",
     "lib/browser/api/native-theme.ts",
-    "lib/browser/api/net-client-request.ts",
     "lib/browser/api/net-fetch.ts",
     "lib/browser/api/net-log.ts",
     "lib/browser/api/net.ts",
@@ -249,6 +248,7 @@ auto_filenames = {
     "lib/browser/web-view-events.ts",
     "lib/common/api/module-list.ts",
     "lib/common/api/native-image.ts",
+    "lib/common/api/net-client-request.ts",
     "lib/common/api/shell.ts",
     "lib/common/define-properties.ts",
     "lib/common/deprecate.ts",
@@ -340,12 +340,16 @@ auto_filenames = {
   ]
 
   utility_bundle_deps = [
+    "lib/browser/api/net-fetch.ts",
     "lib/browser/message-port-main.ts",
+    "lib/common/api/net-client-request.ts",
     "lib/common/define-properties.ts",
     "lib/common/init.ts",
     "lib/common/reset-search-paths.ts",
+    "lib/common/webpack-globals-provider.ts",
     "lib/utility/api/exports/electron.ts",
     "lib/utility/api/module-list.ts",
+    "lib/utility/api/net.ts",
     "lib/utility/init.ts",
     "lib/utility/parent-port.ts",
     "package.json",

+ 3 - 3
filenames.gni

@@ -283,7 +283,6 @@ filenames = {
     "shell/browser/api/electron_api_menu.h",
     "shell/browser/api/electron_api_native_theme.cc",
     "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.h",
     "shell/browser/api/electron_api_notification.cc",
@@ -309,8 +308,6 @@ filenames = {
     "shell/browser/api/electron_api_system_preferences.h",
     "shell/browser/api/electron_api_tray.cc",
     "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.h",
     "shell/browser/api/electron_api_view.cc",
@@ -550,8 +547,11 @@ filenames = {
     "shell/common/api/electron_api_key_weak_map.h",
     "shell/common/api/electron_api_native_image.cc",
     "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_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_bindings.cc",
     "shell/common/api/electron_bindings.h",

+ 5 - 4
lib/browser/api/net-fetch.ts

@@ -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 { 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; } {
   let res: (x: T) => void;
@@ -13,7 +13,8 @@ function createDeferredPromise<T, E extends Error = Error> (): { promise: Promis
   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>();
   let req: Request;
   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.
   const credentials = req.credentials === 'same-origin' && !origin ? 'include' : req.credentials;
 
-  const r = net.request(allowAnyProtocol({
+  const r = request(allowAnyProtocol({
     session,
     method: req.method,
     url: req.url,

+ 6 - 3
lib/browser/api/net.ts

@@ -1,10 +1,13 @@
-import { IncomingMessage, session } from 'electron/main';
+import { app, IncomingMessage, session } 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) {
+  if (!app.isReady()) {
+    throw new Error('net module can only be used after app is ready');
+  }
   return new ClientRequest(options, callback);
 }
 

+ 2 - 1
lib/browser/api/session.ts

@@ -1,8 +1,9 @@
 import { fetchWithSession } from '@electron/internal/browser/api/net-fetch';
+import { net } from 'electron/main';
 const { fromPartition, fromPath, Session } = process._linkedBinding('electron_browser_session');
 
 Session.prototype.fetch = function (input: RequestInfo, init?: RequestInit) {
-  return fetchWithSession(input, init, this);
+  return fetchWithSession(input, init, this, net.request);
 };
 
 export default {

+ 1 - 1
lib/browser/guest-view-manager.ts

@@ -14,7 +14,7 @@ interface GuestInstance {
 }
 
 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);
 

+ 16 - 16
lib/browser/api/net-client-request.ts → lib/common/api/net-client-request.ts

@@ -1,14 +1,15 @@
 import * as url from 'url';
 import { Readable, Writable } from 'stream';
-import { app } from 'electron/main';
-import type { ClientRequestConstructorOptions, UploadProgress } from 'electron/main';
+import type {
+  ClientRequestConstructorOptions,
+  UploadProgress
+} from 'electron/common';
 
 const {
   isValidHeaderName,
   isValidHeaderValue,
   createURLLoader
-} = process._linkedBinding('electron_browser_net');
-const { Session } = process._linkedBinding('electron_browser_session');
+} = process._linkedBinding('electron_common_net');
 
 const kHttpProtocols = new Set(['http:', 'https:']);
 
@@ -283,14 +284,17 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
     const key = name.toLowerCase();
     urlLoaderOptions.headers[key] = { name, value };
   }
-  if (options.session) {
-    if (!(options.session instanceof Session)) { throw new TypeError('`session` should be an instance of the Session class'); }
-    urlLoaderOptions.session = options.session;
-  } else if (options.partition) {
-    if (typeof options.partition === 'string') {
-      urlLoaderOptions.partition = options.partition;
-    } else {
-      throw new TypeError('`partition` should be a string');
+  if (process.type !== 'utility') {
+    const { Session } = process._linkedBinding('electron_browser_session');
+    if (options.session) {
+      if (!(options.session instanceof Session)) { throw new TypeError('`session` should be an instance of the Session class'); }
+      urlLoaderOptions.session = options.session;
+    } else if (options.partition) {
+      if (typeof options.partition === 'string') {
+        urlLoaderOptions.partition = options.partition;
+      } else {
+        throw new TypeError('`partition` should be a string');
+      }
     }
   }
   return urlLoaderOptions;
@@ -312,10 +316,6 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
   constructor (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
     super({ autoDestroy: true });
 
-    if (!app.isReady()) {
-      throw new Error('net module can only be used after app is ready');
-    }
-
     if (callback) {
       this.once('response', callback);
     }

+ 3 - 1
lib/utility/api/module-list.ts

@@ -1,2 +1,4 @@
 // Utility side modules, please sort alphabetically.
-export const utilityNodeModuleList: ElectronInternal.ModuleEntry[] = [];
+export const utilityNodeModuleList: ElectronInternal.ModuleEntry[] = [
+  { name: 'net', loader: () => require('./net') }
+];

+ 22 - 0
lib/utility/api/net.ts

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

+ 3 - 0
lib/utility/init.ts

@@ -1,3 +1,4 @@
+import { EventEmitter } from 'events';
 import { pathToFileURL } from 'url';
 
 import { ParentPort } from '@electron/internal/utility/parent-port';
@@ -15,6 +16,8 @@ require('../common/reset-search-paths');
 // Import common settings.
 require('@electron/internal/common/init');
 
+process._linkedBinding('electron_browser_event_emitter').setEventEmitterPrototype(EventEmitter.prototype);
+
 const parentPort: ParentPort = new ParentPort();
 Object.defineProperty(process, 'parentPort', {
   enumerable: true,

+ 2 - 2
package.json

@@ -6,11 +6,11 @@
   "devDependencies": {
     "@azure/storage-blob": "^12.9.0",
     "@electron/asar": "^3.2.1",
-    "@electron/docs-parser": "^1.1.1",
+    "@electron/docs-parser": "^1.2.0",
     "@electron/fiddle-core": "^1.0.4",
     "@electron/github-app-auth": "^2.0.0",
     "@electron/lint-roller": "^1.9.0",
-    "@electron/typescript-definitions": "^8.14.5",
+    "@electron/typescript-definitions": "^8.15.2",
     "@octokit/rest": "^19.0.7",
     "@primer/octicons": "^10.0.0",
     "@types/basic-auth": "^1.1.3",

+ 18 - 0
shell/browser/api/electron_api_utility_process.cc

@@ -13,6 +13,7 @@
 #include "base/process/kill.h"
 #include "base/process/launch.h"
 #include "base/process/process.h"
+#include "chrome/browser/browser_process.h"
 #include "content/public/browser/child_process_host.h"
 #include "content/public/browser/service_process_host.h"
 #include "content/public/common/result_codes.h"
@@ -22,6 +23,7 @@
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "shell/browser/api/message_port.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/file_path_converter.h"
 #include "shell/common/gin_helper/dictionary.h"
@@ -192,6 +194,22 @@ UtilityProcessWrapper::UtilityProcessWrapper(
   connector_->set_connection_error_handler(base::BindOnce(
       &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));
 }
 

+ 24 - 10
shell/browser/net/resolve_host_function.cc

@@ -15,7 +15,10 @@
 #include "net/base/net_errors.h"
 #include "net/base/network_isolation_key.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/common/process_util.h"
+#include "shell/services/node/node_service.h"
 #include "url/origin.h"
 
 using content::BrowserThread;
@@ -30,15 +33,17 @@ ResolveHostFunction::ResolveHostFunction(
     : browser_context_(browser_context),
       host_(std::move(host)),
       params_(std::move(params)),
-      callback_(std::move(callback)) {}
+      callback_(std::move(callback)) {
+  DETACH_FROM_SEQUENCE(sequence_checker_);
+}
 
 ResolveHostFunction::~ResolveHostFunction() {
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK(!receiver_.is_bound());
 }
 
 void ResolveHostFunction::Run() {
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK(!receiver_.is_bound());
 
   // Start the request.
@@ -50,12 +55,21 @@ void ResolveHostFunction::Run() {
       net::ResolveErrorInfo(net::ERR_FAILED),
       /*resolved_addresses=*/absl::nullopt,
       /*endpoint_results_with_metadata=*/absl::nullopt));
-  browser_context_->GetDefaultStoragePartition()
-      ->GetNetworkContext()
-      ->ResolveHost(network::mojom::HostResolverHost::NewHostPortPair(
-                        std::move(host_port_pair)),
-                    net::NetworkAnonymizationKey(), std::move(params_),
-                    std::move(resolve_host_client));
+  if (electron::IsUtilityProcess()) {
+    URLLoaderBundle::GetInstance()->GetHostResolver()->ResolveHost(
+        network::mojom::HostResolverHost::NewHostPortPair(
+            std::move(host_port_pair)),
+        net::NetworkAnonymizationKey(), std::move(params_),
+        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(
@@ -64,7 +78,7 @@ void ResolveHostFunction::OnComplete(
     const absl::optional<net::AddressList>& resolved_addresses,
     const absl::optional<net::HostResolverEndpointResults>&
         endpoint_results_with_metadata) {
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
   // Ensure that we outlive the `receiver_.reset()` call.
   scoped_refptr<ResolveHostFunction> self(this);

+ 3 - 0
shell/browser/net/resolve_host_function.h

@@ -9,6 +9,7 @@
 
 #include "base/memory/raw_ptr.h"
 #include "base/memory/ref_counted.h"
+#include "base/sequence_checker.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "net/base/address_list.h"
 #include "net/dns/public/host_resolver_results.h"
@@ -53,6 +54,8 @@ class ResolveHostFunction
                   const absl::optional<net::HostResolverEndpointResults>&
                       endpoint_results_with_metadata) override;
 
+  SEQUENCE_CHECKER(sequence_checker_);
+
   // Receiver for the currently in-progress request, if any.
   mojo::Receiver<network::mojom::ResolveHostClient> receiver_{this};
 

+ 38 - 3
shell/browser/api/electron_api_net.cc → shell/common/api/electron_api_net.cc

@@ -9,13 +9,16 @@
 #include "net/base/network_change_notifier.h"
 #include "net/http/http_util.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/gurl_converter.h"
+#include "shell/common/gin_converters/net_converter.h"
 #include "shell/common/gin_helper/dictionary.h"
 #include "shell/common/gin_helper/error_thrower.h"
 #include "shell/common/gin_helper/object_template_builder.h"
-
+#include "shell/common/gin_helper/promise.h"
 #include "shell/common/node_includes.h"
 
 namespace {
@@ -40,6 +43,37 @@ base::FilePath FileURLToFilePath(v8::Isolate* isolate, const GURL& url) {
   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;
 
 void Initialize(v8::Local<v8::Object> exports,
@@ -54,8 +88,9 @@ void Initialize(v8::Local<v8::Object> exports,
   dict.SetMethod("isValidHeaderValue", &IsValidHeaderValue);
   dict.SetMethod("createURLLoader", &SimpleURLLoaderWrapper::Create);
   dict.SetMethod("fileURLToFilePath", &FileURLToFilePath);
+  dict.SetMethod("resolveHost", &ResolveHost);
 }
 
 }  // namespace
 
-NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_net, Initialize)
+NODE_LINKED_BINDING_CONTEXT_AWARE(electron_common_net, Initialize)

+ 29 - 14
shell/browser/api/electron_api_url_loader.cc → shell/common/api/electron_api_url_loader.cc

@@ -2,7 +2,7 @@
 // Use of this source code is governed by the MIT license that can be
 // 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 <memory>
@@ -13,6 +13,7 @@
 #include "base/containers/fixed_flat_map.h"
 #include "base/memory/raw_ptr.h"
 #include "base/no_destructor.h"
+#include "base/sequence_checker.h"
 #include "gin/handle.h"
 #include "gin/object_template_builder.h"
 #include "gin/wrappable.h"
@@ -39,7 +40,10 @@
 #include "shell/common/gin_converters/net_converter.h"
 #include "shell/common/gin_helper/dictionary.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/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/mojom/fetch/fetch_api_request.mojom.h"
 
@@ -186,6 +190,7 @@ class JSChunkedDataPipeGetter : public gin::Wrappable<JSChunkedDataPipeGetter>,
       mojo::PendingReceiver<network::mojom::ChunkedDataPipeGetter>
           chunked_data_pipe_getter)
       : isolate_(isolate), body_func_(isolate, body_func) {
+    DETACH_FROM_SEQUENCE(sequence_checker_);
     receiver_.Bind(std::move(chunked_data_pipe_getter));
   }
 
@@ -195,7 +200,7 @@ class JSChunkedDataPipeGetter : public gin::Wrappable<JSChunkedDataPipeGetter>,
   }
 
   void StartReading(mojo::ScopedDataPipeProducerHandle pipe) override {
-    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
     if (body_func_.IsEmpty()) {
       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,
                             MojoResult result) {
-    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
     is_writing_ = false;
     if (result == MOJO_RESULT_OK) {
       promise.Resolve();
@@ -278,6 +283,7 @@ class JSChunkedDataPipeGetter : public gin::Wrappable<JSChunkedDataPipeGetter>,
     size_callback_.Reset();
   }
 
+  SEQUENCE_CHECKER(sequence_checker_);
   GetSizeCallback size_callback_;
   mojo::Receiver<network::mojom::ChunkedDataPipeGetter> receiver_{this};
   std::unique_ptr<mojo::DataPipeProducer> data_producer_;
@@ -320,6 +326,7 @@ SimpleURLLoaderWrapper::SimpleURLLoaderWrapper(
     : browser_context_(browser_context),
       request_options_(options),
       request_(std::move(request)) {
+  DETACH_FROM_SEQUENCE(sequence_checker_);
   if (!request_->trusted_params)
     request_->trusted_params = network::ResourceRequest::TrustedParams();
   mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver>
@@ -393,7 +400,7 @@ void SimpleURLLoaderWrapper::OnAuthRequired(
     const scoped_refptr<net::HttpResponseHeaders>& head_headers,
     mojo::PendingRemote<network::mojom::AuthChallengeResponder>
         auth_challenge_responder) {
-  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   mojo::Remote<network::mojom::AuthChallengeResponder> auth_responder(
       std::move(auth_challenge_responder));
   // WeakPtr because if we're Cancel()ed while waiting for auth, and the
@@ -462,6 +469,10 @@ void SimpleURLLoaderWrapper::Cancel() {
 }
 scoped_refptr<network::SharedURLLoaderFactory>
 SimpleURLLoaderWrapper::GetURLLoaderFactoryForURL(const GURL& url) {
+  if (electron::IsUtilityProcess()) {
+    return URLLoaderBundle::GetInstance()->GetSharedURLLoaderFactory();
+  }
+  CHECK(browser_context_);
   scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory;
   auto* protocol_registry =
       ProtocolRegistry::FromBrowserContext(browser_context_);
@@ -660,18 +671,22 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
     }
   }
 
-  std::string partition;
-  gin::Handle<Session> session;
-  if (!opts.Get("session", &session)) {
-    if (opts.Get("partition", &partition))
-      session = Session::FromPartition(args->isolate(), partition);
-    else  // default session
-      session = Session::FromPartition(args->isolate(), "");
+  ElectronBrowserContext* browser_context = nullptr;
+  if (electron::IsBrowserProcess()) {
+    std::string partition;
+    gin::Handle<Session> session;
+    if (!opts.Get("session", &session)) {
+      if (opts.Get("partition", &partition))
+        session = Session::FromPartition(args->isolate(), partition);
+      else  // default session
+        session = Session::FromPartition(args->isolate(), "");
+    }
+    browser_context = session->browser_context();
   }
 
   auto ret = gin::CreateHandle(
-      args->isolate(), new SimpleURLLoaderWrapper(session->browser_context(),
-                                                  std::move(request), options));
+      args->isolate(),
+      new SimpleURLLoaderWrapper(browser_context, std::move(request), options));
   ret->Pin();
   if (!chunk_pipe_getter.IsEmpty()) {
     ret->PinBodyGetter(chunk_pipe_getter);
@@ -681,7 +696,7 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
 
 void SimpleURLLoaderWrapper::OnDataReceived(base::StringPiece string_piece,
                                             base::OnceClosure resume) {
-  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
   v8::HandleScope handle_scope(isolate);
   auto array_buffer = v8::ArrayBuffer::New(isolate, string_piece.size());

+ 2 - 0
shell/browser/api/electron_api_url_loader.h → shell/common/api/electron_api_url_loader.h

@@ -11,6 +11,7 @@
 
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
+#include "base/sequence_checker.h"
 #include "gin/wrappable.h"
 #include "mojo/public/cpp/bindings/receiver_set.h"
 #include "net/base/auth.h"
@@ -133,6 +134,7 @@ class SimpleURLLoaderWrapper
   void Pin();
   void PinBodyGetter(v8::Local<v8::Value>);
 
+  SEQUENCE_CHECKER(sequence_checker_);
   raw_ptr<ElectronBrowserContext> browser_context_;
   int request_options_;
   std::unique_ptr<network::ResourceRequest> request_;

+ 6 - 3
shell/common/node_bindings.cc

@@ -57,7 +57,6 @@
   V(electron_browser_menu)               \
   V(electron_browser_message_port)       \
   V(electron_browser_native_theme)       \
-  V(electron_browser_net)                \
   V(electron_browser_notification)       \
   V(electron_browser_power_monitor)      \
   V(electron_browser_power_save_blocker) \
@@ -76,7 +75,8 @@
   V(electron_browser_web_contents_view)  \
   V(electron_browser_web_frame_main)     \
   V(electron_browser_web_view_manager)   \
-  V(electron_browser_window)
+  V(electron_browser_window)             \
+  V(electron_common_net)
 
 #define ELECTRON_COMMON_BINDINGS(V)   \
   V(electron_common_asar)             \
@@ -95,7 +95,10 @@
   V(electron_renderer_ipc)            \
   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_VIEWS_BINDINGS(V) V(electron_browser_image_view)
 

+ 36 - 0
shell/services/node/node_service.cc

@@ -7,7 +7,11 @@
 #include <utility>
 
 #include "base/command_line.h"
+#include "base/no_destructor.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/common/api/electron_bindings.h"
 #include "shell/common/gin_converters/file_path_converter.h"
@@ -18,6 +22,34 @@
 
 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(
     mojo::PendingReceiver<node::mojom::NodeService> receiver)
     : node_bindings_{NodeBindings::Create(
@@ -42,6 +74,10 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
 
   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());
 
   v8::HandleScope scope(js_env_->isolate());

+ 25 - 0
shell/services/node/node_service.h

@@ -8,7 +8,12 @@
 #include <memory>
 
 #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/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"
 
 namespace node {
@@ -23,6 +28,26 @@ class ElectronBindings;
 class JavascriptEnvironment;
 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 {
  public:
   explicit NodeService(

+ 4 - 0
shell/services/node/public/mojom/node_service.mojom

@@ -6,6 +6,8 @@ module node.mojom;
 
 import "mojo/public/mojom/base/file_path.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";
 
 struct NodeServiceParams {
@@ -13,6 +15,8 @@ struct NodeServiceParams {
   array<string> args;
   array<string> exec_args;
   blink.mojom.MessagePortDescriptor port;
+  pending_remote<network.mojom.URLLoaderFactory> url_loader_factory;
+  pending_remote<network.mojom.HostResolver> host_resolver;
 };
 
 [ServiceSandbox=sandbox.mojom.Sandbox.kNoSandbox]

+ 85 - 0
spec/api-net-custom-protocols-spec.ts

@@ -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 - 0
spec/api-net-session-spec.ts

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

File diff suppressed because it is too large
+ 411 - 992
spec/api-net-spec.ts


+ 49 - 0
spec/fixtures/api/utility-process/api-net-spec.js

@@ -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 - 0
spec/lib/net-helpers.ts

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

+ 2 - 1
typings/internal-ambient.d.ts

@@ -104,6 +104,7 @@ declare namespace NodeJS {
     Net: any;
     net: any;
     createURLLoader(options: CreateURLLoaderOptions): URLLoader;
+    resolveHost(host: string, options?: Electron.ResolveHostOptions): Promise<Electron.ResolvedHost>;
   }
 
   interface NotificationBinding {
@@ -212,6 +213,7 @@ declare namespace NodeJS {
     _linkedBinding(name: 'electron_common_environment'): EnvironmentBinding;
     _linkedBinding(name: 'electron_common_features'): FeaturesBinding;
     _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_v8_util'): V8UtilBinding;
     _linkedBinding(name: 'electron_browser_app'): { app: Electron.App, App: Function };
@@ -225,7 +227,6 @@ declare namespace NodeJS {
     _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_native_theme'): { nativeTheme: Electron.NativeTheme };
-    _linkedBinding(name: 'electron_browser_net'): NetBinding;
     _linkedBinding(name: 'electron_browser_notification'): NotificationBinding;
     _linkedBinding(name: 'electron_browser_power_monitor'): PowerMonitorBinding;
     _linkedBinding(name: 'electron_browser_power_save_blocker'): { powerSaveBlocker: Electron.PowerSaveBlocker };

+ 8 - 8
yarn.lock

@@ -146,10 +146,10 @@
   optionalDependencies:
     "@types/glob" "^7.1.1"
 
-"@electron/docs-parser@^1.1.1":
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/@electron/docs-parser/-/docs-parser-1.1.1.tgz#14bec2940f81f4debb95a2b0f186f7f00d682899"
-  integrity sha512-IB6XCDaNTHqm7h0Joa1LUtZhmBvItRWp0KUNuNtuXLEv/6q6ZYw9wn89QzlFYxgH8ZTleF9dWpJby0mIpsX0Ng==
+"@electron/docs-parser@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@electron/docs-parser/-/docs-parser-1.2.0.tgz#dc3032012dd270c667777e097e185d92e7ff86ef"
+  integrity sha512-Rz/lMLRDSvEshYNmSC30v/3rk7Mj6EL/76wraKvfM5XvYPHsmApo9CedvcJNNMm7+Rc29NOohoqA4B2/XtFm1Q==
   dependencies:
     "@types/markdown-it" "^12.0.0"
     chai "^4.2.0"
@@ -219,10 +219,10 @@
     vscode-languageserver-textdocument "^1.0.8"
     vscode-uri "^3.0.7"
 
-"@electron/typescript-definitions@^8.14.5":
-  version "8.14.7"
-  resolved "https://registry.yarnpkg.com/@electron/typescript-definitions/-/typescript-definitions-8.14.7.tgz#f8838eac200fa8106ce0a6b7044463b0fd86e9b6"
-  integrity sha512-y1kOB9Ckkd09+KpNDIID6LHO7WP69WoMiogGwNIRtBnEZIeK/aN5uy6plEF1OXYoJVkRKkM/ZSBhhwhk4H1rEA==
+"@electron/typescript-definitions@^8.15.2":
+  version "8.15.3"
+  resolved "https://registry.yarnpkg.com/@electron/typescript-definitions/-/typescript-definitions-8.15.3.tgz#cbdac770b671c841deeb57a9587519d3d7f75d6d"
+  integrity sha512-Cm9LWnEsdOto67zVmGM48lcAbbeUZULElv2Ai5rBBg/M6oJhb03m/ZVImMLuevekQVPGJNLxV6Ygd9o/2arx4Q==
   dependencies:
     "@types/node" "^11.13.7"
     chalk "^2.4.2"

Some files were not shown because too many files changed in this diff