forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1192 lines
		
	
	
	
		
			35 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1192 lines
		
	
	
	
		
			35 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 EXPORTED_SYMBOLS = ["WebRequest"];
 | 
						|
 | 
						|
/* exported WebRequest */
 | 
						|
 | 
						|
/* globals ChannelWrapper */
 | 
						|
 | 
						|
const { nsIHttpActivityObserver, nsISocketTransport } = Ci;
 | 
						|
 | 
						|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | 
						|
const { XPCOMUtils } = ChromeUtils.import(
 | 
						|
  "resource://gre/modules/XPCOMUtils.jsm"
 | 
						|
);
 | 
						|
 | 
						|
XPCOMUtils.defineLazyModuleGetters(this, {
 | 
						|
  ExtensionParent: "resource://gre/modules/ExtensionParent.jsm",
 | 
						|
  ExtensionUtils: "resource://gre/modules/ExtensionUtils.jsm",
 | 
						|
  WebRequestUpload: "resource://gre/modules/WebRequestUpload.jsm",
 | 
						|
  SecurityInfo: "resource://gre/modules/SecurityInfo.jsm",
 | 
						|
});
 | 
						|
 | 
						|
// WebRequest.jsm's only consumer is ext-webRequest.js, so we can depend on
 | 
						|
// the apiManager.global being initialized.
 | 
						|
XPCOMUtils.defineLazyGetter(this, "tabTracker", () => {
 | 
						|
  return ExtensionParent.apiManager.global.tabTracker;
 | 
						|
});
 | 
						|
XPCOMUtils.defineLazyGetter(this, "getCookieStoreIdForOriginAttributes", () => {
 | 
						|
  return ExtensionParent.apiManager.global.getCookieStoreIdForOriginAttributes;
 | 
						|
});
 | 
						|
 | 
						|
function runLater(job) {
 | 
						|
  Services.tm.dispatchToMainThread(job);
 | 
						|
}
 | 
						|
 | 
						|
