801 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
		
		
			
		
	
	
			801 lines
		
	
	
		
			29 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 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._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); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   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); | ||
|  |   } | ||
|  | 
 | ||
|  |   _onResponse(fromCache, httpChannel, topic) { | ||
|  |     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); | ||
|  |   } | ||
|  | 
 | ||
|  |   onDataAvailable(aRequest, aInputStream, aOffset, aCount) { | ||
|  |     if (this._disposed) { | ||
|  |       this.originalListener.onDataAvailable(aRequest, aInputStream, aOffset, aCount); | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     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); | ||
|  |     this.originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount); | ||
|  |   } | ||
|  | 
 | ||
|  |   onStartRequest(aRequest) { | ||
|  |     this.originalListener.onStartRequest(aRequest); | ||
|  |   } | ||
|  | 
 | ||
|  |   onStopRequest(aRequest, aStatusCode) { | ||
|  |     this.originalListener.onStopRequest(aRequest, aStatusCode); | ||
|  |     if (this._disposed) | ||
|  |       return; | ||
|  | 
 | ||
|  |     if (aStatusCode === 0) { | ||
|  |       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; |