NetworkObserver.sys.mjs (42759B)
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 /** 6 * NetworkObserver is the main class in DevTools to observe network requests 7 * out of many events fired by the platform code. 8 */ 9 10 // Enable logging all platform events this module listen to 11 const DEBUG_PLATFORM_EVENTS = false; 12 // Enables defining criteria to filter the logs 13 const DEBUG_PLATFORM_EVENTS_FILTER = () => { 14 // e.g return eventName == "HTTP_TRANSACTION:REQUEST_HEADER" && channel.URI.spec == "http://foo.com"; 15 return true; 16 }; 17 18 const lazy = {}; 19 20 import { DevToolsInfaillibleUtils } from "resource://devtools/shared/DevToolsInfaillibleUtils.sys.mjs"; 21 22 ChromeUtils.defineESModuleGetters( 23 lazy, 24 { 25 ChannelMap: 26 "resource://devtools/shared/network-observer/ChannelMap.sys.mjs", 27 NetUtil: "resource://gre/modules/NetUtil.sys.mjs", 28 NetworkAuthListener: 29 "resource://devtools/shared/network-observer/NetworkAuthListener.sys.mjs", 30 NetworkHelper: 31 "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs", 32 NetworkOverride: 33 "resource://devtools/shared/network-observer/NetworkOverride.sys.mjs", 34 NetworkResponseListener: 35 "resource://devtools/shared/network-observer/NetworkResponseListener.sys.mjs", 36 NetworkTimings: 37 "resource://devtools/shared/network-observer/NetworkTimings.sys.mjs", 38 NetworkThrottleManager: 39 "resource://devtools/shared/network-observer/NetworkThrottleManager.sys.mjs", 40 NetworkUtils: 41 "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs", 42 wildcardToRegExp: 43 "resource://devtools/shared/network-observer/WildcardToRegexp.sys.mjs", 44 }, 45 { global: "contextual" } 46 ); 47 48 const gActivityDistributor = Cc[ 49 "@mozilla.org/network/http-activity-distributor;1" 50 ].getService(Ci.nsIHttpActivityDistributor); 51 52 function logPlatformEvent(eventName, channel, message = "") { 53 if (!DEBUG_PLATFORM_EVENTS) { 54 return; 55 } 56 if (DEBUG_PLATFORM_EVENTS_FILTER(eventName, channel)) { 57 dump( 58 `[netmonitor] ${channel.channelId} - ${eventName} ${message} - ${channel.URI.spec}\n` 59 ); 60 } 61 } 62 63 // The maximum uint32 value. 64 const PR_UINT32_MAX = 4294967295; 65 66 const HTTP_TRANSACTION_CODES = { 67 0x5001: "REQUEST_HEADER", 68 0x5002: "REQUEST_BODY_SENT", 69 0x5003: "RESPONSE_START", 70 0x5004: "RESPONSE_HEADER", 71 0x5005: "RESPONSE_COMPLETE", 72 0x5006: "TRANSACTION_CLOSE", 73 0x500c: "EARLYHINT_RESPONSE_HEADER", 74 75 0x4b0003: "STATUS_RESOLVING", 76 0x4b000b: "STATUS_RESOLVED", 77 0x4b0007: "STATUS_CONNECTING_TO", 78 0x4b0004: "STATUS_CONNECTED_TO", 79 0x4b0005: "STATUS_SENDING_TO", 80 0x4b000a: "STATUS_WAITING_FOR", 81 0x4b0006: "STATUS_RECEIVING_FROM", 82 0x4b000c: "STATUS_TLS_STARTING", 83 0x4b000d: "STATUS_TLS_ENDING", 84 }; 85 86 const HTTP_DOWNLOAD_ACTIVITIES = [ 87 gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_START, 88 gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER, 89 gActivityDistributor.ACTIVITY_SUBTYPE_PROXY_RESPONSE_HEADER, 90 gActivityDistributor.ACTIVITY_SUBTYPE_EARLYHINT_RESPONSE_HEADER, 91 gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE, 92 gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE, 93 ]; 94 95 /** 96 * The network monitor uses the nsIHttpActivityDistributor to monitor network 97 * requests. The nsIObserverService is also used for monitoring 98 * http-on-examine-response notifications. All network request information is 99 * routed to the remote Web Console. 100 * 101 * @class 102 * @param {object} options 103 * @param {Function(nsIChannel): boolean} options.ignoreChannelFunction 104 * This function will be called for every detected channel to decide if it 105 * should be monitored or not. 106 * @param {Function(NetworkEvent): owner} options.onNetworkEvent 107 * This method is invoked once for every new network request with two 108 * arguments: 109 * - {Object} networkEvent: object created by NetworkUtils:createNetworkEvent, 110 * containing initial network request information as an argument. 111 * - {nsIChannel} channel: the channel for which the request was detected 112 * 113 * `onNetworkEvent()` must return an "owner" object which holds several add*() 114 * methods which are used to add further network request/response information. 115 */ 116 export class NetworkObserver { 117 /** 118 * Map of URL patterns to RegExp 119 * 120 * @type {Map} 121 */ 122 #blockedURLs = new Map(); 123 124 /** 125 * Map of URL to local file path in order to redirect URL 126 * to local file overrides. 127 * 128 * This will replace the content of some request with the content of local files. 129 */ 130 #overrides = new Map(); 131 132 /** 133 * Used by NetworkHelper.parseSecurityInfo to skip decoding known certificates. 134 * 135 * @type {Map} 136 */ 137 #decodedCertificateCache = new Map(); 138 /** 139 * Whether the consumer supports listening and handling auth prompts. 140 * 141 * @type {boolean} 142 */ 143 #authPromptListenerEnabled = false; 144 /** 145 * See constructor argument of the same name. 146 * 147 * @type {Function} 148 */ 149 #ignoreChannelFunction; 150 /** 151 * Used to store channels intercepted for service-worker requests. 152 * 153 * @type {WeakSet} 154 */ 155 #interceptedChannels = new WeakSet(); 156 /** 157 * Explicit flag to check if this observer was already destroyed. 158 * 159 * @type {boolean} 160 */ 161 #isDestroyed = false; 162 /** 163 * See constructor argument of the same name. 164 * 165 * @type {Function} 166 */ 167 #onNetworkEvent; 168 /** 169 * Object that holds the activity objects for ongoing requests. 170 * 171 * @type {ChannelMap} 172 */ 173 #openRequests = new lazy.ChannelMap(); 174 /** 175 * The maximum size (in bytes) of the individual response bodies to be stored. 176 * 177 * @type {number} 178 */ 179 #responseBodyLimit = 0; 180 /** 181 * Network response bodies are piped through a buffer of the given size 182 * (in bytes). 183 * 184 * @type {number} 185 */ 186 #responsePipeSegmentSize = Services.prefs.getIntPref( 187 "network.buffer.cache.size" 188 ); 189 /** 190 * Whether to save the bodies of network requests and responses. 191 * 192 * @type {boolean} 193 */ 194 #saveRequestAndResponseBodies = true; 195 /** 196 * Whether response bodies should be decoded or not. 197 * 198 * @type {boolean} 199 */ 200 #decodeResponseBodies = true; 201 /** 202 * Throttling configuration, see constructor of NetworkThrottleManager 203 * 204 * @type {object} 205 */ 206 #throttleData = null; 207 /** 208 * NetworkThrottleManager instance, created when a valid throttleData is set. 209 * 210 * @type {NetworkThrottleManager} 211 */ 212 #throttler = null; 213 214 constructor(options = {}) { 215 const { 216 decodeResponseBodies, 217 ignoreChannelFunction, 218 onNetworkEvent, 219 responseBodyLimit, 220 } = options; 221 222 if (typeof ignoreChannelFunction !== "function") { 223 throw new Error( 224 `Expected "ignoreChannelFunction" to be a function, got ${ignoreChannelFunction} (${typeof ignoreChannelFunction})` 225 ); 226 } 227 228 if (typeof onNetworkEvent !== "function") { 229 throw new Error( 230 `Expected "onNetworkEvent" to be a function, got ${onNetworkEvent} (${typeof onNetworkEvent})` 231 ); 232 } 233 234 this.#ignoreChannelFunction = ignoreChannelFunction; 235 this.#onNetworkEvent = onNetworkEvent; 236 237 // Set decodeResponseBodies if provided, otherwise default to "true". 238 if (typeof decodeResponseBodies === "boolean") { 239 this.#decodeResponseBodies = decodeResponseBodies; 240 } 241 242 // Set the provided responseBodyLimit if any, otherwise use the default "0". 243 if (typeof responseBodyLimit === "number") { 244 this.#responseBodyLimit = responseBodyLimit; 245 } 246 247 // Start all platform observers. 248 if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { 249 gActivityDistributor.addObserver(this); 250 gActivityDistributor.observeProxyResponse = true; 251 252 Services.obs.addObserver( 253 this.#httpResponseExaminer, 254 "http-on-examine-response" 255 ); 256 Services.obs.addObserver( 257 this.#httpResponseExaminer, 258 "http-on-examine-cached-response" 259 ); 260 Services.obs.addObserver( 261 this.#httpModifyExaminer, 262 "http-on-modify-request" 263 ); 264 Services.obs.addObserver( 265 this.#fileChannelExaminer, 266 "file-channel-opened" 267 ); 268 Services.obs.addObserver( 269 this.#dataChannelExaminer, 270 "data-channel-opened" 271 ); 272 Services.obs.addObserver( 273 this.#httpBeforeConnect, 274 "http-on-before-connect" 275 ); 276 277 Services.obs.addObserver(this.#httpStopRequest, "http-on-stop-request"); 278 } else { 279 Services.obs.addObserver( 280 this.#httpFailedOpening, 281 "http-on-failed-opening-request" 282 ); 283 } 284 // In child processes, only watch for service worker requests 285 // everything else only happens in the parent process 286 Services.obs.addObserver( 287 this.#serviceWorkerRequest, 288 "service-worker-synthesized-response" 289 ); 290 } 291 292 setAuthPromptListenerEnabled(enabled) { 293 this.#authPromptListenerEnabled = enabled; 294 } 295 296 /** 297 * Update the maximum size in bytes that can be collected for network response 298 * bodies. Responses for which the NetworkResponseListener has already been 299 * created will not be using the new limit, only later responses will be 300 * affected. 301 * 302 * @param {number} responseBodyLimit 303 * The new responseBodyLimit to use. 304 */ 305 setResponseBodyLimit(responseBodyLimit) { 306 this.#responseBodyLimit = responseBodyLimit; 307 } 308 309 setSaveRequestAndResponseBodies(save) { 310 this.#saveRequestAndResponseBodies = save; 311 } 312 313 getThrottleData() { 314 return this.#throttleData; 315 } 316 317 /** 318 * Update the network throttling configuration. 319 * 320 * @param {object|null} value 321 * The network throttling configuration object, or null if throttling 322 * should be disabled. 323 */ 324 setThrottleData(value) { 325 this.#throttleData = value; 326 327 // If value is null, the user is disabling throttling, destroy the previous 328 // throttler. 329 if (this.#throttler && value === null) { 330 this.#throttler.destroy(); 331 } 332 this.#throttler = null; 333 } 334 335 #getThrottler() { 336 if (this.#throttleData !== null && this.#throttler === null) { 337 this.#throttler = new lazy.NetworkThrottleManager(this.#throttleData); 338 } 339 return this.#throttler; 340 } 341 342 #serviceWorkerRequest = DevToolsInfaillibleUtils.makeInfallible( 343 (subject, topic) => { 344 const channel = subject.QueryInterface(Ci.nsIHttpChannel); 345 346 if (this.#ignoreChannelFunction(channel)) { 347 return; 348 } 349 350 logPlatformEvent(topic, channel); 351 352 this.#interceptedChannels.add(subject); 353 354 // Service workers never fire http-on-examine-cached-response, so fake one. 355 this.#httpResponseExaminer(channel, "http-on-examine-cached-response"); 356 } 357 ); 358 359 /** 360 * Observes for http-on-failed-opening-request notification to catch any 361 * channels for which asyncOpen has synchronously failed. This is the only 362 * place to catch early security check failures. 363 */ 364 #httpFailedOpening = DevToolsInfaillibleUtils.makeInfallible( 365 (subject, topic) => { 366 if ( 367 this.#isDestroyed || 368 topic != "http-on-failed-opening-request" || 369 !(subject instanceof Ci.nsIHttpChannel) 370 ) { 371 return; 372 } 373 374 const channel = subject.QueryInterface(Ci.nsIHttpChannel); 375 if (this.#ignoreChannelFunction(channel)) { 376 return; 377 } 378 379 logPlatformEvent(topic, channel); 380 381 // Ignore preload requests to avoid duplicity request entries in 382 // the Network panel. If a preload fails (for whatever reason) 383 // then the platform kicks off another 'real' request. 384 if (lazy.NetworkUtils.isPreloadRequest(channel)) { 385 return; 386 } 387 388 this.#httpResponseExaminer(subject, topic); 389 } 390 ); 391 392 #httpBeforeConnect = DevToolsInfaillibleUtils.makeInfallible( 393 (subject, topic) => { 394 if ( 395 this.#isDestroyed || 396 topic != "http-on-before-connect" || 397 !(subject instanceof Ci.nsIHttpChannel) 398 ) { 399 return; 400 } 401 402 const channel = subject.QueryInterface(Ci.nsIHttpChannel); 403 if (this.#ignoreChannelFunction(channel)) { 404 return; 405 } 406 407 // Here we create the network event from an early platform notification. 408 // Additional details about the event will be provided using the various 409 // callbacks on the network event owner. 410 const httpActivity = this.#createOrGetActivityObject(channel); 411 this.#createNetworkEvent(httpActivity); 412 413 // Handle overrides in http-on-before-connect because we need to redirect 414 // the request to the override before reaching the server. 415 this.#checkForContentOverride(httpActivity); 416 } 417 ); 418 419 #httpStopRequest = DevToolsInfaillibleUtils.makeInfallible( 420 (subject, topic) => { 421 if ( 422 this.#isDestroyed || 423 topic != "http-on-stop-request" || 424 !(subject instanceof Ci.nsIHttpChannel) 425 ) { 426 return; 427 } 428 429 const channel = subject.QueryInterface(Ci.nsIHttpChannel); 430 if (this.#ignoreChannelFunction(channel)) { 431 return; 432 } 433 434 logPlatformEvent(topic, channel); 435 436 const httpActivity = this.#createOrGetActivityObject(channel); 437 if (httpActivity.owner) { 438 // Try extracting server timings. Note that they will be sent to the client 439 // in the `_onTransactionClose` method together with network event timings. 440 const serverTimings = 441 lazy.NetworkTimings.extractServerTimings(httpActivity); 442 httpActivity.owner.addServerTimings(serverTimings); 443 444 // If the owner isn't set we need to create the network event and send 445 // it to the client. This happens in case where: 446 // - the request has been blocked (e.g. CORS) and "http-on-stop-request" is the first notification. 447 // - the NetworkObserver is start *after* the request started and we only receive the http-stop notification, 448 // but that doesn't mean the request is blocked, so check for its status. 449 } else if (Components.isSuccessCode(channel.status)) { 450 // Do not pass any blocked reason, as this request is just fine. 451 // Bug 1489217 - Prevent watching for this request response content, 452 // as this request is already running, this is too late to watch for it. 453 this.#createNetworkEvent(httpActivity, { 454 inProgressRequest: true, 455 }); 456 } else { 457 // Handles any early blockings e.g by Web Extensions or by CORS 458 const { extension, blockedReason } = lazy.NetworkUtils.getBlockedReason( 459 channel, 460 httpActivity.fromCache 461 ); 462 this.#createNetworkEvent(httpActivity, { 463 blockedReason, 464 extension, 465 }); 466 } 467 } 468 ); 469 470 /** 471 * Check if the current channel has its content being overriden 472 * by the content of some local file. 473 */ 474 #checkForContentOverride(httpActivity) { 475 const channel = httpActivity.channel; 476 const overridePath = this.#overrides.get(channel.URI.spec); 477 if (!overridePath) { 478 return false; 479 } 480 481 dump(" Override " + channel.URI.spec + " to " + overridePath + "\n"); 482 try { 483 lazy.NetworkOverride.overrideChannelWithFilePath(channel, overridePath); 484 // Handle the activity as being from the cache to avoid looking up 485 // typical information from the http channel, which would error for 486 // overridden channels. 487 httpActivity.fromCache = true; 488 httpActivity.isOverridden = true; 489 } catch (e) { 490 dump("Exception while trying to override request content: " + e + "\n"); 491 } 492 493 return true; 494 } 495 496 /** 497 * Observe notifications for the http-on-examine-response topic, coming from 498 * the nsIObserverService. 499 * 500 * @private 501 * @param nsIHttpChannel subject 502 * @param string topic 503 * @returns void 504 */ 505 #httpResponseExaminer = DevToolsInfaillibleUtils.makeInfallible( 506 (subject, topic) => { 507 // The httpResponseExaminer is used to retrieve the uncached response 508 // headers. 509 if ( 510 this.#isDestroyed || 511 (topic != "http-on-examine-response" && 512 topic != "http-on-examine-cached-response" && 513 topic != "http-on-failed-opening-request") || 514 !(subject instanceof Ci.nsIHttpChannel) || 515 !(subject instanceof Ci.nsIClassifiedChannel) 516 ) { 517 return; 518 } 519 520 const blockedOrFailed = topic === "http-on-failed-opening-request"; 521 522 subject.QueryInterface(Ci.nsIClassifiedChannel); 523 const channel = subject.QueryInterface(Ci.nsIHttpChannel); 524 525 if (this.#ignoreChannelFunction(channel)) { 526 return; 527 } 528 529 logPlatformEvent( 530 topic, 531 subject, 532 blockedOrFailed 533 ? "blockedOrFailed:" + channel.loadInfo.requestBlockingReason 534 : channel.responseStatus 535 ); 536 537 channel.QueryInterface(Ci.nsIHttpChannelInternal); 538 539 // Retrieve or create the http activity. 540 const httpActivity = this.#createOrGetActivityObject(channel); 541 542 // Preserve the initial responseStatus which can be modified over the 543 // course of the network request, especially for 304 Not Modified channels 544 // which will be replaced by the original 200 channel from the cache. 545 // (this is the correct behavior according to the fetch spec). 546 httpActivity.responseStatus = httpActivity.channel.responseStatus; 547 548 if (topic === "http-on-examine-cached-response") { 549 this.#handleExamineCachedResponse(httpActivity); 550 } else if (topic === "http-on-failed-opening-request") { 551 this.#handleFailedOpeningRequest(httpActivity); 552 } 553 554 if (httpActivity.owner) { 555 httpActivity.owner.addResponseStart({ 556 channel: httpActivity.channel, 557 fromCache: httpActivity.fromCache || httpActivity.fromServiceWorker, 558 fromServiceWorker: httpActivity.fromServiceWorker, 559 rawHeaders: httpActivity.responseRawHeaders, 560 proxyResponseRawHeaders: httpActivity.proxyResponseRawHeaders, 561 earlyHintsResponseRawHeaders: 562 httpActivity.earlyHintsResponseRawHeaders, 563 }); 564 } 565 } 566 ); 567 568 #handleExamineCachedResponse(httpActivity) { 569 const channel = httpActivity.channel; 570 571 const fromServiceWorker = this.#interceptedChannels.has(channel); 572 const fromCache = !fromServiceWorker; 573 574 // Set the cache flags on the httpActivity object, they will be used later 575 // on during the lifecycle of the channel. 576 httpActivity.fromCache = fromCache; 577 httpActivity.fromServiceWorker = fromServiceWorker; 578 579 // Service worker requests emits cached-response notification on non-e10s, 580 // and we fake one on e10s. 581 this.#interceptedChannels.delete(channel); 582 583 if (!httpActivity.owner) { 584 // If this is a cached response (which are also emitted by service worker requests), 585 // there never was a request event so we need to construct one here 586 // so the frontend gets all the expected events. 587 this.#createNetworkEvent(httpActivity); 588 } 589 590 httpActivity.owner.addCacheDetails({ 591 fromCache: httpActivity.fromCache, 592 fromServiceWorker: httpActivity.fromServiceWorker, 593 }); 594 595 // We need to send the request body to the frontend for 596 // the faked (cached/service worker request) event. 597 this.#prepareRequestBody(httpActivity); 598 this.#sendRequestBody(httpActivity); 599 600 // There also is never any timing events, so we can fire this 601 // event with zeroed out values. 602 const timings = lazy.NetworkTimings.extractHarTimings(httpActivity); 603 const serverTimings = 604 lazy.NetworkTimings.extractServerTimings(httpActivity); 605 const serviceWorkerTimings = 606 lazy.NetworkTimings.extractServiceWorkerTimings(httpActivity); 607 608 httpActivity.owner.addServerTimings(serverTimings); 609 httpActivity.owner.addServiceWorkerTimings(serviceWorkerTimings); 610 httpActivity.owner.addEventTimings( 611 timings.total, 612 timings.timings, 613 timings.offsets 614 ); 615 } 616 617 #handleFailedOpeningRequest(httpActivity) { 618 const channel = httpActivity.channel; 619 const { blockedReason } = lazy.NetworkUtils.getBlockedReason( 620 channel, 621 httpActivity.fromCache 622 ); 623 624 this.#createNetworkEvent(httpActivity, { 625 blockedReason, 626 }); 627 } 628 629 /** 630 * Observe notifications for the http-on-modify-request topic, coming from 631 * the nsIObserverService. 632 * 633 * @private 634 * @param nsIHttpChannel aSubject 635 * @returns void 636 */ 637 #httpModifyExaminer = DevToolsInfaillibleUtils.makeInfallible(subject => { 638 const throttler = this.#getThrottler(); 639 if (throttler) { 640 const channel = subject.QueryInterface(Ci.nsIHttpChannel); 641 if (this.#ignoreChannelFunction(channel)) { 642 return; 643 } 644 logPlatformEvent("http-on-modify-request", channel); 645 646 // Read any request body here, before it is throttled. 647 const httpActivity = this.#createOrGetActivityObject(channel); 648 this.#prepareRequestBody(httpActivity); 649 throttler.manageUpload(channel); 650 } 651 }); 652 653 #dataChannelExaminer = DevToolsInfaillibleUtils.makeInfallible( 654 (subject, topic) => { 655 if ( 656 topic != "data-channel-opened" || 657 !(subject instanceof Ci.nsIDataChannel) 658 ) { 659 return; 660 } 661 const channel = subject.QueryInterface(Ci.nsIDataChannel); 662 channel.QueryInterface(Ci.nsIIdentChannel); 663 channel.QueryInterface(Ci.nsIChannel); 664 665 if (this.#ignoreChannelFunction(channel)) { 666 return; 667 } 668 669 logPlatformEvent(topic, channel); 670 671 const networkEventActor = this.#onNetworkEvent({}, channel, true); 672 lazy.NetworkUtils.handleDataChannel(channel, networkEventActor); 673 } 674 ); 675 676 /** 677 * Observe notifications for the file-channel-opened topic 678 * 679 * @private 680 * @param nsIFileChannel subject 681 * @param string topic 682 * @returns void 683 */ 684 #fileChannelExaminer = DevToolsInfaillibleUtils.makeInfallible( 685 (subject, topic) => { 686 if ( 687 this.#isDestroyed || 688 topic != "file-channel-opened" || 689 !(subject instanceof Ci.nsIFileChannel) 690 ) { 691 return; 692 } 693 const channel = subject.QueryInterface(Ci.nsIFileChannel); 694 channel.QueryInterface(Ci.nsIIdentChannel); 695 channel.QueryInterface(Ci.nsIChannel); 696 697 if (this.#ignoreChannelFunction(channel)) { 698 return; 699 } 700 701 logPlatformEvent(topic, channel); 702 const owner = this.#onNetworkEvent({}, channel, true); 703 704 owner.addResponseStart({ 705 channel, 706 fromCache: false, 707 rawHeaders: "", 708 }); 709 710 // For file URLs we can not set up a stream listener as for http, 711 // so we have to create a response manually and complete it. 712 const response = { 713 contentCharset: channel.contentCharset, 714 contentLength: channel.contentLength, 715 contentType: channel.contentType, 716 mimeType: lazy.NetworkHelper.addCharsetToMimeType( 717 channel.contentType, 718 channel.contentCharset 719 ), 720 // Same as for cached responses, the transferredSize for file URLs 721 // should be 0 regardless of the actual size of the response. 722 transferredSize: 0, 723 }; 724 725 // For file URIs all timings can be set to zero. 726 const result = lazy.NetworkTimings.getEmptyHARTimings(); 727 owner.addEventTimings(result.total, result.timings, result.offsets); 728 729 const fstream = Cc[ 730 "@mozilla.org/network/file-input-stream;1" 731 ].createInstance(Ci.nsIFileInputStream); 732 fstream.init(channel.file, -1, 0, 0); 733 response.text = lazy.NetUtil.readInputStreamToString( 734 fstream, 735 fstream.available() 736 ); 737 fstream.close(); 738 739 // Set the bodySize to the current response.text.length 740 response.bodySize = response.text.length; 741 742 if ( 743 !response.mimeType || 744 !lazy.NetworkHelper.isTextMimeType(response.mimeType) 745 ) { 746 response.encoding = "base64"; 747 try { 748 response.text = btoa(response.text); 749 } catch (err) { 750 // Ignore. 751 } 752 } 753 754 // Set the size/decodedBodySize to the updated response.text.length, after 755 // potentially decoding the data. 756 // NB: `size` is used by DevTools, while WebDriverBiDi relies on 757 // decodedBodySize, because the name is more explicit. 758 response.decodedBodySize = response.text.length; 759 response.size = response.decodedBodySize; 760 761 // Security information is not relevant for file channel, but it should 762 // not be considered as insecure either. Set empty string as security 763 // state. 764 owner.addSecurityInfo({ state: "" }); 765 owner.addResponseContent(response); 766 owner.addResponseContentComplete({}); 767 } 768 ); 769 770 /** 771 * A helper function for observeActivity. This does whatever work 772 * is required by a particular http activity event. Arguments are 773 * the same as for observeActivity. 774 */ 775 #dispatchActivity( 776 httpActivity, 777 channel, 778 activityType, 779 activitySubtype, 780 timestamp, 781 extraSizeData, 782 extraStringData 783 ) { 784 // Store the time information for this activity subtype. 785 if (activitySubtype in HTTP_TRANSACTION_CODES) { 786 const stage = HTTP_TRANSACTION_CODES[activitySubtype]; 787 if (stage in httpActivity.timings) { 788 httpActivity.timings[stage].last = timestamp; 789 } else { 790 httpActivity.timings[stage] = { 791 first: timestamp, 792 last: timestamp, 793 }; 794 } 795 } 796 switch (activitySubtype) { 797 case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT: 798 this.#prepareRequestBody(httpActivity); 799 this.#sendRequestBody(httpActivity); 800 break; 801 case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER: 802 httpActivity.responseRawHeaders = extraStringData; 803 httpActivity.headersSize = extraStringData.length; 804 break; 805 case gActivityDistributor.ACTIVITY_SUBTYPE_PROXY_RESPONSE_HEADER: 806 httpActivity.proxyResponseRawHeaders = extraStringData; 807 break; 808 case gActivityDistributor.ACTIVITY_SUBTYPE_EARLYHINT_RESPONSE_HEADER: 809 httpActivity.earlyHintsResponseRawHeaders = extraStringData; 810 httpActivity.headersSize = extraStringData.length; 811 break; 812 case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE: 813 this.#onTransactionClose(httpActivity); 814 break; 815 default: 816 break; 817 } 818 } 819 820 getActivityTypeString(activityType, activitySubtype) { 821 if ( 822 activityType === Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_SOCKET_TRANSPORT 823 ) { 824 for (const name in Ci.nsISocketTransport) { 825 if (Ci.nsISocketTransport[name] === activitySubtype) { 826 return "SOCKET_TRANSPORT:" + name; 827 } 828 } 829 } else if ( 830 activityType === Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION 831 ) { 832 for (const name in Ci.nsIHttpActivityObserver) { 833 if (Ci.nsIHttpActivityObserver[name] === activitySubtype) { 834 return "HTTP_TRANSACTION:" + name.replace("ACTIVITY_SUBTYPE_", ""); 835 } 836 } 837 } 838 return "unexpected-activity-types:" + activityType + ":" + activitySubtype; 839 } 840 841 /** 842 * Begin observing HTTP traffic that originates inside the current tab. 843 * 844 * @see https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIHttpActivityObserver 845 * 846 * @param nsIHttpChannel channel 847 * @param number activityType 848 * @param number activitySubtype 849 * @param number timestamp 850 * @param number extraSizeData 851 * @param string extraStringData 852 */ 853 observeActivity = DevToolsInfaillibleUtils.makeInfallible( 854 function ( 855 channel, 856 activityType, 857 activitySubtype, 858 timestamp, 859 extraSizeData, 860 extraStringData 861 ) { 862 if ( 863 this.#isDestroyed || 864 (activityType != gActivityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION && 865 activityType != gActivityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) 866 ) { 867 return; 868 } 869 870 if ( 871 !(channel instanceof Ci.nsIHttpChannel) || 872 !(channel instanceof Ci.nsIClassifiedChannel) 873 ) { 874 return; 875 } 876 877 channel = channel.QueryInterface(Ci.nsIHttpChannel); 878 channel = channel.QueryInterface(Ci.nsIClassifiedChannel); 879 880 if (DEBUG_PLATFORM_EVENTS) { 881 logPlatformEvent( 882 this.getActivityTypeString(activityType, activitySubtype), 883 channel 884 ); 885 } 886 887 if ( 888 activitySubtype == gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER 889 ) { 890 this.#onRequestHeader(channel, timestamp, extraStringData); 891 return; 892 } 893 894 // Iterate over all currently ongoing requests. If channel can't 895 // be found within them, then exit this function. 896 const httpActivity = this.#findActivityObject(channel); 897 if (!httpActivity) { 898 return; 899 } 900 901 // If we're throttling, we must not report events as they arrive 902 // from platform, but instead let the throttler emit the events 903 // after some time has elapsed. 904 if ( 905 httpActivity.downloadThrottle && 906 HTTP_DOWNLOAD_ACTIVITIES.includes(activitySubtype) 907 ) { 908 const callback = this.#dispatchActivity.bind(this); 909 httpActivity.downloadThrottle.addActivityCallback( 910 callback, 911 httpActivity, 912 channel, 913 activityType, 914 activitySubtype, 915 timestamp, 916 extraSizeData, 917 extraStringData 918 ); 919 } else { 920 this.#dispatchActivity( 921 httpActivity, 922 channel, 923 activityType, 924 activitySubtype, 925 timestamp, 926 extraSizeData, 927 extraStringData 928 ); 929 } 930 } 931 ); 932 933 /** 934 * Craft the "event" object passed to the Watcher class in order 935 * to instantiate the NetworkEventActor. 936 * 937 * /!\ This method does many other important things: 938 * - Cancel requests blocked by DevTools 939 * - Fetch request headers/cookies 940 * - Set a few attributes on http activity object 941 * - Set a few attributes on file activity object 942 * - Register listener to record response content 943 */ 944 #createNetworkEvent( 945 httpActivity, 946 { timestamp, blockedReason, extension, inProgressRequest } = {} 947 ) { 948 if ( 949 blockedReason === undefined && 950 this.#shouldBlockChannel(httpActivity.channel) 951 ) { 952 // Check the request URL with ones manually blocked by the user in DevTools. 953 // If it's meant to be blocked, we cancel the request and annotate the event. 954 httpActivity.channel.cancel(Cr.NS_BINDING_ABORTED); 955 blockedReason = "devtools"; 956 } 957 958 httpActivity.owner = this.#onNetworkEvent( 959 { 960 timestamp, 961 blockedReason, 962 extension, 963 discardRequestBody: !this.#saveRequestAndResponseBodies, 964 discardResponseBody: !this.#saveRequestAndResponseBodies, 965 }, 966 httpActivity.channel 967 ); 968 969 // Bug 1489217 - Avoid watching for response content for blocked or in-progress requests 970 // as it can't be observed and would throw if we try. 971 if (blockedReason === undefined && !inProgressRequest) { 972 this.#setupResponseListener(httpActivity); 973 } 974 975 const wrapper = ChannelWrapper.get(httpActivity.channel); 976 if (this.#authPromptListenerEnabled && !wrapper.hasNetworkAuthListener) { 977 new lazy.NetworkAuthListener(httpActivity.channel, httpActivity.owner); 978 wrapper.hasNetworkAuthListener = true; 979 } 980 } 981 982 /** 983 * Handler for ACTIVITY_SUBTYPE_REQUEST_HEADER. When a request starts the 984 * headers are sent to the server. This method creates the |httpActivity| 985 * object where we store the request and response information that is 986 * collected through its lifetime. 987 * 988 * @private 989 * @param nsIHttpChannel channel 990 * @param number timestamp 991 * @param string rawHeaders 992 * @return void 993 */ 994 #onRequestHeader(channel, timestamp, rawHeaders) { 995 if (this.#ignoreChannelFunction(channel)) { 996 return; 997 } 998 999 const httpActivity = this.#createOrGetActivityObject(channel); 1000 if (timestamp) { 1001 httpActivity.timings.REQUEST_HEADER = { 1002 first: timestamp, 1003 last: timestamp, 1004 }; 1005 } 1006 1007 // TODO: In theory httpActivity.owner should not be missing here because 1008 // the network event should have been created in http-on-before-connect. 1009 // However, there is a scenario in DevTools where this can still happen: 1010 // if NetworkObserver clear() is called after the event was detected, the 1011 // activity will be deleted again have an ownerless notification here. 1012 if (!httpActivity.owner) { 1013 // If we are not creating events using the early platform notification 1014 // this should be the first time we are notified about this channel. 1015 this.#createNetworkEvent(httpActivity, { 1016 timestamp, 1017 }); 1018 } 1019 1020 httpActivity.owner.addRawHeaders({ 1021 channel, 1022 rawHeaders, 1023 }); 1024 } 1025 1026 /** 1027 * Check if the provided channel should be blocked given the current 1028 * blocked URLs configured for this network observer. 1029 */ 1030 #shouldBlockChannel(channel) { 1031 for (const regexp of this.#blockedURLs.values()) { 1032 if (regexp.test(channel.URI.spec)) { 1033 return true; 1034 } 1035 } 1036 return false; 1037 } 1038 1039 /** 1040 * Find an HTTP activity object for the channel. 1041 * 1042 * @param nsIHttpChannel channel 1043 * The HTTP channel whose activity object we want to find. 1044 * @return object 1045 * The HTTP activity object, or null if it is not found. 1046 */ 1047 #findActivityObject(channel) { 1048 return this.#openRequests.get(channel); 1049 } 1050 1051 /** 1052 * Find an existing activity object, or create a new one. This 1053 * object is used for storing all the request and response 1054 * information. 1055 * 1056 * This is a HAR-like object. Conformance to the spec is not guaranteed at 1057 * this point. 1058 * 1059 * @see http://www.softwareishard.com/blog/har-12-spec 1060 * @param {nsIChannel} channel 1061 * The channel for which the activity object is created. 1062 * @return object 1063 * The new HTTP activity object. 1064 */ 1065 #createOrGetActivityObject(channel) { 1066 let activity = this.#findActivityObject(channel); 1067 if (!activity) { 1068 const isHttpChannel = channel instanceof Ci.nsIHttpChannel; 1069 1070 if (isHttpChannel) { 1071 // Most of the data needed from the channel is only available via the 1072 // nsIHttpChannelInternal interface. 1073 channel.QueryInterface(Ci.nsIHttpChannelInternal); 1074 } else { 1075 channel.QueryInterface(Ci.nsIChannel); 1076 } 1077 1078 activity = { 1079 // The nsIChannel for which this activity object was created. 1080 channel, 1081 // See #prepareRequestBody() 1082 charset: isHttpChannel ? lazy.NetworkUtils.getCharset(channel) : null, 1083 // The postData sent by this request. 1084 sentBody: null, 1085 // The URL for the current channel. 1086 url: channel.URI.spec, 1087 // The encoded response body size. 1088 bodySize: 0, 1089 // The response headers size. 1090 headersSize: 0, 1091 // needed for host specific security info but file urls do not have hostname 1092 hostname: isHttpChannel ? channel.URI.host : null, 1093 discardRequestBody: isHttpChannel 1094 ? !this.#saveRequestAndResponseBodies 1095 : false, 1096 discardResponseBody: isHttpChannel 1097 ? !this.#saveRequestAndResponseBodies 1098 : false, 1099 // internal timing information, see observeActivity() 1100 timings: {}, 1101 // the activity owner which is notified when changes happen 1102 owner: null, 1103 }; 1104 1105 this.#openRequests.set(channel, activity); 1106 } 1107 1108 return activity; 1109 } 1110 1111 /** 1112 * Block a request based on certain filtering options. 1113 * 1114 * Currently, exact URL match or URL patterns are supported. 1115 */ 1116 blockRequest(filter) { 1117 if (!filter || !filter.url) { 1118 // In the future, there may be other types of filters, such as domain. 1119 // For now, ignore anything other than URL. 1120 return; 1121 } 1122 1123 this.#addBlockedUrl(filter.url); 1124 } 1125 1126 /** 1127 * Unblock a request based on certain filtering options. 1128 * 1129 * Currently, exact URL match or URL patterns are supported. 1130 */ 1131 unblockRequest(filter) { 1132 if (!filter || !filter.url) { 1133 // In the future, there may be other types of filters, such as domain. 1134 // For now, ignore anything other than URL. 1135 return; 1136 } 1137 1138 this.#blockedURLs.delete(filter.url); 1139 } 1140 1141 /** 1142 * Updates the list of blocked request strings 1143 * 1144 * This match will be a (String).includes match, not an exact URL match 1145 */ 1146 setBlockedUrls(urls) { 1147 urls = urls || []; 1148 this.#blockedURLs = new Map(); 1149 urls.forEach(url => this.#addBlockedUrl(url)); 1150 } 1151 1152 #addBlockedUrl(url) { 1153 this.#blockedURLs.set(url, lazy.wildcardToRegExp(url)); 1154 } 1155 1156 /** 1157 * Returns a list of blocked requests 1158 * Useful as blockedURLs is mutated by both console & netmonitor 1159 */ 1160 getBlockedUrls() { 1161 return this.#blockedURLs.keys(); 1162 } 1163 1164 override(url, path) { 1165 this.#overrides.set(url, path); 1166 1167 // Clear in-memory cache, so that the subsequent request reaches the 1168 // http handling and the override works. 1169 ChromeUtils.clearResourceCache({ url }); 1170 } 1171 1172 removeOverride(url) { 1173 this.#overrides.delete(url); 1174 1175 ChromeUtils.clearResourceCache({ url }); 1176 } 1177 1178 /** 1179 * Setup the network response listener for the given HTTP activity. The 1180 * NetworkResponseListener is responsible for storing the response body. 1181 * 1182 * @private 1183 * @param object httpActivity 1184 * The HTTP activity object we are tracking. 1185 */ 1186 #setupResponseListener(httpActivity) { 1187 const channel = httpActivity.channel; 1188 channel.QueryInterface(Ci.nsITraceableChannel); 1189 1190 if (!httpActivity.fromCache) { 1191 const throttler = this.#getThrottler(); 1192 if (throttler) { 1193 httpActivity.downloadThrottle = throttler.manage(channel); 1194 } 1195 } 1196 1197 // The response will be written into the outputStream of this pipe. 1198 // This allows us to buffer the data we are receiving and read it 1199 // asynchronously. 1200 // Both ends of the pipe must be blocking. 1201 const sink = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); 1202 1203 // The streams need to be blocking because this is required by the 1204 // stream tee. 1205 sink.init(false, false, this.#responsePipeSegmentSize, PR_UINT32_MAX, null); 1206 1207 // Add listener for the response body. 1208 const newListener = new lazy.NetworkResponseListener(httpActivity, { 1209 decodedCertificateCache: this.#decodedCertificateCache, 1210 decodeResponseBody: this.#decodeResponseBodies, 1211 fromServiceWorker: httpActivity.fromServiceWorker, 1212 responseBodyLimit: this.#responseBodyLimit, 1213 }); 1214 1215 // Remember the input stream, so it isn't released by GC. 1216 newListener.inputStream = sink.inputStream; 1217 newListener.sink = sink; 1218 1219 const tee = Cc["@mozilla.org/network/stream-listener-tee;1"].createInstance( 1220 Ci.nsIStreamListenerTee 1221 ); 1222 1223 const originalListener = channel.setNewListener(tee); 1224 1225 tee.init(originalListener, sink.outputStream, newListener); 1226 } 1227 1228 /** 1229 * Handler for ACTIVITY_SUBTYPE_REQUEST_BODY_SENT. Read and record the request 1230 * body here. It will be available in addResponseStart. 1231 * 1232 * @private 1233 * @param object httpActivity 1234 * The HTTP activity object we are working with. 1235 */ 1236 #prepareRequestBody(httpActivity) { 1237 // Return early if we don't need the request body, or if we've 1238 // already found it. 1239 if (httpActivity.discardRequestBody || httpActivity.sentBody !== null) { 1240 return; 1241 } 1242 1243 const sentBody = lazy.NetworkHelper.readPostDataFromRequest( 1244 httpActivity.channel, 1245 httpActivity.charset 1246 ); 1247 1248 if (sentBody !== null) { 1249 httpActivity.sentBody = sentBody.data; 1250 } 1251 } 1252 1253 /** 1254 * Handler for ACTIVITY_SUBTYPE_TRANSACTION_CLOSE. This method updates the HAR 1255 * timing information on the HTTP activity object and clears the request 1256 * from the list of known open requests. 1257 * 1258 * @private 1259 * @param object httpActivity 1260 * The HTTP activity object we work with. 1261 */ 1262 #onTransactionClose(httpActivity) { 1263 if (httpActivity.owner) { 1264 const result = lazy.NetworkTimings.extractHarTimings(httpActivity); 1265 const serverTimings = 1266 lazy.NetworkTimings.extractServerTimings(httpActivity); 1267 1268 httpActivity.owner.addServerTimings(serverTimings); 1269 httpActivity.owner.addEventTimings( 1270 result.total, 1271 result.timings, 1272 result.offsets 1273 ); 1274 } 1275 } 1276 1277 #sendRequestBody(httpActivity) { 1278 if (httpActivity.sentBody !== null) { 1279 const limit = Services.prefs.getIntPref( 1280 "devtools.netmonitor.requestBodyLimit" 1281 ); 1282 const size = httpActivity.sentBody.length; 1283 if (size > limit && limit > 0) { 1284 httpActivity.sentBody = httpActivity.sentBody.substr(0, limit); 1285 } 1286 httpActivity.owner.addRequestPostData({ 1287 text: httpActivity.sentBody, 1288 size, 1289 }); 1290 httpActivity.sentBody = null; 1291 } 1292 } 1293 1294 /* 1295 * Clears the open requests channel map. 1296 */ 1297 clear() { 1298 this.#openRequests.clear(); 1299 } 1300 1301 /** 1302 * Suspend observer activity. This is called when the Network monitor actor stops 1303 * listening. 1304 */ 1305 destroy() { 1306 if (this.#isDestroyed) { 1307 return; 1308 } 1309 1310 if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { 1311 gActivityDistributor.removeObserver(this); 1312 Services.obs.removeObserver( 1313 this.#httpResponseExaminer, 1314 "http-on-examine-response" 1315 ); 1316 Services.obs.removeObserver( 1317 this.#httpResponseExaminer, 1318 "http-on-examine-cached-response" 1319 ); 1320 Services.obs.removeObserver( 1321 this.#httpModifyExaminer, 1322 "http-on-modify-request" 1323 ); 1324 Services.obs.removeObserver( 1325 this.#fileChannelExaminer, 1326 "file-channel-opened" 1327 ); 1328 Services.obs.removeObserver( 1329 this.#dataChannelExaminer, 1330 "data-channel-opened" 1331 ); 1332 1333 Services.obs.removeObserver( 1334 this.#httpStopRequest, 1335 "http-on-stop-request" 1336 ); 1337 Services.obs.removeObserver( 1338 this.#httpBeforeConnect, 1339 "http-on-before-connect" 1340 ); 1341 } else { 1342 Services.obs.removeObserver( 1343 this.#httpFailedOpening, 1344 "http-on-failed-opening-request" 1345 ); 1346 } 1347 1348 Services.obs.removeObserver( 1349 this.#serviceWorkerRequest, 1350 "service-worker-synthesized-response" 1351 ); 1352 1353 this.#ignoreChannelFunction = null; 1354 this.#onNetworkEvent = null; 1355 this.#throttler = null; 1356 this.#decodedCertificateCache.clear(); 1357 this.clear(); 1358 1359 this.#isDestroyed = true; 1360 } 1361 }