Skip to content

Commit e1cc78f

Browse files
authored
feat: allow setting the Origin header and Sec-Fetch-* headers in net.request() (electron#26135)
1 parent b8372fd commit e1cc78f

File tree

5 files changed

+251
-13
lines changed

5 files changed

+251
-13
lines changed

docs/api/client-request.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ following properties:
4747
be aborted. When mode is `manual` the redirection will be cancelled unless
4848
[`request.followRedirect`](#requestfollowredirect) is invoked synchronously
4949
during the [`redirect`](#event-redirect) event. Defaults to `follow`.
50+
* `origin` String (optional) - The origin URL of the request.
5051

5152
`options` properties such as `protocol`, `host`, `hostname`, `port` and `path`
5253
strictly follow the Node.js model as described in the

lib/browser/api/net.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ class ChunkedBodyStream extends Writable {
197197

198198
type RedirectPolicy = 'manual' | 'follow' | 'error';
199199

200-
function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, extraHeaders: Record<string, string> } {
200+
function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }> } {
201201
const options: any = typeof optionsIn === 'string' ? url.parse(optionsIn) : { ...optionsIn };
202202

203203
let urlStr: string = options.url;
@@ -249,22 +249,26 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
249249
throw new TypeError('headers must be an object');
250250
}
251251

252-
const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, extraHeaders: Record<string, string | string[]> } = {
252+
const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }> } = {
253253
method: (options.method || 'GET').toUpperCase(),
254254
url: urlStr,
255255
redirectPolicy,
256-
extraHeaders: options.headers || {},
256+
headers: {},
257257
body: null as any,
258258
useSessionCookies: options.useSessionCookies,
259-
credentials: options.credentials
259+
credentials: options.credentials,
260+
origin: options.origin
260261
};
261-
for (const [name, value] of Object.entries(urlLoaderOptions.extraHeaders!)) {
262+
const headers: Record<string, string | string[]> = options.headers || {};
263+
for (const [name, value] of Object.entries(headers)) {
262264
if (!isValidHeaderName(name)) {
263265
throw new Error(`Invalid header name: '${name}'`);
264266
}
265267
if (!isValidHeaderValue(value.toString())) {
266268
throw new Error(`Invalid value for header '${name}': '${value}'`);
267269
}
270+
const key = name.toLowerCase();
271+
urlLoaderOptions.headers[key] = { name, value };
268272
}
269273
if (options.session) {
270274
// 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 {
289293
_aborted: boolean = false;
290294
_chunkedEncoding: boolean | undefined;
291295
_body: Writable | undefined;
292-
_urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { extraHeaders: Record<string, string> };
296+
_urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { headers: Record<string, { name: string, value: string | string[] }> };
293297
_redirectPolicy: RedirectPolicy;
294298
_followRedirectCb?: () => void;
295299
_uploadProgress?: { active: boolean, started: boolean, current: number, total: number };
@@ -350,7 +354,7 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
350354
}
351355

352356
const key = name.toLowerCase();
353-
this._urlLoaderOptions.extraHeaders[key] = value;
357+
this._urlLoaderOptions.headers[key] = { name, value };
354358
}
355359

356360
getHeader (name: string) {
@@ -359,7 +363,8 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
359363
}
360364

361365
const key = name.toLowerCase();
362-
return this._urlLoaderOptions.extraHeaders[key];
366+
const header = this._urlLoaderOptions.headers[key];
367+
return header && header.value as any;
363368
}
364369

365370
removeHeader (name: string) {
@@ -372,7 +377,7 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
372377
}
373378

374379
const key = name.toLowerCase();
375-
delete this._urlLoaderOptions.extraHeaders[key];
380+
delete this._urlLoaderOptions.headers[key];
376381
}
377382

