feat: net.fetch() supports custom protocols (#36606)
This commit is contained in:
parent
76c825d619
commit
6bd9ee6988
8 changed files with 321 additions and 36 deletions
|
@ -101,6 +101,10 @@ Limitations:
|
||||||
* The `.type` and `.url` values of the returned `Response` object are
|
* The `.type` and `.url` values of the returned `Response` object are
|
||||||
incorrect.
|
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.
|
||||||
|
|
||||||
### `net.isOnline()`
|
### `net.isOnline()`
|
||||||
|
|
||||||
Returns `boolean` - Whether there is currently internet connection.
|
Returns `boolean` - Whether there is currently internet connection.
|
||||||
|
|
|
@ -768,6 +768,10 @@ Limitations:
|
||||||
* The `.type` and `.url` values of the returned `Response` object are
|
* The `.type` and `.url` values of the returned `Response` object are
|
||||||
incorrect.
|
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.
|
||||||
|
|
||||||
#### `ses.disableNetworkEmulation()`
|
#### `ses.disableNetworkEmulation()`
|
||||||
|
|
||||||
Disables any network emulation already active for the `session`. Resets to
|
Disables any network emulation already active for the `session`. Resets to
|
||||||
|
|
|
@ -10,7 +10,7 @@ const {
|
||||||
} = process._linkedBinding('electron_browser_net');
|
} = process._linkedBinding('electron_browser_net');
|
||||||
const { Session } = process._linkedBinding('electron_browser_session');
|
const { Session } = process._linkedBinding('electron_browser_session');
|
||||||
|
|
||||||
const kSupportedProtocols = new Set(['http:', 'https:']);
|
const kHttpProtocols = new Set(['http:', 'https:']);
|
||||||
|
|
||||||
// set of headers that Node.js discards duplicates for
|
// set of headers that Node.js discards duplicates for
|
||||||
// see https://nodejs.org/api/http.html#http_message_headers
|
// see https://nodejs.org/api/http.html#http_message_headers
|
||||||
|
@ -195,7 +195,20 @@ class ChunkedBodyStream extends Writable {
|
||||||
|
|
||||||
type RedirectPolicy = 'manual' | 'follow' | 'error';
|
type RedirectPolicy = 'manual' | 'follow' | 'error';
|
||||||
|
|
||||||
function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }> } {
|
const kAllowNonHttpProtocols = Symbol('kAllowNonHttpProtocols');
|
||||||
|
export function allowAnyProtocol (opts: ClientRequestConstructorOptions): ClientRequestConstructorOptions {
|
||||||
|
return {
|
||||||
|
...opts,
|
||||||
|
[kAllowNonHttpProtocols]: true
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtraURLLoaderOptions = {
|
||||||
|
redirectPolicy: RedirectPolicy;
|
||||||
|
headers: Record<string, { name: string, value: string | string[] }>;
|
||||||
|
allowNonHttpProtocols: boolean;
|
||||||
|
}
|
||||||
|
function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & ExtraURLLoaderOptions {
|
||||||
const options: any = typeof optionsIn === 'string' ? url.parse(optionsIn) : { ...optionsIn };
|
const options: any = typeof optionsIn === 'string' ? url.parse(optionsIn) : { ...optionsIn };
|
||||||
|
|
||||||
let urlStr: string = options.url;
|
let urlStr: string = options.url;
|
||||||
|
@ -203,9 +216,6 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
|
||||||
if (!urlStr) {
|
if (!urlStr) {
|
||||||
const urlObj: url.UrlObject = {};
|
const urlObj: url.UrlObject = {};
|
||||||
const protocol = options.protocol || 'http:';
|
const protocol = options.protocol || 'http:';
|
||||||
if (!kSupportedProtocols.has(protocol)) {
|
|
||||||
throw new Error('Protocol "' + protocol + '" not supported');
|
|
||||||
}
|
|
||||||
urlObj.protocol = protocol;
|
urlObj.protocol = protocol;
|
||||||
|
|
||||||
if (options.host) {
|
if (options.host) {
|
||||||
|
@ -247,7 +257,7 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
|
||||||
throw new TypeError('headers must be an object');
|
throw new TypeError('headers must be an object');
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }> } = {
|
const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }>, allowNonHttpProtocols: boolean } = {
|
||||||
method: (options.method || 'GET').toUpperCase(),
|
method: (options.method || 'GET').toUpperCase(),
|
||||||
url: urlStr,
|
url: urlStr,
|
||||||
redirectPolicy,
|
redirectPolicy,
|
||||||
|
@ -257,7 +267,8 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
|
||||||
credentials: options.credentials,
|
credentials: options.credentials,
|
||||||
origin: options.origin,
|
origin: options.origin,
|
||||||
referrerPolicy: options.referrerPolicy,
|
referrerPolicy: options.referrerPolicy,
|
||||||
cache: options.cache
|
cache: options.cache,
|
||||||
|
allowNonHttpProtocols: Object.prototype.hasOwnProperty.call(options, kAllowNonHttpProtocols)
|
||||||
};
|
};
|
||||||
const headers: Record<string, string | string[]> = options.headers || {};
|
const headers: Record<string, string | string[]> = options.headers || {};
|
||||||
for (const [name, value] of Object.entries(headers)) {
|
for (const [name, value] of Object.entries(headers)) {
|
||||||
|
@ -308,6 +319,10 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { redirectPolicy, ...urlLoaderOptions } = parseOptions(options);
|
const { redirectPolicy, ...urlLoaderOptions } = parseOptions(options);
|
||||||
|
const urlObj = new URL(urlLoaderOptions.url);
|
||||||
|
if (!urlLoaderOptions.allowNonHttpProtocols && !kHttpProtocols.has(urlObj.protocol)) {
|
||||||
|
throw new Error('ClientRequest only supports http: and https: protocols');
|
||||||
|
}
|
||||||
if (urlLoaderOptions.credentials === 'same-origin' && !urlLoaderOptions.origin) { throw new Error('credentials: same-origin requires origin to be set'); }
|
if (urlLoaderOptions.credentials === 'same-origin' && !urlLoaderOptions.origin) { throw new Error('credentials: same-origin requires origin to be set'); }
|
||||||
this._urlLoaderOptions = urlLoaderOptions;
|
this._urlLoaderOptions = urlLoaderOptions;
|
||||||
this._redirectPolicy = redirectPolicy;
|
this._redirectPolicy = redirectPolicy;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { net, IncomingMessage, Session as SessionT } from 'electron/main';
|
import { net, IncomingMessage, Session as SessionT } from 'electron/main';
|
||||||
import { Readable, Writable, isReadable } from 'stream';
|
import { Readable, Writable, isReadable } from 'stream';
|
||||||
|
import { allowAnyProtocol } from '@electron/internal/browser/api/net-client-request';
|
||||||
|
|
||||||
function createDeferredPromise<T, E extends Error = Error> (): { promise: Promise<T>; resolve: (x: T) => void; reject: (e: E) => void; } {
|
function createDeferredPromise<T, E extends Error = Error> (): { promise: Promise<T>; resolve: (x: T) => void; reject: (e: E) => void; } {
|
||||||
let res: (x: T) => void;
|
let res: (x: T) => void;
|
||||||
|
@ -72,7 +73,7 @@ export function fetchWithSession (input: RequestInfo, init: RequestInit | undefi
|
||||||
// We can't set credentials to same-origin unless there's an origin set.
|
// We can't set credentials to same-origin unless there's an origin set.
|
||||||
const credentials = req.credentials === 'same-origin' && !origin ? 'include' : req.credentials;
|
const credentials = req.credentials === 'same-origin' && !origin ? 'include' : req.credentials;
|
||||||
|
|
||||||
const r = net.request({
|
const r = net.request(allowAnyProtocol({
|
||||||
session,
|
session,
|
||||||
method: req.method,
|
method: req.method,
|
||||||
url: req.url,
|
url: req.url,
|
||||||
|
@ -81,7 +82,7 @@ export function fetchWithSession (input: RequestInfo, init: RequestInit | undefi
|
||||||
cache: req.cache,
|
cache: req.cache,
|
||||||
referrerPolicy: req.referrerPolicy,
|
referrerPolicy: req.referrerPolicy,
|
||||||
redirect: req.redirect
|
redirect: req.redirect
|
||||||
});
|
}));
|
||||||
|
|
||||||
// cors is the default mode, but we can't set mode=cors without an origin.
|
// cors is the default mode, but we can't set mode=cors without an origin.
|
||||||
if (req.mode && (req.mode !== 'cors' || origin)) {
|
if (req.mode && (req.mode !== 'cors' || origin)) {
|
||||||
|
|
|
@ -18,14 +18,19 @@
|
||||||
#include "mojo/public/cpp/system/data_pipe_producer.h"
|
#include "mojo/public/cpp/system/data_pipe_producer.h"
|
||||||
#include "net/base/load_flags.h"
|
#include "net/base/load_flags.h"
|
||||||
#include "net/http/http_util.h"
|
#include "net/http/http_util.h"
|
||||||
|
#include "net/url_request/redirect_util.h"
|
||||||
#include "services/network/public/cpp/resource_request.h"
|
#include "services/network/public/cpp/resource_request.h"
|
||||||
#include "services/network/public/cpp/simple_url_loader.h"
|
#include "services/network/public/cpp/simple_url_loader.h"
|
||||||
|
#include "services/network/public/cpp/url_util.h"
|
||||||
|
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
|
||||||
#include "services/network/public/mojom/chunked_data_pipe_getter.mojom.h"
|
#include "services/network/public/mojom/chunked_data_pipe_getter.mojom.h"
|
||||||
#include "services/network/public/mojom/http_raw_headers.mojom.h"
|
#include "services/network/public/mojom/http_raw_headers.mojom.h"
|
||||||
#include "services/network/public/mojom/url_loader_factory.mojom.h"
|
#include "services/network/public/mojom/url_loader_factory.mojom.h"
|
||||||
#include "shell/browser/api/electron_api_session.h"
|
#include "shell/browser/api/electron_api_session.h"
|
||||||
#include "shell/browser/electron_browser_context.h"
|
#include "shell/browser/electron_browser_context.h"
|
||||||
#include "shell/browser/javascript_environment.h"
|
#include "shell/browser/javascript_environment.h"
|
||||||
|
#include "shell/browser/net/asar/asar_url_loader_factory.h"
|
||||||
|
#include "shell/browser/protocol_registry.h"
|
||||||
#include "shell/common/gin_converters/callback_converter.h"
|
#include "shell/common/gin_converters/callback_converter.h"
|
||||||
#include "shell/common/gin_converters/gurl_converter.h"
|
#include "shell/common/gin_converters/gurl_converter.h"
|
||||||
#include "shell/common/gin_converters/net_converter.h"
|
#include "shell/common/gin_converters/net_converter.h"
|
||||||
|
@ -336,34 +341,49 @@ gin::WrapperInfo SimpleURLLoaderWrapper::kWrapperInfo = {
|
||||||
gin::kEmbedderNativeGin};
|
gin::kEmbedderNativeGin};
|
||||||
|
|
||||||
SimpleURLLoaderWrapper::SimpleURLLoaderWrapper(
|
SimpleURLLoaderWrapper::SimpleURLLoaderWrapper(
|
||||||
|
ElectronBrowserContext* browser_context,
|
||||||
std::unique_ptr<network::ResourceRequest> request,
|
std::unique_ptr<network::ResourceRequest> request,
|
||||||
network::mojom::URLLoaderFactory* url_loader_factory,
|
int options)
|
||||||
int options) {
|
: browser_context_(browser_context),
|
||||||
if (!request->trusted_params)
|
request_options_(options),
|
||||||
request->trusted_params = network::ResourceRequest::TrustedParams();
|
request_(std::move(request)) {
|
||||||
|
if (!request_->trusted_params)
|
||||||
|
request_->trusted_params = network::ResourceRequest::TrustedParams();
|
||||||
mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver>
|
mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver>
|
||||||
url_loader_network_observer_remote;
|
url_loader_network_observer_remote;
|
||||||
url_loader_network_observer_receivers_.Add(
|
url_loader_network_observer_receivers_.Add(
|
||||||
this,
|
this,
|
||||||
url_loader_network_observer_remote.InitWithNewPipeAndPassReceiver());
|
url_loader_network_observer_remote.InitWithNewPipeAndPassReceiver());
|
||||||
request->trusted_params->url_loader_network_observer =
|
request_->trusted_params->url_loader_network_observer =
|
||||||
std::move(url_loader_network_observer_remote);
|
std::move(url_loader_network_observer_remote);
|
||||||
// Chromium filters headers using browser rules, while for net module we have
|
// Chromium filters headers using browser rules, while for net module we have
|
||||||
// every header passed. The following setting will allow us to capture the
|
// every header passed. The following setting will allow us to capture the
|
||||||
// raw headers in the URLLoader.
|
// raw headers in the URLLoader.
|
||||||
request->trusted_params->report_raw_headers = true;
|
request_->trusted_params->report_raw_headers = true;
|
||||||
// SimpleURLLoader wants to control the request body itself. We have other
|
Start();
|
||||||
// ideas.
|
}
|
||||||
auto request_body = std::move(request->request_body);
|
|
||||||
auto* request_ref = request.get();
|
void SimpleURLLoaderWrapper::Start() {
|
||||||
|
// Make a copy of the request; we'll need to re-send it if we get redirected.
|
||||||
|
auto request = std::make_unique<network::ResourceRequest>();
|
||||||
|
*request = *request_;
|
||||||
|
|
||||||
|
// SimpleURLLoader has no way to set a data pipe as the request body, which
|
||||||
|
// we need to do for streaming upload, so instead we "cheat" and pretend to
|
||||||
|
// SimpleURLLoader like there is no request_body when we construct it. Later,
|
||||||
|
// we will sneakily put the request_body back while it isn't looking.
|
||||||
|
scoped_refptr<network::ResourceRequestBody> request_body =
|
||||||
|
std::move(request->request_body);
|
||||||
|
|
||||||
|
network::ResourceRequest* request_ref = request.get();
|
||||||
loader_ =
|
loader_ =
|
||||||
network::SimpleURLLoader::Create(std::move(request), kTrafficAnnotation);
|
network::SimpleURLLoader::Create(std::move(request), kTrafficAnnotation);
|
||||||
if (request_body) {
|
|
||||||
|
if (request_body)
|
||||||
request_ref->request_body = std::move(request_body);
|
request_ref->request_body = std::move(request_body);
|
||||||
}
|
|
||||||
|
|
||||||
loader_->SetAllowHttpErrorResults(true);
|
loader_->SetAllowHttpErrorResults(true);
|
||||||
loader_->SetURLLoaderFactoryOptions(options);
|
loader_->SetURLLoaderFactoryOptions(request_options_);
|
||||||
loader_->SetOnResponseStartedCallback(base::BindOnce(
|
loader_->SetOnResponseStartedCallback(base::BindOnce(
|
||||||
&SimpleURLLoaderWrapper::OnResponseStarted, base::Unretained(this)));
|
&SimpleURLLoaderWrapper::OnResponseStarted, base::Unretained(this)));
|
||||||
loader_->SetOnRedirectCallback(base::BindRepeating(
|
loader_->SetOnRedirectCallback(base::BindRepeating(
|
||||||
|
@ -373,7 +393,8 @@ SimpleURLLoaderWrapper::SimpleURLLoaderWrapper(
|
||||||
loader_->SetOnDownloadProgressCallback(base::BindRepeating(
|
loader_->SetOnDownloadProgressCallback(base::BindRepeating(
|
||||||
&SimpleURLLoaderWrapper::OnDownloadProgress, base::Unretained(this)));
|
&SimpleURLLoaderWrapper::OnDownloadProgress, base::Unretained(this)));
|
||||||
|
|
||||||
loader_->DownloadAsStream(url_loader_factory, this);
|
url_loader_factory_ = GetURLLoaderFactoryForURL(request_ref->url);
|
||||||
|
loader_->DownloadAsStream(url_loader_factory_.get(), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SimpleURLLoaderWrapper::Pin() {
|
void SimpleURLLoaderWrapper::Pin() {
|
||||||
|
@ -458,6 +479,42 @@ void SimpleURLLoaderWrapper::Cancel() {
|
||||||
// This ensures that no further callbacks will be called, so there's no need
|
// This ensures that no further callbacks will be called, so there's no need
|
||||||
// for additional guards.
|
// for additional guards.
|
||||||
}
|
}
|
||||||
|
scoped_refptr<network::SharedURLLoaderFactory>
|
||||||
|
SimpleURLLoaderWrapper::GetURLLoaderFactoryForURL(const GURL& url) {
|
||||||
|
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory;
|
||||||
|
auto* protocol_registry =
|
||||||
|
ProtocolRegistry::FromBrowserContext(browser_context_);
|
||||||
|
// 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())) {
|
||||||
|
auto& protocol_handler =
|
||||||
|
protocol_registry->intercept_handlers().at(url.scheme());
|
||||||
|
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote =
|
||||||
|
ElectronURLLoaderFactory::Create(protocol_handler.first,
|
||||||
|
protocol_handler.second);
|
||||||
|
url_loader_factory = network::SharedURLLoaderFactory::Create(
|
||||||
|
std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
|
||||||
|
std::move(pending_remote)));
|
||||||
|
} else if (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,
|
||||||
|
protocol_handler.second);
|
||||||
|
url_loader_factory = network::SharedURLLoaderFactory::Create(
|
||||||
|
std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
|
||||||
|
std::move(pending_remote)));
|
||||||
|
} else if (url.SchemeIsFile()) {
|
||||||
|
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote =
|
||||||
|
AsarURLLoaderFactory::Create();
|
||||||
|
url_loader_factory = network::SharedURLLoaderFactory::Create(
|
||||||
|
std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
|
||||||
|
std::move(pending_remote)));
|
||||||
|
} else {
|
||||||
|
url_loader_factory = browser_context_->GetURLLoaderFactory();
|
||||||
|
}
|
||||||
|
return url_loader_factory;
|
||||||
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
||||||
|
@ -634,12 +691,9 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
||||||
session = Session::FromPartition(args->isolate(), "");
|
session = Session::FromPartition(args->isolate(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto url_loader_factory = session->browser_context()->GetURLLoaderFactory();
|
|
||||||
|
|
||||||
auto ret = gin::CreateHandle(
|
auto ret = gin::CreateHandle(
|
||||||
args->isolate(),
|
args->isolate(), new SimpleURLLoaderWrapper(session->browser_context(),
|
||||||
new SimpleURLLoaderWrapper(std::move(request), url_loader_factory.get(),
|
std::move(request), options));
|
||||||
options));
|
|
||||||
ret->Pin();
|
ret->Pin();
|
||||||
if (!chunk_pipe_getter.IsEmpty()) {
|
if (!chunk_pipe_getter.IsEmpty()) {
|
||||||
ret->PinBodyGetter(chunk_pipe_getter);
|
ret->PinBodyGetter(chunk_pipe_getter);
|
||||||
|
@ -691,6 +745,45 @@ void SimpleURLLoaderWrapper::OnRedirect(
|
||||||
const network::mojom::URLResponseHead& response_head,
|
const network::mojom::URLResponseHead& response_head,
|
||||||
std::vector<std::string>* removed_headers) {
|
std::vector<std::string>* removed_headers) {
|
||||||
Emit("redirect", redirect_info, response_head.headers.get());
|
Emit("redirect", redirect_info, response_head.headers.get());
|
||||||
|
|
||||||
|
if (!loader_)
|
||||||
|
// The redirect was aborted by JS.
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Optimization: if both the old and new URLs are handled by the network
|
||||||
|
// service, just FollowRedirect.
|
||||||
|
if (network::IsURLHandledByNetworkService(redirect_info.new_url) &&
|
||||||
|
network::IsURLHandledByNetworkService(request_->url))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Otherwise, restart the request (potentially picking a new
|
||||||
|
// URLLoaderFactory). See
|
||||||
|
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/loader/navigation_url_loader_impl.cc;l=534-550;drc=fbaec92ad5982f83aa4544d5c88d66d08034a9f4
|
||||||
|
|
||||||
|
bool should_clear_upload = false;
|
||||||
|
net::RedirectUtil::UpdateHttpRequest(
|
||||||
|
request_->url, request_->method, redirect_info, *removed_headers,
|
||||||
|
/* modified_headers = */ absl::nullopt, &request_->headers,
|
||||||
|
&should_clear_upload);
|
||||||
|
if (should_clear_upload) {
|
||||||
|
// The request body is no longer applicable.
|
||||||
|
request_->request_body.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
request_->url = redirect_info.new_url;
|
||||||
|
request_->method = redirect_info.new_method;
|
||||||
|
request_->site_for_cookies = redirect_info.new_site_for_cookies;
|
||||||
|
|
||||||
|
// See if navigation network isolation key needs to be updated.
|
||||||
|
request_->trusted_params->isolation_info =
|
||||||
|
request_->trusted_params->isolation_info.CreateForRedirect(
|
||||||
|
url::Origin::Create(request_->url));
|
||||||
|
|
||||||
|
request_->referrer = GURL(redirect_info.new_referrer);
|
||||||
|
request_->referrer_policy = redirect_info.new_referrer_policy;
|
||||||
|
request_->navigation_redirect_chain.push_back(redirect_info.new_url);
|
||||||
|
|
||||||
|
Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SimpleURLLoaderWrapper::OnUploadProgress(uint64_t position,
|
void SimpleURLLoaderWrapper::OnUploadProgress(uint64_t position,
|
||||||
|
|
|
@ -31,8 +31,13 @@ class Handle;
|
||||||
namespace network {
|
namespace network {
|
||||||
class SimpleURLLoader;
|
class SimpleURLLoader;
|
||||||
struct ResourceRequest;
|
struct ResourceRequest;
|
||||||
|
class SharedURLLoaderFactory;
|
||||||
} // namespace network
|
} // namespace network
|
||||||
|
|
||||||
|
namespace electron {
|
||||||
|
class ElectronBrowserContext;
|
||||||
|
}
|
||||||
|
|
||||||
namespace electron::api {
|
namespace electron::api {
|
||||||
|
|
||||||
/** Wraps a SimpleURLLoader to make it usable from JavaScript */
|
/** Wraps a SimpleURLLoader to make it usable from JavaScript */
|
||||||
|
@ -54,8 +59,8 @@ class SimpleURLLoaderWrapper
|
||||||
const char* GetTypeName() override;
|
const char* GetTypeName() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SimpleURLLoaderWrapper(std::unique_ptr<network::ResourceRequest> request,
|
SimpleURLLoaderWrapper(ElectronBrowserContext* browser_context,
|
||||||
network::mojom::URLLoaderFactory* url_loader_factory,
|
std::unique_ptr<network::ResourceRequest> request,
|
||||||
int options);
|
int options);
|
||||||
|
|
||||||
// SimpleURLLoaderStreamConsumer:
|
// SimpleURLLoaderStreamConsumer:
|
||||||
|
@ -99,6 +104,9 @@ class SimpleURLLoaderWrapper
|
||||||
mojo::PendingReceiver<network::mojom::URLLoaderNetworkServiceObserver>
|
mojo::PendingReceiver<network::mojom::URLLoaderNetworkServiceObserver>
|
||||||
observer) override;
|
observer) override;
|
||||||
|
|
||||||
|
scoped_refptr<network::SharedURLLoaderFactory> GetURLLoaderFactoryForURL(
|
||||||
|
const GURL& url);
|
||||||
|
|
||||||
// SimpleURLLoader callbacks
|
// SimpleURLLoader callbacks
|
||||||
void OnResponseStarted(const GURL& final_url,
|
void OnResponseStarted(const GURL& final_url,
|
||||||
const network::mojom::URLResponseHead& response_head);
|
const network::mojom::URLResponseHead& response_head);
|
||||||
|
@ -112,6 +120,10 @@ class SimpleURLLoaderWrapper
|
||||||
void Pin();
|
void Pin();
|
||||||
void PinBodyGetter(v8::Local<v8::Value>);
|
void PinBodyGetter(v8::Local<v8::Value>);
|
||||||
|
|
||||||
|
ElectronBrowserContext* browser_context_;
|
||||||
|
int request_options_;
|
||||||
|
std::unique_ptr<network::ResourceRequest> request_;
|
||||||
|
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
|
||||||
std::unique_ptr<network::SimpleURLLoader> loader_;
|
std::unique_ptr<network::SimpleURLLoader> loader_;
|
||||||
v8::Global<v8::Value> pinned_wrapper_;
|
v8::Global<v8::Value> pinned_wrapper_;
|
||||||
v8::Global<v8::Value> pinned_chunk_pipe_getter_;
|
v8::Global<v8::Value> pinned_chunk_pipe_getter_;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as dns from 'dns';
|
import * as dns from 'dns';
|
||||||
import { net, session, ClientRequest, BrowserWindow, ClientRequestConstructorOptions } from 'electron/main';
|
import { net, session, ClientRequest, BrowserWindow, ClientRequestConstructorOptions, protocol } from 'electron/main';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
|
import * as path from 'path';
|
||||||
import { Socket } from 'net';
|
import { Socket } from 'net';
|
||||||
import { defer, listen } from './lib/spec-helpers';
|
import { defer, listen } from './lib/spec-helpers';
|
||||||
import { once } from 'events';
|
import { once } from 'events';
|
||||||
|
@ -163,9 +164,9 @@ describe('net module', () => {
|
||||||
|
|
||||||
it('should post the correct data in a POST request', async () => {
|
it('should post the correct data in a POST request', async () => {
|
||||||
const bodyData = 'Hello World!';
|
const bodyData = 'Hello World!';
|
||||||
|
let postedBodyData: string = '';
|
||||||
const serverUrl = await respondOnce.toSingleURL(async (request, response) => {
|
const serverUrl = await respondOnce.toSingleURL(async (request, response) => {
|
||||||
const postedBodyData = await collectStreamBody(request);
|
postedBodyData = await collectStreamBody(request);
|
||||||
expect(postedBodyData).to.equal(bodyData);
|
|
||||||
response.end();
|
response.end();
|
||||||
});
|
});
|
||||||
const urlRequest = net.request({
|
const urlRequest = net.request({
|
||||||
|
@ -175,16 +176,72 @@ describe('net module', () => {
|
||||||
urlRequest.write(bodyData);
|
urlRequest.write(bodyData);
|
||||||
const response = await getResponse(urlRequest);
|
const response = await getResponse(urlRequest);
|
||||||
expect(response.statusCode).to.equal(200);
|
expect(response.statusCode).to.equal(200);
|
||||||
|
expect(postedBodyData).to.equal(bodyData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a 307 redirected POST request preserves the body', async () => {
|
||||||
|
const bodyData = 'Hello World!';
|
||||||
|
let postedBodyData: string = '';
|
||||||
|
let methodAfterRedirect: string | undefined;
|
||||||
|
const serverUrl = await respondNTimes.toRoutes({
|
||||||
|
'/redirect': (req, res) => {
|
||||||
|
res.statusCode = 307;
|
||||||
|
res.setHeader('location', serverUrl);
|
||||||
|
return res.end();
|
||||||
|
},
|
||||||
|
'/': async (req, res) => {
|
||||||
|
methodAfterRedirect = req.method;
|
||||||
|
postedBodyData = await collectStreamBody(req);
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
}, 2);
|
||||||
|
const urlRequest = net.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: serverUrl + '/redirect'
|
||||||
|
});
|
||||||
|
urlRequest.write(bodyData);
|
||||||
|
const response = await getResponse(urlRequest);
|
||||||
|
expect(response.statusCode).to.equal(200);
|
||||||
|
await collectStreamBody(response);
|
||||||
|
expect(methodAfterRedirect).to.equal('POST');
|
||||||
|
expect(postedBodyData).to.equal(bodyData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a 302 redirected POST request DOES NOT preserve the body', async () => {
|
||||||
|
const bodyData = 'Hello World!';
|
||||||
|
let postedBodyData: string = '';
|
||||||
|
let methodAfterRedirect: string | undefined;
|
||||||
|
const serverUrl = await respondNTimes.toRoutes({
|
||||||
|
'/redirect': (req, res) => {
|
||||||
|
res.statusCode = 302;
|
||||||
|
res.setHeader('location', serverUrl);
|
||||||
|
return res.end();
|
||||||
|
},
|
||||||
|
'/': async (req, res) => {
|
||||||
|
methodAfterRedirect = req.method;
|
||||||
|
postedBodyData = await collectStreamBody(req);
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
}, 2);
|
||||||
|
const urlRequest = net.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: serverUrl + '/redirect'
|
||||||
|
});
|
||||||
|
urlRequest.write(bodyData);
|
||||||
|
const response = await getResponse(urlRequest);
|
||||||
|
expect(response.statusCode).to.equal(200);
|
||||||
|
await collectStreamBody(response);
|
||||||
|
expect(methodAfterRedirect).to.equal('GET');
|
||||||
|
expect(postedBodyData).to.equal('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support chunked encoding', async () => {
|
it('should support chunked encoding', async () => {
|
||||||
|
let receivedRequest: http.IncomingMessage = null as any;
|
||||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||||
response.statusCode = 200;
|
response.statusCode = 200;
|
||||||
response.statusMessage = 'OK';
|
response.statusMessage = 'OK';
|
||||||
response.chunkedEncoding = true;
|
response.chunkedEncoding = true;
|
||||||
expect(request.method).to.equal('POST');
|
receivedRequest = request;
|
||||||
expect(request.headers['transfer-encoding']).to.equal('chunked');
|
|
||||||
expect(request.headers['content-length']).to.equal(undefined);
|
|
||||||
request.on('data', (chunk: Buffer) => {
|
request.on('data', (chunk: Buffer) => {
|
||||||
response.write(chunk);
|
response.write(chunk);
|
||||||
});
|
});
|
||||||
|
@ -210,6 +267,9 @@ describe('net module', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await getResponse(urlRequest);
|
const response = await getResponse(urlRequest);
|
||||||
|
expect(receivedRequest.method).to.equal('POST');
|
||||||
|
expect(receivedRequest.headers['transfer-encoding']).to.equal('chunked');
|
||||||
|
expect(receivedRequest.headers['content-length']).to.equal(undefined);
|
||||||
expect(response.statusCode).to.equal(200);
|
expect(response.statusCode).to.equal(200);
|
||||||
const received = await collectStreamBodyBuffer(response);
|
const received = await collectStreamBodyBuffer(response);
|
||||||
expect(sent.equals(received)).to.be.true();
|
expect(sent.equals(received)).to.be.true();
|
||||||
|
@ -1446,6 +1506,9 @@ describe('net module', () => {
|
||||||
urlRequest.end();
|
urlRequest.end();
|
||||||
urlRequest.on('redirect', () => { urlRequest.abort(); });
|
urlRequest.on('redirect', () => { urlRequest.abort(); });
|
||||||
urlRequest.on('error', () => {});
|
urlRequest.on('error', () => {});
|
||||||
|
urlRequest.on('response', () => {
|
||||||
|
expect.fail('Unexpected response');
|
||||||
|
});
|
||||||
await once(urlRequest, 'abort');
|
await once(urlRequest, 'abort');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2078,6 +2141,20 @@ describe('net module', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('non-http schemes', () => {
|
||||||
|
it('should be rejected by net.request', async () => {
|
||||||
|
expect(() => {
|
||||||
|
net.request('file://bar');
|
||||||
|
}).to.throw('ClientRequest only supports http: and https: protocols');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be rejected by net.request when passed in url:', async () => {
|
||||||
|
expect(() => {
|
||||||
|
net.request({ url: 'file://bar' });
|
||||||
|
}).to.throw('ClientRequest only supports http: and https: protocols');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('net.fetch', () => {
|
describe('net.fetch', () => {
|
||||||
// NB. there exist much more comprehensive tests for fetch() in the form of
|
// NB. there exist much more comprehensive tests for fetch() in the form of
|
||||||
// the WPT: https://github.com/web-platform-tests/wpt/tree/master/fetch
|
// the WPT: https://github.com/web-platform-tests/wpt/tree/master/fetch
|
||||||
|
@ -2167,5 +2244,83 @@ describe('net module', () => {
|
||||||
await expect(r.text()).to.be.rejectedWith(/ERR_INCOMPLETE_CHUNKED_ENCODING/);
|
await expect(r.text()).to.be.rejectedWith(/ERR_INCOMPLETE_CHUNKED_ENCODING/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can request file:// URLs', async () => {
|
||||||
|
const resp = await net.fetch(url.pathToFileURL(path.join(__dirname, 'fixtures', 'hello.txt')).toString());
|
||||||
|
expect(resp.ok).to.be.true();
|
||||||
|
// trimRight instead of asserting the whole string to avoid line ending shenanigans on WOA
|
||||||
|
expect((await resp.text()).trimRight()).to.equal('hello world');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can make requests to custom protocols', async () => {
|
||||||
|
protocol.registerStringProtocol('electron-test', (req, cb) => { cb('hello ' + req.url); });
|
||||||
|
defer(() => {
|
||||||
|
protocol.unregisterProtocol('electron-test');
|
||||||
|
});
|
||||||
|
const body = await net.fetch('electron-test://foo').then(r => r.text());
|
||||||
|
expect(body).to.equal('hello electron-test://foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs through intercept handlers', async () => {
|
||||||
|
protocol.interceptStringProtocol('http', (req, cb) => { cb('hello ' + req.url); });
|
||||||
|
defer(() => {
|
||||||
|
protocol.uninterceptProtocol('http');
|
||||||
|
});
|
||||||
|
const body = await net.fetch('http://foo').then(r => r.text());
|
||||||
|
expect(body).to.equal('hello http://foo/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('file: runs through intercept handlers', async () => {
|
||||||
|
protocol.interceptStringProtocol('file', (req, cb) => { cb('hello ' + req.url); });
|
||||||
|
defer(() => {
|
||||||
|
protocol.uninterceptProtocol('file');
|
||||||
|
});
|
||||||
|
const body = await net.fetch('file://foo').then(r => r.text());
|
||||||
|
expect(body).to.equal('hello file://foo/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be redirected', async () => {
|
||||||
|
protocol.interceptStringProtocol('file', (req, cb) => { cb({ statusCode: 302, headers: { location: 'electron-test://bar' } }); });
|
||||||
|
defer(() => {
|
||||||
|
protocol.uninterceptProtocol('file');
|
||||||
|
});
|
||||||
|
protocol.registerStringProtocol('electron-test', (req, cb) => { cb('hello ' + req.url); });
|
||||||
|
defer(() => {
|
||||||
|
protocol.unregisterProtocol('electron-test');
|
||||||
|
});
|
||||||
|
const body = await net.fetch('file://foo').then(r => r.text());
|
||||||
|
expect(body).to.equal('hello electron-test://bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not follow redirect when redirect: error', async () => {
|
||||||
|
protocol.registerStringProtocol('electron-test', (req, cb) => {
|
||||||
|
if (/redirect/.test(req.url)) return cb({ statusCode: 302, headers: { location: 'electron-test://bar' } });
|
||||||
|
cb('hello ' + req.url);
|
||||||
|
});
|
||||||
|
defer(() => {
|
||||||
|
protocol.unregisterProtocol('electron-test');
|
||||||
|
});
|
||||||
|
await expect(net.fetch('electron-test://redirect', { redirect: 'error' })).to.eventually.be.rejectedWith('Attempted to redirect, but redirect policy was \'error\'');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a 307 redirected POST request preserves the body', async () => {
|
||||||
|
const bodyData = 'Hello World!';
|
||||||
|
let postedBodyData: any;
|
||||||
|
protocol.registerStringProtocol('electron-test', async (req, cb) => {
|
||||||
|
if (/redirect/.test(req.url)) return cb({ statusCode: 307, headers: { location: 'electron-test://bar' } });
|
||||||
|
postedBodyData = req.uploadData![0].bytes.toString();
|
||||||
|
cb('hello ' + req.url);
|
||||||
|
});
|
||||||
|
defer(() => {
|
||||||
|
protocol.unregisterProtocol('electron-test');
|
||||||
|
});
|
||||||
|
const response = await net.fetch('electron-test://redirect', {
|
||||||
|
method: 'POST',
|
||||||
|
body: bodyData
|
||||||
|
});
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
await response.text();
|
||||||
|
expect(postedBodyData).to.equal(bodyData);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
1
spec/fixtures/hello.txt
vendored
Normal file
1
spec/fixtures/hello.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
hello world
|
Loading…
Add table
Add a link
Reference in a new issue