network-event-actor.js (23257B)
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 { Actor } = require("resource://devtools/shared/protocol.js"); 8 const { 9 networkEventSpec, 10 } = require("resource://devtools/shared/specs/network-event.js"); 11 12 const { 13 TYPES: { NETWORK_EVENT }, 14 } = require("resource://devtools/server/actors/resources/index.js"); 15 const { 16 LongStringActor, 17 } = require("resource://devtools/server/actors/string.js"); 18 19 const lazy = {}; 20 21 ChromeUtils.defineESModuleGetters( 22 lazy, 23 { 24 NetworkUtils: 25 "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs", 26 }, 27 { global: "contextual" } 28 ); 29 30 const CONTENT_TYPE_REGEXP = /^content-type/i; 31 32 const REDIRECT_STATES = [ 33 301, // HTTP Moved Permanently 34 302, // HTTP Found 35 303, // HTTP See Other 36 307, // HTTP Temporary Redirect 37 ]; 38 39 function isDataChannel(channel) { 40 return channel instanceof Ci.nsIDataChannel; 41 } 42 43 function isFileChannel(channel) { 44 return channel instanceof Ci.nsIFileChannel; 45 } 46 47 /** 48 * Creates an actor for a network event. 49 * 50 * @class 51 * @param {DevToolsServerConnection} conn 52 * The connection into which this Actor will be added. 53 * @param {object} sessionContext 54 * The Session Context to help know what is debugged. 55 * See devtools/server/actors/watcher/session-context.js 56 * @param {object} options 57 * Dictionary object with the following attributes: 58 * - onNetworkEventUpdate: optional function 59 * Callback for updates for the network event 60 * - onNetworkEventDestroy: optional function 61 * Callback for the destruction of the network event 62 * @param {object} networkEventOptions 63 * Object describing the network event or the configuration of the 64 * network observer, and which cannot be easily inferred from the raw 65 * channel. 66 * - extension: optional object with `blocking` or `blocked` extension IDs 67 * id of the blocking webextension if any 68 * - blockedReason: optional number or string 69 * - discardRequestBody: boolean 70 * - discardResponseBody: boolean 71 * - fromCache: boolean 72 * - fromServiceWorker: boolean 73 * - timestamp: number 74 * @param {nsIChannel} channel 75 * The channel related to this network event 76 */ 77 class NetworkEventActor extends Actor { 78 constructor( 79 conn, 80 sessionContext, 81 { onNetworkEventUpdate, onNetworkEventDestroy }, 82 networkEventOptions, 83 channel 84 ) { 85 super(conn, networkEventSpec); 86 87 this._sessionContext = sessionContext; 88 this._onNetworkEventUpdate = onNetworkEventUpdate; 89 this._onNetworkEventDestroy = onNetworkEventDestroy; 90 91 // Store the channelId which will act as resource id. 92 this._channelId = channel.channelId; 93 94 this._timings = {}; 95 this._serverTimings = []; 96 97 this._discardRequestBody = !!networkEventOptions.discardRequestBody; 98 this._discardResponseBody = !!networkEventOptions.discardResponseBody; 99 100 this._response = { 101 headers: [], 102 cookies: [], 103 content: {}, 104 }; 105 106 this._earlyHintsResponse = { 107 headers: [], 108 rawHeaders: "", 109 }; 110 111 if (isDataChannel(channel) || isFileChannel(channel)) { 112 this._innerWindowId = lazy.NetworkUtils.getChannelInnerWindowId(channel); 113 this._isNavigationRequest = false; 114 115 this._request = { 116 cookies: [], 117 headers: [], 118 postData: {}, 119 rawHeaders: "", 120 }; 121 this._resource = this._createResource(networkEventOptions, channel); 122 return; 123 } 124 125 // innerWindowId and isNavigationRequest are used to check if the actor 126 // should be destroyed when a window is destroyed. See network-events.js. 127 this._innerWindowId = lazy.NetworkUtils.getChannelInnerWindowId(channel); 128 this._isNavigationRequest = lazy.NetworkUtils.isNavigationRequest(channel); 129 130 // Retrieve cookies and headers from the channel 131 const { cookies, headers } = 132 lazy.NetworkUtils.fetchRequestHeadersAndCookies(channel); 133 134 this._request = { 135 cookies, 136 headers, 137 postData: {}, 138 }; 139 140 this._resource = this._createResource(networkEventOptions, channel); 141 } 142 143 /** 144 * Return the network event actor as a resource, and add the actorID which is 145 * not available in the constructor yet. 146 */ 147 asResource() { 148 return { 149 actor: this.actorID, 150 ...this._resource, 151 }; 152 } 153 154 /** 155 * Create the resource corresponding to this actor. 156 */ 157 _createResource(networkEventOptions, channel) { 158 let wsChannel; 159 let method; 160 if (isDataChannel(channel) || isFileChannel(channel)) { 161 channel.QueryInterface(Ci.nsIChannel); 162 wsChannel = null; 163 method = "GET"; 164 } else { 165 channel = channel.QueryInterface(Ci.nsIHttpChannel); 166 wsChannel = lazy.NetworkUtils.getWebSocketChannel(channel); 167 method = channel.requestMethod; 168 } 169 170 // Use the WebSocket channel URL for websockets. 171 const url = wsChannel ? wsChannel.URI.spec : channel.URI.spec; 172 173 let browsingContextID = 174 lazy.NetworkUtils.getChannelBrowsingContextID(channel); 175 176 // Ensure that we have a browsing context ID for all requests. 177 // Only privileged requests debugged via the Browser Toolbox (sessionContext.type == "all") can be unrelated to any browsing context. 178 if (!browsingContextID && this._sessionContext.type != "all") { 179 throw new Error(`Got a request ${url} without a browsingContextID set`); 180 } 181 182 // The browsingContextID is used by the ResourceCommand on the client 183 // to find the related Target Front. 184 // 185 // For now in the browser and web extension toolboxes, requests 186 // do not relate to any specific WindowGlobalTargetActor 187 // as we are still using a unique target (ParentProcessTargetActor) for everything. 188 if ( 189 this._sessionContext.type == "all" || 190 this._sessionContext.type == "webextension" 191 ) { 192 browsingContextID = -1; 193 } 194 195 const cause = lazy.NetworkUtils.getCauseDetails(channel); 196 // Both xhr and fetch are flagged as XHR in DevTools. 197 const isXHR = cause.type == "xhr" || cause.type == "fetch"; 198 199 // For websocket requests the serial is used instead of the channel id. 200 const stacktraceResourceId = 201 cause.type == "websocket" ? wsChannel.serial : channel.channelId; 202 203 // If a timestamp was provided, it is a high resolution timestamp 204 // corresponding to ACTIVITY_SUBTYPE_REQUEST_HEADER. Fallback to Date.now(). 205 const timeStamp = networkEventOptions.timestamp 206 ? networkEventOptions.timestamp / 1000 207 : Date.now(); 208 209 let blockedReason = networkEventOptions.blockedReason; 210 211 // Check if blockedReason was set to a falsy value, meaning the blocked did 212 // not give an explicit blocked reason. 213 if ( 214 blockedReason === 0 || 215 blockedReason === false || 216 blockedReason === null || 217 blockedReason === "" 218 ) { 219 blockedReason = "unknown"; 220 } 221 222 const resource = { 223 resourceId: this._channelId, 224 resourceType: NETWORK_EVENT, 225 blockedReason, 226 extension: networkEventOptions.extension, 227 browsingContextID, 228 cause, 229 // This is used specifically in the browser toolbox console to distinguish privileged 230 // resources from the parent process from those from the content. 231 chromeContext: lazy.NetworkUtils.isChannelFromSystemPrincipal(channel), 232 innerWindowId: this._innerWindowId, 233 isNavigationRequest: this._isNavigationRequest, 234 isThirdPartyTrackingResource: 235 lazy.NetworkUtils.isThirdPartyTrackingResource(channel), 236 isXHR, 237 method, 238 priority: lazy.NetworkUtils.getChannelPriority(channel), 239 private: lazy.NetworkUtils.isChannelPrivate(channel), 240 referrerPolicy: lazy.NetworkUtils.getReferrerPolicy(channel), 241 stacktraceResourceId, 242 startedDateTime: new Date(timeStamp).toISOString(), 243 securityFlags: channel.loadInfo.securityFlags, 244 timeStamp, 245 timings: {}, 246 url, 247 }; 248 249 return resource; 250 } 251 252 /** 253 * Releases this actor from the pool. 254 */ 255 destroy(conn) { 256 if (!this._channelId) { 257 return; 258 } 259 260 if (this._onNetworkEventDestroy) { 261 this._onNetworkEventDestroy(this._channelId); 262 } 263 264 this._channelId = null; 265 super.destroy(conn); 266 } 267 268 release() { 269 // Per spec, destroy is automatically going to be called after this request 270 } 271 272 getInnerWindowId() { 273 return this._innerWindowId; 274 } 275 276 isNavigationRequest() { 277 return this._isNavigationRequest; 278 } 279 280 /** 281 * The "getRequestHeaders" packet type handler. 282 * 283 * @return object 284 * The response packet - network request headers. 285 */ 286 getRequestHeaders() { 287 let rawHeaders; 288 let headersSize = 0; 289 if (this._request.rawHeaders) { 290 headersSize = this._request.rawHeaders.length; 291 rawHeaders = this._createLongStringActor(this._request.rawHeaders); 292 } 293 294 return { 295 headers: this._request.headers.map(header => ({ 296 name: header.name, 297 value: this._createLongStringActor(header.value), 298 })), 299 headersSize, 300 rawHeaders, 301 }; 302 } 303 304 /** 305 * The "getRequestCookies" packet type handler. 306 * 307 * @return object 308 * The response packet - network request cookies. 309 */ 310 getRequestCookies() { 311 return { 312 cookies: this._request.cookies.map(cookie => ({ 313 name: cookie.name, 314 value: this._createLongStringActor(cookie.value), 315 })), 316 }; 317 } 318 319 /** 320 * The "getRequestPostData" packet type handler. 321 * 322 * @return object 323 * The response packet - network POST data. 324 */ 325 getRequestPostData() { 326 let postDataText; 327 if (this._request.postData.text) { 328 // Create a long string actor for the postData text if needed. 329 postDataText = this._createLongStringActor(this._request.postData.text); 330 } 331 332 return { 333 postData: { 334 size: this._request.postData.size, 335 text: postDataText, 336 }, 337 postDataDiscarded: this._discardRequestBody, 338 }; 339 } 340 341 /** 342 * The "getSecurityInfo" packet type handler. 343 * 344 * @return object 345 * The response packet - connection security information. 346 */ 347 getSecurityInfo() { 348 return { 349 securityInfo: this._securityInfo, 350 }; 351 } 352 353 /** 354 * The "getEarlyHintsResponseHeaders" packet type handler. 355 * 356 * @return object 357 * The response packet - network early hint response headers. 358 */ 359 getEarlyHintsResponseHeaders() { 360 const { rawHeaders, headers } = this._earlyHintsResponse; 361 return { 362 headers: headers.map(header => ({ 363 name: header.name, 364 value: this._createLongStringActor(header.value), 365 })), 366 headersSize: rawHeaders.length, 367 rawHeaders: this._createLongStringActor(rawHeaders), 368 }; 369 } 370 371 /** 372 * The "getResponseHeaders" packet type handler. 373 * 374 * @return object 375 * The response packet - network response headers. 376 */ 377 getResponseHeaders() { 378 let rawHeaders; 379 let headersSize = 0; 380 if (this._response.rawHeaders) { 381 headersSize = this._response.rawHeaders.length; 382 rawHeaders = this._createLongStringActor(this._response.rawHeaders); 383 } 384 385 return { 386 headers: this._response.headers.map(header => ({ 387 name: header.name, 388 value: this._createLongStringActor(header.value), 389 })), 390 headersSize, 391 rawHeaders, 392 }; 393 } 394 395 /** 396 * The "getResponseCache" packet type handler. 397 * 398 * @return object 399 * The cache packet - network cache information. 400 */ 401 getResponseCache() { 402 return { 403 cache: this._response.responseCache, 404 }; 405 } 406 407 /** 408 * The "getResponseCookies" packet type handler. 409 * 410 * @return object 411 * The response packet - network response cookies. 412 */ 413 getResponseCookies() { 414 // As opposed to request cookies, response cookies can come with additional 415 // properties. 416 const cookieOptionalProperties = [ 417 "domain", 418 "expires", 419 "httpOnly", 420 "path", 421 "samesite", 422 "secure", 423 ]; 424 425 return { 426 cookies: this._response.cookies.map(cookie => { 427 const cookieResponse = { 428 name: cookie.name, 429 value: this._createLongStringActor(cookie.value), 430 }; 431 432 for (const prop of cookieOptionalProperties) { 433 if (prop in cookie) { 434 cookieResponse[prop] = cookie[prop]; 435 } 436 } 437 return cookieResponse; 438 }), 439 }; 440 } 441 442 /** 443 * The "getResponseContent" packet type handler. 444 * 445 * @return object 446 * The response packet - network response content. 447 */ 448 getResponseContent() { 449 const content = { ...this._response.content }; 450 if (this._response.contentLongStringActor) { 451 // Remove the old actor from the pool as new actor will be created 452 // with updated content. 453 this.unmanage(this._response.contentLongStringActor); 454 } 455 this._response.contentLongStringActor = new LongStringActor( 456 this.conn, 457 content.text 458 ); 459 // bug 1462561 - Use "json" type and manually manage/marshall actors to workaround 460 // protocol.js performance issue 461 this.manage(this._response.contentLongStringActor); 462 content.text = this._response.contentLongStringActor.form(); 463 464 return { 465 content, 466 contentDiscarded: this._discardResponseBody, 467 }; 468 } 469 470 /** 471 * The "getEventTimings" packet type handler. 472 * 473 * @return object 474 * The response packet - network event timings. 475 */ 476 getEventTimings() { 477 return { 478 timings: this._timings, 479 totalTime: this._totalTime, 480 offsets: this._offsets, 481 serverTimings: this._serverTimings, 482 serviceWorkerTimings: this._serviceWorkerTimings, 483 }; 484 } 485 486 /****************************************************************** 487 * Listeners for new network event data coming from NetworkMonitor. 488 *****************************************************************/ 489 490 addCacheDetails({ fromCache, fromServiceWorker }) { 491 this._resource.fromCache = fromCache; 492 this._resource.fromServiceWorker = fromServiceWorker; 493 this._onEventUpdate(lazy.NetworkUtils.NETWORK_EVENT_TYPES.CACHE_DETAILS, { 494 fromCache, 495 fromServiceWorker, 496 }); 497 } 498 499 addRawHeaders({ channel, rawHeaders }) { 500 this._request.rawHeaders = rawHeaders; 501 502 // For regular requests, some additional headers might only be available 503 // when rawHeaders are provided, so we update the request headers here. 504 const { headers } = 505 lazy.NetworkUtils.fetchRequestHeadersAndCookies(channel); 506 this._request.headers = headers; 507 } 508 509 /** 510 * Add network request POST data. 511 * 512 * @param object postData 513 * The request POST data. 514 */ 515 addRequestPostData(postData) { 516 // Ignore calls when this actor is already destroyed 517 if (this.isDestroyed()) { 518 return; 519 } 520 521 this._request.postData = postData; 522 this._onEventUpdate( 523 lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_POSTDATA, 524 {} 525 ); 526 } 527 528 /** 529 * Add the initial network response information. 530 * 531 * @param {object} options 532 * @param {nsIChannel} options.channel 533 * @param {boolean} options.fromCache 534 * @param {string} options.rawHeaders 535 * @param {string} options.proxyResponseRawHeaders 536 * @param {string} options.earlyHintsResponseRawHeaders 537 */ 538 addResponseStart({ 539 channel, 540 fromCache, 541 rawHeaders = "", 542 proxyResponseRawHeaders, 543 earlyHintsResponseRawHeaders, 544 }) { 545 // Ignore calls when this actor is already destroyed 546 if (this.isDestroyed()) { 547 return; 548 } 549 550 // Avoid reading responseStatus and responseStatusText dynamically from 551 // the channel as much as possible. In some cases, eg 304 Not Modified, the 552 // channel is dynamically replaced with the original channel and we lose the 553 // original information about the channel status as the request progresses. 554 // Reading this synchronously in this method is fine, but we extract them as 555 // separate variables here to bring some attention to this issue. 556 const { responseStatus, responseStatusText } = channel; 557 558 fromCache = fromCache || lazy.NetworkUtils.isFromCache(channel); 559 const isDataOrFile = isDataChannel(channel) || isFileChannel(channel); 560 561 // Read response headers and cookies. 562 let responseHeaders = []; 563 let responseCookies = []; 564 if (!this._blockedReason && !isDataOrFile) { 565 const { cookies, headers } = 566 lazy.NetworkUtils.fetchResponseHeadersAndCookies(channel); 567 responseCookies = cookies; 568 responseHeaders = headers; 569 } 570 571 // Handle response headers 572 this._response.rawHeaders = rawHeaders; 573 this._response.headers = responseHeaders; 574 this._response.cookies = responseCookies; 575 576 // Handle the rest of the response start metadata. 577 this._response.headersSize = rawHeaders ? rawHeaders.length : 0; 578 579 // Early Hint Response Headers 580 if (earlyHintsResponseRawHeaders) { 581 this._earlyHintsResponse.headers = 582 lazy.NetworkUtils.parseEarlyHintsResponseHeaders( 583 earlyHintsResponseRawHeaders 584 ); 585 this._earlyHintsResponse.rawHeaders = earlyHintsResponseRawHeaders; 586 } 587 588 // Discard the response body for known redirect response statuses. 589 if (REDIRECT_STATES.includes(responseStatus)) { 590 this._discardResponseBody = true; 591 } 592 593 // Mime type needs to be sent on response start for identifying an sse channel. 594 const contentTypeHeader = responseHeaders.find(header => 595 CONTENT_TYPE_REGEXP.test(header.name) 596 ); 597 598 let mimeType = ""; 599 if (contentTypeHeader) { 600 mimeType = contentTypeHeader.value; 601 } 602 603 let waitingTime = null; 604 if (!isDataOrFile) { 605 const timedChannel = channel.QueryInterface(Ci.nsITimedChannel); 606 waitingTime = Math.round( 607 (timedChannel.responseStartTime - timedChannel.requestStartTime) / 1000 608 ); 609 } 610 611 let proxyInfo = []; 612 if (proxyResponseRawHeaders) { 613 // The typical format for proxy raw headers is `HTTP/2 200 Connected\r\nConnection: keep-alive` 614 // The content is parsed and split into http version (HTTP/2), status(200) and status text (Connected) 615 proxyInfo = proxyResponseRawHeaders.split("\r\n")[0].split(" "); 616 } 617 618 this._onEventUpdate(lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_START, { 619 httpVersion: isDataOrFile 620 ? null 621 : lazy.NetworkUtils.getHttpVersion(channel), 622 mimeType, 623 remoteAddress: fromCache ? "" : channel.remoteAddress, 624 remotePort: fromCache ? "" : channel.remotePort, 625 status: isDataOrFile ? "200" : responseStatus + "", 626 statusText: isDataOrFile ? "0K" : responseStatusText, 627 earlyHintsStatus: earlyHintsResponseRawHeaders ? "103" : "", 628 waitingTime, 629 isResolvedByTRR: channel.isResolvedByTRR, 630 proxyHttpVersion: proxyInfo[0], 631 proxyStatus: proxyInfo[1], 632 proxyStatusText: proxyInfo[2], 633 }); 634 } 635 636 /** 637 * Add connection security information. 638 * 639 * @param object info 640 * The object containing security information. 641 */ 642 addSecurityInfo(info, isRacing) { 643 // Ignore calls when this actor is already destroyed 644 if (this.isDestroyed()) { 645 return; 646 } 647 648 this._securityInfo = info; 649 650 this._onEventUpdate(lazy.NetworkUtils.NETWORK_EVENT_TYPES.SECURITY_INFO, { 651 state: info.state, 652 isRacing, 653 }); 654 } 655 656 /** 657 * Add network response content end. 658 * 659 * @param object 660 */ 661 addResponseContentComplete({ blockedReason, extension }) { 662 // Ignore calls when this actor is already destroyed 663 if (this.isDestroyed()) { 664 return; 665 } 666 667 this._onEventUpdate( 668 lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_CONTENT_COMPLETE, 669 { 670 blockedReason, 671 extension, 672 } 673 ); 674 } 675 676 /** 677 * Add network response content. 678 * 679 * @param object content 680 * The response content. 681 */ 682 addResponseContent(content, data) { 683 const { blockedReason, extension } = data || {}; 684 685 // Ignore calls when this actor is already destroyed 686 if (this.isDestroyed()) { 687 return; 688 } 689 690 this._response.content = content; 691 this._onEventUpdate( 692 lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_CONTENT, 693 { 694 mimeType: content.mimeType, 695 contentSize: content.size, 696 transferredSize: content.transferredSize, 697 blockedReason, 698 extension, 699 } 700 ); 701 } 702 703 addResponseCache(content) { 704 // Ignore calls when this actor is already destroyed 705 if (this.isDestroyed()) { 706 return; 707 } 708 this._response.responseCache = content.responseCache; 709 this._onEventUpdate( 710 lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_CACHE, 711 {} 712 ); 713 } 714 715 /** 716 * Add network event timing information. 717 * 718 * @param number total 719 * The total time of the network event. 720 * @param object timings 721 * Timing details about the network event. 722 * @param object offsets 723 */ 724 addEventTimings(total, timings, offsets) { 725 // Ignore calls when this actor is already destroyed 726 if (this.isDestroyed()) { 727 return; 728 } 729 730 this._totalTime = total; 731 this._timings = timings; 732 this._offsets = offsets; 733 734 this._onEventUpdate(lazy.NetworkUtils.NETWORK_EVENT_TYPES.EVENT_TIMINGS, { 735 totalTime: total, 736 }); 737 } 738 739 /** 740 * Store server timing information. They are merged together 741 * with network event timing data when they are available and 742 * notification sent to the client. 743 * See `addEventTimings` above for more information. 744 * 745 * @param object serverTimings 746 * Timing details extracted from the Server-Timing header. 747 */ 748 addServerTimings(serverTimings) { 749 if (!serverTimings || this.isDestroyed()) { 750 return; 751 } 752 this._serverTimings = serverTimings; 753 } 754 755 /** 756 * Store service worker timing information. They are merged together 757 * with network event timing data when they are available and 758 * notification sent to the client. 759 * See `addEventTimnings`` above for more information. 760 * 761 * @param object serviceWorkerTimings 762 * Timing details extracted from the Timed Channel. 763 */ 764 addServiceWorkerTimings(serviceWorkerTimings) { 765 if (!serviceWorkerTimings || this.isDestroyed()) { 766 return; 767 } 768 this._serviceWorkerTimings = serviceWorkerTimings; 769 } 770 771 _createLongStringActor(string) { 772 if (string?.actorID) { 773 return string; 774 } 775 776 const longStringActor = new LongStringActor(this.conn, string); 777 // bug 1462561 - Use "json" type and manually manage/marshall actors to workaround 778 // protocol.js performance issue 779 this.manage(longStringActor); 780 return longStringActor.form(); 781 } 782 783 /** 784 * Sends the updated event data to the client 785 * 786 * @private 787 * @param string updateType 788 * @param object data 789 * The properties that have changed for the event 790 */ 791 _onEventUpdate(updateType, data) { 792 if (this._onNetworkEventUpdate) { 793 this._onNetworkEventUpdate({ 794 resourceId: this._channelId, 795 updateType, 796 ...data, 797 }); 798 } 799 } 800 } 801 802 exports.NetworkEventActor = NetworkEventActor;