Dmitry Gozman f2af30cf90
browser(firefox): properly instrument requests intercepted by service worker (#2594)
When httpChannel is intercepted by Service Worker:
- it gets an internal redirect to another channel with the same id;
- once serivce worker responds, the channel gets the data, but
  does not get any onResponse notifications.

So, we update our ResponseBodyListener (the nsIRequestObserver implementation)
to the new request and force onResponse from there once data is available or
request finishes.
2020-06-16 17:19:01 -07:00

871 lines
32 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";
const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
const {CommonUtils} = ChromeUtils.import("resource://services-common/utils.js");
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
const Cm = Components.manager;
const CC = Components.Constructor;
const helper = new Helper();
const UINT32_MAX = Math.pow(2, 32)-1;
const BinaryInputStream = CC('@mozilla.org/binaryinputstream;1', 'nsIBinaryInputStream', 'setInputStream');
const BinaryOutputStream = CC('@mozilla.org/binaryoutputstream;1', 'nsIBinaryOutputStream', 'setOutputStream');
const StorageStream = CC('@mozilla.org/storagestream;1', 'nsIStorageStream', 'init');
// Cap response storage with 100Mb per tracked tab.
const MAX_RESPONSE_STORAGE_SIZE = 100 * 1024 * 1024;
/**
* This is a nsIChannelEventSink implementation that monitors channel redirects.
*/
const SINK_CLASS_DESCRIPTION = "Juggler NetworkMonitor Channel Event Sink";
const SINK_CLASS_ID = Components.ID("{c2b4c83e-607a-405a-beab-0ef5dbfb7617}");
const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
const SINK_CATEGORY_NAME = "net-channel-event-sinks";
const pageNetworkSymbol = Symbol('PageNetwork');
class PageNetwork {
static _forPageTarget(networkObserver, target) {
let result = target[pageNetworkSymbol];
if (!result) {
result = new PageNetwork(networkObserver, target);
target[pageNetworkSymbol] = result;
}
return result;
}
constructor(networkObserver, target) {
EventEmitter.decorate(this);
this._networkObserver = networkObserver;
this._target = target;
this._sessionCount = 0;
this._extraHTTPHeaders = null;
this._responseStorage = null;
this._requestInterceptionEnabled = false;
this._requestIdToInterceptor = null;
}
addSession() {
if (this._sessionCount === 0) {
this._responseStorage = new ResponseStorage(this._networkObserver, MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10);
}
++this._sessionCount;
return () => this._stopTracking();
}
_stopTracking() {
--this._sessionCount;
if (this._sessionCount === 0) {
this._extraHTTPHeaders = null;
this._responseStorage = null;
this._requestInterceptionEnabled = false;
this._requestIdToInterceptor = null;
}
}
_isActive() {
return this._sessionCount > 0;
}
setExtraHTTPHeaders(headers) {
this._extraHTTPHeaders = headers;
}
enableRequestInterception() {
this._requestInterceptionEnabled = true;
}
disableRequestInterception() {
this._requestInterceptionEnabled = false;
const interceptors = this._requestIdToInterceptor;
if (!interceptors)
return;
this._requestIdToInterceptor = null;
for (const interceptor of interceptors.values())
interceptor._resume();
}
resumeInterceptedRequest(requestId, method, headers, postData) {
this._takeInterceptor(requestId)._resume(method, headers, postData);
}
fulfillInterceptedRequest(requestId, status, statusText, headers, base64body) {
this._takeInterceptor(requestId)._fulfill(status, statusText, headers, base64body);
}
abortInterceptedRequest(requestId, errorCode) {
this._takeInterceptor(requestId)._abort(errorCode);
}
getResponseBody(requestId) {
if (!this._responseStorage)
throw new Error('Responses are not tracked for the given browser');
return this._responseStorage.getBase64EncodedResponse(requestId);
}
_ensureInterceptors() {
if (!this._requestIdToInterceptor)
this._requestIdToInterceptor = new Map();
return this._requestIdToInterceptor;
}
_takeInterceptor(requestId) {
const interceptors = this._requestIdToInterceptor;
if (!interceptors)
throw new Error(`Request interception is not enabled`);
const interceptor = interceptors.get(requestId);
if (!interceptor)
throw new Error(`Cannot find request "${requestId}"`);
interceptors.delete(requestId);
return interceptor;
}
}
class NetworkObserver {
static instance() {
return NetworkObserver._instance || null;
}
constructor(targetRegistry) {
EventEmitter.decorate(this);
NetworkObserver._instance = this;
this._targetRegistry = targetRegistry;
this._activityDistributor = Cc["@mozilla.org/network/http-activity-distributor;1"].getService(Ci.nsIHttpActivityDistributor);
this._activityDistributor.addObserver(this);
this._redirectMap = new Map(); // oldId => newId
this._resumedRequestIdToHeaders = new Map(); // requestId => { headers }
this._postResumeChannelIdToRequestId = new Map(); // post-resume channel id => pre-resume request id
this._pendingAuthentication = new Set(); // pre-auth id
this._postAuthChannelIdToRequestId = new Map(); // pre-auth id => post-auth id
this._bodyListeners = new Map(); // channel id => ResponseBodyListener.
this._channelsReceivedOnResponse = new Set(); // channel ids that have seen onResponse.
const protocolProxyService = Cc['@mozilla.org/network/protocol-proxy-service;1'].getService();
this._channelProxyFilter = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIProtocolProxyChannelFilter]),
applyFilter: (channel, defaultProxyInfo, proxyFilter) => {
const originAttributes = channel.loadInfo && channel.loadInfo.originAttributes;
const browserContext = originAttributes ? this._targetRegistry.browserContextForUserContextId(originAttributes.userContextId) : null;
const proxy = browserContext ? browserContext.proxy : null;
if (!proxy) {
proxyFilter.onProxyFilterResult(defaultProxyInfo);
return;
}
if (proxy.bypass.some(domain => channel.URI.host.endsWith(domain))) {
proxyFilter.onProxyFilterResult(defaultProxyInfo);
return;
}
proxyFilter.onProxyFilterResult(protocolProxyService.newProxyInfo(
proxy.type,
proxy.host,
proxy.port,
'', /* aProxyAuthorizationHeader */
'', /* aConnectionIsolationKey */
0, /* aFlags */
UINT32_MAX, /* aFailoverTimeout */
null, /* failover proxy */
));
},
};
protocolProxyService.registerChannelFilter(this._channelProxyFilter, 0 /* position */);
this._channelSink = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]),
asyncOnChannelRedirect: (oldChannel, newChannel, flags, callback) => {
this._onRedirect(oldChannel, newChannel, flags);
callback.onRedirectVerifyCallback(Cr.NS_OK);
},
};
this._channelSinkFactory = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIFactory]),
createInstance: (aOuter, aIID) => this._channelSink.QueryInterface(aIID),
};
// Register self as ChannelEventSink to track redirects.
const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
registrar.registerFactory(SINK_CLASS_ID, SINK_CLASS_DESCRIPTION, SINK_CONTRACT_ID, this._channelSinkFactory);
Services.catMan.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, SINK_CONTRACT_ID, false, true);
this._eventListeners = [
helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'),
helper.addObserver(this._onResponse.bind(this, false /* fromCache */), 'http-on-examine-response'),
helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-cached-response'),
helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-merged-response'),
];
}
_requestAuthenticated(httpChannel) {
this._pendingAuthentication.add(httpChannel.channelId + '');
}
_requestIdBeforeAuthentication(httpChannel) {
const id = httpChannel.channelId + '';
return this._postAuthChannelIdToRequestId.has(id) ? id : undefined;
}
_requestId(httpChannel) {
const id = httpChannel.channelId + '';
return this._postResumeChannelIdToRequestId.get(id) || this._postAuthChannelIdToRequestId.get(id) || id;
}
_onRedirect(oldChannel, newChannel, flags) {
if (!(oldChannel instanceof Ci.nsIHttpChannel) || !(newChannel instanceof Ci.nsIHttpChannel))
return;
const oldHttpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel);
const newHttpChannel = newChannel.QueryInterface(Ci.nsIHttpChannel);
const pageNetwork = this._pageNetworkForChannel(oldHttpChannel);
if (!pageNetwork)
return;
const oldRequestId = this._requestId(oldHttpChannel);
const newRequestId = this._requestId(newHttpChannel);
if (this._resumedRequestIdToHeaders.has(oldRequestId)) {
// When we call resetInterception on a request, we get a new "redirected" request for it.
const { method, headers, postData } = this._resumedRequestIdToHeaders.get(oldRequestId);
if (headers) {
// Apply new request headers from interception resume.
for (const header of requestHeaders(newChannel))
newChannel.setRequestHeader(header.name, '', false /* merge */);
for (const header of headers)
newChannel.setRequestHeader(header.name, header.value, false /* merge */);
}
if (method)
newChannel.requestMethod = method;
if (postData && newChannel instanceof Ci.nsIUploadChannel) {
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
synthesized.data = atob(postData);
newChannel.setUploadStream(synthesized, 'application/octet-stream', -1);
}
// Use the old request id for the new "redirected" request for protocol consistency.
this._resumedRequestIdToHeaders.delete(oldRequestId);
this._postResumeChannelIdToRequestId.set(newRequestId, oldRequestId);
} else if (!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL)) {
// Regular (non-internal) redirect.
this._redirectMap.set(newRequestId, oldRequestId);
} else {
// Requests intercepted by Service Worker get redirected to a different channel with the same id.
// In addition, they do not receive onResponse. We update body listener to use the new channel,
// and it will ensure onResponse before any data is available or request finishes.
const bodyListener = this._bodyListeners.get(oldHttpChannel.channelId + '');
if (bodyListener)
bodyListener._httpChannel = newHttpChannel;
}
}
observeActivity(channel, activityType, activitySubtype, timestamp, extraSizeData, extraStringData) {
if (activityType !== Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION)
return;
if (!(channel instanceof Ci.nsIHttpChannel))
return;
const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
const pageNetwork = this._pageNetworkForChannel(httpChannel);
if (!pageNetwork)
return;
if (activitySubtype !== Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE)
return;
if (this._isResumedChannel(httpChannel))
return;
if (this._requestIdBeforeAuthentication(httpChannel))
return;
this._sendOnRequestFinished(pageNetwork, httpChannel);
}
pageNetworkForTarget(target) {
return PageNetwork._forPageTarget(this, target);
}
_pageNetworkForChannel(httpChannel) {
let loadContext = helper.getLoadContext(httpChannel);
if (!loadContext)
return;
const target = this._targetRegistry.targetForBrowser(loadContext.topFrameElement);
if (!target)
return;
const pageNetwork = PageNetwork._forPageTarget(this, target);
if (!pageNetwork._isActive())
return;
return pageNetwork;
}
_isResumedChannel(httpChannel) {
return this._postResumeChannelIdToRequestId.has(httpChannel.channelId + '');
}
_onRequest(channel, topic) {
if (!(channel instanceof Ci.nsIHttpChannel))
return;
const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
const pageNetwork = this._pageNetworkForChannel(httpChannel);
if (!pageNetwork)
return;
if (this._isResumedChannel(httpChannel)) {
// Ignore onRequest for resumed requests, but listen to their response.
new ResponseBodyListener(this, pageNetwork, httpChannel);
return;
}
// Convert pending auth bit into auth mapping.
const channelId = httpChannel.channelId + '';
if (this._pendingAuthentication.has(channelId)) {
this._postAuthChannelIdToRequestId.set(channelId, channelId + '-auth');
this._redirectMap.set(channelId + '-auth', channelId);
this._pendingAuthentication.delete(channelId);
const bodyListener = this._bodyListeners.get(channelId);
if (bodyListener)
bodyListener.dispose();
}
const browserContext = pageNetwork._target.browserContext();
if (browserContext)
this._appendExtraHTTPHeaders(httpChannel, browserContext.extraHTTPHeaders);
this._appendExtraHTTPHeaders(httpChannel, pageNetwork._extraHTTPHeaders);
const requestId = this._requestId(httpChannel);
const isRedirect = this._redirectMap.has(requestId);
const interceptionEnabled = this._isInterceptionEnabledForPage(pageNetwork);
if (!interceptionEnabled) {
new NotificationCallbacks(this, pageNetwork, httpChannel, false);
this._sendOnRequest(httpChannel, false);
new ResponseBodyListener(this, pageNetwork, httpChannel);
} else if (isRedirect) {
// We pretend that redirect is interceptable in the protocol, although it's actually not
// and therefore we do not instantiate the interceptor.
// TODO: look into REDIRECT_MODE_MANUAL.
const interceptors = pageNetwork._ensureInterceptors();
interceptors.set(requestId, {
_resume: () => {},
_abort: () => {},
_fulfill: () => {},
});
new NotificationCallbacks(this, pageNetwork, httpChannel, false);
this._sendOnRequest(httpChannel, true);
new ResponseBodyListener(this, pageNetwork, httpChannel);
} else {
const previousCallbacks = httpChannel.notificationCallbacks;
if (previousCallbacks instanceof Ci.nsIInterfaceRequestor) {
const interceptor = previousCallbacks.getInterface(Ci.nsINetworkInterceptController);
// We assume that interceptor is a service worker if there is one.
if (interceptor && interceptor.shouldPrepareForIntercept(httpChannel.URI, httpChannel)) {
new NotificationCallbacks(this, pageNetwork, httpChannel, false);
this._sendOnRequest(httpChannel, false);
new ResponseBodyListener(this, pageNetwork, httpChannel);
} else {
// We'll issue onRequest once it's intercepted.
new NotificationCallbacks(this, pageNetwork, httpChannel, true);
}
} else {
// We'll issue onRequest once it's intercepted.
new NotificationCallbacks(this, pageNetwork, httpChannel, true);
}
}
}
_isInterceptionEnabledForPage(pageNetwork) {
if (pageNetwork._requestInterceptionEnabled)
return true;
const browserContext = pageNetwork._target.browserContext();
if (browserContext && browserContext.requestInterceptionEnabled)
return true;
if (browserContext && browserContext.settings.onlineOverride === 'offline')
return true;
return false;
}
_appendExtraHTTPHeaders(httpChannel, headers) {
if (!headers)
return;
for (const header of headers)
httpChannel.setRequestHeader(header.name, header.value, false /* merge */);
}
_onIntercepted(httpChannel, interceptor) {
const pageNetwork = this._pageNetworkForChannel(httpChannel);
if (!pageNetwork) {
interceptor._resume();
return;
}
const browserContext = pageNetwork._target.browserContext();
if (browserContext && browserContext.settings.onlineOverride === 'offline') {
interceptor._abort(Cr.NS_ERROR_OFFLINE);
return;
}
const interceptionEnabled = this._isInterceptionEnabledForPage(pageNetwork);
this._sendOnRequest(httpChannel, !!interceptionEnabled);
if (interceptionEnabled)
pageNetwork._ensureInterceptors().set(this._requestId(httpChannel), interceptor);
else
interceptor._resume();
}
_sendOnRequest(httpChannel, isIntercepted) {
const pageNetwork = this._pageNetworkForChannel(httpChannel);
if (!pageNetwork)
return;
const causeType = httpChannel.loadInfo ? httpChannel.loadInfo.externalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER;
const internalCauseType = httpChannel.loadInfo ? httpChannel.loadInfo.internalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER;
const requestId = this._requestId(httpChannel);
const redirectedFrom = this._redirectMap.get(requestId);
this._redirectMap.delete(requestId);
pageNetwork.emit(PageNetwork.Events.Request, httpChannel, {
url: httpChannel.URI.spec,
isIntercepted,
requestId,
redirectedFrom,
postData: readRequestPostData(httpChannel),
headers: requestHeaders(httpChannel),
method: httpChannel.requestMethod,
navigationId: httpChannel.isMainDocumentChannel ? this._requestIdBeforeAuthentication(httpChannel) || this._requestId(httpChannel) : undefined,
cause: causeTypeToString(causeType),
internalCause: causeTypeToString(internalCauseType),
});
}
_sendOnRequestFinished(pageNetwork, httpChannel) {
pageNetwork.emit(PageNetwork.Events.RequestFinished, httpChannel, {
requestId: this._requestId(httpChannel),
});
this._cleanupChannelState(httpChannel);
}
_sendOnRequestFailed(pageNetwork, httpChannel, error) {
pageNetwork.emit(PageNetwork.Events.RequestFailed, httpChannel, {
requestId: this._requestId(httpChannel),
errorCode: helper.getNetworkErrorStatusText(error),
});
this._cleanupChannelState(httpChannel);
}
_cleanupChannelState(httpChannel) {
const id = httpChannel.channelId + '';
this._postResumeChannelIdToRequestId.delete(id);
this._postAuthChannelIdToRequestId.delete(id);
this._channelsReceivedOnResponse.delete(id);
}
_onResponse(fromCache, httpChannel, topic) {
if (this._channelsReceivedOnResponse.has(httpChannel.channelId + '')) {
// We can come here twice because of service workers, see ResponseBodyLoader.
return;
}
this._channelsReceivedOnResponse.add(httpChannel.channelId + '');
const pageNetwork = this._pageNetworkForChannel(httpChannel);
if (!pageNetwork)
return;
httpChannel.QueryInterface(Ci.nsIHttpChannelInternal);
const headers = [];
httpChannel.visitResponseHeaders({
visitHeader: (name, value) => headers.push({name, value}),
});
let remoteIPAddress = undefined;
let remotePort = undefined;
try {
remoteIPAddress = httpChannel.remoteAddress;
remotePort = httpChannel.remotePort;
} catch (e) {
// remoteAddress is not defined for cached requests.
}
pageNetwork.emit(PageNetwork.Events.Response, httpChannel, {
requestId: this._requestId(httpChannel),
securityDetails: getSecurityDetails(httpChannel),
fromCache,
headers,
remoteIPAddress,
remotePort,
status: httpChannel.responseStatus,
statusText: httpChannel.responseStatusText,
});
}
_onResponseFinished(pageNetwork, httpChannel, body) {
if (!pageNetwork._isActive())
return;
pageNetwork._responseStorage.addResponseBody(httpChannel, body);
this._sendOnRequestFinished(pageNetwork, httpChannel);
}
dispose() {
this._activityDistributor.removeObserver(this);
const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
registrar.unregisterFactory(SINK_CLASS_ID, this._channelSinkFactory);
Services.catMan.deleteCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, false);
helper.removeListeners(this._eventListeners);
}
}
const protocolVersionNames = {
[Ci.nsITransportSecurityInfo.TLS_VERSION_1]: 'TLS 1',
[Ci.nsITransportSecurityInfo.TLS_VERSION_1_1]: 'TLS 1.1',
[Ci.nsITransportSecurityInfo.TLS_VERSION_1_2]: 'TLS 1.2',
[Ci.nsITransportSecurityInfo.TLS_VERSION_1_3]: 'TLS 1.3',
};
function getSecurityDetails(httpChannel) {
const securityInfo = httpChannel.securityInfo;
if (!securityInfo)
return null;
securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
if (!securityInfo.serverCert)
return null;
return {
protocol: protocolVersionNames[securityInfo.protocolVersion] || '<unknown>',
subjectName: securityInfo.serverCert.commonName,
issuer: securityInfo.serverCert.issuerCommonName,
// Convert to seconds.
validFrom: securityInfo.serverCert.validity.notBefore / 1000 / 1000,
validTo: securityInfo.serverCert.validity.notAfter / 1000 / 1000,
};
}
function readRequestPostData(httpChannel) {
if (!(httpChannel instanceof Ci.nsIUploadChannel))
return undefined;
const iStream = httpChannel.uploadStream;
if (!iStream)
return undefined;
const isSeekableStream = iStream instanceof Ci.nsISeekableStream;
let prevOffset;
if (isSeekableStream) {
prevOffset = iStream.tell();
iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
}
// Read data from the stream.
let text = undefined;
try {
text = NetUtil.readInputStreamToString(iStream, iStream.available());
const converter = Cc['@mozilla.org/intl/scriptableunicodeconverter']
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = 'UTF-8';
text = converter.ConvertToUnicode(text);
} catch (err) {
text = undefined;
}
// Seek locks the file, so seek to the beginning only if necko hasn't
// read it yet, since necko doesn't seek to 0 before reading (at lest
// not till 459384 is fixed).
if (isSeekableStream && prevOffset == 0)
iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
return text;
}
function requestHeaders(httpChannel) {
const headers = [];
httpChannel.visitRequestHeaders({
visitHeader: (name, value) => headers.push({name, value}),
});
return headers;
}
function causeTypeToString(causeType) {
for (let key in Ci.nsIContentPolicy) {
if (Ci.nsIContentPolicy[key] === causeType)
return key;
}
return 'TYPE_OTHER';
}
class ResponseStorage {
constructor(networkObserver, maxTotalSize, maxResponseSize) {
this._networkObserver = networkObserver;
this._totalSize = 0;
this._maxResponseSize = maxResponseSize;
this._maxTotalSize = maxTotalSize;
this._responses = new Map();
}
addResponseBody(httpChannel, body) {
if (body.length > this._maxResponseSize) {
this._responses.set(requestId, {
evicted: true,
body: '',
});
return;
}
let encodings = [];
if ((httpChannel instanceof Ci.nsIEncodedChannel) && httpChannel.contentEncodings && !httpChannel.applyConversion) {
const encodingHeader = httpChannel.getResponseHeader("Content-Encoding");
encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
}
this._responses.set(this._networkObserver._requestId(httpChannel), {body, encodings});
this._totalSize += body.length;
if (this._totalSize > this._maxTotalSize) {
for (let [requestId, response] of this._responses) {
this._totalSize -= response.body.length;
response.body = '';
response.evicted = true;
if (this._totalSize < this._maxTotalSize)
break;
}
}
}
getBase64EncodedResponse(requestId) {
const response = this._responses.get(requestId);
if (!response)
throw new Error(`Request "${requestId}" is not found`);
if (response.evicted)
return {base64body: '', evicted: true};
let result = response.body;
if (response.encodings && response.encodings.length) {
for (const encoding of response.encodings)
result = CommonUtils.convertString(result, encoding, 'uncompressed');
}
return {base64body: btoa(result)};
}
}
class ResponseBodyListener {
constructor(networkObserver, pageNetwork, httpChannel) {
this._networkObserver = networkObserver;
this._pageNetwork = pageNetwork;
this._httpChannel = httpChannel;
this._chunks = [];
this.QueryInterface = ChromeUtils.generateQI([Ci.nsIStreamListener]);
httpChannel.QueryInterface(Ci.nsITraceableChannel);
this.originalListener = httpChannel.setNewListener(this);
this._disposed = false;
this._networkObserver._bodyListeners.set(this._httpChannel.channelId + '', this);
}
_ensureOnResponse() {
// For requests intercepted by Service Worker, we do not get onResponse normally,
// but we do get nsIRequestObserver notifications.
this._networkObserver._onResponse(false /* fromCache */, this._httpChannel, '');
}
onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
if (this._disposed) {
try {
this.originalListener.onDataAvailable(aRequest, aInputStream, aOffset, aCount);
} catch (e) {
// Be ready to original listener exceptions.
}
return;
}
this._ensureOnResponse();
const iStream = new BinaryInputStream(aInputStream);
const sStream = new StorageStream(8192, aCount, null);
const oStream = new BinaryOutputStream(sStream.getOutputStream(0));
// Copy received data as they come.
const data = iStream.readBytes(aCount);
this._chunks.push(data);
oStream.writeBytes(data, aCount);
try {
this.originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount);
} catch (e) {
// Be ready to original listener exceptions.
}
}
onStartRequest(aRequest) {
try {
this.originalListener.onStartRequest(aRequest);
} catch (e) {
// Be ready to original listener exceptions.
}
}
onStopRequest(aRequest, aStatusCode) {
try {
this.originalListener.onStopRequest(aRequest, aStatusCode);
} catch (e) {
// Be ready to original listener exceptions.
}
if (this._disposed)
return;
if (aStatusCode === 0) {
this._ensureOnResponse();
const body = this._chunks.join('');
this._networkObserver._onResponseFinished(this._pageNetwork, this._httpChannel, body);
} else {
this._networkObserver._sendOnRequestFailed(this._pageNetwork, this._httpChannel, aStatusCode);
}
delete this._chunks;
this.dispose();
}
dispose() {
this._disposed = true;
this._networkObserver._bodyListeners.delete(this._httpChannel.channelId + '');
}
}
class NotificationCallbacks {
constructor(networkObserver, pageNetwork, httpChannel, shouldIntercept) {
this._networkObserver = networkObserver;
this._pageNetwork = pageNetwork;
this._shouldIntercept = shouldIntercept;
this._httpChannel = httpChannel;
this._previousCallbacks = httpChannel.notificationCallbacks;
httpChannel.notificationCallbacks = this;
const qis = [
Ci.nsIAuthPrompt2,
Ci.nsIAuthPromptProvider,
Ci.nsIInterfaceRequestor,
];
if (shouldIntercept)
qis.push(Ci.nsINetworkInterceptController);
this.QueryInterface = ChromeUtils.generateQI(qis);
}
getInterface(iid) {
if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPromptProvider))
return this;
if (this._shouldIntercept && iid.equals(Ci.nsINetworkInterceptController))
return this;
if (iid.equals(Ci.nsIAuthPrompt)) // Block nsIAuthPrompt - we want nsIAuthPrompt2 to be used instead.
throw Cr.NS_ERROR_NO_INTERFACE;
if (this._previousCallbacks)
return this._previousCallbacks.getInterface(iid);
throw Cr.NS_ERROR_NO_INTERFACE;
}
_forward(iid, method, args) {
if (!this._previousCallbacks)
return;
try {
const impl = this._previousCallbacks.getInterface(iid);
impl[method].apply(impl, args);
} catch (e) {
if (e.result != Cr.NS_ERROR_NO_INTERFACE)
throw e;
}
}
// nsIAuthPromptProvider
getAuthPrompt(aPromptReason, iid) {
return this;
}
// nsIAuthPrompt2
asyncPromptAuth(aChannel, aCallback, aContext, level, authInfo) {
let canceled = false;
Promise.resolve().then(() => {
if (canceled)
return;
const hasAuth = this.promptAuth(aChannel, level, authInfo);
if (hasAuth)
aCallback.onAuthAvailable(aContext, authInfo);
else
aCallback.onAuthCancelled(aContext, true);
});
return {
QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
cancel: () => {
aCallback.onAuthCancelled(aContext, false);
canceled = true;
}
};
}
// nsIAuthPrompt2
promptAuth(aChannel, level, authInfo) {
if (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED)
return false;
const browserContext = this._pageNetwork._target.browserContext();
const credentials = browserContext ? browserContext.httpCredentials : undefined;
if (!credentials)
return false;
authInfo.username = credentials.username;
authInfo.password = credentials.password;
this._networkObserver._requestAuthenticated(this._httpChannel);
return true;
}
// nsINetworkInterceptController
shouldPrepareForIntercept(aURI, channel) {
if (!(channel instanceof Ci.nsIHttpChannel))
return false;
const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
return httpChannel.channelId === this._httpChannel.channelId;
}
// nsINetworkInterceptController
channelIntercepted(intercepted) {
this._intercepted = intercepted.QueryInterface(Ci.nsIInterceptedChannel);
const httpChannel = this._intercepted.channel.QueryInterface(Ci.nsIHttpChannel);
this._networkObserver._onIntercepted(httpChannel, this);
}
_resume(method, headers, postData) {
this._networkObserver._resumedRequestIdToHeaders.set(this._networkObserver._requestId(this._httpChannel), { method, headers, postData });
this._intercepted.resetInterception();
}
_fulfill(status, statusText, headers, base64body) {
this._intercepted.synthesizeStatus(status, statusText);
for (const header of headers)
this._intercepted.synthesizeHeader(header.name, header.value);
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
const body = base64body ? atob(base64body) : '';
synthesized.data = body;
this._intercepted.startSynthesizedResponse(synthesized, null, null, '', false);
this._intercepted.finishSynthesizedResponse();
this._pageNetwork.emit(PageNetwork.Events.Response, this._httpChannel, {
requestId: this._networkObserver._requestId(this._httpChannel),
securityDetails: null,
fromCache: false,
headers,
status,
statusText,
});
this._networkObserver._onResponseFinished(this._pageNetwork, this._httpChannel, body);
}
_abort(errorCode) {
const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE;
this._intercepted.cancelInterception(error);
this._networkObserver._sendOnRequestFailed(this._pageNetwork, this._httpChannel, error);
}
}
const errorMap = {
'aborted': Cr.NS_ERROR_ABORT,
'accessdenied': Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED,
'addressunreachable': Cr.NS_ERROR_UNKNOWN_HOST,
'blockedbyclient': Cr.NS_ERROR_FAILURE,
'blockedbyresponse': Cr.NS_ERROR_FAILURE,
'connectionaborted': Cr.NS_ERROR_NET_INTERRUPT,
'connectionclosed': Cr.NS_ERROR_FAILURE,
'connectionfailed': Cr.NS_ERROR_FAILURE,
'connectionrefused': Cr.NS_ERROR_CONNECTION_REFUSED,
'connectionreset': Cr.NS_ERROR_NET_RESET,
'internetdisconnected': Cr.NS_ERROR_OFFLINE,
'namenotresolved': Cr.NS_ERROR_UNKNOWN_HOST,
'timedout': Cr.NS_ERROR_NET_TIMEOUT,
'failed': Cr.NS_ERROR_FAILURE,
};
PageNetwork.Events = {
Request: Symbol('PageNetwork.Events.Request'),
Response: Symbol('PageNetwork.Events.Response'),
RequestFinished: Symbol('PageNetwork.Events.RequestFinished'),
RequestFailed: Symbol('PageNetwork.Events.RequestFailed'),
};
var EXPORTED_SYMBOLS = ['NetworkObserver', 'PageNetwork'];
this.NetworkObserver = NetworkObserver;
this.PageNetwork = PageNetwork;