IPPChannelFilter.sys.mjs (12477B)
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 http://mozilla.org/MPL/2.0/. */ 4 5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 const lazy = XPCOMUtils.declareLazy({ 8 IPPExceptionsManager: 9 "moz-src:///browser/components/ipprotection/IPPExceptionsManager.sys.mjs", 10 ProxyService: { 11 service: "@mozilla.org/network/protocol-proxy-service;1", 12 iid: Ci.nsIProtocolProxyService, 13 }, 14 }); 15 const { TRANSPARENT_PROXY_RESOLVES_HOST } = Ci.nsIProxyInfo; 16 const failOverTimeout = 10; // seconds 17 18 const MODE_PREF = "browser.ipProtection.mode"; 19 20 export const IPPMode = Object.freeze({ 21 MODE_FULL: 0, 22 MODE_PB: 1, 23 MODE_TRACKER: 2, 24 }); 25 26 const TRACKING_FLAGS = 27 Ci.nsIClassifiedChannel.CLASSIFIED_TRACKING | 28 Ci.nsIClassifiedChannel.CLASSIFIED_TRACKING_AD | 29 Ci.nsIClassifiedChannel.CLASSIFIED_TRACKING_ANALYTICS | 30 Ci.nsIClassifiedChannel.CLASSIFIED_TRACKING_SOCIAL | 31 Ci.nsIClassifiedChannel.CLASSIFIED_TRACKING_CONTENT; 32 33 const DEFAULT_EXCLUDED_URL_PREFS = [ 34 "browser.ipProtection.guardian.endpoint", 35 "identity.fxaccounts.remote.profile.uri", 36 "identity.fxaccounts.auth.uri", 37 "identity.fxaccounts.remote.profile.uri", 38 ]; 39 40 const ESSENTIAL_URL_PREFS = [ 41 "toolkit.telemetry.server", 42 "network.trr.uri", 43 "network.trr.default_provider_uri", 44 ]; 45 46 /** 47 * IPPChannelFilter is a class that implements the nsIProtocolProxyChannelFilter 48 * when active it will funnel all requests to its provided proxy. 49 * 50 * the connection can be stopped 51 * 52 */ 53 export class IPPChannelFilter { 54 /** 55 * Creates a new IPPChannelFilter that can connect to a proxy server. After 56 * created, the proxy can be immediately activated. It will suspend all the 57 * received nsIChannel until the object is fully initialized. 58 * 59 * @param {Array<string>} [excludedPages] - list of page URLs whose *origin* should bypass the proxy 60 */ 61 static create(excludedPages = []) { 62 return new IPPChannelFilter(excludedPages); 63 } 64 65 /** 66 * Sets the IPP Mode. 67 * 68 * @param {IPPMode} [mode] - the new mode 69 */ 70 static setMode(mode) { 71 Services.prefs.setIntPref(MODE_PREF, mode); 72 } 73 74 /** 75 * Takes a protocol definition and constructs the appropriate nsIProxyInfo 76 * 77 * @typedef {import("./IPProtectionServerlist.sys.mjs").MasqueProtocol} MasqueProtocol 78 * @typedef {import("./IPProtectionServerlist.sys.mjs").ConnectProtocol } ConnectProtocol 79 * 80 * @param {string} authToken - a bearer token for the proxy server. 81 * @param {string} isolationKey - the isolation key for the proxy connection. 82 * @param {MasqueProtocol|ConnectProtocol} protocol - the protocol definition. 83 * @param {nsIProxyInfo} fallBackInfo - optional fallback proxy info. 84 * @returns {nsIProxyInfo} 85 */ 86 static constructProxyInfo( 87 authToken, 88 isolationKey, 89 protocol, 90 fallBackInfo = null 91 ) { 92 switch (protocol.name) { 93 case "masque": 94 return lazy.ProxyService.newMASQUEProxyInfo( 95 protocol.host, 96 protocol.port, 97 protocol.templateString, 98 authToken, 99 isolationKey, 100 TRANSPARENT_PROXY_RESOLVES_HOST, 101 failOverTimeout, 102 fallBackInfo 103 ); 104 case "connect": 105 return lazy.ProxyService.newProxyInfo( 106 protocol.scheme, 107 protocol.host, 108 protocol.port, 109 authToken, 110 isolationKey, 111 TRANSPARENT_PROXY_RESOLVES_HOST, 112 failOverTimeout, 113 fallBackInfo 114 ); 115 default: 116 throw new Error( 117 "Cannot construct ProxyInfo for Unknown server-protocol: " + 118 protocol.name 119 ); 120 } 121 } 122 /** 123 * Takes a server definition and constructs the appropriate nsIProxyInfo 124 * If the server supports multiple Protocols, a fallback chain will be created. 125 * The first protocol in the list will be the primary one, with the others as fallbacks. 126 * 127 * @typedef {import("./IPProtectionServerlist.sys.mjs").Server} Server 128 * @param {string} authToken - a bearer token for the proxy server. 129 * @param {Server} server - the server to connect to. 130 * @returns {nsIProxyInfo} 131 */ 132 static serverToProxyInfo(authToken, server) { 133 const isolationKey = IPPChannelFilter.makeIsolationKey(); 134 return server.protocols.reduceRight((fallBackInfo, protocol) => { 135 return IPPChannelFilter.constructProxyInfo( 136 authToken, 137 isolationKey, 138 protocol, 139 fallBackInfo 140 ); 141 }, null); 142 } 143 144 /** 145 * Initialize a IPPChannelFilter object. After this step, the filter, if 146 * active, will process the new and the pending channels. 147 * 148 * @typedef {import("./IPProtectionServerlist.sys.mjs").Server} Server 149 * @param {string} authToken - a bearer token for the proxy server. 150 * @param {Server} server - the server to connect to. 151 */ 152 initialize(authToken = "", server) { 153 if (this.proxyInfo) { 154 throw new Error("Double initialization?!?"); 155 } 156 const proxyInfo = IPPChannelFilter.serverToProxyInfo(authToken, server); 157 Object.freeze(proxyInfo); 158 this.proxyInfo = proxyInfo; 159 160 this.#processPendingChannels(); 161 } 162 163 /** 164 * @param {Array<string>} [excludedPages] 165 */ 166 constructor(excludedPages = []) { 167 // Normalize and store excluded origins (scheme://host[:port]) 168 this.#excludedOrigins = new Set(); 169 excludedPages.forEach(url => { 170 this.addPageExclusion(url); 171 }); 172 173 DEFAULT_EXCLUDED_URL_PREFS.forEach(pref => { 174 const prefValue = Services.prefs.getStringPref(pref, ""); 175 if (prefValue) { 176 this.addPageExclusion(prefValue); 177 } 178 }); 179 180 // Get origins essential to starting the proxy and exclude 181 // them prior to connecting 182 this.#essentialOrigins = new Set(); 183 ESSENTIAL_URL_PREFS.forEach(pref => { 184 const prefValue = Services.prefs.getStringPref(pref, ""); 185 if (prefValue) { 186 this.addEssentialExclusion(prefValue); 187 } 188 }); 189 190 XPCOMUtils.defineLazyPreferenceGetter( 191 this, 192 "mode", 193 MODE_PREF, 194 IPPMode.MODE_FULL 195 ); 196 } 197 198 /** 199 * This method (which is required by the nsIProtocolProxyService interface) 200 * is called to apply proxy filter rules for the given URI and proxy object 201 * (or list of proxy objects). 202 * 203 * @param {nsIChannel} channel The channel for which these proxy settings apply. 204 * @param {nsIProxyInfo} _defaultProxyInfo The proxy (or list of proxies) that 205 * would be used by default for the given URI. This may be null. 206 * @param {nsIProxyProtocolFilterResult} proxyFilter 207 */ 208 applyFilter(channel, _defaultProxyInfo, proxyFilter) { 209 // If this channel should be excluded (origin match), do nothing 210 if (!this.#matchMode(channel) || this.shouldExclude(channel)) { 211 // Calling this with "null" will enforce a non-proxy connection 212 proxyFilter.onProxyFilterResult(null); 213 return; 214 } 215 216 if (!this.proxyInfo) { 217 // We are not initialized yet! 218 this.#pendingChannels.push({ channel, proxyFilter }); 219 return; 220 } 221 222 proxyFilter.onProxyFilterResult(this.proxyInfo); 223 224 // Notify observers that the channel is being proxied 225 this.#observers.forEach(observer => { 226 observer(channel); 227 }); 228 } 229 230 #matchMode(channel) { 231 switch (this.mode) { 232 case IPPMode.MODE_PB: 233 return !!channel.loadInfo.originAttributes.privateBrowsingId; 234 235 case IPPMode.MODE_TRACKER: 236 return ( 237 TRACKING_FLAGS & 238 channel.loadInfo.triggeringThirdPartyClassificationFlags 239 ); 240 241 case IPPMode.MODE_FULL: 242 default: 243 return true; 244 } 245 } 246 247 /** 248 * Decide whether a channel should bypass the proxy based on origin. 249 * 250 * @param {nsIChannel} channel 251 * @returns {boolean} 252 */ 253 shouldExclude(channel) { 254 try { 255 const uri = channel.URI; // nsIURI 256 if (!uri) { 257 return true; 258 } 259 260 if (!["http", "https"].includes(uri.scheme)) { 261 return true; 262 } 263 264 const origin = uri.prePath; // scheme://host[:port] 265 266 if (!this.proxyInfo && this.#essentialOrigins.has(origin)) { 267 return true; 268 } 269 270 let loadingPrincipal = channel.loadInfo?.loadingPrincipal; 271 let hasExclusion = 272 loadingPrincipal && 273 lazy.IPPExceptionsManager.hasExclusion(loadingPrincipal); 274 275 if (hasExclusion) { 276 return true; 277 } 278 279 return this.#excludedOrigins.has(origin); 280 } catch (_) { 281 return true; 282 } 283 } 284 285 /** 286 * Adds a page URL to the exclusion list. 287 * 288 * @param {string} url - The URL to exclude. 289 * @param {Set<string>} [list] - The exclusion list to add the URL to. 290 */ 291 addPageExclusion(url, list = this.#excludedOrigins) { 292 try { 293 const uri = Services.io.newURI(url); 294 // prePath is scheme://host[:port] 295 list.add(uri.prePath); 296 } catch (_) { 297 // ignore bad entries 298 } 299 } 300 301 /** 302 * Adds a URL to the essential exclusion list. 303 * 304 * @param {string} url - The URL to exclude. 305 */ 306 addEssentialExclusion(url) { 307 this.addPageExclusion(url, this.#essentialOrigins); 308 } 309 310 /** 311 * Starts the Channel Filter, feeding all following Requests through the proxy. 312 */ 313 start() { 314 lazy.ProxyService.registerChannelFilter( 315 this /* nsIProtocolProxyChannelFilter aFilter */, 316 0 /* unsigned long aPosition */ 317 ); 318 this.#active = true; 319 } 320 321 /** 322 * Stops the Channel Filter, stopping all following Requests from being proxied. 323 */ 324 stop() { 325 if (!this.#active) { 326 return; 327 } 328 329 lazy.ProxyService.unregisterChannelFilter(this); 330 331 this.#abortPendingChannels(); 332 333 this.#active = false; 334 this.#abort.abort(); 335 } 336 337 /** 338 * Returns the isolation key of the proxy connection. 339 * All ProxyInfo objects related to this Connection will have the same isolation key. 340 */ 341 get isolationKey() { 342 return this.proxyInfo.connectionIsolationKey; 343 } 344 345 get hasPendingChannels() { 346 return !!this.#pendingChannels.length; 347 } 348 349 /** 350 * Replaces the authentication token used by the proxy connection. 351 * --> Important <--: This Changes the isolationKey of the Connection! 352 * 353 * @param {string} newToken - The new authentication token. 354 */ 355 replaceAuthToken(newToken) { 356 const newInfo = lazy.ProxyService.newProxyInfo( 357 this.proxyInfo.type, 358 this.proxyInfo.host, 359 this.proxyInfo.port, 360 newToken, 361 IPPChannelFilter.makeIsolationKey(), 362 TRANSPARENT_PROXY_RESOLVES_HOST, 363 failOverTimeout, 364 null // Failover proxy info 365 ); 366 Object.freeze(newInfo); 367 this.proxyInfo = newInfo; 368 } 369 370 /** 371 * Returns an async generator that yields channels this Connection is proxying. 372 * 373 * This allows to introspect channels that are proxied, i.e 374 * to measure usage, or catch proxy errors. 375 * 376 * @returns {AsyncGenerator<nsIChannel>} An async generator that yields proxied channels. 377 * @yields {object} 378 * Proxied channels. 379 */ 380 async *proxiedChannels() { 381 const stop = Promise.withResolvers(); 382 this.#abort.signal.addEventListener( 383 "abort", 384 () => { 385 stop.reject(); 386 }, 387 { once: true } 388 ); 389 while (this.#active) { 390 const { promise, resolve } = Promise.withResolvers(); 391 this.#observers.push(resolve); 392 try { 393 const result = await Promise.race([stop.promise, promise]); 394 this.#observers = this.#observers.filter( 395 observer => observer !== resolve 396 ); 397 yield result; 398 } catch (error) { 399 // Stop iteration if the filter is stopped or aborted 400 return; 401 } 402 } 403 } 404 405 /** 406 * Returns true if this filter is active. 407 */ 408 get active() { 409 return this.#active; 410 } 411 412 #processPendingChannels() { 413 if (this.#pendingChannels.length) { 414 this.#pendingChannels.forEach(data => 415 this.applyFilter(data.channel, null, data.proxyFilter) 416 ); 417 this.#pendingChannels = []; 418 } 419 } 420 421 #abortPendingChannels() { 422 if (this.#pendingChannels.length) { 423 this.#pendingChannels.forEach(data => 424 data.channel.cancel(Cr.NS_BINDING_ABORTED) 425 ); 426 this.#pendingChannels = []; 427 } 428 } 429 430 #abort = new AbortController(); 431 #observers = []; 432 #active = false; 433 #excludedOrigins = new Set(); 434 #essentialOrigins = new Set(); 435 #pendingChannels = []; 436 437 static makeIsolationKey() { 438 return Math.random().toString(36).slice(2, 18).padEnd(16, "0"); 439 } 440 }