diff --git a/docs/api/client-request.md b/docs/api/client-request.md index 786d6904214a..865f733d7847 100644 --- a/docs/api/client-request.md +++ b/docs/api/client-request.md @@ -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 diff --git a/lib/browser/api/net.ts b/lib/browser/api/net.ts index f2c79babdfaf..c50395668fad 100644 --- a/lib/browser/api/net.ts +++ b/lib/browser/api/net.ts @@ -197,7 +197,7 @@ class ChunkedBodyStream extends Writable { type RedirectPolicy = 'manual' | 'follow' | 'error'; -function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, extraHeaders: Record } { +function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record } { 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 } = { + const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record } = { 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 = 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 }; + _urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { headers: Record }; _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) => { + const stringifyValues = (obj: Record) => { const ret: Record = {}; 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); diff --git a/shell/browser/api/electron_api_url_loader.cc b/shell/browser/api/electron_api_url_loader.cc index 3cd7b96409eb..7647d5c79860 100644 --- a/shell/browser/api/electron_api_url_loader.cc +++ b/shell/browser/api/electron_api_url_loader.cc @@ -376,6 +376,75 @@ gin::Handle 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> extra_headers; diff --git a/spec-main/api-net-spec.ts b/spec-main/api-net-spec.ts index 4d229d664765..887c35be6637 100644 --- a/spec-main/api-net-spec.ts +++ b/spec-main/api-net-spec.ts @@ -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(); diff --git a/typings/internal-ambient.d.ts b/typings/internal-ambient.d.ts index 9cb2ea179da9..08e73f491ad0 100644 --- a/typings/internal-ambient.d.ts +++ b/typings/internal-ambient.d.ts @@ -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;