Bug 1552325 - Implement Network.requestWillBeSent. r=remote-protocol-reviewers,ato

Differential Revision: https://phabricator.services.mozilla.com/D36008

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Alexandre Poirot 2019-06-28 08:16:46 +00:00
parent 49180a060f
commit 7c4f974fe1
10 changed files with 274 additions and 65 deletions

View file

@ -15,7 +15,6 @@ XPCOMUtils.defineLazyModuleGetters(ContentProcessDomains, {
Emulation: "chrome://remote/content/domains/content/Emulation.jsm",
Input: "chrome://remote/content/domains/content/Input.jsm",
Log: "chrome://remote/content/domains/content/Log.jsm",
Network: "chrome://remote/content/domains/content/Network.jsm",
Page: "chrome://remote/content/domains/content/Page.jsm",
Performance: "chrome://remote/content/domains/content/Performance.jsm",
Runtime: "chrome://remote/content/domains/content/Runtime.jsm",

View file

@ -13,6 +13,7 @@ const ParentProcessDomains = {};
XPCOMUtils.defineLazyModuleGetters(ParentProcessDomains, {
Browser: "chrome://remote/content/domains/parent/Browser.jsm",
Input: "chrome://remote/content/domains/parent/Input.jsm",
Network: "chrome://remote/content/domains/parent/Network.jsm",
Page: "chrome://remote/content/domains/parent/Page.jsm",
Target: "chrome://remote/content/domains/parent/Target.jsm",
});

View file

@ -1,36 +0,0 @@
/* 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";
var EXPORTED_SYMBOLS = ["Network"];
const {ContentProcessDomain} = ChromeUtils.import("chrome://remote/content/domains/ContentProcessDomain.jsm");
class Network extends ContentProcessDomain {
constructor(session) {
super(session);
this.enabled = false;
}
destructor() {
this.disable();
super.destructor();
}
// commands
async enable() {
if (!this.enabled) {
this.enabled = true;
}
}
disable() {
if (this.enabled) {
this.enabled = false;
}
}
}

View file

@ -0,0 +1,132 @@
/* 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";
var EXPORTED_SYMBOLS = ["Network"];
const {Domain} = ChromeUtils.import("chrome://remote/content/domains/Domain.jsm");
const {NetworkObserver} = ChromeUtils.import("chrome://remote/content/domains/parent/network/NetworkObserver.jsm");
const LOAD_CAUSE_STRINGS = {
[Ci.nsIContentPolicy.TYPE_INVALID]: "Invalid",
[Ci.nsIContentPolicy.TYPE_OTHER]: "Other",
[Ci.nsIContentPolicy.TYPE_SCRIPT]: "Script",
[Ci.nsIContentPolicy.TYPE_IMAGE]: "Img",
[Ci.nsIContentPolicy.TYPE_STYLESHEET]: "Stylesheet",
[Ci.nsIContentPolicy.TYPE_OBJECT]: "Object",
[Ci.nsIContentPolicy.TYPE_DOCUMENT]: "Document",
[Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "Subdocument",
[Ci.nsIContentPolicy.TYPE_REFRESH]: "Refresh",
[Ci.nsIContentPolicy.TYPE_XBL]: "Xbl",
[Ci.nsIContentPolicy.TYPE_PING]: "Ping",
[Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "Xhr",
[Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST]: "ObjectSubdoc",
[Ci.nsIContentPolicy.TYPE_DTD]: "Dtd",
[Ci.nsIContentPolicy.TYPE_FONT]: "Font",
[Ci.nsIContentPolicy.TYPE_MEDIA]: "Media",
[Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "Websocket",
[Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "Csp",
[Ci.nsIContentPolicy.TYPE_XSLT]: "Xslt",
[Ci.nsIContentPolicy.TYPE_BEACON]: "Beacon",
[Ci.nsIContentPolicy.TYPE_FETCH]: "Fetch",
[Ci.nsIContentPolicy.TYPE_IMAGESET]: "Imageset",
[Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "WebManifest",
};
class Network extends Domain {
constructor(session) {
super(session);
this.enabled = false;
this._onRequest = this._onRequest.bind(this);
}
destructor() {
this.disable();
super.destructor();
}
enable() {
if (this.enabled) {
return;
}
this.enabled = true;
this._networkObserver = new NetworkObserver();
const { browser } = this.session.target;
this._networkObserver.startTrackingBrowserNetwork(browser);
this._networkObserver.on("request", this._onRequest);
}
disable() {
if (!this.enabled) {
return;
}
const { browser } = this.session.target;
this._networkObserver.stopTrackingBrowserNetwork(browser);
this._networkObserver.off("request", this._onRequest);
this._networkObserver.dispose();
this.enabled = false;
}
_onRequest(eventName, httpChannel, data) {
const topFrame = getLoadContext(httpChannel).topFrameElement;
const request = {
url: httpChannel.URI.spec,
urlFragment: undefined,
method: httpChannel.requestMethod,
headers: [],
postData: undefined,
hasPostData: false,
mixedContentType: undefined,
initialPriority: undefined,
referrerPolicy: undefined,
isLinkPreload: false,
};
let loaderId = undefined;
let causeType = Ci.nsIContentPolicy.TYPE_OTHER;
let causeUri = topFrame.currentURI.spec;
if (httpChannel.loadInfo) {
causeType = httpChannel.loadInfo.externalContentPolicyType;
const { loadingPrincipal } = httpChannel.loadInfo;
if (loadingPrincipal && loadingPrincipal.URI) {
causeUri = loadingPrincipal.URI.spec;
}
if (causeType == Ci.nsIContentPolicy.TYPE_DOCUMENT) {
// Puppeteer expect this specialy of CDP where loaderId = requestId
// for the toplevel document request
loaderId = String(httpChannel.channelId);
}
}
this.emit("Network.requestWillBeSent", {
requestId: String(httpChannel.channelId),
loaderId,
documentURL: causeUri,
request,
timestamp: undefined,
wallTime: undefined,
initiator: undefined,
redirectResponse: undefined,
type: LOAD_CAUSE_STRINGS[causeType] || "unknown",
frameId: topFrame.outerWindowID,
hasUserGesture: undefined,
});
}
}
function getLoadContext(httpChannel) {
let loadContext = null;
try {
if (httpChannel.notificationCallbacks) {
loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext);
}
} catch (e) {}
try {
if (!loadContext && httpChannel.loadGroup) {
loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
}
} catch (e) { }
return loadContext;
}

View file

@ -5,14 +5,12 @@
"use strict";
const {EventEmitter} = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm");
const {Helper} = ChromeUtils.import("chrome://juggler/content/Helper.js");
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
const {CommonUtils} = ChromeUtils.import("resource://services-common/utils.js");
const Cm = Components.manager;
const CC = Components.Constructor;
const helper = new Helper();
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream", "setInputStream");
const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", "nsIBinaryOutputStream", "setOutputStream");
@ -35,17 +33,6 @@ const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
const SINK_CATEGORY_NAME = "net-channel-event-sinks";
class NetworkObserver {
static instance() {
return NetworkObserver._instance || null;
}
static initialize() {
if (NetworkObserver._instance) {
return;
}
NetworkObserver._instance = new NetworkObserver();
}
constructor() {
EventEmitter.decorate(this);
this._browserSessionCount = new Map();
@ -74,12 +61,13 @@ class NetworkObserver {
this._extraHTTPHeaders = new Map();
this._browserResponseStorages = new Map();
this._eventListeners = [
helper.addObserver(this._onRequest.bind(this), "http-on-modify-request"),
helper.addObserver(this._onResponse.bind(this, false /* fromCache */), "http-on-examine-response"),
helper.addObserver(this._onResponse.bind(this, true /* fromCache */), "http-on-examine-cached-response"),
helper.addObserver(this._onResponse.bind(this, true /* fromCache */), "http-on-examine-merged-response"),
];
this._onRequest = this._onRequest.bind(this);
this._onExamineResponse = this._onResponse.bind(this, false /* fromCache */);
this._onCachedResponse = this._onResponse.bind(this, true /* fromCache */);
Services.obs.addObserver(this._onRequest, "http-on-modify-request");
Services.obs.addObserver(this._onExamineResponse, "http-on-examine-response");
Services.obs.addObserver(this._onCachedResponse, "http-on-examine-cached-response");
Services.obs.addObserver(this._onCachedResponse, "http-on-examine-merged-response");
}
setExtraHTTPHeaders(browser, headers) {
@ -152,7 +140,7 @@ class NetworkObserver {
httpChannel.resume();
this.emit("requestfailed", httpChannel, {
requestId: requestId(httpChannel),
errorCode: helper.getNetworkErrorStatusText(httpChannel.status),
errorCode: getNetworkErrorStatusText(httpChannel.status),
});
}
@ -294,7 +282,10 @@ class NetworkObserver {
const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
registrar.unregisterFactory(SINK_CLASS_ID, this._channelSinkFactory);
Services.catMan.deleteCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, false);
helper.removeListeners(this._eventListeners);
Services.obs.removeObserver(this._onRequest, "http-on-modify-request");
Services.obs.removeObserver(this._onExamineResponse, "http-on-examine-response");
Services.obs.removeObserver(this._onCachedResponse, "http-on-examine-cached-response");
Services.obs.removeObserver(this._onCachedResponse, "http-on-examine-merged-response");
}
}
@ -459,7 +450,7 @@ class ResponseBodyListener {
this.originalListener = httpChannel.setNewListener(this);
}
onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount) {
onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
const iStream = new BinaryInputStream(aInputStream);
const sStream = new StorageStream(8192, aCount, null);
const oStream = new BinaryOutputStream(sStream.getOutputStream(0));
@ -469,20 +460,75 @@ class ResponseBodyListener {
this._chunks.push(data);
oStream.writeBytes(data, aCount);
this.originalListener.onDataAvailable(aRequest, aContext, sStream.newInputStream(0), aOffset, aCount);
this.originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount);
}
onStartRequest(aRequest, aContext) {
this.originalListener.onStartRequest(aRequest, aContext);
onStartRequest(aRequest) {
this.originalListener.onStartRequest(aRequest);
}
onStopRequest(aRequest, aContext, aStatusCode) {
this.originalListener.onStopRequest(aRequest, aContext, aStatusCode);
onStopRequest(aRequest, aStatusCode) {
this.originalListener.onStopRequest(aRequest, aStatusCode);
const body = this._chunks.join("");
delete this._chunks;
this._networkObserver._onResponseFinished(this._browser, this._httpChannel, body);
}
}
function getNetworkErrorStatusText(status) {
if (!status)
return null;
for (const key of Object.keys(Cr)) {
if (Cr[key] === status)
return key;
}
// Security module. The following is taken from
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/How_to_check_the_secruity_state_of_an_XMLHTTPRequest_over_SSL
if ((status & 0xff0000) === 0x5a0000) {
// NSS_SEC errors (happen below the base value because of negative vals)
if ((status & 0xffff) < Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE)) {
// The bases are actually negative, so in our positive numeric space, we
// need to subtract the base off our value.
const nssErr = Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff);
switch (nssErr) {
case 11:
return "SEC_ERROR_EXPIRED_CERTIFICATE";
case 12:
return "SEC_ERROR_REVOKED_CERTIFICATE";
case 13:
return "SEC_ERROR_UNKNOWN_ISSUER";
case 20:
return "SEC_ERROR_UNTRUSTED_ISSUER";
case 21:
return "SEC_ERROR_UNTRUSTED_CERT";
case 36:
return "SEC_ERROR_CA_CERT_INVALID";
case 90:
return "SEC_ERROR_INADEQUATE_KEY_USAGE";
case 176:
return "SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED";
default:
return "SEC_ERROR_UNKNOWN";
}
}
const sslErr = Math.abs(Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff);
switch (sslErr) {
case 3:
return "SSL_ERROR_NO_CERTIFICATE";
case 4:
return "SSL_ERROR_BAD_CERTIFICATE";
case 8:
return "SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE";
case 9:
return "SSL_ERROR_UNSUPPORTED_VERSION";
case 12:
return "SSL_ERROR_BAD_CERT_DOMAIN";
default:
return "SSL_ERROR_UNKNOWN";
}
}
return "<unknown error>";
}
var EXPORTED_SYMBOLS = ["NetworkObserver"];
this.NetworkObserver = NetworkObserver;

