123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- import { expect } from 'chai';
- import * as dns from 'node:dns';
- import { net, session, BrowserWindow, ClientRequestConstructorOptions } from 'electron/main';
- import { collectStreamBody, getResponse, respondNTimes, respondOnce } from './lib/net-helpers';
- // See https://github.com/nodejs/node/issues/40702.
- dns.setDefaultResultOrder('ipv4first');
- describe('net module (session)', () => {
- beforeEach(() => {
- respondNTimes.routeFailure = false;
- });
- afterEach(async function () {
- await session.defaultSession.clearCache();
- if (respondNTimes.routeFailure && this.test) {
- if (!this.test.isFailed()) {
- throw new Error('Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error');
- }
- }
- });
- describe('HTTP basics', () => {
- for (const extraOptions of [{}, { credentials: 'include' }, { useSessionCookies: false, credentials: 'include' }] as ClientRequestConstructorOptions[]) {
- describe(`authentication when ${JSON.stringify(extraOptions)}`, () => {
- it('should 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, ...extraOptions });
- let logInCount = 0;
- request.on('login', () => {
- logInCount++;
- });
- const response = await getResponse(request);
- await collectStreamBody(response);
- expect(logInCount).to.equal(0, 'should not receive a login event, credentials should be cached');
- });
- 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, ...extraOptions });
- let logInCount = 0;
- request.on('login', () => {
- logInCount++;
- });
- const response = await getResponse(request);
- const body = await collectStreamBody(response);
- expect(response.statusCode).to.equal(200);
- expect(body).to.equal('ok');
- expect(logInCount).to.equal(0, 'should not receive a login event, credentials should be cached');
- });
- });
- }
- describe('authentication when {"credentials":"omit"}', () => {
- 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('should be able to set cookie header line', async () => {
- const cookieHeaderName = 'Cookie';
- const cookieHeaderValue = 'test=12345';
- 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;
- response.statusMessage = 'OK';
- response.end();
- });
- await customSession.cookies.set({
- url: `${serverUrl}`,
- name: 'test',
- value: '11111',
- expirationDate: 0
- });
- const urlRequest = net.request({
- method: 'GET',
- url: serverUrl,
- session: customSession
- });
- urlRequest.setHeader(cookieHeaderName, cookieHeaderValue);
- expect(urlRequest.getHeader(cookieHeaderName)).to.equal(cookieHeaderValue);
- const response = await getResponse(urlRequest);
- expect(response.statusCode).to.equal(200);
- await collectStreamBody(response);
- });
- it('should not use the sessions cookie store by default', async () => {
- const serverUrl = await respondOnce.toSingleURL((request, response) => {
- response.statusCode = 200;
- response.statusMessage = 'OK';
- response.setHeader('x-cookie', `${request.headers.cookie!}`);
- response.end();
- });
- const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
- const cookieVal = `${Date.now()}`;
- await sess.cookies.set({
- url: serverUrl,
- name: 'wild_cookie',
- value: cookieVal
- });
- const urlRequest = net.request({
- url: serverUrl,
- session: sess
- });
- const response = await getResponse(urlRequest);
- 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;
- response.statusMessage = 'OK';
- response.setHeader('x-cookie', request.headers.cookie!);
- response.end();
- });
- const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
- const cookieVal = `${Date.now()}`;
- await sess.cookies.set({
- url: serverUrl,
- name: 'wild_cookie',
- value: cookieVal
- });
- const urlRequest = net.request({
- url: serverUrl,
- session: sess,
- ...extraOptions
- });
- const response = await getResponse(urlRequest);
- expect(response.headers['x-cookie']).to.equal(`wild_cookie=${cookieVal}`);
- });
- it('should be able to use the sessions cookie store with set-cookie', async () => {
- const serverUrl = await respondOnce.toSingleURL((request, response) => {
- response.statusCode = 200;
- response.statusMessage = 'OK';
- response.setHeader('set-cookie', 'foo=bar');
- response.end();
- });
- 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,
- ...extraOptions
- });
- await collectStreamBody(await getResponse(urlRequest));
- cookies = await sess.cookies.get({});
- expect(cookies).to.have.lengthOf(1);
- expect(cookies[0]).to.deep.equal({
- name: 'foo',
- value: 'bar',
- domain: '127.0.0.1',
- hostOnly: true,
- path: '/',
- secure: false,
- httpOnly: false,
- session: true,
- sameSite: 'unspecified'
- });
- });
- for (const mode of ['Lax', 'Strict']) {
- it(`should be able to use the sessions cookie store with same-site ${mode} cookies`, async () => {
- const serverUrl = await respondNTimes.toSingleURL((request, response) => {
- response.statusCode = 200;
- response.statusMessage = 'OK';
- response.setHeader('set-cookie', `same=site; SameSite=${mode}`);
- response.setHeader('x-cookie', `${request.headers.cookie}`);
- response.end();
- }, 2);
- 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,
- ...extraOptions
- });
- const response = await getResponse(urlRequest);
- expect(response.headers['x-cookie']).to.equal('undefined');
- await collectStreamBody(response);
- cookies = await sess.cookies.get({});
- expect(cookies).to.have.lengthOf(1);
- expect(cookies[0]).to.deep.equal({
- name: 'same',
- value: 'site',
- domain: '127.0.0.1',
- hostOnly: true,
- path: '/',
- secure: false,
- httpOnly: false,
- session: true,
- sameSite: mode.toLowerCase()
- });
- const urlRequest2 = net.request({
- url: serverUrl,
- session: sess,
- ...extraOptions
- });
- const response2 = await getResponse(urlRequest2);
- expect(response2.headers['x-cookie']).to.equal('same=site');
- });
- }
- it('should be able to use the sessions cookie store safely across redirects', async () => {
- const serverUrl = await respondOnce.toSingleURL(async (request, response) => {
- response.statusCode = 302;
- response.statusMessage = 'Moved';
- const newUrl = await respondOnce.toSingleURL((req, res) => {
- res.statusCode = 200;
- res.statusMessage = 'OK';
- res.setHeader('x-cookie', req.headers.cookie!);
- res.end();
- });
- response.setHeader('x-cookie', request.headers.cookie!);
- response.setHeader('location', newUrl.replace('127.0.0.1', 'localhost'));
- response.end();
- });
- 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');
- expect(localhostUrl).to.not.equal(serverUrl);
- // cookies with lax or strict same-site settings will not
- // persist after redirects. no_restriction must be used
- await Promise.all([
- sess.cookies.set({
- url: serverUrl,
- name: 'wild_cookie',
- sameSite: 'no_restriction',
- value: cookie127Val
- }), sess.cookies.set({
- url: localhostUrl,
- name: 'wild_cookie',
- sameSite: 'no_restriction',
- value: cookieLocalVal
- })
- ]);
- const urlRequest = net.request({
- url: serverUrl,
- session: sess,
- ...extraOptions
- });
- urlRequest.on('redirect', (status, method, url, headers) => {
- // The initial redirect response should have received the 127 value here
- expect(headers['x-cookie'][0]).to.equal(`wild_cookie=${cookie127Val}`);
- urlRequest.followRedirect();
- });
- const response = await getResponse(urlRequest);
- // We expect the server to have received the localhost value here
- // The original request was to a 127.0.0.1 URL
- // That request would have the cookie127Val cookie attached
- // The request is then redirect to a localhost URL (different site)
- // Because we are using the session cookie store it should do the safe / secure thing
- // and attach the cookies for the new target domain
- expect(response.headers['x-cookie']).to.equal(`wild_cookie=${cookieLocalVal}`);
- });
- });
- }
- it('should be able correctly filter out cookies that are secure', async () => {
- const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
- await Promise.all([
- sess.cookies.set({
- url: 'https://electronjs.org',
- domain: 'electronjs.org',
- name: 'cookie1',
- value: '1',
- secure: true
- }),
- sess.cookies.set({
- url: 'https://electronjs.org',
- domain: 'electronjs.org',
- name: 'cookie2',
- value: '2',
- secure: false
- })
- ]);
- const secureCookies = await sess.cookies.get({
- secure: true
- });
- expect(secureCookies).to.have.lengthOf(1);
- expect(secureCookies[0].name).to.equal('cookie1');
- const cookies = await sess.cookies.get({
- secure: false
- });
- expect(cookies).to.have.lengthOf(1);
- expect(cookies[0].name).to.equal('cookie2');
- });
- it('throws when an invalid domain is passed', async () => {
- const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
- await expect(sess.cookies.set({
- url: 'https://electronjs.org',
- domain: 'wssss.iamabaddomain.fun',
- name: 'cookie1'
- })).to.eventually.be.rejectedWith(/Failed to set cookie with an invalid domain attribute/);
- });
- it('should be able correctly filter out cookies that are session', async () => {
- const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
- await Promise.all([
- sess.cookies.set({
- url: 'https://electronjs.org',
- domain: 'electronjs.org',
- name: 'cookie1',
- value: '1'
- }),
- sess.cookies.set({
- url: 'https://electronjs.org',
- domain: 'electronjs.org',
- name: 'cookie2',
- value: '2',
- expirationDate: Math.round(Date.now() / 1000) + 10000
- })
- ]);
- const sessionCookies = await sess.cookies.get({
- session: true
- });
- expect(sessionCookies).to.have.lengthOf(1);
- expect(sessionCookies[0].name).to.equal('cookie1');
- const cookies = await sess.cookies.get({
- session: false
- });
- expect(cookies).to.have.lengthOf(1);
- expect(cookies[0].name).to.equal('cookie2');
- });
- it('should be able correctly filter out cookies that are httpOnly', async () => {
- const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
- await Promise.all([
- sess.cookies.set({
- url: 'https://electronjs.org',
- domain: 'electronjs.org',
- name: 'cookie1',
- value: '1',
- httpOnly: true
- }),
- sess.cookies.set({
- url: 'https://electronjs.org',
- domain: 'electronjs.org',
- name: 'cookie2',
- value: '2',
- httpOnly: false
- })
- ]);
- const httpOnlyCookies = await sess.cookies.get({
- httpOnly: true
- });
- expect(httpOnlyCookies).to.have.lengthOf(1);
- expect(httpOnlyCookies[0].name).to.equal('cookie1');
- const cookies = await sess.cookies.get({
- httpOnly: false
- });
- expect(cookies).to.have.lengthOf(1);
- expect(cookies[0].name).to.equal('cookie2');
- });
- describe('webRequest', () => {
- afterEach(() => {
- session.defaultSession.webRequest.onBeforeRequest(null);
- });
- it('Should throw when invalid filters are passed', () => {
- expect(() => {
- session.defaultSession.webRequest.onBeforeRequest(
- { urls: ['*://www.googleapis.com'] },
- (details, callback) => { callback({ cancel: false }); }
- );
- }).to.throw('Invalid url pattern *://www.googleapis.com: Empty path.');
- expect(() => {
- session.defaultSession.webRequest.onBeforeRequest(
- { urls: ['*://www.googleapis.com/', '*://blahblah.dev'] },
- (details, callback) => { callback({ cancel: false }); }
- );
- }).to.throw('Invalid url pattern *://blahblah.dev: Empty path.');
- });
- it('Should not throw when valid filters are passed', () => {
- expect(() => {
- session.defaultSession.webRequest.onBeforeRequest(
- { urls: ['*://www.googleapis.com/'] },
- (details, callback) => { callback({ cancel: false }); }
- );
- }).to.not.throw();
- });
- it('Requests should be intercepted by webRequest module', async () => {
- const requestUrl = '/requestUrl';
- const redirectUrl = '/redirectUrl';
- let requestIsRedirected = false;
- const serverUrl = await respondOnce.toURL(redirectUrl, (request, response) => {
- requestIsRedirected = true;
- response.end();
- });
- let requestIsIntercepted = false;
- session.defaultSession.webRequest.onBeforeRequest(
- (details, callback) => {
- if (details.url === `${serverUrl}${requestUrl}`) {
- requestIsIntercepted = true;
- callback({
- redirectURL: `${serverUrl}${redirectUrl}`
- });
- } else {
- callback({
- cancel: false
- });
- }
- });
- const urlRequest = net.request(`${serverUrl}${requestUrl}`);
- const response = await getResponse(urlRequest);
- expect(response.statusCode).to.equal(200);
- await collectStreamBody(response);
- expect(requestIsRedirected).to.be.true('The server should receive a request to the forward URL');
- expect(requestIsIntercepted).to.be.true('The request should be intercepted by the webRequest 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-${Math.random()}`;
- let requestIsRedirected = false;
- const serverUrl = await respondOnce.toURL(redirectUrl, (request, response) => {
- requestIsRedirected = true;
- response.end();
- });
- session.defaultSession.webRequest.onBeforeRequest(() => {
- expect.fail('Request should not be intercepted by the default session');
- });
- const customSession = session.fromPartition(customPartitionName, { cache: false });
- let requestIsIntercepted = false;
- customSession.webRequest.onBeforeRequest((details, callback) => {
- if (details.url === `${serverUrl}${requestUrl}`) {
- requestIsIntercepted = true;
- callback({
- redirectURL: `${serverUrl}${redirectUrl}`
- });
- } else {
- callback({
- cancel: false
- });
- }
- });
- const urlRequest = net.request({
- url: `${serverUrl}${requestUrl}`,
- session: customSession
- });
- const response = await getResponse(urlRequest);
- expect(response.statusCode).to.equal(200);
- await collectStreamBody(response);
- expect(requestIsRedirected).to.be.true('The server should receive a request to the forward URL');
- expect(requestIsIntercepted).to.be.true('The request should be intercepted by the webRequest 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-${Math.random()}`;
- let requestIsRedirected = false;
- const serverUrl = await respondOnce.toURL(redirectUrl, (request, response) => {
- requestIsRedirected = true;
- response.end();
- });
- session.defaultSession.webRequest.onBeforeRequest(() => {
- expect.fail('Request should not be intercepted by the default session');
- });
- const customSession = session.fromPartition(customPartitionName, { cache: false });
- let requestIsIntercepted = false;
- customSession.webRequest.onBeforeRequest((details, callback) => {
- if (details.url === `${serverUrl}${requestUrl}`) {
- requestIsIntercepted = true;
- callback({
- redirectURL: `${serverUrl}${redirectUrl}`
- });
- } else {
- callback({
- cancel: false
- });
- }
- });
- const urlRequest = net.request({
- url: `${serverUrl}${requestUrl}`,
- partition: customPartitionName
- });
- const response = await getResponse(urlRequest);
- expect(response.statusCode).to.equal(200);
- await collectStreamBody(response);
- expect(requestIsRedirected).to.be.true('The server should receive a request to the forward URL');
- expect(requestIsIntercepted).to.be.true('The request should be intercepted by the webRequest module');
- });
- it('triggers webRequest handlers when bypassCustomProtocolHandlers', async () => {
- let webRequestDetails: Electron.OnBeforeRequestListenerDetails | null = null;
- const serverUrl = await respondOnce.toSingleURL((req, res) => res.end('hi'));
- session.defaultSession.webRequest.onBeforeRequest((details, cb) => {
- webRequestDetails = details;
- cb({});
- });
- const body = await net.fetch(serverUrl, { bypassCustomProtocolHandlers: true }).then(r => r.text());
- expect(body).to.equal('hi');
- expect(webRequestDetails).to.have.property('url', serverUrl);
- });
- });
- it('should throw if given an invalid session option', () => {
- expect(() => {
- net.request({
- url: 'https://foo',
- session: 1 as any
- });
- }).to.throw('`session` should be an instance of the Session class');
- });
- it('should throw if given an invalid partition option', () => {
- expect(() => {
- net.request({
- url: 'https://foo',
- partition: 1 as any
- });
- }).to.throw('`partition` should be a string');
- });
- it('should throw if given a header value that is empty(null/undefined)', () => {
- const emptyHeaderValues = [null, undefined];
- const errorMsg = '`value` required in setHeader("foo", value)';
- for (const emptyValue of emptyHeaderValues) {
- expect(() => {
- net.request({
- url: 'https://foo',
- headers: { foo: emptyValue as any }
- } as any);
- }).to.throw(errorMsg);
- const request = net.request({ url: 'https://foo' });
- expect(() => {
- request.setHeader('foo', emptyValue as any);
- }).to.throw(errorMsg);
- }
- });
- });
- describe('net.fetch', () => {
- it('should be able to use a session cookie store', async () => {
- const serverUrl = await respondOnce.toSingleURL((request, response) => {
- response.statusCode = 200;
- response.statusMessage = 'OK';
- response.setHeader('x-cookie', request.headers.cookie!);
- response.end();
- });
- const sess = session.fromPartition(`cookie-tests-${Math.random()}`);
- const cookieVal = `${Date.now()}`;
- await sess.cookies.set({
- url: serverUrl,
- name: 'wild_cookie',
- value: cookieVal
- });
- const response = await sess.fetch(serverUrl, {
- credentials: 'include'
- });
- expect(response.headers.get('x-cookie')).to.equal(`wild_cookie=${cookieVal}`);
- });
- });
- });
|