diff options
Diffstat (limited to 'toolkit/modules/addons/WebRequest.jsm')
-rw-r--r-- | toolkit/modules/addons/WebRequest.jsm | 918 |
1 files changed, 918 insertions, 0 deletions
diff --git a/toolkit/modules/addons/WebRequest.jsm b/toolkit/modules/addons/WebRequest.jsm new file mode 100644 index 000000000..c720dae5d --- /dev/null +++ b/toolkit/modules/addons/WebRequest.jsm @@ -0,0 +1,918 @@ +/* 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 EXPORTED_SYMBOLS = ["WebRequest"]; + +/* exported WebRequest */ + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; +const Cr = Components.results; + +const {nsIHttpActivityObserver, nsISocketTransport} = Ci; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", + "resource://gre/modules/AppConstants.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils", + "resource://gre/modules/ExtensionUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon", + "resource://gre/modules/WebRequestCommon.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WebRequestUpload", + "resource://gre/modules/WebRequestUpload.jsm"); + +XPCOMUtils.defineLazyGetter(this, "ExtensionError", () => ExtensionUtils.ExtensionError); + +function attachToChannel(channel, key, data) { + if (channel instanceof Ci.nsIWritablePropertyBag2) { + let wrapper = {wrappedJSObject: data}; + channel.setPropertyAsInterface(key, wrapper); + } + return data; +} + +function extractFromChannel(channel, key) { + if (channel instanceof Ci.nsIPropertyBag2 && channel.hasKey(key)) { + let data = channel.get(key); + return data && data.wrappedJSObject; + } + return null; +} + +function getData(channel) { + const key = "mozilla.webRequest.data"; + return extractFromChannel(channel, key) || attachToChannel(channel, key, {}); +} + +var RequestId = { + count: 1, + create(channel = null) { + let id = (this.count++).toString(); + if (channel) { + getData(channel).requestId = id; + } + return id; + }, + + get(channel) { + return channel && getData(channel).requestId || this.create(channel); + }, +}; + +function runLater(job) { + Services.tm.currentThread.dispatch(job, Ci.nsIEventTarget.DISPATCH_NORMAL); +} + +function parseFilter(filter) { + if (!filter) { + filter = {}; + } + + // FIXME: Support windowId filtering. + return {urls: filter.urls || null, types: filter.types || null}; +} + +function parseExtra(extra, allowed = []) { + if (extra) { + for (let ex of extra) { + if (allowed.indexOf(ex) == -1) { + throw new ExtensionError(`Invalid option ${ex}`); + } + } + } + + let result = {}; + for (let al of allowed) { + if (extra && extra.indexOf(al) != -1) { + result[al] = true; + } + } + return result; +} + +function mergeStatus(data, channel, event) { + try { + data.statusCode = channel.responseStatus; + let statusText = channel.responseStatusText; + let maj = {}; + let min = {}; + channel.QueryInterface(Ci.nsIHttpChannelInternal).getResponseVersion(maj, min); + data.statusLine = `HTTP/${maj.value}.${min.value} ${data.statusCode} ${statusText}`; + } catch (e) { + // NS_ERROR_NOT_AVAILABLE might be thrown if it's an internal redirect, happening before + // any actual HTTP traffic. Otherwise, let's report. + if (event !== "onRedirect" || e.result !== Cr.NS_ERROR_NOT_AVAILABLE) { + Cu.reportError(`webRequest Error: ${e} trying to merge status in ${event}@${channel.name}`); + } + } +} + +function isThenable(value) { + return value && typeof value === "object" && typeof value.then === "function"; +} + +class HeaderChanger { + constructor(channel) { + this.channel = channel; + + this.originalHeaders = new Map(); + this.visitHeaders((name, value) => { + this.originalHeaders.set(name.toLowerCase(), value); + }); + } + + toArray() { + return Array.from(this.originalHeaders, + ([name, value]) => ({name, value})); + } + + validateHeaders(headers) { + // We should probably use schema validation for this. + + if (!Array.isArray(headers)) { + return false; + } + + return headers.every(header => { + if (typeof header !== "object" || header === null) { + return false; + } + + if (typeof header.name !== "string") { + return false; + } + + return (typeof header.value === "string" || + Array.isArray(header.binaryValue)); + }); + } + + applyChanges(headers) { + if (!this.validateHeaders(headers)) { + /* globals uneval */ + Cu.reportError(`Invalid header array: ${uneval(headers)}`); + return; + } + + let newHeaders = new Set(headers.map( + ({name}) => name.toLowerCase())); + + // Remove missing headers. + for (let name of this.originalHeaders.keys()) { + if (!newHeaders.has(name)) { + this.setHeader(name, ""); + } + } + + // Set new or changed headers. + for (let {name, value, binaryValue} of headers) { + if (binaryValue) { + value = String.fromCharCode(...binaryValue); + } + if (value !== this.originalHeaders.get(name.toLowerCase())) { + this.setHeader(name, value); + } + } + } +} + +class RequestHeaderChanger extends HeaderChanger { + setHeader(name, value) { + try { + this.channel.setRequestHeader(name, value, false); + } catch (e) { + Cu.reportError(new Error(`Error setting request header ${name}: ${e}`)); + } + } + + visitHeaders(visitor) { + if (this.channel instanceof Ci.nsIHttpChannel) { + this.channel.visitRequestHeaders(visitor); + } + } +} + +class ResponseHeaderChanger extends HeaderChanger { + setHeader(name, value) { + try { + if (name.toLowerCase() === "content-type" && value) { + // The Content-Type header value can't be modified, so we + // set the channel's content type directly, instead, and + // record that we made the change for the sake of + // subsequent observers. + this.channel.contentType = value; + + getData(this.channel).contentType = value; + } else { + this.channel.setResponseHeader(name, value, false); + } + } catch (e) { + Cu.reportError(new Error(`Error setting response header ${name}: ${e}`)); + } + } + + visitHeaders(visitor) { + if (this.channel instanceof Ci.nsIHttpChannel) { + try { + this.channel.visitResponseHeaders((name, value) => { + if (name.toLowerCase() === "content-type") { + value = getData(this.channel).contentType || value; + } + + visitor(name, value); + }); + } catch (e) { + // Throws if response headers aren't available yet. + } + } + } +} + +var HttpObserverManager; + +var ContentPolicyManager = { + policyData: new Map(), + policies: new Map(), + idMap: new Map(), + nextId: 0, + + init() { + Services.ppmm.initialProcessData.webRequestContentPolicies = this.policyData; + + Services.ppmm.addMessageListener("WebRequest:ShouldLoad", this); + Services.mm.addMessageListener("WebRequest:ShouldLoad", this); + }, + + receiveMessage(msg) { + let browser = msg.target instanceof Ci.nsIDOMXULElement ? msg.target : null; + + let requestId = RequestId.create(); + for (let id of msg.data.ids) { + let callback = this.policies.get(id); + if (!callback) { + // It's possible that this listener has been removed and the + // child hasn't learned yet. + continue; + } + let response = null; + let listenerKind = "onStop"; + let data = Object.assign({requestId, browser}, msg.data); + delete data.ids; + try { + response = callback(data); + if (response) { + if (response.cancel) { + listenerKind = "onError"; + data.error = "NS_ERROR_ABORT"; + return {cancel: true}; + } + // FIXME: Need to handle redirection here (for non-HTTP URIs only) + } + } catch (e) { + Cu.reportError(e); + } finally { + runLater(() => this.runChannelListener(listenerKind, data)); + } + } + + return {}; + }, + + runChannelListener(kind, data) { + let listeners = HttpObserverManager.listeners[kind]; + let uri = BrowserUtils.makeURI(data.url); + let policyType = data.type; + for (let [callback, opts] of listeners.entries()) { + if (!HttpObserverManager.shouldRunListener(policyType, uri, opts.filter)) { + continue; + } + callback(data); + } + }, + + addListener(callback, opts) { + // Clone opts, since we're going to modify them for IPC. + opts = Object.assign({}, opts); + let id = this.nextId++; + opts.id = id; + if (opts.filter.urls) { + opts.filter = Object.assign({}, opts.filter); + opts.filter.urls = opts.filter.urls.serialize(); + } + Services.ppmm.broadcastAsyncMessage("WebRequest:AddContentPolicy", opts); + + this.policyData.set(id, opts); + + this.policies.set(id, callback); + this.idMap.set(callback, id); + }, + + removeListener(callback) { + let id = this.idMap.get(callback); + Services.ppmm.broadcastAsyncMessage("WebRequest:RemoveContentPolicy", {id}); + + this.policyData.delete(id); + this.idMap.delete(callback); + this.policies.delete(id); + }, +}; +ContentPolicyManager.init(); + +function StartStopListener(manager, loadContext) { + this.manager = manager; + this.loadContext = loadContext; + this.orig = null; +} + +StartStopListener.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver, + Ci.nsIStreamListener]), + + onStartRequest: function(request, context) { + this.manager.onStartRequest(request, this.loadContext); + this.orig.onStartRequest(request, context); + }, + + onStopRequest(request, context, statusCode) { + try { + this.orig.onStopRequest(request, context, statusCode); + } catch (e) { + Cu.reportError(e); + } + this.manager.onStopRequest(request, this.loadContext); + }, + + onDataAvailable(...args) { + return this.orig.onDataAvailable(...args); + }, +}; + +var ChannelEventSink = { + _classDescription: "WebRequest channel event sink", + _classID: Components.ID("115062f8-92f1-11e5-8b7f-080027b0f7ec"), + _contractID: "@mozilla.org/webrequest/channel-event-sink;1", + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink, + Ci.nsIFactory]), + + init() { + Components.manager.QueryInterface(Ci.nsIComponentRegistrar) + .registerFactory(this._classID, this._classDescription, this._contractID, this); + }, + + register() { + let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager); + catMan.addCategoryEntry("net-channel-event-sinks", this._contractID, this._contractID, false, true); + }, + + unregister() { + let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager); + catMan.deleteCategoryEntry("net-channel-event-sinks", this._contractID, false); + }, + + // nsIChannelEventSink implementation + asyncOnChannelRedirect(oldChannel, newChannel, flags, redirectCallback) { + runLater(() => redirectCallback.onRedirectVerifyCallback(Cr.NS_OK)); + try { + HttpObserverManager.onChannelReplaced(oldChannel, newChannel); + } catch (e) { + // we don't wanna throw: it would abort the redirection + } + }, + + // nsIFactory implementation + createInstance(outer, iid) { + if (outer) { + throw Cr.NS_ERROR_NO_AGGREGATION; + } + return this.QueryInterface(iid); + }, +}; + +ChannelEventSink.init(); + +HttpObserverManager = { + modifyInitialized: false, + examineInitialized: false, + redirectInitialized: false, + activityInitialized: false, + needTracing: false, + + listeners: { + opening: new Map(), + modify: new Map(), + afterModify: new Map(), + headersReceived: new Map(), + onRedirect: new Map(), + onStart: new Map(), + onError: new Map(), + onStop: new Map(), + }, + + get activityDistributor() { + return Cc["@mozilla.org/network/http-activity-distributor;1"].getService(Ci.nsIHttpActivityDistributor); + }, + + addOrRemove() { + let needModify = this.listeners.opening.size || this.listeners.modify.size || this.listeners.afterModify.size; + if (needModify && !this.modifyInitialized) { + this.modifyInitialized = true; + Services.obs.addObserver(this, "http-on-modify-request", false); + } else if (!needModify && this.modifyInitialized) { + this.modifyInitialized = false; + Services.obs.removeObserver(this, "http-on-modify-request"); + } + this.needTracing = this.listeners.onStart.size || + this.listeners.onError.size || + this.listeners.onStop.size; + + let needExamine = this.needTracing || + this.listeners.headersReceived.size; + + if (needExamine && !this.examineInitialized) { + this.examineInitialized = true; + Services.obs.addObserver(this, "http-on-examine-response", false); + Services.obs.addObserver(this, "http-on-examine-cached-response", false); + Services.obs.addObserver(this, "http-on-examine-merged-response", false); + } else if (!needExamine && this.examineInitialized) { + this.examineInitialized = false; + Services.obs.removeObserver(this, "http-on-examine-response"); + Services.obs.removeObserver(this, "http-on-examine-cached-response"); + Services.obs.removeObserver(this, "http-on-examine-merged-response"); + } + + let needRedirect = this.listeners.onRedirect.size; + if (needRedirect && !this.redirectInitialized) { + this.redirectInitialized = true; + ChannelEventSink.register(); + } else if (!needRedirect && this.redirectInitialized) { + this.redirectInitialized = false; + ChannelEventSink.unregister(); + } + + let needActivity = this.listeners.onError.size; + if (needActivity && !this.activityInitialized) { + this.activityInitialized = true; + this.activityDistributor.addObserver(this); + } else if (!needActivity && this.activityInitialized) { + this.activityInitialized = false; + this.activityDistributor.removeObserver(this); + } + }, + + addListener(kind, callback, opts) { + this.listeners[kind].set(callback, opts); + this.addOrRemove(); + }, + + removeListener(kind, callback) { + this.listeners[kind].delete(callback); + this.addOrRemove(); + }, + + getLoadContext(channel) { + try { + return channel.QueryInterface(Ci.nsIChannel) + .notificationCallbacks + .getInterface(Components.interfaces.nsILoadContext); + } catch (e) { + try { + return channel.loadGroup + .notificationCallbacks + .getInterface(Components.interfaces.nsILoadContext); + } catch (e) { + return null; + } + } + }, + + observe(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + switch (topic) { + case "http-on-modify-request": + let loadContext = this.getLoadContext(channel); + + this.runChannelListener(channel, loadContext, "opening"); + break; + case "http-on-examine-cached-response": + case "http-on-examine-merged-response": + getData(channel).fromCache = true; + // falls through + case "http-on-examine-response": + this.examine(channel, topic, data); + break; + } + }, + + // We map activity values with tentative error names, e.g. "STATUS_RESOLVING" => "NS_ERROR_NET_ON_RESOLVING". + get activityErrorsMap() { + let prefix = /^(?:ACTIVITY_SUBTYPE_|STATUS_)/; + let map = new Map(); + for (let iface of [nsIHttpActivityObserver, nsISocketTransport]) { + for (let c of Object.keys(iface).filter(name => prefix.test(name))) { + map.set(iface[c], c.replace(prefix, "NS_ERROR_NET_ON_")); + } + } + delete this.activityErrorsMap; + this.activityErrorsMap = map; + return this.activityErrorsMap; + }, + GOOD_LAST_ACTIVITY: nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_HEADER, + observeActivity(channel, activityType, activitySubtype /* , aTimestamp, aExtraSizeData, aExtraStringData */) { + let channelData = getData(channel); + let lastActivity = channelData.lastActivity || 0; + if (activitySubtype === nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE && + lastActivity && lastActivity !== this.GOOD_LAST_ACTIVITY) { + let loadContext = this.getLoadContext(channel); + if (!this.errorCheck(channel, loadContext, channelData)) { + this.runChannelListener(channel, loadContext, "onError", + {error: this.activityErrorsMap.get(lastActivity) || + `NS_ERROR_NET_UNKNOWN_${lastActivity}`}); + } + } else if (lastActivity !== this.GOOD_LAST_ACTIVITY && + lastActivity !== nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE) { + channelData.lastActivity = activitySubtype; + } + }, + + shouldRunListener(policyType, uri, filter) { + return WebRequestCommon.typeMatches(policyType, filter.types) && + WebRequestCommon.urlMatches(uri, filter.urls); + }, + + get resultsMap() { + delete this.resultsMap; + this.resultsMap = new Map(Object.keys(Cr).map(name => [Cr[name], name])); + return this.resultsMap; + }, + maybeError(channel, extraData = null, channelData = null) { + if (!(extraData && extraData.error)) { + if (!Components.isSuccessCode(channel.status)) { + extraData = {error: this.resultsMap.get(channel.status)}; + } + } + return extraData; + }, + errorCheck(channel, loadContext, channelData = null) { + let errorData = this.maybeError(channel, null, channelData); + if (errorData) { + this.runChannelListener(channel, loadContext, "onError", errorData); + } + return errorData; + }, + + /** + * Resumes the channel if it is currently suspended due to this + * listener. + * + * @param {nsIChannel} channel + * The channel to possibly suspend. + */ + maybeResume(channel) { + let data = getData(channel); + if (data.suspended) { + channel.resume(); + data.suspended = false; + } + }, + + /** + * Suspends the channel if it is not currently suspended due to this + * listener. Returns true if the channel was suspended as a result of + * this call. + * + * @param {nsIChannel} channel + * The channel to possibly suspend. + * @returns {boolean} + * True if this call resulted in the channel being suspended. + */ + maybeSuspend(channel) { + let data = getData(channel); + if (!data.suspended) { + channel.suspend(); + data.suspended = true; + return true; + } + }, + + getRequestData(channel, loadContext, policyType, extraData) { + let {loadInfo} = channel; + + let data = { + requestId: RequestId.get(channel), + url: channel.URI.spec, + method: channel.requestMethod, + browser: loadContext && loadContext.topFrameElement, + type: WebRequestCommon.typeForPolicyType(policyType), + fromCache: getData(channel).fromCache, + windowId: 0, + parentWindowId: 0, + }; + + if (loadInfo) { + let originPrincipal = loadInfo.triggeringPrincipal; + if (originPrincipal.URI) { + data.originUrl = originPrincipal.URI.spec; + } + + // If there is no loadingPrincipal, check that the request is not going to + // inherit a system principal. triggeringPrincipal is the context that + // initiated the load, but is not necessarily the principal that the + // request results in, only rely on that if no other principal is available. + let {isSystemPrincipal} = Services.scriptSecurityManager; + let isTopLevel = !loadInfo.loadingPrincipal && !!data.browser; + data.isSystemPrincipal = !isTopLevel && + isSystemPrincipal(loadInfo.loadingPrincipal || + loadInfo.principalToInherit || + loadInfo.triggeringPrincipal); + + if (loadInfo.frameOuterWindowID) { + Object.assign(data, { + windowId: loadInfo.frameOuterWindowID, + parentWindowId: loadInfo.outerWindowID, + }); + } else { + Object.assign(data, { + windowId: loadInfo.outerWindowID, + parentWindowId: loadInfo.parentOuterWindowID, + }); + } + } + + if (channel instanceof Ci.nsIHttpChannelInternal) { + try { + data.ip = channel.remoteAddress; + } catch (e) { + // The remoteAddress getter throws if the address is unavailable, + // but ip is an optional property so just ignore the exception. + } + } + + return Object.assign(data, extraData); + }, + + runChannelListener(channel, loadContext = null, kind, extraData = null) { + let handlerResults = []; + let requestHeaders; + let responseHeaders; + + try { + if (this.activityInitialized) { + let channelData = getData(channel); + if (kind === "onError") { + if (channelData.errorNotified) { + return; + } + channelData.errorNotified = true; + } else if (this.errorCheck(channel, loadContext, channelData)) { + return; + } + } + + let {loadInfo} = channel; + let policyType = (loadInfo ? loadInfo.externalContentPolicyType + : Ci.nsIContentPolicy.TYPE_OTHER); + + let includeStatus = (["headersReceived", "onRedirect", "onStart", "onStop"].includes(kind) && + channel instanceof Ci.nsIHttpChannel); + + let commonData = null; + let uri = channel.URI; + let requestBody; + for (let [callback, opts] of this.listeners[kind].entries()) { + if (!this.shouldRunListener(policyType, uri, opts.filter)) { + continue; + } + + if (!commonData) { + commonData = this.getRequestData(channel, loadContext, policyType, extraData); + } + let data = Object.assign({}, commonData); + + if (opts.requestHeaders) { + requestHeaders = requestHeaders || new RequestHeaderChanger(channel); + data.requestHeaders = requestHeaders.toArray(); + } + + if (opts.responseHeaders) { + responseHeaders = responseHeaders || new ResponseHeaderChanger(channel); + data.responseHeaders = responseHeaders.toArray(); + } + + if (opts.requestBody) { + requestBody = requestBody || WebRequestUpload.createRequestBody(channel); + data.requestBody = requestBody; + } + + if (includeStatus) { + mergeStatus(data, channel, kind); + } + + try { + let result = callback(data); + + if (result && typeof result === "object" && opts.blocking + && !AddonManagerPermissions.isHostPermitted(uri.host) + && (!loadInfo || !loadInfo.loadingPrincipal + || !loadInfo.loadingPrincipal.URI + || !AddonManagerPermissions.isHostPermitted(loadInfo.loadingPrincipal.URI.host))) { + handlerResults.push({opts, result}); + } + } catch (e) { + Cu.reportError(e); + } + } + } catch (e) { + Cu.reportError(e); + } + + return this.applyChanges(kind, channel, loadContext, handlerResults, + requestHeaders, responseHeaders); + }, + + applyChanges: Task.async(function* (kind, channel, loadContext, handlerResults, requestHeaders, responseHeaders) { + let asyncHandlers = handlerResults.filter(({result}) => isThenable(result)); + let isAsync = asyncHandlers.length > 0; + let shouldResume = false; + + try { + if (isAsync) { + shouldResume = this.maybeSuspend(channel); + + for (let value of asyncHandlers) { + try { + value.result = yield value.result; + } catch (e) { + Cu.reportError(e); + value.result = {}; + } + } + } + + for (let {opts, result} of handlerResults) { + if (!result || typeof result !== "object") { + continue; + } + + if (result.cancel) { + this.maybeResume(channel); + channel.cancel(Cr.NS_ERROR_ABORT); + + this.errorCheck(channel, loadContext); + return; + } + + if (result.redirectUrl) { + try { + this.maybeResume(channel); + + channel.redirectTo(BrowserUtils.makeURI(result.redirectUrl)); + return; + } catch (e) { + Cu.reportError(e); + } + } + + if (opts.requestHeaders && result.requestHeaders && requestHeaders) { + requestHeaders.applyChanges(result.requestHeaders); + } + + if (opts.responseHeaders && result.responseHeaders && responseHeaders) { + responseHeaders.applyChanges(result.responseHeaders); + } + } + + if (kind === "opening") { + yield this.runChannelListener(channel, loadContext, "modify"); + } else if (kind === "modify") { + yield this.runChannelListener(channel, loadContext, "afterModify"); + } + } catch (e) { + Cu.reportError(e); + } + + // Only resume the channel if it was suspended by this call. + if (shouldResume) { + this.maybeResume(channel); + } + }), + + examine(channel, topic, data) { + let loadContext = this.getLoadContext(channel); + + if (this.needTracing) { + // Check whether we've already added a listener to this channel, + // so we don't wind up chaining multiple listeners. + let channelData = getData(channel); + if (!channelData.hasListener && channel instanceof Ci.nsITraceableChannel) { + let responseStatus = channel.responseStatus; + // skip redirections, https://bugzilla.mozilla.org/show_bug.cgi?id=728901#c8 + if (responseStatus < 300 || responseStatus >= 400) { + let listener = new StartStopListener(this, loadContext); + let orig = channel.setNewListener(listener); + listener.orig = orig; + channelData.hasListener = true; + } + } + } + + this.runChannelListener(channel, loadContext, "headersReceived"); + }, + + onChannelReplaced(oldChannel, newChannel) { + this.runChannelListener(oldChannel, this.getLoadContext(oldChannel), + "onRedirect", {redirectUrl: newChannel.URI.spec}); + }, + + onStartRequest(channel, loadContext) { + this.runChannelListener(channel, loadContext, "onStart"); + }, + + onStopRequest(channel, loadContext) { + this.runChannelListener(channel, loadContext, "onStop"); + }, +}; + +var onBeforeRequest = { + get allowedOptions() { + delete this.allowedOptions; + this.allowedOptions = ["blocking"]; + if (!AppConstants.RELEASE_OR_BETA) { + this.allowedOptions.push("requestBody"); + } + return this.allowedOptions; + }, + addListener(callback, filter = null, opt_extraInfoSpec = null) { + let opts = parseExtra(opt_extraInfoSpec, this.allowedOptions); + opts.filter = parseFilter(filter); + ContentPolicyManager.addListener(callback, opts); + HttpObserverManager.addListener("opening", callback, opts); + }, + + removeListener(callback) { + HttpObserverManager.removeListener("opening", callback); + ContentPolicyManager.removeListener(callback); + }, +}; + +function HttpEvent(internalEvent, options) { + this.internalEvent = internalEvent; + this.options = options; +} + +HttpEvent.prototype = { + addListener(callback, filter = null, opt_extraInfoSpec = null) { + let opts = parseExtra(opt_extraInfoSpec, this.options); + opts.filter = parseFilter(filter); + HttpObserverManager.addListener(this.internalEvent, callback, opts); + }, + + removeListener(callback) { + HttpObserverManager.removeListener(this.internalEvent, callback); + }, +}; + +var onBeforeSendHeaders = new HttpEvent("modify", ["requestHeaders", "blocking"]); +var onSendHeaders = new HttpEvent("afterModify", ["requestHeaders"]); +var onHeadersReceived = new HttpEvent("headersReceived", ["blocking", "responseHeaders"]); +var onBeforeRedirect = new HttpEvent("onRedirect", ["responseHeaders"]); +var onResponseStarted = new HttpEvent("onStart", ["responseHeaders"]); +var onCompleted = new HttpEvent("onStop", ["responseHeaders"]); +var onErrorOccurred = new HttpEvent("onError"); + +var WebRequest = { + // http-on-modify observer for HTTP(S), content policy for the other protocols (notably, data:) + onBeforeRequest: onBeforeRequest, + + // http-on-modify observer. + onBeforeSendHeaders: onBeforeSendHeaders, + + // http-on-modify observer. + onSendHeaders: onSendHeaders, + + // http-on-examine-*observer. + onHeadersReceived: onHeadersReceived, + + // nsIChannelEventSink. + onBeforeRedirect: onBeforeRedirect, + + // OnStartRequest channel listener. + onResponseStarted: onResponseStarted, + + // OnStopRequest channel listener. + onCompleted: onCompleted, + + // nsIHttpActivityObserver. + onErrorOccurred: onErrorOccurred, +}; + +Services.ppmm.loadProcessScript("resource://gre/modules/WebRequestContent.js", true); |