This commit is contained in:
Jeremy Rose 2023-03-27 10:00:55 -07:00 коммит произвёл GitHub
Родитель 6a6908c4c8
Коммит fda8ea9277
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
25 изменённых файлов: 1254 добавлений и 89 удалений

Просмотреть файл

@ -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', () => {

1
typings/internal-ambient.d.ts поставляемый
Просмотреть файл

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

5
typings/internal-electron.d.ts поставляемый
Просмотреть файл

@ -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 {}