333 lines
9.6 KiB
JavaScript
333 lines
9.6 KiB
JavaScript
/* 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: {
|
|
// 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.
|
|
requestCookiesAvailable: true,
|
|
requestHeadersAvailable: true,
|
|
},
|
|
uri: channel.URI.spec,
|
|
};
|
|
this.networkEvents.set(resource.resourceId, networkEvent);
|
|
|
|
this.onAvailable([resource]);
|
|
|
|
networkEventActor.addCacheDetails({ fromCache });
|
|
if (type == RESOURCE_TYPES.BLOCKED) {
|
|
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 { resourceUpdates, receivedUpdates } = networkEvent;
|
|
|
|
switch (updateResource.updateType) {
|
|
case "cacheDetails":
|
|
resourceUpdates.fromCache = updateResource.fromCache;
|
|
resourceUpdates.fromServiceWorker = updateResource.fromServiceWorker;
|
|
break;
|
|
case "responseStart":
|
|
// 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;
|
|
|
|
resourceUpdates.responseHeadersAvailable = true;
|
|
resourceUpdates.responseCookiesAvailable = true;
|
|
break;
|
|
case "responseContent":
|
|
resourceUpdates.contentSize = updateResource.contentSize;
|
|
resourceUpdates.mimeType = updateResource.mimeType;
|
|
resourceUpdates.transferredSize = updateResource.transferredSize;
|
|
break;
|
|
case "eventTimings":
|
|
resourceUpdates.totalTime = updateResource.totalTime;
|
|
break;
|
|
}
|
|
|
|
resourceUpdates[`${updateResource.updateType}Available`] = true;
|
|
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 isComplete =
|
|
receivedUpdates.includes("responseStart") &&
|
|
receivedUpdates.includes("responseContent") &&
|
|
receivedUpdates.includes("eventTimings");
|
|
|
|
if (isComplete) {
|
|
this._emitUpdate(networkEvent);
|
|
}
|
|
}
|
|
|
|
_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;
|