Bug 1270096 - Netmonitor request replay: disable CORS checks, set request headers accurately r?Honza draft
authorJarda Snajdr <jsnajdr@gmail.com>
Mon, 23 May 2016 15:27:45 +0200
changeset 412580 89a2fe60b9556dc79fd8f28d8f10c7b36d66c77b
parent 412489 cfdb7af3af2e92e95f71ca2f1672bf5433beeb89
child 531018 fd4db0c43499e67520395285335c22a7e048507b
push id29205
push userbmo:jsnajdr@gmail.com
push dateMon, 12 Sep 2016 09:36:34 +0000
reviewersHonza
bugs1270096
milestone51.0a1
Bug 1270096 - Netmonitor request replay: disable CORS checks, set request headers accurately r?Honza MozReview-Commit-ID: Ew7zPkrhOHd
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_resend.js
devtools/client/netmonitor/test/browser_net_resend_cors.js
devtools/client/netmonitor/test/browser_net_resend_headers.js
devtools/server/actors/webconsole.js
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -109,16 +109,18 @@ skip-if = (os == 'linux' && e10s && debu
 [browser_net_post-data-02.js]
 [browser_net_post-data-03.js]
 [browser_net_prefs-and-l10n.js]
 [browser_net_prefs-reload.js]
 [browser_net_raw_headers.js]
 [browser_net_reload-button.js]
 [browser_net_reload-markers.js]
 [browser_net_req-resp-bodies.js]
+[browser_net_resend_cors.js]
+[browser_net_resend_headers.js]
 [browser_net_resend.js]
 [browser_net_security-details.js]
 [browser_net_security-error.js]
 [browser_net_security-icon-click.js]
 [browser_net_security-redirect.js]
 [browser_net_security-state.js]
 [browser_net_security-tab-deselect.js]
 [browser_net_security-tab-visibility.js]
--- a/devtools/client/netmonitor/test/browser_net_resend.js
+++ b/devtools/client/netmonitor/test/browser_net_resend.js
@@ -4,16 +4,17 @@
 "use strict";
 
 /**
  * Tests if resending a request works.
  */
 
 const ADD_QUERY = "t1=t2";
 const ADD_HEADER = "Test-header: true";
+const ADD_UA_HEADER = "User-Agent: Custom-Agent";
 const ADD_POSTDATA = "&t3=t4";
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
   info("Starting test... ");
 
   let { panelWin } = monitor;
   let { document, EVENTS, NetMonitorView } = panelWin;
@@ -124,16 +125,21 @@ add_task(function* () {
     let headersFocus = once(headers, "focus", false);
     headers.focus();
     yield headersFocus;
 
     // add a header
     type(["VK_RETURN"]);
     type(ADD_HEADER);
 
+    // add a User-Agent header, to check if default headers can be modified
+    // (there will be two of them, first gets overwritten by the second)
+    type(["VK_RETURN"]);
+    type(ADD_UA_HEADER);
+
     let postData = document.getElementById("custom-postdata-value");
     let postFocus = once(postData, "focus", false);
     postData.focus();
     yield postFocus;
 
     // add to POST data
     type(ADD_POSTDATA);
   }
@@ -144,16 +150,19 @@ add_task(function* () {
   function testSentRequest(data, origData) {
     is(data.method, origData.method, "correct method in sent request");
     is(data.url, origData.url + "&" + ADD_QUERY, "correct url in sent request");
 
     let { headers } = data.requestHeaders;
     let hasHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_HEADER);
     ok(hasHeader, "new header added to sent request");
 
+    let hasUAHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_UA_HEADER);
+    ok(hasUAHeader, "User-Agent header added to sent request");
+
     is(data.requestPostData.postData.text,
        origData.requestPostData.postData.text + ADD_POSTDATA,
        "post data added to sent request");
   }
 
   function type(string) {
     for (let ch of string) {
       EventUtils.synthesizeKey(ch, {}, panelWin);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_resend_cors.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if resending a CORS request avoids the security checks and doesn't send
+ * a preflight OPTIONS request (bug 1270096 and friends)
+ */
+
+add_task(function* () {
+  let { tab, monitor } = yield initNetMonitor(CORS_URL);
+  info("Starting test... ");
+
+  let { EVENTS, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+
+  RequestsMenu.lazyUpdate = false;
+
+  let requestUrl = "http://test1.example.com" + CORS_SJS_PATH;
+
+  info("Waiting for OPTIONS, then POST");
+  let wait = waitForNetworkEvents(monitor, 1, 1);
+  yield ContentTask.spawn(tab.linkedBrowser, requestUrl, function* (url) {
+    content.wrappedJSObject.performRequests(url, "triggering/preflight", "post-data");
+  });
+  yield wait;
+
+  const METHODS = ["OPTIONS", "POST"];
+
+  // Check the requests that were sent
+  for (let [i, method] of METHODS.entries()) {
+    let { attachment } = RequestsMenu.getItemAtIndex(i);
+    is(attachment.method, method, `The ${method} request has the right method`);
+    is(attachment.url, requestUrl, `The ${method} request has the right URL`);
+  }
+
+  // Resend both requests without modification. Wait for resent OPTIONS, then POST.
+  // POST is supposed to have no preflight OPTIONS request this time (CORS is disabled)
+  let onRequests = waitForNetworkEvents(monitor, 1, 1);
+  for (let [i, method] of METHODS.entries()) {
+    let item = RequestsMenu.getItemAtIndex(i);
+
+    info(`Selecting the ${method} request (at index ${i})`);
+    let onUpdate = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+    RequestsMenu.selectedItem = item;
+    yield onUpdate;
+
+    info("Cloning the selected request into a custom clone");
+    let onPopulate = monitor.panelWin.once(EVENTS.CUSTOMREQUESTVIEW_POPULATED);
+    RequestsMenu.cloneSelectedRequest();
+    yield onPopulate;
+
+    info("Sending the cloned request (without change)");
+    RequestsMenu.sendCustomRequest();
+  }
+
+  info("Waiting for both resent requests");
+  yield onRequests;
+
+  // Check the resent requests
+  for (let [i, method] of METHODS.entries()) {
+    let index = i + 2;
+    let item = RequestsMenu.getItemAtIndex(index).attachment;
+    is(item.method, method, `The ${method} request has the right method`);
+    is(item.url, requestUrl, `The ${method} request has the right URL`);
+    is(item.status, 200, `The ${method} response has the right status`);
+
+    if (method === "POST") {
+      is(item.requestPostData.postData.text, "post-data",
+        "The POST request has the right POST data");
+      // eslint-disable-next-line mozilla/no-cpows-in-tests
+      is(item.responseContent.content.text, "Access-Control-Allow-Origin: *",
+        "The POST response has the right content");
+    }
+  }
+
+  info("Finishing the test");
+  return teardown(monitor);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_resend_headers.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if custom request headers are not ignored (bug 1270096 and friends)
+ */
+
+add_task(function* () {
+  let { monitor } = yield initNetMonitor(SIMPLE_SJS);
+  info("Starting test... ");
+
+  let { NetMonitorView, NetMonitorController } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+
+  RequestsMenu.lazyUpdate = false;
+
+  let requestUrl = SIMPLE_SJS;
+  let requestHeaders = [
+    { name: "Host", value: "fakehost.example.com" },
+    { name: "User-Agent", value: "Testzilla" },
+    { name: "Referer", value: "http://example.com/referrer" },
+    { name: "Accept", value: "application/jarda"},
+    { name: "Accept-Encoding", value: "compress, identity, funcoding" },
+    { name: "Accept-Language", value: "cs-CZ" }
+  ];
+
+  let wait = waitForNetworkEvents(monitor, 0, 1);
+  NetMonitorController.webConsoleClient.sendHTTPRequest({
+    url: requestUrl,
+    method: "POST",
+    headers: requestHeaders,
+    body: "Hello"
+  });
+  yield wait;
+
+  let { attachment } = RequestsMenu.getItemAtIndex(0);
+  is(attachment.method, "POST", "The request has the right method");
+  is(attachment.url, requestUrl, "The request has the right URL");
+
+  for (let { name, value } of attachment.requestHeaders.headers) {
+    info(`Request header: ${name}: ${value}`);
+  }
+
+  function hasRequestHeader(name, value) {
+    let { headers } = attachment.requestHeaders;
+    return headers.some(h => h.name === name && h.value === value);
+  }
+
+  function hasNotRequestHeader(name) {
+    let { headers } = attachment.requestHeaders;
+    return headers.every(h => h.name !== name);
+  }
+
+  for (let { name, value } of requestHeaders) {
+    ok(hasRequestHeader(name, value), `The ${name} header has the right value`);
+  }
+
+  // Check that the Cookie header was not added silently (i.e., that the request is
+  // anonymous.
+  for (let name of ["Cookie"]) {
+    ok(hasNotRequestHeader(name), `The ${name} header is not present`);
+  }
+
+  return teardown(monitor);
+});
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -18,16 +18,17 @@ const ErrorDocs = require("devtools/serv
 loader.lazyRequireGetter(this, "NetworkMonitor", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "NetworkMonitorChild", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "ConsoleProgressListener", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "StackTraceCollector", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "events", "sdk/event/core");
 loader.lazyRequireGetter(this, "ServerLoggingListener", "devtools/shared/webconsole/server-logger", true);
 loader.lazyRequireGetter(this, "JSPropertyProvider", "devtools/shared/webconsole/js-property-provider", true);
 loader.lazyRequireGetter(this, "Parser", "resource://devtools/shared/Parser.jsm", true);
+loader.lazyRequireGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm", true);
 
 for (let name of ["WebConsoleUtils", "ConsoleServiceListener",
     "ConsoleAPIListener", "addWebConsoleCommands",
     "ConsoleReflowListener", "CONSOLE_WORKER_IDS"]) {
   Object.defineProperty(this, name, {
     get: function (prop) {
       if (prop == "WebConsoleUtils") {
         prop = "Utils";
@@ -1558,33 +1559,56 @@ WebConsoleActor.prototype =
     actor = new NetworkEventActor(this);
     this._actorPool.addActor(actor);
     return actor;
   },
 
   /**
    * Send a new HTTP request from the target's window.
    *
-   * @param object aMessage
+   * @param object message
    *        Object with 'request' - the HTTP request details.
    */
-  onSendHTTPRequest: function WCA_onSendHTTPRequest(aMessage)
-  {
-    let details = aMessage.request;
+  onSendHTTPRequest(message) {
+    let { url, method, headers, body } = message.request;
+
+    // Set the loadingNode and loadGroup to the target document - otherwise the
+    // request won't show up in the opened netmonitor.
+    let doc = this.window.document;
+
+    let channel = NetUtil.newChannel({
+      uri: NetUtil.newURI(url),
+      loadingNode: doc,
+      securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+      contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER
+    });
+
+    channel.QueryInterface(Ci.nsIHttpChannel);
 
-    // send request from target's window
-    let request = new this.window.XMLHttpRequest();
-    request.open(details.method, details.url, true);
+    channel.loadGroup = doc.documentLoadGroup;
+    channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE |
+                         Ci.nsIRequest.INHIBIT_CACHING |
+                         Ci.nsIRequest.LOAD_ANONYMOUS;
+
+    channel.requestMethod = method;
+
+    for (let {name, value} of headers) {
+      channel.setRequestHeader(name, value, false);
+    }
 
-    for (let {name, value} of details.headers) {
-      request.setRequestHeader(name, value);
+    if (body) {
+      channel.QueryInterface(Ci.nsIUploadChannel2);
+      let bodyStream = Cc["@mozilla.org/io/string-input-stream;1"]
+        .createInstance(Ci.nsIStringInputStream);
+      bodyStream.setData(body, body.length);
+      channel.explicitSetUploadStream(bodyStream, null, -1, method, false);
     }
-    request.send(details.body);
 
-    let channel = request.channel.QueryInterface(Ci.nsIHttpChannel);
+    NetUtil.asyncFetch(channel, () => {});
+
     let actor = this.getNetworkEventActor(channel.channelId);
 
     // map channel to actor so we can associate future events with it
     this._netEvents.set(channel.channelId, actor);
 
     return {
       from: this.actorID,
       eventActor: actor.grip()