NetworkUtils.sys.mjs (30227B)
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 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters( 8 lazy, 9 { 10 NetworkHelper: 11 "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs", 12 NetworkTimings: 13 "resource://devtools/shared/network-observer/NetworkTimings.sys.mjs", 14 }, 15 { global: "contextual" } 16 ); 17 18 ChromeUtils.defineLazyGetter(lazy, "tpFlagsMask", () => { 19 const trackingProtectionLevel2Enabled = Services.prefs 20 .getStringPref("urlclassifier.trackingTable") 21 .includes("content-track-digest256"); 22 23 return trackingProtectionLevel2Enabled 24 ? ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING & 25 ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_STRICT_TRACKING 26 : ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING & 27 Ci.nsIClassifiedChannel.CLASSIFIED_ANY_STRICT_TRACKING; 28 }); 29 30 // List of compression encodings that can be handled by the 31 // NetworkResponseListener. 32 const ACCEPTED_COMPRESSION_ENCODINGS = [ 33 "gzip", 34 "deflate", 35 "br", 36 "x-gzip", 37 "x-deflate", 38 "zstd", 39 ]; 40 41 // These include types indicating the availability of data e.g responseCookies 42 // or the networkEventOwner action which triggered the specific update e.g responseStart. 43 // These types are specific to devtools and used by BiDi. 44 const NETWORK_EVENT_TYPES = { 45 CACHE_DETAILS: "cacheDetails", 46 EARLY_HINT_RESPONSE_HEADERS: "earlyHintsResponseHeaders", 47 EVENT_TIMINGS: "eventTimings", 48 REQUEST_COOKIES: "requestCookies", 49 REQUEST_HEADERS: "requestHeaders", 50 REQUEST_POSTDATA: "requestPostData", 51 RESPONSE_CACHE: "responseCache", 52 RESPONSE_CONTENT: "responseContent", 53 RESPONSE_CONTENT_COMPLETE: "responseContentComplete", 54 RESPONSE_COOKIES: "responseCookies", 55 RESPONSE_HEADERS: "responseHeaders", 56 RESPONSE_START: "responseStart", 57 SECURITY_INFO: "securityInfo", 58 RESPONSE_END: "responseEnd", 59 }; 60 61 /** 62 * Convert a nsIContentPolicy constant to a display string 63 */ 64 const LOAD_CAUSE_STRINGS = { 65 [Ci.nsIContentPolicy.TYPE_INVALID]: "invalid", 66 [Ci.nsIContentPolicy.TYPE_OTHER]: "other", 67 [Ci.nsIContentPolicy.TYPE_SCRIPT]: "script", 68 [Ci.nsIContentPolicy.TYPE_IMAGE]: "img", 69 [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "stylesheet", 70 [Ci.nsIContentPolicy.TYPE_OBJECT]: "object", 71 [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "document", 72 [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "subdocument", 73 [Ci.nsIContentPolicy.TYPE_PING]: "ping", 74 [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "xhr", 75 [Ci.nsIContentPolicy.TYPE_DTD]: "dtd", 76 [Ci.nsIContentPolicy.TYPE_FONT]: "font", 77 [Ci.nsIContentPolicy.TYPE_MEDIA]: "media", 78 [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "websocket", 79 [Ci.nsIContentPolicy.TYPE_WEB_TRANSPORT]: "webtransport", 80 [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "csp", 81 [Ci.nsIContentPolicy.TYPE_XSLT]: "xslt", 82 [Ci.nsIContentPolicy.TYPE_BEACON]: "beacon", 83 [Ci.nsIContentPolicy.TYPE_FETCH]: "fetch", 84 [Ci.nsIContentPolicy.TYPE_IMAGESET]: "imageset", 85 [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "webManifest", 86 [Ci.nsIContentPolicy.TYPE_WEB_IDENTITY]: "webidentity", 87 }; 88 89 function causeTypeToString(causeType, loadFlags, internalContentPolicyType) { 90 let prefix = ""; 91 if ( 92 (causeType == Ci.nsIContentPolicy.TYPE_IMAGESET || 93 internalContentPolicyType == Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE) && 94 loadFlags & Ci.nsIRequest.LOAD_BACKGROUND 95 ) { 96 prefix = "lazy-"; 97 } 98 99 return prefix + LOAD_CAUSE_STRINGS[causeType] || "unknown"; 100 } 101 102 function stringToCauseType(value) { 103 return Object.keys(LOAD_CAUSE_STRINGS).find( 104 key => LOAD_CAUSE_STRINGS[key] === value 105 ); 106 } 107 108 function isChannelFromSystemPrincipal(channel) { 109 let principal; 110 111 if (channel.isDocument) { 112 // The loadingPrincipal is the principal where the request will be used. 113 principal = channel.loadInfo.loadingPrincipal; 114 } else { 115 // The triggeringPrincipal is the principal of the resource which triggered 116 // the request. Except for document loads, this is normally the best way 117 // to know if a request is done on behalf of a chrome resource. 118 // For instance if a chrome stylesheet loads a resource which is used in a 119 // content page, the loadingPrincipal will be a content principal, but the 120 // triggeringPrincipal will be the system principal. 121 principal = channel.loadInfo.triggeringPrincipal; 122 } 123 124 return !!principal?.isSystemPrincipal; 125 } 126 127 function isChromeFileChannel(channel) { 128 if (!(channel instanceof Ci.nsIFileChannel)) { 129 return false; 130 } 131 132 return ( 133 channel.originalURI.spec.startsWith("chrome://") || 134 channel.originalURI.spec.startsWith("resource://") 135 ); 136 } 137 138 function isPrivilegedChannel(channel) { 139 return ( 140 isChannelFromSystemPrincipal(channel) || 141 isChromeFileChannel(channel) || 142 channel.loadInfo.isInDevToolsContext 143 ); 144 } 145 146 /** 147 * Get the browsing context id for the channel. 148 * 149 * @param {*} channel 150 * @returns {number} 151 */ 152 function getChannelBrowsingContextID(channel) { 153 // `frameBrowsingContextID` is non-0 if the channel is loading an iframe. 154 // If available, use it instead of `browsingContextID` which is exceptionally 155 // set to the parent's BrowsingContext id for such channels. 156 if (channel.loadInfo.frameBrowsingContextID) { 157 return channel.loadInfo.frameBrowsingContextID; 158 } 159 160 if (channel.loadInfo.browsingContextID) { 161 return channel.loadInfo.browsingContextID; 162 } 163 164 if (channel.loadInfo.workerAssociatedBrowsingContextID) { 165 return channel.loadInfo.workerAssociatedBrowsingContextID; 166 } 167 168 // At least WebSocket channel aren't having a browsingContextID set on their loadInfo 169 // We fallback on top frame element, which works, but will be wrong for WebSocket 170 // in same-process iframes... 171 const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel); 172 // topFrame is typically null for some chrome requests like favicons 173 if (topFrame && topFrame.browsingContext) { 174 return topFrame.browsingContext.id; 175 } 176 return null; 177 } 178 179 /** 180 * Get the innerWindowId for the channel. 181 * 182 * @param {*} channel 183 * @returns {number} 184 */ 185 function getChannelInnerWindowId(channel) { 186 if (channel.loadInfo.innerWindowID) { 187 return channel.loadInfo.innerWindowID; 188 } 189 // At least WebSocket channel aren't having a browsingContextID set on their loadInfo 190 // We fallback on top frame element, which works, but will be wrong for WebSocket 191 // in same-process iframes... 192 const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel); 193 // topFrame is typically null for some chrome requests like favicons 194 if (topFrame?.browsingContext?.currentWindowGlobal) { 195 return topFrame.browsingContext.currentWindowGlobal.innerWindowId; 196 } 197 return null; 198 } 199 200 /** 201 * Does this channel represent a Preload request. 202 * 203 * @param {*} channel 204 * @returns {boolean} 205 */ 206 function isPreloadRequest(channel) { 207 const type = channel.loadInfo.internalContentPolicyType; 208 return ( 209 type == Ci.nsIContentPolicy.TYPE_INTERNAL_SCRIPT_PRELOAD || 210 type == Ci.nsIContentPolicy.TYPE_INTERNAL_MODULE_PRELOAD || 211 type == Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_PRELOAD || 212 type == Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET_PRELOAD || 213 type == Ci.nsIContentPolicy.TYPE_INTERNAL_FONT_PRELOAD || 214 type == Ci.nsIContentPolicy.TYPE_INTERNAL_JSON_PRELOAD 215 ); 216 } 217 218 /** 219 * Get the channel cause details. 220 * 221 * @param {nsIChannel} channel 222 * @returns {object} 223 * - loadingDocumentUri {string} uri of the document which created the 224 * channel 225 * - type {string} cause type as string 226 */ 227 function getCauseDetails(channel) { 228 // Determine the cause and if this is an XHR request. 229 let causeType = Ci.nsIContentPolicy.TYPE_OTHER; 230 let causeUri = null; 231 232 if (channel.loadInfo) { 233 causeType = channel.loadInfo.externalContentPolicyType; 234 const { loadingPrincipal } = channel.loadInfo; 235 if (loadingPrincipal) { 236 causeUri = loadingPrincipal.spec; 237 } 238 } 239 240 return { 241 loadingDocumentUri: causeUri, 242 type: causeTypeToString( 243 causeType, 244 channel.loadFlags, 245 channel.loadInfo.internalContentPolicyType 246 ), 247 }; 248 } 249 250 /** 251 * Get the channel priority. Priority is a number which typically ranges from 252 * -20 (lowest priority) to 20 (highest priority). Can be null if the channel 253 * does not implement nsISupportsPriority. 254 * 255 * @param {nsIChannel} channel 256 * @returns {number|undefined} 257 */ 258 function getChannelPriority(channel) { 259 if (channel instanceof Ci.nsISupportsPriority) { 260 return channel.priority; 261 } 262 263 return null; 264 } 265 266 /** 267 * Get the channel HTTP version as an uppercase string starting with "HTTP/" 268 * (eg "HTTP/2"). 269 * 270 * @param {nsIChannel} channel 271 * @returns {string} 272 */ 273 function getHttpVersion(channel) { 274 if (!(channel instanceof Ci.nsIHttpChannelInternal)) { 275 return null; 276 } 277 278 // Determine the HTTP version. 279 const httpVersionMaj = {}; 280 const httpVersionMin = {}; 281 282 channel.QueryInterface(Ci.nsIHttpChannelInternal); 283 channel.getResponseVersion(httpVersionMaj, httpVersionMin); 284 285 // The official name HTTP version 2.0 and 3.0 are HTTP/2 and HTTP/3, omit the 286 // trailing `.0`. 287 if (httpVersionMin.value == 0) { 288 return "HTTP/" + httpVersionMaj.value; 289 } 290 291 return "HTTP/" + httpVersionMaj.value + "." + httpVersionMin.value; 292 } 293 294 const UNKNOWN_PROTOCOL_STRINGS = ["", "unknown"]; 295 const HTTP_PROTOCOL_STRINGS = ["http", "https"]; 296 /** 297 * Get the protocol for the provided httpActivity. Either the ALPN negotiated 298 * protocol or as a fallback a protocol computed from the scheme and the 299 * response status. 300 * 301 * TODO: The `protocol` is similar to another response property called 302 * `httpVersion`. `httpVersion` is uppercase and purely computed from the 303 * response status, whereas `protocol` uses nsIHttpChannel.protocolVersion by 304 * default and otherwise falls back on `httpVersion`. Ideally we should merge 305 * the two properties. 306 * 307 * @param {object} httpActivity 308 * The httpActivity object for which we need to get the protocol. 309 * 310 * @returns {string} 311 * The protocol as a string. 312 */ 313 function getProtocol(channel) { 314 let protocol = ""; 315 try { 316 const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); 317 // protocolVersion corresponds to ALPN negotiated protocol. 318 protocol = httpChannel.protocolVersion; 319 } catch (e) { 320 // Ignore errors reading protocolVersion. 321 } 322 323 if (UNKNOWN_PROTOCOL_STRINGS.includes(protocol)) { 324 protocol = channel.URI.scheme; 325 const httpVersion = getHttpVersion(channel); 326 if ( 327 typeof httpVersion == "string" && 328 HTTP_PROTOCOL_STRINGS.includes(protocol) 329 ) { 330 protocol = httpVersion.toLowerCase(); 331 } 332 } 333 334 return protocol; 335 } 336 337 /** 338 * Get the channel referrer policy as a string 339 * (eg "strict-origin-when-cross-origin"). 340 * 341 * @param {nsIChannel} channel 342 * @returns {string} 343 */ 344 function getReferrerPolicy(channel) { 345 return channel.referrerInfo 346 ? channel.referrerInfo.getReferrerPolicyString() 347 : ""; 348 } 349 350 /** 351 * Check if the channel is private. 352 * 353 * @param {nsIChannel} channel 354 * @returns {boolean} 355 */ 356 function isChannelPrivate(channel) { 357 channel.QueryInterface(Ci.nsIPrivateBrowsingChannel); 358 return channel.isChannelPrivate; 359 } 360 361 /** 362 * Check if the channel data is loaded from the cache or not. 363 * 364 * @param {nsIChannel} channel 365 * The channel for which we need to check the cache status. 366 * 367 * @returns {boolean} 368 * True if the channel data is loaded from the cache, false otherwise. 369 */ 370 function isFromCache(channel) { 371 if (channel instanceof Ci.nsICacheInfoChannel) { 372 return channel.isFromCache(); 373 } 374 375 return false; 376 } 377 378 /** 379 * isNavigationRequest is true for the one request used to load a new top level 380 * document of a given tab, or top level window. It will typically be false for 381 * navigation requests of iframes, i.e. the request loading another document in 382 * an iframe. 383 * 384 * @param {nsIChannel} channel 385 * @return {boolean} 386 */ 387 function isNavigationRequest(channel) { 388 return channel.isMainDocumentChannel && channel.loadInfo.isTopLevelLoad; 389 } 390 391 /** 392 * Returns true if the channel has been processed by URL-Classifier features 393 * and is considered third-party with the top window URI, and if it has loaded 394 * a resource that is classified as a tracker. 395 * 396 * @param {nsIChannel} channel 397 * @return {boolean} 398 */ 399 function isThirdPartyTrackingResource(channel) { 400 // Only consider channels classified as level-1 to be trackers if our preferences 401 // would not cause such channels to be blocked in strict content blocking mode. 402 // Make sure the value produced here is a boolean. 403 return !!( 404 channel instanceof Ci.nsIClassifiedChannel && 405 channel.isThirdPartyTrackingResource() && 406 (channel.thirdPartyClassificationFlags & lazy.tpFlagsMask) == 0 407 ); 408 } 409 410 /** 411 * Retrieve the websocket channel for the provided channel, if available. 412 * Returns null otherwise. 413 * 414 * @param {nsIChannel} channel 415 * @returns {nsIWebSocketChannel|null} 416 */ 417 function getWebSocketChannel(channel) { 418 let wsChannel = null; 419 if (channel.notificationCallbacks) { 420 try { 421 wsChannel = channel.notificationCallbacks.QueryInterface( 422 Ci.nsIWebSocketChannel 423 ); 424 } catch (e) { 425 // Not all channels implement nsIWebSocketChannel. 426 } 427 } 428 return wsChannel; 429 } 430 431 /** 432 * For a given channel, fetch the request's headers and cookies. 433 * 434 * @param {nsIChannel} channel 435 * @return {object} 436 * An object with two properties: 437 * @property {Array<object>} cookies 438 * Array of { name, value } objects. 439 * @property {Array<object>} headers 440 * Array of { name, value } objects. 441 */ 442 function fetchRequestHeadersAndCookies(channel) { 443 const headers = []; 444 let cookies = []; 445 let cookieHeader = null; 446 447 // Copy the request header data. 448 channel.visitRequestHeaders({ 449 visitHeader(name, value) { 450 // The `Proxy-Authorization` header even though it appears on the channel is not 451 // actually sent to the server for non CONNECT requests after the HTTP/HTTPS tunnel 452 // is setup by the proxy. 453 if (name == "Proxy-Authorization") { 454 return; 455 } 456 if (name == "Cookie") { 457 cookieHeader = value; 458 } 459 headers.push({ name, value }); 460 }, 461 }); 462 463 if (cookieHeader) { 464 cookies = lazy.NetworkHelper.parseCookieHeader(cookieHeader); 465 } 466 467 return { cookies, headers }; 468 } 469 470 /** 471 * Parse the early hint raw headers string to an 472 * array of name/value object header pairs 473 * 474 * @param {string} rawHeaders 475 * @returns {Array} 476 */ 477 function parseEarlyHintsResponseHeaders(rawHeaders) { 478 const headers = rawHeaders.split("\r\n"); 479 // Remove the line with the HTTP version and the status 480 headers.shift(); 481 return headers 482 .map(header => { 483 const [name, value] = header.split(":"); 484 return { name, value }; 485 }) 486 .filter(header => header.name.length); 487 } 488 489 /** 490 * For a given channel, fetch the response's headers and cookies. 491 * 492 * @param {nsIChannel} channel 493 * @return {object} 494 * An object with two properties: 495 * @property {Array<object>} cookies 496 * Array of { name, value } objects. 497 * @property {Array<object>} headers 498 * Array of { name, value } objects. 499 */ 500 function fetchResponseHeadersAndCookies(channel) { 501 // Read response headers and cookies. 502 const headers = []; 503 const setCookieHeaders = []; 504 505 const SET_COOKIE_REGEXP = /set-cookie/i; 506 channel.visitOriginalResponseHeaders({ 507 visitHeader(name, value) { 508 if (SET_COOKIE_REGEXP.test(name)) { 509 setCookieHeaders.push(value); 510 } 511 headers.push({ name, value }); 512 }, 513 }); 514 515 return { 516 cookies: lazy.NetworkHelper.parseSetCookieHeaders(setCookieHeaders), 517 headers, 518 }; 519 } 520 521 /** 522 * Check if a given network request should be logged by a network monitor 523 * based on the specified filters. 524 * 525 * @param {(nsIHttpChannel|nsIFileChannel)} channel 526 * Request to check. 527 * @param filters 528 * NetworkObserver filters to match against. An object with one of the following attributes: 529 * - sessionContext: When inspecting requests from the parent process, pass the WatcherActor's session context. 530 * This helps know what is the overall debugged scope. 531 * See watcher actor constructor for more info. 532 * - targetActor: When inspecting requests from the content process, pass the WindowGlobalTargetActor. 533 * This helps know what exact subset of request we should accept. 534 * This is especially useful to behave correctly regarding EFT, where we should include or not 535 * iframes requests. 536 * - browserId, addonId, window: All these attributes are legacy. 537 * Only browserId attribute is still used by the legacy WebConsoleActor startListener API. 538 * @return boolean 539 * True if the network request should be logged, false otherwise. 540 */ 541 function matchRequest(channel, filters) { 542 // NetworkEventWatcher should now pass a session context for the parent process codepath 543 if (filters.sessionContext) { 544 const { type } = filters.sessionContext; 545 if (type == "all") { 546 return true; 547 } 548 549 // Ignore requests from chrome or add-on code when we don't monitor the whole browser 550 if ( 551 channel.loadInfo?.loadingDocument === null && 552 isPrivilegedChannel(channel) 553 ) { 554 return false; 555 } 556 557 // When a page fails loading in top level or in iframe, an error page is shown 558 // which will trigger a request to about:neterror (which is translated into a file:// URI request). 559 // Ignore this request in regular toolbox (but not in the browser toolbox). 560 if (channel.loadInfo?.loadErrorPage) { 561 return false; 562 } 563 564 if (type == "browser-element") { 565 if (!channel.loadInfo.browsingContext) { 566 const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel); 567 // `topFrame` is typically null for some chrome requests like favicons 568 // And its `browsingContext` attribute might be null if the request happened 569 // while the tab is being closed. 570 return ( 571 topFrame?.browsingContext?.browserId == 572 filters.sessionContext.browserId 573 ); 574 } 575 return ( 576 channel.loadInfo.browsingContext.browserId == 577 filters.sessionContext.browserId 578 ); 579 } 580 if (type == "webextension") { 581 return ( 582 channel.loadInfo?.loadingPrincipal?.addonId === 583 filters.sessionContext.addonId 584 ); 585 } 586 throw new Error("Unsupported session context type: " + type); 587 } 588 589 // NetworkEventContentWatcher and NetworkEventStackTraces pass a target actor instead, from the content processes 590 // Because of EFT, we can't use session context as we have to know what exact windows the target actor covers. 591 if (filters.targetActor) { 592 // Ignore requests from chrome or add-on code when we don't monitor the whole browser 593 if ( 594 filters.targetActor.sessionContext?.type !== "all" && 595 isPrivilegedChannel(channel) 596 ) { 597 return false; 598 } 599 600 // Bug 1769982 the target actor might be destroying and accessing windows will throw. 601 // Ignore all further request when this happens. 602 let windows; 603 try { 604 windows = filters.targetActor.windows; 605 } catch (e) { 606 return false; 607 } 608 const win = lazy.NetworkHelper.getWindowForRequest(channel); 609 return windows.includes(win); 610 } 611 612 throw new Error( 613 "matchRequest expects either a 'targetActor' or a 'sessionContext' attribute" 614 ); 615 } 616 617 function getBlockedReason(channel, fromCache = false) { 618 let blockedReason; 619 const extension = {}; 620 const { status } = channel; 621 622 try { 623 const request = channel.QueryInterface(Ci.nsIHttpChannel); 624 const properties = request.QueryInterface(Ci.nsIPropertyBag); 625 626 blockedReason = request.loadInfo.requestBlockingReason; 627 extension.blocking = properties.getProperty("cancelledByExtension"); 628 629 // WebExtensionPolicy is not available for workers 630 if (typeof WebExtensionPolicy !== "undefined") { 631 extension.blocking = WebExtensionPolicy.getByID(extension.blocking).name; 632 } 633 } catch (err) { 634 // "cancelledByExtension" doesn't have to be available. 635 } 636 637 if ( 638 blockedReason === Ci.nsILoadInfo.BLOCKING_REASON_CLASSIFY_HARMFULADDON_URI 639 ) { 640 try { 641 const properties = channel.QueryInterface(Ci.nsIPropertyBag); 642 extension.blocked = properties.getProperty("blockedExtension"); 643 } catch (err) { 644 // "blockedExtension" doesn't have to be available. 645 } 646 } 647 648 // These are platform errors which are not exposed to the users, 649 // usually the requests (with these errors) might be displayed with various 650 // other status codes. 651 const ignoreList = [ 652 // These are emited when the request is already in the cache. 653 "NS_ERROR_PARSED_DATA_CACHED", 654 // This is emited when there is some issues around images e.g When the img.src 655 // links to a non existent url. This is typically shown as a 404 request. 656 "NS_IMAGELIB_ERROR_FAILURE", 657 // This is emited when there is a redirect. They are shown as 301 requests. 658 "NS_BINDING_REDIRECTED", 659 // E.g Emited by send beacon requests. 660 "NS_ERROR_ABORT", 661 // This is emmited when browser.http.blank_page_with_error_response.enabled 662 // is set to false, and a 404 or 500 request has no content. 663 // They are shown as 404 or 500 requests. 664 "NS_ERROR_NET_EMPTY_RESPONSE", 665 ]; 666 667 // NS_BINDING_ABORTED are emmited when request are abruptly halted, these are valid and should not be ignored. 668 // They can also be emmited for requests already cache which have the `cached` status, these should be ignored. 669 if (fromCache) { 670 ignoreList.push("NS_BINDING_ABORTED"); 671 } 672 673 // If the request has not failed or is not blocked by a web extension, check for 674 // any errors not on the ignore list. e.g When a host is not found (NS_ERROR_UNKNOWN_HOST). 675 if ( 676 blockedReason == 0 && 677 !Components.isSuccessCode(status) && 678 !ignoreList.includes(ChromeUtils.getXPCOMErrorName(status)) 679 ) { 680 blockedReason = ChromeUtils.getXPCOMErrorName(status); 681 } 682 683 return { extension, blockedReason }; 684 } 685 686 function getCharset(channel) { 687 const win = lazy.NetworkHelper.getWindowForRequest(channel); 688 return win ? win.document.characterSet : null; 689 } 690 691 /** 692 * Data channels are either handled in the parent process NetworkObserver for 693 * navigation requests, or in content processes for any other request. 694 * 695 * This function allows to apply the same logic to build the network event actor 696 * in both cases. 697 * 698 * @param {nsIDataChannel} channel 699 * The data channel for which we are creating a network event actor. 700 * @param {object} networkEventActor 701 * The network event actor owning this resource. 702 */ 703 function handleDataChannel(channel, networkEventActor) { 704 networkEventActor.addResponseStart({ 705 channel, 706 fromCache: false, 707 // According to the fetch spec for data URLs we can just hardcode 708 // "Content-Type" header. 709 rawHeaders: "content-type: " + channel.contentType, 710 }); 711 712 // For data URLs we can not set up a stream listener as for http, 713 // so we have to create a response manually and complete it. 714 const response = { 715 // TODO: Bug 1903807. Re-evaluate if it's correct to just return 716 // zero for `bodySize` and `decodedBodySize`. 717 bodySize: 0, 718 decodedBodySize: 0, 719 contentCharset: channel.contentCharset, 720 contentLength: channel.contentLength, 721 contentType: channel.contentType, 722 mimeType: lazy.NetworkHelper.addCharsetToMimeType( 723 channel.contentType, 724 channel.contentCharset 725 ), 726 transferredSize: 0, 727 }; 728 729 // For data URIs all timings can be set to zero. 730 const result = lazy.NetworkTimings.getEmptyHARTimings(); 731 networkEventActor.addEventTimings( 732 result.total, 733 result.timings, 734 result.offsets 735 ); 736 737 const url = channel.URI.spec; 738 response.text = url.substring(url.indexOf(",") + 1); 739 if ( 740 !response.mimeType || 741 !lazy.NetworkHelper.isTextMimeType(response.mimeType) 742 ) { 743 response.encoding = "base64"; 744 } 745 746 // Note: `size`` is only used by DevTools, WebDriverBiDi relies on 747 // `bodySize` and `decodedBodySize`. Waiting on Bug 1903807 to decide 748 // if those fields should have non-0 values as well. 749 response.size = response.text.length; 750 751 // Security information is not relevant for data channel, but it should 752 // not be considered as insecure either. Set empty string as security 753 // state. 754 networkEventActor.addSecurityInfo({ state: "" }); 755 networkEventActor.addResponseContent(response); 756 networkEventActor.addResponseContentComplete({}); 757 } 758 759 /** 760 * Sets a flag on the resource to specify that the data for a network event 761 * is available. The flag is used by the consumer of the resource (frontend) 762 * to determine when to lazily fetch the data. 763 * 764 * @param {object} resource - This could be a network resource object or a network resource 765 * updates object. 766 * @param {Array} networkEvents 767 */ 768 function setEventAsAvailable(resource, networkEvents) { 769 for (const event of networkEvents) { 770 if (!Object.values(NETWORK_EVENT_TYPES).includes(event)) { 771 console.warn(`${event} is not a valid network event type.`); 772 return; 773 } 774 resource[`${event}Available`] = true; 775 } 776 } 777 778 /** 779 * Helper to decode the content of a response object built by a 780 * NetworkResponseListener. 781 * 782 * @param {Array<TypedArray>} chunks 783 * Array of response chunks read via NetUtil.readInputStream. 784 * @param {object} options 785 * @param {string} options.charset 786 * Charset used for the response. 787 * @param {Array<string>} options.compressionEncodings 788 * Array of compression encodings applied to the response. 789 * @param {number} encodedBodySize 790 * The total size of the encoded response. 791 * @param {string} encoding 792 * The "encoding" of the response as computed by NetworkResponseListener. 793 * (can be either undefined or "base64" if the mime type is not text) 794 * @returns {string} 795 * The decoded content, as a string. 796 */ 797 async function decodeResponseChunks(chunks, options) { 798 const charset = options.charset || null; 799 const { compressionEncodings = [], encodedBodySize, encoding } = options; 800 801 const bytes = new Uint8Array(encodedBodySize); 802 let offset = 0; 803 for (const chunk of chunks) { 804 bytes.set(new Uint8Array(chunk), offset); 805 offset += chunk.byteLength; 806 } 807 808 const ArrayBufferInputStream = Components.Constructor( 809 "@mozilla.org/io/arraybuffer-input-stream;1", 810 "nsIArrayBufferInputStream", 811 "setData" 812 ); 813 const bodyStream = new ArrayBufferInputStream( 814 bytes.buffer, 815 0, 816 bytes.byteLength 817 ); 818 819 let decodedContent; 820 if (compressionEncodings.length) { 821 decodedContent = await decodeCompressedStream( 822 bodyStream, 823 bytes.byteLength, 824 compressionEncodings, 825 charset 826 ); 827 } else { 828 decodedContent = decodeUncompressedStream( 829 bodyStream, 830 bytes.byteLength, 831 charset 832 ); 833 } 834 835 decodedContent = lazy.NetworkHelper.convertToUnicode(decodedContent, charset); 836 if (encoding === "base64") { 837 try { 838 decodedContent = btoa(decodedContent); 839 } catch { 840 // Ignore `btoa`` errors because encoding="base64" does not guarantee the 841 // content is actually base64 (loosely based on the mime type not being 842 // a text mime type). 843 } 844 } 845 846 return decodedContent; 847 } 848 849 function decodeUncompressedStream(stream, length) { 850 const sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( 851 Ci.nsIScriptableInputStream 852 ); 853 sis.init(stream); 854 return sis.readBytes(length); 855 } 856 857 async function decodeCompressedStream(stream, length, encodings) { 858 const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance( 859 Ci.nsIStreamLoader 860 ); 861 const onDecodingComplete = new Promise(resolve => { 862 listener.init({ 863 onStreamComplete: function onStreamComplete( 864 _loader, 865 _context, 866 _status, 867 _length, 868 data 869 ) { 870 // `data`` might be a very large array, chunk calls to fromCharCode to 871 // avoid "RangeError: too many arguments provided for a function call". 872 const CHUNK_SIZE = 65536; 873 let result = ""; 874 for (let i = 0; i < data.length; i += CHUNK_SIZE) { 875 const chunk = data.slice(i, i + CHUNK_SIZE); 876 result += String.fromCharCode.apply(null, chunk); 877 } 878 resolve(result); 879 }, 880 }); 881 }); 882 883 const scs = Cc["@mozilla.org/streamConverters;1"].getService( 884 Ci.nsIStreamConverterService 885 ); 886 887 let converter; 888 let nextListener = listener; 889 for (const encoding of encodings) { 890 // There can be multiple compressions applied 891 converter = scs.asyncConvertData( 892 encoding, 893 "uncompressed", 894 nextListener, 895 null 896 ); 897 nextListener = converter; 898 } 899 900 converter.onStartRequest(null, null); 901 converter.onDataAvailable(null, stream, 0, length); 902 converter.onStopRequest(null, null, null); 903 904 return onDecodingComplete; 905 } 906 907 /** 908 * Remove any frames in a stack that are related to chrome resource files. 909 * 910 * @param array stack 911 * An array of frames, each of which has a 912 * 'filename' property. 913 * @return array 914 * An array of stack frames with any chrome frames removed. 915 * The original array is not modified. 916 */ 917 function removeChromeFrames(stacktrace) { 918 return stacktrace.filter(({ filename }) => { 919 return ( 920 filename && 921 !filename.startsWith("resource://") && 922 !filename.startsWith("chrome://") 923 ); 924 }); 925 } 926 927 export const NetworkUtils = { 928 ACCEPTED_COMPRESSION_ENCODINGS, 929 causeTypeToString, 930 decodeResponseChunks, 931 fetchRequestHeadersAndCookies, 932 fetchResponseHeadersAndCookies, 933 getBlockedReason, 934 getCauseDetails, 935 getChannelBrowsingContextID, 936 getChannelInnerWindowId, 937 getChannelPriority, 938 getCharset, 939 getHttpVersion, 940 getProtocol, 941 getReferrerPolicy, 942 getWebSocketChannel, 943 handleDataChannel, 944 isChannelFromSystemPrincipal, 945 isChannelPrivate, 946 isFromCache, 947 isNavigationRequest, 948 isPreloadRequest, 949 isThirdPartyTrackingResource, 950 matchRequest, 951 NETWORK_EVENT_TYPES, 952 parseEarlyHintsResponseHeaders, 953 removeChromeFrames, 954 setEventAsAvailable, 955 stringToCauseType, 956 };