feat: add protocol.handle (#36674)

This commit is contained in:
Jeremy Rose 2023-03-27 10:00:55 -07:00 committed by GitHub
parent 6a6908c4c8
commit fda8ea9277
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1254 additions and 89 deletions

View file

@ -243,6 +243,8 @@ it is not allowed to add or remove a custom header.
* `encoding` string (optional) * `encoding` string (optional)
* `callback` Function (optional) * `callback` Function (optional)
Returns `this`.
Sends the last chunk of the request data. Subsequent write or end operations Sends the last chunk of the request data. Subsequent write or end operations
will not be allowed. The `finish` event is emitted just after the end operation. will not be allowed. The `finish` event is emitted just after the end operation.

View file

@ -65,8 +65,8 @@ requests according to the specified protocol scheme in the `options` object.
### `net.fetch(input[, init])` ### `net.fetch(input[, init])`
* `input` string | [Request](https://nodejs.org/api/globals.html#request) * `input` string | [GlobalRequest](https://nodejs.org/api/globals.html#request)
* `init` [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options) (optional) * `init` [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options) & { bypassCustomProtocolHandlers?: boolean } (optional)
Returns `Promise<GlobalResponse>` - see [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). Returns `Promise<GlobalResponse>` - see [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response).
@ -101,9 +101,23 @@ Limitations:
* The `.type` and `.url` values of the returned `Response` object are * The `.type` and `.url` values of the returned `Response` object are
incorrect. incorrect.
Requests made with `net.fetch` can be made to [custom protocols](protocol.md) By default, requests made with `net.fetch` can be made to [custom
as well as `file:`, and will trigger [webRequest](web-request.md) handlers if protocols](protocol.md) as well as `file:`, and will trigger
present. [webRequest](web-request.md) handlers if present. When the non-standard
`bypassCustomProtocolHandlers` option is set in RequestInit, custom protocol
handlers will not be called for this request. This allows forwarding an
intercepted request to the built-in handler. [webRequest](web-request.md)
handlers will still be triggered when bypassing custom protocols.
```js
protocol.handle('https', (req) => {
if (req.url === 'https://my-app.com') {
return new Response('<body>my app</body>')
} else {
return net.fetch(req, { bypassCustomProtocolHandlers: true })
}
})
```
### `net.isOnline()` ### `net.isOnline()`

View file

@ -8,15 +8,11 @@ An example of implementing a protocol that has the same effect as the
`file://` protocol: `file://` protocol:
```javascript ```javascript
const { app, protocol } = require('electron') const { app, protocol, net } = require('electron')
const path = require('path')
const url = require('url')
app.whenReady().then(() => { app.whenReady().then(() => {
protocol.registerFileProtocol('atom', (request, callback) => { protocol.handle('atom', (request) =>
const filePath = url.fileURLToPath('file://' + request.url.slice('atom://'.length)) net.fetch('file://' + request.url.slice('atom://'.length)))
callback(filePath)
})
}) })
``` ```
@ -38,14 +34,15 @@ to register it to that session explicitly.
```javascript ```javascript
const { session, app, protocol } = require('electron') const { session, app, protocol } = require('electron')
const path = require('path') const path = require('path')
const url = require('url')
app.whenReady().then(() => { app.whenReady().then(() => {
const partition = 'persist:example' const partition = 'persist:example'
const ses = session.fromPartition(partition) const ses = session.fromPartition(partition)
ses.protocol.registerFileProtocol('atom', (request, callback) => { ses.protocol.handle('atom', (request) => {
const url = request.url.substr(7) const path = request.url.slice('atom://'.length)
callback({ path: path.normalize(`${__dirname}/${url}`) }) return net.fetch(url.pathToFileURL(path.join(__dirname, path)))
}) })
mainWindow = new BrowserWindow({ webPreferences: { partition } }) mainWindow = new BrowserWindow({ webPreferences: { partition } })
@ -109,7 +106,74 @@ The `<video>` and `<audio>` HTML elements expect protocols to buffer their
responses by default. The `stream` flag configures those elements to correctly responses by default. The `stream` flag configures those elements to correctly
expect streaming responses. expect streaming responses.
### `protocol.registerFileProtocol(scheme, handler)` ### `protocol.handle(scheme, handler)`
* `scheme` string - scheme to handle, for example `https` or `my-app`. This is
the bit before the `:` in a URL.
* `handler` Function<[GlobalResponse](https://nodejs.org/api/globals.html#response) | Promise<GlobalResponse>>
* `request` [GlobalRequest](https://nodejs.org/api/globals.html#request)
Register a protocol handler for `scheme`. Requests made to URLs with this
scheme will delegate to this handler to determine what response should be sent.
Either a `Response` or a `Promise<Response>` can be returned.
Example:
```js
import { app, protocol } from 'electron'
import { join } from 'path'
import { pathToFileURL } from 'url'
protocol.registerSchemesAsPrivileged([
{
scheme: 'app',
privileges: {
standard: true,
secure: true,
supportsFetchAPI: true
}
}
])
app.whenReady().then(() => {
protocol.handle('app', (req) => {
const { host, pathname } = new URL(req.url)
if (host === 'bundle') {
if (pathname === '/') {
return new Response('<h1>hello, world</h1>', {
headers: { 'content-type': 'text/html' }
})
}
// NB, this does not check for paths that escape the bundle, e.g.
// app://bundle/../../secret_file.txt
return net.fetch(pathToFileURL(join(__dirname, pathname)))
} else if (host === 'api') {
return net.fetch('https://api.my-server.com/' + pathname, {
method: req.method,
headers: req.headers,
body: req.body
})
}
})
})
```
See the MDN docs for [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) for more details.
### `protocol.unhandle(scheme)`
* `scheme` string - scheme for which to remove the handler.
Removes a protocol handler registered with `protocol.handle`.
### `protocol.isProtocolHandled(scheme)`
* `scheme` string
Returns `boolean` - Whether `scheme` is already handled.
### `protocol.registerFileProtocol(scheme, handler)` _Deprecated_
* `scheme` string * `scheme` string
* `handler` Function * `handler` Function
@ -130,7 +194,7 @@ path or an object that has a `path` property, e.g. `callback(filePath)` or
By default the `scheme` is treated like `http:`, which is parsed differently By default the `scheme` is treated like `http:`, which is parsed differently
from protocols that follow the "generic URI syntax" like `file:`. from protocols that follow the "generic URI syntax" like `file:`.
### `protocol.registerBufferProtocol(scheme, handler)` ### `protocol.registerBufferProtocol(scheme, handler)` _Deprecated_
* `scheme` string * `scheme` string
* `handler` Function * `handler` Function
@ -154,7 +218,7 @@ protocol.registerBufferProtocol('atom', (request, callback) => {
}) })
``` ```
### `protocol.registerStringProtocol(scheme, handler)` ### `protocol.registerStringProtocol(scheme, handler)` _Deprecated_
* `scheme` string * `scheme` string
* `handler` Function * `handler` Function
@ -170,7 +234,7 @@ The usage is the same with `registerFileProtocol`, except that the `callback`
should be called with either a `string` or an object that has the `data` should be called with either a `string` or an object that has the `data`
property. property.
### `protocol.registerHttpProtocol(scheme, handler)` ### `protocol.registerHttpProtocol(scheme, handler)` _Deprecated_
* `scheme` string * `scheme` string
* `handler` Function * `handler` Function
@ -185,7 +249,7 @@ Registers a protocol of `scheme` that will send an HTTP request as a response.
The usage is the same with `registerFileProtocol`, except that the `callback` The usage is the same with `registerFileProtocol`, except that the `callback`
should be called with an object that has the `url` property. should be called with an object that has the `url` property.
### `protocol.registerStreamProtocol(scheme, handler)` ### `protocol.registerStreamProtocol(scheme, handler)` _Deprecated_
* `scheme` string * `scheme` string
* `handler` Function * `handler` Function
@ -234,7 +298,7 @@ protocol.registerStreamProtocol('atom', (request, callback) => {
}) })
``` ```
### `protocol.unregisterProtocol(scheme)` ### `protocol.unregisterProtocol(scheme)` _Deprecated_
* `scheme` string * `scheme` string
@ -242,13 +306,13 @@ Returns `boolean` - Whether the protocol was successfully unregistered
Unregisters the custom protocol of `scheme`. Unregisters the custom protocol of `scheme`.
### `protocol.isProtocolRegistered(scheme)` ### `protocol.isProtocolRegistered(scheme)` _Deprecated_
* `scheme` string * `scheme` string
Returns `boolean` - Whether `scheme` is already registered. Returns `boolean` - Whether `scheme` is already registered.
### `protocol.interceptFileProtocol(scheme, handler)` ### `protocol.interceptFileProtocol(scheme, handler)` _Deprecated_
* `scheme` string * `scheme` string
* `handler` Function * `handler` Function
@ -261,7 +325,7 @@ Returns `boolean` - Whether the protocol was successfully intercepted
Intercepts `scheme` protocol and uses `handler` as the protocol's new handler Intercepts `scheme` protocol and uses `handler` as the protocol's new handler
which sends a file as a response. which sends a file as a response.
### `protocol.interceptStringProtocol(scheme, handler)` ### `protocol.interceptStringProtocol(scheme, handler)` _Deprecated_
* `scheme` string * `scheme` string
* `handler` Function * `handler` Function
@ -274,7 +338,7 @@ Returns `boolean` - Whether the protocol was successfully intercepted
Intercepts `scheme` protocol and uses `handler` as the protocol's new handler Intercepts `scheme` protocol and uses `handler` as the protocol's new handler
which sends a `string` as a response. which sends a `string` as a response.
### `protocol.interceptBufferProtocol(scheme, handler)` ### `protocol.interceptBufferProtocol(scheme, handler)` _Deprecated_
* `scheme` string * `scheme` string
* `handler` Function * `handler` Function
@ -287,7 +351,7 @@ Returns `boolean` - Whether the protocol was successfully intercepted
Intercepts `scheme` protocol and uses `handler` as the protocol's new handler Intercepts `scheme` protocol and uses `handler` as the protocol's new handler
which sends a `Buffer` as a response. which sends a `Buffer` as a response.
### `protocol.interceptHttpProtocol(scheme, handler)` ### `protocol.interceptHttpProtocol(scheme, handler)` _Deprecated_
* `scheme` string * `scheme` string
* `handler` Function * `handler` Function
@ -300,7 +364,7 @@ Returns `boolean` - Whether the protocol was successfully intercepted
Intercepts `scheme` protocol and uses `handler` as the protocol's new handler Intercepts `scheme` protocol and uses `handler` as the protocol's new handler
which sends a new HTTP request as a response. which sends a new HTTP request as a response.
### `protocol.interceptStreamProtocol(scheme, handler)` ### `protocol.interceptStreamProtocol(scheme, handler)` _Deprecated_
* `scheme` string * `scheme` string
* `handler` Function * `handler` Function
@ -313,7 +377,7 @@ Returns `boolean` - Whether the protocol was successfully intercepted
Same as `protocol.registerStreamProtocol`, except that it replaces an existing Same as `protocol.registerStreamProtocol`, except that it replaces an existing
protocol handler. protocol handler.
### `protocol.uninterceptProtocol(scheme)` ### `protocol.uninterceptProtocol(scheme)` _Deprecated_
* `scheme` string * `scheme` string
@ -321,7 +385,7 @@ Returns `boolean` - Whether the protocol was successfully unintercepted
Remove the interceptor installed for `scheme` and restore its original handler. Remove the interceptor installed for `scheme` and restore its original handler.
### `protocol.isProtocolIntercepted(scheme)` ### `protocol.isProtocolIntercepted(scheme)` _Deprecated_
* `scheme` string * `scheme` string

View file

@ -784,9 +784,23 @@ Limitations:
* The `.type` and `.url` values of the returned `Response` object are * The `.type` and `.url` values of the returned `Response` object are
incorrect. incorrect.
Requests made with `ses.fetch` can be made to [custom protocols](protocol.md) By default, requests made with `net.fetch` can be made to [custom
as well as `file:`, and will trigger [webRequest](web-request.md) handlers if protocols](protocol.md) as well as `file:`, and will trigger
present. [webRequest](web-request.md) handlers if present. When the non-standard
`bypassCustomProtocolHandlers` option is set in RequestInit, custom protocol
handlers will not be called for this request. This allows forwarding an
intercepted request to the built-in handler. [webRequest](web-request.md)
handlers will still be triggered when bypassing custom protocols.
```js
protocol.handle('https', (req) => {
if (req.url === 'https://my-app.com') {
return new Response('<body>my app</body>')
} else {
return net.fetch(req, { bypassCustomProtocolHandlers: true })
}
})
```
#### `ses.disableNetworkEmulation()` #### `ses.disableNetworkEmulation()`

View file

@ -2,8 +2,8 @@
* `type` 'file' - `file`. * `type` 'file' - `file`.
* `filePath` string - Path of file to be uploaded. * `filePath` string - Path of file to be uploaded.
* `offset` Integer - Defaults to `0`. * `offset` Integer (optional) - Defaults to `0`.
* `length` Integer - Number of bytes to read from `offset`. * `length` Integer (optional) - Number of bytes to read from `offset`.
Defaults to `0`. Defaults to `0`.
* `modificationTime` Double - Last Modification time in * `modificationTime` Double (optional) - Last Modification time in
number of seconds since the UNIX epoch. number of seconds since the UNIX epoch. Defaults to `0`.

View file

@ -12,6 +12,55 @@ This document uses the following convention to categorize breaking changes:
* **Deprecated:** An API was marked as deprecated. The API will continue to function, but will emit a deprecation warning, and will be removed in a future release. * **Deprecated:** An API was marked as deprecated. The API will continue to function, but will emit a deprecation warning, and will be removed in a future release.
* **Removed:** An API or feature was removed, and is no longer supported by Electron. * **Removed:** An API or feature was removed, and is no longer supported by Electron.
## Planned Breaking API Changes (25.0)
### Deprecated: `protocol.{register,intercept}{Buffer,String,Stream,File,Http}Protocol`
The `protocol.register*Protocol` and `protocol.intercept*Protocol` methods have
been replaced with [`protocol.handle`](api/protocol.md#protocolhandlescheme-handler).
The new method can either register a new protocol or intercept an existing
protocol, and responses can be of any type.
```js
// Deprecated in Electron 25
protocol.registerBufferProtocol('some-protocol', () => {
callback({ mimeType: 'text/html', data: Buffer.from('<h5>Response</h5>') })
})
// Replace with
protocol.handle('some-protocol', () => {
return new Response(
Buffer.from('<h5>Response</h5>'), // Could also be a string or ReadableStream.
{ headers: { 'content-type': 'text/html' } }
)
})
```
```js
// Deprecated in Electron 25
protocol.registerHttpProtocol('some-protocol', () => {
callback({ url: 'https://electronjs.org' })
})
// Replace with
protocol.handle('some-protocol', () => {
return net.fetch('https://electronjs.org')
})
```
```js
// Deprecated in Electron 25
protocol.registerFileProtocol('some-protocol', () => {
callback({ filePath: '/path/to/my/file' })
})
// Replace with
protocol.handle('some-protocol', () => {
return net.fetch('file:///path/to/my/file')
})
```
## Planned Breaking API Changes (24.0) ## Planned Breaking API Changes (24.0)
### API Changed: `nativeImage.createThumbnailFromPath(path, size)` ### API Changed: `nativeImage.createThumbnailFromPath(path, size)`

View file

@ -13,7 +13,7 @@ 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 | undefined, session: SessionT): Promise<Response> { export function fetchWithSession (input: RequestInfo, init: (RequestInit & {bypassCustomProtocolHandlers?: boolean}) | undefined, session: SessionT): Promise<Response> {
const p = createDeferredPromise<Response>(); const p = createDeferredPromise<Response>();
let req: Request; let req: Request;
try { try {
@ -84,6 +84,8 @@ export function fetchWithSession (input: RequestInfo, init: RequestInit | undefi
redirect: req.redirect redirect: req.redirect
})); }));
(r as any)._urlLoaderOptions.bypassCustomProtocolHandlers = !!init?.bypassCustomProtocolHandlers;
// cors is the default mode, but we can't set mode=cors without an origin. // cors is the default mode, but we can't set mode=cors without an origin.
if (req.mode && (req.mode !== 'cors' || origin)) { if (req.mode && (req.mode !== 'cors' || origin)) {
r.setHeader('Sec-Fetch-Mode', req.mode); r.setHeader('Sec-Fetch-Mode', req.mode);
@ -104,6 +106,7 @@ export function fetchWithSession (input: RequestInfo, init: RequestInit | undefi
status: resp.statusCode, status: resp.statusCode,
statusText: resp.statusMessage statusText: resp.statusMessage
}); });
(rResp as any).__original_resp = resp;
p.resolve(rResp); p.resolve(rResp);
}); });