View file

@ -39,7 +39,6 @@ remote.jar:
content/domains/content/Emulation.jsm (domains/content/Emulation.jsm)
content/domains/content/Input.jsm (domains/content/Input.jsm)
content/domains/content/Log.jsm (domains/content/Log.jsm)
content/domains/content/Network.jsm (domains/content/Network.jsm)
content/domains/content/Page.jsm (domains/content/Page.jsm)
content/domains/content/Performance.jsm (domains/content/Performance.jsm)
content/domains/content/Runtime.jsm (domains/content/Runtime.jsm)
@ -47,6 +46,8 @@ remote.jar:
content/domains/content/Security.jsm (domains/content/Security.jsm)
content/domains/parent/Browser.jsm (domains/parent/Browser.jsm)
content/domains/parent/Input.jsm (domains/parent/Input.jsm)
content/domains/parent/Network.jsm (domains/parent/Network.jsm)
content/domains/parent/network/NetworkObserver.jsm (domains/parent/network/NetworkObserver.jsm)
content/domains/parent/Page.jsm (domains/parent/Page.jsm)
content/domains/parent/Target.jsm (domains/parent/Target.jsm)

View file

@ -4,12 +4,15 @@ subsuite = remote
prefs = remote.enabled=true
support-files =
chrome-remote-interface.js
doc_network_requestWillBeSent.html
doc_page_frameNavigated.html
file_network_requestWillBeSent.js
head.js
[browser_cdp.js]
[browser_input_dispatchKeyEvent.js]
[browser_main_target.js]
[browser_network_requestWillBeSent.js]
[browser_page_bringToFront.js]
[browser_page_frameNavigated.js]
[browser_page_runtime_events.js]