378383
_write (chunk: Buffer, encoding: BufferEncoding, callback: () => void) {
@@ -401,15 +406,20 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
401406

402407
_startRequest () {
403408
this._started = true;
404-
const stringifyValues = (obj: Record<string, any>) => {
409+
const stringifyValues = (obj: Record<string, { name: string, value: string | string[] }>) => {
405410
const ret: Record<string, string> = {};
406411
for (const k of Object.keys(obj)) {
407-
ret[k] = obj[k].toString();
412+
const kv = obj[k];
413+
ret[kv.name] = kv.value.toString();
408414
}
409415
return ret;
410416
};
411-
this._urlLoaderOptions.referrer = this._urlLoaderOptions.extraHeaders.referer || '';
412-
const opts = { ...this._urlLoaderOptions, extraHeaders: stringifyValues(this._urlLoaderOptions.extraHeaders) };
417+
this._urlLoaderOptions.referrer = this.getHeader('referer') || '';
418+
this._urlLoaderOptions.origin = this._urlLoaderOptions.origin || this.getHeader('origin') || '';
419+
this._urlLoaderOptions.hasUserActivation = this.getHeader('sec-fetch-user') === '?1';
420+
this._urlLoaderOptions.mode = this.getHeader('sec-fetch-mode') || '';
421+
this._urlLoaderOptions.destination = this.getHeader('sec-fetch-dest') || '';
422+
const opts = { ...this._urlLoaderOptions, extraHeaders: stringifyValues(this._urlLoaderOptions.headers) };
413423
this._urlLoader = createURLLoader(opts);
414424
this._urlLoader.on('response-started', (event, finalUrl, responseHead) => {
415425
const response = this._response = new IncomingMessage(responseHead);

shell/browser/api/electron_api_url_loader.cc

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,75 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
376376
opts.Get("method", &request->method);
377377
opts.Get("url", &request->url);
378378
opts.Get("referrer", &request->referrer);
379+
std::string origin;
380+
opts.Get("origin", &origin);
381+
if (!origin.empty()) {
382+
request->request_initiator = url::Origin::Create(GURL(origin));
383+
}
384+
bool has_user_activation;
385+
if (opts.Get("hasUserActivation", &has_user_activation)) {
386+
request->trusted_params = network::ResourceRequest::TrustedParams();
387+
request->trusted_params->has_user_activation = has_user_activation;
388+
}
389+
390+
std::string mode;
391+
if (opts.Get("mode", &mode) && !mode.empty()) {
392+
if (mode == "navigate") {
393+
request->mode = network::mojom::RequestMode::kNavigate;
394+
} else if (mode == "cors") {
395+
request->mode = network::mojom::RequestMode::kCors;
396+
} else if (mode == "no-cors") {
397+
request->mode = network::mojom::RequestMode::kNoCors;
398+
} else if (mode == "same-origin") {
399+
request->mode = network::mojom::RequestMode::kSameOrigin;
400+
}
401+
}
402+
403+
std::string destination;
404+
if (opts.Get("destination", &destination) && !destination.empty()) {
405+
if (destination == "empty") {
406+
request->destination = network::mojom::RequestDestination::kEmpty;
407+
} else if (destination == "audio") {
408+
request->destination = network::mojom::RequestDestination::kAudio;
409+
} else if (destination == "audioworklet") {
410+
request->destination = network::mojom::RequestDestination::kAudioWorklet;
411+
} else if (destination == "document") {
412+
request->destination = network::mojom::RequestDestination::kDocument;
413+
} else if (destination == "embed") {
414+
request->destination = network::mojom::RequestDestination::kEmbed;
415+
} else if (destination == "font") {
416+
request->destination = network::mojom::RequestDestination::kFont;
417+
} else if (destination == "frame") {
418+
request->destination = network::mojom::RequestDestination::kFrame;
419+
} else if (destination == "iframe") {
420+
request->destination = network::mojom::RequestDestination::kIframe;
421+
} else if (destination == "image") {
422+
request->destination = network::mojom::RequestDestination::kImage;
423+
} else if (destination == "manifest") {
424+
request->destination = network::mojom::RequestDestination::kManifest;
425+
} else if (destination == "object") {
426+
request->destination = network::mojom::RequestDestination::kObject;
427+
} else if (destination == "paintworklet") {
428+
request->destination = network::mojom::RequestDestination::kPaintWorklet;
429+
} else if (destination == "report") {
430+
request->destination = network::mojom::RequestDestination::kReport;
431+
} else if (destination == "script") {
432+
request->destination = network::mojom::RequestDestination::kScript;
433+
} else if (destination == "serviceworker") {
434+
request->destination = network::mojom::RequestDestination::kServiceWorker;
435+
} else if (destination == "style") {
436+
request->destination = network::mojom::RequestDestination::kStyle;
437+
} else if (destination == "track") {
438+
request->destination = network::mojom::RequestDestination::kTrack;
439+
} else if (destination == "video") {
440+
request->destination = network::mojom::RequestDestination::kVideo;
441+
} else if (destination == "worker") {
442+
request->destination = network::mojom::RequestDestination::kWorker;
443+
} else if (destination == "xslt") {
444+
request->destination = network::mojom::RequestDestination::kXslt;
445+
}
446+
}
447+
379448
bool credentials_specified =
380449
opts.Get("credentials", &request->credentials_mode);
381450
std::vector<std::pair<std::string, std::string>> extra_headers;

spec-main/api-net-spec.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,26 @@ describe('net module', () => {
475475
await collectStreamBody(response);
476476
});
477477

478+
it('should not change the case of header name', async () => {
479+
const customHeaderName = 'X-Header-Name';
480+
const customHeaderValue = 'value';
481+
const serverUrl = await respondOnce.toSingleURL((request, response) => {
482+
expect(request.headers[customHeaderName.toLowerCase()]).to.equal(customHeaderValue.toString());
483+
expect(request.rawHeaders.includes(customHeaderName)).to.equal(true);
484+
response.statusCode = 200;
485+
response.statusMessage = 'OK';
486+
response.end();
487+
});
488+
489+
const urlRequest = net.request(serverUrl);
490+
urlRequest.setHeader(customHeaderName, customHeaderValue);
491+
expect(urlRequest.getHeader(customHeaderName)).to.equal(customHeaderValue);
492+
urlRequest.write('');
493+
const response = await getResponse(urlRequest);
494+
expect(response.statusCode).to.equal(200);
495+
await collectStreamBody(response);
496+
});
497+
478498
it('should not be able to set a custom HTTP request header after first write', async () => {
479499
const customHeaderName = 'Some-Custom-Header-Name';
480500
const customHeaderValue = 'Some-Customer-Header-Value';
@@ -777,6 +797,140 @@ describe('net module', () => {
777797
it('should not store cookies');
778798
});
779799

800+
it('should set sec-fetch-site to same-origin for request from same origin', async () => {
801+
const serverUrl = await respondOnce.toSingleURL((request, response) => {
802+
expect(request.headers['sec-fetch-site']).to.equal('same-origin');
803+
response.statusCode = 200;
804+
response.statusMessage = 'OK';
805+
response.end();
806+
});
807+
const urlRequest = net.request({
808+
url: serverUrl,
809+
origin: serverUrl
810+
});
811+
await collectStreamBody(await getResponse(urlRequest));
812+
});
813+
814+
it('should set sec-fetch-site to same-origin for request with the same origin header', async () => {
815+
const serverUrl = await respondOnce.toSingleURL((request, response) => {
816+
expect(request.headers['sec-fetch-site']).to.equal('same-origin');
817+
response.statusCode = 200;
818+
response.statusMessage = 'OK';
819+
response.end();
820+
});
821+
const urlRequest = net.request({
822+
url: serverUrl
823+
});
824+
urlRequest.setHeader('Origin', serverUrl);
825+
await collectStreamBody(await getResponse(urlRequest));
826+
});
827+
828+
it('should set sec-fetch-site to cross-site for request from other origin', async () => {
829+
const serverUrl = await respondOnce.toSingleURL((request, response) => {
830+
expect(request.headers['sec-fetch-site']).to.equal('cross-site');
831+
response.statusCode = 200;
832+
response.statusMessage = 'OK';
833+
response.end();
834+
});
835+
const urlRequest = net.request({
836+
url: serverUrl,
837+
origin: 'https://not-exists.com'
838+
});
839+
await collectStreamBody(await getResponse(urlRequest));
840+
});
841+
842+
it('should not send sec-fetch-user header by default', async () => {
843+
const serverUrl = await respondOnce.toSingleURL((request, response) => {
844+
expect(request.headers).not.to.have.property('sec-fetch-user');
845+
response.statusCode = 200;
846+
response.statusMessage = 'OK';
847+
response.end();
848+
});
849+
const urlRequest = net.request({
850+
url: serverUrl
851+
});
852+
await collectStreamBody(await getResponse(urlRequest));
853+
});
854+
855+
it('should set sec-fetch-user to ?1 if requested', async () => {
856+
const serverUrl = await respondOnce.toSingleURL((request, response) => {
857+
expect(request.headers['sec-fetch-user']).to.equal('?1');
858+
response.statusCode = 200;
859+
response.statusMessage = 'OK';
860+
response.end();
861+
});
862+
const urlRequest = net.request({
863+
url: serverUrl
864+
});
865+
urlRequest.setHeader('sec-fetch-user', '?1');
866+
await collectStreamBody(await getResponse(urlRequest));
867+
});
868+
869+
it('should set sec-fetch-mode to no-cors by default', async () => {
870+
const serverUrl = await respondOnce.toSingleURL((request, response) => {
871+
expect(request.headers['sec-fetch-mode']).to.equal('no-cors');
872+
response.statusCode = 200;
873+
response.statusMessage = 'OK';
874+
response.end();
875+
});
876+
const urlRequest = net.request({
877+
url: serverUrl
878+
});
879+
await collectStreamBody(await getResponse(urlRequest));
880+
});
881+
882+
['navigate', 'cors', 'no-cors', 'same-origin'].forEach((mode) => {
883+
it(`should set sec-fetch-mode to ${mode} if requested`, async () => {
884+
const serverUrl = await respondOnce.toSingleURL((request, response) => {
885+
expect(request.headers['sec-fetch-mode']).to.equal(mode);
886+
response.statusCode = 200;
887+
response.statusMessage = 'OK';
888+
response.end();
889+
});
890+
const urlRequest = net.request({
891+
url: serverUrl,
892+
origin: serverUrl
893+
});
894+
urlRequest.setHeader('sec-fetch-mode', mode);
895+
await collectStreamBody(await getResponse(urlRequest));
896+
});
897+
});
898+
899+
it('should set sec-fetch-dest to empty by default', async () => {
900+
const serverUrl = await respondOnce.toSingleURL((request, response) => {
901+
expect(request.headers['sec-fetch-dest']).to.equal('empty');
902+
response.statusCode = 200;
903+
response.statusMessage = 'OK';
904+
response.end();
905+
});
906+
const urlRequest = net.request({
907+
url: serverUrl
908+
});
909+
await collectStreamBody(await getResponse(urlRequest));
910+
});
911+
912+
[
913+
'empty', 'audio', 'audioworklet', 'document', 'embed', 'font',
914+
'frame', 'iframe', 'image', 'manifest', 'object', 'paintworklet',
915+
'report', 'script', 'serviceworker', 'style', 'track', 'video',
916+
'worker', 'xslt'
917+
].forEach((dest) => {
918+
it(`should set sec-fetch-dest to ${dest} if requested`, async () => {
919+
const serverUrl = await respondOnce.toSingleURL((request, response) => {
920+
expect(request.headers['sec-fetch-dest']).to.equal(dest);
921+
response.statusCode = 200;
922+
response.statusMessage = 'OK';
923+
response.end();
924+
});
925+
const urlRequest = net.request({
926+
url: serverUrl,
927+
origin: serverUrl
928+
});
929+
urlRequest.setHeader('sec-fetch-dest', dest);
930+
await collectStreamBody(await getResponse(urlRequest));
931+
});
932+
});
933+
780934
it('should be able to abort an HTTP request before first write', async () => {
781935
const serverUrl = await respondOnce.toSingleURL((request, response) => {
782936
response.end();

typings/internal-ambient.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ declare namespace NodeJS {
119119
session?: Electron.Session;
120120
partition?: string;
121121
referrer?: string;
122+
origin?: string;
123+
hasUserActivation?: boolean;
124+
mode?: string;
125+
destination?: string;
122126
};
123127
type ResponseHead = {
124128
statusCode: number;

0 commit comments

Comments
 (0)