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 support to callback in protocol interceptor handler without argument to perform as if no interceptors. #41929

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 59 additions & 0 deletions docs/api/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,65 @@ Removes a protocol handler registered with `protocol.handle`.

Returns `boolean` - Whether `scheme` is already handled.

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

* `scheme` string
* `handler` Function
* `request` [ProtocolRequest](structures/protocol-request.md)
* `callback` Function
* `response` (string | [ProtocolResponse](structures/protocol-response.md)) (optional)

Returns `boolean` - Whether the protocol was successfully registered

Intercepts `scheme` protocol and uses `handler` as the protocol's new handler
which you can response with a new http request or file or buffer or stream or
behave as if there is no interceptions.

Examples:

Response with a new http request.

```js
const sess = session.fromPartition('persist:example')
sess.protocol.interceptProtocol('https', (request, callback) => {
callback({
url: 'https://example.com',
referrer: 'https://example.com',
method: 'POST',
uploadData: { contentType: 'text/plain', data: 'Hello World' },
session: sess
})
})
```

But please notice the code above. if the scheme is http and send a new http request in the handler that will hit the handler again. You must end up not responding with a new http request.

Response with file.

```js
protocol.interceptProtocol('https', (request, callback) => {
callback({
path: '/path/to/the/file'
})
})
```

Response with data.

```js
protocol.interceptProtocol('https', (request, callback) => {
callback({ mimeType: 'text/html', data: Buffer.from('<h5>Response</h5>') })
})
```

Response as if it hasn't been intercepted.

```js
protocol.interceptProtocol('https', (request, callback) => {
callback()
})
```

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

* `scheme` string
Expand Down
54 changes: 35 additions & 19 deletions shell/browser/net/electron_url_loader_factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,7 @@ void ElectronURLLoaderFactory::OnComplete(
}
}