View file

@ -1,33 +1,130 @@
import { app, session } from 'electron/main'; import { ProtocolRequest, session } from 'electron/main';
import { createReadStream } from 'fs';
import { Readable } from 'stream';
import { ReadableStream } from 'stream/web';
// Global protocol APIs. // Global protocol APIs.
const protocol = process._linkedBinding('electron_browser_protocol'); const { registerSchemesAsPrivileged, getStandardSchemes, Protocol } = process._linkedBinding('electron_browser_protocol');
// Fallback protocol APIs of default session. const ERR_FAILED = -2;
Object.setPrototypeOf(protocol, new Proxy({}, { const ERR_UNEXPECTED = -9;
get (_target, property) {
if (!app.isReady()) return;
const protocol = session.defaultSession!.protocol; const isBuiltInScheme = (scheme: string) => scheme === 'http' || scheme === 'https';
if (!Object.prototype.hasOwnProperty.call(protocol, property)) return;
// Returning a native function directly would throw error. function makeStreamFromPipe (pipe: any): ReadableStream {
return (...args: any[]) => (protocol[property as keyof Electron.Protocol] as Function)(...args); const buf = new Uint8Array(1024 * 1024 /* 1 MB */);
}, return new ReadableStream({
async pull (controller) {
try {
const rv = await pipe.read(buf);
if (rv > 0) {
controller.enqueue(buf.subarray(0, rv));
} else {
controller.close();
}
} catch (e) {
controller.error(e);
}
}
});
}
ownKeys () { function convertToRequestBody (uploadData: ProtocolRequest['uploadData']): RequestInit['body'] {
if (!app.isReady()) return []; if (!uploadData) return null;
return Reflect.ownKeys(session.defaultSession!.protocol); // Optimization: skip creating a stream if the request is just a single buffer.
}, if (uploadData.length === 1 && (uploadData[0] as any).type === 'rawData') return uploadData[0].bytes;
has: (target, property: string) => { const chunks = [...uploadData] as any[]; // TODO: types are wrong
if (!app.isReady()) return false; let current: ReadableStreamDefaultReader | null = null;
return Reflect.has(session.defaultSession!.protocol, property); return new ReadableStream({
}, pull (controller) {
if (current) {
current.read().then(({ done, value }) => {
controller.enqueue(value);
if (done) current = null;
}, (err) => {
controller.error(err);
});
} else {
if (!chunks.length) { return controller.close(); }
const chunk = chunks.shift()!;
if (chunk.type === 'rawData') { controller.enqueue(chunk.bytes); } else if (chunk.type === 'file') {
current = Readable.toWeb(createReadStream(chunk.filePath, { start: chunk.offset ?? 0, end: chunk.length >= 0 ? chunk.offset + chunk.length : undefined })).getReader();
this.pull!(controller);
} else if (chunk.type === 'stream') {
current = makeStreamFromPipe(chunk.body).getReader();
this.pull!(controller);
}
}
}
}) as RequestInit['body'];
}
getOwnPropertyDescriptor () { Protocol.prototype.handle = function (this: Electron.Protocol, scheme: string, handler: (req: Request) => Response | Promise<Response>) {
return { configurable: true, enumerable: true }; const register = isBuiltInScheme(scheme) ? this.interceptProtocol : this.registerProtocol;
} const success = register.call(this, scheme, async (preq: ProtocolRequest, cb: any) => {
})); try {
const body = convertToRequestBody(preq.uploadData);
const req = new Request(preq.url, {
headers: preq.headers,
method: preq.method,
referrer: preq.referrer,
body,
duplex: body instanceof ReadableStream ? 'half' : undefined
} as any);
const res = await handler(req);
if (!res || typeof res !== 'object') {
return cb({ error: ERR_UNEXPECTED });
}
if (res.type === 'error') { cb({ error: ERR_FAILED }); } else {
cb({
data: res.body ? Readable.fromWeb(res.body as ReadableStream<ArrayBufferView>) : null,
headers: Object.fromEntries(res.headers),
statusCode: res.status,
statusText: res.statusText,
mimeType: (res as any).__original_resp?._responseHead?.mimeType
});
}
} catch (e) {
console.error(e);
cb({ error: ERR_UNEXPECTED });
}
});
if (!success) throw new Error(`Failed to register protocol: ${scheme}`);
};
Protocol.prototype.unhandle = function (this: Electron.Protocol, scheme: string) {
const unregister = isBuiltInScheme(scheme) ? this.uninterceptProtocol : this.unregisterProtocol;
if (!unregister.call(this, scheme)) { throw new Error(`Failed to unhandle protocol: ${scheme}`); }
};
Protocol.prototype.isProtocolHandled = function (this: Electron.Protocol, scheme: string) {
const isRegistered = isBuiltInScheme(scheme) ? this.isProtocolIntercepted : this.isProtocolRegistered;
return isRegistered.call(this, scheme);
};
const protocol = {
registerSchemesAsPrivileged,
getStandardSchemes,
registerStringProtocol: (...args) => session.defaultSession.protocol.registerStringProtocol(...args),
registerBufferProtocol: (...args) => session.defaultSession.protocol.registerBufferProtocol(...args),
registerStreamProtocol: (...args) => session.defaultSession.protocol.registerStreamProtocol(...args),
registerFileProtocol: (...args) => session.defaultSession.protocol.registerFileProtocol(...args),
registerHttpProtocol: (...args) => session.defaultSession.protocol.registerHttpProtocol(...args),
registerProtocol: (...args) => session.defaultSession.protocol.registerProtocol(...args),
unregisterProtocol: (...args) => session.defaultSession.protocol.unregisterProtocol(...args),
isProtocolRegistered: (...args) => session.defaultSession.protocol.isProtocolRegistered(...args),
interceptStringProtocol: (...args) => session.defaultSession.protocol.interceptStringProtocol(...args),
interceptBufferProtocol: (...args) => session.defaultSession.protocol.interceptBufferProtocol(...args),
interceptStreamProtocol: (...args) => session.defaultSession.protocol.interceptStreamProtocol(...args),
interceptFileProtocol: (...args) => session.defaultSession.protocol.interceptFileProtocol(...args),
interceptHttpProtocol: (...args) => session.defaultSession.protocol.interceptHttpProtocol(...args),
interceptProtocol: (...args) => session.defaultSession.protocol.interceptProtocol(...args),
uninterceptProtocol: (...args) => session.defaultSession.protocol.uninterceptProtocol(...args),
isProtocolIntercepted: (...args) => session.defaultSession.protocol.isProtocolIntercepted(...args),
handle: (...args) => session.defaultSession.protocol.handle(...args),
unhandle: (...args) => session.defaultSession.protocol.unhandle(...args),
isProtocolHandled: (...args) => session.defaultSession.protocol.isProtocolHandled(...args)
} as typeof Electron.protocol;
export default protocol; export default protocol;

