Browse Source

feat: [net] add "credentials" option to net.request (#25284)

* feat: [net] add "credentials" option to net.request

* remove debugging log

* add tests
Jeremy Rose 4 years ago
parent
commit
0e7d59dd79

+ 26 - 16
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

+ 3 - 1
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)) {

+ 27 - 1
shell/browser/api/electron_api_url_loader.cc

@@ -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;
   }

+ 332 - 249
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);
+    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 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);
+        });
       });
-      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 {
+    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 });
-      request.on('login', (authInfo, cb) => {
-        cb();
+        });
+        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 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);
-      });
-      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);
-      });
-      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 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 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);
+      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');
+      });
     });
   });
 
@@ -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}`);
-    });
-
-    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'
-      });
-    });
-
-    ['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
+    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}`);
         });
-        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()
+
+        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'
+          });
         });
-        const urlRequest2 = 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 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;

+ 1 - 0
typings/internal-ambient.d.ts

@@ -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;