From 0e7d59dd7914317f66b2d6a4d3c6580a2e71b5e7 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Tue, 29 Sep 2020 09:03:33 -0700 Subject: [PATCH] feat: [net] add "credentials" option to net.request (#25284) * feat: [net] add "credentials" option to net.request * remove debugging log * add tests --- docs/api/client-request.md | 42 +- lib/browser/api/net.ts | 4 +- shell/browser/api/electron_api_url_loader.cc | 28 +- spec-main/api-net-spec.ts | 579 +++++++++++-------- typings/internal-ambient.d.ts | 1 + 5 files changed, 388 insertions(+), 266 deletions(-) diff --git a/docs/api/client-request.md b/docs/api/client-request.md index 0c78d052638c..0bbfc89d64fa 100644 --- a/docs/api/client-request.md +++ b/docs/api/client-request.md @@ -13,30 +13,40 @@ interface and is therefore an [EventEmitter][event-emitter]. the request URL. If it is an object, it is expected to fully specify an HTTP request via the following properties: * `method` String (optional) - The HTTP request method. Defaults to the GET -method. + method. * `url` String (optional) - The request URL. Must be provided in the absolute -form with the protocol scheme specified as http or https. + form with the protocol scheme specified as http or https. * `session` Session (optional) - The [`Session`](session.md) instance with -which the request is associated. + 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 -specified, `partition` is ignored. + with which the request is associated. Defaults to the empty string. The + `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'. + 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 diff --git a/lib/browser/api/net.ts b/lib/browser/api/net.ts index 1ddc3d31522d..7dfa2ee73648 100644 --- a/lib/browser/api/net.ts +++ b/lib/browser/api/net.ts @@ -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)) { diff --git a/shell/browser/api/electron_api_url_loader.cc b/shell/browser/api/electron_api_url_loader.cc index 09b1d371caf3..df344e204824 100644 --- a/shell/browser/api/electron_api_url_loader.cc +++ b/shell/browser/api/electron_api_url_loader.cc @@ -47,6 +47,27 @@ struct Converter { } }; +template <> +struct Converter { + static bool FromV8(v8::Isolate* isolate, + v8::Local 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::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 extra_headers; if (opts.Get("extraHeaders", &extra_headers)) { for (const auto& it : extra_headers) { @@ -370,7 +393,10 @@ gin::Handle 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; } diff --git a/spec-main/api-net-spec.ts b/spec-main/api-net-spec.ts index ce2453538f46..e7598aa0ad84 100644 --- a/spec-main/api-net-spec.ts +++ b/spec-main/api-net-spec.ts @@ -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,117 +215,191 @@ describe('net module', () => { expect(chunkIndex).to.be.equal(chunkCount); }); - it('should emit the login event when 401', async () => { - const [user, pass] = ['user', 'pass']; - 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'); - }); - let loginAuthInfo: Electron.AuthInfo; - const request = net.request({ method: 'GET', url: serverUrl }); - request.on('login', (authInfo, cb) => { - loginAuthInfo = authInfo; - cb(user, pass); - }); - const response = await getResponse(request); - expect(response.statusCode).to.equal(200); - expect(loginAuthInfo!.realm).to.equal('Foo'); - expect(loginAuthInfo!.scheme).to.equal('basic'); - }); + 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) => { + if (!request.headers.authorization) { + return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end(); + } + response.writeHead(200).end('ok'); + }); + let loginAuthInfo: Electron.AuthInfo; + const request = net.request({ method: 'GET', url: serverUrl, ...extraOptions }); + request.on('login', (authInfo, cb) => { + loginAuthInfo = authInfo; + cb(user, pass); + }); + const response = await getResponse(request); + expect(response.statusCode).to.equal(200); + expect(loginAuthInfo!.realm).to.equal('Foo'); + expect(loginAuthInfo!.scheme).to.equal('basic'); + }); - it('should response when cancelling authentication', async () => { - const serverUrl = await respondOnce.toSingleURL((request, response) => { - if (!request.headers.authorization) { - response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }); - response.end('unauthenticated'); - } else { + 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"' }); + response.end('unauthenticated'); + } else { + response.writeHead(200).end('ok'); + } + }); + 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'); + }); + + 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'); + }); + + it('should upload body when 401', async () => { + const [user, pass] = ['user', 'pass']; + const serverUrl = await respondOnce.toSingleURL((request, response) => { + if (!request.headers.authorization) { + return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end(); + } + response.writeHead(200); + request.on('data', (chunk) => response.write(chunk)); + request.on('end', () => response.end()); + }); + const requestData = randomString(kOneKiloByte); + const request = net.request({ method: 'GET', url: serverUrl, ...extraOptions }); + request.on('login', (authInfo, cb) => { + cb(user, pass); + }); + request.write(requestData); + const response = await getResponse(request); + const responseData = await collectStreamBody(response); + 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"'); }); - const request = net.request({ method: 'GET', url: serverUrl }); - request.on('login', (authInfo, cb) => { - cb(); - }); - const response = await getResponse(request); - const body = await collectStreamBody(response); - expect(body).to.equal('unauthenticated'); - }); - 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); + 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"'); }); - await bw.loadURL(serverUrl); - bw.close(); - const request = net.request({ method: 'GET', url: serverUrl }); - 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); + 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'); }); - 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 }); - 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'); - }); - - it('should upload body when 401', async () => { - const [user, pass] = ['user', 'pass']; - const serverUrl = await respondOnce.toSingleURL((request, response) => { - if (!request.headers.authorization) { - return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end(); - } - response.writeHead(200); - request.on('data', (chunk) => response.write(chunk)); - request.on('end', () => response.end()); - }); - const requestData = randomString(kOneKiloByte); - const request = net.request({ method: 'GET', url: serverUrl }); - request.on('login', (authInfo, cb) => { - cb(user, pass); - }); - request.write(requestData); - const response = await getResponse(request); - const responseData = await collectStreamBody(response); - expect(responseData).to.equal(requestData); }); }); @@ -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,151 +600,160 @@ describe('net module', () => { expect(response.headers['x-cookie']).to.equal('undefined'); }); - 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-2'); - const cookieVal = `${Date.now()}`; - await sess.cookies.set({ - url: serverUrl, - name: 'wild_cookie', - value: cookieVal - }); - const urlRequest = net.request({ - url: serverUrl, - session: sess, - useSessionCookies: true - }); - const response = await getResponse(urlRequest); - expect(response.headers['x-cookie']).to.equal(`wild_cookie=${cookieVal}`); - }); + 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-3'); - let cookies = await sess.cookies.get({}); - expect(cookies).to.have.lengthOf(0); - const urlRequest = net.request({ - url: serverUrl, - session: sess, - useSessionCookies: true - }); - 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' - }); - }); + 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' + }); + }); - ['Lax', 'Strict'].forEach((mode) => { - 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-same-site-${mode}`); - let cookies = await sess.cookies.get({}); - expect(cookies).to.have.lengthOf(0); - const urlRequest = net.request({ - url: serverUrl, - session: sess, - useSessionCookies: true + ['Lax', 'Strict'].forEach((mode) => { + 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'); + }); }); - 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, - useSessionCookies: true - }); - 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(); + 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); + await Promise.all([ + sess.cookies.set({ + url: serverUrl, + name: 'wild_cookie', + value: cookie127Val + }), sess.cookies.set({ + url: localhostUrl, + name: 'wild_cookie', + 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}`); }); - 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-4'); - 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); - await Promise.all([ - sess.cookies.set({ - url: serverUrl, - name: 'wild_cookie', - value: cookie127Val - }), sess.cookies.set({ - url: localhostUrl, - name: 'wild_cookie', - value: cookieLocalVal - }) - ]); - const urlRequest = net.request({ - url: serverUrl, - session: sess, - useSessionCookies: true - }); - 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}`); + } + + 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 () => { @@ -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; diff --git a/typings/internal-ambient.d.ts b/typings/internal-ambient.d.ts index 3c16474c2459..15dc286cfa4d 100644 --- a/typings/internal-ambient.d.ts +++ b/typings/internal-ambient.d.ts @@ -94,6 +94,7 @@ declare namespace NodeJS { url: string; extraHeaders?: Record; useSessionCookies?: boolean; + credentials?: 'include' | 'omit'; body: Uint8Array | BodyFunc; session?: Electron.Session; partition?: string;