зеркало из https://github.com/electron/electron.git
feat: add protocol.handle (#36674)
This commit is contained in:
Родитель
6a6908c4c8
Коммит
fda8ea9277
|
@ -243,6 +243,8 @@ it is not allowed to add or remove a custom header.
|
|||
* `encoding` string (optional)
|
||||
* `callback` Function (optional)
|
||||
|
||||
Returns `this`.
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -65,8 +65,8 @@ requests according to the specified protocol scheme in the `options` object.
|
|||
|
||||
### `net.fetch(input[, init])`
|
||||
|
||||
* `input` string | [Request](https://nodejs.org/api/globals.html#request)
|
||||
* `init` [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options) (optional)
|
||||
* `input` string | [GlobalRequest](https://nodejs.org/api/globals.html#request)
|
||||
* `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).
|
||||
|
||||
|
@ -101,9 +101,23 @@ Limitations:
|
|||
* The `.type` and `.url` values of the returned `Response` object are
|
||||
incorrect.
|
||||
|
||||
Requests made with `net.fetch` can be made to [custom protocols](protocol.md)
|
||||
as well as `file:`, and will trigger [webRequest](web-request.md) handlers if
|
||||
present.
|
||||
By default, requests made with `net.fetch` can be made to [custom
|
||||
protocols](protocol.md) as well as `file:`, and will trigger
|
||||
[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()`
|
||||
|
||||
|
|
|
@ -8,15 +8,11 @@ An example of implementing a protocol that has the same effect as the
|
|||
`file://` protocol:
|
||||
|
||||
```javascript
|
||||
const { app, protocol } = require('electron')
|
||||
const path = require('path')
|
||||
const url = require('url')
|
||||
const { app, protocol, net } = require('electron')
|
||||
|
||||
app.whenReady().then(() => {
|
||||
protocol.registerFileProtocol('atom', (request, callback) => {
|
||||
const filePath = url.fileURLToPath('file://' + request.url.slice('atom://'.length))
|
||||
callback(filePath)
|
||||
})
|
||||
protocol.handle('atom', (request) =>
|
||||
net.fetch('file://' + request.url.slice('atom://'.length)))
|
||||
})
|
||||
```
|
||||
|
||||
|
@ -38,14 +34,15 @@ to register it to that session explicitly.
|
|||
```javascript
|
||||
const { session, app, protocol } = require('electron')
|
||||
const path = require('path')
|
||||
const url = require('url')
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const partition = 'persist:example'
|
||||
const ses = session.fromPartition(partition)
|
||||
|
||||
ses.protocol.registerFileProtocol('atom', (request, callback) => {
|
||||
const url = request.url.substr(7)
|
||||
callback({ path: path.normalize(`${__dirname}/${url}`) })
|
||||
ses.protocol.handle('atom', (request) => {
|
||||
const path = request.url.slice('atom://'.length)
|
||||
return net.fetch(url.pathToFileURL(path.join(__dirname, path)))
|
||||
})
|
||||
|
||||
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
|
||||
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
|
||||
* `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
|
||||
from protocols that follow the "generic URI syntax" like `file:`.
|
||||
|
||||
### `protocol.registerBufferProtocol(scheme, handler)`
|
||||
### `protocol.registerBufferProtocol(scheme, handler)` _Deprecated_
|
||||
|
||||
* `scheme` string
|
||||
* `handler` Function
|
||||
|
@ -154,7 +218,7 @@ protocol.registerBufferProtocol('atom', (request, callback) => {
|
|||
})
|
||||
```
|
||||
|
||||
### `protocol.registerStringProtocol(scheme, handler)`
|
||||
### `protocol.registerStringProtocol(scheme, handler)` _Deprecated_
|
||||
|
||||
* `scheme` string
|
||||
* `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`
|
||||
property.
|
||||
|
||||
### `protocol.registerHttpProtocol(scheme, handler)`
|
||||
### `protocol.registerHttpProtocol(scheme, handler)` _Deprecated_
|
||||
|
||||
* `scheme` string
|
||||
* `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`
|
||||
should be called with an object that has the `url` property.
|
||||
|
||||
### `protocol.registerStreamProtocol(scheme, handler)`
|
||||
### `protocol.registerStreamProtocol(scheme, handler)` _Deprecated_
|
||||
|
||||
* `scheme` string
|
||||
* `handler` Function
|
||||
|
@ -234,7 +298,7 @@ protocol.registerStreamProtocol('atom', (request, callback) => {
|
|||
})
|
||||
```
|
||||
|
||||
### `protocol.unregisterProtocol(scheme)`
|
||||
### `protocol.unregisterProtocol(scheme)` _Deprecated_
|
||||
|
||||
* `scheme` string
|
||||
|
||||
|
@ -242,13 +306,13 @@ Returns `boolean` - Whether the protocol was successfully unregistered
|
|||
|
||||
Unregisters the custom protocol of `scheme`.
|
||||
|
||||
### `protocol.isProtocolRegistered(scheme)`
|
||||
### `protocol.isProtocolRegistered(scheme)` _Deprecated_
|
||||
|
||||
* `scheme` string
|
||||
|
||||
Returns `boolean` - Whether `scheme` is already registered.
|
||||
|
||||
### `protocol.interceptFileProtocol(scheme, handler)`
|
||||
### `protocol.interceptFileProtocol(scheme, handler)` _Deprecated_
|
||||
|
||||
* `scheme` string
|
||||
* `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
|
||||
which sends a file as a response.
|
||||
|
||||
### `protocol.interceptStringProtocol(scheme, handler)`
|
||||
### `protocol.interceptStringProtocol(scheme, handler)` _Deprecated_
|
||||
|
||||
* `scheme` string
|
||||
* `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
|
||||
which sends a `string` as a response.
|
||||
|
||||
### `protocol.interceptBufferProtocol(scheme, handler)`
|
||||
### `protocol.interceptBufferProtocol(scheme, handler)` _Deprecated_
|
||||
|
||||
* `scheme` string
|
||||
* `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
|
||||
which sends a `Buffer` as a response.
|
||||
|
||||
### `protocol.interceptHttpProtocol(scheme, handler)`
|
||||
### `protocol.interceptHttpProtocol(scheme, handler)` _Deprecated_
|
||||
|
||||
* `scheme` string
|
||||
* `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
|
||||
which sends a new HTTP request as a response.
|
||||
|
||||
### `protocol.interceptStreamProtocol(scheme, handler)`
|
||||
### `protocol.interceptStreamProtocol(scheme, handler)` _Deprecated_
|
||||
|
||||
* `scheme` string
|
||||
* `handler` Function
|
||||
|
@ -313,7 +377,7 @@ Returns `boolean` - Whether the protocol was successfully intercepted
|
|||
Same as `protocol.registerStreamProtocol`, except that it replaces an existing
|
||||
protocol handler.
|
||||
|
||||
### `protocol.uninterceptProtocol(scheme)`
|
||||
### `protocol.uninterceptProtocol(scheme)` _Deprecated_
|
||||
|
||||
* `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.
|
||||
|
||||
### `protocol.isProtocolIntercepted(scheme)`
|
||||
### `protocol.isProtocolIntercepted(scheme)` _Deprecated_
|
||||
|
||||
* `scheme` string
|
||||
|
||||
|
|
|
@ -784,9 +784,23 @@ Limitations:
|
|||
* The `.type` and `.url` values of the returned `Response` object are
|
||||
incorrect.
|
||||
|
||||
Requests made with `ses.fetch` can be made to [custom protocols](protocol.md)
|
||||
as well as `file:`, and will trigger [webRequest](web-request.md) handlers if
|
||||
present.
|
||||
By default, requests made with `net.fetch` can be made to [custom
|
||||
protocols](protocol.md) as well as `file:`, and will trigger
|
||||
[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()`
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
* `type` 'file' - `file`.
|
||||
* `filePath` string - Path of file to be uploaded.
|
||||
* `offset` Integer - Defaults to `0`.
|
||||
* `length` Integer - Number of bytes to read from `offset`.
|
||||
* `offset` Integer (optional) - Defaults to `0`.
|
||||
* `length` Integer (optional) - Number of bytes to read from `offset`.
|
||||
Defaults to `0`.
|
||||
* `modificationTime` Double - Last Modification time in
|
||||
number of seconds since the UNIX epoch.
|
||||
* `modificationTime` Double (optional) - Last Modification time in
|
||||
number of seconds since the UNIX epoch. Defaults to `0`.
|
||||
|
|
|
@ -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.
|
||||
* **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)
|
||||
|
||||
### API Changed: `nativeImage.createThumbnailFromPath(path, size)`
|
||||
|
|
|
@ -13,7 +13,7 @@ function createDeferredPromise<T, E extends Error = Error> (): { promise: Promis
|
|||
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>();
|
||||
let req: Request;
|
||||
try {
|
||||
|
@ -84,6 +84,8 @@ export function fetchWithSession (input: RequestInfo, init: RequestInit | undefi
|
|||
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.
|
||||
if (req.mode && (req.mode !== 'cors' || origin)) {
|
||||
r.setHeader('Sec-Fetch-Mode', req.mode);
|
||||
|
@ -104,6 +106,7 @@ export function fetchWithSession (input: RequestInfo, init: RequestInit | undefi
|
|||
status: resp.statusCode,
|
||||
statusText: resp.statusMessage
|
||||
});
|
||||
(rResp as any).__original_resp = resp;
|
||||
p.resolve(rResp);
|
||||
});
|
||||
|
||||
|
|
|
@ -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.
|
||||
const protocol = process._linkedBinding('electron_browser_protocol');
|
||||
const { registerSchemesAsPrivileged, getStandardSchemes, Protocol } = process._linkedBinding('electron_browser_protocol');
|
||||
|
||||
// Fallback protocol APIs of default session.
|
||||
Object.setPrototypeOf(protocol, new Proxy({}, {
|
||||
get (_target, property) {
|
||||
if (!app.isReady()) return;
|
||||
const ERR_FAILED = -2;
|
||||
const ERR_UNEXPECTED = -9;
|
||||
|
||||
const protocol = session.defaultSession!.protocol;
|
||||
if (!Object.prototype.hasOwnProperty.call(protocol, property)) return;
|
||||
const isBuiltInScheme = (scheme: string) => scheme === 'http' || scheme === 'https';
|
||||
|
||||
// Returning a native function directly would throw error.
|
||||
return (...args: any[]) => (protocol[property as keyof Electron.Protocol] as Function)(...args);
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ownKeys () {
|
||||
if (!app.isReady()) return [];
|
||||
return Reflect.ownKeys(session.defaultSession!.protocol);
|
||||
},
|
||||
function convertToRequestBody (uploadData: ProtocolRequest['uploadData']): RequestInit['body'] {
|
||||
if (!uploadData) return null;
|
||||
// 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) => {
|
||||
if (!app.isReady()) return false;
|
||||
return Reflect.has(session.defaultSession!.protocol, property);
|
||||
},
|
||||
const chunks = [...uploadData] as any[]; // TODO: types are wrong
|
||||
let current: ReadableStreamDefaultReader | null = null;
|
||||
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 () {
|
||||
return { configurable: true, enumerable: true };
|
||||
}
|
||||
}));
|
||||
Protocol.prototype.handle = function (this: Electron.Protocol, scheme: string, handler: (req: Request) => Response | Promise<Response>) {
|
||||
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;
|
||||
|
|
|
@ -124,5 +124,6 @@ chore_introduce_blocking_api_for_electron.patch
|
|||
chore_patch_out_partition_attribute_dcheck_for_webviews.patch
|
||||
expose_v8initializer_codegenerationcheckcallbackinmainthread.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
|
||||
revert_roll_clang_rust_llvmorg-16-init-17653-g39da55e8-3.patch
|
||||
|
|
|
@ -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,
|
|
@ -273,9 +273,17 @@ gin::Handle<Protocol> Protocol::Create(
|
|||
isolate, new Protocol(isolate, browser_context->protocol_registry()));
|
||||
}
|
||||
|
||||
gin::ObjectTemplateBuilder Protocol::GetObjectTemplateBuilder(
|
||||
v8::Isolate* isolate) {
|
||||
return gin::Wrappable<Protocol>::GetObjectTemplateBuilder(isolate)
|
||||
// static
|
||||
gin::Handle<Protocol> Protocol::New(gin_helper::ErrorThrower thrower) {
|
||||
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",
|
||||
&Protocol::RegisterProtocolFor<ProtocolType::kString>)
|
||||
.SetMethod("registerBufferProtocol",
|
||||
|
@ -304,7 +312,8 @@ gin::ObjectTemplateBuilder Protocol::GetObjectTemplateBuilder(
|
|||
.SetMethod("interceptProtocol",
|
||||
&Protocol::InterceptProtocolFor<ProtocolType::kFree>)
|
||||
.SetMethod("uninterceptProtocol", &Protocol::UninterceptProtocol)
|
||||
.SetMethod("isProtocolIntercepted", &Protocol::IsProtocolIntercepted);
|
||||
.SetMethod("isProtocolIntercepted", &Protocol::IsProtocolIntercepted)
|
||||
.Build();
|
||||
}
|
||||
|
||||
const char* Protocol::GetTypeName() {
|
||||
|
@ -333,6 +342,7 @@ void Initialize(v8::Local<v8::Object> exports,
|
|||
void* priv) {
|
||||
v8::Isolate* isolate = context->GetIsolate();
|
||||
gin_helper::Dictionary dict(isolate, exports);
|
||||
dict.Set("Protocol", electron::api::Protocol::GetConstructor(context));
|
||||
dict.SetMethod("registerSchemesAsPrivileged", &RegisterSchemesAsPrivileged);
|
||||
dict.SetMethod("getStandardSchemes", &electron::api::GetStandardSchemes);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#include "gin/handle.h"
|
||||
#include "gin/wrappable.h"
|
||||
#include "shell/browser/net/electron_url_loader_factory.h"
|
||||
#include "shell/common/gin_helper/constructible.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
|
@ -37,15 +38,19 @@ enum class ProtocolError {
|
|||
};
|
||||
|
||||
// Protocol implementation based on network services.
|
||||
class Protocol : public gin::Wrappable<Protocol> {
|
||||
class Protocol : public gin::Wrappable<Protocol>,
|
||||
public gin_helper::Constructible<Protocol> {
|
||||
public:
|
||||
static gin::Handle<Protocol> Create(v8::Isolate* isolate,
|
||||
ElectronBrowserContext* browser_context);
|
||||
|
||||
static gin::Handle<Protocol> New(gin_helper::ErrorThrower thrower);
|
||||
|
||||
// gin::Wrappable
|
||||
static gin::WrapperInfo kWrapperInfo;
|
||||
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
|
||||
v8::Isolate* isolate) override;
|
||||
static v8::Local<v8::ObjectTemplate> FillObjectTemplate(
|
||||
v8::Isolate* isolate,
|
||||
v8::Local<v8::ObjectTemplate> tmpl);
|
||||
const char* GetTypeName() override;
|
||||
|
||||
private:
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
#include "shell/browser/electron_browser_context.h"
|
||||
#include "shell/browser/javascript_environment.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/common/gin_converters/callback_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
|
||||
// ProxyingURLLoaderFactory would handle them later on, so that we can
|
||||
// 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 =
|
||||
protocol_registry->intercept_handlers().at(url.scheme());
|
||||
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote =
|
||||
|
@ -497,7 +501,8 @@ SimpleURLLoaderWrapper::GetURLLoaderFactoryForURL(const GURL& url) {
|
|||
url_loader_factory = network::SharedURLLoaderFactory::Create(
|
||||
std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
|
||||
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());
|
||||
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote =
|
||||
ElectronURLLoaderFactory::Create(protocol_handler.first,
|
||||
|
@ -528,6 +533,10 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
|||
auto request = std::make_unique<network::ResourceRequest>();
|
||||
opts.Get("method", &request->method);
|
||||
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);
|
||||
opts.Get("referrer", &request->referrer);
|
||||
request->referrer_policy =
|
||||
|
@ -648,7 +657,7 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
|||
|
||||
bool use_session_cookies = false;
|
||||
opts.Get("useSessionCookies", &use_session_cookies);
|
||||
int options = 0;
|
||||
int options = network::mojom::kURLLoadOptionSniffMimeType;
|
||||
if (!credentials_specified && !use_session_cookies) {
|
||||
// This is the default case, as well as the case when credentials is not
|
||||
// specified and useSessionCookies is false. credentials_mode will be
|
||||
|
@ -657,6 +666,11 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
|||
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> chunk_pipe_getter;
|
||||
if (opts.Get("body", &body)) {
|
||||
|
@ -738,6 +752,7 @@ void SimpleURLLoaderWrapper::OnResponseStarted(
|
|||
dict.Set("httpVersion", response_head.headers->GetHttpVersion());
|
||||
dict.Set("headers", response_head.headers.get());
|
||||
dict.Set("rawHeaders", response_head.raw_response_headers);
|
||||
dict.Set("mimeType", response_head.mime_type);
|
||||
Emit("response-started", final_url, dict);
|
||||
}
|
||||
|
||||
|
|
|
@ -85,7 +85,10 @@ void NodeStreamLoader::NotifyComplete(int result) {
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -126,6 +129,8 @@ void NodeStreamLoader::ReadMore() {
|
|||
// Hold the buffer until the write is done.
|
||||
buffer_.Reset(isolate_, buffer);
|
||||
|
||||
bytes_written_ += node::Buffer::Length(buffer);
|
||||
|
||||
// Write buffer to mojo pipe asynchronously.
|
||||
is_reading_ = false;
|
||||
is_writing_ = true;
|
||||
|
|
|
@ -81,6 +81,8 @@ class NodeStreamLoader : public network::mojom::URLLoader {
|
|||
// Whether we are in the middle of a stream.read().
|
||||
bool is_reading_ = false;
|
||||
|
||||
size_t bytes_written_ = 0;
|
||||
|
||||
// When NotifyComplete is called while writing, we will save the result and
|
||||
// quit with it after the write is done.
|
||||
bool ended_ = false;
|
||||
|
|
|
@ -806,18 +806,23 @@ void ProxyingURLLoaderFactory::CreateLoaderAndStart(
|
|||
}
|
||||
|
||||
// Check if user has intercepted this scheme.
|
||||
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());
|
||||
bool bypass_custom_protocol_handlers =
|
||||
options & kBypassCustomProtocolHandlers;
|
||||
if (!bypass_custom_protocol_handlers) {
|
||||
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>>
|
||||
it->second.second.Run(
|
||||
request, base::BindOnce(&ElectronURLLoaderFactory::StartLoading,
|
||||
std::move(loader), request_id, options, request,
|
||||
std::move(client), traffic_annotation,
|
||||
std::move(loader_remote), it->second.first));
|
||||
return;
|
||||
// <scheme, <type, handler>>
|
||||
it->second.second.Run(
|
||||
request,
|
||||
base::BindOnce(&ElectronURLLoaderFactory::StartLoading,
|
||||
std::move(loader), request_id, options, request,
|
||||
std::move(client), traffic_annotation,
|
||||
std::move(loader_remote), it->second.first));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The loader of ServiceWorker forbids loading scripts from file:// URLs, and
|
||||
|
|
|
@ -36,6 +36,8 @@
|
|||
|
||||
namespace electron {
|
||||
|
||||
const uint32_t kBypassCustomProtocolHandlers = 1 << 30;
|
||||
|
||||
// This class is responsible for following tasks when NetworkService is enabled:
|
||||
// 1. handling intercepted protocols;
|
||||
// 2. implementing webRequest module;
|
||||
|
|
|
@ -15,16 +15,21 @@
|
|||
#include "base/values.h"
|
||||
#include "gin/converter.h"
|
||||
#include "gin/dictionary.h"
|
||||
#include "gin/object_template_builder.h"
|
||||
#include "net/cert/x509_certificate.h"
|
||||
#include "net/cert/x509_util.h"
|
||||
#include "net/http/http_response_headers.h"
|
||||
#include "net/http/http_version.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_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/common/gin_converters/gurl_converter.h"
|
||||
#include "shell/common/gin_converters/std_converter.h"
|
||||
#include "shell/common/gin_converters/value_converter.h"
|
||||
#include "shell/common/gin_helper/promise.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
|
||||
namespace gin {
|
||||
|
@ -246,6 +251,246 @@ bool Converter<net::HttpRequestHeaders>::FromV8(v8::Isolate* isolate,
|
|||
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
|
||||
v8::Local<v8::Value> Converter<network::ResourceRequestBody>::ToV8(
|
||||
v8::Isolate* isolate,
|
||||
|
@ -288,6 +533,21 @@ v8::Local<v8::Value> Converter<network::ResourceRequestBody>::ToV8(
|
|||
upload_data.Set("dataPipe", holder);
|
||||
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:
|
||||
NOTREACHED() << "Found unsupported data element";
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ PromiseBase::PromiseBase(v8::Isolate* isolate,
|
|||
context_(isolate, isolate->GetCurrentContext()),
|
||||
resolver_(isolate, handle) {}
|
||||
|
||||
PromiseBase::PromiseBase() : isolate_(nullptr) {}
|
||||
|
||||
PromiseBase::PromiseBase(PromiseBase&&) = default;
|
||||
|
||||
PromiseBase::~PromiseBase() = default;
|
||||
|
|
|
@ -30,6 +30,7 @@ class PromiseBase {
|
|||
public:
|
||||
explicit PromiseBase(v8::Isolate* isolate);
|
||||
PromiseBase(v8::Isolate* isolate, v8::Local<v8::Promise::Resolver> handle);
|
||||
PromiseBase();
|
||||
~PromiseBase();
|
||||
|
||||
// disable copy
|
||||
|
|
|
@ -1438,6 +1438,18 @@ describe('net module', () => {
|
|||
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 when calling getHeader without a name', () => {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { expect } from 'chai';
|
||||
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 path from 'path';
|
||||
import * as url from 'url';
|
||||
import * as http from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as qs from 'querystring';
|
||||
|
@ -10,7 +11,7 @@ import * as stream from 'stream';
|
|||
import { EventEmitter, once } from 'events';
|
||||
import { closeAllWindows, closeWindow } from './lib/window-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';
|
||||
|
||||
const fixturesPath = path.resolve(__dirname, 'fixtures');
|
||||
|
@ -34,7 +35,9 @@ const postData = {
|
|||
};
|
||||
|
||||
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 () {
|
||||
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();
|
||||
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.
|
||||
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 promiseReject: Function = null as unknown as Function;
|
||||
const promise: any = new Promise((resolve, reject) => {
|
||||
|
@ -860,7 +866,7 @@ describe('protocol module', () => {
|
|||
});
|
||||
|
||||
it('can have fetch working in it', async () => {
|
||||
const requestReceived = defer();
|
||||
const requestReceived = deferPromise();
|
||||
const server = http.createServer((req, res) => {
|
||||
res.end();
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import { expect } from 'chai';
|
||||
import * as http from 'http';
|
||||
import * as http2 from 'http2';
|
||||
import * as qs from 'querystring';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as url from 'url';
|
||||
import * as WebSocket from 'ws';
|
||||
import { ipcMain, protocol, session, WebContents, webContents } from 'electron/main';
|
||||
import { Socket } from 'net';
|
||||
import { listen } from './lib/spec-helpers';
|
||||
import { AddressInfo, Socket } from 'net';
|
||||
import { listen, defer } from './lib/spec-helpers';
|
||||
import { once } from 'events';
|
||||
import { ReadableStream } from 'stream/web';
|
||||
|
||||
const fixturesPath = path.resolve(__dirname, 'fixtures');
|
||||
|
||||
|
@ -35,14 +38,35 @@ describe('webRequest module', () => {
|
|||
}
|
||||
});
|
||||
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 () => {
|
||||
protocol.registerStringProtocol('cors', (req, cb) => cb(''));
|
||||
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(() => {
|
||||
server.close();
|
||||
h2server.close();
|
||||
protocol.unregisterProtocol('cors');
|
||||
});
|
||||
|
||||
|
@ -50,6 +74,8 @@ describe('webRequest module', () => {
|
|||
// NB. sandbox: true is used because it makes navigations much (~8x) faster.
|
||||
before(async () => {
|
||||
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'));
|
||||
});
|
||||
after(() => contents.destroy());
|
||||
|
@ -161,6 +187,92 @@ describe('webRequest module', () => {
|
|||
});
|
||||
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', () => {
|
||||
|
|
|
@ -141,6 +141,7 @@ declare namespace NodeJS {
|
|||
hasUserActivation?: boolean;
|
||||
mode?: string;
|
||||
destination?: string;
|
||||
bypassCustomProtocolHandlers?: boolean;
|
||||
};
|
||||
type ResponseHead = {
|
||||
statusCode: number;
|
||||
|
|
|
@ -182,6 +182,11 @@ declare namespace Electron {
|
|||
setBackgroundThrottling(allowed: boolean): void;
|
||||
}
|
||||
|
||||
interface Protocol {
|
||||
registerProtocol(scheme: string, handler: any): boolean;
|
||||
interceptProtocol(scheme: string, handler: any): boolean;
|
||||
}
|
||||
|
||||
namespace Main {
|
||||
class BaseWindow extends Electron.BaseWindow {}
|
||||
class View extends Electron.View {}
|
||||
|
|
Загрузка…
Ссылка в новой задаче