summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/network-monitor
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/network-monitor')
-rw-r--r--devtools/server/actors/network-monitor/channel-event-sink.js99
-rw-r--r--devtools/server/actors/network-monitor/moz.build12
-rw-r--r--devtools/server/actors/network-monitor/network-content.js144
-rw-r--r--devtools/server/actors/network-monitor/network-event-actor.js630
-rw-r--r--devtools/server/actors/network-monitor/network-parent.js175
5 files changed, 1060 insertions, 0 deletions
diff --git a/devtools/server/actors/network-monitor/channel-event-sink.js b/devtools/server/actors/network-monitor/channel-event-sink.js
new file mode 100644
index 0000000000..8ff00302f9
--- /dev/null
+++ b/devtools/server/actors/network-monitor/channel-event-sink.js
@@ -0,0 +1,99 @@
+/* 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 { ComponentUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ComponentUtils.sys.mjs"
+);
+
+/**
+ * This is a nsIChannelEventSink implementation that monitors channel redirects and
+ * informs the registered "collectors" about the old and new channels.
+ */
+const SINK_CLASS_DESCRIPTION = "NetworkMonitor Channel Event Sink";
+const SINK_CLASS_ID = Components.ID("{e89fa076-c845-48a8-8c45-2604729eba1d}");
+const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
+const SINK_CATEGORY_NAME = "net-channel-event-sinks";
+
+class ChannelEventSink {
+ constructor() {
+ this.wrappedJSObject = this;
+ this.collectors = new Set();
+ }
+
+ QueryInterface = ChromeUtils.generateQI(["nsIChannelEventSink"]);
+
+ registerCollector(collector) {
+ this.collectors.add(collector);
+ }
+
+ unregisterCollector(collector) {
+ this.collectors.delete(collector);
+
+ if (this.collectors.size == 0) {
+ ChannelEventSinkFactory.unregister();
+ }
+ }
+
+ // eslint-disable-next-line no-shadow
+ asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+ for (const collector of this.collectors) {
+ try {
+ collector.onChannelRedirect(oldChannel, newChannel, flags);
+ } catch (ex) {
+ console.error(
+ "ChannelEventSink collector's 'onChannelRedirect' threw an exception",
+ ex
+ );
+ }
+ }
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ }
+}
+
+const ChannelEventSinkFactory =
+ ComponentUtils.generateSingletonFactory(ChannelEventSink);
+
+ChannelEventSinkFactory.register = function () {
+ const registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ if (registrar.isCIDRegistered(SINK_CLASS_ID)) {
+ return;
+ }
+
+ registrar.registerFactory(
+ SINK_CLASS_ID,
+ SINK_CLASS_DESCRIPTION,
+ SINK_CONTRACT_ID,
+ ChannelEventSinkFactory
+ );
+
+ Services.catMan.addCategoryEntry(
+ SINK_CATEGORY_NAME,
+ SINK_CONTRACT_ID,
+ SINK_CONTRACT_ID,
+ false,
+ true
+ );
+};
+
+ChannelEventSinkFactory.unregister = function () {
+ const registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.unregisterFactory(SINK_CLASS_ID, ChannelEventSinkFactory);
+
+ Services.catMan.deleteCategoryEntry(
+ SINK_CATEGORY_NAME,
+ SINK_CONTRACT_ID,
+ false
+ );
+};
+
+ChannelEventSinkFactory.getService = function () {
+ // Make sure the ChannelEventSink service is registered before accessing it
+ ChannelEventSinkFactory.register();
+
+ return Cc[SINK_CONTRACT_ID].getService(Ci.nsIChannelEventSink)
+ .wrappedJSObject;
+};
+exports.ChannelEventSinkFactory = ChannelEventSinkFactory;
diff --git a/devtools/server/actors/network-monitor/moz.build b/devtools/server/actors/network-monitor/moz.build
new file mode 100644
index 0000000000..717ccc2807
--- /dev/null
+++ b/devtools/server/actors/network-monitor/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "channel-event-sink.js",
+ "network-content.js",
+ "network-event-actor.js",
+ "network-parent.js",
+)
diff --git a/devtools/server/actors/network-monitor/network-content.js b/devtools/server/actors/network-monitor/network-content.js
new file mode 100644
index 0000000000..c292d9da05
--- /dev/null
+++ b/devtools/server/actors/network-monitor/network-content.js
@@ -0,0 +1,144 @@
+/* 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 { Actor } = require("resource://devtools/shared/protocol.js");
+const {
+ networkContentSpec,
+} = require("resource://devtools/shared/specs/network-content.js");
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "NetUtil",
+ "resource://gre/modules/NetUtil.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NetworkUtils:
+ "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
+});
+
+loader.lazyRequireGetter(
+ this,
+ "WebConsoleUtils",
+ "resource://devtools/server/actors/webconsole/utils.js",
+ true
+);
+
+const {
+ TYPES: { NETWORK_EVENT_STACKTRACE },
+ getResourceWatcher,
+} = require("resource://devtools/server/actors/resources/index.js");
+
+/**
+ * This actor manages all network functionality runnning
+ * in the content process.
+ *
+ * @constructor
+ *
+ */
+class NetworkContentActor extends Actor {
+ constructor(conn, targetActor) {
+ super(conn, networkContentSpec);
+ this.targetActor = targetActor;
+ }
+
+ get networkEventStackTraceWatcher() {
+ return getResourceWatcher(this.targetActor, NETWORK_EVENT_STACKTRACE);
+ }
+
+ /**
+ * Send an HTTP request
+ *
+ * @param {Object} request
+ * The details of the HTTP Request.
+ * @return {Number}
+ * The channel id for the request
+ */
+ async sendHTTPRequest(request) {
+ return new Promise(resolve => {
+ const { url, method, headers, body, cause } = request;
+ // Set the loadingNode and loadGroup to the target document - otherwise the
+ // request won't show up in the opened netmonitor.
+ const doc = this.targetActor.window.document;
+
+ const channel = lazy.NetUtil.newChannel({
+ uri: lazy.NetUtil.newURI(url),
+ loadingNode: doc,
+ securityFlags:
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType:
+ lazy.NetworkUtils.stringToCauseType(cause.type) ||
+ Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+
+ channel.QueryInterface(Ci.nsIHttpChannel);
+ channel.loadGroup = doc.documentLoadGroup;
+ channel.loadFlags |=
+ Ci.nsIRequest.LOAD_BYPASS_CACHE |
+ Ci.nsIRequest.INHIBIT_CACHING |
+ Ci.nsIRequest.LOAD_ANONYMOUS;
+
+ if (method == "CONNECT") {
+ throw new Error(
+ "The CONNECT method is restricted and cannot be sent by devtools"
+ );
+ }
+ channel.requestMethod = method;
+
+ if (headers) {
+ for (const { name, value } of headers) {
+ if (name.toLowerCase() == "referer") {
+ // The referer header and referrerInfo object should always match. So
+ // if we want to set the header from privileged context, we should set
+ // referrerInfo. The referrer header will get set internally.
+ channel.setNewReferrerInfo(
+ value,
+ Ci.nsIReferrerInfo.UNSAFE_URL,
+ true
+ );
+ } else {
+ channel.setRequestHeader(name, value, false);
+ }
+ }
+ }
+
+ if (body) {
+ channel.QueryInterface(Ci.nsIUploadChannel2);
+ const bodyStream = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ bodyStream.setData(body, body.length);
+ channel.explicitSetUploadStream(bodyStream, null, -1, method, false);
+ }
+
+ // Make sure the fetch has completed before sending the channel id,
+ // so that there is a higher possibilty that the request get into the
+ // redux store beforehand (but this does not gurantee that).
+ lazy.NetUtil.asyncFetch(channel, () =>
+ resolve({ channelId: channel.channelId })
+ );
+ });
+ }
+
+ /**
+ * Gets the stacktrace for the specified network resource.
+ * @param {Number} resourceId
+ * The id for the network resource
+ * @return {Object}
+ * The response packet - stack trace.
+ */
+ getStackTrace(resourceId) {
+ if (!this.networkEventStackTraceWatcher) {
+ throw new Error("Not listening for network event stacktraces");
+ }
+ const stacktrace =
+ this.networkEventStackTraceWatcher.getStackTrace(resourceId);
+ return WebConsoleUtils.removeFramesAboveDebuggerEval(stacktrace);
+ }
+}
+
+exports.NetworkContentActor = NetworkContentActor;
diff --git a/devtools/server/actors/network-monitor/network-event-actor.js b/devtools/server/actors/network-monitor/network-event-actor.js
new file mode 100644
index 0000000000..228d6b582c
--- /dev/null
+++ b/devtools/server/actors/network-monitor/network-event-actor.js
@@ -0,0 +1,630 @@
+/* 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 { Actor } = require("resource://devtools/shared/protocol.js");
+const {
+ networkEventSpec,
+} = require("resource://devtools/shared/specs/network-event.js");
+
+const {
+ TYPES: { NETWORK_EVENT },
+} = require("resource://devtools/server/actors/resources/index.js");
+const {
+ LongStringActor,
+} = require("resource://devtools/server/actors/string.js");
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NetworkUtils:
+ "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
+});
+
+const CONTENT_TYPE_REGEXP = /^content-type/i;
+
+/**
+ * Creates an actor for a network event.
+ *
+ * @constructor
+ * @param {DevToolsServerConnection} conn
+ * The connection into which this Actor will be added.
+ * @param {Object} sessionContext
+ * The Session Context to help know what is debugged.
+ * See devtools/server/actors/watcher/session-context.js
+ * @param {Object} options
+ * Dictionary object with the following attributes:
+ * - onNetworkEventUpdate: optional function
+ * Callback for updates for the network event
+ * - onNetworkEventDestroy: optional function
+ * Callback for the destruction of the network event
+ * @param {Object} networkEventOptions
+ * Object describing the network event or the configuration of the
+ * network observer, and which cannot be easily inferred from the raw
+ * channel.
+ * - blockingExtension: optional string
+ * id of the blocking webextension if any
+ * - blockedReason: optional number or string
+ * - discardRequestBody: boolean
+ * - discardResponseBody: boolean
+ * - fromCache: boolean
+ * - fromServiceWorker: boolean
+ * - rawHeaders: string
+ * - timestamp: number
+ * @param {nsIChannel} channel
+ * The channel related to this network event
+ */
+class NetworkEventActor extends Actor {
+ constructor(
+ conn,
+ sessionContext,
+ { onNetworkEventUpdate, onNetworkEventDestroy },
+ networkEventOptions,
+ channel
+ ) {
+ super(conn, networkEventSpec);
+
+ this._sessionContext = sessionContext;
+ this._onNetworkEventUpdate = onNetworkEventUpdate;
+ this._onNetworkEventDestroy = onNetworkEventDestroy;
+
+ // Store the channelId which will act as resource id.
+ this._channelId = channel.channelId;
+
+ // innerWindowId and isNavigationRequest are used to check if the actor
+ // should be destroyed when a window is destroyed. See network-events.js.
+ this._innerWindowId = lazy.NetworkUtils.getChannelInnerWindowId(channel);
+ this._isNavigationRequest = lazy.NetworkUtils.isNavigationRequest(channel);
+
+ // Retrieve cookies and headers from the channel
+ const { cookies, headers } =
+ lazy.NetworkUtils.fetchRequestHeadersAndCookies(channel);
+
+ this._request = {
+ cookies,
+ headers,
+ postData: {},
+ rawHeaders: networkEventOptions.rawHeaders,
+ };
+
+ this._response = {
+ headers: [],
+ cookies: [],
+ content: {},
+ };
+
+ this._timings = {};
+ this._serverTimings = [];
+
+ this._discardRequestBody = !!networkEventOptions.discardRequestBody;
+ this._discardResponseBody = !!networkEventOptions.discardResponseBody;
+
+ this._resource = this._createResource(networkEventOptions, channel);
+ }
+
+ /**
+ * Return the network event actor as a resource, and add the actorID which is
+ * not available in the constructor yet.
+ */
+ asResource() {
+ return {
+ actor: this.actorID,
+ ...this._resource,
+ };
+ }
+
+ /**
+ * Create the resource corresponding to this actor.
+ */
+ _createResource(networkEventOptions, channel) {
+ channel = channel.QueryInterface(Ci.nsIHttpChannel);
+ const wsChannel = lazy.NetworkUtils.getWebSocketChannel(channel);
+
+ // Use the WebSocket channel URL for websockets.
+ const url = wsChannel ? wsChannel.URI.spec : channel.URI.spec;
+
+ let browsingContextID =
+ lazy.NetworkUtils.getChannelBrowsingContextID(channel);
+
+ // Ensure that we have a browsing context ID for all requests.
+ // Only privileged requests debugged via the Browser Toolbox (sessionContext.type == "all") can be unrelated to any browsing context.
+ if (!browsingContextID && this._sessionContext.type != "all") {
+ throw new Error(`Got a request ${url} without a browsingContextID set`);
+ }
+
+ // The browsingContextID is used by the ResourceCommand on the client
+ // to find the related Target Front.
+ //
+ // For now in the browser and web extension toolboxes, requests
+ // do not relate to any specific WindowGlobalTargetActor
+ // as we are still using a unique target (ParentProcessTargetActor) for everything.
+ if (
+ this._sessionContext.type == "all" ||
+ this._sessionContext.type == "webextension"
+ ) {
+ browsingContextID = -1;
+ }
+
+ const cause = lazy.NetworkUtils.getCauseDetails(channel);
+ // Both xhr and fetch are flagged as XHR in DevTools.
+ const isXHR = cause.type == "xhr" || cause.type == "fetch";
+
+ // For websocket requests the serial is used instead of the channel id.
+ const stacktraceResourceId =
+ cause.type == "websocket" ? wsChannel.serial : channel.channelId;
+
+ // If a timestamp was provided, it is a high resolution timestamp
+ // corresponding to ACTIVITY_SUBTYPE_REQUEST_HEADER. Fallback to Date.now().
+ const timeStamp = networkEventOptions.timestamp
+ ? networkEventOptions.timestamp / 1000
+ : Date.now();
+
+ let blockedReason = networkEventOptions.blockedReason;
+
+ // Check if blockedReason was set to a falsy value, meaning the blocked did
+ // not give an explicit blocked reason.
+ if (
+ blockedReason === 0 ||
+ blockedReason === false ||
+ blockedReason === null ||
+ blockedReason === ""
+ ) {
+ blockedReason = "unknown";
+ }
+
+ const resource = {
+ resourceId: channel.channelId,
+ resourceType: NETWORK_EVENT,
+ blockedReason,
+ blockingExtension: networkEventOptions.blockingExtension,
+ browsingContextID,
+ cause,
+ // This is used specifically in the browser toolbox console to distinguish privileged
+ // resources from the parent process from those from the contet
+ chromeContext: lazy.NetworkUtils.isChannelFromSystemPrincipal(channel),
+ fromCache: networkEventOptions.fromCache,
+ fromServiceWorker: networkEventOptions.fromServiceWorker,
+ innerWindowId: this._innerWindowId,
+ isNavigationRequest: this._isNavigationRequest,
+ isThirdPartyTrackingResource:
+ lazy.NetworkUtils.isThirdPartyTrackingResource(channel),
+ isXHR,
+ method: channel.requestMethod,
+ priority: lazy.NetworkUtils.getChannelPriority(channel),
+ private: lazy.NetworkUtils.isChannelPrivate(channel),
+ referrerPolicy: lazy.NetworkUtils.getReferrerPolicy(channel),
+ stacktraceResourceId,
+ startedDateTime: new Date(timeStamp).toISOString(),
+ timeStamp,
+ timings: {},
+ url,
+ };
+
+ return resource;
+ }
+
+ /**
+ * Releases this actor from the pool.
+ */
+ destroy(conn) {
+ if (!this._channelId) {
+ return;
+ }
+
+ if (this._onNetworkEventDestroy) {
+ this._onNetworkEventDestroy(this._channelId);
+ }
+
+ this._channelId = null;
+ super.destroy(conn);
+ }
+
+ release() {
+ // Per spec, destroy is automatically going to be called after this request
+ }
+
+ getInnerWindowId() {
+ return this._innerWindowId;
+ }
+
+ isNavigationRequest() {
+ return this._isNavigationRequest;
+ }
+
+ /**
+ * The "getRequestHeaders" packet type handler.
+ *
+ * @return object
+ * The response packet - network request headers.
+ */
+ getRequestHeaders() {
+ let rawHeaders;
+ let headersSize = 0;
+ if (this._request.rawHeaders) {
+ headersSize = this._request.rawHeaders.length;
+ rawHeaders = this._createLongStringActor(this._request.rawHeaders);
+ }
+
+ return {
+ headers: this._request.headers.map(header => ({
+ name: header.name,
+ value: this._createLongStringActor(header.value),
+ })),
+ headersSize,
+ rawHeaders,
+ };
+ }
+
+ /**
+ * The "getRequestCookies" packet type handler.
+ *
+ * @return object
+ * The response packet - network request cookies.
+ */
+ getRequestCookies() {
+ return {
+ cookies: this._request.cookies.map(cookie => ({
+ name: cookie.name,
+ value: this._createLongStringActor(cookie.value),
+ })),
+ };
+ }
+
+ /**
+ * The "getRequestPostData" packet type handler.
+ *
+ * @return object
+ * The response packet - network POST data.
+ */
+ getRequestPostData() {
+ let postDataText;
+ if (this._request.postData.text) {
+ // Create a long string actor for the postData text if needed.
+ postDataText = this._createLongStringActor(this._request.postData.text);
+ }
+
+ return {
+ postData: {
+ size: this._request.postData.size,
+ text: postDataText,
+ },
+ postDataDiscarded: this._discardRequestBody,
+ };
+ }
+
+ /**
+ * The "getSecurityInfo" packet type handler.
+ *
+ * @return object
+ * The response packet - connection security information.
+ */
+ getSecurityInfo() {
+ return {
+ securityInfo: this._securityInfo,
+ };
+ }
+
+ /**
+ * The "getResponseHeaders" packet type handler.
+ *
+ * @return object
+ * The response packet - network response headers.
+ */
+ getResponseHeaders() {
+ let rawHeaders;
+ let headersSize = 0;
+ if (this._response.rawHeaders) {
+ headersSize = this._response.rawHeaders.length;
+ rawHeaders = this._createLongStringActor(this._response.rawHeaders);
+ }
+
+ return {
+ headers: this._response.headers.map(header => ({
+ name: header.name,
+ value: this._createLongStringActor(header.value),
+ })),
+ headersSize,
+ rawHeaders,
+ };
+ }
+
+ /**
+ * The "getResponseCache" packet type handler.
+ *
+ * @return object
+ * The cache packet - network cache information.
+ */
+ getResponseCache() {
+ return {
+ cache: this._response.responseCache,
+ };
+ }
+
+ /**
+ * The "getResponseCookies" packet type handler.
+ *
+ * @return object
+ * The response packet - network response cookies.
+ */
+ getResponseCookies() {
+ // As opposed to request cookies, response cookies can come with additional
+ // properties.
+ const cookieOptionalProperties = [
+ "domain",
+ "expires",
+ "httpOnly",
+ "path",
+ "samesite",
+ "secure",
+ ];
+
+ return {
+ cookies: this._response.cookies.map(cookie => {
+ const cookieResponse = {
+ name: cookie.name,
+ value: this._createLongStringActor(cookie.value),
+ };
+
+ for (const prop of cookieOptionalProperties) {
+ if (prop in cookie) {
+ cookieResponse[prop] = cookie[prop];
+ }
+ }
+ return cookieResponse;
+ }),
+ };
+ }
+
+ /**
+ * The "getResponseContent" packet type handler.
+ *
+ * @return object
+ * The response packet - network response content.
+ */
+ getResponseContent() {
+ return {
+ content: this._response.content,
+ contentDiscarded: this._discardResponseBody,
+ };
+ }
+
+ /**
+ * The "getEventTimings" packet type handler.
+ *
+ * @return object
+ * The response packet - network event timings.
+ */
+ getEventTimings() {
+ return {
+ timings: this._timings,
+ totalTime: this._totalTime,
+ offsets: this._offsets,
+ serverTimings: this._serverTimings,
+ };
+ }
+
+ /** ****************************************************************
+ * Listeners for new network event data coming from NetworkMonitor.
+ ******************************************************************/
+
+ /**
+ * Add network request POST data.
+ *
+ * @param object postData
+ * The request POST data.
+ */
+ addRequestPostData(postData) {
+ // Ignore calls when this actor is already destroyed
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ this._request.postData = postData;
+ this._onEventUpdate("requestPostData", {});
+ }
+
+ /**
+ * Add the initial network response information.
+ *
+ * @param {object} options
+ * @param {nsIChannel} options.channel
+ * @param {boolean} options.fromCache
+ * @param {string} options.rawHeaders
+ */
+ addResponseStart({ channel, fromCache, rawHeaders = "" }) {
+ // Ignore calls when this actor is already destroyed
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ fromCache = fromCache || lazy.NetworkUtils.isFromCache(channel);
+
+ // Read response headers and cookies.
+ let responseHeaders = [];
+ let responseCookies = [];
+ if (!this._blockedReason) {
+ const { cookies, headers } =
+ lazy.NetworkUtils.fetchResponseHeadersAndCookies(channel);
+ responseCookies = cookies;
+ responseHeaders = headers;
+ }
+
+ // Handle response headers
+ this._response.rawHeaders = rawHeaders;
+ this._response.headers = responseHeaders;
+ this._response.cookies = responseCookies;
+
+ // Handle the rest of the response start metadata.
+ this._response.headersSize = rawHeaders ? rawHeaders.length : 0;
+
+ // Discard the response body for known response statuses.
+ if (lazy.NetworkUtils.isRedirectedChannel(channel)) {
+ this._discardResponseBody = true;
+ }
+
+ // Mime type needs to be sent on response start for identifying an sse channel.
+ const contentTypeHeader = responseHeaders.find(header =>
+ CONTENT_TYPE_REGEXP.test(header.name)
+ );
+
+ let mimeType = "";
+ if (contentTypeHeader) {
+ mimeType = contentTypeHeader.value;
+ }
+
+ const timedChannel = channel.QueryInterface(Ci.nsITimedChannel);
+ const waitingTime = Math.round(
+ (timedChannel.responseStartTime - timedChannel.requestStartTime) / 1000
+ );
+
+ this._onEventUpdate("responseStart", {
+ httpVersion: lazy.NetworkUtils.getHttpVersion(channel),
+ mimeType,
+ remoteAddress: fromCache ? "" : channel.remoteAddress,
+ remotePort: fromCache ? "" : channel.remotePort,
+ status: channel.responseStatus + "",
+ statusText: channel.responseStatusText,
+ waitingTime,
+ });
+ }
+
+ /**
+ * Add connection security information.
+ *
+ * @param object info
+ * The object containing security information.
+ */
+ addSecurityInfo(info, isRacing) {
+ // Ignore calls when this actor is already destroyed
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ this._securityInfo = info;
+
+ this._onEventUpdate("securityInfo", {
+ state: info.state,
+ isRacing,
+ });
+ }
+
+ /**
+ * Add network response content.
+ *
+ * @param object content
+ * The response content.
+ * @param object
+ * - boolean discardedResponseBody
+ * Tells if the response content was recorded or not.
+ */
+ addResponseContent(
+ content,
+ { discardResponseBody, blockedReason, blockingExtension }
+ ) {
+ // Ignore calls when this actor is already destroyed
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ this._response.content = content;
+ content.text = new LongStringActor(this.conn, content.text);
+ // bug 1462561 - Use "json" type and manually manage/marshall actors to workaround
+ // protocol.js performance issue
+ this.manage(content.text);
+ content.text = content.text.form();
+
+ this._onEventUpdate("responseContent", {
+ mimeType: content.mimeType,
+ contentSize: content.size,
+ transferredSize: content.transferredSize,
+ blockedReason,
+ blockingExtension,
+ });
+ }
+
+ addResponseCache(content) {
+ // Ignore calls when this actor is already destroyed
+ if (this.isDestroyed()) {
+ return;
+ }
+ this._response.responseCache = content.responseCache;
+ this._onEventUpdate("responseCache", {});
+ }
+
+ /**
+ * Add network event timing information.
+ *
+ * @param number total
+ * The total time of the network event.
+ * @param object timings
+ * Timing details about the network event.
+ * @param object offsets
+ * @param object serverTimings
+ * Timing details extracted from the Server-Timing header.
+ */
+ addEventTimings(total, timings, offsets, serverTimings) {
+ // Ignore calls when this actor is already destroyed
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ this._totalTime = total;
+ this._timings = timings;
+ this._offsets = offsets;
+
+ if (serverTimings) {
+ this._serverTimings = serverTimings;
+ }
+
+ this._onEventUpdate("eventTimings", { totalTime: total });
+ }
+
+ /**
+ * Store server timing information. They will be merged together
+ * with network event timing data when they are available and
+ * notification sent to the client.
+ * See `addEventTimnings`` above for more information.
+ *
+ * @param object serverTimings
+ * Timing details extracted from the Server-Timing header.
+ */
+ addServerTimings(serverTimings) {
+ if (serverTimings) {
+ this._serverTimings = serverTimings;
+ }
+ }
+
+ _createLongStringActor(string) {
+ if (string?.actorID) {
+ return string;
+ }
+
+ const longStringActor = new LongStringActor(this.conn, string);
+ // bug 1462561 - Use "json" type and manually manage/marshall actors to workaround
+ // protocol.js performance issue
+ this.manage(longStringActor);
+ return longStringActor.form();
+ }
+
+ /**
+ * Sends the updated event data to the client
+ *
+ * @private
+ * @param string updateType
+ * @param object data
+ * The properties that have changed for the event
+ */
+ _onEventUpdate(updateType, data) {
+ if (this._onNetworkEventUpdate) {
+ this._onNetworkEventUpdate({
+ resourceId: this._channelId,
+ updateType,
+ ...data,
+ });
+ }
+ }
+}
+
+exports.NetworkEventActor = NetworkEventActor;
diff --git a/devtools/server/actors/network-monitor/network-parent.js b/devtools/server/actors/network-monitor/network-parent.js
new file mode 100644
index 0000000000..bc7eab1051
--- /dev/null
+++ b/devtools/server/actors/network-monitor/network-parent.js
@@ -0,0 +1,175 @@
+/* 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 { Actor } = require("resource://devtools/shared/protocol.js");
+const {
+ networkParentSpec,
+} = require("resource://devtools/shared/specs/network-parent.js");
+
+const {
+ TYPES: { NETWORK_EVENT },
+ getResourceWatcher,
+} = require("resource://devtools/server/actors/resources/index.js");
+
+/**
+ * This actor manages all network functionality running
+ * in the parent process.
+ *
+ * @constructor
+ *
+ */
+class NetworkParentActor extends Actor {
+ constructor(watcherActor) {
+ super(watcherActor.conn, networkParentSpec);
+ this.watcherActor = watcherActor;
+ }
+
+ // Caches the throttling data so that on clearing the
+ // current network throttling it can be reset to the previous.
+ defaultThrottleData = undefined;
+
+ isEqual(next, current) {
+ // If both objects, check all entries
+ if (current && next && next == current) {
+ return Object.entries(current).every(([k, v]) => {
+ return next[k] === v;
+ });
+ }
+ return false;
+ }
+
+ get networkEventWatcher() {
+ return getResourceWatcher(this.watcherActor, NETWORK_EVENT);
+ }
+
+ setNetworkThrottling(throttleData) {
+ if (!this.networkEventWatcher) {
+ throw new Error("Not listening for network events");
+ }
+
+ if (throttleData !== null) {
+ throttleData = {
+ latencyMean: throttleData.latency,
+ latencyMax: throttleData.latency,
+ downloadBPSMean: throttleData.downloadThroughput,
+ downloadBPSMax: throttleData.downloadThroughput,
+ uploadBPSMean: throttleData.uploadThroughput,
+ uploadBPSMax: throttleData.uploadThroughput,
+ };
+ }
+
+ const currentThrottleData = this.networkEventWatcher.getThrottleData();
+ if (this.isEqual(throttleData, currentThrottleData)) {
+ return;
+ }
+
+ if (this.defaultThrottleData === undefined) {
+ this.defaultThrottleData = currentThrottleData;
+ }
+
+ this.networkEventWatcher.setThrottleData(throttleData);
+ }
+
+ getNetworkThrottling() {
+ if (!this.networkEventWatcher) {
+ throw new Error("Not listening for network events");
+ }
+ const throttleData = this.networkEventWatcher.getThrottleData();
+ if (!throttleData) {
+ return null;
+ }
+ return {
+ downloadThroughput: throttleData.downloadBPSMax,
+ uploadThroughput: throttleData.uploadBPSMax,
+ latency: throttleData.latencyMax,
+ };
+ }
+
+ clearNetworkThrottling() {
+ if (this.defaultThrottleData !== undefined) {
+ this.setNetworkThrottling(this.defaultThrottleData);
+ }
+ }
+
+ setSaveRequestAndResponseBodies(save) {
+ if (!this.networkEventWatcher) {
+ throw new Error("Not listening for network events");
+ }
+ this.networkEventWatcher.setSaveRequestAndResponseBodies(save);
+ }
+
+ /**
+ * Sets the urls to block.
+ *
+ * @param Array urls
+ * The response packet - stack trace.
+ */
+ setBlockedUrls(urls) {
+ if (!this.networkEventWatcher) {
+ throw new Error("Not listening for network events");
+ }
+ this.networkEventWatcher.setBlockedUrls(urls);
+ return {};
+ }
+
+ /**
+ * Returns the urls that are block
+ */
+ getBlockedUrls() {
+ if (!this.networkEventWatcher) {
+ throw new Error("Not listening for network events");
+ }
+ return this.networkEventWatcher.getBlockedUrls();
+ }
+
+ /**
+ * Blocks the requests based on the filters
+ * @param {Object} filters
+ */
+ blockRequest(filters) {
+ if (!this.networkEventWatcher) {
+ throw new Error("Not listening for network events");
+ }
+ this.networkEventWatcher.blockRequest(filters);
+ }
+
+ /**
+ * Unblocks requests based on the filters
+ * @param {Object} filters
+ */
+ unblockRequest(filters) {
+ if (!this.networkEventWatcher) {
+ throw new Error("Not listening for network events");
+ }
+ this.networkEventWatcher.unblockRequest(filters);
+ }
+
+ setPersist(enabled) {
+ // We will always call this method, even if we are still using legacy listener.
+ // Do not throw, we will always persist in that deprecated codepath.
+ if (!this.networkEventWatcher) {
+ return;
+ }
+ this.networkEventWatcher.setPersist(enabled);
+ }
+
+ override(url, path) {
+ if (!this.networkEventWatcher) {
+ throw new Error("Not listening for network events");
+ }
+ this.networkEventWatcher.override(url, path);
+ return {};
+ }
+
+ removeOverride(url) {
+ if (!this.networkEventWatcher) {
+ throw new Error("Not listening for network events");
+ }
+ this.networkEventWatcher.removeOverride(url);
+ }
+}
+
+exports.NetworkParentActor = NetworkParentActor;