IPPNetworkErrorObserver.sys.mjs (3701B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 5 /** 6 * Service Class to observe and record proxy-errors related to IP-Protection 7 * 8 * @fires IPPNetworkErrorObserver#"proxy-http-error" 9 * Fired when the Proxy has recieved the Connect Request and responded with 10 * a non-2xx HTTP status code 11 */ 12 export class IPPNetworkErrorObserver { 13 constructor() {} 14 15 start() { 16 if (this.#active) { 17 return; 18 } 19 Services.obs.addObserver(this, "http-on-stop-request"); 20 Services.obs.addObserver(this, "http-on-failed-opening-request"); 21 this.#active = true; 22 } 23 stop() { 24 if (!this.#active) { 25 return; 26 } 27 this.#active = false; 28 this.#isolationKeys.clear(); 29 Services.obs.removeObserver(this, "http-on-failed-opening-request"); 30 Services.obs.removeObserver(this, "http-on-stop-request"); 31 } 32 33 addIsolationKey(key) { 34 if (typeof key !== "string" || !key) { 35 throw new Error("Isolation key must be a non-empty string"); 36 } 37 this.#isolationKeys.add(key); 38 } 39 removeIsolationKey(key) { 40 if (typeof key !== "string" || !key) { 41 throw new Error("Isolation key must be a non-empty string"); 42 } 43 this.#isolationKeys.delete(key); 44 } 45 addEventListener(...args) { 46 this._event.addEventListener(...args); 47 } 48 49 removeEventListener(...args) { 50 this._event.removeEventListener(...args); 51 } 52 53 observe(subject, topic, _data) { 54 if ( 55 topic !== "http-on-stop-request" && 56 topic !== "http-on-failed-opening-request" 57 ) { 58 return; 59 } 60 try { 61 const chan = subject.QueryInterface(Ci.nsIHttpChannel); 62 const key = this.getKey(chan); 63 if (!key) { 64 // If the isolation key is unknown to us or does not 65 // exist, no need to care. 66 return; 67 } 68 const proxiedChannel = chan.QueryInterface(Ci.nsIProxiedChannel); 69 const proxycode = proxiedChannel.httpProxyConnectResponseCode; 70 switch (proxycode) { 71 case 0: 72 case 200: 73 // All good :) 74 return; 75 default: 76 this.#emitProxyHTTPError(this.#classifyLoad(chan), key, proxycode); 77 } 78 } catch (err) { 79 // If the channel is not an nsIHttpChannel or not proxied - all good. 80 } 81 } 82 /** 83 * Checks if a channel should be counted. 84 * 85 * @param {nsIHttpChannel} channel 86 * @returns {boolean} true if the channel should be counted. 87 */ 88 getKey(channel) { 89 try { 90 const proxiedChannel = channel.QueryInterface(Ci.nsIProxiedChannel); 91 const proxyInfo = proxiedChannel.proxyInfo; 92 if (!proxyInfo) { 93 // No proxy info, nothing to do. 94 return null; 95 } 96 const isolationKey = proxyInfo.connectionIsolationKey; 97 if (!isolationKey || !this.#isolationKeys.has(isolationKey)) { 98 return null; 99 } 100 return isolationKey; 101 } catch (err) { 102 // If the channel is not an nsIHttpChannel or nsIProxiedChannel, as it's irrelevant 103 // for this class. 104 } 105 return null; 106 } 107 108 #classifyLoad(channel) { 109 try { 110 if (channel.isMainDocumentChannel) { 111 return "error"; 112 } 113 return "warning"; 114 } catch (_) {} 115 return "unknown"; 116 } 117 118 #emitProxyHTTPError(level, isolationKey, httpStatus) { 119 this._event.dispatchEvent( 120 new CustomEvent("proxy-http-error", { 121 detail: { level, isolationKey, httpStatus }, 122 }) 123 ); 124 } 125 _event = new EventTarget(); 126 127 #active = false; 128 #isolationKeys = new Set(); 129 } 130 131 IPPNetworkErrorObserver.prototype.QueryInterface = ChromeUtils.generateQI([ 132 Ci.nsIObserver, 133 ]);