feat: [net] add "credentials" option to net.request (#25284)
* feat: [net] add "credentials" option to net.request * remove debugging log * add tests
This commit is contained in:
parent
8970c80520
commit
0e7d59dd79
5 changed files with 388 additions and 266 deletions
|
@ -20,23 +20,33 @@ form with the protocol scheme specified as http or https.
|
|||
which the request is associated.
|
||||
* `partition` String (optional) - The name of the [`partition`](session.md)
|
||||
with which the request is associated. Defaults to the empty string. The
|
||||
`session` option prevails on `partition`. Thus if a `session` is explicitly
|
||||
`session` option supersedes `partition`. Thus if a `session` is explicitly
|
||||
specified, `partition` is ignored.
|
||||
* `credentials` String (optional) - Can be `include` or `omit`. Whether to
|
||||
send [credentials](https://fetch.spec.whatwg.org/#credentials) with this
|
||||
request. If set to `include`, credentials from the session associated with
|
||||
the request will be used. If set to `omit`, credentials will not be sent
|
||||
with the request (and the `'login'` event will not be triggered in the
|
||||
event of a 401). This matches the behavior of the
|
||||
[fetch](https://fetch.spec.whatwg.org/#concept-request-credentials-mode)
|
||||
option of the same name. If this option is not specified, authentication
|
||||
data from the session will be sent, and cookies will not be sent (unless
|
||||
`useSessionCookies` is set).
|
||||
* `useSessionCookies` Boolean (optional) - Whether to send cookies with this
|
||||
request from the provided session. This will make the `net` request's
|
||||
cookie behavior match a `fetch` request. Default is `false`.
|
||||
* `protocol` String (optional) - The protocol scheme in the form 'scheme:'.
|
||||
Currently supported values are 'http:' or 'https:'. Defaults to 'http:'.
|
||||
request from the provided session. If `credentials` is specified, this
|
||||
option has no effect. Default is `false`.
|
||||
* `protocol` String (optional) - Can be `http:` or `https:`. The protocol
|
||||
scheme in the form 'scheme:'. Defaults to 'http:'.
|
||||
* `host` String (optional) - The server host provided as a concatenation of
|
||||
the hostname and the port number 'hostname:port'.
|
||||
* `hostname` String (optional) - The server host name.
|
||||
* `port` Integer (optional) - The server's listening port number.
|
||||
* `path` String (optional) - The path part of the request URL.
|
||||
* `redirect` String (optional) - The redirect mode for this request. Should be
|
||||
one of `follow`, `error` or `manual`. Defaults to `follow`. When mode is `error`,
|
||||
any redirection will be aborted. When mode is `manual` the redirection will be
|
||||
cancelled unless [`request.followRedirect`](#requestfollowredirect) is invoked
|
||||
synchronously during the [`redirect`](#event-redirect) event.
|
||||
* `redirect` String (optional) - Can be `follow`, `error` or `manual`. The
|
||||
redirect mode for this request. When mode is `error`, any redirection will
|
||||
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`.
|
||||
|
||||
`options` properties such as `protocol`, `host`, `hostname`, `port` and `path`
|
||||
strictly follow the Node.js model as described in the
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as url from 'url';
|
|||
import { Readable, Writable } from 'stream';
|
||||
import { app } from 'electron/main';
|
||||
import type { ClientRequestConstructorOptions, UploadProgress } from 'electron/main';
|
||||
|
||||
const {
|
||||
isValidHeaderName,
|
||||
isValidHeaderValue,
|
||||
|
@ -243,7 +244,8 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
|
|||
redirectPolicy,
|
||||
extraHeaders: options.headers || {},
|
||||
body: null as any,
|
||||
useSessionCookies: options.useSessionCookies || false
|
||||
useSessionCookies: options.useSessionCookies,
|
||||
credentials: options.credentials
|
||||
};
|
||||
for (const [name, value] of Object.entries(urlLoaderOptions.extraHeaders!)) {
|
||||
if (!isValidHeaderName(name)) {
|
||||
|
|
|
@ -47,6 +47,27 @@ struct Converter<network::mojom::HttpRawHeaderPairPtr> {
|
|||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Converter<network::mojom::CredentialsMode> {
|
||||
static bool FromV8(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> val,
|
||||
network::mojom::CredentialsMode* out) {
|
||||
std::string mode;
|
||||
if (!ConvertFromV8(isolate, val, &mode))
|
||||
return false;
|
||||
if (mode == "omit")
|
||||
*out = network::mojom::CredentialsMode::kOmit;
|
||||
else if (mode == "include")
|
||||
*out = network::mojom::CredentialsMode::kInclude;
|
||||
else
|
||||
// "same-origin" is technically a member of this enum as well, but it
|
||||
// doesn't make sense in the context of `net.request()`, so don't convert
|
||||
// it.
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}; // namespace gin
|
||||
|
||||
} // namespace gin
|
||||
|
||||
namespace electron {
|
||||
|
@ -355,6 +376,8 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
|||
opts.Get("method", &request->method);
|
||||
opts.Get("url", &request->url);
|
||||
opts.Get("referrer", &request->referrer);
|
||||
bool credentials_specified =
|
||||
opts.Get("credentials", &request->credentials_mode);
|
||||
std::map<std::string, std::string> extra_headers;
|
||||
if (opts.Get("extraHeaders", &extra_headers)) {
|
||||
for (const auto& it : extra_headers) {
|
||||
|
@ -370,7 +393,10 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
|||
bool use_session_cookies = false;
|
||||
opts.Get("useSessionCookies", &use_session_cookies);
|
||||
int options = 0;
|
||||
if (!use_session_cookies) {
|
||||
if (!credentials_specified && !use_session_cookies) {
|
||||
// This is the default case, as well as the case when credentials is not
|
||||
// specified and useSessionCoookies is false. credentials_mode will be
|
||||
// kInclude, but cookies will be blocked.
|
||||
request->credentials_mode = network::mojom::CredentialsMode::kInclude;
|
||||
options |= network::mojom::kURLLoadOptionBlockAllCookies;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { expect } from 'chai';
|
||||
import { net, session, ClientRequest, BrowserWindow } from 'electron/main';
|
||||
import { net, session, ClientRequest, BrowserWindow, ClientRequestConstructorOptions } from 'electron/main';
|
||||
import * as http from 'http';
|
||||
import * as url from 'url';
|
||||
import { AddressInfo, Socket } from 'net';
|
||||
|
@ -215,6 +215,8 @@ describe('net module', () => {
|
|||
expect(chunkIndex).to.be.equal(chunkCount);
|
||||
});
|
||||
|
||||
for (const extraOptions of [{}, { credentials: 'include' }, { useSessionCookies: false, credentials: 'include' }] as ClientRequestConstructorOptions[]) {
|
||||
describe(`authentication when ${JSON.stringify(extraOptions)}`, () => {
|
||||
it('should emit the login event when 401', async () => {
|
||||
const [user, pass] = ['user', 'pass'];
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
|
@ -224,7 +226,7 @@ describe('net module', () => {
|
|||
response.writeHead(200).end('ok');
|
||||
});
|
||||
let loginAuthInfo: Electron.AuthInfo;
|
||||
const request = net.request({ method: 'GET', url: serverUrl });
|
||||
const request = net.request({ method: 'GET', url: serverUrl, ...extraOptions });
|
||||
request.on('login', (authInfo, cb) => {
|
||||
loginAuthInfo = authInfo;
|
||||
cb(user, pass);
|
||||
|
@ -235,7 +237,7 @@ describe('net module', () => {
|
|||
expect(loginAuthInfo!.scheme).to.equal('basic');
|
||||
});
|
||||
|
||||
it('should response when cancelling authentication', async () => {
|
||||
it('should receive 401 response when cancelling authentication', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
if (!request.headers.authorization) {
|
||||
response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' });
|
||||
|
@ -244,12 +246,13 @@ describe('net module', () => {
|
|||
response.writeHead(200).end('ok');
|
||||
}
|
||||
});
|
||||
const request = net.request({ method: 'GET', url: serverUrl });
|
||||
const request = net.request({ method: 'GET', url: serverUrl, ...extraOptions });
|
||||
request.on('login', (authInfo, cb) => {
|
||||
cb();
|
||||
});
|
||||
const response = await getResponse(request);
|
||||
const body = await collectStreamBody(response);
|
||||
expect(response.statusCode).to.equal(401);
|
||||
expect(body).to.equal('unauthenticated');
|
||||
});
|
||||
|
||||
|
@ -268,7 +271,7 @@ describe('net module', () => {
|
|||
});
|
||||
await bw.loadURL(serverUrl);
|
||||
bw.close();
|
||||
const request = net.request({ method: 'GET', url: serverUrl });
|
||||
const request = net.request({ method: 'GET', url: serverUrl, ...extraOptions });
|
||||
let logInCount = 0;
|
||||
request.on('login', () => {
|
||||
logInCount++;
|
||||
|
@ -295,7 +298,7 @@ describe('net module', () => {
|
|||
});
|
||||
await bw.loadURL('http://127.0.0.1:9999');
|
||||
bw.close();
|
||||
const request = net.request({ method: 'GET', url: 'http://127.0.0.1:9999', session: customSession });
|
||||
const request = net.request({ method: 'GET', url: 'http://127.0.0.1:9999', session: customSession, ...extraOptions });
|
||||
let logInCount = 0;
|
||||
request.on('login', () => {
|
||||
logInCount++;
|
||||
|
@ -318,7 +321,7 @@ describe('net module', () => {
|
|||
request.on('end', () => response.end());
|
||||
});
|
||||
const requestData = randomString(kOneKiloByte);
|
||||
const request = net.request({ method: 'GET', url: serverUrl });
|
||||
const request = net.request({ method: 'GET', url: serverUrl, ...extraOptions });
|
||||
request.on('login', (authInfo, cb) => {
|
||||
cb(user, pass);
|
||||
});
|
||||
|
@ -328,6 +331,77 @@ describe('net module', () => {
|
|||
expect(responseData).to.equal(requestData);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('authentication when {"credentials":"omit"}', () => {
|
||||
it('should not emit the login event when 401', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
if (!request.headers.authorization) {
|
||||
return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end();
|
||||
}
|
||||
response.writeHead(200).end('ok');
|
||||
});
|
||||
const request = net.request({ method: 'GET', url: serverUrl, credentials: 'omit' });
|
||||
request.on('login', () => {
|
||||
expect.fail('unexpected login event');
|
||||
});
|
||||
const response = await getResponse(request);
|
||||
expect(response.statusCode).to.equal(401);
|
||||
expect(response.headers['www-authenticate']).to.equal('Basic realm="Foo"');
|
||||
});
|
||||
|
||||
it('should not share credentials with WebContents', async () => {
|
||||
const [user, pass] = ['user', 'pass'];
|
||||
const serverUrl = await respondNTimes.toSingleURL((request, response) => {
|
||||
if (!request.headers.authorization) {
|
||||
return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end();
|
||||
}
|
||||
return response.writeHead(200).end('ok');
|
||||
}, 2);
|
||||
const bw = new BrowserWindow({ show: false });
|
||||
bw.webContents.on('login', (event, details, authInfo, cb) => {
|
||||
event.preventDefault();
|
||||
cb(user, pass);
|
||||
});
|
||||
await bw.loadURL(serverUrl);
|
||||
bw.close();
|
||||
const request = net.request({ method: 'GET', url: serverUrl, credentials: 'omit' });
|
||||
request.on('login', () => {
|
||||
expect.fail();
|
||||
});
|
||||
const response = await getResponse(request);
|
||||
expect(response.statusCode).to.equal(401);
|
||||
expect(response.headers['www-authenticate']).to.equal('Basic realm="Foo"');
|
||||
});
|
||||
|
||||
it('should share proxy credentials with WebContents', async () => {
|
||||
const [user, pass] = ['user', 'pass'];
|
||||
const proxyUrl = await respondNTimes((request, response) => {
|
||||
if (!request.headers['proxy-authorization']) {
|
||||
return response.writeHead(407, { 'Proxy-Authenticate': 'Basic realm="Foo"' }).end();
|
||||
}
|
||||
return response.writeHead(200).end('ok');
|
||||
}, 2);
|
||||
const customSession = session.fromPartition(`net-proxy-test-${Math.random()}`);
|
||||
await customSession.setProxy({ proxyRules: proxyUrl.replace('http://', ''), proxyBypassRules: '<-loopback>' });
|
||||
const bw = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
|
||||
bw.webContents.on('login', (event, details, authInfo, cb) => {
|
||||
event.preventDefault();
|
||||
cb(user, pass);
|
||||
});
|
||||
await bw.loadURL('http://127.0.0.1:9999');
|
||||
bw.close();
|
||||
const request = net.request({ method: 'GET', url: 'http://127.0.0.1:9999', session: customSession, credentials: 'omit' });
|
||||
request.on('login', () => {
|
||||
expect.fail();
|
||||
});
|
||||
const response = await getResponse(request);
|
||||
const body = await collectStreamBody(response);
|
||||
expect(response.statusCode).to.equal(200);
|
||||
expect(body).to.equal('ok');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ClientRequest API', () => {
|
||||
it('request/response objects should emit expected events', async () => {
|
||||
|
@ -466,7 +540,7 @@ describe('net module', () => {
|
|||
it('should be able to set cookie header line', async () => {
|
||||
const cookieHeaderName = 'Cookie';
|
||||
const cookieHeaderValue = 'test=12345';
|
||||
const customSession = session.fromPartition('test-cookie-header');
|
||||
const customSession = session.fromPartition(`test-cookie-header-${Math.random()}`);
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
expect(request.headers[cookieHeaderName.toLowerCase()]).to.equal(cookieHeaderValue);
|
||||
response.statusCode = 200;
|
||||
|
@ -511,7 +585,7 @@ describe('net module', () => {
|
|||
response.setHeader('x-cookie', `${request.headers.cookie!}`);
|
||||
response.end();
|
||||
});
|
||||
const sess = session.fromPartition('cookie-tests-1');
|
||||
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||
const cookieVal = `${Date.now()}`;
|
||||
await sess.cookies.set({
|
||||
url: serverUrl,
|
||||
|
@ -526,6 +600,8 @@ describe('net module', () => {
|
|||
expect(response.headers['x-cookie']).to.equal('undefined');
|
||||
});
|
||||
|
||||
for (const extraOptions of [{ useSessionCookies: true }, { credentials: 'include' }] as ClientRequestConstructorOptions[]) {
|
||||
describe(`when ${JSON.stringify(extraOptions)}`, () => {
|
||||
it('should be able to use the sessions cookie store', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
response.statusCode = 200;
|
||||
|
@ -533,7 +609,7 @@ describe('net module', () => {
|
|||
response.setHeader('x-cookie', request.headers.cookie!);
|
||||
response.end();
|
||||
});
|
||||
const sess = session.fromPartition('cookie-tests-2');
|
||||
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||
const cookieVal = `${Date.now()}`;
|
||||
await sess.cookies.set({
|
||||
url: serverUrl,
|
||||
|
@ -543,7 +619,7 @@ describe('net module', () => {
|
|||
const urlRequest = net.request({
|
||||
url: serverUrl,
|
||||
session: sess,
|
||||
useSessionCookies: true
|
||||
...extraOptions
|
||||
});
|
||||
const response = await getResponse(urlRequest);
|
||||
expect(response.headers['x-cookie']).to.equal(`wild_cookie=${cookieVal}`);
|
||||
|
@ -556,13 +632,13 @@ describe('net module', () => {
|
|||
response.setHeader('set-cookie', 'foo=bar');
|
||||
response.end();
|
||||
});
|
||||
const sess = session.fromPartition('cookie-tests-3');
|
||||
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||
let cookies = await sess.cookies.get({});
|
||||
expect(cookies).to.have.lengthOf(0);
|
||||
const urlRequest = net.request({
|
||||
url: serverUrl,
|
||||
session: sess,
|
||||
useSessionCookies: true
|
||||
...extraOptions
|
||||
});
|
||||
await collectStreamBody(await getResponse(urlRequest));
|
||||
cookies = await sess.cookies.get({});
|
||||
|
@ -589,13 +665,13 @@ describe('net module', () => {
|
|||
response.setHeader('x-cookie', `${request.headers.cookie}`);
|
||||
response.end();
|
||||
}, 2);
|
||||
const sess = session.fromPartition(`cookie-tests-same-site-${mode}`);
|
||||
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||
let cookies = await sess.cookies.get({});
|
||||
expect(cookies).to.have.lengthOf(0);
|
||||
const urlRequest = net.request({
|
||||
url: serverUrl,
|
||||
session: sess,
|
||||
useSessionCookies: true
|
||||
...extraOptions
|
||||
});
|
||||
const response = await getResponse(urlRequest);
|
||||
expect(response.headers['x-cookie']).to.equal('undefined');
|
||||
|
@ -616,7 +692,7 @@ describe('net module', () => {
|
|||
const urlRequest2 = net.request({
|
||||
url: serverUrl,
|
||||
session: sess,
|
||||
useSessionCookies: true
|
||||
...extraOptions
|
||||
});
|
||||
const response2 = await getResponse(urlRequest2);
|
||||
expect(response2.headers['x-cookie']).to.equal('same=site');
|
||||
|
@ -637,7 +713,7 @@ describe('net module', () => {
|
|||
response.setHeader('location', newUrl.replace('127.0.0.1', 'localhost'));
|
||||
response.end();
|
||||
});
|
||||
const sess = session.fromPartition('cookie-tests-4');
|
||||
const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
|
||||
const cookie127Val = `${Date.now()}-127`;
|
||||
const cookieLocalVal = `${Date.now()}-local`;
|
||||
const localhostUrl = serverUrl.replace('127.0.0.1', 'localhost');
|
||||
|
@ -656,7 +732,7 @@ describe('net module', () => {
|
|||
const urlRequest = net.request({
|
||||
url: serverUrl,
|
||||
session: sess,
|
||||
useSessionCookies: true
|
||||
...extraOptions
|
||||
});
|
||||
urlRequest.on('redirect', (status, method, url, headers) => {
|
||||
// The initial redirect response should have received the 127 value here
|
||||
|
@ -672,6 +748,13 @@ describe('net module', () => {
|
|||
// and attach the cookies for the new target domain
|
||||
expect(response.headers['x-cookie']).to.equal(`wild_cookie=${cookieLocalVal}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('when {"credentials":"omit"}', () => {
|
||||
it('should not send cookies');
|
||||
it('should not store cookies');
|
||||
});
|
||||
|
||||
it('should be able to abort an HTTP request before first write', async () => {
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
|
@ -916,7 +999,7 @@ describe('net module', () => {
|
|||
it('should to able to create and intercept a request using a custom session object', async () => {
|
||||
const requestUrl = '/requestUrl';
|
||||
const redirectUrl = '/redirectUrl';
|
||||
const customPartitionName = 'custom-partition';
|
||||
const customPartitionName = `custom-partition-${Math.random()}`;
|
||||
let requestIsRedirected = false;
|
||||
const serverUrl = await respondOnce.toURL(redirectUrl, (request, response) => {
|
||||
requestIsRedirected = true;
|
||||
|
@ -957,7 +1040,7 @@ describe('net module', () => {
|
|||
it('should to able to create and intercept a request using a custom partition name', async () => {
|
||||
const requestUrl = '/requestUrl';
|
||||
const redirectUrl = '/redirectUrl';
|
||||
const customPartitionName = 'custom-partition';
|
||||
const customPartitionName = `custom-partition-${Math.random()}`;
|
||||
let requestIsRedirected = false;
|
||||
const serverUrl = await respondOnce.toURL(redirectUrl, (request, response) => {
|
||||
requestIsRedirected = true;
|
||||
|
|
1
typings/internal-ambient.d.ts
vendored
1
typings/internal-ambient.d.ts
vendored
|
@ -94,6 +94,7 @@ declare namespace NodeJS {
|
|||
url: string;
|
||||
extraHeaders?: Record<string, string>;
|
||||
useSessionCookies?: boolean;
|
||||
credentials?: 'include' | 'omit';
|
||||
body: Uint8Array | BodyFunc;
|
||||
session?: Electron.Session;
|
||||
partition?: string;
|
||||
|
|
Loading…
Reference in a new issue