Bug 1151368 - Block requests by URL in DevTools. r=ochameau
authorJ. Ryan Stinnett <jryans@gmail.com>
Fri, 19 Apr 2019 18:25:19 +0000
changeset 470233 eaf2a199d4eaa2b991bb319bd6df919d793ccf8f
parent 470232 60b2b50f00a7f7167ce645815cb0cd566c778e5b
child 470234 6b031fd49d1e292c4033e6f86f909a689a78b176
push id35891
push userrgurzau@mozilla.com
push dateSat, 20 Apr 2019 09:35:22 +0000
treeherdermozilla-central@6e082b675763 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1151368
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 1151368 - Block requests by URL in DevTools. r=ochameau This is a very basic form of request blocking for the Network Monitor. It only supports blocking a request via right-click. This change adds the minimal UI and server support to block the request. There is no UI to indicate what happened to the request yet, so it will just look like a "confused" request that never started. Future patches will improve from here. Differential Revision: https://phabricator.services.mozilla.com/D26579
devtools/client/locales/en-US/netmonitor.properties
devtools/client/netmonitor/src/actions/requests.js
devtools/client/netmonitor/src/components/RequestListContent.js
devtools/client/netmonitor/src/connector/chrome-connector.js
devtools/client/netmonitor/src/connector/firefox-connector.js
devtools/client/netmonitor/src/connector/index.js
devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
devtools/server/actors/network-monitor.js
devtools/server/actors/network-monitor/network-observer.js
devtools/server/actors/webconsole.js
devtools/shared/specs/webconsole.js
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -1066,16 +1066,20 @@ netmonitor.context.resend.accesskey=n
 # on the context menu that opens a form to edit and resend the currently
 # displayed request
 netmonitor.context.editAndResend=Edit and Resend
 
 # LOCALIZATION NOTE (netmonitor.context.editAndResend.accesskey): This is the access key
 # for the "Edit and Resend" menu item displayed in the context menu for a request
 netmonitor.context.editAndResend.accesskey=E
 
+# LOCALIZATION NOTE (netmonitor.context.blockURL): This is the label displayed
+# on the context menu that blocks any requests matching the selected request's URL.
+netmonitor.context.blockURL=Block URL
+
 # LOCALIZATION NOTE (netmonitor.context.newTab):  This is the label
 # for the Open in New Tab menu item displayed in the context menu of the
 # network container
 netmonitor.context.newTab=Open in New Tab
 
 # LOCALIZATION NOTE (netmonitor.context.newTab.accesskey): This is the access key
 # for the Open in New Tab menu item displayed in the context menu of the
 # network container
--- a/devtools/client/netmonitor/src/actions/requests.js
+++ b/devtools/client/netmonitor/src/actions/requests.js
@@ -73,16 +73,32 @@ function sendCustomRequest(connector) {
         type: SEND_CUSTOM_REQUEST,
         id: response.eventActor.actor,
       });
     });
   };
 }
 
 /**
+ * Tell the backend to block future requests that match the URL of the selected one.
+ */
+function blockSelectedRequestURL(connector) {
+  return (dispatch, getState) => {
+    const selected = getSelectedRequest(getState());
+
+    if (!selected) {
+      return;
+    }
+
+    const { url } = selected;
+    connector.blockRequest({ url });
+  };
+}
+
+/**
  * Remove a request from the list. Supports removing only cloned requests with a
  * "isCustom" attribute. Other requests never need to be removed.
  */
 function removeSelectedCustomRequest() {
   return {
     type: REMOVE_SELECTED_CUSTOM_REQUEST,
   };
 }
