Bug 1391992 add cookiestoreid to proxy and webrequest details r=robwu
authorShane Caraveo <scaraveo@mozilla.com>
Mon, 29 Apr 2019 18:36:17 +0000
changeset 530616 e64b96d4af3d9d98b34541217f61ff138a1c7880
parent 530615 7ba49ce135e667a8e97f00889a434965f331bfb8
child 530617 53ffb5f187119724ba5942a4ef7470dc7a961221
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrobwu
bugs1391992
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1391992 add cookiestoreid to proxy and webrequest details r=robwu Differential Revision: https://phabricator.services.mozilla.com/D28929
toolkit/components/extensions/ExtensionXPCShellUtils.jsm
toolkit/components/extensions/ProxyScriptContext.jsm
toolkit/components/extensions/parent/.eslintrc.js
toolkit/components/extensions/parent/ext-toolkit.js
toolkit/components/extensions/parent/ext-webRequest.js
toolkit/components/extensions/schemas/proxy.json
toolkit/components/extensions/schemas/web_request.json
toolkit/components/extensions/test/xpcshell/test_ext_webRequest_userContextId.js
toolkit/components/extensions/test/xpcshell/test_proxy_userContextId.js
toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
--- a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -111,20 +111,21 @@ function promiseBrowserLoaded(browser, u
     // use one. But we also need to make sure it stays alive until we're
     // done with it, so thunk away a strong reference to keep it alive.
     kungFuDeathGrip.add(listener);
     browser.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
   });
 }
 
 class ContentPage {
-  constructor(remote = REMOTE_CONTENT_SCRIPTS, extension = null, privateBrowsing = false) {
+  constructor(remote = REMOTE_CONTENT_SCRIPTS, extension = null, privateBrowsing = false, userContextId = undefined) {
     this.remote = remote;
     this.extension = extension;
     this.privateBrowsing = privateBrowsing;
+    this.userContextId = userContextId;
 
     this.browserReady = this._initBrowser();
   }
 
   async _initBrowser() {
     this.windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
 
     if (this.privateBrowsing) {
@@ -148,16 +149,19 @@ class ContentPage {
     await promiseObserved("chrome-document-global-created",
                           win => win.document == chromeShell.document);
 
     let chromeDoc = await promiseDocumentLoaded(chromeShell.document);
 
     let browser = chromeDoc.createElement("browser");
     browser.setAttribute("type", "content");
     browser.setAttribute("disableglobalhistory", "true");
+    if (this.userContextId) {
+      browser.setAttribute("usercontextid", this.userContextId);
+    }
 
     if (this.extension && this.extension.remote) {
       this.remote = true;
       browser.setAttribute("remote", "true");
       browser.setAttribute("remoteType", "extension");
       browser.sameProcessAsFrameLoader = this.extension.groupFrameLoader;
     }
 
@@ -855,18 +859,18 @@ var ExtensionTestUtils = {
    *        If true, load the URL in a content process. If false, load
    *        it in the parent process.
    * @param {string} [options.redirectUrl]
    *        An optional URL that the initial page is expected to
    *        redirect to.
    *
    * @returns {ContentPage}
    */
-  loadContentPage(url, {extension = undefined, remote = undefined, redirectUrl = undefined, privateBrowsing = false} = {}) {
+  loadContentPage(url, {extension = undefined, remote = undefined, redirectUrl = undefined, privateBrowsing = false, userContextId = undefined} = {}) {
     ContentTask.setTestScope(this.currentScope);
 
-    let contentPage = new ContentPage(remote, extension && extension.extension, privateBrowsing);
+    let contentPage = new ContentPage(remote, extension && extension.extension, privateBrowsing, userContextId);
 
     return contentPage.loadURL(url, redirectUrl).then(() => {
       return contentPage;
     });
   },
 };
--- a/toolkit/components/extensions/ProxyScriptContext.jsm
+++ b/toolkit/components/extensions/ProxyScriptContext.jsm
@@ -22,16 +22,19 @@ ChromeUtils.defineModuleGetter(this, "Sc
                                "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "ProxyService",
                                    "@mozilla.org/network/protocol-proxy-service;1",
                                    "nsIProtocolProxyService");
 
 XPCOMUtils.defineLazyGetter(this, "tabTracker", () => {
   return ExtensionParent.apiManager.global.tabTracker;
 });
+XPCOMUtils.defineLazyGetter(this, "getCookieStoreIdForOriginAttributes", () => {
+  return ExtensionParent.apiManager.global.getCookieStoreIdForOriginAttributes;
+});
 
 const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
 
 // DNS is resolved on the SOCKS proxy server.
 const {TRANSPARENT_PROXY_RESOLVES_HOST} = Ci.nsIProxyInfo;
 
 // The length of time (seconds) to wait for a proxy to resolve before ignoring it.
 const PROXY_TIMEOUT_SEC = 10;
@@ -251,17 +254,19 @@ class ProxyChannelFilter {
     this.extraInfoSpec = extraInfoSpec || [];
 
     ProxyService.registerChannelFilter(
       this /* nsIProtocolProxyChannelFilter aFilter */,
       0 /* unsigned long aPosition */
     );
   }
 
-  // Copy from WebRequest.jsm with small changes.
+  // Originally duplicated from WebRequest.jsm with small changes.  Keep this
+  // in sync with WebRequest.jsm as well as parent/ext-webRequest.js when
+  // apropiate.
   getRequestData(channel, extraData) {
     let originAttributes = channel.loadInfo && channel.loadInfo.originAttributes;
     let data = {
       requestId: String(channel.id),
       url: channel.finalURL,
       method: channel.method,
       type: channel.type,
       fromCache: !!channel.fromCache,
@@ -274,16 +279,19 @@ class ProxyChannelFilter {
       parentFrameId: channel.parentWindowId,
 
       frameAncestors: channel.frameAncestors || undefined,
 
       timeStamp: Date.now(),
 
       ...extraData,
     };
+    if (originAttributes && this.extension.hasPermission("cookies")) {
+      data.cookieStoreId = getCookieStoreIdForOriginAttributes(originAttributes);
+    }
     if (this.extraInfoSpec.includes("requestHeaders")) {
       data.requestHeaders = channel.getRequestHeaders();
     }
     return data;
   }
 
   /**
    * This method (which is required by the nsIProtocolProxyService interface)
--- a/toolkit/components/extensions/parent/.eslintrc.js
+++ b/toolkit/components/extensions/parent/.eslintrc.js
@@ -12,15 +12,16 @@ module.exports = {
     "TabManagerBase": true,
     "TabTrackerBase": true,
     "WindowBase": true,
     "WindowManagerBase": true,
     "WindowTrackerBase": true,
     "getUserContextIdForCookieStoreId": true,
     "getContainerForCookieStoreId": true,
     "getCookieStoreIdForContainer": true,
+    "getCookieStoreIdForOriginAttributes": true,
     "getCookieStoreIdForTab": true,
     "isContainerCookieStoreId": true,
     "isDefaultCookieStoreId": true,
     "isPrivateCookieStoreId": true,
     "isValidCookieStoreId": true,
   },
 };
--- a/toolkit/components/extensions/parent/ext-toolkit.js
+++ b/toolkit/components/extensions/parent/ext-toolkit.js
@@ -1,17 +1,18 @@
 "use strict";
 
 // These are defined on "global" which is used for the same scopes as the other
 // ext-*.js files.
 /* exported getCookieStoreIdForTab, getCookieStoreIdForContainer,
             getContainerForCookieStoreId,
             isValidCookieStoreId, isContainerCookieStoreId,
             EventManager, URL */
-/* global getCookieStoreIdForTab:false, getCookieStoreIdForContainer:false,
+/* global getCookieStoreIdForTab:false, getCookieStoreIdForOriginAttributes:false,
+          getCookieStoreIdForContainer:false,
           getContainerForCookieStoreId: false,
           isValidCookieStoreId:false, isContainerCookieStoreId:false,
           isDefaultCookieStoreId: false, isPrivateCookieStoreId:false,
           EventManager: false */
 
 ChromeUtils.defineModuleGetter(this, "ContextualIdentityService",
                                "resource://gre/modules/ContextualIdentityService.jsm");
 
@@ -35,16 +36,28 @@ global.getCookieStoreIdForTab = function
 
   if (tab.userContextId) {
     return getCookieStoreIdForContainer(tab.userContextId);
   }
 
   return DEFAULT_STORE;
 };
 
+global.getCookieStoreIdForOriginAttributes = function(originAttributes) {
+  if (originAttributes.privateBrowsingId) {
+    return PRIVATE_STORE;
+  }
+
+  if (originAttributes.userContextId) {
+    return getCookieStoreIdForContainer(originAttributes.userContextId);
+  }
+
+  return DEFAULT_STORE;
+};
+
 global.isPrivateCookieStoreId = function(storeId) {
   return storeId == PRIVATE_STORE;
 };
 
 global.isDefaultCookieStoreId = function(storeId) {
   return storeId == DEFAULT_STORE;
 };
 
--- a/toolkit/components/extensions/parent/ext-webRequest.js
+++ b/toolkit/components/extensions/parent/ext-webRequest.js
@@ -21,16 +21,19 @@ function registerEvent(extension, eventN
     if (filter.windowId != null && browserData.windowId != filter.windowId) {
       return;
     }
 
     let event = data.serialize(eventName);
     event.tabId = browserData.tabId;
     if (data.originAttributes) {
       event.incognito = data.originAttributes.privateBrowsingId > 0;
+      if (extension.hasPermission("cookies")) {
+        event.cookieStoreId = getCookieStoreIdForOriginAttributes(data.originAttributes);
+      }
     }
     if (data.registerTraceableChannel) {
       // If this is a primed listener, no tabParent was passed in here,
       // but the convert() callback later in this function will be called
       // when the background page is started.  Force that to happen here
       // after which we'll have a valid tabParent.
       if (fire.wakeup) {
         await fire.wakeup();
--- a/toolkit/components/extensions/schemas/proxy.json
+++ b/toolkit/components/extensions/schemas/proxy.json
@@ -143,16 +143,17 @@
             "name": "details",
             "properties": {
               "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
               "url": {"type": "string"},
               "method": {"type": "string", "description": "Standard HTTP method."},
               "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "incognito": {"type": "boolean", "optional": true, "description": "True for private browsing requests."},
+              "cookieStoreId": {"type": "string", "optional": true, "description": "The cookie store ID of the contextual identity."},
               "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
               "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "webRequest.ResourceType", "description": "How the requested resource will be used."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
               "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
               "requestHeaders": {"$ref": "webRequest.HttpHeaders", "optional": true, "description": "The HTTP request headers that are going to be sent out with this request."}
--- a/toolkit/components/extensions/schemas/web_request.json
+++ b/toolkit/components/extensions/schemas/web_request.json
@@ -421,16 +421,17 @@
             "name": "details",
             "properties": {
               "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
               "url": {"type": "string"},
               "method": {"type": "string", "description": "Standard HTTP method."},
               "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "incognito": {"type": "boolean", "optional": true, "description": "True for private browsing requests."},
+              "cookieStoreId": {"type": "string", "optional": true, "description": "The cookie store ID of the contextual identity."},
               "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
               "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
               "requestBody": {
                 "type": "object",
                 "optional": true,
                 "description": "Contains the HTTP request body data. Only provided if extraInfoSpec contains 'requestBody'.",
                 "properties": {
                   "error": {"type": "string", "optional": true, "description": "Errors when obtaining request body data."},
@@ -490,16 +491,17 @@
             "name": "details",
             "properties": {
               "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
               "url": {"type": "string"},
               "method": {"type": "string", "description": "Standard HTTP method."},
               "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "incognito": {"type": "boolean", "optional": true, "description": "True for private browsing requests."},
+              "cookieStoreId": {"type": "string", "optional": true, "description": "The cookie store ID of the contextual identity."},
               "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
               "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "requestHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP request headers that are going to be sent out with this request."}
             }
           }
@@ -536,16 +538,17 @@
             "name": "details",
             "properties": {
               "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
               "url": {"type": "string"},
               "method": {"type": "string", "description": "Standard HTTP method."},
               "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "incognito": {"type": "boolean", "optional": true, "description": "True for private browsing requests."},
+              "cookieStoreId": {"type": "string", "optional": true, "description": "The cookie store ID of the contextual identity."},
               "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
               "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "requestHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP request headers that have been sent out with this request."}
             }
           }
@@ -577,16 +580,17 @@
             "name": "details",
             "properties": {
               "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
               "url": {"type": "string"},
               "method": {"type": "string", "description": "Standard HTTP method."},
               "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "incognito": {"type": "boolean", "optional": true, "description": "True for private browsing requests."},
+              "cookieStoreId": {"type": "string", "optional": true, "description": "The cookie store ID of the contextual identity."},
               "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
               "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line)."},
               "responseHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP response headers that have been received with this response."},
               "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."}
@@ -625,16 +629,17 @@
             "name": "details",
             "properties": {
               "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
               "url": {"type": "string"},
               "method": {"type": "string", "description": "Standard HTTP method."},
               "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "incognito": {"type": "boolean", "optional": true, "description": "True for private browsing requests."},
+              "cookieStoreId": {"type": "string", "optional": true, "description": "The cookie store ID of the contextual identity."},
               "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
               "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "scheme": {"type": "string", "description": "The authentication scheme, e.g. Basic or Digest."},
               "realm": {"type": "string", "description": "The authentication realm provided by the server, if there is one.", "optional": true},
               "challenger": {"type": "object", "description": "The server requesting authentication.", "properties": {"host": {"type": "string"}, "port": {"type": "integer"}}},
@@ -685,16 +690,17 @@
             "name": "details",
             "properties": {
               "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
               "url": {"type": "string"},
               "method": {"type": "string", "description": "Standard HTTP method."},
               "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "incognito": {"type": "boolean", "optional": true, "description": "True for private browsing requests."},
+              "cookieStoreId": {"type": "string", "optional": true, "description": "The cookie store ID of the contextual identity."},
               "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
               "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
               "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
               "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."},
@@ -730,16 +736,17 @@
             "name": "details",
             "properties": {
               "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
               "url": {"type": "string"},
               "method": {"type": "string", "description": "Standard HTTP method."},
               "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "incognito": {"type": "boolean", "optional": true, "description": "True for private browsing requests."},
+              "cookieStoreId": {"type": "string", "optional": true, "description": "The cookie store ID of the contextual identity."},
               "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
               "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
               "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
               "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."},
@@ -776,16 +783,17 @@
             "name": "details",
             "properties": {
               "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
               "url": {"type": "string"},
               "method": {"type": "string", "description": "Standard HTTP method."},
               "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "incognito": {"type": "boolean", "optional": true, "description": "True for private browsing requests."},
+              "cookieStoreId": {"type": "string", "optional": true, "description": "The cookie store ID of the contextual identity."},
               "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
               "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
               "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
               "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."},
@@ -821,16 +829,17 @@
             "name": "details",
             "properties": {
               "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
               "url": {"type": "string"},
               "method": {"type": "string", "description": "Standard HTTP method."},
               "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "incognito": {"type": "boolean", "optional": true, "description": "True for private browsing requests."},
+              "cookieStoreId": {"type": "string", "optional": true, "description": "The cookie store ID of the contextual identity."},
               "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
               "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
               "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
               "error": {"type": "string", "description": "The error description. This string is <em>not</em> guaranteed to remain backwards compatible between releases. You must not parse and act based upon its content."}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_userContextId.js
@@ -0,0 +1,51 @@
+"use strict";
+
+const server = createHttpServer({hosts: ["example.com"]});
+
+server.registerPathHandler("/dummy", (request, response) => {
+  response.setStatusLine(request.httpVersion, 200, "OK");
+  response.setHeader("Content-Type", "text/html", false);
+  response.write("<!DOCTYPE html><html></html>");
+});
+
+add_task(async function test_userContextId_webrequest() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["webRequest", "webRequestBlocking", "<all_urls>", "cookies"],
+    },
+    background() {
+      browser.webRequest.onBeforeRequest.addListener(async (details) => {
+        browser.test.assertEq(details.cookieStoreId, "firefox-container-2", "cookieStoreId is set");
+        browser.test.notifyPass("webRequest");
+      }, {urls: ["<all_urls>"]}, ["blocking"]);
+    },
+  });
+  await extension.startup();
+
+  let contentPage = await ExtensionTestUtils.loadContentPage("http://example.com/dummy", {userContextId: 2});
+  await extension.awaitFinish("webRequest");
+
+  await extension.unload();
+  await contentPage.close();
+});
+
+add_task(async function test_userContextId_webrequest_nopermission() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["webRequest", "webRequestBlocking", "<all_urls>"],
+    },
+    background() {
+      browser.webRequest.onBeforeRequest.addListener(async (details) => {
+        browser.test.assertEq(details.cookieStoreId, undefined, "cookieStoreId not set, requires cookies permission");
+        browser.test.notifyPass("webRequest");
+      }, {urls: ["<all_urls>"]}, ["blocking"]);
+    },
+  });
+  await extension.startup();
+
+  let contentPage = await ExtensionTestUtils.loadContentPage("http://example.com/dummy", {userContextId: 2});
+  await extension.awaitFinish("webRequest");
+
+  await extension.unload();
+  await contentPage.close();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_proxy_userContextId.js
@@ -0,0 +1,52 @@
+"use strict";
+
+const server = createHttpServer({hosts: ["example.com"]});
+
+server.registerPathHandler("/dummy", (request, response) => {
+  response.setStatusLine(request.httpVersion, 200, "OK");
+  response.setHeader("Content-Type", "text/html", false);
+  response.write("<!DOCTYPE html><html></html>");
+});
+
+
+add_task(async function test_userContextId_proxy_onRequest() {
+  // This extension will succeed if it gets a request
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["proxy", "<all_urls>", "cookies"],
+    },
+    background() {
+      browser.proxy.onRequest.addListener(async (details) => {
+        browser.test.assertEq(details.cookieStoreId, "firefox-container-2", "cookieStoreId is set");
+        browser.test.notifyPass("proxy.onRequest");
+      }, {urls: ["<all_urls>"]});
+    },
+  });
+  await extension.startup();
+
+  let contentPage = await ExtensionTestUtils.loadContentPage("http://example.com/dummy", {userContextId: 2});
+  await extension.awaitFinish("proxy.onRequest");
+  await extension.unload();
+  await contentPage.close();
+});
+
+add_task(async function test_userContextId_proxy_onRequest_nopermission() {
+  // This extension will succeed if it gets a request
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["proxy", "<all_urls>"],
+    },
+    background() {
+      browser.proxy.onRequest.addListener(async (details) => {
+        browser.test.assertEq(details.cookieStoreId, undefined, "cookieStoreId not set, requires cookies permission");
+        browser.test.notifyPass("proxy.onRequest");
+      }, {urls: ["<all_urls>"]});
+    },
+  });
+  await extension.startup();
+
+  let contentPage = await ExtensionTestUtils.loadContentPage("http://example.com/dummy", {userContextId: 2});
+  await extension.awaitFinish("proxy.onRequest");
+  await extension.unload();
+  await contentPage.close();
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -153,26 +153,28 @@ skip-if = os == "android" && debug
 skip-if = os == "android" && debug
 [test_ext_webRequest_responseBody.js]
 skip-if = os == "android" && debug
 [test_ext_webRequest_set_cookie.js]
 skip-if = appname == "thunderbird" || (os == "android" && debug)
 [test_ext_webRequest_startup.js]
 skip-if = os == "android" && debug
 [test_ext_webRequest_suspend.js]
+[test_ext_webRequest_userContextId.js]
 [test_ext_webRequest_webSocket.js]
 skip-if = appname == "thunderbird"
 [test_ext_xhr_capabilities.js]
 [test_native_manifests.js]
 subprocess = true
 skip-if = os == "android"
 [test_ext_permissions.js]
 skip-if = appname == "thunderbird" || os == "android" # Bug 1350559
 [test_ext_permissions_uninstall.js]
 skip-if = appname == "thunderbird" || os == "android" # Bug 1350559
 [test_proxy_listener.js]
 [test_proxy_incognito.js]
 skip-if = os == "android" # incognito not supported on android
 [test_proxy_scripts.js]
 [test_proxy_scripts_results.js]
+[test_proxy_userContextId.js]
 [test_ext_brokenlinks.js]
 [test_ext_performance_counters.js]
 skip-if = appname == "thunderbird" || os == "android"