Bug 1157817 - Show blocked requests in the Network Monitor r=Honza,Harald
authorDavid Walsh <dwalsh@mozilla.com>
Tue, 28 May 2019 12:13:08 +0000
changeset 475816 b067a5b108dd7d396453500caf047c41fc678ad2
parent 475815 93d4fcebc9d7a0d2ade280e634f2864ce8004335
child 475817 e615cb65ecb5b32773ba1753d95eed82e7be10be
push id36076
push useropoprus@mozilla.com
push dateTue, 28 May 2019 21:44:47 +0000
treeherdermozilla-central@c3f75e081427 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza, Harald
bugs1157817
milestone69.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 1157817 - Show blocked requests in the Network Monitor r=Honza,Harald Displays blocked requests in the Network monitor request listing, providing a reason for why the request was blocked based on response codes provided b nsILoadInfo.idl Differential Revision: https://phabricator.services.mozilla.com/D31907
devtools/client/locales/en-US/netmonitor.properties
devtools/client/netmonitor/src/components/RequestListColumnTransferredSize.js
devtools/client/netmonitor/src/constants.js
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_block-csp.js
devtools/client/netmonitor/test/browser_net_block.js
devtools/client/netmonitor/test/head.js
devtools/client/netmonitor/test/html_csp-test-page.html
devtools/server/actors/network-monitor/network-observer.js
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -235,21 +235,25 @@ networkMenu.sizeUnavailable.title=Transf
 # cached.
 networkMenu.sizeCached=cached
 
 # LOCALIZATION NOTE (networkMenu.sizeServiceWorker): This is the label displayed
 # in the network menu specifying the transferred of a request computed
 # by a service worker.
 networkMenu.sizeServiceWorker=service worker
 
-# LOCALIZATION NOTE (networkMenu.sizeServiceWorker): This is the label displayed
+# LOCALIZATION NOTE (networkMenu.blockedBy): This is the label displayed
 # in the network menu specifying the request was blocked by something.
 # %S is replaced by the blocked reason, which could be "DevTools", "CORS", etc.
 networkMenu.blockedBy=blocked by %S
 
+# LOCALIZATION NOTE (networkMenu.blocked): This is a generic message for a
+# URL that has been blocked for an unknown reason
+networkMenu.blocked=blocked
+
 # LOCALIZATION NOTE (networkMenu.totalMS2): This is the label displayed
 # in the network menu specifying the time for a request to finish (in milliseconds).
 networkMenu.totalMS2=%S ms
 
 # This string is used to concatenate tooltips (netmonitor.waterfall.tooltip.*)
 # in the requests waterfall for total time (in milliseconds). \\u0020 represents
 # a whitespace. You can replace this with a different character, e.g. an hyphen
 # or a period, if a comma doesn't work for your language.
--- a/devtools/client/netmonitor/src/components/RequestListColumnTransferredSize.js
+++ b/devtools/client/netmonitor/src/components/RequestListColumnTransferredSize.js
@@ -5,16 +5,17 @@
 "use strict";
 
 const { Component } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { getFormattedSize } = require("../utils/format-utils");
 const { L10N } = require("../utils/l10n");
 const { propertiesEqual } = require("../utils/request-utils");
