Skip to content

Commit

Permalink
Support parent/internal redirects in netmon. (#964)
Browse files Browse the repository at this point in the history
Requests that are trapped by a service worker, but then returned to the browser because the service worker didn't handle them, are treated as internal redirects in the browser, and are exposed to the parent process.
  • Loading branch information
kannanvijayan committed Mar 21, 2023
1 parent 8b8ebf1 commit c4fe9dd
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 14 deletions.
69 changes: 69 additions & 0 deletions devtools/server/actors/replay/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const { setTimeout } = Components.utils.import(
"resource://gre/modules/Timer.jsm"
);

const { ComponentUtils } = ChromeUtils.import("resource://gre/modules/ComponentUtils.jsm");
const { EventEmitter } = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm");

const { CryptoUtils } = ChromeUtils.import(
Expand Down Expand Up @@ -1655,6 +1656,74 @@ function sendChannelResponseStart(channel, fromCache, fromServiceWorker) {
});
}

//
// Handle request redirects.
//
const SINK_CLASS_DESCRIPTION = "Replay Networking Event Sink In Parent";
const SINK_CLASS_ID = Components.ID("{e9caba93-270b-486c-95e1-894864b40ac2}");
const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
const SINK_CATEGORY_NAME = "net-channel-event-sinks";
class ReplayChannelEventSinkParent {
asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
let oldChan, newChan, recording, newChanRecording;
try {
oldChan = ensureHttpChannel(oldChannel);
newChan = ensureHttpChannel(newChannel);
recording = getChannelRecording(oldChan);
newChanRecording = getChannelRecording(newChan);
} catch (err) {
ChromeUtils.recordReplayLog(`Failed to get channel ids`);
}

// Sanity check for redirects happening from/to valid channels, and
// the recordings being the same between them.
if (
(oldChan && newChan && recording) &&
(recording === newChanRecording)
) {
// Only send redirect messages to child if the redirect is internal,
// and involves different channel ids.
if (
(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL) &&
(oldChan.channelId !== newChan.channelId)
) {
recording.sendProcessMessage("RecordingChannelInternalRedirect", {
oldChannelId: oldChan.channelId,
newChannelId: newChan.channelId
});
}
} else if (recording) {
const oldChanId = oldChan ? oldChan.channelId : -1;
const newChanId = newChan ? newChan.channelId : -1;
const recordingsSame = recording === newRecording;
ChromeUtils.recordReplayLog(
`Unusable network redirect: ${oldChanId} -> ${newChanId} (sameRecordings=${recordingsSame})`
);
}
callback.onRedirectVerifyCallback(Cr.NS_OK);
}
}
ReplayChannelEventSinkParent.prototype.QueryInterface = ChromeUtils.generateQI([
"nsIChannelEventSink",
]);
const ReplayChannelEventSinkFactory = ComponentUtils.generateSingletonFactory(
ReplayChannelEventSinkParent
);
const registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
registrar.registerFactory(
SINK_CLASS_ID,
SINK_CLASS_DESCRIPTION,
SINK_CONTRACT_ID,
ReplayChannelEventSinkFactory
);
Services.catMan.addCategoryEntry(
SINK_CATEGORY_NAME,
SINK_CONTRACT_ID,
SINK_CONTRACT_ID,
false,
true
);