View file

@ -124,5 +124,6 @@ chore_introduce_blocking_api_for_electron.patch
chore_patch_out_partition_attribute_dcheck_for_webviews.patch chore_patch_out_partition_attribute_dcheck_for_webviews.patch
expose_v8initializer_codegenerationcheckcallbackinmainthread.patch expose_v8initializer_codegenerationcheckcallbackinmainthread.patch
chore_patch_out_profile_methods_in_profile_selections_cc.patch chore_patch_out_profile_methods_in_profile_selections_cc.patch
add_gin_converter_support_for_arraybufferview.patch
chore_defer_usb_service_getdevices_request_until_usb_service_is.patch chore_defer_usb_service_getdevices_request_until_usb_service_is.patch
revert_roll_clang_rust_llvmorg-16-init-17653-g39da55e8-3.patch revert_roll_clang_rust_llvmorg-16-init-17653-g39da55e8-3.patch

View file

@ -0,0 +1,60 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jeremy Rose <japthorp@slack-corp.com>
Date: Wed, 8 Mar 2023 14:53:17 -0800
Subject: add gin::Converter support for ArrayBufferView
This should be upstreamed.
diff --git a/gin/converter.cc b/gin/converter.cc
index 4eb8c3d8c8392512eeb235bc18012589549b872b..d0432f6fff09cdcebed55ccf03a6524a445ef346 100644
--- a/gin/converter.cc
+++ b/gin/converter.cc
@@ -18,6 +18,7 @@
#include "v8/include/v8-value.h"
using v8::ArrayBuffer;
+using v8::ArrayBufferView;
using v8::External;
using v8::Function;
using v8::Int32;
@@ -244,6 +245,20 @@ bool Converter<Local<ArrayBuffer>>::FromV8(Isolate* isolate,
return true;
}
+Local<Value> Converter<Local<ArrayBufferView>>::ToV8(Isolate* isolate,
+ Local<ArrayBufferView> val) {
+ return val.As<Value>();
+}
+
+bool Converter<Local<ArrayBufferView>>::FromV8(Isolate* isolate,
+ Local<Value> val,
+ Local<ArrayBufferView>* out) {
+ if (!val->IsArrayBufferView())
+ return false;
+ *out = Local<ArrayBufferView>::Cast(val);
+ return true;
+}
+
Local<Value> Converter<Local<External>>::ToV8(Isolate* isolate,
Local<External> val) {
return val.As<Value>();
diff --git a/gin/converter.h b/gin/converter.h
index eb704fcd56dee861e18e9cd64a857d68dea6f415..d32a8c26403cf32f3333ed85c23292915e6f0681 100644
--- a/gin/converter.h
+++ b/gin/converter.h
@@ -180,6 +180,15 @@ struct GIN_EXPORT Converter<v8::Local<v8::ArrayBuffer> > {
v8::Local<v8::ArrayBuffer>* out);
};
+template<>
+struct GIN_EXPORT Converter<v8::Local<v8::ArrayBufferView> > {
+ static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
+ v8::Local<v8::ArrayBufferView> val);
+ static bool FromV8(v8::Isolate* isolate,
+ v8::Local<v8::Value> val,
+ v8::Local<v8::ArrayBufferView>* out);
+};
+
template<>
struct GIN_EXPORT Converter<v8::Local<v8::External> > {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,

View file

@ -273,9 +273,17 @@ gin::Handle<Protocol> Protocol::Create(
isolate, new Protocol(isolate, browser_context->protocol_registry())); isolate, new Protocol(isolate, browser_context->protocol_registry()));
} }
gin::ObjectTemplateBuilder Protocol::GetObjectTemplateBuilder( // static
v8::Isolate* isolate) { gin::Handle<Protocol> Protocol::New(gin_helper::ErrorThrower thrower) {
return gin::Wrappable<Protocol>::GetObjectTemplateBuilder(isolate) thrower.ThrowError("Protocol cannot be created from JS");
return gin::Handle<Protocol>();
}
// static
v8::Local<v8::ObjectTemplate> Protocol::FillObjectTemplate(
v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> tmpl) {
return gin::ObjectTemplateBuilder(isolate, "Protocol", tmpl)
.SetMethod("registerStringProtocol", .SetMethod("registerStringProtocol",
&Protocol::RegisterProtocolFor<ProtocolType::kString>) &Protocol::RegisterProtocolFor<ProtocolType::kString>)
.SetMethod("registerBufferProtocol", .SetMethod("registerBufferProtocol",
@ -304,7 +312,8 @@ gin::ObjectTemplateBuilder Protocol::GetObjectTemplateBuilder(
.SetMethod("interceptProtocol", .SetMethod("interceptProtocol",
&Protocol::InterceptProtocolFor<ProtocolType::kFree>) &Protocol::InterceptProtocolFor<ProtocolType::kFree>)
.SetMethod("uninterceptProtocol", &Protocol::UninterceptProtocol) .SetMethod("uninterceptProtocol", &Protocol::UninterceptProtocol)
.SetMethod("isProtocolIntercepted", &Protocol::IsProtocolIntercepted); .SetMethod("isProtocolIntercepted", &Protocol::IsProtocolIntercepted)
.Build();
} }
const char* Protocol::GetTypeName() { const char* Protocol::GetTypeName() {
@ -333,6 +342,7 @@ void Initialize(v8::Local<v8::Object> exports,
void* priv) { void* priv) {
v8::Isolate* isolate = context->GetIsolate(); v8::Isolate* isolate = context->GetIsolate();
gin_helper::Dictionary dict(isolate, exports); gin_helper::Dictionary dict(isolate, exports);
dict.Set("Protocol", electron::api::Protocol::GetConstructor(context));
dict.SetMethod("registerSchemesAsPrivileged", &RegisterSchemesAsPrivileged); dict.SetMethod("registerSchemesAsPrivileged", &RegisterSchemesAsPrivileged);
dict.SetMethod("getStandardSchemes", &electron::api::GetStandardSchemes); dict.SetMethod("getStandardSchemes", &electron::api::GetStandardSchemes);
} }

View file

@ -12,6 +12,7 @@
#include "gin/handle.h" #include "gin/handle.h"
#include "gin/wrappable.h" #include "gin/wrappable.h"
#include "shell/browser/net/electron_url_loader_factory.h" #include "shell/browser/net/electron_url_loader_factory.h"
#include "shell/common/gin_helper/constructible.h"
namespace electron { namespace electron {
@ -37,15 +38,19 @@ enum class ProtocolError {
}; };
// Protocol implementation based on network services. // Protocol implementation based on network services.
class Protocol : public gin::Wrappable<Protocol> { class Protocol : public gin::Wrappable<Protocol>,
public gin_helper::Constructible<Protocol> {
public: public:
static gin::Handle<Protocol> Create(v8::Isolate* isolate, static gin::Handle<Protocol> Create(v8::Isolate* isolate,
ElectronBrowserContext* browser_context); ElectronBrowserContext* browser_context);
static gin::Handle<Protocol> New(gin_helper::ErrorThrower thrower);
// gin::Wrappable // gin::Wrappable
static gin::WrapperInfo kWrapperInfo; static gin::WrapperInfo kWrapperInfo;
gin::ObjectTemplateBuilder GetObjectTemplateBuilder( static v8::Local<v8::ObjectTemplate> FillObjectTemplate(
v8::Isolate* isolate) override; v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> tmpl);
const char* GetTypeName() override; const char* GetTypeName() override;
private: private:

View file

@ -30,6 +30,7 @@
#include "shell/browser/electron_browser_context.h" #include "shell/browser/electron_browser_context.h"
#include "shell/browser/javascript_environment.h" #include "shell/browser/javascript_environment.h"
#include "shell/browser/net/asar/asar_url_loader_factory.h" #include "shell/browser/net/asar/asar_url_loader_factory.h"
#include "shell/browser/net/proxying_url_loader_factory.h"
#include "shell/browser/protocol_registry.h" #include "shell/browser/protocol_registry.h"
#include "shell/common/gin_converters/callback_converter.h" #include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/gurl_converter.h" #include "shell/common/gin_converters/gurl_converter.h"
@ -488,7 +489,10 @@ SimpleURLLoaderWrapper::GetURLLoaderFactoryForURL(const GURL& url) {
// Explicitly handle intercepted protocols here, even though // Explicitly handle intercepted protocols here, even though
// ProxyingURLLoaderFactory would handle them later on, so that we can // ProxyingURLLoaderFactory would handle them later on, so that we can
// correctly intercept file:// scheme URLs. // correctly intercept file:// scheme URLs.
if (protocol_registry->IsProtocolIntercepted(url.scheme())) { bool bypass_custom_protocol_handlers =
request_options_ & kBypassCustomProtocolHandlers;
if (!bypass_custom_protocol_handlers &&
protocol_registry->IsProtocolIntercepted(url.scheme())) {
auto& protocol_handler = auto& protocol_handler =
protocol_registry->intercept_handlers().at(url.scheme()); protocol_registry->intercept_handlers().at(url.scheme());
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote = mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote =
@ -497,7 +501,8 @@ SimpleURLLoaderWrapper::GetURLLoaderFactoryForURL(const GURL& url) {
url_loader_factory = network::SharedURLLoaderFactory::Create( url_loader_factory = network::SharedURLLoaderFactory::Create(
std::make_unique<network::WrapperPendingSharedURLLoaderFactory>( std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
std::move(pending_remote))); std::move(pending_remote)));
} else if (protocol_registry->IsProtocolRegistered(url.scheme())) { } else if (!bypass_custom_protocol_handlers &&
protocol_registry->IsProtocolRegistered(url.scheme())) {
auto& protocol_handler = protocol_registry->handlers().at(url.scheme()); auto& protocol_handler = protocol_registry->handlers().at(url.scheme());
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote = mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote =
ElectronURLLoaderFactory::Create(protocol_handler.first, ElectronURLLoaderFactory::Create(protocol_handler.first,
@ -528,6 +533,10 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
auto request = std::make_unique<network::ResourceRequest>(); auto request = std::make_unique<network::ResourceRequest>();
opts.Get("method", &request->method); opts.Get("method", &request->method);
opts.Get("url", &request->url); opts.Get("url", &request->url);
if (!request->url.is_valid()) {
args->ThrowTypeError("Invalid URL");
return gin::Handle<SimpleURLLoaderWrapper>();
}
request->site_for_cookies = net::SiteForCookies::FromUrl(request->url); request->site_for_cookies = net::SiteForCookies::FromUrl(request->url);
opts.Get("referrer", &request->referrer); opts.Get("referrer", &request->referrer);
request->referrer_policy = request->referrer_policy =
@ -648,7 +657,7 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
bool use_session_cookies = false; bool use_session_cookies = false;
opts.Get("useSessionCookies", &use_session_cookies); opts.Get("useSessionCookies", &use_session_cookies);
int options = 0; int options = network::mojom::kURLLoadOptionSniffMimeType;
if (!credentials_specified && !use_session_cookies) { if (!credentials_specified && !use_session_cookies) {
// This is the default case, as well as the case when credentials is not // This is the default case, as well as the case when credentials is not
// specified and useSessionCookies is false. credentials_mode will be // specified and useSessionCookies is false. credentials_mode will be
@ -657,6 +666,11 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
options |= network::mojom::kURLLoadOptionBlockAllCookies; options |= network::mojom::kURLLoadOptionBlockAllCookies;
} }
bool bypass_custom_protocol_handlers = false;
opts.Get("bypassCustomProtocolHandlers", &bypass_custom_protocol_handlers);
if (bypass_custom_protocol_handlers)
options |= kBypassCustomProtocolHandlers;
v8::Local<v8::Value> body; v8::Local<v8::Value> body;
v8::Local<v8::Value> chunk_pipe_getter; v8::Local<v8::Value> chunk_pipe_getter;
if (opts.Get("body", &body)) { if (opts.Get("body", &body)) {
@ -738,6 +752,7 @@ void SimpleURLLoaderWrapper::OnResponseStarted(
dict.Set("httpVersion", response_head.headers->GetHttpVersion()); dict.Set("httpVersion", response_head.headers->GetHttpVersion());
dict.Set("headers", response_head.headers.get()); dict.Set("headers", response_head.headers.get());
dict.Set("rawHeaders", response_head.raw_response_headers); dict.Set("rawHeaders", response_head.raw_response_headers);
dict.Set("mimeType", response_head.mime_type);
Emit("response-started", final_url, dict); Emit("response-started", final_url, dict);
} }

View file

@ -85,7 +85,10 @@ void NodeStreamLoader::NotifyComplete(int result) {
return; return;
} }
client_->OnComplete(network::URLLoaderCompletionStatus(result)); network::URLLoaderCompletionStatus status(result);
status.completion_time = base::TimeTicks::Now();
status.decoded_body_length = bytes_written_;
client_->OnComplete(status);
delete this; delete this;
} }
@ -126,6 +129,8 @@ void NodeStreamLoader::ReadMore() {
// Hold the buffer until the write is done. // Hold the buffer until the write is done.
buffer_.Reset(isolate_, buffer); buffer_.Reset(isolate_, buffer);
bytes_written_ += node::Buffer::Length(buffer);
// Write buffer to mojo pipe asynchronously. // Write buffer to mojo pipe asynchronously.
is_reading_ = false; is_reading_ = false;
is_writing_ = true; is_writing_ = true;

View file

@ -81,6 +81,8 @@ class NodeStreamLoader : public network::mojom::URLLoader {
// Whether we are in the middle of a stream.read(). // Whether we are in the middle of a stream.read().
bool is_reading_ = false; bool is_reading_ = false;
size_t bytes_written_ = 0;
// When NotifyComplete is called while writing, we will save the result and // When NotifyComplete is called while writing, we will save the result and
// quit with it after the write is done. // quit with it after the write is done.
bool ended_ = false; bool ended_ = false;

View file

@ -806,18 +806,23 @@ void ProxyingURLLoaderFactory::CreateLoaderAndStart(
} }
// Check if user has intercepted this scheme. // Check if user has intercepted this scheme.
auto it = intercepted_handlers_.find(request.url.scheme()); bool bypass_custom_protocol_handlers =
if (it != intercepted_handlers_.end()) { options & kBypassCustomProtocolHandlers;
mojo::PendingRemote<network::mojom::URLLoaderFactory> loader_remote; if (!bypass_custom_protocol_handlers) {
this->Clone(loader_remote.InitWithNewPipeAndPassReceiver()); auto it = intercepted_handlers_.find(request.url.scheme());
if (it != intercepted_handlers_.end()) {
mojo::PendingRemote<network::mojom::URLLoaderFactory> loader_remote;
this->Clone(loader_remote.InitWithNewPipeAndPassReceiver());
// <scheme, <type, handler>> // <scheme, <type, handler>>
it->second.second.Run( it->second.second.Run(
request, base::BindOnce(&ElectronURLLoaderFactory::StartLoading, request,
std::move(loader), request_id, options, request, base::BindOnce(&ElectronURLLoaderFactory::StartLoading,
std::move(client), traffic_annotation, std::move(loader), request_id, options, request,
std::move(loader_remote), it->second.first)); std::move(client), traffic_annotation,
return; std::move(loader_remote), it->second.first));
return;
}
} }
// The loader of ServiceWorker forbids loading scripts from file:// URLs, and // The loader of ServiceWorker forbids loading scripts from file:// URLs, and

View file

@ -36,6 +36,8 @@
namespace electron { namespace electron {
const uint32_t kBypassCustomProtocolHandlers = 1 << 30;
// This class is responsible for following tasks when NetworkService is enabled: // This class is responsible for following tasks when NetworkService is enabled:
// 1. handling intercepted protocols; // 1. handling intercepted protocols;
// 2. implementing webRequest module; // 2. implementing webRequest module;

View file

@ -15,16 +15,21 @@
#include "base/values.h" #include "base/values.h"
#include "gin/converter.h" #include "gin/converter.h"
#include "gin/dictionary.h" #include "gin/dictionary.h"
#include "gin/object_template_builder.h"
#include "net/cert/x509_certificate.h" #include "net/cert/x509_certificate.h"
#include "net/cert/x509_util.h" #include "net/cert/x509_util.h"
#include "net/http/http_response_headers.h" #include "net/http/http_response_headers.h"
#include "net/http/http_version.h" #include "net/http/http_version.h"
#include "net/url_request/redirect_info.h" #include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/data_element.h"
#include "services/network/public/cpp/resource_request.h" #include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/resource_request_body.h"
#include "services/network/public/mojom/chunked_data_pipe_getter.mojom.h"
#include "shell/browser/api/electron_api_data_pipe_holder.h" #include "shell/browser/api/electron_api_data_pipe_holder.h"
#include "shell/common/gin_converters/gurl_converter.h" #include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/std_converter.h" #include "shell/common/gin_converters/std_converter.h"
#include "shell/common/gin_converters/value_converter.h" #include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/node_includes.h" #include "shell/common/node_includes.h"
namespace gin { namespace gin {
@ -246,6 +251,246 @@ bool Converter<net::HttpRequestHeaders>::FromV8(v8::Isolate* isolate,
return true; return true;
} }
class ChunkedDataPipeReadableStream
: public gin::Wrappable<ChunkedDataPipeReadableStream> {
public:
static gin::Handle<ChunkedDataPipeReadableStream> Create(
v8::Isolate* isolate,
network::ResourceRequestBody* request,
network::DataElementChunkedDataPipe* data_element) {
return gin::CreateHandle(isolate, new ChunkedDataPipeReadableStream(
isolate, request, data_element));
}
// gin::Wrappable
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override {
return gin::Wrappable<
ChunkedDataPipeReadableStream>::GetObjectTemplateBuilder(isolate)
.SetMethod("read", &ChunkedDataPipeReadableStream::Read);
}
static gin::WrapperInfo kWrapperInfo;
private:
ChunkedDataPipeReadableStream(
v8::Isolate* isolate,
network::ResourceRequestBody* request,
network::DataElementChunkedDataPipe* data_element)
: isolate_(isolate),
resource_request_body_(request),
data_element_(data_element),
handle_watcher_(FROM_HERE,
mojo::SimpleWatcher::ArmingPolicy::MANUAL,
base::SequencedTaskRunner::GetCurrentDefault()) {}
~ChunkedDataPipeReadableStream() override = default;
int Init() {
chunked_data_pipe_getter_.Bind(
data_element_->ReleaseChunkedDataPipeGetter());
for (auto& element : *resource_request_body_->elements_mutable()) {
if (element.type() ==
network::mojom::DataElement::Tag::kChunkedDataPipe &&
data_element_ == &element.As<network::DataElementChunkedDataPipe>()) {
element = network::DataElement(
network::DataElementBytes(std::vector<uint8_t>()));
break;
}
}
chunked_data_pipe_getter_.set_disconnect_handler(
base::BindOnce(&ChunkedDataPipeReadableStream::OnDataPipeGetterClosed,
base::Unretained(this)));
chunked_data_pipe_getter_->GetSize(
base::BindOnce(&ChunkedDataPipeReadableStream::OnSizeReceived,
base::Unretained(this)));
mojo::ScopedDataPipeProducerHandle data_pipe_producer;
mojo::ScopedDataPipeConsumerHandle data_pipe_consumer;
MojoResult result =
mojo::CreateDataPipe(nullptr, data_pipe_producer, data_pipe_consumer);
if (result != MOJO_RESULT_OK)
return net::ERR_INSUFFICIENT_RESOURCES;
chunked_data_pipe_getter_->StartReading(std::move(data_pipe_producer));
data_pipe_ = std::move(data_pipe_consumer);
return net::OK;
}
v8::Local<v8::Promise> Read(v8::Local<v8::ArrayBufferView> buf) {
gin_helper::Promise<int> promise(isolate_);
v8::Local<v8::Promise> handle = promise.GetHandle();
int status = ReadInternal(buf);
if (status == net::ERR_IO_PENDING) {
promise_ = std::move(promise);
} else {
if (status < 0)
std::move(promise).RejectWithErrorMessage(net::ErrorToString(status));
else
std::move(promise).Resolve(status);
}
return handle;
}
int ReadInternal(v8::Local<v8::ArrayBufferView> buf) {
if (!data_pipe_)
status_ = Init();
// If there was an error either passed to the ReadCallback or as a result of
// closing the DataPipeGetter pipe, fail the read.
if (status_ != net::OK)
return status_;
// Nothing else to do, if the entire body was read.
if (size_ && bytes_read_ == *size_) {
// This shouldn't be called if the stream was already completed.
DCHECK(!is_eof_);
is_eof_ = true;
return net::OK;
}
if (!handle_watcher_.IsWatching()) {
handle_watcher_.Watch(
data_pipe_.get(),
MOJO_HANDLE_SIGNAL_READABLE | MOJO_HANDLE_SIGNAL_PEER_CLOSED,
base::BindRepeating(&ChunkedDataPipeReadableStream::OnHandleReadable,
base::Unretained(this)));
}
uint32_t num_bytes = buf->ByteLength();
if (size_ && num_bytes > *size_ - bytes_read_)
num_bytes = *size_ - bytes_read_;
MojoResult rv = data_pipe_->ReadData(
static_cast<void*>(static_cast<char*>(buf->Buffer()->Data()) +
buf->ByteOffset()),
&num_bytes, MOJO_READ_DATA_FLAG_NONE);
if (rv == MOJO_RESULT_OK) {
bytes_read_ += num_bytes;
// Not needed for correctness, but this allows the consumer to send the
// final chunk and the end of stream message together, for protocols that
// allow it.
if (size_ && *size_ == bytes_read_)
is_eof_ = true;
return num_bytes;
}
if (rv == MOJO_RESULT_SHOULD_WAIT) {
handle_watcher_.ArmOrNotify();
buf_.Reset(isolate_, buf);
return net::ERR_IO_PENDING;
}
// The pipe was closed. If the size isn't known yet, could be a success or a
// failure.
if (!size_) {
// Need to keep the buffer around because its presence is used to indicate
// that there's a pending UploadDataStream read.
buf_.Reset(isolate_, buf);
handle_watcher_.Cancel();
data_pipe_.reset();
return net::ERR_IO_PENDING;
}
// |size_| was checked earlier, so if this point is reached, the pipe was
// closed before receiving all bytes.
DCHECK_LT(bytes_read_, *size_);
return net::ERR_FAILED;
}
void OnSizeReceived(int32_t status, uint64_t size) {
DCHECK(!size_);
DCHECK_EQ(net::OK, status_);
status_ = status;
if (status == net::OK) {
size_ = size;
if (size == bytes_read_) {
// Only set this as a final chunk if there's a read in progress. Setting
// it asynchronously could result in confusing consumers.
if (!buf_.IsEmpty())
is_eof_ = true;
} else if (size < bytes_read_ ||
(!buf_.IsEmpty() && !data_pipe_.is_valid())) {
// If more data was received than was expected, or there's a pending
// read and data pipe was closed without passing in as many bytes as
// expected, the upload can't continue. If there's no pending read but
// the pipe was closed, the closure and size difference will be noticed
// on the next read attempt.
status_ = net::ERR_FAILED;
}
}
// If this is done, and there's a pending read, complete the pending read.
// If there's not a pending read, either |status_| will be reported on the
// next read, the file will be marked as done, so ReadInternal() won't be
// called again.
if (!buf_.IsEmpty() && (is_eof_ || status_ != net::OK)) {
// |data_pipe_| isn't needed any more, and if it's still open, a close
// pipe message would cause issues, since this class normally only watches
// the pipe when there's a pending read.
handle_watcher_.Cancel();
data_pipe_.reset();
// Clear |buf_| as well, so it's only non-null while there's a pending
// read.
buf_.Reset();
chunked_data_pipe_getter_.reset();
OnReadCompleted(status_);
// |this| may have been deleted at this point.
}
}
void OnHandleReadable(MojoResult result) {
DCHECK(!buf_.IsEmpty());
v8::HandleScope handle_scope(isolate_);
v8::Local<v8::ArrayBufferView> buf = buf_.Get(isolate_);
buf_.Reset();
int rv = ReadInternal(buf);
if (rv != net::ERR_IO_PENDING)
OnReadCompleted(rv);
// |this| may have been deleted at this point.
}
void OnReadCompleted(int result) {
if (result < 0)
std::move(promise_).RejectWithErrorMessage(net::ErrorToString(result));
else
std::move(promise_).Resolve(result);
}
void OnDataPipeGetterClosed() {
// If the size hasn't been received yet, treat this as receiving an error.
// Otherwise, this will only be a problem if/when InitInternal() tries to
// start reading again, so do nothing.
if (status_ == net::OK && !size_)
OnSizeReceived(net::ERR_FAILED, 0);
}
v8::Isolate* isolate_;
int status_ = net::OK;
scoped_refptr<network::ResourceRequestBody> resource_request_body_;
network::DataElementChunkedDataPipe* data_element_;
mojo::Remote<network::mojom::ChunkedDataPipeGetter> chunked_data_pipe_getter_;
mojo::ScopedDataPipeConsumerHandle data_pipe_;
mojo::SimpleWatcher handle_watcher_;
absl::optional<uint64_t> size_;
uint64_t bytes_read_ = 0;
bool is_eof_ = false;
v8::Global<v8::ArrayBufferView> buf_;
gin_helper::Promise<int> promise_;
};
gin::WrapperInfo ChunkedDataPipeReadableStream::kWrapperInfo = {
gin::kEmbedderNativeGin};
// static // static
v8::Local<v8::Value> Converter<network::ResourceRequestBody>::ToV8( v8::Local<v8::Value> Converter<network::ResourceRequestBody>::ToV8(
v8::Isolate* isolate, v8::Isolate* isolate,
@ -288,6 +533,21 @@ v8::Local<v8::Value> Converter<network::ResourceRequestBody>::ToV8(
upload_data.Set("dataPipe", holder); upload_data.Set("dataPipe", holder);
break; break;
} }
case network::mojom::DataElement::Tag::kChunkedDataPipe: {
upload_data.Set("type", "stream");
// ReleaseChunkedDataPipeGetter mutates the element, but unfortunately
// gin converters are only allowed const references, so we need to cast
// off the const here.
auto& mutable_element =
const_cast<network::DataElementChunkedDataPipe&>(
element.As<network::DataElementChunkedDataPipe>());
upload_data.Set(
"body",
ChunkedDataPipeReadableStream::Create(
isolate, const_cast<network::ResourceRequestBody*>(&val),
&mutable_element));
break;
}
default: default:
NOTREACHED() << "Found unsupported data element"; NOTREACHED() << "Found unsupported data element";
} }

View file

@ -17,6 +17,8 @@ PromiseBase::PromiseBase(v8::Isolate* isolate,
context_(isolate, isolate->GetCurrentContext()), context_(isolate, isolate->GetCurrentContext()),
resolver_(isolate, handle) {} resolver_(isolate, handle) {}
PromiseBase::PromiseBase() : isolate_(nullptr) {}
PromiseBase::PromiseBase(PromiseBase&&) = default; PromiseBase::PromiseBase(PromiseBase&&) = default;
PromiseBase::~PromiseBase() = default; PromiseBase::~PromiseBase() = default;

View file

@ -30,6 +30,7 @@ class PromiseBase {
public: public:
explicit PromiseBase(v8::Isolate* isolate); explicit PromiseBase(v8::Isolate* isolate);
PromiseBase(v8::Isolate* isolate, v8::Local<v8::Promise::Resolver> handle); PromiseBase(v8::Isolate* isolate, v8::Local<v8::Promise::Resolver> handle);
PromiseBase();
~PromiseBase(); ~PromiseBase();
// disable copy // disable copy

View file

@ -1438,6 +1438,18 @@ describe('net module', () => {
expect(requestIsRedirected).to.be.true('The server should receive a request to the forward URL'); 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'); 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 when calling getHeader without a name', () => { it('should throw when calling getHeader without a name', () => {

View file

@ -1,8 +1,9 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { protocol, webContents, WebContents, session, BrowserWindow, ipcMain } from 'electron/main'; import { protocol, webContents, WebContents, session, BrowserWindow, ipcMain, net } from 'electron/main';
import * as ChildProcess from 'child_process'; import * as ChildProcess from 'child_process';
import * as path from 'path'; import * as path from 'path';
import * as url from 'url';
import * as http from 'http'; import * as http from 'http';
import * as fs from 'fs'; import * as fs from 'fs';
import * as qs from 'querystring'; import * as qs from 'querystring';
@ -10,7 +11,7 @@ import * as stream from 'stream';
import { EventEmitter, once } from 'events'; import { EventEmitter, once } from 'events';
import { closeAllWindows, closeWindow } from './lib/window-helpers'; import { closeAllWindows, closeWindow } from './lib/window-helpers';
import { WebmGenerator } from './lib/video-helpers'; import { WebmGenerator } from './lib/video-helpers';
import { listen } from './lib/spec-helpers'; import { listen, defer, ifit } from './lib/spec-helpers';
import { setTimeout } from 'timers/promises'; import { setTimeout } from 'timers/promises';
const fixturesPath = path.resolve(__dirname, 'fixtures'); const fixturesPath = path.resolve(__dirname, 'fixtures');
@ -34,7 +35,9 @@ const postData = {
}; };
function getStream (chunkSize = text.length, data: Buffer | string = text) { function getStream (chunkSize = text.length, data: Buffer | string = text) {
const body = new stream.PassThrough(); // allowHalfOpen required, otherwise Readable.toWeb gets confused and thinks
// the stream isn't done when the readable half ends.
const body = new stream.PassThrough({ allowHalfOpen: false });
async function sendChunks () { async function sendChunks () {
await setTimeout(0); // the stream protocol API breaks if you send data immediately. await setTimeout(0); // the stream protocol API breaks if you send data immediately.
@ -54,9 +57,12 @@ function getStream (chunkSize = text.length, data: Buffer | string = text) {
sendChunks(); sendChunks();
return body; return body;
} }
function getWebStream (chunkSize = text.length, data: Buffer | string = text): ReadableStream<ArrayBufferView> {
return stream.Readable.toWeb(getStream(chunkSize, data)) as ReadableStream<ArrayBufferView>;
}
// A promise that can be resolved externally. // A promise that can be resolved externally.
function defer (): Promise<any> & {resolve: Function, reject: Function} { function deferPromise (): Promise<any> & {resolve: Function, reject: Function} {
let promiseResolve: Function = null as unknown as Function; let promiseResolve: Function = null as unknown as Function;
let promiseReject: Function = null as unknown as Function; let promiseReject: Function = null as unknown as Function;
const promise: any = new Promise((resolve, reject) => { const promise: any = new Promise((resolve, reject) => {
@ -860,7 +866,7 @@ describe('protocol module', () => {
}); });
it('can have fetch working in it', async () => { it('can have fetch working in it', async () => {
const requestReceived = defer(); const requestReceived = deferPromise();
const server = http.createServer((req, res) => { const server = http.createServer((req, res) => {
res.end(); res.end();
server.close(); server.close();
@ -1093,4 +1099,422 @@ describe('protocol module', () => {
} }
} }
}); });
describe('handle', () => {
afterEach(closeAllWindows);
it('receives requests to a custom scheme', async () => {
protocol.handle('test-scheme', (req) => new Response('hello ' + req.url));
defer(() => { protocol.unhandle('test-scheme'); });
const resp = await net.fetch('test-scheme://foo');
expect(resp.status).to.equal(200);
});
it('can be unhandled', async () => {
protocol.handle('test-scheme', (req) => new Response('hello ' + req.url));
defer(() => {
try {
// In case of failure, make sure we unhandle. But we should succeed
// :)
protocol.unhandle('test-scheme');
} catch (_ignored) { /* ignore */ }
});
const resp1 = await net.fetch('test-scheme://foo');
expect(resp1.status).to.equal(200);
protocol.unhandle('test-scheme');
await expect(net.fetch('test-scheme://foo')).to.eventually.be.rejectedWith(/ERR_UNKNOWN_URL_SCHEME/);
});
it('receives requests to an existing scheme', async () => {
protocol.handle('https', (req) => new Response('hello ' + req.url));
defer(() => { protocol.unhandle('https'); });
const body = await net.fetch('https://foo').then(r => r.text());
expect(body).to.equal('hello https://foo/');
});
it('receives requests to an existing scheme when navigating', async () => {
protocol.handle('https', (req) => new Response('hello ' + req.url));
defer(() => { protocol.unhandle('https'); });
const w = new BrowserWindow({ show: false });
await w.loadURL('https://localhost');
expect(await w.webContents.executeJavaScript('document.body.textContent')).to.equal('hello https://localhost/');
});
it('can send buffer body', async () => {
protocol.handle('test-scheme', (req) => new Response(Buffer.from('hello ' + req.url)));
defer(() => { protocol.unhandle('test-scheme'); });
const body = await net.fetch('test-scheme://foo').then(r => r.text());
expect(body).to.equal('hello test-scheme://foo');
});
it('can send stream body', async () => {
protocol.handle('test-scheme', () => new Response(getWebStream()));
defer(() => { protocol.unhandle('test-scheme'); });
const body = await net.fetch('test-scheme://foo').then(r => r.text());
expect(body).to.equal(text);
});
it('accepts urls with no hostname in non-standard schemes', async () => {
protocol.handle('test-scheme', (req) => new Response(req.url));
defer(() => { protocol.unhandle('test-scheme'); });
{
const body = await net.fetch('test-scheme://foo').then(r => r.text());
expect(body).to.equal('test-scheme://foo');
}
{
const body = await net.fetch('test-scheme:///foo').then(r => r.text());
expect(body).to.equal('test-scheme:///foo');
}
{
const body = await net.fetch('test-scheme://').then(r => r.text());
expect(body).to.equal('test-scheme://');
}
});
it('accepts urls with a port-like component in non-standard schemes', async () => {
protocol.handle('test-scheme', (req) => new Response(req.url));
defer(() => { protocol.unhandle('test-scheme'); });
{
const body = await net.fetch('test-scheme://foo:30').then(r => r.text());
expect(body).to.equal('test-scheme://foo:30');
}
});
it('normalizes urls in standard schemes', async () => {
// NB. 'app' is registered as a standard scheme in test setup.
protocol.handle('app', (req) => new Response(req.url));
defer(() => { protocol.unhandle('app'); });
{
const body = await net.fetch('app://foo').then(r => r.text());
expect(body).to.equal('app://foo/');
}
{
const body = await net.fetch('app:///foo').then(r => r.text());
expect(body).to.equal('app://foo/');
}
// NB. 'app' is registered with the default scheme type of 'host'.
{
const body = await net.fetch('app://foo:1234').then(r => r.text());
expect(body).to.equal('app://foo/');
}
await expect(net.fetch('app://')).to.be.rejectedWith('Invalid URL');
});
it('fails on URLs with a username', async () => {
// NB. 'app' is registered as a standard scheme in test setup.
protocol.handle('http', (req) => new Response(req.url));
defer(() => { protocol.unhandle('http'); });
await expect(contents.loadURL('http://x@foo:1234')).to.be.rejectedWith(/ERR_UNEXPECTED/);
});
it('normalizes http urls', async () => {
protocol.handle('http', (req) => new Response(req.url));
defer(() => { protocol.unhandle('http'); });
{
const body = await net.fetch('http://foo').then(r => r.text());
expect(body).to.equal('http://foo/');
}
});
it('can send errors', async () => {
protocol.handle('test-scheme', () => Response.error());
defer(() => { protocol.unhandle('test-scheme'); });
await expect(net.fetch('test-scheme://foo')).to.eventually.be.rejectedWith('net::ERR_FAILED');
});
it('handles a synchronous error in the handler', async () => {
protocol.handle('test-scheme', () => { throw new Error('test'); });
defer(() => { protocol.unhandle('test-scheme'); });
await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
});
it('handles an asynchronous error in the handler', async () => {
protocol.handle('test-scheme', () => Promise.reject(new Error('rejected promise')));
defer(() => { protocol.unhandle('test-scheme'); });
await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
});
it('correctly sets statusCode', async () => {
protocol.handle('test-scheme', () => new Response(null, { status: 201 }));
defer(() => { protocol.unhandle('test-scheme'); });
const resp = await net.fetch('test-scheme://foo');
expect(resp.status).to.equal(201);
});
it('correctly sets content-type and charset', async () => {
protocol.handle('test-scheme', () => new Response(null, { headers: { 'content-type': 'text/html; charset=testcharset' } }));
defer(() => { protocol.unhandle('test-scheme'); });
const resp = await net.fetch('test-scheme://foo');
expect(resp.headers.get('content-type')).to.equal('text/html; charset=testcharset');
});
it('can forward to http', async () => {
const server = http.createServer((req, res) => {
res.end(text);
});
defer(() => { server.close(); });
const { url } = await listen(server);
protocol.handle('test-scheme', () => net.fetch(url));
defer(() => { protocol.unhandle('test-scheme'); });
const body = await net.fetch('test-scheme://foo').then(r => r.text());
expect(body).to.equal(text);
});
it('can forward an http request with headers', async () => {
const server = http.createServer((req, res) => {
res.setHeader('foo', 'bar');
res.end(text);
});
defer(() => { server.close(); });
const { url } = await listen(server);
protocol.handle('test-scheme', (req) => net.fetch(url, { headers: req.headers }));
defer(() => { protocol.unhandle('test-scheme'); });
const resp = await net.fetch('test-scheme://foo');
expect(resp.headers.get('foo')).to.equal('bar');
});
it('can forward to file', async () => {
protocol.handle('test-scheme', () => net.fetch(url.pathToFileURL(path.join(__dirname, 'fixtures', 'hello.txt')).toString()));
defer(() => { protocol.unhandle('test-scheme'); });
const body = await net.fetch('test-scheme://foo').then(r => r.text());
expect(body.trimEnd()).to.equal('hello world');
});
it('can receive simple request body', async () => {
protocol.handle('test-scheme', (req) => new Response(req.body));
defer(() => { protocol.unhandle('test-scheme'); });
const body = await net.fetch('test-scheme://foo', {
method: 'POST',
body: 'foobar'
}).then(r => r.text());
expect(body).to.equal('foobar');
});
it('can receive stream request body', async () => {
protocol.handle('test-scheme', (req) => new Response(req.body));
defer(() => { protocol.unhandle('test-scheme'); });
const body = await net.fetch('test-scheme://foo', {
method: 'POST',
body: getWebStream(),
duplex: 'half' // https://github.com/microsoft/TypeScript/issues/53157
} as any).then(r => r.text());
expect(body).to.equal(text);
});
it('can receive multi-part postData from loadURL', async () => {
protocol.handle('test-scheme', (req) => new Response(req.body));
defer(() => { protocol.unhandle('test-scheme'); });
await contents.loadURL('test-scheme://foo', { postData: [{ type: 'rawData', bytes: Buffer.from('a') }, { type: 'rawData', bytes: Buffer.from('b') }] });
expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal('ab');
});
it('can receive file postData from loadURL', async () => {
protocol.handle('test-scheme', (req) => new Response(req.body));
defer(() => { protocol.unhandle('test-scheme'); });
await contents.loadURL('test-scheme://foo', { postData: [{ type: 'file', filePath: path.join(fixturesPath, 'hello.txt'), length: 'hello world\n'.length, offset: 0, modificationTime: 0 }] });
expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal('hello world\n');
});
it('can receive file postData from a form', async () => {
protocol.handle('test-scheme', (req) => new Response(req.body));
defer(() => { protocol.unhandle('test-scheme'); });
await contents.loadURL('data:text/html,<form action="test-scheme://foo" method=POST enctype="multipart/form-data"><input name=foo type=file>');
const { debugger: dbg } = contents;
dbg.attach();
const { root } = await dbg.sendCommand('DOM.getDocument');
const { nodeId: fileInputNodeId } = await dbg.sendCommand('DOM.querySelector', { nodeId: root.nodeId, selector: 'input' });
await dbg.sendCommand('DOM.setFileInputFiles', {
nodeId: fileInputNodeId,
files: [
path.join(fixturesPath, 'hello.txt')
]
});
const navigated = once(contents, 'did-finish-load');
await contents.executeJavaScript('document.querySelector("form").submit()');
await navigated;
expect(await contents.executeJavaScript('document.documentElement.textContent')).to.match(/------WebKitFormBoundary.*\nContent-Disposition: form-data; name="foo"; filename="hello.txt"\nContent-Type: text\/plain\n\nhello world\n\n------WebKitFormBoundary.*--\n/);
});
it('can receive streaming fetch upload', async () => {
protocol.handle('no-cors', (req) => new Response(req.body));
defer(() => { protocol.unhandle('no-cors'); });
await contents.loadURL('no-cors://foo');
const fetchBodyResult = await contents.executeJavaScript(`
const stream = new ReadableStream({
async start(controller) {
controller.enqueue('hello world');
controller.close();
},
}).pipeThrough(new TextEncoderStream());
fetch(location.href, {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text())
`);
expect(fetchBodyResult).to.equal('hello world');
});
it('can receive streaming fetch upload when a webRequest handler is present', async () => {
session.defaultSession.webRequest.onBeforeRequest((details, cb) => {
console.log('webRequest', details.url, details.method);
cb({});
});
defer(() => {
session.defaultSession.webRequest.onBeforeRequest(null);
});
protocol.handle('no-cors', (req) => {
console.log('handle', req.url, req.method);
return new Response(req.body);
});
defer(() => { protocol.unhandle('no-cors'); });
await contents.loadURL('no-cors://foo');
const fetchBodyResult = await contents.executeJavaScript(`
const stream = new ReadableStream({
async start(controller) {
controller.enqueue('hello world');
controller.close();
},
}).pipeThrough(new TextEncoderStream());
fetch(location.href, {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text())
`);
expect(fetchBodyResult).to.equal('hello world');
});
it('can receive an error from streaming fetch upload', async () => {
protocol.handle('no-cors', (req) => new Response(req.body));
defer(() => { protocol.unhandle('no-cors'); });
await contents.loadURL('no-cors://foo');
const fetchBodyResult = await contents.executeJavaScript(`
const stream = new ReadableStream({
async start(controller) {
controller.error('test')
},
});
fetch(location.href, {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text()).catch(err => err)
`);
expect(fetchBodyResult).to.be.an.instanceOf(Error);
});
it('gets an error from streaming fetch upload when the renderer dies', async () => {
let gotRequest: Function;
const receivedRequest = new Promise<Request>(resolve => { gotRequest = resolve; });
protocol.handle('no-cors', (req) => {
if (/fetch/.test(req.url)) gotRequest(req);
return new Response();
});
defer(() => { protocol.unhandle('no-cors'); });
await contents.loadURL('no-cors://foo');
contents.executeJavaScript(`
const stream = new ReadableStream({
async start(controller) {
window.controller = controller // no GC
},
});
fetch(location.href + '/fetch', {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text()).catch(err => err)
`);
const req = await receivedRequest;
contents.destroy();
// Undo .destroy() for the next test
contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
await expect(req.body!.getReader().read()).to.eventually.be.rejectedWith('net::ERR_FAILED');
});
it('can bypass intercepeted protocol handlers', async () => {
protocol.handle('http', () => new Response('custom'));
defer(() => { protocol.unhandle('http'); });
const server = http.createServer((req, res) => {
res.end('default');
});
defer(() => server.close());
const { url } = await listen(server);
expect(await net.fetch(url, { bypassCustomProtocolHandlers: true }).then(r => r.text())).to.equal('default');
});
it('bypassing custom protocol handlers also bypasses new protocols', async () => {
protocol.handle('app', () => new Response('custom'));
defer(() => { protocol.unhandle('app'); });
await expect(net.fetch('app://foo', { bypassCustomProtocolHandlers: true })).to.be.rejectedWith('net::ERR_UNKNOWN_URL_SCHEME');
});
it('can forward to the original handler', async () => {
protocol.handle('http', (req) => net.fetch(req, { bypassCustomProtocolHandlers: true }));
defer(() => { protocol.unhandle('http'); });
const server = http.createServer((req, res) => {
res.end('hello');
server.close();
});
const { url } = await listen(server);
await contents.loadURL(url);
expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal('hello');
});
it('supports sniffing mime type', async () => {
protocol.handle('http', async (req) => {
return net.fetch(req, { bypassCustomProtocolHandlers: true });
});
defer(() => { protocol.unhandle('http'); });
const server = http.createServer((req, res) => {
if (/html/.test(req.url ?? '')) { res.end('<!doctype html><body>hi'); } else { res.end('hi'); }
});
const { url } = await listen(server);
defer(() => server.close());
{
await contents.loadURL(url);
const doc = await contents.executeJavaScript('document.documentElement.outerHTML');
expect(doc).to.match(/white-space: pre-wrap/);
}
{
await contents.loadURL(url + '?html');
const doc = await contents.executeJavaScript('document.documentElement.outerHTML');
expect(doc).to.equal('<html><head></head><body>hi</body></html>');
}
});
// TODO(nornagon): this test doesn't pass on Linux currently, investigate.
ifit(process.platform !== 'linux')('is fast', async () => {
// 128 MB of spaces.
const chunk = new Uint8Array(128 * 1024 * 1024);
chunk.fill(' '.charCodeAt(0));
const server = http.createServer((req, res) => {
// The sniffed mime type for the space-filled chunk will be
// text/plain, which chews up all its performance in the renderer
// trying to wrap lines. Setting content-type to text/html measures
// something closer to just the raw cost of getting the bytes over
// the wire.
res.setHeader('content-type', 'text/html');
res.end(chunk);
});
defer(() => server.close());
const { url } = await listen(server);
const rawTime = await (async () => {
await contents.loadURL(url); // warm
const begin = Date.now();
await contents.loadURL(url);
const end = Date.now();
return end - begin;
})();
// Fetching through an intercepted handler should not be too much slower
// than it would be if the protocol hadn't been intercepted.
protocol.handle('http', async (req) => {
return net.fetch(req, { bypassCustomProtocolHandlers: true });
});
defer(() => { protocol.unhandle('http'); });
const interceptedTime = await (async () => {
const begin = Date.now();
await contents.loadURL(url);
const end = Date.now();
return end - begin;
})();
expect(interceptedTime).to.be.lessThan(rawTime * 1.5);
});
});
}); });

View file

@ -1,13 +1,16 @@
import { expect } from 'chai'; import { expect } from 'chai';
import * as http from 'http'; import * as http from 'http';
import * as http2 from 'http2';
import * as qs from 'querystring'; import * as qs from 'querystring';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs';
import * as url from 'url'; import * as url from 'url';
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
import { ipcMain, protocol, session, WebContents, webContents } from 'electron/main'; import { ipcMain, protocol, session, WebContents, webContents } from 'electron/main';
import { Socket } from 'net'; import { AddressInfo, Socket } from 'net';
import { listen } from './lib/spec-helpers'; import { listen, defer } from './lib/spec-helpers';
import { once } from 'events'; import { once } from 'events';
import { ReadableStream } from 'stream/web';
const fixturesPath = path.resolve(__dirname, 'fixtures'); const fixturesPath = path.resolve(__dirname, 'fixtures');
@ -35,14 +38,35 @@ describe('webRequest module', () => {
} }
}); });
let defaultURL: string; let defaultURL: string;
let http2URL: string;
const certPath = path.join(fixturesPath, 'certificates');
const h2server = http2.createSecureServer({
key: fs.readFileSync(path.join(certPath, 'server.key')),
cert: fs.readFileSync(path.join(certPath, 'server.pem'))
}, async (req, res) => {
if (req.method === 'POST') {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
res.end(Buffer.concat(chunks).toString('utf8'));
} else {
res.end('<html></html>');
}
});
before(async () => { before(async () => {
protocol.registerStringProtocol('cors', (req, cb) => cb('')); protocol.registerStringProtocol('cors', (req, cb) => cb(''));
defaultURL = (await listen(server)).url + '/'; defaultURL = (await listen(server)).url + '/';
await new Promise<void>((resolve) => {
h2server.listen(0, '127.0.0.1', () => resolve());
});
http2URL = `https://127.0.0.1:${(h2server.address() as AddressInfo).port}/`;
console.log(http2URL);
}); });
after(() => { after(() => {
server.close(); server.close();
h2server.close();
protocol.unregisterProtocol('cors'); protocol.unregisterProtocol('cors');
}); });
@ -50,6 +74,8 @@ describe('webRequest module', () => {
// NB. sandbox: true is used because it makes navigations much (~8x) faster. // NB. sandbox: true is used because it makes navigations much (~8x) faster.
before(async () => { before(async () => {
contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true }); contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
// const w = new BrowserWindow({webPreferences: {sandbox: true}})
// contents = w.webContents
await contents.loadFile(path.join(fixturesPath, 'pages', 'fetch.html')); await contents.loadFile(path.join(fixturesPath, 'pages', 'fetch.html'));
}); });
after(() => contents.destroy()); after(() => contents.destroy());
@ -161,6 +187,92 @@ describe('webRequest module', () => {
}); });
await expect(ajax(fileURL)).to.eventually.be.rejected(); await expect(ajax(fileURL)).to.eventually.be.rejected();
}); });
it('can handle a streaming upload', async () => {
// Streaming fetch uploads are only supported on HTTP/2, which is only
// supported over TLS, so...
session.defaultSession.setCertificateVerifyProc((req, cb) => cb(0));
defer(() => {
session.defaultSession.setCertificateVerifyProc(null);
});
const contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
defer(() => contents.close());
await contents.loadURL(http2URL);
ses.webRequest.onBeforeRequest((details, callback) => {
callback({});
});
const result = await contents.executeJavaScript(`
const stream = new ReadableStream({
async start(controller) {
controller.enqueue('hello world');
controller.close();
},
}).pipeThrough(new TextEncoderStream());
fetch("${http2URL}", {
method: 'POST',
body: stream,
duplex: 'half',
}).then(r => r.text())
`);
expect(result).to.equal('hello world');
});
it('can handle a streaming upload if the uploadData is read', async () => {
// Streaming fetch uploads are only supported on HTTP/2, which is only
// supported over TLS, so...
session.defaultSession.setCertificateVerifyProc((req, cb) => cb(0));
defer(() => {
session.defaultSession.setCertificateVerifyProc(null);
});
const contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
defer(() => contents.close());
await contents.loadURL(http2URL);
function makeStreamFromPipe (pipe: any): ReadableStream {
const buf = new Uint8Array(1024 * 1024 /* 1 MB */);
return new ReadableStream({
async pull (controller) {
try {
const rv = await pipe.read(buf);
if (rv > 0) {
controller.enqueue(buf.subarray(0, rv));
} else {
controller.close();
}
} catch (e) {
controller.error(e);
}
}
});
}
ses.webRequest.onBeforeRequest(async (details, callback) => {
const chunks = [];
for await (const chunk of makeStreamFromPipe((details.uploadData[0] as any).body)) { chunks.push(chunk); }
callback({});
});
const result = await contents.executeJavaScript(`
const stream = new ReadableStream({
async start(controller) {
controller.enqueue('hello world');
controller.close();
},
}).pipeThrough(new TextEncoderStream());
fetch("${http2URL}", {
method: 'POST',
body: stream,
duplex: 'half',
}).then(r => r.text())
`);
// NOTE: since the upload stream was consumed by the onBeforeRequest
// handler, it can't be used again to upload to the actual server.
// This is a limitation of the WebRequest API.
expect(result).to.equal('');
});
}); });
describe('webRequest.onBeforeSendHeaders', () => { describe('webRequest.onBeforeSendHeaders', () => {

View file

@ -141,6 +141,7 @@ declare namespace NodeJS {
hasUserActivation?: boolean; hasUserActivation?: boolean;
mode?: string; mode?: string;
destination?: string; destination?: string;
bypassCustomProtocolHandlers?: boolean;
}; };
type ResponseHead = { type ResponseHead = {
statusCode: number; statusCode: number;

View file

@ -182,6 +182,11 @@ declare namespace Electron {
setBackgroundThrottling(allowed: boolean): void; setBackgroundThrottling(allowed: boolean): void;
} }
interface Protocol {
registerProtocol(scheme: string, handler: any): boolean;
interceptProtocol(scheme: string, handler: any): boolean;
}
namespace Main { namespace Main {
class BaseWindow extends Electron.BaseWindow {} class BaseWindow extends Electron.BaseWindow {}
class View extends Electron.View {} class View extends Electron.View {}