// static
void ElectronURLLoaderFactory::StartLoading(
void ElectronURLLoaderFactory::StartLoadingWithResponse(
mojo::PendingReceiver<network::mojom::URLLoader> loader,
int32_t request_id,
uint32_t options,
Expand All @@ -326,20 +325,10 @@ void ElectronURLLoaderFactory::StartLoading(
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
ProtocolType type,
gin::Arguments* args) {
// Send network error when there is no argument passed.
//
// Note that we should not throw JS error in the callback no matter what is
// passed, to keep compatibility with old code.
v8::Local<v8::Value> response;
if (!args->GetNext(&response)) {
OnComplete(std::move(client), request_id,
network::URLLoaderCompletionStatus(net::ERR_NOT_IMPLEMENTED));
return;
}

v8::Isolate* isolate,
v8::Local<v8::Value> response) {
// Parse {error} object.
gin_helper::Dictionary dict = ToDict(args->isolate(), response);
gin_helper::Dictionary dict = ToDict(isolate, response);
if (!dict.IsEmpty()) {
int error_code;
if (dict.Get("error", &error_code)) {
Expand Down Expand Up @@ -417,7 +406,7 @@ void ElectronURLLoaderFactory::StartLoading(
break;
case ProtocolType::kString: {
std::string data;
if (gin::ConvertFromV8(args->isolate(), response, &data))
if (gin::ConvertFromV8(isolate, response, &data))
SendContents(std::move(client), std::move(head), data);
else if (!dict.IsEmpty() && dict.Get("data", &data))
SendContents(std::move(client), std::move(head), data);
Expand All @@ -428,7 +417,7 @@ void ElectronURLLoaderFactory::StartLoading(
}
case ProtocolType::kFile: {
base::FilePath path;
if (gin::ConvertFromV8(args->isolate(), response, &path))
if (gin::ConvertFromV8(isolate, response, &path))
StartLoadingFile(std::move(client), std::move(loader), std::move(head),
request, path, dict);
else if (!dict.IsEmpty() && dict.Get("path", &path))
Expand Down Expand Up @@ -466,8 +455,8 @@ void ElectronURLLoaderFactory::StartLoading(
data.As<v8::ArrayBufferView>());
} else if (data->IsString()) {
SendContents(std::move(client), std::move(head),
gin::V8ToString(args->isolate(), data));
} else if (LooksLikeStream(args->isolate(), data)) {
gin::V8ToString(isolate, data));
} else if (LooksLikeStream(isolate, data)) {
StartLoadingStream(std::move(client), std::move(loader),
std::move(head), dict);
} else if (!dict.IsEmpty()) {
Expand All @@ -492,6 +481,33 @@ void ElectronURLLoaderFactory::StartLoading(
}
}

// static
void ElectronURLLoaderFactory::StartLoading(
mojo::PendingReceiver<network::mojom::URLLoader> loader,
int32_t request_id,
uint32_t options,
const network::ResourceRequest& request,
mojo::PendingRemote<network::mojom::URLLoaderClient> client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
ProtocolType type,
gin::Arguments* args) {
// Send network error when there is no argument passed.
//
// Note that we should not throw JS error in the callback no matter what is
// passed, to keep compatibility with old code.
v8::Local<v8::Value> response;
if (!args->GetNext(&response)) {
OnComplete(std::move(client), request_id,
network::URLLoaderCompletionStatus(net::ERR_NOT_IMPLEMENTED));
return;
}
ElectronURLLoaderFactory::StartLoadingWithResponse(
std::move(loader), request_id, options, request, std::move(client),
traffic_annotation, std::move(target_factory), type, args->isolate(),
response);
}

// static
void ElectronURLLoaderFactory::StartLoadingBuffer(
mojo::PendingRemote<network::mojom::URLLoaderClient> client,
Expand Down
16 changes: 14 additions & 2 deletions shell/browser/net/electron_url_loader_factory.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class ElectronURLLoaderFactory : public network::SelfDeletingURLLoaderFactory {
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation)
override;

static void StartLoading(
static void StartLoadingWithResponse(
mojo::PendingReceiver<network::mojom::URLLoader> loader,
int32_t request_id,
uint32_t options,
Expand All @@ -118,7 +118,8 @@ class ElectronURLLoaderFactory : public network::SelfDeletingURLLoaderFactory {
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
ProtocolType type,
gin::Arguments* args);
v8::Isolate* isolate,
v8::Local<v8::Value> response);

// disable copy
ElectronURLLoaderFactory(const ElectronURLLoaderFactory&) = delete;
Expand All @@ -131,6 +132,17 @@ class ElectronURLLoaderFactory : public network::SelfDeletingURLLoaderFactory {
mojo::PendingReceiver<network::mojom::URLLoaderFactory> factory_receiver);
~ElectronURLLoaderFactory() override;

static void StartLoading(
mojo::PendingReceiver<network::mojom::URLLoader> loader,
int32_t request_id,
uint32_t options,
const network::ResourceRequest& request,
mojo::PendingRemote<network::mojom::URLLoaderClient> client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
ProtocolType type,
gin::Arguments* args);

static void OnComplete(
mojo::PendingRemote<network::mojom::URLLoaderClient> client,
int32_t request_id,
Expand Down
34 changes: 33 additions & 1 deletion shell/browser/net/proxying_url_loader_factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,38 @@ bool ProxyingURLLoaderFactory::ShouldIgnoreConnectionsLimit(
return false;
}

// static
void ProxyingURLLoaderFactory::StartLoading(
mojo::PendingReceiver<network::mojom::URLLoader> loader,
int32_t request_id,
uint32_t options,
const network::ResourceRequest& request,
mojo::PendingRemote<network::mojom::URLLoaderClient> client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
ProtocolType type,
gin::Arguments* args) {
v8::Local<v8::Value> response;
if (!args->GetNext(&response) || response->IsNullOrUndefined() ||
(response->IsObject() &&
response.As<v8::Object>()
->GetOwnPropertyNames(args->isolate()->GetCurrentContext())
.ToLocalChecked()
->Length() == 0)) {
mojo::Remote<network::mojom::URLLoaderFactory> target_factory_remote(
std::move(target_factory));
// trigger receiver of itself but with kBypassCustomProtocolHandlers
target_factory_remote->CreateLoaderAndStart(
std::move(loader), request_id, options | kBypassCustomProtocolHandlers,
request, std::move(client), traffic_annotation);
return;
}
ElectronURLLoaderFactory::StartLoadingWithResponse(
std::move(loader), request_id, options, request, std::move(client),
traffic_annotation, std::move(target_factory), type, args->isolate(),
response);
}

void ProxyingURLLoaderFactory::CreateLoaderAndStart(
mojo::PendingReceiver<network::mojom::URLLoader> loader,
int32_t request_id,
Expand Down Expand Up @@ -816,7 +848,7 @@ void ProxyingURLLoaderFactory::CreateLoaderAndStart(
// <scheme, <type, handler>>
it->second.second.Run(
request,
base::BindOnce(&ElectronURLLoaderFactory::StartLoading,
base::BindOnce(&ProxyingURLLoaderFactory::StartLoading,
std::move(loader), request_id, options, request,
std::move(client), traffic_annotation,
std::move(loader_remote), it->second.first));
Expand Down
10 changes: 10 additions & 0 deletions shell/browser/net/proxying_url_loader_factory.h
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,16 @@ class ProxyingURLLoaderFactory
bool IsForServiceWorkerScript() const;

private:
static void StartLoading(
mojo::PendingReceiver<network::mojom::URLLoader> loader,
int32_t request_id,
uint32_t options,
const network::ResourceRequest& request,
mojo::PendingRemote<network::mojom::URLLoaderClient> client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
ProtocolType type,
gin::Arguments* args);
void OnTargetFactoryError();
void OnProxyBindingError();
void RemoveRequest(int32_t network_service_request_id, uint64_t request_id);
Expand Down
71 changes: 71 additions & 0 deletions spec/api-protocol-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const interceptStringProtocol = protocol.interceptStringProtocol;
const interceptBufferProtocol = protocol.interceptBufferProtocol;
const interceptHttpProtocol = protocol.interceptHttpProtocol;
const interceptStreamProtocol = protocol.interceptStreamProtocol;
const interceptProtocol: (scheme: string, handler: (req: Electron.ProtocolRequest, callback: (res?: Electron.ProtocolResponse) => void) => void) => boolean = protocol.interceptProtocol;
const unregisterProtocol = protocol.unregisterProtocol;
const uninterceptProtocol = protocol.uninterceptProtocol;

Expand Down Expand Up @@ -750,6 +751,76 @@ describe('protocol module', () => {
});
});

describe('protocol.interceptProtocol', () => {
const text = 'Hi, Chaofan!';
it('callback with null can behave like no intercept', async () => {
const server = http.createServer((req, res) => {
res.end(text);
});
after(() => server.close());
const { url } = await listen(server);
interceptProtocol('http', (req, callback) => {
callback();
});
await contents.loadURL(url);
expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal(text);
});

it('callback with {data} can response directly', async () => {
interceptProtocol('http', (req, callback) => {
callback({
data: 'hello'
});
});
await contents.loadURL('http://foo');
expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal('hello');
});

it('callback with null can behave like no intercept - redirect case', async () => {
const server = http.createServer((req, res) => {
if (req.url === '/serverRedirect') {
res.statusCode = 301;
res.setHeader('Location', `${url}/foo`);
res.end();
} else {
res.end(text);
}
});
after(() => server.close());
const { url } = await listen(server);
interceptProtocol('http', (req, callback) => {
callback();
});
await contents.loadURL(`${url}/serverRedirect`);
// Redirect should change the page url if not We may met the situation that returns data from page B to page A.
expect(await contents.getURL()).to.equal(`${url}/foo`);
expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal(text);
});

it('callback with null can behave like no intercept - post case', async () => {
const server = http.createServer((req, res) => {
let body = '';
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
res.end(body);
});
});
after(() => server.close());
const { url } = await listen(server);
interceptProtocol('http', (req, callback) => {
callback();
});

const r = await ajax(url, {
method: 'POST',
body: text
});
expect(r.data).to.equal(text);
});
});

describe('protocol.uninterceptProtocol', () => {
it('returns false when scheme does not exist', () => {
expect(uninterceptProtocol('not-exist')).to.equal(false);
Expand Down