@@ -99,15 +115,16 @@ function clearRequests() {
 function toggleRecording() {
   return {
     type: TOGGLE_RECORDING,
   };
 }
 
 module.exports = {
   addRequest,
+  blockSelectedRequestURL,
   clearRequests,
   cloneSelectedRequest,
   removeSelectedCustomRequest,
   sendCustomRequest,
   toggleRecording,
   updateRequest,
 };
--- a/devtools/client/netmonitor/src/components/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/RequestListContent.js
@@ -45,16 +45,17 @@ const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 4
 const MAX_SCROLL_HEIGHT = 2147483647;
 
 /**
  * Renders the actual contents of the request list.
  */
 class RequestListContent extends Component {
   static get propTypes() {
     return {
+      blockSelectedRequestURL: PropTypes.func.isRequired,
       connector: PropTypes.object.isRequired,
       columns: PropTypes.object.isRequired,
       networkDetailsOpen: PropTypes.bool.isRequired,
       networkDetailsWidth: PropTypes.number,
       networkDetailsHeight: PropTypes.number,
       cloneSelectedRequest: PropTypes.func.isRequired,
       sendCustomRequest: PropTypes.func.isRequired,
       displayedRequests: PropTypes.array.isRequired,
@@ -260,22 +261,24 @@ class RequestListContent extends Compone
   }
 
   onContextMenu(evt) {
     evt.preventDefault();
     const { selectedRequest, displayedRequests } = this.props;
 
     if (!this.contextMenu) {
       const {
+        blockSelectedRequestURL,
         connector,
         cloneSelectedRequest,
         sendCustomRequest,
         openStatistics,
       } = this.props;
       this.contextMenu = new RequestListContextMenu({
+        blockSelectedRequestURL,
         connector,
         cloneSelectedRequest,
         sendCustomRequest,
         openStatistics,
         openRequestInTab: this.openRequestInTab,
       });
     }
 
@@ -354,16 +357,19 @@ module.exports = connect(
     networkDetailsHeight: state.ui.networkDetailsHeight,
     displayedRequests: getDisplayedRequests(state),
     firstRequestStartedMillis: state.requests.firstStartedMillis,
     selectedRequest: getSelectedRequest(state),
     scale: getWaterfallScale(state),
     requestFilterTypes: state.filters.requestFilterTypes,
   }),
   (dispatch, props) => ({
+    blockSelectedRequestURL: () => {
+      dispatch(Actions.blockSelectedRequestURL(props.connector));
+    },
     cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
     sendCustomRequest: () => dispatch(Actions.sendCustomRequest(props.connector)),
     openStatistics: (open) => dispatch(Actions.openStatistics(props.connector, open)),
     /**
      * A handler that opens the stack trace tab when a stack trace is available
      */
     onCauseBadgeMouseDown: (cause) => {
       if (cause.stacktrace && cause.stacktrace.length > 0) {
--- a/devtools/client/netmonitor/src/connector/chrome-connector.js
+++ b/devtools/client/netmonitor/src/connector/chrome-connector.js
@@ -84,16 +84,25 @@ class ChromeConnector {
    *
    * @param {object} data data payload would like to sent to backend
    * @param {function} callback callback will be invoked after the request finished
    */
   sendHTTPRequest(data, callback) {
     // TODO : not support. currently didn't provide this feature in CDP API.
   }
 
+  /**
+   * Block future requests matching a filter.
+   *
+   * @param {object} filter request filter specifying what to block
+   */
+  blockRequest(filter) {
+    // TODO: Implement for Chrome as well.
+  }
+
   setPreferences() {
     // TODO : implement.
   }
 
   viewSourceInDebugger() {
     // TODO : implement.
   }
 }
--- a/devtools/client/netmonitor/src/connector/firefox-connector.js
+++ b/devtools/client/netmonitor/src/connector/firefox-connector.js
@@ -229,16 +229,25 @@ class FirefoxConnector {
    * @param {object} data data payload would like to sent to backend
    * @param {function} callback callback will be invoked after the request finished
    */
   sendHTTPRequest(data, callback) {
     this.webConsoleClient.sendHTTPRequest(data).then(callback);
   }
 
   /**
+   * Block future requests matching a filter.
+   *
+   * @param {object} filter request filter specifying what to block
+   */
+  blockRequest(filter) {
+    return this.webConsoleClient.blockRequest(filter);
+  }
+
+  /**
    * Set network preferences to control network flow
    *
    * @param {object} request request payload would like to sent to backend
    * @param {function} callback callback will be invoked after the request finished
    */
   setPreferences(request) {
     return this.webConsoleClient.setPreferences(request);
   }
--- a/devtools/client/netmonitor/src/connector/index.js
+++ b/devtools/client/netmonitor/src/connector/index.js
@@ -91,16 +91,20 @@ class Connector {
   getTabTarget() {
     return this.connector.getTabTarget();
   }
 
   sendHTTPRequest() {
     return this.connector.sendHTTPRequest(...arguments);
   }
 
+  blockRequest() {
+    return this.connector.blockRequest(...arguments);
+  }
+
   setPreferences() {
     return this.connector.setPreferences(...arguments);
   }
 
   triggerActivity() {
     return this.connector.triggerActivity(...arguments);
   }
 
--- a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
@@ -39,16 +39,17 @@ class RequestListContextMenu {
       requestPostDataAvailable,
       responseHeaders,
       responseHeadersAvailable,
       responseContent,
       responseContentAvailable,
       url,
     } = selectedRequest;
     const {
+      blockSelectedRequestURL,
       connector,
       cloneSelectedRequest,
       sendCustomRequest,
       openStatistics,
       openRequestInTab,
     } = this.props;
     const menu = [];
     const copySubmenu = [];
@@ -197,16 +198,23 @@ class RequestListContextMenu {
       id: "request-list-context-resend",
       label: L10N.getStr("netmonitor.context.editAndResend"),
       accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"),
       visible: !!(selectedRequest && !isCustom),
       click: cloneSelectedRequest,
     });
 
     menu.push({
+      id: "request-list-context-block-url",
+      label: L10N.getStr("netmonitor.context.blockURL"),
+      visible: !!selectedRequest,
+      click: blockSelectedRequestURL,
+    });
+
+    menu.push({
       type: "separator",
       visible: copySubmenu.slice(15, 16).some((subMenu) => subMenu.visible),
     });
 
     menu.push({
       id: "request-list-context-newtab",
       label: L10N.getStr("netmonitor.context.newTab"),
       accesskey: L10N.getStr("netmonitor.context.newTab.accesskey"),
--- a/devtools/server/actors/network-monitor.js
+++ b/devtools/server/actors/network-monitor.js
@@ -47,16 +47,17 @@ const NetworkMonitorActor = ActorClassWi
     this.observer = new NetworkObserver(filters, this);
     this.observer.init();
 
     this.stackTraces = new Set();
 
     this.onStackTraceAvailable = this.onStackTraceAvailable.bind(this);
     this.onRequestContent = this.onRequestContent.bind(this);
     this.onSetPreference = this.onSetPreference.bind(this);
+    this.onBlockRequest = this.onBlockRequest.bind(this);
     this.onGetNetworkEventActor = this.onGetNetworkEventActor.bind(this);
     this.onDestroyMessage = this.onDestroyMessage.bind(this);
 
     this.startListening();
   },
 
   onDestroyMessage({ data }) {
     if (data.actorID == this.parentID) {
@@ -66,29 +67,33 @@ const NetworkMonitorActor = ActorClassWi
 
   startListening() {
     this.messageManager.addMessageListener("debug:request-stack-available",
       this.onStackTraceAvailable);
     this.messageManager.addMessageListener("debug:request-content:request",
       this.onRequestContent);
     this.messageManager.addMessageListener("debug:netmonitor-preference",
       this.onSetPreference);
+    this.messageManager.addMessageListener("debug:block-request",
+      this.onBlockRequest);
     this.messageManager.addMessageListener("debug:get-network-event-actor:request",
       this.onGetNetworkEventActor);
     this.messageManager.addMessageListener("debug:destroy-network-monitor",
       this.onDestroyMessage);
   },
 
   stopListening() {
     this.messageManager.removeMessageListener("debug:request-stack-available",
       this.onStackTraceAvailable);
     this.messageManager.removeMessageListener("debug:request-content:request",
       this.onRequestContent);
     this.messageManager.removeMessageListener("debug:netmonitor-preference",
       this.onSetPreference);
+    this.messageManager.removeMessageListener("debug:block-request",
+      this.onBlockRequest);
     this.messageManager.removeMessageListener("debug:get-network-event-actor:request",
       this.onGetNetworkEventActor);
     this.messageManager.removeMessageListener("debug:destroy-network-monitor",
       this.onDestroyMessage);
   },
 
   destroy() {
     Actor.prototype.destroy.call(this);
@@ -167,16 +172,21 @@ const NetworkMonitorActor = ActorClassWi
     if ("saveRequestAndResponseBodies" in data) {
       this.observer.saveRequestAndResponseBodies = data.saveRequestAndResponseBodies;
     }
     if ("throttleData" in data) {
       this.observer.throttleData = data.throttleData;
     }
   },
 
+  onBlockRequest({ data }) {
+    const { filter } = data;
+    this.observer.blockRequest(filter);
+  },
+
   onGetNetworkEventActor({ data }) {
     const actor = this.getNetworkEventActor(data.channelId);
     this.messageManager.sendAsyncMessage("debug:get-network-event-actor:response", {
       channelId: data.channelId,
       actor: actor.form(),
     });
   },
 
--- a/devtools/server/actors/network-monitor/network-observer.js
+++ b/devtools/server/actors/network-monitor/network-observer.js
@@ -1,17 +1,17 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft= javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const {Cc, Ci} = require("chrome");
+const {Cc, Ci, Cr} = require("chrome");
 const Services = require("Services");
 const flags = require("devtools/shared/flags");
 
 loader.lazyRequireGetter(this, "NetworkHelper",
   "devtools/shared/webconsole/network-helper");
 loader.lazyRequireGetter(this, "DevToolsUtils",
   "devtools/shared/DevToolsUtils");
 loader.lazyRequireGetter(this, "NetworkThrottleManager",
@@ -121,16 +121,18 @@ exports.matchRequest = matchRequest;
  */
 function NetworkObserver(filters, owner) {
   this.filters = filters;
   this.owner = owner;
 
   this.openRequests = new Map();
   this.openResponses = new Map();
 
+  this.blockedURLs = new Set();
+
   this._httpResponseExaminer =
     DevToolsUtils.makeInfallible(this._httpResponseExaminer).bind(this);
   this._httpModifyExaminer =
     DevToolsUtils.makeInfallible(this._httpModifyExaminer).bind(this);
   this._serviceWorkerRequest = this._serviceWorkerRequest.bind(this);
 
   this._throttleData = null;
   this._throttler = null;
@@ -640,27 +642,47 @@ NetworkObserver.prototype = {
 
       this.openRequests.set(channel, httpActivity);
     }
 
     return httpActivity;
   },
 
   /**
+   * Block a request based on certain filtering options.
+   *
+   * Currently, an exact URL match is the only supported filter type.
+   */
+  blockRequest(filter) {
+    if (!filter || !filter.url) {
+      // In the future, there may be other types of filters, such as domain.
+      // For now, ignore anything other than URL.
+      return;
+    }
+
+    this.blockedURLs.add(filter.url);
+  },
+
+  /**
    * Setup the network response listener for the given HTTP activity. The
    * NetworkResponseListener is responsible for storing the response body.
    *
    * @private
    * @param object httpActivity
    *        The HTTP activity object we are tracking.
    */
   _setupResponseListener: function(httpActivity, fromCache) {
     const channel = httpActivity.channel;
     channel.QueryInterface(Ci.nsITraceableChannel);
 
+    if (this.blockedURLs.has(httpActivity.url)) {
+      channel.cancel(Cr.NS_BINDING_ABORTED);
+      return;
+    }
+
     if (!fromCache) {
       const throttler = this._getThrottler();
       if (throttler) {
         httpActivity.downloadThrottle = throttler.manage(channel);
       }
     }
 
     // The response will be written into the outputStream of this pipe.
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -1677,16 +1677,38 @@ WebConsoleActor.prototype =
       messageManager.addMessageListener("debug:get-network-event-actor:response",
         onMessage);
       messageManager.sendAsyncMessage("debug:get-network-event-actor:request",
         { channelId });
     });
   },
 
   /**
+   * Block a request based on certain filtering options.
+   *
+   * Currently, an exact URL match is the only supported filter type.
+   * In the future, there may be other types of filters, such as domain.
+   * For now, ignore anything other than URL.
+   *
+   * @param object filter
+   *   An object containing a `url` key with a URL to block.
+   */
+  async blockRequest({ filter }) {
+    if (this.netmonitors) {
+      for (const { messageManager } of this.netmonitors) {
+        messageManager.sendAsyncMessage("debug:block-request", {
+          filter,
+        });
+      }
+    }
+
+    return {};
+  },
+
+  /**
    * Handler for file activity. This method sends the file request information
    * to the remote Web Console client.
    *
    * @see ConsoleProgressListener
    * @param string fileURI
    *        The requested file URI.
    */
   onFileActivity: function(fileURI) {
@@ -1827,11 +1849,12 @@ WebConsoleActor.prototype.requestTypes =
   getCachedMessages: WebConsoleActor.prototype.getCachedMessages,
   evaluateJS: WebConsoleActor.prototype.evaluateJS,
   evaluateJSAsync: WebConsoleActor.prototype.evaluateJSAsync,
   autocomplete: WebConsoleActor.prototype.autocomplete,
   clearMessagesCache: WebConsoleActor.prototype.clearMessagesCache,
   getPreferences: WebConsoleActor.prototype.getPreferences,
   setPreferences: WebConsoleActor.prototype.setPreferences,
   sendHTTPRequest: WebConsoleActor.prototype.sendHTTPRequest,
+  blockRequest: WebConsoleActor.prototype.blockRequest,
 };
 
 exports.WebConsoleActor = WebConsoleActor;
--- a/devtools/shared/specs/webconsole.js
+++ b/devtools/shared/specs/webconsole.js
@@ -229,15 +229,21 @@ const webconsoleSpecPrototype = {
      *        The details of the HTTP request.
      */
     sendHTTPRequest: {
       request: {
         request: Arg(0, "json"),
       },
       response: RetVal("json"),
     },
+
+    blockRequest: {
+      request: {
+        filter: Arg(0, "json"),
+      },
+    },
   },
 };
 
 const webconsoleSpec = generateActorSpec(webconsoleSpecPrototype);
 
 exports.webconsoleSpecPrototype = webconsoleSpecPrototype;
 exports.webconsoleSpec = webconsoleSpec;