feat: implement net.fetch (#36733)
This commit is contained in:
parent
63f94f2359
commit
872d1fe05a
13 changed files with 969 additions and 539 deletions
|
@ -23,12 +23,14 @@ following properties:
|
|||
with which the request is associated. Defaults to the empty string. The
|
||||
`session` option supersedes `partition`. Thus if a `session` is explicitly
|
||||
specified, `partition` is ignored.
|
||||
* `credentials` string (optional) - Can be `include` or `omit`. Whether to
|
||||
send [credentials](https://fetch.spec.whatwg.org/#credentials) with this
|
||||
* `credentials` string (optional) - Can be `include`, `omit` or
|
||||
`same-origin`. Whether to send
|
||||
[credentials](https://fetch.spec.whatwg.org/#credentials) with this
|
||||
request. If set to `include`, credentials from the session associated with
|
||||
the request will be used. If set to `omit`, credentials will not be sent
|
||||
with the request (and the `'login'` event will not be triggered in the
|
||||
event of a 401). This matches the behavior of the
|
||||
event of a 401). If set to `same-origin`, `origin` must also be specified.
|
||||
This matches the behavior of the
|
||||
[fetch](https://fetch.spec.whatwg.org/#concept-request-credentials-mode)
|
||||
option of the same name. If this option is not specified, authentication
|
||||
data from the session will be sent, and cookies will not be sent (unless
|
||||
|
@ -49,6 +51,13 @@ following properties:
|
|||
[`request.followRedirect`](#requestfollowredirect) is invoked synchronously
|
||||
during the [`redirect`](#event-redirect) event. Defaults to `follow`.
|
||||
* `origin` string (optional) - The origin URL of the request.
|
||||
* `referrerPolicy` string (optional) - can be `""`, `no-referrer`,
|
||||
`no-referrer-when-downgrade`, `origin`, `origin-when-cross-origin`,
|
||||
`unsafe-url`, `same-origin`, `strict-origin`, or
|
||||
`strict-origin-when-cross-origin`. Defaults to
|
||||
`strict-origin-when-cross-origin`.
|
||||
* `cache` string (optional) - can be `default`, `no-store`, `reload`,
|
||||
`no-cache`, `force-cache` or `only-if-cached`.
|
||||
|
||||
`options` properties such as `protocol`, `host`, `hostname`, `port` and `path`
|
||||
strictly follow the Node.js model as described in the
|
||||
|
|
|
@ -63,6 +63,44 @@ Creates a [`ClientRequest`](./client-request.md) instance using the provided
|
|||
The `net.request` method would be used to issue both secure and insecure HTTP
|
||||
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)
|
||||
|
||||
Returns `Promise<GlobalResponse>` - see [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response).
|
||||
|
||||
Sends a request, similarly to how `fetch()` works in the renderer, using
|
||||
Chrome's network stack. This differs from Node's `fetch()`, which uses
|
||||
Node.js's HTTP stack.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
async function example () {
|
||||
const response = await net.fetch('https://my.app')
|
||||
if (response.ok) {
|
||||
const body = await response.json()
|
||||
// ... use the result.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This method will issue requests from the [default
|
||||
session](session.md#sessiondefaultsession). To send a `fetch` request from
|
||||
another session, use [ses.fetch()](session.md#sesfetchinput-init).
|
||||
|
||||
See the MDN documentation for
|
||||
[`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/fetch) for more
|
||||
details.
|
||||
|
||||
Limitations:
|
||||
|
||||
* `net.fetch()` does not support the `data:` or `blob:` schemes.
|
||||
* The value of the `integrity` option is ignored.
|
||||
* The `.type` and `.url` values of the returned `Response` object are
|
||||
incorrect.
|
||||
|
||||
### `net.isOnline()`
|
||||
|
||||
Returns `boolean` - Whether there is currently internet connection.
|
||||
|
|
|
@ -731,6 +731,43 @@ Returns `Promise<void>` - Resolves when all connections are closed.
|
|||
|
||||
**Note:** It will terminate / fail all requests currently in flight.
|
||||
|
||||
#### `ses.fetch(input[, init])`
|
||||
|
||||
* `input` string | [GlobalRequest](https://nodejs.org/api/globals.html#request)
|
||||
* `init` [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options) (optional)
|
||||
|
||||
Returns `Promise<GlobalResponse>` - see [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response).
|
||||
|
||||
Sends a request, similarly to how `fetch()` works in the renderer, using
|
||||
Chrome's network stack. This differs from Node's `fetch()`, which uses
|
||||
Node.js's HTTP stack.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
async function example () {
|
||||
const response = await net.fetch('https://my.app')
|
||||
if (response.ok) {
|
||||
const body = await response.json()
|
||||
// ... use the result.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See also [`net.fetch()`](net.md#netfetchinput-init), a convenience method which
|
||||
issues requests from the [default session](#sessiondefaultsession).
|
||||
|
||||
See the MDN documentation for
|
||||
[`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/fetch) for more
|
||||
details.
|
||||
|
||||
Limitations:
|
||||
|
||||
* `net.fetch()` does not support the `data:` or `blob:` schemes.
|
||||
* The value of the `integrity` option is ignored.
|
||||
* The `.type` and `.url` values of the returned `Response` object are
|
||||
incorrect.
|
||||
|
||||
#### `ses.disableNetworkEmulation()`
|
||||
|
||||
Disables any network emulation already active for the `session`. Resets to
|
||||
|
|
|
@ -208,6 +208,8 @@ auto_filenames = {
|
|||
"lib/browser/api/message-channel.ts",
|
||||
"lib/browser/api/module-list.ts",
|
||||
"lib/browser/api/native-theme.ts",
|
||||
"lib/browser/api/net-client-request.ts",
|
||||
"lib/browser/api/net-fetch.ts",
|
||||
"lib/browser/api/net-log.ts",
|
||||
"lib/browser/api/net.ts",
|
||||
"lib/browser/api/notification.ts",
|
||||
|
|
522
lib/browser/api/net-client-request.ts
Normal file
522
lib/browser/api/net-client-request.ts
Normal file
|
@ -0,0 +1,522 @@
|
|||
import * as url from 'url';
|
||||
import { Readable, Writable } from 'stream';
|
||||
import { app } from 'electron/main';
|
||||
import type { ClientRequestConstructorOptions, UploadProgress } from 'electron/main';
|
||||
|
||||
const {
|
||||
isValidHeaderName,
|
||||
isValidHeaderValue,
|
||||
createURLLoader
|
||||
} = process._linkedBinding('electron_browser_net');
|
||||
const { Session } = process._linkedBinding('electron_browser_session');
|
||||
|
||||
const kSupportedProtocols = new Set(['http:', 'https:']);
|
||||
|
||||
// set of headers that Node.js discards duplicates for
|
||||
// see https://nodejs.org/api/http.html#http_message_headers
|
||||
const discardableDuplicateHeaders = new Set([
|
||||
'content-type',
|
||||
'content-length',
|
||||
'user-agent',
|
||||
'referer',
|
||||
'host',
|
||||
'authorization',
|
||||
'proxy-authorization',
|
||||
'if-modified-since',
|
||||
'if-unmodified-since',
|
||||
'from',
|
||||
'location',
|
||||
'max-forwards',
|
||||
'retry-after',
|
||||
'etag',
|
||||
'last-modified',
|
||||
'server',
|
||||
'age',
|
||||
'expires'
|
||||
]);
|
||||
|
||||
class IncomingMessage extends Readable {
|
||||
_shouldPush: boolean = false;
|
||||
_data: (Buffer | null)[] = [];
|
||||
_responseHead: NodeJS.ResponseHead;
|
||||
_resume: (() => void) | null = null;
|
||||
|
||||
constructor (responseHead: NodeJS.ResponseHead) {
|
||||
super();
|
||||
this._responseHead = responseHead;
|
||||
}
|
||||
|
||||
get statusCode () {
|
||||
return this._responseHead.statusCode;
|
||||
}
|
||||
|
||||
get statusMessage () {
|
||||
return this._responseHead.statusMessage;
|
||||
}
|
||||
|
||||
get headers () {
|
||||
const filteredHeaders: Record<string, string | string[]> = {};
|
||||
const { headers, rawHeaders } = this._responseHead;
|
||||
for (const [name, values] of Object.entries(headers)) {
|
||||
filteredHeaders[name] = discardableDuplicateHeaders.has(name) ? values[0] : values.join(', ');
|
||||
}
|
||||
const cookies = rawHeaders.filter(({ key }) => key.toLowerCase() === 'set-cookie').map(({ value }) => value);
|
||||
// keep set-cookie as an array per Node.js rules
|
||||
// see https://nodejs.org/api/http.html#http_message_headers
|
||||
if (cookies.length) { filteredHeaders['set-cookie'] = cookies; }
|
||||
return filteredHeaders;
|
||||
}
|
||||
|
||||
get rawHeaders () {
|
||||
const rawHeadersArr: string[] = [];
|
||||
const { rawHeaders } = this._responseHead;
|
||||
rawHeaders.forEach(header => {
|
||||
rawHeadersArr.push(header.key, header.value);
|
||||
});
|
||||
return rawHeadersArr;
|
||||
}
|
||||
|
||||
get httpVersion () {
|
||||
return `${this.httpVersionMajor}.${this.httpVersionMinor}`;
|
||||
}
|
||||
|
||||
get httpVersionMajor () {
|
||||
return this._responseHead.httpVersion.major;
|
||||
}
|
||||
|
||||
get httpVersionMinor () {
|
||||
return this._responseHead.httpVersion.minor;
|
||||
}
|
||||
|
||||
get rawTrailers () {
|
||||
throw new Error('HTTP trailers are not supported');
|
||||
}
|
||||
|
||||
get trailers () {
|
||||
throw new Error('HTTP trailers are not supported');
|
||||
}
|
||||
|
||||
_storeInternalData (chunk: Buffer | null, resume: (() => void) | null) {
|
||||
// save the network callback for use in _pushInternalData
|
||||
this._resume = resume;
|
||||
this._data.push(chunk);
|
||||
this._pushInternalData();
|
||||
}
|
||||
|
||||
_pushInternalData () {
|
||||
while (this._shouldPush && this._data.length > 0) {
|
||||
const chunk = this._data.shift();
|
||||
this._shouldPush = this.push(chunk);
|
||||
}
|
||||
if (this._shouldPush && this._resume) {
|
||||
// Reset the callback, so that a new one is used for each
|
||||
// batch of throttled data. Do this before calling resume to avoid a
|
||||
// potential race-condition
|
||||
const resume = this._resume;
|
||||
this._resume = null;
|
||||
|
||||
resume();
|
||||
}
|
||||
}
|
||||
|
||||
_read () {
|
||||
this._shouldPush = true;
|
||||
this._pushInternalData();
|
||||
}
|
||||
}
|
||||
|
||||
/** Writable stream that buffers up everything written to it. */
|
||||
class SlurpStream extends Writable {
|
||||
_data: Buffer;
|
||||
constructor () {
|
||||
super();
|
||||
this._data = Buffer.alloc(0);
|
||||
}
|
||||
|
||||
_write (chunk: Buffer, encoding: string, callback: () => void) {
|
||||
this._data = Buffer.concat([this._data, chunk]);
|
||||
callback();
|
||||
}
|
||||
|
||||
data () { return this._data; }
|
||||
}
|
||||
|
||||
class ChunkedBodyStream extends Writable {
|
||||
_pendingChunk: Buffer | undefined;
|
||||
_downstream?: NodeJS.DataPipe;
|
||||
_pendingCallback?: (error?: Error) => void;
|
||||
_clientRequest: ClientRequest;
|
||||
|
||||
constructor (clientRequest: ClientRequest) {
|
||||
super();
|
||||
this._clientRequest = clientRequest;
|
||||
}
|
||||
|
||||
_write (chunk: Buffer, encoding: string, callback: () => void) {
|
||||
if (this._downstream) {
|
||||
this._downstream.write(chunk).then(callback, callback);
|
||||
} else {
|
||||
// the contract of _write is that we won't be called again until we call
|
||||
// the callback, so we're good to just save a single chunk.
|
||||
this._pendingChunk = chunk;
|
||||
this._pendingCallback = callback;
|
||||
|
||||
// The first write to a chunked body stream begins the request.
|
||||
this._clientRequest._startRequest();
|
||||
}
|
||||
}
|
||||
|
||||
_final (callback: () => void) {
|
||||
this._downstream!.done();
|
||||
callback();
|
||||
}
|
||||
|
||||
startReading (pipe: NodeJS.DataPipe) {
|
||||
if (this._downstream) {
|
||||
throw new Error('two startReading calls???');
|
||||
}
|
||||
this._downstream = pipe;
|
||||
if (this._pendingChunk) {
|
||||
const doneWriting = (maybeError: Error | void) => {
|
||||
// If the underlying request has been aborted, we honestly don't care about the error
|
||||
// all work should cease as soon as we abort anyway, this error is probably a
|
||||
// "mojo pipe disconnected" error (code=9)
|
||||
if (this._clientRequest._aborted) return;
|
||||
|
||||
const cb = this._pendingCallback!;
|
||||
delete this._pendingCallback;
|
||||
delete this._pendingChunk;
|
||||
cb(maybeError || undefined);
|
||||
};
|
||||
this._downstream.write(this._pendingChunk).then(doneWriting, doneWriting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RedirectPolicy = 'manual' | 'follow' | 'error';
|
||||
|
||||
function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }> } {
|
||||
const options: any = typeof optionsIn === 'string' ? url.parse(optionsIn) : { ...optionsIn };
|
||||
|
||||
let urlStr: string = options.url;
|
||||
|
||||
if (!urlStr) {
|
||||
const urlObj: url.UrlObject = {};
|
||||
const protocol = options.protocol || 'http:';
|
||||
if (!kSupportedProtocols.has(protocol)) {
|
||||
throw new Error('Protocol "' + protocol + '" not supported');
|
||||
}
|
||||
urlObj.protocol = protocol;
|
||||
|
||||
if (options.host) {
|
||||
urlObj.host = options.host;
|
||||
} else {
|
||||
if (options.hostname) {
|
||||
urlObj.hostname = options.hostname;
|
||||
} else {
|
||||
urlObj.hostname = 'localhost';
|
||||
}
|
||||
|
||||
if (options.port) {
|
||||
urlObj.port = options.port;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.path && / /.test(options.path)) {
|
||||
// The actual regex is more like /[^A-Za-z0-9\-._~!$&'()*+,;=/:@]/
|
||||
// with an additional rule for ignoring percentage-escaped characters
|
||||
// but that's a) hard to capture in a regular expression that performs
|
||||
// well, and b) possibly too restrictive for real-world usage. That's
|
||||
// why it only scans for spaces because those are guaranteed to create
|
||||
// an invalid request.
|
||||
throw new TypeError('Request path contains unescaped characters');
|
||||
}
|
||||
const pathObj = url.parse(options.path || '/');
|
||||
urlObj.pathname = pathObj.pathname;
|
||||
urlObj.search = pathObj.search;
|
||||
urlObj.hash = pathObj.hash;
|
||||
urlStr = url.format(urlObj);
|
||||
}
|
||||
|
||||
const redirectPolicy = options.redirect || 'follow';
|
||||
if (!['follow', 'error', 'manual'].includes(redirectPolicy)) {
|
||||
throw new Error('redirect mode should be one of follow, error or manual');
|
||||
}
|
||||
|
||||
if (options.headers != null && typeof options.headers !== 'object') {
|
||||
throw new TypeError('headers must be an object');
|
||||
}
|
||||
|
||||
const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }> } = {
|
||||
method: (options.method || 'GET').toUpperCase(),
|
||||
url: urlStr,
|
||||
redirectPolicy,
|
||||
headers: {},
|
||||
body: null as any,
|
||||
useSessionCookies: options.useSessionCookies,
|
||||
credentials: options.credentials,
|
||||
origin: options.origin,
|
||||
referrerPolicy: options.referrerPolicy,
|
||||
cache: options.cache
|
||||
};
|
||||
const headers: Record<string, string | string[]> = options.headers || {};
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
if (!isValidHeaderName(name)) {
|
||||
throw new Error(`Invalid header name: '${name}'`);
|
||||
}
|
||||
if (!isValidHeaderValue(value.toString())) {
|
||||
throw new Error(`Invalid value for header '${name}': '${value}'`);
|
||||
}
|
||||
const key = name.toLowerCase();
|
||||
urlLoaderOptions.headers[key] = { name, value };
|
||||
}
|
||||
if (options.session) {
|
||||
if (!(options.session instanceof Session)) { throw new TypeError('`session` should be an instance of the Session class'); }
|
||||
urlLoaderOptions.session = options.session;
|
||||
} else if (options.partition) {
|
||||
if (typeof options.partition === 'string') {
|
||||
urlLoaderOptions.partition = options.partition;
|
||||
} else {
|
||||
throw new TypeError('`partition` should be a string');
|
||||
}
|
||||
}
|
||||
return urlLoaderOptions;
|
||||
}
|
||||
|
||||
export class ClientRequest extends Writable implements Electron.ClientRequest {
|
||||
_started: boolean = false;
|
||||
_firstWrite: boolean = false;
|
||||
_aborted: boolean = false;
|
||||
_chunkedEncoding: boolean | undefined;
|
||||
_body: Writable | undefined;
|
||||
_urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { headers: Record<string, { name: string, value: string | string[] }> };
|
||||
_redirectPolicy: RedirectPolicy;
|
||||
_followRedirectCb?: () => void;
|
||||
_uploadProgress?: { active: boolean, started: boolean, current: number, total: number };
|
||||
_urlLoader?: NodeJS.URLLoader;
|
||||
_response?: IncomingMessage;
|
||||
|
||||
constructor (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
|
||||
super({ autoDestroy: true });
|
||||
|
||||
if (!app.isReady()) {
|
||||
throw new Error('net module can only be used after app is ready');
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
this.once('response', callback);
|
||||
}
|
||||
|
||||
const { redirectPolicy, ...urlLoaderOptions } = parseOptions(options);
|
||||
if (urlLoaderOptions.credentials === 'same-origin' && !urlLoaderOptions.origin) { throw new Error('credentials: same-origin requires origin to be set'); }
|
||||
this._urlLoaderOptions = urlLoaderOptions;
|
||||
this._redirectPolicy = redirectPolicy;
|
||||
}
|
||||
|
||||
get chunkedEncoding () {
|
||||
return this._chunkedEncoding || false;
|
||||
}
|
||||
|
||||
set chunkedEncoding (value: boolean) {
|
||||
if (this._started) {
|
||||
throw new Error('chunkedEncoding can only be set before the request is started');
|
||||
}
|
||||
if (typeof this._chunkedEncoding !== 'undefined') {
|
||||
throw new Error('chunkedEncoding can only be set once');
|
||||
}
|
||||
this._chunkedEncoding = !!value;
|
||||
if (this._chunkedEncoding) {
|
||||
this._body = new ChunkedBodyStream(this);
|
||||
this._urlLoaderOptions.body = (pipe: NodeJS.DataPipe) => {
|
||||
(this._body! as ChunkedBodyStream).startReading(pipe);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setHeader (name: string, value: string) {
|
||||
if (typeof name !== 'string') {
|
||||
throw new TypeError('`name` should be a string in setHeader(name, value)');
|
||||
}
|
||||
if (value == null) {
|
||||
throw new Error('`value` required in setHeader("' + name + '", value)');
|
||||
}
|
||||
if (this._started || this._firstWrite) {
|
||||
throw new Error('Can\'t set headers after they are sent');
|
||||
}
|
||||
if (!isValidHeaderName(name)) {
|
||||
throw new Error(`Invalid header name: '${name}'`);
|
||||
}
|
||||
if (!isValidHeaderValue(value.toString())) {
|
||||
throw new Error(`Invalid value for header '${name}': '${value}'`);
|
||||
}
|
||||
|
||||
const key = name.toLowerCase();
|
||||
this._urlLoaderOptions.headers[key] = { name, value };
|
||||
}
|
||||
|
||||
getHeader (name: string) {
|
||||
if (name == null) {
|
||||
throw new Error('`name` is required for getHeader(name)');
|
||||
}
|
||||
|
||||
const key = name.toLowerCase();
|
||||
const header = this._urlLoaderOptions.headers[key];
|
||||
return header && header.value as any;
|
||||
}
|
||||
|
||||
removeHeader (name: string) {
|
||||
if (name == null) {
|
||||
throw new Error('`name` is required for removeHeader(name)');
|
||||
}
|
||||
|
||||
if (this._started || this._firstWrite) {
|
||||
throw new Error('Can\'t remove headers after they are sent');
|
||||
}
|
||||
|
||||
const key = name.toLowerCase();
|
||||
delete this._urlLoaderOptions.headers[key];
|
||||
}
|
||||
|
||||
_write (chunk: Buffer, encoding: BufferEncoding, callback: () => void) {
|
||||
this._firstWrite = true;
|
||||
if (!this._body) {
|
||||
this._body = new SlurpStream();
|
||||
this._body.on('finish', () => {
|
||||
this._urlLoaderOptions.body = (this._body as SlurpStream).data();
|
||||
this._startRequest();
|
||||
});
|
||||
}
|
||||
// TODO: is this the right way to forward to another stream?
|
||||
this._body.write(chunk, encoding, callback);
|
||||
}
|
||||
|
||||
_final (callback: () => void) {
|
||||
if (this._body) {
|
||||
// TODO: is this the right way to forward to another stream?
|
||||
this._body.end(callback);
|
||||
} else {
|
||||
// end() called without a body, go ahead and start the request
|
||||
this._startRequest();
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
_startRequest () {
|
||||
this._started = true;
|
||||
const stringifyValues = (obj: Record<string, { name: string, value: string | string[] }>) => {
|
||||
const ret: Record<string, string> = {};
|
||||
for (const k of Object.keys(obj)) {
|
||||
const kv = obj[k];
|
||||
ret[kv.name] = kv.value.toString();
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
this._urlLoaderOptions.referrer = this.getHeader('referer') || '';
|
||||
this._urlLoaderOptions.origin = this._urlLoaderOptions.origin || this.getHeader('origin') || '';
|
||||
this._urlLoaderOptions.hasUserActivation = this.getHeader('sec-fetch-user') === '?1';
|
||||
this._urlLoaderOptions.mode = this.getHeader('sec-fetch-mode') || '';
|
||||
this._urlLoaderOptions.destination = this.getHeader('sec-fetch-dest') || '';
|
||||
const opts = { ...this._urlLoaderOptions, extraHeaders: stringifyValues(this._urlLoaderOptions.headers) };
|
||||
this._urlLoader = createURLLoader(opts);
|
||||
this._urlLoader.on('response-started', (event, finalUrl, responseHead) => {
|
||||
const response = this._response = new IncomingMessage(responseHead);
|
||||
this.emit('response', response);
|
||||
});
|
||||
this._urlLoader.on('data', (event, data, resume) => {
|
||||
this._response!._storeInternalData(Buffer.from(data), resume);
|
||||
});
|
||||
this._urlLoader.on('complete', () => {
|
||||
if (this._response) { this._response._storeInternalData(null, null); }
|
||||
});
|
||||
this._urlLoader.on('error', (event, netErrorString) => {
|
||||
const error = new Error(netErrorString);
|
||||
if (this._response) this._response.destroy(error);
|
||||
this._die(error);
|
||||
});
|
||||
|
||||
this._urlLoader.on('login', (event, authInfo, callback) => {
|
||||
const handled = this.emit('login', authInfo, callback);
|
||||
if (!handled) {
|
||||
// If there were no listeners, cancel the authentication request.
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
this._urlLoader.on('redirect', (event, redirectInfo, headers) => {
|
||||
const { statusCode, newMethod, newUrl } = redirectInfo;
|
||||
if (this._redirectPolicy === 'error') {
|
||||
this._die(new Error('Attempted to redirect, but redirect policy was \'error\''));
|
||||
} else if (this._redirectPolicy === 'manual') {
|
||||
let _followRedirect = false;
|
||||
this._followRedirectCb = () => { _followRedirect = true; };
|
||||
try {
|
||||
this.emit('redirect', statusCode, newMethod, newUrl, headers);
|
||||
} finally {
|
||||
this._followRedirectCb = undefined;
|
||||
if (!_followRedirect && !this._aborted) {
|
||||
this._die(new Error('Redirect was cancelled'));
|
||||
}
|
||||
}
|
||||
} else if (this._redirectPolicy === 'follow') {
|
||||
// Calling followRedirect() when the redirect policy is 'follow' is
|
||||
// allowed but does nothing. (Perhaps it should throw an error
|
||||
// though...? Since the redirect will happen regardless.)
|
||||
try {
|
||||
this._followRedirectCb = () => {};
|
||||
this.emit('redirect', statusCode, newMethod, newUrl, headers);
|
||||
} finally {
|
||||
this._followRedirectCb = undefined;
|
||||
}
|
||||
} else {
|
||||
this._die(new Error(`Unexpected redirect policy '${this._redirectPolicy}'`));
|
||||
}
|
||||
});
|
||||
|
||||
this._urlLoader.on('upload-progress', (event, position, total) => {
|
||||
this._uploadProgress = { active: true, started: true, current: position, total };
|
||||
this.emit('upload-progress', position, total); // Undocumented, for now
|
||||
});
|
||||
|
||||
this._urlLoader.on('download-progress', (event, current) => {
|
||||
if (this._response) {
|
||||
this._response.emit('download-progress', current); // Undocumented, for now
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
followRedirect () {
|
||||
if (this._followRedirectCb) {
|
||||
this._followRedirectCb();
|
||||
} else {
|
||||
throw new Error('followRedirect() called, but was not waiting for a redirect');
|
||||
}
|
||||
}
|
||||
|
||||
abort () {
|
||||
if (!this._aborted) {
|
||||
process.nextTick(() => { this.emit('abort'); });
|
||||
}
|
||||
this._aborted = true;
|
||||
this._die();
|
||||
}
|
||||
|
||||
_die (err?: Error) {
|
||||
// Node.js assumes that any stream which is ended is no longer capable of emitted events
|
||||
// which is a faulty assumption for the case of an object that is acting like a stream
|
||||
// (our urlRequest). If we don't emit here, this causes errors since we *do* expect
|
||||
// that error events can be emitted after urlRequest.end().
|
||||
if ((this as any)._writableState.destroyed && err) {
|
||||
this.emit('error', err);
|
||||
}
|
||||
|
||||
this.destroy(err);
|
||||
if (this._urlLoader) {
|
||||
this._urlLoader.cancel();
|
||||
if (this._response) this._response.destroy(err);
|
||||
}
|
||||
}
|
||||
|
||||
getUploadProgress (): UploadProgress {
|
||||
return this._uploadProgress ? { ...this._uploadProgress } : { active: false, started: false, current: 0, total: 0 };
|
||||
}
|
||||
}
|
116
lib/browser/api/net-fetch.ts
Normal file
116
lib/browser/api/net-fetch.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
import { net, IncomingMessage, Session as SessionT } from 'electron/main';
|
||||
import { Readable, Writable, isReadable } from 'stream';
|
||||
|
||||
function createDeferredPromise<T, E extends Error = Error> (): { promise: Promise<T>; resolve: (x: T) => void; reject: (e: E) => void; } {
|
||||
let res: (x: T) => void;
|
||||
let rej: (e: E) => void;
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
res = resolve;
|
||||
rej = reject;
|
||||
});
|
||||
|
||||
return { promise, resolve: res!, reject: rej! };
|
||||
}
|
||||
|
||||
export function fetchWithSession (input: RequestInfo, init: RequestInit | undefined, session: SessionT): Promise<Response> {
|
||||
const p = createDeferredPromise<Response>();
|
||||
let req: Request;
|
||||
try {
|
||||
req = new Request(input, init);
|
||||
} catch (e: any) {
|
||||
p.reject(e);
|
||||
return p.promise;
|
||||
}
|
||||
|
||||
if (req.signal.aborted) {
|
||||
// 1. Abort the fetch() call with p, request, null, and
|
||||
// requestObject’s signal’s abort reason.
|
||||
const error = (req.signal as any).reason ?? new DOMException('The operation was aborted.', 'AbortError');
|
||||
p.reject(error);
|
||||
|
||||
if (req.body != null && isReadable(req.body as unknown as NodeJS.ReadableStream)) {
|
||||
req.body.cancel(error).catch((err) => {
|
||||
if (err.code === 'ERR_INVALID_STATE') {
|
||||
// Node bug?
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Return p.
|
||||
return p.promise;
|
||||
}
|
||||
|
||||
let locallyAborted = false;
|
||||
req.signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
// 1. Set locallyAborted to true.
|
||||
locallyAborted = true;
|
||||
|
||||
// 2. Abort the fetch() call with p, request, responseObject,
|
||||
// and requestObject’s signal’s abort reason.
|
||||
const error = (req.signal as any).reason ?? new DOMException('The operation was aborted.', 'AbortError');
|
||||
p.reject(error);
|
||||
if (req.body != null && isReadable(req.body as unknown as NodeJS.ReadableStream)) {
|
||||
req.body.cancel(error).catch((err) => {
|
||||
if (err.code === 'ERR_INVALID_STATE') {
|
||||
// Node bug?
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
r.abort();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
const origin = req.headers.get('origin') ?? undefined;
|
||||
// We can't set credentials to same-origin unless there's an origin set.
|
||||
const credentials = req.credentials === 'same-origin' && !origin ? 'include' : req.credentials;
|
||||
|
||||
const r = net.request({
|
||||
session,
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
origin,
|
||||
credentials,
|
||||
cache: req.cache,
|
||||
referrerPolicy: req.referrerPolicy,
|
||||
redirect: req.redirect
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
for (const [k, v] of req.headers) {
|
||||
r.setHeader(k, v);
|
||||
}
|
||||
|
||||
r.on('response', (resp: IncomingMessage) => {
|
||||
if (locallyAborted) return;
|
||||
const headers = new Headers();
|
||||
for (const [k, v] of Object.entries(resp.headers)) { headers.set(k, Array.isArray(v) ? v.join(', ') : v); }
|
||||
const nullBodyStatus = [101, 204, 205, 304];
|
||||
const body = nullBodyStatus.includes(resp.statusCode) || req.method === 'HEAD' ? null : Readable.toWeb(resp as unknown as Readable) as ReadableStream;
|
||||
const rResp = new Response(body, {
|
||||
headers,
|
||||
status: resp.statusCode,
|
||||
statusText: resp.statusMessage
|
||||
});
|
||||
p.resolve(rResp);
|
||||
});
|
||||
|
||||
r.on('error', (err) => {
|
||||
p.reject(err);
|
||||
});
|
||||
|
||||
if (!req.body?.pipeTo(Writable.toWeb(r as unknown as Writable)).then(() => r.end())) { r.end(); }
|
||||
|
||||
return p.promise;
|
||||
}
|
|
@ -1,531 +1,17 @@
|
|||
import * as url from 'url';
|
||||
import { Readable, Writable } from 'stream';
|
||||
import { app } from 'electron/main';
|
||||
import type { ClientRequestConstructorOptions, UploadProgress } from 'electron/main';
|
||||
import { IncomingMessage, session } from 'electron/main';
|
||||
import type { ClientRequestConstructorOptions } from 'electron/main';
|
||||
import { ClientRequest } from '@electron/internal/browser/api/net-client-request';
|
||||
|
||||
const {
|
||||
isOnline,
|
||||
isValidHeaderName,
|
||||
isValidHeaderValue,
|
||||
createURLLoader
|
||||
} = process._linkedBinding('electron_browser_net');
|
||||
|
||||
const kSupportedProtocols = new Set(['http:', 'https:']);
|
||||
|
||||
// set of headers that Node.js discards duplicates for
|
||||
// see https://nodejs.org/api/http.html#http_message_headers
|
||||
const discardableDuplicateHeaders = new Set([
|
||||
'content-type',
|
||||
'content-length',
|
||||
'user-agent',
|
||||
'referer',
|
||||
'host',
|
||||
'authorization',
|
||||
'proxy-authorization',
|
||||
'if-modified-since',
|
||||
'if-unmodified-since',
|
||||
'from',
|
||||
'location',
|
||||
'max-forwards',
|
||||
'retry-after',
|
||||
'etag',
|
||||
'last-modified',
|
||||
'server',
|
||||
'age',
|
||||
'expires'
|
||||
]);
|
||||
|
||||
class IncomingMessage extends Readable {
|
||||
_shouldPush: boolean = false;
|
||||
_data: (Buffer | null)[] = [];
|
||||
_responseHead: NodeJS.ResponseHead;
|
||||
_resume: (() => void) | null = null;
|
||||
|
||||
constructor (responseHead: NodeJS.ResponseHead) {
|
||||
super();
|
||||
this._responseHead = responseHead;
|
||||
}
|
||||
|
||||
get statusCode () {
|
||||
return this._responseHead.statusCode;
|
||||
}
|
||||
|
||||
get statusMessage () {
|
||||
return this._responseHead.statusMessage;
|
||||
}
|
||||
|
||||
get headers () {
|
||||
const filteredHeaders: Record<string, string | string[]> = {};
|
||||
const { headers, rawHeaders } = this._responseHead;
|
||||
for (const [name, values] of Object.entries(headers)) {
|
||||
filteredHeaders[name] = discardableDuplicateHeaders.has(name) ? values[0] : values.join(', ');
|
||||
}
|
||||
const cookies = rawHeaders.filter(({ key }) => key.toLowerCase() === 'set-cookie').map(({ value }) => value);
|
||||
// keep set-cookie as an array per Node.js rules
|
||||
// see https://nodejs.org/api/http.html#http_message_headers
|
||||
if (cookies.length) { filteredHeaders['set-cookie'] = cookies; }
|
||||
return filteredHeaders;
|
||||
}
|
||||
|
||||
get rawHeaders () {
|
||||
const rawHeadersArr: string[] = [];
|
||||
const { rawHeaders } = this._responseHead;
|
||||
rawHeaders.forEach(header => {
|
||||
rawHeadersArr.push(header.key, header.value);
|
||||
});
|
||||
return rawHeadersArr;
|
||||
}
|
||||
|
||||
get httpVersion () {
|
||||
return `${this.httpVersionMajor}.${this.httpVersionMinor}`;
|
||||
}
|
||||
|
||||
get httpVersionMajor () {
|
||||
return this._responseHead.httpVersion.major;
|
||||
}
|
||||
|
||||
get httpVersionMinor () {
|
||||
return this._responseHead.httpVersion.minor;
|
||||
}
|
||||
|
||||
get rawTrailers () {
|
||||
throw new Error('HTTP trailers are not supported');
|
||||
}
|
||||
|
||||
get trailers () {
|
||||
throw new Error('HTTP trailers are not supported');
|
||||
}
|
||||
|
||||
_storeInternalData (chunk: Buffer | null, resume: (() => void) | null) {
|
||||
// save the network callback for use in _pushInternalData
|
||||
this._resume = resume;
|
||||
this._data.push(chunk);
|
||||
this._pushInternalData();
|
||||
}
|
||||
|
||||
_pushInternalData () {
|
||||
while (this._shouldPush && this._data.length > 0) {
|
||||
const chunk = this._data.shift();
|
||||
this._shouldPush = this.push(chunk);
|
||||
}
|
||||
if (this._shouldPush && this._resume) {
|
||||
// Reset the callback, so that a new one is used for each
|
||||
// batch of throttled data. Do this before calling resume to avoid a
|
||||
// potential race-condition
|
||||
const resume = this._resume;
|
||||
this._resume = null;
|
||||
|
||||
resume();
|
||||
}
|
||||
}
|
||||
|
||||
_read () {
|
||||
this._shouldPush = true;
|
||||
this._pushInternalData();
|
||||
}
|
||||
}
|
||||
|
||||
/** Writable stream that buffers up everything written to it. */
|
||||
class SlurpStream extends Writable {
|
||||
_data: Buffer;
|
||||
constructor () {
|
||||
super();
|
||||
this._data = Buffer.alloc(0);
|
||||
}
|
||||
|
||||
_write (chunk: Buffer, encoding: string, callback: () => void) {
|
||||
this._data = Buffer.concat([this._data, chunk]);
|
||||
callback();
|
||||
}
|
||||
|
||||
data () { return this._data; }
|
||||
}
|
||||
|
||||
class ChunkedBodyStream extends Writable {
|
||||
_pendingChunk: Buffer | undefined;
|
||||
_downstream?: NodeJS.DataPipe;
|
||||
_pendingCallback?: (error?: Error) => void;
|
||||
_clientRequest: ClientRequest;
|
||||
|
||||
constructor (clientRequest: ClientRequest) {
|
||||
super();
|
||||
this._clientRequest = clientRequest;
|
||||
}
|
||||
|
||||
_write (chunk: Buffer, encoding: string, callback: () => void) {
|
||||
if (this._downstream) {
|
||||
this._downstream.write(chunk).then(callback, callback);
|
||||
} else {
|
||||
// the contract of _write is that we won't be called again until we call
|
||||
// the callback, so we're good to just save a single chunk.
|
||||
this._pendingChunk = chunk;
|
||||
this._pendingCallback = callback;
|
||||
|
||||
// The first write to a chunked body stream begins the request.
|
||||
this._clientRequest._startRequest();
|
||||
}
|
||||
}
|
||||
|
||||
_final (callback: () => void) {
|
||||
this._downstream!.done();
|
||||
callback();
|
||||
}
|
||||
|
||||
startReading (pipe: NodeJS.DataPipe) {
|
||||
if (this._downstream) {
|
||||
throw new Error('two startReading calls???');
|
||||
}
|
||||
this._downstream = pipe;
|
||||
if (this._pendingChunk) {
|
||||
const doneWriting = (maybeError: Error | void) => {
|
||||
// If the underlying request has been aborted, we honestly don't care about the error
|
||||
// all work should cease as soon as we abort anyway, this error is probably a
|
||||
// "mojo pipe disconnected" error (code=9)
|
||||
if (this._clientRequest._aborted) return;
|
||||
|
||||
const cb = this._pendingCallback!;
|
||||
delete this._pendingCallback;
|
||||
delete this._pendingChunk;
|
||||
cb(maybeError || undefined);
|
||||
};
|
||||
this._downstream.write(this._pendingChunk).then(doneWriting, doneWriting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RedirectPolicy = 'manual' | 'follow' | 'error';
|
||||
|
||||
function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }> } {
|
||||
const options: any = typeof optionsIn === 'string' ? url.parse(optionsIn) : { ...optionsIn };
|
||||
|
||||
let urlStr: string = options.url;
|
||||
|
||||
if (!urlStr) {
|
||||
const urlObj: url.UrlObject = {};
|
||||
const protocol = options.protocol || 'http:';
|
||||
if (!kSupportedProtocols.has(protocol)) {
|
||||
throw new Error('Protocol "' + protocol + '" not supported');
|
||||
}
|
||||
urlObj.protocol = protocol;
|
||||
|
||||
if (options.host) {
|
||||
urlObj.host = options.host;
|
||||
} else {
|
||||
if (options.hostname) {
|
||||
urlObj.hostname = options.hostname;
|
||||
} else {
|
||||
urlObj.hostname = 'localhost';
|
||||
}
|
||||
|
||||
if (options.port) {
|
||||
urlObj.port = options.port;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.path && / /.test(options.path)) {
|
||||
// The actual regex is more like /[^A-Za-z0-9\-._~!$&'()*+,;=/:@]/
|
||||
// with an additional rule for ignoring percentage-escaped characters
|
||||
// but that's a) hard to capture in a regular expression that performs
|
||||
// well, and b) possibly too restrictive for real-world usage. That's
|
||||
// why it only scans for spaces because those are guaranteed to create
|
||||
// an invalid request.
|
||||
throw new TypeError('Request path contains unescaped characters');
|
||||
}
|
||||
const pathObj = url.parse(options.path || '/');
|
||||
urlObj.pathname = pathObj.pathname;
|
||||
urlObj.search = pathObj.search;
|
||||
urlObj.hash = pathObj.hash;
|
||||
urlStr = url.format(urlObj);
|
||||
}
|
||||
|
||||
const redirectPolicy = options.redirect || 'follow';
|
||||
if (!['follow', 'error', 'manual'].includes(redirectPolicy)) {
|
||||
throw new Error('redirect mode should be one of follow, error or manual');
|
||||
}
|
||||
|
||||
if (options.headers != null && typeof options.headers !== 'object') {
|
||||
throw new TypeError('headers must be an object');
|
||||
}
|
||||
|
||||
const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }> } = {
|
||||
method: (options.method || 'GET').toUpperCase(),
|
||||
url: urlStr,
|
||||
redirectPolicy,
|
||||
headers: {},
|
||||
body: null as any,
|
||||
useSessionCookies: options.useSessionCookies,
|
||||
credentials: options.credentials,
|
||||
origin: options.origin
|
||||
};
|
||||
const headers: Record<string, string | string[]> = options.headers || {};
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
if (!isValidHeaderName(name)) {
|
||||
throw new Error(`Invalid header name: '${name}'`);
|
||||
}
|
||||
if (!isValidHeaderValue(value.toString())) {
|
||||
throw new Error(`Invalid value for header '${name}': '${value}'`);
|
||||
}
|
||||
const key = name.toLowerCase();
|
||||
urlLoaderOptions.headers[key] = { name, value };
|
||||
}
|
||||
if (options.session) {
|
||||
// Weak check, but it should be enough to catch 99% of accidental misuses.
|
||||
if (options.session.constructor && options.session.constructor.name === 'Session') {
|
||||
urlLoaderOptions.session = options.session;
|
||||
} else {
|
||||
throw new TypeError('`session` should be an instance of the Session class');
|
||||
}
|
||||
} else if (options.partition) {
|
||||
if (typeof options.partition === 'string') {
|
||||
urlLoaderOptions.partition = options.partition;
|
||||
} else {
|
||||
throw new TypeError('`partition` should be a string');
|
||||
}
|
||||
}
|
||||
return urlLoaderOptions;
|
||||
}
|
||||
|
||||
export class ClientRequest extends Writable implements Electron.ClientRequest {
|
||||
_started: boolean = false;
|
||||
_firstWrite: boolean = false;
|
||||
_aborted: boolean = false;
|
||||
_chunkedEncoding: boolean | undefined;
|
||||
_body: Writable | undefined;
|
||||
_urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { headers: Record<string, { name: string, value: string | string[] }> };
|
||||
_redirectPolicy: RedirectPolicy;
|
||||
_followRedirectCb?: () => void;
|
||||
_uploadProgress?: { active: boolean, started: boolean, current: number, total: number };
|
||||
_urlLoader?: NodeJS.URLLoader;
|
||||
_response?: IncomingMessage;
|
||||
|
||||
constructor (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
|
||||
super({ autoDestroy: true });
|
||||
|
||||
if (!app.isReady()) {
|
||||
throw new Error('net module can only be used after app is ready');
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
this.once('response', callback);
|
||||
}
|
||||
|
||||
const { redirectPolicy, ...urlLoaderOptions } = parseOptions(options);
|
||||
this._urlLoaderOptions = urlLoaderOptions;
|
||||
this._redirectPolicy = redirectPolicy;
|
||||
}
|
||||
|
||||
get chunkedEncoding () {
|
||||
return this._chunkedEncoding || false;
|
||||
}
|
||||
|
||||
set chunkedEncoding (value: boolean) {
|
||||
if (this._started) {
|
||||
throw new Error('chunkedEncoding can only be set before the request is started');
|
||||
}
|
||||
if (typeof this._chunkedEncoding !== 'undefined') {
|
||||
throw new Error('chunkedEncoding can only be set once');
|
||||
}
|
||||
this._chunkedEncoding = !!value;
|
||||
if (this._chunkedEncoding) {
|
||||
this._body = new ChunkedBodyStream(this);
|
||||
this._urlLoaderOptions.body = (pipe: NodeJS.DataPipe) => {
|
||||
(this._body! as ChunkedBodyStream).startReading(pipe);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setHeader (name: string, value: string) {
|
||||
if (typeof name !== 'string') {
|
||||
throw new TypeError('`name` should be a string in setHeader(name, value)');
|
||||
}
|
||||
if (value == null) {
|
||||
throw new Error('`value` required in setHeader("' + name + '", value)');
|
||||
}
|
||||
if (this._started || this._firstWrite) {
|
||||
throw new Error('Can\'t set headers after they are sent');
|
||||
}
|
||||
if (!isValidHeaderName(name)) {
|
||||
throw new Error(`Invalid header name: '${name}'`);
|
||||
}
|
||||
if (!isValidHeaderValue(value.toString())) {
|
||||
throw new Error(`Invalid value for header '${name}': '${value}'`);
|
||||
}
|
||||
|
||||
const key = name.toLowerCase();
|
||||
this._urlLoaderOptions.headers[key] = { name, value };
|
||||
}
|
||||
|
||||
getHeader (name: string) {
|
||||
if (name == null) {
|
||||
throw new Error('`name` is required for getHeader(name)');
|
||||
}
|
||||
|
||||
const key = name.toLowerCase();
|
||||
const header = this._urlLoaderOptions.headers[key];
|
||||
return header && header.value as any;
|
||||
}
|
||||
|
||||
removeHeader (name: string) {
|
||||
if (name == null) {
|
||||
throw new Error('`name` is required for removeHeader(name)');
|
||||
}
|
||||
|
||||
if (this._started || this._firstWrite) {
|
||||
throw new Error('Can\'t remove headers after they are sent');
|
||||
}
|
||||
|
||||
const key = name.toLowerCase();
|
||||
delete this._urlLoaderOptions.headers[key];
|
||||
}
|
||||
|
||||
_write (chunk: Buffer, encoding: BufferEncoding, callback: () => void) {
|
||||
this._firstWrite = true;
|
||||
if (!this._body) {
|
||||
this._body = new SlurpStream();
|
||||
this._body.on('finish', () => {
|
||||
this._urlLoaderOptions.body = (this._body as SlurpStream).data();
|
||||
this._startRequest();
|
||||
});
|
||||
}
|
||||
// TODO: is this the right way to forward to another stream?
|
||||
this._body.write(chunk, encoding, callback);
|
||||
}
|
||||
|
||||
_final (callback: () => void) {
|
||||
if (this._body) {
|
||||
// TODO: is this the right way to forward to another stream?
|
||||
this._body.end(callback);
|
||||
} else {
|
||||
// end() called without a body, go ahead and start the request
|
||||
this._startRequest();
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
_startRequest () {
|
||||
this._started = true;
|
||||
const stringifyValues = (obj: Record<string, { name: string, value: string | string[] }>) => {
|
||||
const ret: Record<string, string> = {};
|
||||
for (const k of Object.keys(obj)) {
|
||||
const kv = obj[k];
|
||||
ret[kv.name] = kv.value.toString();
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
this._urlLoaderOptions.referrer = this.getHeader('referer') || '';
|
||||
this._urlLoaderOptions.origin = this._urlLoaderOptions.origin || this.getHeader('origin') || '';
|
||||
this._urlLoaderOptions.hasUserActivation = this.getHeader('sec-fetch-user') === '?1';
|
||||
this._urlLoaderOptions.mode = this.getHeader('sec-fetch-mode') || '';
|
||||
this._urlLoaderOptions.destination = this.getHeader('sec-fetch-dest') || '';
|
||||
const opts = { ...this._urlLoaderOptions, extraHeaders: stringifyValues(this._urlLoaderOptions.headers) };
|
||||
this._urlLoader = createURLLoader(opts);
|
||||
this._urlLoader.on('response-started', (event, finalUrl, responseHead) => {
|
||||
const response = this._response = new IncomingMessage(responseHead);
|
||||
this.emit('response', response);
|
||||
});
|
||||
this._urlLoader.on('data', (event, data, resume) => {
|
||||
this._response!._storeInternalData(Buffer.from(data), resume);
|
||||
});
|
||||
this._urlLoader.on('complete', () => {
|
||||
if (this._response) { this._response._storeInternalData(null, null); }
|
||||
});
|
||||
this._urlLoader.on('error', (event, netErrorString) => {
|
||||
const error = new Error(netErrorString);
|
||||
if (this._response) this._response.destroy(error);
|
||||
this._die(error);
|
||||
});
|
||||
|
||||
this._urlLoader.on('login', (event, authInfo, callback) => {
|
||||
const handled = this.emit('login', authInfo, callback);
|
||||
if (!handled) {
|
||||
// If there were no listeners, cancel the authentication request.
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
this._urlLoader.on('redirect', (event, redirectInfo, headers) => {
|
||||
const { statusCode, newMethod, newUrl } = redirectInfo;
|
||||
if (this._redirectPolicy === 'error') {
|
||||
this._die(new Error('Attempted to redirect, but redirect policy was \'error\''));
|
||||
} else if (this._redirectPolicy === 'manual') {
|
||||
let _followRedirect = false;
|
||||
this._followRedirectCb = () => { _followRedirect = true; };
|
||||
try {
|
||||
this.emit('redirect', statusCode, newMethod, newUrl, headers);
|
||||
} finally {
|
||||
this._followRedirectCb = undefined;
|
||||
if (!_followRedirect && !this._aborted) {
|
||||
this._die(new Error('Redirect was cancelled'));
|
||||
}
|
||||
}
|
||||
} else if (this._redirectPolicy === 'follow') {
|
||||
// Calling followRedirect() when the redirect policy is 'follow' is
|
||||
// allowed but does nothing. (Perhaps it should throw an error
|
||||
// though...? Since the redirect will happen regardless.)
|
||||
try {
|
||||
this._followRedirectCb = () => {};
|
||||
this.emit('redirect', statusCode, newMethod, newUrl, headers);
|
||||
} finally {
|
||||
this._followRedirectCb = undefined;
|
||||
}
|
||||
} else {
|
||||
this._die(new Error(`Unexpected redirect policy '${this._redirectPolicy}'`));
|
||||
}
|
||||
});
|
||||
|
||||
this._urlLoader.on('upload-progress', (event, position, total) => {
|
||||
this._uploadProgress = { active: true, started: true, current: position, total };
|
||||
this.emit('upload-progress', position, total); // Undocumented, for now
|
||||
});
|
||||
|
||||
this._urlLoader.on('download-progress', (event, current) => {
|
||||
if (this._response) {
|
||||
this._response.emit('download-progress', current); // Undocumented, for now
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
followRedirect () {
|
||||
if (this._followRedirectCb) {
|
||||
this._followRedirectCb();
|
||||
} else {
|
||||
throw new Error('followRedirect() called, but was not waiting for a redirect');
|
||||
}
|
||||
}
|
||||
|
||||
abort () {
|
||||
if (!this._aborted) {
|
||||
process.nextTick(() => { this.emit('abort'); });
|
||||
}
|
||||
this._aborted = true;
|
||||
this._die();
|
||||
}
|
||||
|
||||
_die (err?: Error) {
|
||||
// Node.js assumes that any stream which is ended is no longer capable of emitted events
|
||||
// which is a faulty assumption for the case of an object that is acting like a stream
|
||||
// (our urlRequest). If we don't emit here, this causes errors since we *do* expect
|
||||
// that error events can be emitted after urlRequest.end().
|
||||
if ((this as any)._writableState.destroyed && err) {
|
||||
this.emit('error', err);
|
||||
}
|
||||
|
||||
this.destroy(err);
|
||||
if (this._urlLoader) {
|
||||
this._urlLoader.cancel();
|
||||
if (this._response) this._response.destroy(err);
|
||||
}
|
||||
}
|
||||
|
||||
getUploadProgress (): UploadProgress {
|
||||
return this._uploadProgress ? { ...this._uploadProgress } : { active: false, started: false, current: 0, total: 0 };
|
||||
}
|
||||
}
|
||||
const { isOnline } = process._linkedBinding('electron_browser_net');
|
||||
|
||||
export function request (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
|
||||
return new ClientRequest(options, callback);
|
||||
}
|
||||
|
||||
export function fetch (input: RequestInfo, init?: RequestInit): Promise<Response> {
|
||||
return session.defaultSession.fetch(input, init);
|
||||
}
|
||||
|
||||
exports.isOnline = isOnline;
|
||||
|
||||
Object.defineProperty(exports, 'online', {
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
const { fromPartition } = process._linkedBinding('electron_browser_session');
|
||||
import { fetchWithSession } from '@electron/internal/browser/api/net-fetch';
|
||||
const { fromPartition, Session } = process._linkedBinding('electron_browser_session');
|
||||
|
||||
Session.prototype.fetch = function (input: RequestInfo, init?: RequestInit) {
|
||||
return fetchWithSession(input, init, this);
|
||||
};
|
||||
|
||||
export default {
|
||||
fromPartition,
|
||||
|
|
|
@ -1203,10 +1203,16 @@ gin::Handle<Session> Session::FromPartition(v8::Isolate* isolate,
|
|||
return CreateFrom(isolate, browser_context);
|
||||
}
|
||||
|
||||
gin::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
|
||||
v8::Isolate* isolate) {
|
||||
return gin_helper::EventEmitterMixin<Session>::GetObjectTemplateBuilder(
|
||||
isolate)
|
||||
// static
|
||||
gin::Handle<Session> Session::New() {
|
||||
gin_helper::ErrorThrower(JavascriptEnvironment::GetIsolate())
|
||||
.ThrowError("Session objects cannot be created with 'new'");
|
||||
return gin::Handle<Session>();
|
||||
}
|
||||
|
||||
void Session::FillObjectTemplate(v8::Isolate* isolate,
|
||||
v8::Local<v8::ObjectTemplate> templ) {
|
||||
gin::ObjectTemplateBuilder(isolate, "Session", templ)
|
||||
.SetMethod("resolveProxy", &Session::ResolveProxy)
|
||||
.SetMethod("getCacheSize", &Session::GetCacheSize)
|
||||
.SetMethod("clearCache", &Session::ClearCache)
|
||||
|
@ -1276,7 +1282,8 @@ gin::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
|
|||
.SetProperty("protocol", &Session::Protocol)
|
||||
.SetProperty("serviceWorkers", &Session::ServiceWorkerContext)
|
||||
.SetProperty("webRequest", &Session::WebRequest)
|
||||
.SetProperty("storagePath", &Session::GetPath);
|
||||
.SetProperty("storagePath", &Session::GetPath)
|
||||
.Build();
|
||||
}
|
||||
|
||||
const char* Session::GetTypeName() {
|
||||
|
@ -1307,6 +1314,7 @@ void Initialize(v8::Local<v8::Object> exports,
|
|||
void* priv) {
|
||||
v8::Isolate* isolate = context->GetIsolate();
|
||||
gin_helper::Dictionary dict(isolate, exports);
|
||||
dict.Set("Session", Session::GetConstructor(context));
|
||||
dict.SetMethod("fromPartition", &FromPartition);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#include "shell/browser/event_emitter_mixin.h"
|
||||
#include "shell/browser/net/resolve_proxy_helper.h"
|
||||
#include "shell/common/gin_helper/cleaned_up_at_exit.h"
|
||||
#include "shell/common/gin_helper/constructible.h"
|
||||
#include "shell/common/gin_helper/error_thrower.h"
|
||||
#include "shell/common/gin_helper/function_template_extensions.h"
|
||||
#include "shell/common/gin_helper/pinnable.h"
|
||||
|
@ -57,6 +58,7 @@ namespace api {
|
|||
|
||||
class Session : public gin::Wrappable<Session>,
|
||||
public gin_helper::Pinnable<Session>,
|
||||
public gin_helper::Constructible<Session>,
|
||||
public gin_helper::EventEmitterMixin<Session>,
|
||||
public gin_helper::CleanedUpAtExit,
|
||||
#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
|
||||
|
@ -71,6 +73,7 @@ class Session : public gin::Wrappable<Session>,
|
|||
static gin::Handle<Session> CreateFrom(
|
||||
v8::Isolate* isolate,
|
||||
ElectronBrowserContext* browser_context);
|
||||
static gin::Handle<Session> New(); // Dummy, do not use!
|
||||
|
||||
static Session* FromBrowserContext(content::BrowserContext* context);
|
||||
|
||||
|
@ -83,8 +86,7 @@ class Session : public gin::Wrappable<Session>,
|
|||
|
||||
// gin::Wrappable
|
||||
static gin::WrapperInfo kWrapperInfo;
|
||||
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
|
||||
v8::Isolate* isolate) override;
|
||||
static void FillObjectTemplate(v8::Isolate*, v8::Local<v8::ObjectTemplate>);
|
||||
const char* GetTypeName() override;
|
||||
|
||||
// Methods.
|
||||
|
|
|
@ -32,6 +32,8 @@
|
|||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/gin_helper/object_template_builder.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
#include "third_party/blink/public/common/loader/referrer_utils.h"
|
||||
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom.h"
|
||||
|
||||
namespace gin {
|
||||
|
||||
|
@ -59,15 +61,84 @@ struct Converter<network::mojom::CredentialsMode> {
|
|||
*out = network::mojom::CredentialsMode::kOmit;
|
||||
else if (mode == "include")
|
||||
*out = network::mojom::CredentialsMode::kInclude;
|
||||
else if (mode == "same-origin")
|
||||
// Note: This only makes sense if the request specifies the "origin"
|
||||
// option.
|
||||
*out = network::mojom::CredentialsMode::kSameOrigin;
|
||||
else
|
||||
// "same-origin" is technically a member of this enum as well, but it
|
||||
// doesn't make sense in the context of `net.request()`, so don't convert
|
||||
// it.
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Converter<blink::mojom::FetchCacheMode> {
|
||||
static bool FromV8(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> val,
|
||||
blink::mojom::FetchCacheMode* out) {
|
||||
std::string cache;
|
||||
if (!ConvertFromV8(isolate, val, &cache))
|
||||
return false;
|
||||
if (cache == "default") {
|
||||
*out = blink::mojom::FetchCacheMode::kDefault;
|
||||
} else if (cache == "no-store") {
|
||||
*out = blink::mojom::FetchCacheMode::kNoStore;
|
||||
} else if (cache == "reload") {
|
||||
*out = blink::mojom::FetchCacheMode::kBypassCache;
|
||||
} else if (cache == "no-cache") {
|
||||
*out = blink::mojom::FetchCacheMode::kValidateCache;
|
||||
} else if (cache == "force-cache") {
|
||||
*out = blink::mojom::FetchCacheMode::kForceCache;
|
||||
} else if (cache == "only-if-cached") {
|
||||
*out = blink::mojom::FetchCacheMode::kOnlyIfCached;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Converter<net::ReferrerPolicy> {
|
||||
static bool FromV8(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> val,
|
||||
net::ReferrerPolicy* out) {
|
||||
std::string referrer_policy;
|
||||
if (!ConvertFromV8(isolate, val, &referrer_policy))
|
||||
return false;
|
||||
if (base::CompareCaseInsensitiveASCII(referrer_policy, "no-referrer") ==
|
||||
0) {
|
||||
*out = net::ReferrerPolicy::NO_REFERRER;
|
||||
} else if (base::CompareCaseInsensitiveASCII(
|
||||
referrer_policy, "no-referrer-when-downgrade") == 0) {
|
||||
*out = net::ReferrerPolicy::CLEAR_ON_TRANSITION_FROM_SECURE_TO_INSECURE;
|
||||
} else if (base::CompareCaseInsensitiveASCII(referrer_policy, "origin") ==
|
||||
0) {
|
||||
*out = net::ReferrerPolicy::ORIGIN;
|
||||
} else if (base::CompareCaseInsensitiveASCII(
|
||||
referrer_policy, "origin-when-cross-origin") == 0) {
|
||||
*out = net::ReferrerPolicy::ORIGIN_ONLY_ON_TRANSITION_CROSS_ORIGIN;
|
||||
} else if (base::CompareCaseInsensitiveASCII(referrer_policy,
|
||||
"unsafe-url") == 0) {
|
||||
*out = net::ReferrerPolicy::NEVER_CLEAR;
|
||||
} else if (base::CompareCaseInsensitiveASCII(referrer_policy,
|
||||
"same-origin") == 0) {
|
||||
*out = net::ReferrerPolicy::CLEAR_ON_TRANSITION_CROSS_ORIGIN;
|
||||
} else if (base::CompareCaseInsensitiveASCII(referrer_policy,
|
||||
"strict-origin") == 0) {
|
||||
*out = net::ReferrerPolicy::
|
||||
ORIGIN_CLEAR_ON_TRANSITION_FROM_SECURE_TO_INSECURE;
|
||||
} else if (referrer_policy == "" ||
|
||||
base::CompareCaseInsensitiveASCII(
|
||||
referrer_policy, "strict-origin-when-cross-origin") == 0) {
|
||||
*out = net::ReferrerPolicy::REDUCE_GRANULARITY_ON_TRANSITION_CROSS_ORIGIN;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace gin
|
||||
|
||||
namespace electron::api {
|
||||
|
@ -401,6 +472,9 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
|||
opts.Get("url", &request->url);
|
||||
request->site_for_cookies = net::SiteForCookies::FromUrl(request->url);
|
||||
opts.Get("referrer", &request->referrer);
|
||||
request->referrer_policy =
|
||||
blink::ReferrerUtils::GetDefaultNetReferrerPolicy();
|
||||
opts.Get("referrerPolicy", &request->referrer_policy);
|
||||
std::string origin;
|
||||
opts.Get("origin", &origin);
|
||||
if (!origin.empty()) {
|
||||
|
@ -484,6 +558,36 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
|||
}
|
||||
}
|
||||
|
||||
blink::mojom::FetchCacheMode cache_mode =
|
||||
blink::mojom::FetchCacheMode::kDefault;
|
||||
opts.Get("cache", &cache_mode);
|
||||
switch (cache_mode) {
|
||||
case blink::mojom::FetchCacheMode::kNoStore:
|
||||
request->load_flags |= net::LOAD_DISABLE_CACHE;
|
||||
break;
|
||||
case blink::mojom::FetchCacheMode::kValidateCache:
|
||||
request->load_flags |= net::LOAD_VALIDATE_CACHE;
|
||||
break;
|
||||
case blink::mojom::FetchCacheMode::kBypassCache:
|
||||
request->load_flags |= net::LOAD_BYPASS_CACHE;
|
||||
break;
|
||||
case blink::mojom::FetchCacheMode::kForceCache:
|
||||
request->load_flags |= net::LOAD_SKIP_CACHE_VALIDATION;
|
||||
break;
|
||||
case blink::mojom::FetchCacheMode::kOnlyIfCached:
|
||||
request->load_flags |=
|
||||
net::LOAD_ONLY_FROM_CACHE | net::LOAD_SKIP_CACHE_VALIDATION;
|
||||
break;
|
||||
case blink::mojom::FetchCacheMode::kUnspecifiedOnlyIfCachedStrict:
|
||||
request->load_flags |= net::LOAD_ONLY_FROM_CACHE;
|
||||
break;
|
||||
case blink::mojom::FetchCacheMode::kDefault:
|
||||
break;
|
||||
case blink::mojom::FetchCacheMode::kUnspecifiedForceCacheMiss:
|
||||
request->load_flags |= net::LOAD_ONLY_FROM_CACHE | net::LOAD_BYPASS_CACHE;
|
||||
break;
|
||||
}
|
||||
|
||||
bool use_session_cookies = false;
|
||||
opts.Get("useSessionCookies", &use_session_cookies);
|
||||
int options = 0;
|
||||
|
|
|
@ -1610,7 +1610,15 @@ describe('net module', () => {
|
|||
response.statusMessage = 'OK';
|
||||
response.end();
|
||||
});
|
||||
const urlRequest = net.request(serverUrl);
|
||||
// The referrerPolicy must be unsafe-url because the referrer's origin
|
||||
// doesn't match the loaded page. With the default referrer policy
|
||||
// (strict-origin-when-cross-origin), the request will be canceled by the
|
||||
// network service when the referrer header is invalid.
|
||||
// See:
|
||||
// - https://source.chromium.org/chromium/chromium/src/+/main:net/url_request/url_request.cc;l=682-683;drc=ae587fa7cd2e5cc308ce69353ee9ce86437e5d41
|
||||
// - https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/mojom/network_context.mojom;l=316-318;drc=ae5c7fcf09509843c1145f544cce3a61874b9698
|
||||
// - https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
|
||||
const urlRequest = net.request({ url: serverUrl, referrerPolicy: 'unsafe-url' });
|
||||
urlRequest.setHeader('referer', referrerURL);
|
||||
urlRequest.end();
|
||||
|
||||
|
@ -2035,4 +2043,95 @@ describe('net module', () => {
|
|||
await collectStreamBody(await getResponse(urlRequest));
|
||||
});
|
||||
});
|
||||
|
||||
describe('net.fetch', () => {
|
||||
// NB. there exist much more comprehensive tests for fetch() in the form of
|
||||
// the WPT: https://github.com/web-platform-tests/wpt/tree/master/fetch
|
||||
// It's possible to run these tests against net.fetch(), but the test
|
||||
// harness to do so is quite complex and hasn't been munged to smoothly run
|
||||
// inside the Electron test runner yet.
|
||||
//
|
||||
// In the meantime, here are some tests for basic functionality and
|
||||
// Electron-specific behavior.
|
||||
|
||||
describe('basic', () => {
|
||||
it('can fetch http urls', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
response.end('test');
|
||||
});
|
||||
const resp = await net.fetch(serverUrl);
|
||||
expect(resp.ok).to.be.true();
|
||||
expect(await resp.text()).to.equal('test');
|
||||
});
|
||||
|
||||
it('can upload a string body', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
request.on('data', chunk => response.write(chunk));
|
||||
request.on('end', () => response.end());
|
||||
});
|
||||
const resp = await net.fetch(serverUrl, {
|
||||
method: 'POST',
|
||||
body: 'anchovies'
|
||||
});
|
||||
expect(await resp.text()).to.equal('anchovies');
|
||||
});
|
||||
|
||||
it('can read response as an array buffer', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
request.on('data', chunk => response.write(chunk));
|
||||
request.on('end', () => response.end());
|
||||
});
|
||||
const resp = await net.fetch(serverUrl, {
|
||||
method: 'POST',
|
||||
body: 'anchovies'
|
||||
});
|
||||
expect(new TextDecoder().decode(new Uint8Array(await resp.arrayBuffer()))).to.equal('anchovies');
|
||||
});
|
||||
|
||||
it('can read response as form data', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
response.setHeader('content-type', 'application/x-www-form-urlencoded');
|
||||
response.end('foo=bar');
|
||||
});
|
||||
const resp = await net.fetch(serverUrl);
|
||||
const result = await resp.formData();
|
||||
expect(result.get('foo')).to.equal('bar');
|
||||
});
|
||||
|
||||
it('should be able to use a session cookie store', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
response.statusCode = 200;
|
||||
response.statusMessage = 'OK';
|
||||
response.setHeader('x-cookie', request.headers.cookie!);
|
||||
response.end();
|
||||
});
|
||||
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||
const cookieVal = `${Date.now()}`;
|
||||
await sess.cookies.set({
|
||||
url: serverUrl,
|
||||
name: 'wild_cookie',
|
||||
value: cookieVal
|
||||
});
|
||||
const response = await sess.fetch(serverUrl, {
|
||||
credentials: 'include'
|
||||
});
|
||||
expect(response.headers.get('x-cookie')).to.equal(`wild_cookie=${cookieVal}`);
|
||||
});
|
||||
|
||||
it('should reject promise on DNS failure', async () => {
|
||||
const r = net.fetch('https://i.do.not.exist');
|
||||
await expect(r).to.be.rejectedWith(/ERR_NAME_NOT_RESOLVED/);
|
||||
});
|
||||
|
||||
it('should reject body promise when stream fails', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
response.write('first chunk');
|
||||
setTimeout(() => response.destroy());
|
||||
});
|
||||
const r = await net.fetch(serverUrl);
|
||||
expect(r.status).to.equal(200);
|
||||
await expect(r.text()).to.be.rejectedWith(/ERR_INCOMPLETE_CHUNKED_ENCODING/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
6
typings/internal-ambient.d.ts
vendored
6
typings/internal-ambient.d.ts
vendored
|
@ -130,11 +130,13 @@ declare namespace NodeJS {
|
|||
url: string;
|
||||
extraHeaders?: Record<string, string>;
|
||||
useSessionCookies?: boolean;
|
||||
credentials?: 'include' | 'omit';
|
||||
credentials?: 'include' | 'omit' | 'same-origin';
|
||||
body: Uint8Array | BodyFunc;
|
||||
session?: Electron.Session;
|
||||
partition?: string;
|
||||
referrer?: string;
|
||||
referrerPolicy?: string;
|
||||
cache?: string;
|
||||
origin?: string;
|
||||
hasUserActivation?: boolean;
|
||||
mode?: string;
|
||||
|
@ -224,7 +226,7 @@ declare namespace NodeJS {
|
|||
_linkedBinding(name: 'electron_browser_power_save_blocker'): { powerSaveBlocker: Electron.PowerSaveBlocker };
|
||||
_linkedBinding(name: 'electron_browser_push_notifications'): { pushNotifications: Electron.PushNotifications };
|
||||
_linkedBinding(name: 'electron_browser_safe_storage'): { safeStorage: Electron.SafeStorage };
|
||||
_linkedBinding(name: 'electron_browser_session'): typeof Electron.Session;
|
||||
_linkedBinding(name: 'electron_browser_session'): {fromPartition: typeof Electron.Session.fromPartition, Session: typeof Electron.Session};
|
||||
_linkedBinding(name: 'electron_browser_screen'): { createScreen(): Electron.Screen };
|
||||
_linkedBinding(name: 'electron_browser_system_preferences'): { systemPreferences: Electron.SystemPreferences };
|
||||
_linkedBinding(name: 'electron_browser_tray'): { Tray: Electron.Tray };
|
||||
|
|
Loading…
Reference in a new issue