summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/addons/WebRequest.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/addons/WebRequest.jsm')
-rw-r--r--toolkit/modules/addons/WebRequest.jsm918
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);