SponsorProtection.sys.mjs (7264B)
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 = {}; 8 9 XPCOMUtils.defineLazyServiceGetter( 10 lazy, 11 "ProxyService", 12 "@mozilla.org/network/protocol-proxy-service;1", 13 Ci.nsIProtocolProxyService 14 ); 15 16 ChromeUtils.defineLazyGetter(lazy, "logConsole", function () { 17 return console.createInstance({ 18 prefix: "SponsorProtection", 19 maxLogLevel: Services.prefs.getBoolPref( 20 "browser.newtabpage.sponsor-protection.debug", 21 false 22 ) 23 ? "Debug" 24 : "Warn", 25 }); 26 }); 27 28 XPCOMUtils.defineLazyPreferenceGetter( 29 lazy, 30 "SPONSOR_PROTECTION_ENABLED", 31 "browser.newtabpage.sponsor-protection.enabled", 32 false 33 ); 34 35 const HTTP_STOP_REQUEST_TOPIC = "http-on-stop-request"; 36 const BYTES_PER_KB = 1024; 37 38 /** 39 * This class tracks a list of <browser> elements that have done a navigation 40 * by way of a sponsored link from New Tab. The goal is to eventually apply 41 * client protections on these <browser> elements such that network traffic is 42 * forwarded through an HTTP CONNECT or MASQUE relay, thus hiding the client's 43 * real IP address from the advertiser site. A less unique UA string will also 44 * be assigned to <browser> elements under protection. 45 */ 46 export class _SponsorProtection { 47 #protectedBrowsers = new WeakSet(); 48 #observerAndFilterAdded = false; 49 #debugEnabled = false; 50 51 constructor() { 52 this.#debugEnabled = Services.prefs.getBoolPref( 53 "browser.newtabpage.sponsor-protection.debug", 54 false 55 ); 56 } 57 58 /** 59 * True if the SponsorProtection mechanism is enabled. 60 */ 61 get enabled() { 62 return lazy.SPONSOR_PROTECTION_ENABLED; 63 } 64 65 /** 66 * True if the debug mode for SponsorProtection was enabled upon construction. 67 * Debug mode adds a decoration to the tab hover preview to help identify 68 * protected browsers, and also emits logging to the console. 69 */ 70 get debugEnabled() { 71 return this.#debugEnabled; 72 } 73 74 /** 75 * Registers a <browser> so that sponsor protection is applied for 76 * subsequent network connections from that <browser>. 77 * 78 * @param {Browser} browser 79 * The <browser> to have sponsor protected applied. 80 */ 81 addProtectedBrowser(browser) { 82 if (!this.enabled) { 83 return; 84 } 85 86 if (!this.#observerAndFilterAdded) { 87 this.#addObserverAndChannelFilter(); 88 } 89 90 this.#protectedBrowsers.add(browser.permanentKey); 91 lazy.logConsole.debug("Registering browser for sponsor protection"); 92 93 // TODO: This is where the clamped UA string will be applied to this 94 // browser. 95 } 96 97 /** 98 * Unregisters a <browser> so that sponsor protection is no longer 99 * applied for subsequent network connections from that <browser>. 100 * This is a no-op if the <browser> was not actually being protected. 101 * 102 * @param {Browser} browser 103 * The <browser> to have sponsor protected removed. 104 */ 105 removeProtectedBrowser(browser) { 106 this.#protectedBrowsers.delete(browser.permanentKey); 107 lazy.logConsole.debug("Unregistering browser for sponsor protection"); 108 109 // TODO: This is where we remove the clamped UA string from this browser. 110 } 111 112 /** 113 * Returns true if the <browser> is having sponsor protection applied. 114 * 115 * @param {Browser} browser 116 * @returns {boolean} 117 */ 118 isProtectedBrowser(browser) { 119 return this.#protectedBrowsers.has(browser.permanentKey); 120 } 121 122 /** 123 * Sets up the HTTP_STOP_REQUEST_TOPIC observer and proxy channel filter when 124 * the number of protected browsers in the WeakSet goes from 0 to 1. 125 */ 126 #addObserverAndChannelFilter() { 127 Services.obs.addObserver(this, HTTP_STOP_REQUEST_TOPIC); 128 129 lazy.ProxyService.registerChannelFilter(this, 0); 130 this.#observerAndFilterAdded = true; 131 lazy.logConsole.debug("Added observer and channel filter."); 132 } 133 134 /** 135 * Removes the HTTP_STOP_REQUEST_TOPIC observer and proxy channel filter when 136 * the number of protected browsers in the WeakSet goes to 0. 137 */ 138 #removeObserverAndChannelFilter() { 139 Services.obs.removeObserver(this, HTTP_STOP_REQUEST_TOPIC); 140 141 lazy.ProxyService.unregisterChannelFilter(this); 142 this.#observerAndFilterAdded = false; 143 lazy.logConsole.debug("Removed observer and channel filter."); 144 } 145 146 /* nsIObserver */ 147 148 /** 149 * Observes the HTTP_STOP_REQUEST_TOPIC observer notification and, if the 150 * associated channel comes from a protected browser, records the request 151 * and response sizes to telemetry. 152 * 153 * This observer is also used to determine if there are remaining protected 154 * browsers - and if not, to unregister the observer and channel filter. 155 * 156 * @param {nsIChannel} subject 157 * For HTTP_STOP_REQUEST_TOPIC, this should be an nsIChannel. 158 * @param {string} topic 159 * @param {string} _data 160 */ 161 observe(subject, topic, _data) { 162 if (topic != HTTP_STOP_REQUEST_TOPIC) { 163 return; 164 } 165 166 // If all the <browser> elements have gone away from our WeakSet, at this 167 // point we can go ahead and get rid of our observer and filter. 168 if ( 169 !ChromeUtils.nondeterministicGetWeakSetKeys(this.#protectedBrowsers) 170 .length 171 ) { 172 this.#removeObserverAndChannelFilter(); 173 return; 174 } 175 176 if (!(subject instanceof Ci.nsIHttpChannel)) { 177 return; 178 } 179 180 let channel = subject; 181 const { browsingContext } = channel.loadInfo; 182 let browser = browsingContext?.top.embedderElement; 183 if (!browser || !this.#protectedBrowsers.has(browser.permanentKey)) { 184 return; 185 } 186 187 // requestSize includes the request headers and payload 188 const requestSize = channel.requestSize; 189 190 // transferSize includes the response headers and payload 191 const responseSize = channel.transferSize; 192 193 Glean.newtab.sponsNavTrafficSent.accumulate( 194 Math.round(requestSize / BYTES_PER_KB) 195 ); 196 Glean.newtab.sponsNavTrafficRecvd.accumulate( 197 Math.round(responseSize / BYTES_PER_KB) 198 ); 199 200 lazy.logConsole.debug( 201 `Channel for ${browser.currentURI.spec} (${channel.URI.spec}) - sent: ${requestSize} recv'd: ${responseSize}` 202 ); 203 } 204 205 /* nsIProtocolProxyChannelFilter */ 206 207 /** 208 * Checks a created nsIChannel to see if it qualifies for proxying. If it 209 * does, proxy configuration appropriate for this client is applied. 210 * 211 * @param {nsIChannel} channel 212 * @param {nsIProxyInfo} proxyInfo 213 * @param {nsIProxyProtocolFilterResult} callback 214 */ 215 applyFilter(channel, proxyInfo, callback) { 216 const { browsingContext } = channel.loadInfo; 217 let browser = browsingContext?.top.embedderElement; 218 if (!browser || !this.#protectedBrowsers.has(browser)) { 219 callback.onProxyFilterResult(proxyInfo); 220 return; 221 } 222 223 // This is where proxy information, if we have any, will be applied for 224 // the connection. For now however, we're not proxying anything, so just 225 // fallthrough to the default behaviour. 226 callback.onProxyFilterResult(proxyInfo); 227 } 228 229 QueryInterface = ChromeUtils.generateQI([ 230 Ci.nsIObserver, 231 Ci.nsIProtocolProxyChannelFilter, 232 ]); 233 } 234 235 export const SponsorProtection = new _SponsorProtection();