+const { BLOCKED_REASON_MESSAGES } = require("../constants");
 
 const SIZE_CACHED = L10N.getStr("networkMenu.sizeCached");
 const SIZE_SERVICE_WORKER = L10N.getStr("networkMenu.sizeServiceWorker");
 const SIZE_UNAVAILABLE = L10N.getStr("networkMenu.sizeUnavailable");
 const SIZE_UNAVAILABLE_TITLE = L10N.getStr("networkMenu.sizeUnavailable.title");
 const UPDATED_TRANSFERRED_PROPS = [
   "transferredSize",
   "fromCache",
@@ -40,17 +41,17 @@ class RequestListColumnTransferredSize e
       fromServiceWorker,
       status,
       transferredSize,
       isRacing,
     } = this.props.item;
     let text;
 
     if (blockedReason) {
-      text = L10N.getFormatStr("networkMenu.blockedBy", blockedReason);
+      text = BLOCKED_REASON_MESSAGES[blockedReason] || L10N.getStr("networkMenu.blocked");
     } else if (fromCache || status === "304") {
       text = SIZE_CACHED;
     } else if (fromServiceWorker) {
       text = SIZE_SERVICE_WORKER;
     } else if (typeof transferredSize == "number") {
       text = getFormattedSize(transferredSize);
       if (isRacing && typeof isRacing == "boolean") {
         text = L10N.getFormatStr("networkMenu.raced", text);
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -395,27 +395,63 @@ const SUPPORTED_HTTP_CODES = [
   "501",
   "502",
   "503",
   "504",
   "505",
   "511",
 ];
 
+// Keys are the codes provided by server, values are localization messages
+// prefixed by "netmonitor.blocked."
+const BLOCKED_REASON_MESSAGES = {
+  devtools: "Blocked by DevTools",
+  1001: "CORS disabled",
+  1002: "CORS Failed",
+  1003: "CORS Not HTTP",
+  1004: "CORS Multiple Origin Not Allowed",
+  1005: "CORS Missing Allow Origin",
+  1006: "CORS No Allow Credentials",
+  1007: "CORS Allow Origin Not Matching Origin",
+  1008: "CORS Missing Allow Credentials",
+  1009: "CORS Origin Header Missing",
+  1010: "CORS External Redirect Not Allowed",
+  1011: "CORS Preflight Did Not Succeed",
+  1012: "CORS Invalid Allow Method",
+  1013: "CORS Method Not Found",
+  1014: "CORS Invalid Allow Header",
+  1015: "CORS Missing Allow Header",
+  2001: "Malware",
+  2002: "Phishing",
+  2003: "Unwanted",
+  2004: "Tracking",
+  2005: "Blocked",
+  2006: "Harmful",
+  3001: "Mixed Block",
+  4000: "CSP",
+  4001: "CSP No Data Protocol",
+  4002: "CSP Web Extension",
+  4003: "CSP ContentBlocked",
+  4004: "CSP Data Document",
+  4005: "CSP Web Browser",
+  4006: "CSP Preload",
+};
+
 const general = {
   ACTIVITY_TYPE,
   EVENTS,
   FILTER_SEARCH_DELAY: 200,
   UPDATE_PROPS,
   HEADERS,
   RESPONSE_HEADERS,
   FILTER_FLAGS,
   FILTER_TAGS,
   REQUESTS_WATERFALL,
   PANELS,
   TIMING_KEYS,
   MIN_COLUMN_WIDTH,
   DEFAULT_COLUMN_WIDTH,
   SUPPORTED_HTTP_CODES,
+  BLOCKED_REASON_MESSAGES,
 };
 
 // flatten constants
 module.exports = Object.assign({}, general, actionTypes);
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -4,16 +4,17 @@ subsuite = devtools
 support-files =
   dropmarker.svg
   head.js
   html_cause-test-page.html
   html_content-type-without-cache-test-page.html
   html_brotli-test-page.html
   html_image-tooltip-test-page.html
   html_cors-test-page.html
+  html_csp-test-page.html
   html_custom-get-page.html
   html_cyrillic-test-page.html
   html_frame-test-page.html
   html_frame-subdocument.html
   html_filter-test-page.html
   html_infinite-get-page.html
   html_json-b64.html
   html_json-basic.html
@@ -72,16 +73,17 @@ support-files =
   !/devtools/client/shared/test/telemetry-test-helpers.js
 
 [browser_net_accessibility-01.js]
 [browser_net_accessibility-02.js]
 [browser_net_api-calls.js]
 [browser_net_background_update.js]
 [browser_net_autoscroll.js]
 [browser_net_block.js]
+[browser_net_block-csp.js]
 [browser_net_cached-status.js]
 skip-if = verify
 [browser_net_cause.js]
 [browser_net_cause_redirect.js]
 [browser_net_cause_source_map.js]
 [browser_net_service-worker-status.js]
 skip-if = (verify && !debug && (os == 'linux'))
 [browser_net_charts-01.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_block-csp.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that CSP violations display in the netmonitor when blocked
+ */
+
+add_task(async function() {
+  const { tab, monitor } = await initNetMonitor(CSP_URL);
+
+  const { document, store, windowRequire } = monitor.panelWin;
+  const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+  const {
+    getDisplayedRequests,
+    getSortedRequests,
+  } = windowRequire("devtools/client/netmonitor/src/selectors/index");
+
+  store.dispatch(Actions.batchEnable(false));
+
+  tab.linkedBrowser.reload();
+
+  await waitForNetworkEvents(monitor, 2);
+
+  info("Waiting until the requests appear in netmonitor");
+
+  // Ensure the attempt to load a JS file shows a blocked CSP error
+  verifyRequestItemTarget(
+    document,
+    getDisplayedRequests(store.getState()),
+    getSortedRequests(store.getState()).get(1),
+    "GET",
+    EXAMPLE_URL + "js_websocket-worker-test.js",
+    {
+      transferred: "CSP Preload",
+      cause: { type: "script" },
+      type: "",
+    }
+  );
+
+  await teardown(monitor);
+});
--- a/devtools/client/netmonitor/test/browser_net_block.js
+++ b/devtools/client/netmonitor/test/browser_net_block.js
@@ -82,18 +82,18 @@ add_task(async function() {
     unblockedRequestSize =
       firstRequest.querySelector(".requests-list-transferred").textContent;
     EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequest);
     unblockedRequestState = getSelectedRequest(store.getState());
     info("Captured unblocked request");
   }
 
   ok(!normalRequestState.blockedReason, "Normal request is not blocked");
-  ok(!normalRequestSize.includes("blocked"), "Normal request has a size");
+  ok(!normalRequestSize.includes("Blocked"), "Normal request has a size");
 
   ok(blockedRequestState.blockedReason, "Blocked request is blocked");
-  ok(blockedRequestSize.includes("blocked"), "Blocked request shows reason as size");
+  ok(blockedRequestSize.includes("Blocked"), "Blocked request shows reason as size");
 
   ok(!unblockedRequestState.blockedReason, "Unblocked request is not blocked");
-  ok(!unblockedRequestSize.includes("blocked"), "Unblocked request has a size");
+  ok(!unblockedRequestSize.includes("Blocked"), "Unblocked request has a size");
 
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -73,16 +73,17 @@ const CUSTOM_GET_URL = EXAMPLE_URL + "ht
 const SINGLE_GET_URL = EXAMPLE_URL + "html_single-get-page.html";
 const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html";
 const CURL_URL = EXAMPLE_URL + "html_copy-as-curl.html";
 const CURL_UTILS_URL = EXAMPLE_URL + "html_curl-utils.html";
 const SEND_BEACON_URL = EXAMPLE_URL + "html_send-beacon.html";
 const CORS_URL = EXAMPLE_URL + "html_cors-test-page.html";
 const PAUSE_URL = EXAMPLE_URL + "html_pause-test-page.html";
 const OPEN_REQUEST_IN_TAB_URL = EXAMPLE_URL + "html_open-request-in-tab.html";
+const CSP_URL = EXAMPLE_URL + "html_csp-test-page.html";
 
 const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
 const SIMPLE_UNSORTED_COOKIES_SJS = EXAMPLE_URL + "sjs_simple-unsorted-cookies-test-server.sjs";
 const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
 const WS_CONTENT_TYPE_SJS = WS_HTTP_URL + "sjs_content-type-test-server.sjs";
 const HTTPS_CONTENT_TYPE_SJS = HTTPS_EXAMPLE_URL + "sjs_content-type-test-server.sjs";
 const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
 const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_csp-test-page.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset=utf-8>
+    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+    <meta http-equiv="Pragma" content="no-cache" />
+    <meta http-equiv="Expires" content="0" />
+    <meta http-equiv="Content-Security-Policy" content="script-src 'none';">
+    <title>Tests breaking CSP with script</title>
+  </head>
+  <body>
+
+    The script in this page will CSP:
+
+    <script src="js_websocket-worker-test.js"></script>
+  </body>
+</html>
\ No newline at end of file
--- a/devtools/server/actors/network-monitor/network-observer.js
+++ b/devtools/server/actors/network-monitor/network-observer.js
@@ -257,42 +257,43 @@ NetworkObserver.prototype = {
   _httpFailedOpening: function(subject, topic) {
     if (!this.owner ||
         (topic != "http-on-failed-opening-request") ||
         !(subject instanceof Ci.nsIHttpChannel)) {
       return;
     }
 
     const channel = subject.QueryInterface(Ci.nsIHttpChannel);
-
     if (!matchRequest(channel, this.filters)) {
       return;
     }
 
-    "add your handling code here";
+    const blockedCode = channel.loadInfo.requestBlockingReason;
+    this._httpResponseExaminer(subject, topic, blockedCode);
   },
 
   /**
    * Observe notifications for the http-on-examine-response topic, coming from
    * the nsIObserverService.
    *
    * @private
    * @param nsIHttpChannel subject
    * @param string topic
    * @returns void
    */
-  _httpResponseExaminer: function(subject, topic) {
+  _httpResponseExaminer: function(subject, topic, blockedReason) {
     // The httpResponseExaminer is used to retrieve the uncached response
     // headers. The data retrieved is stored in openResponses. The
     // NetworkResponseListener is responsible with updating the httpActivity
     // object with the data from the new object in openResponses.
 
     if (!this.owner ||
         (topic != "http-on-examine-response" &&
-         topic != "http-on-examine-cached-response") ||
+         topic != "http-on-examine-cached-response" &&
+         topic != "http-on-failed-opening-request") ||
         !(subject instanceof Ci.nsIHttpChannel)) {
       return;
     }
 
     const channel = subject.QueryInterface(Ci.nsIHttpChannel);
 
     if (!matchRequest(channel, this.filters)) {
       return;
@@ -302,51 +303,55 @@ NetworkObserver.prototype = {
       id: gSequenceId(),
       channel: channel,
       headers: [],
       cookies: [],
     };
 
     const setCookieHeaders = [];
 
-    channel.visitOriginalResponseHeaders({
-      visitHeader: function(name, value) {
-        const lowerName = name.toLowerCase();
-        if (lowerName == "set-cookie") {
-          setCookieHeaders.push(value);
-        }
-        response.headers.push({ name: name, value: value });
-      },
-    });
+    if (!blockedReason) {
+      channel.visitOriginalResponseHeaders({
+        visitHeader: function(name, value) {
+          const lowerName = name.toLowerCase();
+          if (lowerName == "set-cookie") {
+            setCookieHeaders.push(value);
+          }
+          response.headers.push({ name: name, value: value });
+        },
+      });
 
-    if (!response.headers.length) {
-      // No need to continue.
-      return;
-    }
+      if (!response.headers.length) {
+        // No need to continue.
+        return;
+      }
 
-    if (setCookieHeaders.length) {
-      response.cookies = setCookieHeaders.reduce((result, header) => {
-        const cookies = NetworkHelper.parseSetCookieHeader(header);
-        return result.concat(cookies);
-      }, []);
+      if (setCookieHeaders.length) {
+        response.cookies = setCookieHeaders.reduce((result, header) => {
+          const cookies = NetworkHelper.parseSetCookieHeader(header);
+          return result.concat(cookies);
+        }, []);
+      }
     }
 
     // Determine the HTTP version.
     const httpVersionMaj = {};
     const httpVersionMin = {};
 
     channel.QueryInterface(Ci.nsIHttpChannelInternal);
-    channel.getResponseVersion(httpVersionMaj, httpVersionMin);
+    if (!blockedReason) {
+      channel.getResponseVersion(httpVersionMaj, httpVersionMin);
 
-    response.status = channel.responseStatus;
-    response.statusText = channel.responseStatusText;
-    response.httpVersion = "HTTP/" + httpVersionMaj.value + "." +
+      response.status = channel.responseStatus;
+      response.statusText = channel.responseStatusText;
+      response.httpVersion = "HTTP/" + httpVersionMaj.value + "." +
                                      httpVersionMin.value;
 
-    this.openResponses.set(channel, response);
+      this.openResponses.set(channel, response);
+    }
 
     if (topic === "http-on-examine-cached-response") {
       // Service worker requests emits cached-response notification on non-e10s,
       // and we fake one on e10s.
       const fromServiceWorker = this.interceptedChannels.has(channel);
       this.interceptedChannels.delete(channel);
 
       // If this is a cached response, there never was a request event
@@ -365,16 +370,18 @@ NetworkObserver.prototype = {
         headersSize: 0,
       }, "", true);
 
       // There also is never any timing events, so we can fire this
       // event with zeroed out values.
       const timings = this._setupHarTimings(httpActivity, true);
       httpActivity.owner.addEventTimings(timings.total, timings.timings,
                                          timings.offsets);
+    } else if (topic === "http-on-failed-opening-request") {
+      this._createNetworkEvent(channel, {blockedReason});
     }
   },
 
   /**
    * Observe notifications for the http-on-modify-request topic, coming from
    * the nsIObserverService.
    *
    * @private
@@ -501,17 +508,18 @@ NetworkObserver.prototype = {
                              extraStringData);
     }
   }),
 
   /**
    *
    */
   _createNetworkEvent: function(channel, { timestamp, extraStringData,
-                                           fromCache, fromServiceWorker }) {
+                                           fromCache, fromServiceWorker,
+                                           blockedReason }) {
     const httpActivity = this.createOrGetActivityObject(channel);
 
     channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
     httpActivity.private = channel.isChannelPrivate;
 
     if (timestamp) {
       httpActivity.timings.REQUEST_HEADER = {
         first: timestamp,
@@ -591,19 +599,23 @@ NetworkObserver.prototype = {
     });
 
     if (cookieHeader) {
       cookies = NetworkHelper.parseCookieHeader(cookieHeader);
     }
 
     // Check the request URL with ones manually blocked by the user in DevTools.
     // If it's meant to be blocked, we cancel the request and annotate the event.
-    if (this.blockedURLs.has(httpActivity.url)) {
-      channel.cancel(Cr.NS_BINDING_ABORTED);
-      event.blockedReason = "DevTools";
+    if (!blockedReason) {
+      if (this.blockedURLs.has(httpActivity.url)) {
+        channel.cancel(Cr.NS_BINDING_ABORTED);
+        event.blockedReason = "devtools";
+      }
+    } else {
+      event.blockedReason = blockedReason;
     }
 
     httpActivity.owner = this.owner.onNetworkEvent(event);
 
     if (!event.blockedReason) {
       this._setupResponseListener(httpActivity, fromCache);
     }