View file

@ -0,0 +1,52 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test the Network.requestWillBeSent event
const TEST_URI = "data:text/html;charset=utf-8,default-test-page";
const PAGE_URI = "http://example.com/browser/remote/test/browser/doc_network_requestWillBeSent.html";
const JS_URI = "http://example.com/browser/remote/test/browser/file_network_requestWillBeSent.js";
add_task(async function() {
const {client} = await setupTestForUri(TEST_URI);
const {Page, Network} = client;
await Network.enable();
ok(true, "Network domain has been enabled");
let requests = 0;
const onRequests = new Promise(resolve => {
Network.requestWillBeSent(event => {
ok(true, "Received a request");
switch (++requests) {
case 1:
is(event.request.url, PAGE_URI, "Got the page request");
is(event.type, "Document", "The page request has 'Document' type");
is(event.requestId, event.loaderId, "The page request has requestId = loaderId (puppeteer assumes that to detect the page start request)");
break;
case 2:
is(event.request.url, JS_URI, "Got the JS request");
resolve();
break;
case 3:
ok(false, "Expect only two requests");
}
});
});
const { frameId } = await Page.navigate({ url: PAGE_URI });
ok(frameId, "Page.navigate returned a frameId");
info("Wait for Network.requestWillBeSent events");
await onRequests;
await client.close();
ok(true, "The client is closed");
BrowserTestUtils.removeTab(gBrowser.selectedTab);
await RemoteAgent.close();
});

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>Test page for requestWillBeSent</title>
</head>
<body>
<script type="text/javascript" src="file_network_requestWillBeSent.js"></script>
</body>
</html>

View file

@ -0,0 +1,2 @@
// Test file to emit Network.requestWillBeSent events.
var foo = true;