network-events.js (15130B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { Pool } = require("resource://devtools/shared/protocol/Pool.js"); 8 const { ParentProcessWatcherRegistry } = ChromeUtils.importESModule( 9 "resource://devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs", 10 // ParentProcessWatcherRegistry needs to be a true singleton and loads ActorManagerParent 11 // which also has to be a true singleton. 12 { global: "shared" } 13 ); 14 const Targets = require("resource://devtools/server/actors/targets/index.js"); 15 16 const lazy = {}; 17 18 const { XPCOMUtils } = ChromeUtils.importESModule( 19 "resource://gre/modules/XPCOMUtils.sys.mjs", 20 { global: "contextual" } 21 ); 22 XPCOMUtils.defineLazyPreferenceGetter( 23 lazy, 24 "responseBodyLimit", 25 "devtools.netmonitor.responseBodyLimit", 26 0 27 ); 28 29 ChromeUtils.defineESModuleGetters( 30 lazy, 31 { 32 NetworkObserver: 33 "resource://devtools/shared/network-observer/NetworkObserver.sys.mjs", 34 NetworkUtils: 35 "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs", 36 }, 37 { global: "contextual" } 38 ); 39 40 loader.lazyRequireGetter( 41 this, 42 "NetworkEventActor", 43 "resource://devtools/server/actors/network-monitor/network-event-actor.js", 44 true 45 ); 46 47 /** 48 * Handles network events from the parent process 49 */ 50 class NetworkEventWatcher { 51 /** 52 * Start watching for all network events related to a given Watcher Actor. 53 * 54 * @param WatcherActor watcherActor 55 * The watcher actor in the parent process from which we should 56 * observe network events. 57 * @param Object options 58 * Dictionary object with following attributes: 59 * - onAvailable: mandatory function 60 * This will be called for each resource. 61 * - onUpdated: optional function 62 * This would be called multiple times for each resource. 63 */ 64 async watch(watcherActor, { onAvailable, onUpdated }) { 65 this.networkEvents = new Map(); 66 67 this.watcherActor = watcherActor; 68 this.onNetworkEventAvailable = onAvailable; 69 this.onNetworkEventUpdated = onUpdated; 70 // Boolean to know if we keep previous document network events or not. 71 this.persist = false; 72 this.listener = new lazy.NetworkObserver({ 73 decodeResponseBodies: true, 74 responseBodyLimit: lazy.responseBodyLimit, 75 ignoreChannelFunction: this.shouldIgnoreChannel.bind(this), 76 onNetworkEvent: this.onNetworkEvent.bind(this), 77 }); 78 79 this.watcherActor.on( 80 "top-browsing-context-will-navigate", 81 this.#onTopBrowsingContextWillNavigate 82 ); 83 } 84 85 /** 86 * Clear all the network events and the related actors. 87 * 88 * This is called on actor destroy, but also from WatcherActor.clearResources(NETWORK_EVENT) 89 */ 90 clear() { 91 this.networkEvents.clear(); 92 this.listener.clear(); 93 if (this._pool) { 94 this._pool.destroy(); 95 this._pool = null; 96 } 97 } 98 99 /** 100 * A protocol.js Pool to store all NetworkEventActor's which may be destroyed on navigations. 101 */ 102 get pool() { 103 if (this._pool) { 104 return this._pool; 105 } 106 this._pool = new Pool(this.watcherActor.conn, "network-events"); 107 this.watcherActor.manage(this._pool); 108 return this._pool; 109 } 110 111 /** 112 * Instruct to keep reference to previous document requests or not. 113 * If persist is disabled, we will clear all informations about previous document 114 * on each navigation. 115 * If persist is enabled, we will keep all informations for all documents, leading 116 * to lots of allocations! 117 * 118 * @param {boolean} enabled 119 */ 120 setPersist(enabled) { 121 this.persist = enabled; 122 } 123 124 /** 125 * Gets the throttle settings 126 * 127 * @return {*} data 128 */ 129 getThrottleData() { 130 return this.listener.getThrottleData(); 131 } 132 133 /** 134 * Sets the throttle data 135 * 136 * @param {*} data 137 */ 138 setThrottleData(data) { 139 this.listener.setThrottleData(data); 140 } 141 142 /** 143 * Instruct to save or ignore request and response bodies 144 * 145 * @param {boolean} save 146 */ 147 setSaveRequestAndResponseBodies(save) { 148 this.listener.setSaveRequestAndResponseBodies(save); 149 } 150 151 /** 152 * Block requests based on the filters 153 * 154 * @param {object} filters 155 */ 156 blockRequest(filters) { 157 this.listener.blockRequest(filters); 158 } 159 160 /** 161 * Unblock requests based on the fitlers 162 * 163 * @param {object} filters 164 */ 165 unblockRequest(filters) { 166 this.listener.unblockRequest(filters); 167 } 168 169 /** 170 * Calls the listener to set blocked urls 171 * 172 * @param {Array} urls 173 * The urls to block 174 */ 175 176 setBlockedUrls(urls) { 177 this.listener.setBlockedUrls(urls); 178 } 179 180 /** 181 * Calls the listener to get the blocked urls 182 * 183 * @return {Array} urls 184 * The blocked urls 185 */ 186 187 getBlockedUrls() { 188 return this.listener.getBlockedUrls(); 189 } 190 191 override(url, path) { 192 this.listener.override(url, path); 193 } 194 195 removeOverride(url) { 196 this.listener.removeOverride(url); 197 } 198 199 /** 200 * Watch for previous document being unloaded in order to clear 201 * all related network events, in case persist is disabled. 202 * (which is the default behavior) 203 * 204 * This "will-navigate" event should only be fired when debugging tabs 205 * (not for web extensions or browser toolbox). 206 */ 207 #onTopBrowsingContextWillNavigate = () => { 208 // If we persist, we will keep all requests allocated. 209 if (this.persist) { 210 return; 211 } 212 213 const { innerWindowId } = 214 this.watcherActor.browserElement.browsingContext.currentWindowGlobal; 215 216 // When a navigation starts, destroy all network request actors as the UI should not longer show them. 217 // We can easily destroy all requests which aren't navigation request. 218 // But navigation requests should be preserved as they started just before the navigation 219 // (and the will-navigate" event fired). 220 // The current WindowGloball is still for the document we navigate **from**, 221 // so destroy navigation requests from iframes or the WindowGlobal from the previous navigation 222 // with the `innerWindowId` comparison. 223 for (const child of this.pool.poolChildren()) { 224 if ( 225 !child.isNavigationRequest() || 226 (child.getInnerWindowId() && child.getInnerWindowId() != innerWindowId) 227 ) { 228 child.destroy(); 229 } 230 } 231 }; 232 233 /** 234 * Called by NetworkObserver in order to know if the channel should be ignored 235 */ 236 shouldIgnoreChannel(channel) { 237 // First of all, check if the channel matches the watcherActor's session. 238 const filters = { sessionContext: this.watcherActor.sessionContext }; 239 if (!lazy.NetworkUtils.matchRequest(channel, filters)) { 240 return true; 241 } 242 243 // When we are in the browser toolbox in parent process scope, 244 // the session context is still "all", but we are no longer watching frame and process targets. 245 // In this case, we should ignore all requests belonging to a BrowsingContext that isn't in the parent process 246 // (i.e. the process where this Watcher runs) 247 const isParentProcessOnlyBrowserToolbox = 248 this.watcherActor.sessionContext.type == "all" && 249 !ParentProcessWatcherRegistry.isWatchingTargets( 250 this.watcherActor, 251 Targets.TYPES.FRAME 252 ); 253 if (isParentProcessOnlyBrowserToolbox) { 254 // We should ignore all requests coming from BrowsingContext running in another process 255 const browsingContextID = 256 lazy.NetworkUtils.getChannelBrowsingContextID(channel); 257 const browsingContext = BrowsingContext.get(browsingContextID); 258 // We accept any request that isn't bound to any BrowsingContext. 259 // This is most likely a privileged request done from a JSM/C++. 260 // `isInProcess` will be true, when the document executes in the parent process. 261 // 262 // Note that we will still accept all requests that aren't bound to any BrowsingContext 263 // See browser_resources_network_events_parent_process.js test with privileged request 264 // made from the content processes. 265 // We miss some attribute on channel/loadInfo to know that it comes from the content process. 266 if (browsingContext?.currentWindowGlobal.isInProcess === false) { 267 return true; 268 } 269 } 270 return false; 271 } 272 273 onNetworkEvent(networkEventOptions, channel) { 274 if (channel.channelId && this.networkEvents.has(channel.channelId)) { 275 throw new Error( 276 `Got notified about channel ${channel.channelId} more than once.` 277 ); 278 } 279 280 const actor = new NetworkEventActor( 281 this.watcherActor.conn, 282 this.watcherActor.sessionContext, 283 { 284 onNetworkEventUpdate: this.onNetworkEventUpdate.bind(this), 285 onNetworkEventDestroy: this.onNetworkEventDestroy.bind(this), 286 }, 287 networkEventOptions, 288 channel 289 ); 290 this.pool.manage(actor); 291 292 const resource = actor.asResource(); 293 const isBlocked = !!resource.blockedReason; 294 const networkEvent = { 295 browsingContextID: resource.browsingContextID, 296 innerWindowId: resource.innerWindowId, 297 resourceId: resource.resourceId, 298 isBlocked, 299 receivedUpdates: [], 300 resourceUpdates: {}, 301 }; 302 303 // Requests already come with request cookies and headers, so those 304 // should always be considered as available. But the client still 305 // heavily relies on those `Available` flags to fetch additional data, 306 // so it is better to keep them for consistency. 307 308 // Set the flags on the resource so that the front-end can fetch 309 // and display request headers and cookies details asap. 310 lazy.NetworkUtils.setEventAsAvailable(resource, [ 311 lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_COOKIES, 312 lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_HEADERS, 313 ]); 314 315 this.networkEvents.set(resource.resourceId, networkEvent); 316 317 this.onNetworkEventAvailable([resource]); 318 319 // Blocked requests will not receive further updates and should emit an 320 // update packet immediately. 321 // The frontend expects to receive a dedicated update to consider the 322 // request as completed. TODO: lift this restriction so that we can only 323 // emit a resource available notification if no update is needed. 324 if (isBlocked) { 325 lazy.NetworkUtils.setEventAsAvailable(networkEvent.resourceUpdates, [ 326 lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_END, 327 ]); 328 this._emitUpdate(networkEvent); 329 } 330 331 return actor; 332 } 333 334 onNetworkEventUpdate(updateResource) { 335 const networkEvent = this.networkEvents.get(updateResource.resourceId); 336 337 if (!networkEvent) { 338 return; 339 } 340 const { NETWORK_EVENT_TYPES } = lazy.NetworkUtils; 341 const { resourceUpdates, receivedUpdates } = networkEvent; 342 343 const networkEventTypes = [ 344 NETWORK_EVENT_TYPES.RESPONSE_COOKIES, 345 NETWORK_EVENT_TYPES.RESPONSE_HEADERS, 346 ]; 347 348 switch (updateResource.updateType) { 349 case NETWORK_EVENT_TYPES.CACHE_DETAILS: 350 resourceUpdates.fromCache = updateResource.fromCache; 351 resourceUpdates.fromServiceWorker = updateResource.fromServiceWorker; 352 break; 353 case NETWORK_EVENT_TYPES.RESPONSE_START: 354 resourceUpdates.httpVersion = updateResource.httpVersion; 355 resourceUpdates.status = updateResource.status; 356 resourceUpdates.statusText = updateResource.statusText; 357 resourceUpdates.earlyHintsStatus = updateResource.earlyHintsStatus; 358 resourceUpdates.remoteAddress = updateResource.remoteAddress; 359 resourceUpdates.remotePort = updateResource.remotePort; 360 // The mimetype is only set when then the contentType is available 361 // in the _onResponseHeader and not for cached/service worker requests 362 // in _httpResponseExaminer. 363 resourceUpdates.mimeType = updateResource.mimeType; 364 resourceUpdates.waitingTime = updateResource.waitingTime; 365 resourceUpdates.isResolvedByTRR = updateResource.isResolvedByTRR; 366 resourceUpdates.proxyHttpVersion = updateResource.proxyHttpVersion; 367 resourceUpdates.proxyStatus = updateResource.proxyStatus; 368 resourceUpdates.proxyStatusText = updateResource.proxyStatusText; 369 370 if (resourceUpdates.earlyHintsStatus.length) { 371 networkEventTypes.push( 372 NETWORK_EVENT_TYPES.EARLY_HINT_RESPONSE_HEADERS 373 ); 374 } 375 376 lazy.NetworkUtils.setEventAsAvailable( 377 resourceUpdates, 378 networkEventTypes 379 ); 380 381 break; 382 case NETWORK_EVENT_TYPES.RESPONSE_CONTENT: 383 resourceUpdates.contentSize = updateResource.contentSize; 384 resourceUpdates.transferredSize = updateResource.transferredSize; 385 resourceUpdates.mimeType = updateResource.mimeType; 386 break; 387 case NETWORK_EVENT_TYPES.RESPONSE_CONTENT_COMPLETE: 388 resourceUpdates.extension = updateResource.extension; 389 resourceUpdates.blockedReason = updateResource.blockedReason; 390 break; 391 case NETWORK_EVENT_TYPES.EVENT_TIMINGS: 392 resourceUpdates.totalTime = updateResource.totalTime; 393 break; 394 case NETWORK_EVENT_TYPES.SECURITY_INFO: 395 resourceUpdates.securityState = updateResource.state; 396 resourceUpdates.isRacing = updateResource.isRacing; 397 break; 398 } 399 400 lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [ 401 updateResource.updateType, 402 ]); 403 404 receivedUpdates.push(updateResource.updateType); 405 406 const isResponseComplete = 407 receivedUpdates.includes(NETWORK_EVENT_TYPES.EVENT_TIMINGS) && 408 receivedUpdates.includes(NETWORK_EVENT_TYPES.RESPONSE_CONTENT_COMPLETE) && 409 receivedUpdates.includes(NETWORK_EVENT_TYPES.SECURITY_INFO); 410 411 if (isResponseComplete) { 412 // Lets add an event to clearly define the last update expected to be 413 // emitted. There will be no more updates after this. 414 lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [ 415 lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_END, 416 ]); 417 } 418 419 if ( 420 updateResource.updateType == NETWORK_EVENT_TYPES.RESPONSE_START || 421 updateResource.updateType == NETWORK_EVENT_TYPES.RESPONSE_CONTENT || 422 isResponseComplete 423 ) { 424 this._emitUpdate(networkEvent); 425 // clean up already sent updates 426 networkEvent.resourceUpdates = {}; 427 } 428 } 429 430 _emitUpdate(networkEvent) { 431 this.onNetworkEventUpdated([ 432 { 433 resourceId: networkEvent.resourceId, 434 resourceUpdates: networkEvent.resourceUpdates, 435 browsingContextID: networkEvent.browsingContextID, 436 innerWindowId: networkEvent.innerWindowId, 437 }, 438 ]); 439 } 440 441 onNetworkEventDestroy(channelId) { 442 if (this.networkEvents.has(channelId)) { 443 this.networkEvents.delete(channelId); 444 } 445 } 446 447 /** 448 * Stop watching for network event related to a given Watcher Actor. 449 */ 450 destroy() { 451 if (this.listener) { 452 this.clear(); 453 this.listener.destroy(); 454 this.watcherActor.off( 455 "top-browsing-context-will-navigate", 456 this.#onTopBrowsingContextWillNavigate 457 ); 458 } 459 } 460 } 461 462 module.exports = NetworkEventWatcher;