const distributor =
Cc["@mozilla.org/network/http-activity-distributor;1"]
.getService(Ci.nsIHttpActivityDistributor);
Expand Down
69 changes: 55 additions & 14 deletions devtools/server/actors/replay/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,20 @@ if (isRecordingOrReplaying) {
// it can be applied to the new channel created to handle the redirect.
const gPendingRedirects = new Map();

// ChannelId => ChannelId
// Some redirects are handled by the parent process when a service
// worker is involved. In this case, the parent process will create
// a new channel to handle the redirect, and treat it as an "internal"
// redirect to a new channel, but without any "http-on-opening-request"
// for it. However, "http-on-stop-request" will be fired on the new
// channel.
//
// We want to treat these target channels as equivalent to the original
// channel. This map tracks the original channel for each target channel.
// The various network event handler functions below will map channel
// ids to actual channel ids.
const gInternalRedirects = new Map();

function getChannel(subject) {
if (!(subject instanceof Ci.nsIHttpChannel)) {
return null;
Expand All @@ -1217,9 +1231,12 @@ if (isRecordingOrReplaying) {
const channelId = +data;
const isRequestBody = topic === "replay-request-start";

const streamId = `${isRequestBody ? "request" : "response"}-${channelId}`;
notifyRequestEvent(channelId, isRequestBody ? "request-body" : "response-body", {});
notifyNetworkStreamStart(streamId, isRequestBody ? "request-data" : "response-data", `${channelId}`);
// This may be for an internally redirected request.
const actualChannelId = gInternalRedirects.get(channelId) || channelId;

const streamId = `${isRequestBody ? "request" : "response"}-${actualChannelId}`;
notifyRequestEvent(actualChannelId, isRequestBody ? "request-body" : "response-body", {});
notifyNetworkStreamStart(streamId, isRequestBody ? "request-data" : "response-data", `${actualChannelId}`);

let offset = 0;
listenForStreamData();
Expand Down Expand Up @@ -1249,6 +1266,7 @@ if (isRecordingOrReplaying) {

let available;
try {
// Check status of stream - throws if the stream is closed.
available = inputStream.available();
} catch {
onStreamEnd();
Expand All @@ -1271,7 +1289,6 @@ if (isRecordingOrReplaying) {
if (!channel) {
return;
}

onHttpOpeningRequest(channel.channelId, getChannelRequestData(channel), false);
}, "http-on-opening-request");

Expand Down Expand Up @@ -1339,13 +1356,20 @@ if (isRecordingOrReplaying) {
}, "http-on-stop-request");

function onHttpStopRequest(channel) {
if (!gActiveRequests.has(channel.channelId)) {
// This may be for an internally redirected request.
const actualChannelId =
gInternalRedirects.get(channel.channelId) || channel.channelId;

if (!gActiveRequests.has(actualChannelId)) {
return;
}

gActiveRequests.delete(channel.channelId);
gActiveRequests.delete(actualChannelId);
if (actualChannelId !== channel.channelId) {
gInternalRedirects.delete(channel.channelId);
}

notifyRequestEvent(channel.channelId, "request-done", getChannelRequestDoneData(channel));
notifyRequestEvent(actualChannelId, "request-done", getChannelRequestDoneData(channel));
}

function notifyRequestEvent(channelId, kind, data) {
Expand All @@ -1363,11 +1387,13 @@ if (isRecordingOrReplaying) {
receiveMessage(msg) {
const { channelId, requestRawHeaders } = msg.data;

if (!gActiveRequests.has(channelId)) {
// This may be for an internally redirected request.
const actualChannelId = gInternalRedirects.get(channelId) || channelId;
if (!gActiveRequests.has(actualChannelId)) {
return;
}

notifyRequestEvent(channelId, "request-raw-headers", {
notifyRequestEvent(actualChannelId, "request-raw-headers", {
requestRawHeaders
});
},
Expand All @@ -1377,36 +1403,51 @@ if (isRecordingOrReplaying) {
receiveMessage(msg) {
const { channelId, data } = msg.data;

if (!gActiveRequests.has(channelId)) {
// This may be for an internally redirected request.
const actualChannelId = gInternalRedirects.get(channelId) || channelId;
if (!gActiveRequests.has(actualChannelId)) {
return;
}

const { remoteDestination, ...response } = data;

if (remoteDestination) {
notifyRequestEvent(channelId, "request-destination", {
notifyRequestEvent(actualChannelId, "request-destination", {
destinationAddress: remoteDestination.address,
destinationPort: remoteDestination.port,
});
}
notifyRequestEvent(channelId, "response", response);
notifyRequestEvent(actualChannelId, "response", response);
},
});

Services.cpmm.addMessageListener("RecordingChannelResponseRawHeaders", {
receiveMessage(msg) {
const { channelId, responseRawHeaders } = msg.data;

if (!gActiveRequests.has(channelId)) {
// This may be for an internally redirected request.
const actualChannelId = gInternalRedirects.get(channelId) || channelId;
if (!gActiveRequests.has(actualChannelId)) {
return;
}

notifyRequestEvent(channelId, "response-raw-headers", {
notifyRequestEvent(actualChannelId, "response-raw-headers", {
responseRawHeaders,
});
},
});

Services.cpmm.addMessageListener("RecordingChannelInternalRedirect", {
receiveMessage(msg) {
const { oldChannelId, newChannelId } = msg.data;

if (!gActiveRequests.has(oldChannelId)) {
return;
}
gInternalRedirects.set(newChannelId, oldChannelId);
},
});

const SINK_CLASS_DESCRIPTION = "Replay Networking Event Sink";
const SINK_CLASS_ID = Components.ID("{96692fd5-4d0d-4656-9283-fdf0e3c65553}");
const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
Expand Down

0 comments on commit c4fe9dd

Please sign in to comment.