Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
loader.lazyRequireGetter(
this,
"NetworkEventActor",
"resource://devtools/server/actors/network-monitor/network-event-actor.js",
true
);
const lazy = {};
ChromeUtils.defineESModuleGetters(
lazy,
{
NetworkUtils:
"resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
},
{ global: "contextual" }
);
// Internal resource types used to create the appropriate network event based
// on where the channel / resource is coming from.
const RESOURCE_TYPES = {
BLOCKED: "blocked-resource",
CACHED: "cached-resource",
DATA_CHANNEL: "data-channel-resource",
};
/**
* Handles network events from the content process
* This currently only handles events for requests (js/css) blocked by CSP.
*/
class NetworkEventContentWatcher {
/**
* Start watching for all network events related to a given Target Actor.
*
* @param TargetActor targetActor
* The target actor in the content process from which we should
* observe network events.
* @param Object options
* Dictionary object with following attributes:
* - onAvailable: mandatory function
* This will be called for each resource.
* - onUpdated: optional function
* This would be called multiple times for each resource.
*/
async watch(targetActor, { onAvailable, onUpdated }) {
// Map from channelId to network event objects.
this.networkEvents = new Map();
this.targetActor = targetActor;
this.onAvailable = onAvailable;
this.onUpdated = onUpdated;
Services.obs.addObserver(
this.httpFailedOpeningRequest,
"http-on-failed-opening-request"
);
Services.obs.addObserver(
this.httpOnResourceCacheResponse,
"http-on-resource-cache-response"
);
Services.obs.addObserver(this.onDataChannelOpened, "data-channel-opened");
}
/**
* Allows clearing of network events
*/
clear() {
this.networkEvents.clear();
}
httpFailedOpeningRequest = subject => {
const channel = subject.QueryInterface(Ci.nsIHttpChannel);
// Ignore preload requests to avoid duplicity request entries in
// the Network panel. If a preload fails (for whatever reason)
// then the platform kicks off another 'real' request.
if (lazy.NetworkUtils.isPreloadRequest(channel)) {
return;
}
if (
!lazy.NetworkUtils.matchRequest(channel, {
targetActor: this.targetActor,
})
) {
return;
}
this.onNetworkEventAvailable(channel, {
networkEventOptions: {
blockedReason: channel.loadInfo.requestBlockingReason,
},
type: RESOURCE_TYPES.BLOCKED,
});
};
httpOnResourceCacheResponse = (subject, topic) => {
if (
topic != "http-on-resource-cache-response" ||
!(subject instanceof Ci.nsIHttpChannel)
) {
return;
}
const channel = subject.QueryInterface(Ci.nsIHttpChannel);
if (
!lazy.NetworkUtils.matchRequest(channel, {
targetActor: this.targetActor,
})
) {
return;
}
if (
channel.loadInfo?.externalContentPolicyType !==
Ci.nsIContentPolicy.TYPE_SCRIPT
) {
// For images and stylesheets from the cache, only one network request
// should be created per URI.
//
// For scripts from the cache, multiple network requests should be
// created.
const hasURI = Array.from(this.networkEvents.values()).some(
networkEvent => networkEvent.uri === channel.URI.spec
);
if (hasURI) {
return;
}
}
this.onNetworkEventAvailable(channel, {
fromCache: true,
networkEventOptions: {},
type: RESOURCE_TYPES.CACHED,
});
};
onDataChannelOpened = (subject, topic) => {
if (
topic != "data-channel-opened" ||
!(subject instanceof Ci.nsIDataChannel)
) {
return;
}
const channel = subject.QueryInterface(Ci.nsIDataChannel);
channel.QueryInterface(Ci.nsIIdentChannel);
channel.QueryInterface(Ci.nsIChannel);
if (channel.isDocument) {
// Navigation data channels are available in the parent process and will
// be monitored there.
return;
}
if (
!lazy.NetworkUtils.matchRequest(channel, {
targetActor: this.targetActor,
})
) {
return;
}
this.onNetworkEventAvailable(channel, {
fromCache: false,
networkEventOptions: {},
type: RESOURCE_TYPES.DATA_CHANNEL,
});
};
onNetworkEventAvailable(channel, { fromCache, networkEventOptions, type }) {
const networkEventActor = new NetworkEventActor(
this.targetActor.conn,
this.targetActor.sessionContext,
{
onNetworkEventUpdate: this.onNetworkEventUpdate.bind(this),
onNetworkEventDestroy: this.onNetworkEventDestroyed.bind(this),
},
networkEventOptions,
channel
);
this.targetActor.manage(networkEventActor);
const resource = networkEventActor.asResource();
const networkEvent = {
browsingContextID: resource.browsingContextID,
innerWindowId: resource.innerWindowId,
resourceId: resource.resourceId,
receivedUpdates: [],
resourceUpdates: {},
uri: channel.URI.spec,
};
// Requests already come with request cookies and headers, so those
// should always be considered as available. But the client still
// heavily relies on those `Available` flags to fetch additional data,
// so it is better to keep them for consistency.
// Set the flags on the resource so that the front-end can fetch
// and display request headers and cookies details asap.
lazy.NetworkUtils.setEventAsAvailable(resource, [
lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_HEADERS,
lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_COOKIES,
]);
this.networkEvents.set(resource.resourceId, networkEvent);
this.onAvailable([resource]);
networkEventActor.addCacheDetails({ fromCache });
if (type == RESOURCE_TYPES.BLOCKED) {
lazy.NetworkUtils.setEventAsAvailable(networkEvent.resourceUpdates, [
lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_END,
]);
this._emitUpdate(networkEvent);
} else if (type == RESOURCE_TYPES.CACHED) {
networkEventActor.addResponseStart({ channel, fromCache: true });
networkEventActor.addEventTimings(
0 /* totalTime */,
{} /* timings */,
{} /* offsets */
);
networkEventActor.addServerTimings({});
networkEventActor.addResponseContent(
{
mimeType: channel.contentType,
size: channel.contentLength,
text: "",
transferredSize: 0,
},
{}
);
} else if (type == RESOURCE_TYPES.DATA_CHANNEL) {
lazy.NetworkUtils.handleDataChannel(channel, networkEventActor);
}
}
onNetworkEventUpdate(updateResource) {
const networkEvent = this.networkEvents.get(updateResource.resourceId);
if (!networkEvent) {
return;
}
const { NETWORK_EVENT_TYPES } = lazy.NetworkUtils;
const { resourceUpdates, receivedUpdates } = networkEvent;
switch (updateResource.updateType) {
case NETWORK_EVENT_TYPES.CACHE_DETAILS:
resourceUpdates.fromCache = updateResource.fromCache;
resourceUpdates.fromServiceWorker = updateResource.fromServiceWorker;
break;
case NETWORK_EVENT_TYPES.RESPONSE_START:
// For cached image requests channel.responseStatus is set to 200 as
// expected. However responseStatusText is empty. In this case fallback
// to the expected statusText "OK".
let statusText = updateResource.statusText;
if (!statusText && updateResource.status === "200") {
statusText = "OK";
}
resourceUpdates.httpVersion = updateResource.httpVersion;
resourceUpdates.status = updateResource.status;
resourceUpdates.statusText = statusText;
resourceUpdates.remoteAddress = updateResource.remoteAddress;
resourceUpdates.remotePort = updateResource.remotePort;
resourceUpdates.waitingTime = updateResource.waitingTime;
lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [
NETWORK_EVENT_TYPES.RESPONSE_COOKIES,
NETWORK_EVENT_TYPES.RESPONSE_HEADERS,
]);
break;
case NETWORK_EVENT_TYPES.RESPONSE_CONTENT:
resourceUpdates.contentSize = updateResource.contentSize;
resourceUpdates.mimeType = updateResource.mimeType;
resourceUpdates.transferredSize = updateResource.transferredSize;
break;
case NETWORK_EVENT_TYPES.EVENT_TIMINGS:
resourceUpdates.totalTime = updateResource.totalTime;
break;
}
lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [
updateResource.updateType,
]);
receivedUpdates.push(updateResource.updateType);
// Here we explicitly call all three `add` helpers on each network event
// actor so in theory we could check only the last one to be called, ie
// responseContent.
const isResponseComplete =
receivedUpdates.includes(NETWORK_EVENT_TYPES.RESPONSE_START) &&
receivedUpdates.includes(NETWORK_EVENT_TYPES.RESPONSE_CONTENT) &&
receivedUpdates.includes(NETWORK_EVENT_TYPES.EVENT_TIMINGS);
if (isResponseComplete) {
// Lets add an event to clearly define the last update expected to be
// emitted. There will be no more updates after this.
lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [
NETWORK_EVENT_TYPES.RESPONSE_END,
]);
}
if (
updateResource.updateType == NETWORK_EVENT_TYPES.RESPONSE_START ||
isResponseComplete
) {
this._emitUpdate(networkEvent);
// clean up already sent updates
networkEvent.resourceUpdates = {};
}
}
_emitUpdate(networkEvent) {
this.onUpdated([
{
resourceId: networkEvent.resourceId,
resourceUpdates: networkEvent.resourceUpdates,
browsingContextID: networkEvent.browsingContextID,
innerWindowId: networkEvent.innerWindowId,
},
]);
}
onNetworkEventDestroyed(channelId) {
if (this.networkEvents.has(channelId)) {
this.networkEvents.delete(channelId);
}
}
destroy() {
this.clear();
Services.obs.removeObserver(
this.httpFailedOpeningRequest,
"http-on-failed-opening-request"
);
Services.obs.removeObserver(
this.httpOnResourceCacheResponse,
"http-on-resource-cache-response"
);
Services.obs.removeObserver(
this.onDataChannelOpened,
"data-channel-opened"
);
}
}
module.exports = NetworkEventContentWatcher;