function parseFilter(filter) {
 | 
						|
  if (!filter) {
 | 
						|
    filter = {};
 | 
						|
  }
 | 
						|
 | 
						|
  return {
 | 
						|
    urls: filter.urls || null,
 | 
						|
    types: filter.types || null,
 | 
						|
    tabId: filter.tabId ?? null,
 | 
						|
    windowId: filter.windowId ?? null,
 | 
						|
    incognito: filter.incognito ?? null,
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
function parseExtra(extra, allowed = [], optionsObj = {}) {
 | 
						|
  if (extra) {
 | 
						|
    for (let ex of extra) {
 | 
						|
      if (!allowed.includes(ex)) {
 | 
						|
        throw new ExtensionUtils.ExtensionError(`Invalid option ${ex}`);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  let result = Object.assign({}, optionsObj);
 | 
						|
  for (let al of allowed) {
 | 
						|
    if (extra && extra.includes(al)) {
 | 
						|
      result[al] = true;
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return result;
 | 
						|
}
 | 
						|
 | 
						|
function isThenable(value) {
 | 
						|
  return value && typeof value === "object" && typeof value.then === "function";
 | 
						|
}
 | 
						|
 | 
						|
class HeaderChanger {
 | 
						|
  constructor(channel) {
 | 
						|
    this.channel = channel;
 | 
						|
 | 
						|
    this.array = this.readHeaders();
 | 
						|
  }
 | 
						|
 | 
						|
  getMap() {
 | 
						|
    if (!this.map) {
 | 
						|
      this.map = new Map();
 | 
						|
      for (let header of this.array) {
 | 
						|
        this.map.set(header.name.toLowerCase(), header);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return this.map;
 | 
						|
  }
 | 
						|
 | 
						|
  toArray() {
 | 
						|
    return this.array;
 | 
						|
  }
 | 
						|
 | 
						|
  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, opts = {}) {
 | 
						|
    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.
 | 
						|
    let origHeaders = this.getMap();
 | 
						|
    for (let name of origHeaders.keys()) {
 | 
						|
      if (!newHeaders.has(name)) {
 | 
						|
        this.setHeader(name, "", false, opts, name);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Set new or changed headers.  If there are multiple headers with the same
 | 
						|
    // name (e.g. Set-Cookie), merge them, instead of having new values
 | 
						|
    // overwrite previous ones.
 | 
						|
    //
 | 
						|
    // When the new value of a header is equal the existing value of the header
 | 
						|
    // (e.g. the initial response set "Set-Cookie: examplename=examplevalue",
 | 
						|
    // and an extension also added the header
 | 
						|
    // "Set-Cookie: examplename=examplevalue") then the header value is not
 | 
						|
    // re-set, but subsequent headers of the same type will be merged in.
 | 
						|
    //
 | 
						|
    // Multiple addons will be able to provide modifications to any headers
 | 
						|
    // listed in the default set.
 | 
						|
    let headersAlreadySet = new Set();
 | 
						|
    for (let { name, value, binaryValue } of headers) {
 | 
						|
      if (binaryValue) {
 | 
						|
        value = String.fromCharCode(...binaryValue);
 | 
						|
      }
 | 
						|
 | 
						|
      let lowerCaseName = name.toLowerCase();
 | 
						|
      let original = origHeaders.get(lowerCaseName);
 | 
						|
 | 
						|
      if (!original || value !== original.value) {
 | 
						|
        let shouldMerge = headersAlreadySet.has(lowerCaseName);
 | 
						|
        this.setHeader(name, value, shouldMerge, opts, lowerCaseName);
 | 
						|
      }
 | 
						|
 | 
						|
      headersAlreadySet.add(lowerCaseName);
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
const checkRestrictedHeaderValue = (value, opts = {}) => {
 | 
						|
  let uri = Services.io.newURI(`https://${value}/`);
 | 
						|
  let { policy } = opts;
 | 
						|
 | 
						|
  if (policy && !policy.allowedOrigins.matches(uri)) {
 | 
						|
    throw new Error(`Unable to set host header, url missing from permissions.`);
 | 
						|
  }
 | 
						|
 | 
						|
  if (WebExtensionPolicy.isRestrictedURI(uri)) {
 | 
						|
    throw new Error(`Unable to set host header to restricted url.`);
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
class RequestHeaderChanger extends HeaderChanger {
 | 
						|
  setHeader(name, value, merge, opts, lowerCaseName) {
 | 
						|
    try {
 | 
						|
      if (value && lowerCaseName === "host") {
 | 
						|
        checkRestrictedHeaderValue(value, opts);
 | 
						|
      }
 | 
						|
      this.channel.setRequestHeader(name, value, merge);
 | 
						|
    } catch (e) {
 | 
						|
      Cu.reportError(new Error(`Error setting request header ${name}: ${e}`));
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  readHeaders() {
 | 
						|
    return this.channel.getRequestHeaders();
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class ResponseHeaderChanger extends HeaderChanger {
 | 
						|
  didModifyCSP = false;
 | 
						|
 | 
						|
  setHeader(name, value, merge, opts, lowerCaseName) {
 | 
						|
    if (lowerCaseName === "content-security-policy") {
 | 
						|
      // When multiple add-ons change the CSP, enforce the combined (strictest)
 | 
						|
      // policy - see bug 1462989 for motivation.
 | 
						|
      // When value is unset, don't force the header to be merged, to allow
 | 
						|
      // add-ons to clear the header if wanted.
 | 
						|
      if (value) {
 | 
						|
        merge = merge || this.didModifyCSP;
 | 
						|
      }
 | 
						|
      this.didModifyCSP = true;
 | 
						|
    }
 | 
						|
    try {
 | 
						|
      this.channel.setResponseHeader(name, value, merge);
 | 
						|
    } catch (e) {
 | 
						|
      Cu.reportError(new Error(`Error setting response header ${name}: ${e}`));
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  readHeaders() {
 | 
						|
    return this.channel.getResponseHeaders();
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
const MAYBE_CACHED_EVENTS = new Set([
 | 
						|
  "onResponseStarted",
 | 
						|
  "onHeadersReceived",
 | 
						|
  "onBeforeRedirect",
 | 
						|
  "onCompleted",
 | 
						|
  "onErrorOccurred",
 | 
						|
]);
 | 
						|
 | 
						|
const OPTIONAL_PROPERTIES = [
 | 
						|
  "requestHeaders",
 | 
						|
  "responseHeaders",
 | 
						|
  "statusCode",
 | 
						|
  "statusLine",
 | 
						|
  "error",
 | 
						|
  "redirectUrl",
 | 
						|
  "requestBody",
 | 
						|
  "scheme",
 | 
						|
  "realm",
 | 
						|
  "isProxy",
 | 
						|
  "challenger",
 | 
						|
  "proxyInfo",
 | 
						|
  "ip",
 | 
						|
  "frameAncestors",
 | 
						|
  "urlClassification",
 | 
						|
  "requestSize",
 | 
						|
  "responseSize",
 | 
						|
];
 | 
						|
 | 
						|
function serializeRequestData(eventName, extension) {
 | 
						|
  let data = {
 | 
						|
    requestId: this.requestId,
 | 
						|
    url: this.url,
 | 
						|
    originUrl: this.originUrl,
 | 
						|
    documentUrl: this.documentUrl,
 | 
						|
    method: this.method,
 | 
						|
    type: this.type,
 | 
						|
    timeStamp: Date.now(),
 | 
						|
    tabId: this.tabId,
 | 
						|
    frameId: this.frameId,
 | 
						|
    parentFrameId: this.parentFrameId,
 | 
						|
    incognito: this.incognito,
 | 
						|
    thirdParty: this.thirdParty,
 | 
						|
  };
 | 
						|
 | 
						|
  if (extension) {
 | 
						|
    if (extension.hasPermission("cookies")) {
 | 
						|
      data.cookieStoreId = this.cookieStoreId;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  if (MAYBE_CACHED_EVENTS.has(eventName)) {
 | 
						|
    data.fromCache = !!this.fromCache;
 | 
						|
  }
 | 
						|
 | 
						|
  for (let opt of OPTIONAL_PROPERTIES) {
 | 
						|
    if (typeof this[opt] !== "undefined") {
 | 
						|
      data[opt] = this[opt];
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  if (this.urlClassification) {
 | 
						|
    data.urlClassification = {
 | 
						|
      firstParty: this.urlClassification.firstParty.filter(
 | 
						|
        c => !c.startsWith("socialtracking_")
 | 
						|
      ),
 | 
						|
      thirdParty: this.urlClassification.thirdParty.filter(
 | 
						|
        c => !c.startsWith("socialtracking_")
 | 
						|
      ),
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  return data;
 | 
						|
}
 | 
						|
 | 
						|
var HttpObserverManager;
 | 
						|
 | 
						|
var ChannelEventSink = {
 | 
						|
  _classDescription: "WebRequest channel event sink",
 | 
						|
  _classID: Components.ID("115062f8-92f1-11e5-8b7f-080027b0f7ec"),
 | 
						|
  _contractID: "@mozilla.org/webrequest/channel-event-sink;1",
 | 
						|
 | 
						|
  QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink", "nsIFactory"]),
 | 
						|
 | 
						|
  init() {
 | 
						|
    Components.manager
 | 
						|
      .QueryInterface(Ci.nsIComponentRegistrar)
 | 
						|
      .registerFactory(
 | 
						|
        this._classID,
 | 
						|
        this._classDescription,
 | 
						|
        this._contractID,
 | 
						|
        this
 | 
						|
      );
 | 
						|
  },
 | 
						|
 | 
						|
  register() {
 | 
						|
    Services.catMan.addCategoryEntry(
 | 
						|
      "net-channel-event-sinks",
 | 
						|
      this._contractID,
 | 
						|
      this._contractID,
 | 
						|
      false,
 | 
						|
      true
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  unregister() {
 | 
						|
    Services.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 Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
 | 
						|
    }
 | 
						|
    return this.QueryInterface(iid);
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
ChannelEventSink.init();
 | 
						|
 | 
						|
// nsIAuthPrompt2 implementation for onAuthRequired
 | 
						|
class AuthRequestor {
 | 
						|
  constructor(channel, httpObserver) {
 | 
						|
    this.notificationCallbacks = channel.notificationCallbacks;
 | 
						|
    this.loadGroupCallbacks =
 | 
						|
      channel.loadGroup && channel.loadGroup.notificationCallbacks;
 | 
						|
    this.httpObserver = httpObserver;
 | 
						|
  }
 | 
						|
 | 
						|
  getInterface(iid) {
 | 
						|
    if (iid.equals(Ci.nsIAuthPromptProvider) || iid.equals(Ci.nsIAuthPrompt2)) {
 | 
						|
      return this;
 | 
						|
    }
 | 
						|
    try {
 | 
						|
      return this.notificationCallbacks.getInterface(iid);
 | 
						|
    } catch (e) {}
 | 
						|
    throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
 | 
						|
  }
 | 
						|
 | 
						|
  _getForwardedInterface(iid) {
 | 
						|
    try {
 | 
						|
      return this.notificationCallbacks.getInterface(iid);
 | 
						|
    } catch (e) {
 | 
						|
      return this.loadGroupCallbacks.getInterface(iid);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // nsIAuthPromptProvider getAuthPrompt
 | 
						|
  getAuthPrompt(reason, iid) {
 | 
						|
    // This should never get called without getInterface having been called first.
 | 
						|
    if (iid.equals(Ci.nsIAuthPrompt2)) {
 | 
						|
      return this;
 | 
						|
    }
 | 
						|
    return this._getForwardedInterface(Ci.nsIAuthPromptProvider).getAuthPrompt(
 | 
						|
      reason,
 | 
						|
      iid
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  // nsIAuthPrompt2 promptAuth
 | 
						|
  promptAuth(channel, level, authInfo) {
 | 
						|
    this._getForwardedInterface(Ci.nsIAuthPrompt2).promptAuth(
 | 
						|
      channel,
 | 
						|
      level,
 | 
						|
      authInfo
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  _getForwardPrompt(data) {
 | 
						|
    let reason = data.isProxy
 | 
						|
      ? Ci.nsIAuthPromptProvider.PROMPT_PROXY
 | 
						|
      : Ci.nsIAuthPromptProvider.PROMPT_NORMAL;
 | 
						|
    for (let callbacks of [
 | 
						|
      this.notificationCallbacks,
 | 
						|
      this.loadGroupCallbacks,
 | 
						|
    ]) {
 | 
						|
      try {
 | 
						|
        return callbacks
 | 
						|
          .getInterface(Ci.nsIAuthPromptProvider)
 | 
						|
          .getAuthPrompt(reason, Ci.nsIAuthPrompt2);
 | 
						|
      } catch (e) {}
 | 
						|
      try {
 | 
						|
        return callbacks.getInterface(Ci.nsIAuthPrompt2);
 | 
						|
      } catch (e) {}
 | 
						|
    }
 | 
						|
    throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
 | 
						|
  }
 | 
						|
 | 
						|
  // nsIAuthPrompt2 asyncPromptAuth
 | 
						|
  asyncPromptAuth(channel, callback, context, level, authInfo) {
 | 
						|
    let wrapper = ChannelWrapper.get(channel);
 | 
						|
 | 
						|
    let uri = channel.URI;
 | 
						|
    let proxyInfo;
 | 
						|
    let isProxy = !!(authInfo.flags & authInfo.AUTH_PROXY);
 | 
						|
    if (isProxy && channel instanceof Ci.nsIProxiedChannel) {
 | 
						|
      proxyInfo = channel.proxyInfo;
 | 
						|
    }
 | 
						|
    let data = {
 | 
						|
      scheme: authInfo.authenticationScheme,
 | 
						|
      realm: authInfo.realm,
 | 
						|
      isProxy,
 | 
						|
      challenger: {
 | 
						|
        host: proxyInfo ? proxyInfo.host : uri.host,
 | 
						|
        port: proxyInfo ? proxyInfo.port : uri.port,
 | 
						|
      },
 | 
						|
    };
 | 
						|
 | 
						|
    // In the case that no listener provides credentials, we fallback to the
 | 
						|
    // previously set callback class for authentication.
 | 
						|
    wrapper.authPromptForward = () => {
 | 
						|
      try {
 | 
						|
        let prompt = this._getForwardPrompt(data);
 | 
						|
        prompt.asyncPromptAuth(channel, callback, context, level, authInfo);
 | 
						|
      } catch (e) {
 | 
						|
        Cu.reportError(`webRequest asyncPromptAuth failure ${e}`);
 | 
						|
        callback.onAuthCancelled(context, false);
 | 
						|
      }
 | 
						|
      wrapper.authPromptForward = null;
 | 
						|
      wrapper.authPromptCallback = null;
 | 
						|
    };
 | 
						|
    wrapper.authPromptCallback = authCredentials => {
 | 
						|
      // The API allows for canceling the request, providing credentials or
 | 
						|
      // doing nothing, so we do not provide a way to call onAuthCanceled.
 | 
						|
      // Canceling the request will result in canceling the authentication.
 | 
						|
      if (
 | 
						|
        authCredentials &&
 | 
						|
        typeof authCredentials.username === "string" &&
 | 
						|
        typeof authCredentials.password === "string"
 | 
						|
      ) {
 | 
						|
        authInfo.username = authCredentials.username;
 | 
						|
        authInfo.password = authCredentials.password;
 | 
						|
        try {
 | 
						|
          callback.onAuthAvailable(context, authInfo);
 | 
						|
        } catch (e) {
 | 
						|
          Cu.reportError(`webRequest onAuthAvailable failure ${e}`);
 | 
						|
        }
 | 
						|
        // At least one addon has responded, so we won't forward to the regular
 | 
						|
        // prompt handlers.
 | 
						|
        wrapper.authPromptForward = null;
 | 
						|
        wrapper.authPromptCallback = null;
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    this.httpObserver.runChannelListener(wrapper, "onAuthRequired", data);
 | 
						|
 | 
						|
    return {
 | 
						|
      QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
 | 
						|
      cancel() {
 | 
						|
        try {
 | 
						|
          callback.onAuthCancelled(context, false);
 | 
						|
        } catch (e) {
 | 
						|
          Cu.reportError(`webRequest onAuthCancelled failure ${e}`);
 | 
						|
        }
 | 
						|
        wrapper.authPromptForward = null;
 | 
						|
        wrapper.authPromptCallback = null;
 | 
						|
      },
 | 
						|
    };
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
AuthRequestor.prototype.QueryInterface = ChromeUtils.generateQI([
 | 
						|
  "nsIInterfaceRequestor",
 | 
						|
  "nsIAuthPromptProvider",
 | 
						|
  "nsIAuthPrompt2",
 | 
						|
]);
 | 
						|
 | 
						|
// Most WebRequest events are implemented via the observer services, but
 | 
						|
// a few use custom xpcom interfaces.  This class (HttpObserverManager)
 | 
						|
// serves two main purposes:
 | 
						|
//  1. It abstracts away the names and details of the underlying
 | 
						|
//     implementation (e.g., onBeforeBeforeRequest is dispatched from
 | 
						|
//     the http-on-modify-request observable).
 | 
						|
//  2. It aggregates multiple listeners so that a single observer or
 | 
						|
//     handler can serve multiple webRequest listeners.
 | 
						|
HttpObserverManager = {
 | 
						|
  listeners: {
 | 
						|
    // onBeforeRequest uses http-on-modify observer for HTTP(S).
 | 
						|
    onBeforeRequest: new Map(),
 | 
						|
 | 
						|
    // onBeforeSendHeaders and onSendHeaders correspond to the
 | 
						|
    // http-on-before-connect observer.
 | 
						|
    onBeforeSendHeaders: new Map(),
 | 
						|
    onSendHeaders: new Map(),
 | 
						|
 | 
						|
    // onHeadersReceived corresponds to the http-on-examine-* obserservers.
 | 
						|
    onHeadersReceived: new Map(),
 | 
						|
 | 
						|
    // onAuthRequired is handled via the nsIAuthPrompt2 xpcom interface
 | 
						|
    // which is managed here by AuthRequestor.
 | 
						|
    onAuthRequired: new Map(),
 | 
						|
 | 
						|
    // onBeforeRedirect is handled by the nsIChannelEVentSink xpcom interface
 | 
						|
    // which is managed here by ChannelEventSink.
 | 
						|
    onBeforeRedirect: new Map(),
 | 
						|
 | 
						|
    // onResponseStarted, onErrorOccurred, and OnCompleted correspond
 | 
						|
    // to events dispatched by the ChannelWrapper EventTarget.
 | 
						|
    onResponseStarted: new Map(),
 | 
						|
    onErrorOccurred: new Map(),
 | 
						|
    onCompleted: new Map(),
 | 
						|
  },
 | 
						|
 | 
						|
  openingInitialized: false,
 | 
						|
  beforeConnectInitialized: false,
 | 
						|
  examineInitialized: false,
 | 
						|
  redirectInitialized: false,
 | 
						|
  activityInitialized: false,
 | 
						|
  needTracing: false,
 | 
						|
  hasRedirects: false,
 | 
						|
 | 
						|
  getWrapper(nativeChannel) {
 | 
						|
    let wrapper = ChannelWrapper.get(nativeChannel);
 | 
						|
    if (!wrapper._addedListeners) {
 | 
						|
      /* eslint-disable mozilla/balanced-listeners */
 | 
						|
      if (this.listeners.onErrorOccurred.size) {
 | 
						|
        wrapper.addEventListener("error", this);
 | 
						|
      }
 | 
						|
      if (this.listeners.onResponseStarted.size) {
 | 
						|
        wrapper.addEventListener("start", this);
 | 
						|
      }
 | 
						|
      if (this.listeners.onCompleted.size) {
 | 
						|
        wrapper.addEventListener("stop", this);
 | 
						|
      }
 | 
						|
      /* eslint-enable mozilla/balanced-listeners */
 | 
						|
 | 
						|
      wrapper._addedListeners = true;
 | 
						|
    }
 | 
						|
    return wrapper;
 | 
						|
  },
 | 
						|
 | 
						|
  get activityDistributor() {
 | 
						|
    return Cc["@mozilla.org/network/http-activity-distributor;1"].getService(
 | 
						|
      Ci.nsIHttpActivityDistributor
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  // This method is called whenever webRequest listeners are added or removed.
 | 
						|
  // It reconciles the set of listeners with underlying observers, event
 | 
						|
  // handlers, etc. by adding new low-level handlers for any newly added
 | 
						|
  // webRequest listeners and removing those that are no longer needed if
 | 
						|
  // there are no more listeners for corresponding webRequest events.
 | 
						|
  addOrRemove() {
 | 
						|
    let needOpening = this.listeners.onBeforeRequest.size;
 | 
						|
    let needBeforeConnect =
 | 
						|
      this.listeners.onBeforeSendHeaders.size ||
 | 
						|
      this.listeners.onSendHeaders.size;
 | 
						|
    if (needOpening && !this.openingInitialized) {
 | 
						|
      this.openingInitialized = true;
 | 
						|
      Services.obs.addObserver(this, "http-on-modify-request");
 | 
						|
    } else if (!needOpening && this.openingInitialized) {
 | 
						|
      this.openingInitialized = false;
 | 
						|
      Services.obs.removeObserver(this, "http-on-modify-request");
 | 
						|
    }
 | 
						|
    if (needBeforeConnect && !this.beforeConnectInitialized) {
 | 
						|
      this.beforeConnectInitialized = true;
 | 
						|
      Services.obs.addObserver(this, "http-on-before-connect");
 | 
						|
    } else if (!needBeforeConnect && this.beforeConnectInitialized) {
 | 
						|
      this.beforeConnectInitialized = false;
 | 
						|
      Services.obs.removeObserver(this, "http-on-before-connect");
 | 
						|
    }
 | 
						|
 | 
						|
    let haveBlocking = Object.values(this.listeners).some(listeners =>
 | 
						|
      Array.from(listeners.values()).some(listener => listener.blockingAllowed)
 | 
						|
    );
 | 
						|
 | 
						|
    this.needTracing =
 | 
						|
      this.listeners.onResponseStarted.size ||
 | 
						|
      this.listeners.onErrorOccurred.size ||
 | 
						|
      this.listeners.onCompleted.size ||
 | 
						|
      haveBlocking;
 | 
						|
 | 
						|
    let needExamine =
 | 
						|
      this.needTracing ||
 | 
						|
      this.listeners.onHeadersReceived.size ||
 | 
						|
      this.listeners.onAuthRequired.size;
 | 
						|
 | 
						|
    if (needExamine && !this.examineInitialized) {
 | 
						|
      this.examineInitialized = true;
 | 
						|
      Services.obs.addObserver(this, "http-on-examine-response");
 | 
						|
      Services.obs.addObserver(this, "http-on-examine-cached-response");
 | 
						|
      Services.obs.addObserver(this, "http-on-examine-merged-response");
 | 
						|
    } 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");
 | 
						|
    }
 | 
						|
 | 
						|
    // If we have any listeners, we need the channelsink so the channelwrapper is
 | 
						|
    // updated properly. Otherwise events for channels that are redirected will not
 | 
						|
    // happen correctly.  If we have no listeners, shut it down.
 | 
						|
    this.hasRedirects = this.listeners.onBeforeRedirect.size > 0;
 | 
						|
    let needRedirect =
 | 
						|
      this.hasRedirects || needExamine || needOpening || needBeforeConnect;
 | 
						|
    if (needRedirect && !this.redirectInitialized) {
 | 
						|
      this.redirectInitialized = true;
 | 
						|
      ChannelEventSink.register();
 | 
						|
    } else if (!needRedirect && this.redirectInitialized) {
 | 
						|
      this.redirectInitialized = false;
 | 
						|
      ChannelEventSink.unregister();
 | 
						|
    }
 | 
						|
 | 
						|
    let needActivity = this.listeners.onErrorOccurred.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();
 | 
						|
  },
 | 
						|
 | 
						|
  observe(subject, topic, data) {
 | 
						|
    let channel = this.getWrapper(subject);
 | 
						|
    switch (topic) {
 | 
						|
      case "http-on-modify-request":
 | 
						|
        this.runChannelListener(channel, "onBeforeRequest");
 | 
						|
        break;
 | 
						|
      case "http-on-before-connect":
 | 
						|
        this.runChannelListener(channel, "onBeforeSendHeaders");
 | 
						|
        break;
 | 
						|
      case "http-on-examine-cached-response":
 | 
						|
      case "http-on-examine-merged-response":
 | 
						|
        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(
 | 
						|
    nativeChannel,
 | 
						|
    activityType,
 | 
						|
    activitySubtype /* , aTimestamp, aExtraSizeData, aExtraStringData */
 | 
						|
  ) {
 | 
						|
    // Sometimes we get a NullHttpChannel, which implements
 | 
						|
    // nsIHttpChannel but not nsIChannel.
 | 
						|
    if (!(nativeChannel instanceof Ci.nsIChannel)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    let channel = this.getWrapper(nativeChannel);
 | 
						|
 | 
						|
    let lastActivity = channel.lastActivity || 0;
 | 
						|
    if (
 | 
						|
      activitySubtype ===
 | 
						|
        nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE &&
 | 
						|
      lastActivity &&
 | 
						|
      lastActivity !== this.GOOD_LAST_ACTIVITY
 | 
						|
    ) {
 | 
						|
      // Make a trip through the event loop to make sure errors have a
 | 
						|
      // chance to be processed before we fall back to a generic error
 | 
						|
      // string.
 | 
						|
      Services.tm.dispatchToMainThread(() => {
 | 
						|
        channel.errorCheck();
 | 
						|
        if (!channel.errorString) {
 | 
						|
          this.runChannelListener(channel, "onErrorOccurred", {
 | 
						|
            error:
 | 
						|
              this.activityErrorsMap.get(lastActivity) ||
 | 
						|
              `NS_ERROR_NET_UNKNOWN_${lastActivity}`,
 | 
						|
          });
 | 
						|
        }
 | 
						|
      });
 | 
						|
    } else if (
 | 
						|
      lastActivity !== this.GOOD_LAST_ACTIVITY &&
 | 
						|
      lastActivity !==
 | 
						|
        nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE
 | 
						|
    ) {
 | 
						|
      channel.lastActivity = activitySubtype;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  getRequestData(channel, extraData) {
 | 
						|
    let originAttributes = channel.loadInfo?.originAttributes;
 | 
						|
    let data = {
 | 
						|
      requestId: String(channel.id),
 | 
						|
      url: channel.finalURL,
 | 
						|
      method: channel.method,
 | 
						|
      type: channel.type,
 | 
						|
      fromCache: channel.fromCache,
 | 
						|
      incognito: originAttributes?.privateBrowsingId > 0,
 | 
						|
      thirdParty: channel.thirdParty,
 | 
						|
 | 
						|
      originUrl: channel.originURL || undefined,
 | 
						|
      documentUrl: channel.documentURL || undefined,
 | 
						|
 | 
						|
      tabId: this.getBrowserData(channel).tabId,
 | 
						|
      frameId: channel.frameId,
 | 
						|
      parentFrameId: channel.parentFrameId,
 | 
						|
 | 
						|
      frameAncestors: channel.frameAncestors || undefined,
 | 
						|
 | 
						|
      ip: channel.remoteAddress,
 | 
						|
 | 
						|
      proxyInfo: channel.proxyInfo,
 | 
						|
 | 
						|
      serialize: serializeRequestData,
 | 
						|
      requestSize: channel.requestSize,
 | 
						|
      responseSize: channel.responseSize,
 | 
						|
      urlClassification: channel.urlClassification,
 | 
						|
    };
 | 
						|
 | 
						|
    if (originAttributes) {
 | 
						|
      data.cookieStoreId = getCookieStoreIdForOriginAttributes(
 | 
						|
        originAttributes
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return Object.assign(data, extraData);
 | 
						|
  },
 | 
						|
 | 
						|
  handleEvent(event) {
 | 
						|
    let channel = event.currentTarget;
 | 
						|
    switch (event.type) {
 | 
						|
      case "error":
 | 
						|
        this.runChannelListener(channel, "onErrorOccurred", {
 | 
						|
          error: channel.errorString,
 | 
						|
        });
 | 
						|
        break;
 | 
						|
      case "start":
 | 
						|
        this.runChannelListener(channel, "onResponseStarted");
 | 
						|
        break;
 | 
						|
      case "stop":
 | 
						|
        this.runChannelListener(channel, "onCompleted");
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  STATUS_TYPES: new Set([
 | 
						|
    "onHeadersReceived",
 | 
						|
    "onAuthRequired",
 | 
						|
    "onBeforeRedirect",
 | 
						|
    "onResponseStarted",
 | 
						|
    "onCompleted",
 | 
						|
  ]),
 | 
						|
  FILTER_TYPES: new Set([
 | 
						|
    "onBeforeRequest",
 | 
						|
    "onBeforeSendHeaders",
 | 
						|
    "onSendHeaders",
 | 
						|
    "onHeadersReceived",
 | 
						|
    "onAuthRequired",
 | 
						|
    "onBeforeRedirect",
 | 
						|
  ]),
 | 
						|
 | 
						|
  getBrowserData(wrapper) {
 | 
						|
    let browserData = wrapper._browserData;
 | 
						|
    if (!browserData) {
 | 
						|
      if (wrapper.browserElement) {
 | 
						|
        browserData = tabTracker.getBrowserData(wrapper.browserElement);
 | 
						|
      } else {
 | 
						|
        browserData = { tabId: -1, windowId: -1 };
 | 
						|
      }
 | 
						|
      wrapper._browserData = browserData;
 | 
						|
    }
 | 
						|
    return browserData;
 | 
						|
  },
 | 
						|
 | 
						|
  runChannelListener(channel, kind, extraData = null) {
 | 
						|
    let handlerResults = [];
 | 
						|
    let requestHeaders;
 | 
						|
    let responseHeaders;
 | 
						|
 | 
						|
    try {
 | 
						|
      if (kind !== "onErrorOccurred" && channel.errorString) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      let registerFilter = this.FILTER_TYPES.has(kind);
 | 
						|
      let commonData = null;
 | 
						|
      let requestBody;
 | 
						|
      this.listeners[kind].forEach((opts, callback) => {
 | 
						|
        if (opts.filter.tabId !== null || opts.filter.windowId !== null) {
 | 
						|
          const { tabId, windowId } = this.getBrowserData(channel);
 | 
						|
          if (
 | 
						|
            (opts.filter.tabId !== null && tabId != opts.filter.tabId) ||
 | 
						|
            (opts.filter.windowId !== null && windowId != opts.filter.windowId)
 | 
						|
          ) {
 | 
						|
            return;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        if (!channel.matches(opts.filter, opts.policy, extraData)) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (!commonData) {
 | 
						|
          commonData = this.getRequestData(channel, extraData);
 | 
						|
          if (this.STATUS_TYPES.has(kind)) {
 | 
						|
            commonData.statusCode = channel.statusCode;
 | 
						|
            commonData.statusLine = channel.statusLine;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        let data = Object.create(commonData);
 | 
						|
 | 
						|
        if (registerFilter && opts.blocking && opts.policy) {
 | 
						|
          data.registerTraceableChannel = (policy, remoteTab) => {
 | 
						|
            // `channel` is a ChannelWrapper, which contains the actual
 | 
						|
            // underlying nsIChannel in `channel.channel`.  For startup events
 | 
						|
            // that are held until the extension background page is started,
 | 
						|
            // it is possible that the underlying channel can be closed and
 | 
						|
            // cleaned up between the time the event occurred and the time
 | 
						|
            // we reach this code.
 | 
						|
            if (channel.channel) {
 | 
						|
              channel.registerTraceableChannel(policy, remoteTab);
 | 
						|
            }
 | 
						|
          };
 | 
						|
        }
 | 
						|
 | 
						|
        if (opts.requestHeaders) {
 | 
						|
          requestHeaders = requestHeaders || new RequestHeaderChanger(channel);
 | 
						|
          data.requestHeaders = requestHeaders.toArray();
 | 
						|
        }
 | 
						|
 | 
						|
        if (opts.responseHeaders) {
 | 
						|
          try {
 | 
						|
            responseHeaders =
 | 
						|
              responseHeaders || new ResponseHeaderChanger(channel);
 | 
						|
            data.responseHeaders = responseHeaders.toArray();
 | 
						|
          } catch (e) {
 | 
						|
            /* headers may not be available on some redirects */
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        if (opts.requestBody && channel.canModify) {
 | 
						|
          requestBody =
 | 
						|
            requestBody || WebRequestUpload.createRequestBody(channel.channel);
 | 
						|
          data.requestBody = requestBody;
 | 
						|
        }
 | 
						|
 | 
						|
        try {
 | 
						|
          let result = callback(data);
 | 
						|
 | 
						|
          // isProxy is set during onAuth if the auth request is for a proxy.
 | 
						|
          // We allow handling proxy auth regardless of canModify.
 | 
						|
          if (
 | 
						|
            (channel.canModify || data.isProxy) &&
 | 
						|
            typeof result === "object" &&
 | 
						|
            opts.blocking
 | 
						|
          ) {
 | 
						|
            handlerResults.push({ opts, result });
 | 
						|
          }
 | 
						|
        } catch (e) {
 | 
						|
          Cu.reportError(e);
 | 
						|
        }
 | 
						|
      });
 | 
						|
    } catch (e) {
 | 
						|
      Cu.reportError(e);
 | 
						|
    }
 | 
						|
 | 
						|
    return this.applyChanges(
 | 
						|
      kind,
 | 
						|
      channel,
 | 
						|
      handlerResults,
 | 
						|
      requestHeaders,
 | 
						|
      responseHeaders
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  async applyChanges(
 | 
						|
    kind,
 | 
						|
    channel,
 | 
						|
    handlerResults,
 | 
						|
    requestHeaders,
 | 
						|
    responseHeaders
 | 
						|
  ) {
 | 
						|
    let shouldResume = !channel.suspended;
 | 
						|
    let suspenders = [];
 | 
						|
 | 
						|
    try {
 | 
						|
      for (let { opts, result } of handlerResults) {
 | 
						|
        if (isThenable(result)) {
 | 
						|
          suspenders.push(opts.addonId);
 | 
						|
          channel.suspend();
 | 
						|
          try {
 | 
						|
            result = await result;
 | 
						|
          } catch (e) {
 | 
						|
            let error;
 | 
						|
 | 
						|
            if (e instanceof Error) {
 | 
						|
              error = e;
 | 
						|
            } else if (typeof e === "object" && e.message) {
 | 
						|
              error = new Error(e.message, e.fileName, e.lineNumber);
 | 
						|
            }
 | 
						|
 | 
						|
            Cu.reportError(error);
 | 
						|
            continue;
 | 
						|
          }
 | 
						|
          if (!result || typeof result !== "object") {
 | 
						|
            continue;
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        if (
 | 
						|
          kind === "onAuthRequired" &&
 | 
						|
          result.authCredentials &&
 | 
						|
          channel.authPromptCallback
 | 
						|
        ) {
 | 
						|
          channel.authPromptCallback(result.authCredentials);
 | 
						|
        }
 | 
						|
 | 
						|
        // We allow proxy auth to cancel or handle authCredentials regardless of
 | 
						|
        // canModify, but ensure we do nothing else.
 | 
						|
        if (!channel.canModify) {
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
 | 
						|
        if (result.cancel) {
 | 
						|
          let text = "";
 | 
						|
          if (Services.profiler?.IsActive()) {
 | 
						|
            text =
 | 
						|
              `${kind} ${channel.finalURL}` +
 | 
						|
              ` by ${suspenders.join(", ")} canceled`;
 | 
						|
          }
 | 
						|
          channel.resume(text);
 | 
						|
          channel.cancel(
 | 
						|
            Cr.NS_ERROR_ABORT,
 | 
						|
            Ci.nsILoadInfo.BLOCKING_REASON_EXTENSION_WEBREQUEST
 | 
						|
          );
 | 
						|
          let { policy } = opts;
 | 
						|
          if (policy) {
 | 
						|
            let properties = channel.channel.QueryInterface(
 | 
						|
              Ci.nsIWritablePropertyBag
 | 
						|
            );
 | 
						|
            properties.setProperty("cancelledByExtension", policy.id);
 | 
						|
          }
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (result.redirectUrl) {
 | 
						|
          try {
 | 
						|
            let text = "";
 | 
						|
            if (Services.profiler?.IsActive()) {
 | 
						|
              text =
 | 
						|
                `${kind} ${channel.finalURL}` +
 | 
						|
                ` by ${suspenders.join(", ")}` +
 | 
						|
                ` redirected to ${result.redirectUrl}`;
 | 
						|
            }
 | 
						|
            channel.resume(text);
 | 
						|
            channel.redirectTo(Services.io.newURI(result.redirectUrl));
 | 
						|
 | 
						|
            // Web Extensions using the WebRequest API are allowed
 | 
						|
            // to redirect a channel to a data: URI, hence we mark
 | 
						|
            // the channel to let the redirect blocker know. Please
 | 
						|
            // note that this marking needs to happen after the
 | 
						|
            // channel.redirectTo is called because the channel's
 | 
						|
            // RedirectTo() implementation explicitly drops the flag
 | 
						|
            // to avoid additional redirects not caused by the
 | 
						|
            // Web Extension.
 | 
						|
            channel.loadInfo.allowInsecureRedirectToDataURI = true;
 | 
						|
 | 
						|
            // To pass CORS checks, we pretend the current request's
 | 
						|
            // response allows the triggering origin to access.
 | 
						|
            let origin = channel.getRequestHeader("Origin");
 | 
						|
            if (origin) {
 | 
						|
              channel.setResponseHeader("Access-Control-Allow-Origin", origin);
 | 
						|
              channel.setResponseHeader(
 | 
						|
                "Access-Control-Allow-Credentials",
 | 
						|
                "true"
 | 
						|
              );
 | 
						|
 | 
						|
              // Compute an arbitrary 'Access-Control-Allow-Headers'
 | 
						|
              // for the  internal Redirect
 | 
						|
 | 
						|
              let allowHeaders = channel
 | 
						|
                .getRequestHeaders()
 | 
						|
                .map(header => header.name)
 | 
						|
                .join();
 | 
						|
              channel.setResponseHeader(
 | 
						|
                "Access-Control-Allow-Headers",
 | 
						|
                allowHeaders
 | 
						|
              );
 | 
						|
 | 
						|
              channel.setResponseHeader(
 | 
						|
                "Access-Control-Allow-Methods",
 | 
						|
                channel.method
 | 
						|
              );
 | 
						|
            }
 | 
						|
 | 
						|
            return;
 | 
						|
          } catch (e) {
 | 
						|
            Cu.reportError(e);
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        if (result.upgradeToSecure && kind === "onBeforeRequest") {
 | 
						|
          try {
 | 
						|
            channel.upgradeToSecure();
 | 
						|
          } catch (e) {
 | 
						|
            Cu.reportError(e);
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        if (opts.requestHeaders && result.requestHeaders && requestHeaders) {
 | 
						|
          requestHeaders.applyChanges(result.requestHeaders, opts);
 | 
						|
        }
 | 
						|
 | 
						|
        if (opts.responseHeaders && result.responseHeaders && responseHeaders) {
 | 
						|
          responseHeaders.applyChanges(result.responseHeaders, opts);
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      // If a listener did not cancel the request or provide credentials, we
 | 
						|
      // forward the auth request to the base handler.
 | 
						|
      if (kind === "onAuthRequired" && channel.authPromptForward) {
 | 
						|
        channel.authPromptForward();
 | 
						|
      }
 | 
						|
 | 
						|
      if (kind === "onBeforeSendHeaders" && this.listeners.onSendHeaders.size) {
 | 
						|
        await this.runChannelListener(channel, "onSendHeaders");
 | 
						|
      } else if (kind !== "onErrorOccurred") {
 | 
						|
        channel.errorCheck();
 | 
						|
      }
 | 
						|
    } catch (e) {
 | 
						|
      Cu.reportError(e);
 | 
						|
    }
 | 
						|
 | 
						|
    // Only resume the channel if it was suspended by this call.
 | 
						|
    if (shouldResume) {
 | 
						|
      let text = "";
 | 
						|
      if (Services.profiler?.IsActive()) {
 | 
						|
        text = `${kind} ${channel.finalURL} by ${suspenders.join(", ")}`;
 | 
						|
      }
 | 
						|
      channel.resume(text);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  shouldHookListener(listener, channel, extraData) {
 | 
						|
    if (listener.size == 0) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    for (let opts of listener.values()) {
 | 
						|
      if (channel.matches(opts.filter, opts.policy, extraData)) {
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return false;
 | 
						|
  },
 | 
						|
 | 
						|
  examine(channel, topic, data) {
 | 
						|
    if (this.listeners.onHeadersReceived.size) {
 | 
						|
      this.runChannelListener(channel, "onHeadersReceived");
 | 
						|
    }
 | 
						|
 | 
						|
    if (
 | 
						|
      !channel.hasAuthRequestor &&
 | 
						|
      this.shouldHookListener(this.listeners.onAuthRequired, channel, {
 | 
						|
        isProxy: true,
 | 
						|
      })
 | 
						|
    ) {
 | 
						|
      channel.channel.notificationCallbacks = new AuthRequestor(
 | 
						|
        channel.channel,
 | 
						|
        this
 | 
						|
      );
 | 
						|
      channel.hasAuthRequestor = true;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  onChannelReplaced(oldChannel, newChannel) {
 | 
						|
    let channel = this.getWrapper(oldChannel);
 | 
						|
 | 
						|
    // We want originalURI, this will provide a moz-ext rather than jar or file
 | 
						|
    // uri on redirects.
 | 
						|
    if (this.hasRedirects) {
 | 
						|
      this.runChannelListener(channel, "onBeforeRedirect", {
 | 
						|
        redirectUrl: newChannel.originalURI.spec,
 | 
						|
      });
 | 
						|
    }
 | 
						|
    channel.channel = newChannel;
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
function HttpEvent(internalEvent, options) {
 | 
						|
  this.internalEvent = internalEvent;
 | 
						|
  this.options = options;
 | 
						|
}
 | 
						|
 | 
						|
HttpEvent.prototype = {
 | 
						|
  addListener(callback, filter = null, options = null, optionsObject = null) {
 | 
						|
    let opts = parseExtra(options, this.options, optionsObject);
 | 
						|
    opts.filter = parseFilter(filter);
 | 
						|
    HttpObserverManager.addListener(this.internalEvent, callback, opts);
 | 
						|
  },
 | 
						|
 | 
						|
  removeListener(callback) {
 | 
						|
    HttpObserverManager.removeListener(this.internalEvent, callback);
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
var onBeforeRequest = new HttpEvent("onBeforeRequest", [
 | 
						|
  "blocking",
 | 
						|
  "requestBody",
 | 
						|
]);
 | 
						|
var onBeforeSendHeaders = new HttpEvent("onBeforeSendHeaders", [
 | 
						|
  "requestHeaders",
 | 
						|
  "blocking",
 | 
						|
]);
 | 
						|
var onSendHeaders = new HttpEvent("onSendHeaders", ["requestHeaders"]);
 | 
						|
var onHeadersReceived = new HttpEvent("onHeadersReceived", [
 | 
						|
  "blocking",
 | 
						|
  "responseHeaders",
 | 
						|
]);
 | 
						|
var onAuthRequired = new HttpEvent("onAuthRequired", [
 | 
						|
  "blocking",
 | 
						|
  "responseHeaders",
 | 
						|
]);
 | 
						|
var onBeforeRedirect = new HttpEvent("onBeforeRedirect", ["responseHeaders"]);
 | 
						|
var onResponseStarted = new HttpEvent("onResponseStarted", ["responseHeaders"]);
 | 
						|
var onCompleted = new HttpEvent("onCompleted", ["responseHeaders"]);
 | 
						|
var onErrorOccurred = new HttpEvent("onErrorOccurred");
 | 
						|
 | 
						|
var WebRequest = {
 | 
						|
  onBeforeRequest,
 | 
						|
  onBeforeSendHeaders,
 | 
						|
  onSendHeaders,
 | 
						|
  onHeadersReceived,
 | 
						|
  onAuthRequired,
 | 
						|
  onBeforeRedirect,
 | 
						|
  onResponseStarted,
 | 
						|
  onCompleted,
 | 
						|
  onErrorOccurred,
 | 
						|
 | 
						|
  getSecurityInfo: details => {
 | 
						|
    let channel = ChannelWrapper.getRegisteredChannel(
 | 
						|
      details.id,
 | 
						|
      details.policy,
 | 
						|
      details.remoteTab
 | 
						|
    );
 | 
						|
    if (channel) {
 | 
						|
      return SecurityInfo.getSecurityInfo(channel.channel, details.options);
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 |