TRANSPARENT_PROXY_RESOLVES_HOST, failOverTimeout, fallBackInfo ); default: throw new Error( "Cannot construct ProxyInfo for Unknown server-protocol: " + protocol.name ); } } /** * Takes a server definition and constructs the appropriate nsIProxyInfo * If the server supports multiple Protocols, a fallback chain will be created. * The first protocol in the list will be the primary one, with the others as fallbacks. * * @typedef {import("./IPProtectionServerlist.sys.mjs").Server} Server * @param {string} authToken - a bearer token for the proxy server. * @param {Server} server - the server to connect to. * @returns {nsIProxyInfo} */ static serverToProxyInfo(authToken, server) { const isolationKey = IPPChannelFilter.makeIsolationKey(); return server.protocols.reduceRight((fallBackInfo, protocol) => { return IPPChannelFilter.constructProxyInfo( authToken, isolationKey, protocol, fallBackInfo ); }, null); } /** * Initialize a IPPChannelFilter object. After this step, the filter, if * active, will process the new and the pending channels. * * @typedef {import("./IPProtectionServerlist.sys.mjs").Server} Server * @param {string} authToken - a bearer token for the proxy server. * @param {Server} server - the server to connect to. */ initialize(authToken = "", server) { if (this.proxyInfo) { throw new Error("Double initialization?!?"); } const proxyInfo = IPPChannelFilter.serverToProxyInfo(authToken, server); Object.freeze(proxyInfo); this.proxyInfo = proxyInfo; this.#processPendingChannels(); } /** * @param {Array} [excludedPages] */ constructor(excludedPages = []) { // Normalize and store excluded origins (scheme://host[:port]) this.#excludedOrigins = new Set(); excludedPages.forEach(url => { this.addPageExclusion(url); }); DEFAULT_EXCLUDED_URL_PREFS.forEach(pref => { const prefValue = Services.prefs.getStringPref(pref, ""); if (prefValue) { this.addPageExclusion(prefValue); } }); // Get origins essential to starting the proxy and exclude // them prior to connecting this.#essentialOrigins = new Set(); ESSENTIAL_URL_PREFS.forEach(pref => { const prefValue = Services.prefs.getStringPref(pref, ""); if (prefValue) { this.addEssentialExclusion(prefValue); } }); XPCOMUtils.defineLazyPreferenceGetter( this, "mode", MODE_PREF, IPPMode.MODE_FULL ); } /** * This method (which is required by the nsIProtocolProxyService interface) * is called to apply proxy filter rules for the given URI and proxy object * (or list of proxy objects). * * @param {nsIChannel} channel The channel for which these proxy settings apply. * @param {nsIProxyInfo} _defaultProxyInfo The proxy (or list of proxies) that * would be used by default for the given URI. This may be null. * @param {nsIProxyProtocolFilterResult} proxyFilter */ applyFilter(channel, _defaultProxyInfo, proxyFilter) { // If this channel should be excluded (origin match), do nothing if (!this.#matchMode(channel) || this.shouldExclude(channel)) { // Calling this with "null" will enforce a non-proxy connection proxyFilter.onProxyFilterResult(null); return; } if (!this.proxyInfo) { // We are not initialized yet! this.#pendingChannels.push({ channel, proxyFilter }); return; } proxyFilter.onProxyFilterResult(this.proxyInfo); // Notify observers that the channel is being proxied this.#observers.forEach(observer => { observer(channel); }); } #matchMode(channel) { switch (this.mode) { case IPPMode.MODE_PB: return !!channel.loadInfo.originAttributes.privateBrowsingId; case IPPMode.MODE_TRACKER: return ( TRACKING_FLAGS & channel.loadInfo.triggeringThirdPartyClassificationFlags ); case IPPMode.MODE_FULL: default: return true; } } /** * Decide whether a channel should bypass the proxy based on origin. * * @param {nsIChannel} channel * @returns {boolean} */ shouldExclude(channel) { try { const uri = channel.URI; // nsIURI if (!uri) { return true; } if (!["http", "https"].includes(uri.scheme)) { return true; } const origin = uri.prePath; // scheme://host[:port] if (!this.proxyInfo && this.#essentialOrigins.has(origin)) { return true; } let loadingPrincipal = channel.loadInfo?.loadingPrincipal; let hasExclusion = loadingPrincipal && lazy.IPPExceptionsManager.hasExclusion(loadingPrincipal); if (hasExclusion) { return true; } return this.#excludedOrigins.has(origin); } catch (_) { return true; } } /** * Adds a page URL to the exclusion list. * * @param {string} url - The URL to exclude. * @param {Set} [list] - The exclusion list to add the URL to. */ addPageExclusion(url, list = this.#excludedOrigins) { try { const uri = Services.io.newURI(url); // prePath is scheme://host[:port] list.add(uri.prePath); } catch (_) { // ignore bad entries } } /** * Adds a URL to the essential exclusion list. * * @param {string} url - The URL to exclude. */ addEssentialExclusion(url) { this.addPageExclusion(url, this.#essentialOrigins); } /** * Starts the Channel Filter, feeding all following Requests through the proxy. */ start() { lazy.ProxyService.registerChannelFilter( this /* nsIProtocolProxyChannelFilter aFilter */, 0 /* unsigned long aPosition */ ); this.#active = true; } /** * Stops the Channel Filter, stopping all following Requests from being proxied. */ stop() { if (!this.#active) { return; } lazy.ProxyService.unregisterChannelFilter(this); this.#abortPendingChannels(); this.#active = false; this.#abort.abort(); } /** * Returns the isolation key of the proxy connection. * All ProxyInfo objects related to this Connection will have the same isolation key. */ get isolationKey() { return this.proxyInfo.connectionIsolationKey; } get hasPendingChannels() { return !!this.#pendingChannels.length; } /** * Replaces the authentication token used by the proxy connection. * --> Important <--: This Changes the isolationKey of the Connection! * * @param {string} newToken - The new authentication token. */ replaceAuthToken(newToken) { const newInfo = lazy.ProxyService.newProxyInfo( this.proxyInfo.type, this.proxyInfo.host, this.proxyInfo.port, newToken, IPPChannelFilter.makeIsolationKey(), TRANSPARENT_PROXY_RESOLVES_HOST, failOverTimeout, null // Failover proxy info ); Object.freeze(newInfo); this.proxyInfo = newInfo; } /** * Returns an async generator that yields channels this Connection is proxying. * * This allows to introspect channels that are proxied, i.e * to measure usage, or catch proxy errors. * * @returns {AsyncGenerator} An async generator that yields proxied channels. * @yields {object} * Proxied channels. */ async *proxiedChannels() { const stop = Promise.withResolvers(); this.#abort.signal.addEventListener( "abort", () => { stop.reject(); }, { once: true } ); while (this.#active) { const { promise, resolve } = Promise.withResolvers(); this.#observers.push(resolve); try { const result = await Promise.race([stop.promise, promise]); this.#observers = this.#observers.filter( observer => observer !== resolve ); yield result; } catch (error) { // Stop iteration if the filter is stopped or aborted return; } } } /** * Returns true if this filter is active. */ get active() { return this.#active; } #processPendingChannels() { if (this.#pendingChannels.length) { this.#pendingChannels.forEach(data => this.applyFilter(data.channel, null, data.proxyFilter) ); this.#pendingChannels = []; } } #abortPendingChannels() { if (this.#pendingChannels.length) { this.#pendingChannels.forEach(data => data.channel.cancel(Cr.NS_BINDING_ABORTED) ); this.#pendingChannels = []; } } #abort = new AbortController(); #observers = []; #active = false; #excludedOrigins = new Set(); #essentialOrigins = new Set(); #pendingChannels = []; static makeIsolationKey() { return Math.random().toString(36).slice(2, 18).padEnd(16, "0"); } } PK