network-events-content.js (11370B)
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 loader.lazyRequireGetter( 8 this, 9 "NetworkEventActor", 10 "resource://devtools/server/actors/network-monitor/network-event-actor.js", 11 true 12 ); 13 14 const lazy = {}; 15 16 ChromeUtils.defineESModuleGetters( 17 lazy, 18 { 19 NetworkUtils: 20 "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs", 21 }, 22 { global: "contextual" } 23 ); 24 25 // Internal resource types used to create the appropriate network event based 26 // on where the channel / resource is coming from. 27 const RESOURCE_TYPES = { 28 BLOCKED: "blocked-resource", 29 CACHED: "cached-resource", 30 DATA_CHANNEL: "data-channel-resource", 31 }; 32 33 /** 34 * Handles network events from the content process 35 * This currently only handles events for requests (js/css) blocked by CSP. 36 */ 37 class NetworkEventContentWatcher { 38 /** 39 * Start watching for all network events related to a given Target Actor. 40 * 41 * @param TargetActor targetActor 42 * The target actor in the content process from which we should 43 * observe network events. 44 * @param Object options 45 * Dictionary object with following attributes: 46 * - onAvailable: mandatory function 47 * This will be called for each resource. 48 * - onUpdated: optional function 49 * This would be called multiple times for each resource. 50 */ 51 async watch(targetActor, { onAvailable, onUpdated }) { 52 // Map from channelId to network event objects. 53 this.networkEvents = new Map(); 54 55 this.targetActor = targetActor; 56 this.onAvailable = onAvailable; 57 this.onUpdated = onUpdated; 58 59 Services.obs.addObserver( 60 this.httpFailedOpeningRequest, 61 "http-on-failed-opening-request" 62 ); 63 64 Services.obs.addObserver( 65 this.httpOnResourceCacheResponse, 66 "http-on-resource-cache-response" 67 ); 68 69 Services.obs.addObserver(this.onDataChannelOpened, "data-channel-opened"); 70 } 71 /** 72 * Allows clearing of network events 73 */ 74 clear() { 75 this.networkEvents.clear(); 76 } 77 78 httpFailedOpeningRequest = (subject, topic) => { 79 if ( 80 topic != "http-on-failed-opening-request" || 81 !(subject instanceof Ci.nsIHttpChannel) 82 ) { 83 const channel = subject.QueryInterface(Ci.nsIChannel); 84 console.warn( 85 `httpFailedOpeningRequest triggered on non-nsIHttpChannel for uri: ${channel.URI.spec}` 86 ); 87 return; 88 } 89 90 const channel = subject.QueryInterface(Ci.nsIHttpChannel); 91 92 // Ignore preload requests to avoid duplicity request entries in 93 // the Network panel. If a preload fails (for whatever reason) 94 // then the platform kicks off another 'real' request. 95 if (lazy.NetworkUtils.isPreloadRequest(channel)) { 96 return; 97 } 98 99 if ( 100 !lazy.NetworkUtils.matchRequest(channel, { 101 targetActor: this.targetActor, 102 }) 103 ) { 104 return; 105 } 106 107 this.onNetworkEventAvailable(channel, { 108 networkEventOptions: { 109 blockedReason: channel.loadInfo.requestBlockingReason, 110 }, 111 type: RESOURCE_TYPES.BLOCKED, 112 }); 113 }; 114 115 httpOnResourceCacheResponse = (subject, topic) => { 116 if ( 117 topic != "http-on-resource-cache-response" || 118 !(subject instanceof Ci.nsIHttpChannel) 119 ) { 120 return; 121 } 122 123 const channel = subject.QueryInterface(Ci.nsIHttpChannel); 124 125 if ( 126 !lazy.NetworkUtils.matchRequest(channel, { 127 targetActor: this.targetActor, 128 }) 129 ) { 130 return; 131 } 132 133 if ( 134 channel.loadInfo?.externalContentPolicyType !== 135 Ci.nsIContentPolicy.TYPE_SCRIPT 136 ) { 137 // For images and stylesheets from the cache, only one network request 138 // should be created per URI. 139 // 140 // For scripts from the cache, multiple network requests should be 141 // created. 142 const hasURI = Array.from(this.networkEvents.values()).some( 143 networkEvent => networkEvent.uri === channel.URI.spec 144 ); 145 146 if (hasURI) { 147 return; 148 } 149 } 150 151 this.onNetworkEventAvailable(channel, { 152 fromCache: true, 153 networkEventOptions: {}, 154 type: RESOURCE_TYPES.CACHED, 155 }); 156 }; 157 158 onDataChannelOpened = (subject, topic) => { 159 if ( 160 topic != "data-channel-opened" || 161 !(subject instanceof Ci.nsIDataChannel) 162 ) { 163 return; 164 } 165 166 const channel = subject.QueryInterface(Ci.nsIDataChannel); 167 channel.QueryInterface(Ci.nsIIdentChannel); 168 channel.QueryInterface(Ci.nsIChannel); 169 170 if (channel.isDocument) { 171 // Navigation data channels are available in the parent process and will 172 // be monitored there. 173 return; 174 } 175 176 if ( 177 !lazy.NetworkUtils.matchRequest(channel, { 178 targetActor: this.targetActor, 179 }) 180 ) { 181 return; 182 } 183 184 this.onNetworkEventAvailable(channel, { 185 fromCache: false, 186 networkEventOptions: {}, 187 type: RESOURCE_TYPES.DATA_CHANNEL, 188 }); 189 }; 190 191 onNetworkEventAvailable(channel, { fromCache, networkEventOptions, type }) { 192 const networkEventActor = new NetworkEventActor( 193 this.targetActor.conn, 194 this.targetActor.sessionContext, 195 { 196 onNetworkEventUpdate: this.onNetworkEventUpdate.bind(this), 197 onNetworkEventDestroy: this.onNetworkEventDestroyed.bind(this), 198 }, 199 networkEventOptions, 200 channel 201 ); 202 this.targetActor.manage(networkEventActor); 203 204 const resource = networkEventActor.asResource(); 205 206 const networkEvent = { 207 browsingContextID: resource.browsingContextID, 208 innerWindowId: resource.innerWindowId, 209 resourceId: resource.resourceId, 210 receivedUpdates: [], 211 resourceUpdates: {}, 212 uri: channel.URI.spec, 213 }; 214 215 // Requests already come with request cookies and headers, so those 216 // should always be considered as available. But the client still 217 // heavily relies on those `Available` flags to fetch additional data, 218 // so it is better to keep them for consistency. 219 220 // Set the flags on the resource so that the front-end can fetch 221 // and display request headers and cookies details asap. 222 lazy.NetworkUtils.setEventAsAvailable(resource, [ 223 lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_HEADERS, 224 lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_COOKIES, 225 ]); 226 227 this.networkEvents.set(resource.resourceId, networkEvent); 228 229 this.onAvailable([resource]); 230 231 networkEventActor.addCacheDetails({ fromCache }); 232 if (type == RESOURCE_TYPES.BLOCKED) { 233 lazy.NetworkUtils.setEventAsAvailable(networkEvent.resourceUpdates, [ 234 lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_END, 235 ]); 236 this._emitUpdate(networkEvent); 237 } else if (type == RESOURCE_TYPES.CACHED) { 238 networkEventActor.addResponseStart({ channel, fromCache: true }); 239 networkEventActor.addEventTimings( 240 0 /* totalTime */, 241 {} /* timings */, 242 {} /* offsets */ 243 ); 244 networkEventActor.addServerTimings({}); 245 networkEventActor.addResponseContent({ 246 mimeType: channel.contentType, 247 size: channel.contentLength, 248 text: "", 249 transferredSize: 0, 250 }); 251 networkEventActor.addResponseContentComplete({}); 252 } else if (type == RESOURCE_TYPES.DATA_CHANNEL) { 253 lazy.NetworkUtils.handleDataChannel(channel, networkEventActor); 254 } 255 } 256 257 onNetworkEventUpdate(updateResource) { 258 const networkEvent = this.networkEvents.get(updateResource.resourceId); 259 260 if (!networkEvent) { 261 return; 262 } 263 264 const { NETWORK_EVENT_TYPES } = lazy.NetworkUtils; 265 const { resourceUpdates, receivedUpdates } = networkEvent; 266 267 switch (updateResource.updateType) { 268 case NETWORK_EVENT_TYPES.CACHE_DETAILS: 269 resourceUpdates.fromCache = updateResource.fromCache; 270 resourceUpdates.fromServiceWorker = updateResource.fromServiceWorker; 271 break; 272 case NETWORK_EVENT_TYPES.RESPONSE_START: { 273 // For cached image requests channel.responseStatus is set to 200 as 274 // expected. However responseStatusText is empty. In this case fallback 275 // to the expected statusText "OK". 276 let statusText = updateResource.statusText; 277 if (!statusText && updateResource.status === "200") { 278 statusText = "OK"; 279 } 280 resourceUpdates.httpVersion = updateResource.httpVersion; 281 resourceUpdates.status = updateResource.status; 282 resourceUpdates.statusText = statusText; 283 resourceUpdates.remoteAddress = updateResource.remoteAddress; 284 resourceUpdates.remotePort = updateResource.remotePort; 285 resourceUpdates.waitingTime = updateResource.waitingTime; 286 287 lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [ 288 NETWORK_EVENT_TYPES.RESPONSE_COOKIES, 289 NETWORK_EVENT_TYPES.RESPONSE_HEADERS, 290 ]); 291 break; 292 } 293 case NETWORK_EVENT_TYPES.RESPONSE_CONTENT: 294 resourceUpdates.contentSize = updateResource.contentSize; 295 resourceUpdates.mimeType = updateResource.mimeType; 296 resourceUpdates.transferredSize = updateResource.transferredSize; 297 break; 298 case NETWORK_EVENT_TYPES.EVENT_TIMINGS: 299 resourceUpdates.totalTime = updateResource.totalTime; 300 break; 301 } 302 303 lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [ 304 updateResource.updateType, 305 ]); 306 307 receivedUpdates.push(updateResource.updateType); 308 309 // Here we explicitly call all three `add` helpers on each network event 310 // actor so in theory we could check only the last one to be called, ie 311 // responseContent. 312 const isResponseComplete = 313 receivedUpdates.includes(NETWORK_EVENT_TYPES.RESPONSE_START) && 314 receivedUpdates.includes(NETWORK_EVENT_TYPES.RESPONSE_CONTENT_COMPLETE) && 315 receivedUpdates.includes(NETWORK_EVENT_TYPES.EVENT_TIMINGS); 316 317 if (isResponseComplete) { 318 // Lets add an event to clearly define the last update expected to be 319 // emitted. There will be no more updates after this. 320 lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [ 321 NETWORK_EVENT_TYPES.RESPONSE_END, 322 ]); 323 } 324 325 if ( 326 updateResource.updateType == NETWORK_EVENT_TYPES.RESPONSE_START || 327 updateResource.updateType == NETWORK_EVENT_TYPES.RESPONSE_CONTENT || 328 isResponseComplete 329 ) { 330 this._emitUpdate(networkEvent); 331 // clean up already sent updates 332 networkEvent.resourceUpdates = {}; 333 } 334 } 335 336 _emitUpdate(networkEvent) { 337 this.onUpdated([ 338 { 339 resourceId: networkEvent.resourceId, 340 resourceUpdates: networkEvent.resourceUpdates, 341 browsingContextID: networkEvent.browsingContextID, 342 innerWindowId: networkEvent.innerWindowId, 343 }, 344 ]); 345 } 346 347 onNetworkEventDestroyed(channelId) { 348 if (this.networkEvents.has(channelId)) { 349 this.networkEvents.delete(channelId); 350 } 351 } 352 353 destroy() { 354 this.clear(); 355 Services.obs.removeObserver( 356 this.httpFailedOpeningRequest, 357 "http-on-failed-opening-request" 358 ); 359 360 Services.obs.removeObserver( 361 this.httpOnResourceCacheResponse, 362 "http-on-resource-cache-response" 363 ); 364 365 Services.obs.removeObserver( 366 this.onDataChannelOpened, 367 "data-channel-opened" 368 ); 369 } 370 } 371 372 module.exports = NetworkEventContentWatcher;