Skip to content

Commit

Permalink
feat: allow setting the Origin header and Sec-Fetch-* headers in net.…
Browse files Browse the repository at this point in the history
…request() (#26135)
  • Loading branch information
zeeker999 committed Nov 17, 2020
1 parent b8372fd commit e1cc78f
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 13 deletions.
1 change: 1 addition & 0 deletions docs/api/client-request.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 23 additions & 13 deletions lib/browser/api/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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 };
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
69 changes: 69 additions & 0 deletions shell/browser/api/electron_api_url_loader.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
154 changes: 154 additions & 0 deletions spec-main/api-net-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions typings/internal-ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit e1cc78f

Please sign in to comment.