Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add protocol.handle #36674

Merged
merged 40 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
02d618d
feat: add protocol.registerProtocol
nornagon Dec 7, 2022
eec9404
feat: net.request() supports custom protocols
nornagon Dec 8, 2022
dcd3c5b
add missing fixture
nornagon Dec 8, 2022
1a47cc3
net requests can follow cross-scheme redirects
nornagon Dec 9, 2022
6d716bf
fix request body
nornagon Dec 12, 2022
5e9d142
fix redirects not being properly aborted, add tests
nornagon Dec 13, 2022
47f04ef
wip
nornagon Dec 13, 2022
e92da94
remove wip handleProtocol code
nornagon Dec 13, 2022
a8d14df
lint
nornagon Dec 13, 2022
17543eb
Merge branch 'protocol-free' into protocol-handle
nornagon Dec 13, 2022
c7534f4
merge net-headers
nornagon Dec 15, 2022
2c7df1a
feat: implement protocol.handle
nornagon Dec 15, 2022
8d8d0aa
Merge branch 'main' into protocol-handle
nornagon Dec 21, 2022
1ba6c4c
Merge branch 'main' into protocol-handle
nornagon Jan 23, 2023
cb8eddf
undo changes to yarn.lock from merge
nornagon Jan 23, 2023
c4e51fc
Merge remote-tracking branch 'origin/protocol-handle' into protocol-h…
nornagon Mar 6, 2023
d60a769
Merge remote-tracking branch 'origin/main' into protocol-handle
nornagon Mar 6, 2023
613de94
remove logging
nornagon Mar 6, 2023
64ee235
use Request/Response in protocol.handle
nornagon Mar 8, 2023
373e715
Merge remote-tracking branch 'origin/main' into protocol-handle
nornagon Mar 8, 2023
3e8f598
remove old structure
nornagon Mar 8, 2023
422ea88
fix filenames.auto.gni
nornagon Mar 8, 2023
b4c66bd
add const_cast note
nornagon Mar 8, 2023
f2d7c0a
add isProtocolHandled
nornagon Mar 8, 2023
0044604
update docs
nornagon Mar 8, 2023
f99ceab
catch errors in handler body
nornagon Mar 8, 2023
c768632
fix test
nornagon Mar 9, 2023
f65ea4c
support bypassing custom handlers
nornagon Mar 14, 2023
e423401
Merged in branch 'main' to fix mergability
electron-patch-conflict-fixer[bot] Mar 15, 2023
f770a1b
fix patches
nornagon Mar 15, 2023
d5d9671
extract kBypassCustomProtocolHandlers to a constant
nornagon Mar 15, 2023
d6656a6
clean up tests
nornagon Mar 15, 2023
7da0ea8
net.fetch doesn't send accept header
nornagon Mar 16, 2023
0a8ab74
wipish: support passing on sniffed mime type
nornagon Mar 15, 2023
edd9f96
add perf test
nornagon Mar 16, 2023
5b5da4a
document bypassCustomProtocolHandlers
nornagon Mar 16, 2023
2d29368
more URL tests
nornagon Mar 16, 2023
1d95273
fix webRequest/protocol.handle conflict on streaming upload
nornagon Mar 22, 2023
b72ee6a
remove hostAndPort test
nornagon Mar 22, 2023
ab63c10
disable perf test on linux
nornagon Mar 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/api/client-request.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ it is not allowed to add or remove a custom header.
* `encoding` string (optional)
* `callback` Function (optional)

Returns `this`.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is incidental to this PR, but it happens to be true (because ClientRequest inherits from Writable).


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.

Expand Down
16 changes: 16 additions & 0 deletions docs/api/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ 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.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<HandleProtocolResponse | Promise<HandleProtocolResponse>>
* `request` [ProtocolRequest](structures/protocol-request.md)

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.

### `protocol.unhandle(scheme)`

* `scheme` string - scheme for which to remove the handler.

Removes a protocol handler registered with `protocol.handle`.

### `protocol.registerFileProtocol(scheme, handler)`

* `scheme` string
Expand Down
12 changes: 12 additions & 0 deletions docs/api/structures/handle-protocol-response.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# HandleProtocolResponse Object

* `error` Integer (optional) - When assigned, the request will fail with the
error defined by `error`. For the available error numbers you can use, see
the [net error list][net-error].
* `statusCode` number (optional) - The HTTP response code. If not specified,
the response will be sent with status code 200.
* `headers` Record<string, string | string[]> (optional) - The response headers.
* `body` string | Buffer | ReadableStream | null (optional) - the body of the
response. If null or undefined, the body will be empty.
nornagon marked this conversation as resolved.
Show resolved Hide resolved

[net-error]: https://source.chromium.org/chromium/chromium/src/+/main:net/base/net_error_list.h
1 change: 1 addition & 0 deletions filenames.auto.gni
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ auto_filenames = {
"docs/api/structures/file-filter.md",
"docs/api/structures/file-path-with-headers.md",
"docs/api/structures/gpu-feature-status.md",
"docs/api/structures/handle-protocol-response.md",
"docs/api/structures/hid-device.md",
"docs/api/structures/input-event.md",
"docs/api/structures/io-counters.md",
Expand Down
5 changes: 0 additions & 5 deletions lib/browser/api/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ const {
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([
Expand Down Expand Up @@ -203,9 +201,6 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
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) {
Expand Down
81 changes: 52 additions & 29 deletions lib/browser/api/protocol.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,56 @@
import { app, session } from 'electron/main';
import { session } from 'electron/main';

// Global protocol APIs.
const 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 protocol = session.defaultSession!.protocol;
if (!Object.prototype.hasOwnProperty.call(protocol, property)) return;

// Returning a native function directly would throw error.
return (...args: any[]) => (protocol[property as keyof Electron.Protocol] as Function)(...args);
},

ownKeys () {
if (!app.isReady()) return [];
return Reflect.ownKeys(session.defaultSession!.protocol);
},

has: (target, property: string) => {
if (!app.isReady()) return false;
return Reflect.has(session.defaultSession!.protocol, property);
},

getOwnPropertyDescriptor () {
return { configurable: true, enumerable: true };
}
}));
const { registerSchemesAsPrivileged, getStandardSchemes, Protocol } = process._linkedBinding('electron_browser_protocol');

const ERR_UNEXPECTED = -9;

const isBuiltInScheme = (scheme: string) => scheme === 'http' || scheme === 'https';

Protocol.prototype.handle = function (this: Electron.Protocol, scheme: string, handler: (req: any) => Promise<any>) {
const register = isBuiltInScheme(scheme) ? this.interceptProtocol : this.registerProtocol;
const success = register.call(this, scheme, async (req: any, cb: any) => {
const res = await handler(req);
if (!res || typeof res !== 'object') {
return cb({ error: ERR_UNEXPECTED });
}
const { error, body, headers, statusCode } = res;
cb({
data: body,
headers,
error,
statusCode
});
});
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}`); }
};

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)
} as typeof Electron.protocol;

export default protocol;
18 changes: 14 additions & 4 deletions shell/browser/api/electron_api_protocol.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
}
Expand Down
11 changes: 8 additions & 3 deletions shell/browser/api/electron_api_protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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:
Expand Down