diff --git a/devtools/server/actors/replay/connection.js b/devtools/server/actors/replay/connection.js index 6b726f6cc5746..b6d6ea0313cad 100644 --- a/devtools/server/actors/replay/connection.js +++ b/devtools/server/actors/replay/connection.js @@ -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( @@ -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); diff --git a/devtools/server/actors/replay/module.js b/devtools/server/actors/replay/module.js index 37525023a1028..b6400796163c2 100644 --- a/devtools/server/actors/replay/module.js +++ b/devtools/server/actors/replay/module.js @@ -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; @@ -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(); @@ -1249,6 +1266,7 @@ if (isRecordingOrReplaying) { let available; try { + // Check status of stream - throws if the stream is closed. available = inputStream.available(); } catch { onStreamEnd(); @@ -1271,7 +1289,6 @@ if (isRecordingOrReplaying) { if (!channel) { return; } - onHttpOpeningRequest(channel.channelId, getChannelRequestData(channel), false); }, "http-on-opening-request"); @@ -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) { @@ -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 }); }, @@ -1377,19 +1403,21 @@ 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); }, }); @@ -1397,16 +1425,29 @@ if (isRecordingOrReplaying) { 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";