diff options
Diffstat (limited to 'devtools/server/actors/network-monitor')
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; |