Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/**
* Service Class to observe and record proxy-errors related to IP-Protection
*
* @fires event:"proxy-http-error"
* Fired when the Proxy has recieved the Connect Request and responded with
* a non-2xx HTTP status code
*/
export class IPPNetworkErrorObserver {
constructor() {}
start() {
if (this.#active) {
return;
}
Services.obs.addObserver(this, "http-on-stop-request");
Services.obs.addObserver(this, "http-on-failed-opening-request");
this.#active = true;
}
stop() {
if (!this.#active) {
return;
}
this.#active = false;
this.#isolationKeys.clear();
Services.obs.removeObserver(this, "http-on-stop-request");
Services.obs.addObserver(this, "http-on-failed-opening-request");
}
addIsolationKey(key) {
if (typeof key !== "string" || !key) {
throw new Error("Isolation key must be a non-empty string");
}
this.#isolationKeys.add(key);
}
removeIsolationKey(key) {
if (typeof key !== "string" || !key) {
throw new Error("Isolation key must be a non-empty string");
}
this.#isolationKeys.delete(key);
}
addEventListener(...args) {
this._event.addEventListener(...args);
}
removeEventListener(...args) {
this._event.removeEventListener(...args);
}
observe(subject, topic, _data) {
if (
topic !== "http-on-stop-request" &&
topic !== "http-on-failed-opening-request"
) {
return;
}
try {
const chan = subject.QueryInterface(Ci.nsIHttpChannel);
const key = this.getKey(chan);
if (!key) {
// If the isolation key is unknown to us or does not
// exist, no need to care.
return;
}
const proxiedChannel = chan.QueryInterface(Ci.nsIProxiedChannel);
const proxycode = proxiedChannel.httpProxyConnectResponseCode;
switch (proxycode) {
case 0:
case 200:
// All good :)
return;
default:
this.#emitProxyHTTPError(this.#classifyLoad(chan), key, proxycode);
}
} catch (err) {
// If the channel is not an nsIHttpChannel or not proxied - all good.
}
}
/**
* Checks if a channel should be counted.
*
* @param {nsIHttpChannel} channel
* @returns {boolean} true if the channel should be counted.
*/
getKey(channel) {
try {
const proxiedChannel = channel.QueryInterface(Ci.nsIProxiedChannel);
const proxyInfo = proxiedChannel.proxyInfo;
if (!proxyInfo) {
// No proxy info, nothing to do.
return null;
}
const isolationKey = proxyInfo.connectionIsolationKey;
if (!isolationKey || !this.#isolationKeys.has(isolationKey)) {
return null;
}
return isolationKey;
} catch (err) {
// If the channel is not an nsIHttpChannel or nsIProxiedChannel, as it's irrelevant
// for this class.
}
return null;
}
#classifyLoad(channel) {
try {
if (channel.isMainDocumentChannel) {
return "error";
}
return "warning";
} catch (_) {}
return "unknown";
}
#emitProxyHTTPError(level, isolationKey, httpStatus) {
this._event.dispatchEvent(
new CustomEvent("proxy-http-error", {
detail: { level, isolationKey, httpStatus },
})
);
}
_event = new EventTarget();
#active = false;
#isolationKeys = new Set();
}
IPPNetworkErrorObserver.prototype.QueryInterface = ChromeUtils.generateQI([
Ci.nsIObserver,
]);