feat: allow setting the Origin header and Sec-Fetch-* headers in net.request() (#26135)
This commit is contained in:
parent
b8372fdc29
commit
e1cc78f275
5 changed files with 251 additions and 13 deletions
|
@ -47,6 +47,7 @@ following properties:
|
|||
be aborted. When mode is `manual` the redirection will be cancelled unless
|
||||
[`request.followRedirect`](#requestfollowredirect) is invoked synchronously
|
||||
during the [`redirect`](#event-redirect) event. Defaults to `follow`.
|
||||
* `origin` String (optional) - The origin URL of the request.
|
||||
|
||||
`options` properties such as `protocol`, `host`, `hostname`, `port` and `path`
|
||||
strictly follow the Node.js model as described in the
|
||||
|
|
|
@ -197,7 +197,7 @@ class ChunkedBodyStream extends Writable {
|
|||
|
||||
type RedirectPolicy = 'manual' | 'follow' | 'error';
|
||||
|
||||
function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, extraHeaders: Record<string, string> } {
|
||||
function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }> } {
|
||||
const options: any = typeof optionsIn === 'string' ? url.parse(optionsIn) : { ...optionsIn };
|
||||
|
||||
let urlStr: string = options.url;
|
||||
|
@ -249,22 +249,26 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
|
|||
throw new TypeError('headers must be an object');
|
||||
}
|
||||
|
||||
const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, extraHeaders: Record<string, string | string[]> } = {
|
||||
const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }> } = {
|
||||
method: (options.method || 'GET').toUpperCase(),
|
||||
url: urlStr,
|
||||
redirectPolicy,
|
||||
extraHeaders: options.headers || {},
|
||||
headers: {},
|
||||
body: null as any,
|
||||
useSessionCookies: options.useSessionCookies,
|
||||
credentials: options.credentials
|
||||
credentials: options.credentials,
|
||||
origin: options.origin
|
||||
};
|
||||
for (const [name, value] of Object.entries(urlLoaderOptions.extraHeaders!)) {
|
||||
const headers: Record<string, string | string[]> = options.headers || {};
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
if (!isValidHeaderName(name)) {
|
||||
throw new Error(`Invalid header name: '${name}'`);
|
||||
}
|
||||
if (!isValidHeaderValue(value.toString())) {
|
||||
throw new Error(`Invalid value for header '${name}': '${value}'`);
|
||||
}
|
||||
const key = name.toLowerCase();
|
||||
urlLoaderOptions.headers[key] = { name, value };
|
||||
}
|
||||
if (options.session) {
|
||||
// Weak check, but it should be enough to catch 99% of accidental misuses.
|
||||
|
@ -289,7 +293,7 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
|
|||
_aborted: boolean = false;
|
||||
_chunkedEncoding: boolean | undefined;
|
||||
_body: Writable | undefined;
|
||||
_urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { extraHeaders: Record<string, string> };
|
||||
_urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { headers: Record<string, { name: string, value: string | string[] }> };
|
||||
_redirectPolicy: RedirectPolicy;
|
||||
_followRedirectCb?: () => void;
|
||||
_uploadProgress?: { active: boolean, started: boolean, current: number, total: number };
|
||||
|
@ -350,7 +354,7 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
|
|||
}
|
||||
|
||||
const key = name.toLowerCase();
|
||||
this._urlLoaderOptions.extraHeaders[key] = value;
|
||||
this._urlLoaderOptions.headers[key] = { name, value };
|
||||
}
|
||||
|
||||
getHeader (name: string) {
|
||||
|
@ -359,7 +363,8 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
|
|||
}
|
||||
|
||||
const key = name.toLowerCase();
|
||||
return this._urlLoaderOptions.extraHeaders[key];
|
||||
const header = this._urlLoaderOptions.headers[key];
|
||||
return header && header.value as any;
|
||||
}
|
||||
|
||||
removeHeader (name: string) {
|
||||
|
@ -372,7 +377,7 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
|
|||
}
|
||||
|
||||
const key = name.toLowerCase();
|
||||
delete this._urlLoaderOptions.extraHeaders[key];
|
||||
delete this._urlLoaderOptions.headers[key];
|
||||
}
|
||||
|
||||
_write (chunk: Buffer, encoding: BufferEncoding, callback: () => void) {
|
||||
|
@ -401,15 +406,20 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
|
|||
|
||||
_startRequest () {
|
||||
this._started = true;
|
||||
const stringifyValues = (obj: Record<string, any>) => {
|
||||
const stringifyValues = (obj: Record<string, { name: string, value: string | string[] }>) => {
|
||||
const ret: Record<string, string> = {};
|
||||
for (const k of Object.keys(obj)) {
|
||||
ret[k] = obj[k].toString();
|
||||
const kv = obj[k];
|
||||
ret[kv.name] = kv.value.toString();
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
this._urlLoaderOptions.referrer = this._urlLoaderOptions.extraHeaders.referer || '';
|
||||
const opts = { ...this._urlLoaderOptions, extraHeaders: stringifyValues(this._urlLoaderOptions.extraHeaders) };
|
||||
this._urlLoaderOptions.referrer = this.getHeader('referer') || '';
|
||||
this._urlLoaderOptions.origin = this._urlLoaderOptions.origin || this.getHeader('origin') || '';
|
||||
this._urlLoaderOptions.hasUserActivation = this.getHeader('sec-fetch-user') === '?1';
|
||||
this._urlLoaderOptions.mode = this.getHeader('sec-fetch-mode') || '';
|
||||
this._urlLoaderOptions.destination = this.getHeader('sec-fetch-dest') || '';
|
||||
const opts = { ...this._urlLoaderOptions, extraHeaders: stringifyValues(this._urlLoaderOptions.headers) };
|
||||
this._urlLoader = createURLLoader(opts);
|
||||
this._urlLoader.on('response-started', (event, finalUrl, responseHead) => {
|
||||
const response = this._response = new IncomingMessage(responseHead);
|
||||
|
|
|
@ -376,6 +376,75 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
|||
opts.Get("method", &request->method);
|
||||
opts.Get("url", &request->url);
|
||||
opts.Get("referrer", &request->referrer);
|
||||
std::string origin;
|
||||
opts.Get("origin", &origin);
|
||||
if (!origin.empty()) {
|
||||
request->request_initiator = url::Origin::Create(GURL(origin));
|
||||
}
|
||||
bool has_user_activation;
|
||||
if (opts.Get("hasUserActivation", &has_user_activation)) {
|
||||
request->trusted_params = network::ResourceRequest::TrustedParams();
|
||||
request->trusted_params->has_user_activation = has_user_activation;
|
||||
}
|
||||
|
||||
std::string mode;
|
||||
if (opts.Get("mode", &mode) && !mode.empty()) {
|
||||
if (mode == "navigate") {
|
||||
request->mode = network::mojom::RequestMode::kNavigate;
|
||||
} else if (mode == "cors") {
|
||||
request->mode = network::mojom::RequestMode::kCors;
|
||||
} else if (mode == "no-cors") {
|
||||
request->mode = network::mojom::RequestMode::kNoCors;
|
||||
} else if (mode == "same-origin") {
|
||||
request->mode = network::mojom::RequestMode::kSameOrigin;
|
||||
}
|
||||
}
|
||||
|
||||
std::string destination;
|
||||
if (opts.Get("destination", &destination) && !destination.empty()) {
|
||||
if (destination == "empty") {
|
||||
request->destination = network::mojom::RequestDestination::kEmpty;
|
||||
} else if (destination == "audio") {
|
||||
request->destination = network::mojom::RequestDestination::kAudio;
|
||||
} else if (destination == "audioworklet") {
|
||||
request->destination = network::mojom::RequestDestination::kAudioWorklet;
|
||||
} else if (destination == "document") {
|
||||
request->destination = network::mojom::RequestDestination::kDocument;
|
||||
} else if (destination == "embed") {
|
||||
request->destination = network::mojom::RequestDestination::kEmbed;
|
||||
} else if (destination == "font") {
|
||||
request->destination = network::mojom::RequestDestination::kFont;
|
||||
} else if (destination == "frame") {
|
||||
request->destination = network::mojom::RequestDestination::kFrame;
|
||||
} else if (destination == "iframe") {
|
||||
request->destination = network::mojom::RequestDestination::kIframe;
|
||||
} else if (destination == "image") {
|
||||
request->destination = network::mojom::RequestDestination::kImage;
|
||||
} else if (destination == "manifest") {
|
||||
request->destination = network::mojom::RequestDestination::kManifest;
|
||||
} else if (destination == "object") {
|
||||
request->destination = network::mojom::RequestDestination::kObject;
|
||||
} else if (destination == "paintworklet") {
|
||||
request->destination = network::mojom::RequestDestination::kPaintWorklet;
|
||||
} else if (destination == "report") {
|
||||
request->destination = network::mojom::RequestDestination::kReport;
|
||||
} else if (destination == "script") {
|
||||
request->destination = network::mojom::RequestDestination::kScript;
|
||||
} else if (destination == "serviceworker") {
|
||||
request->destination = network::mojom::RequestDestination::kServiceWorker;
|
||||
} else if (destination == "style") {
|
||||
request->destination = network::mojom::RequestDestination::kStyle;
|
||||
} else if (destination == "track") {
|
||||
request->destination = network::mojom::RequestDestination::kTrack;
|
||||
} else if (destination == "video") {
|
||||
request->destination = network::mojom::RequestDestination::kVideo;
|
||||
} else if (destination == "worker") {
|
||||
request->destination = network::mojom::RequestDestination::kWorker;
|
||||
} else if (destination == "xslt") {
|
||||
request->destination = network::mojom::RequestDestination::kXslt;
|
||||
}
|
||||
}
|
||||
|
||||
bool credentials_specified =
|
||||
opts.Get("credentials", &request->credentials_mode);
|
||||
std::vector<std::pair<std::string, std::string>> extra_headers;
|
||||
|
|
|
@ -475,6 +475,26 @@ describe('net module', () => {
|
|||
await collectStreamBody(response);
|
||||
});
|
||||
|
||||
it('should not change the case of header name', async () => {
|
||||
const customHeaderName = 'X-Header-Name';
|
||||
const customHeaderValue = 'value';
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
expect(request.headers[customHeaderName.toLowerCase()]).to.equal(customHeaderValue.toString());
|
||||
expect(request.rawHeaders.includes(customHeaderName)).to.equal(true);
|
||||
response.statusCode = 200;
|
||||
response.statusMessage = 'OK';
|
||||
response.end();
|
||||
});
|
||||
|
||||
const urlRequest = net.request(serverUrl);
|
||||
urlRequest.setHeader(customHeaderName, customHeaderValue);
|
||||
expect(urlRequest.getHeader(customHeaderName)).to.equal(customHeaderValue);
|
||||
urlRequest.write('');
|
||||
const response = await getResponse(urlRequest);
|
||||
expect(response.statusCode).to.equal(200);
|
||||
await collectStreamBody(response);
|
||||
});
|
||||
|
||||
it('should not be able to set a custom HTTP request header after first write', async () => {
|
||||
const customHeaderName = 'Some-Custom-Header-Name';
|
||||
const customHeaderValue = 'Some-Customer-Header-Value';
|
||||
|
@ -777,6 +797,140 @@ describe('net module', () => {
|
|||
it('should not store cookies');
|
||||
});
|
||||
|
||||
it('should set sec-fetch-site to same-origin for request from same origin', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
expect(request.headers['sec-fetch-site']).to.equal('same-origin');
|
||||
response.statusCode = 200;
|
||||
response.statusMessage = 'OK';
|
||||
response.end();
|
||||
});
|
||||
const urlRequest = net.request({
|
||||
url: serverUrl,
|
||||
origin: serverUrl
|
||||
});
|
||||
await collectStreamBody(await getResponse(urlRequest));
|
||||
});
|
||||
|
||||
it('should set sec-fetch-site to same-origin for request with the same origin header', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
expect(request.headers['sec-fetch-site']).to.equal('same-origin');
|
||||
response.statusCode = 200;
|
||||
response.statusMessage = 'OK';
|
||||
response.end();
|
||||
});
|
||||
const urlRequest = net.request({
|
||||
url: serverUrl
|
||||
});
|
||||
urlRequest.setHeader('Origin', serverUrl);
|
||||
await collectStreamBody(await getResponse(urlRequest));
|
||||
});
|
||||
|
||||
it('should set sec-fetch-site to cross-site for request from other origin', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
expect(request.headers['sec-fetch-site']).to.equal('cross-site');
|
||||
response.statusCode = 200;
|
||||
response.statusMessage = 'OK';
|
||||
response.end();
|
||||
});
|
||||
const urlRequest = net.request({
|
||||
url: serverUrl,
|
||||
origin: 'https://not-exists.com'
|
||||
});
|
||||
await collectStreamBody(await getResponse(urlRequest));
|
||||
});
|
||||
|
||||
it('should not send sec-fetch-user header by default', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
expect(request.headers).not.to.have.property('sec-fetch-user');
|
||||
response.statusCode = 200;
|
||||
response.statusMessage = 'OK';
|
||||
response.end();
|
||||
});
|
||||
const urlRequest = net.request({
|
||||
url: serverUrl
|
||||
});
|
||||
await collectStreamBody(await getResponse(urlRequest));
|
||||
});
|
||||
|
||||
it('should set sec-fetch-user to ?1 if requested', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
expect(request.headers['sec-fetch-user']).to.equal('?1');
|
||||
response.statusCode = 200;
|
||||
response.statusMessage = 'OK';
|
||||
response.end();
|
||||
});
|
||||
const urlRequest = net.request({
|
||||
url: serverUrl
|
||||
});
|
||||
urlRequest.setHeader('sec-fetch-user', '?1');
|
||||
await collectStreamBody(await getResponse(urlRequest));
|
||||
});
|
||||
|
||||
it('should set sec-fetch-mode to no-cors by default', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
expect(request.headers['sec-fetch-mode']).to.equal('no-cors');
|
||||
response.statusCode = 200;
|
||||
response.statusMessage = 'OK';
|
||||
response.end();
|
||||
});
|
||||
const urlRequest = net.request({
|
||||
url: serverUrl
|
||||
});
|
||||
await collectStreamBody(await getResponse(urlRequest));
|
||||
});
|
||||
|
||||
['navigate', 'cors', 'no-cors', 'same-origin'].forEach((mode) => {
|
||||
it(`should set sec-fetch-mode to ${mode} if requested`, async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
expect(request.headers['sec-fetch-mode']).to.equal(mode);
|
||||
response.statusCode = 200;
|
||||
response.statusMessage = 'OK';
|
||||
response.end();
|
||||
});
|
||||
const urlRequest = net.request({
|
||||
url: serverUrl,
|
||||
origin: serverUrl
|
||||
});
|
||||
urlRequest.setHeader('sec-fetch-mode', mode);
|
||||
await collectStreamBody(await getResponse(urlRequest));
|
||||
});
|
||||
});
|
||||
|
||||
it('should set sec-fetch-dest to empty by default', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
expect(request.headers['sec-fetch-dest']).to.equal('empty');
|
||||
response.statusCode = 200;
|
||||
response.statusMessage = 'OK';
|
||||
response.end();
|
||||
});
|
||||
const urlRequest = net.request({
|
||||
url: serverUrl
|
||||
});
|
||||
await collectStreamBody(await getResponse(urlRequest));
|
||||
});
|
||||
|
||||
[
|
||||
'empty', 'audio', 'audioworklet', 'document', 'embed', 'font',
|
||||
'frame', 'iframe', 'image', 'manifest', 'object', 'paintworklet',
|
||||
'report', 'script', 'serviceworker', 'style', 'track', 'video',
|
||||
'worker', 'xslt'
|
||||
].forEach((dest) => {
|
||||
it(`should set sec-fetch-dest to ${dest} if requested`, async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
expect(request.headers['sec-fetch-dest']).to.equal(dest);
|
||||
response.statusCode = 200;
|
||||
response.statusMessage = 'OK';
|
||||
response.end();
|
||||
});
|
||||
const urlRequest = net.request({
|
||||
url: serverUrl,
|
||||
origin: serverUrl
|
||||
});
|
||||
urlRequest.setHeader('sec-fetch-dest', dest);
|
||||
await collectStreamBody(await getResponse(urlRequest));
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to abort an HTTP request before first write', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
response.end();
|
||||
|
|
4
typings/internal-ambient.d.ts
vendored
4
typings/internal-ambient.d.ts
vendored
|
@ -119,6 +119,10 @@ declare namespace NodeJS {
|
|||
session?: Electron.Session;
|
||||
partition?: string;
|
||||
referrer?: string;
|
||||
origin?: string;
|
||||
hasUserActivation?: boolean;
|
||||
mode?: string;
|
||||
destination?: string;
|
||||
};
|
||||
type ResponseHead = {
|
||||
statusCode: number;
|
||||
|
|
Loading…
Reference in a new issue