api.js (11374B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /* global ExtensionCommon, ExtensionAPI, Glean, Services, XPCOMUtils, ExtensionUtils */ 8 9 const { AddonManager } = ChromeUtils.importESModule( 10 "resource://gre/modules/AddonManager.sys.mjs" 11 ); 12 const { WebRequest } = ChromeUtils.importESModule( 13 "resource://gre/modules/WebRequest.sys.mjs" 14 ); 15 var { ExtensionParent } = ChromeUtils.importESModule( 16 "resource://gre/modules/ExtensionParent.sys.mjs" 17 ); 18 const lazy = {}; 19 20 ChromeUtils.defineESModuleGetters(lazy, { 21 AddonSearchEngine: 22 "moz-src:///toolkit/components/search/AddonSearchEngine.sys.mjs", 23 ConfigSearchEngine: 24 "moz-src:///toolkit/components/search/ConfigSearchEngine.sys.mjs", 25 }); 26 27 // eslint-disable-next-line mozilla/reject-importGlobalProperties 28 XPCOMUtils.defineLazyGlobalGetters(this, ["ChannelWrapper", "URLSearchParams"]); 29 30 const SEARCH_TOPIC_ENGINE_MODIFIED = "browser-search-engine-modified"; 31 32 this.addonsSearchDetection = class extends ExtensionAPI { 33 getAPI(context) { 34 const { extension } = context; 35 36 // We want to temporarily store the first monitored URLs that have been 37 // redirected, indexed by request IDs, so that the background script can 38 // find the add-on IDs to report. 39 this.firstMatchedUrls = {}; 40 41 return { 42 addonsSearchDetection: { 43 // `getEngines()` returns an array of engines, each with a baseUrl. 44 // AppProvided engines also include a partner paramName, 45 // while Addon provided engines include the addonId. 46 // 47 // Note: We don't return a simple list of URL patterns because the 48 // background script might want to lookup add-on IDs for a given URL in 49 // the case of server-side redirects. 50 async getEngines() { 51 const results = []; 52 53 try { 54 // Delaying accessing Services.search if we didn't get to first paint yet 55 // to avoid triggering search internals from loading too soon during the 56 // application startup. 57 if ( 58 !Cu.isESModuleLoaded( 59 "resource://gre/modules/SearchService.sys.mjs" 60 ) 61 ) { 62 await ExtensionParent.browserPaintedPromise; 63 } 64 // Return earlier if the extension or the application is shutting down. 65 if (extension.hasShutdown || Services.startup.shuttingDown) { 66 return results; 67 } 68 await Services.search.promiseInitialized; 69 const engines = await Services.search.getEngines(); 70 71 for (let engine of engines) { 72 if ( 73 !(engine instanceof lazy.AddonSearchEngine) && 74 !(engine instanceof lazy.ConfigSearchEngine) 75 ) { 76 continue; 77 } 78 79 // The search term isn't used, but avoids a warning of an empty 80 // term. 81 let submission = engine.getSubmission("searchTerm"); 82 if (submission) { 83 const uri = submission.uri; 84 const baseUrl = uri.prePath + uri.filePath; 85 86 // We don't store ids for application provided search engines 87 // because we don't need to report them. However, we do ensure 88 // the pattern is recorded (above), so that we check for 89 // redirects against those. 90 const addonId = engine.wrappedJSObject._extensionID; 91 92 let paramName; 93 for (let [key, value] of new URLSearchParams(uri.query)) { 94 if (value && value === engine.partnerCode) { 95 paramName = key; 96 } 97 } 98 99 results.push({ baseUrl, addonId, paramName }); 100 } 101 } 102 } catch (err) { 103 console.error(err); 104 } 105 106 return results; 107 }, 108 109 // `getAddonVersion()` returns the add-on version if it exists. 110 async getAddonVersion(addonId) { 111 const addon = await AddonManager.getAddonByID(addonId); 112 113 return addon && addon.version; 114 }, 115 116 // `getPublicSuffix()` returns the public suffix/Effective TLD Service 117 // of the given URL. See: `nsIEffectiveTLDService` interface in tree. 118 async getPublicSuffix(url) { 119 try { 120 return Services.eTLD.getBaseDomain(Services.io.newURI(url)); 121 } catch (err) { 122 console.error(err); 123 return null; 124 } 125 }, 126 127 reportSameSiteRedirect(extra) { 128 Glean.addonsSearchDetection.sameSiteRedirect.record(extra); 129 }, 130 131 reportETLDChangeOther(extra) { 132 Glean.addonsSearchDetection.etldChangeOther.record(extra); 133 }, 134 135 reportETLDChangeWebrequest(extra) { 136 Glean.addonsSearchDetection.etldChangeWebrequest.record(extra); 137 }, 138 139 // `onSearchEngineModified` is an event that occurs when the list of 140 // search engines has changed, e.g., a new engine has been added or an 141 // engine has been removed. 142 // 143 // See: https://searchfox.org/mozilla-central/rev/cb44fc4f7bb84f2a18fedba64c8563770df13e34/toolkit/components/search/SearchUtils.sys.mjs#185-193 144 onSearchEngineModified: new ExtensionCommon.EventManager({ 145 context, 146 name: "addonsSearchDetection.onSearchEngineModified", 147 register: fire => { 148 const onSearchEngineModifiedObserver = ( 149 aSubject, 150 aTopic, 151 aData 152 ) => { 153 if ( 154 aTopic !== SEARCH_TOPIC_ENGINE_MODIFIED || 155 // We are only interested in these modified types. 156 !["engine-added", "engine-removed", "engine-changed"].includes( 157 aData 158 ) 159 ) { 160 return; 161 } 162 163 fire.async(); 164 }; 165 166 Services.obs.addObserver( 167 onSearchEngineModifiedObserver, 168 SEARCH_TOPIC_ENGINE_MODIFIED 169 ); 170 171 return () => { 172 Services.obs.removeObserver( 173 onSearchEngineModifiedObserver, 174 SEARCH_TOPIC_ENGINE_MODIFIED 175 ); 176 }; 177 }, 178 }).api(), 179 180 // `onRedirected` is an event fired after a request has stopped and 181 // this request has been redirected once or more. The registered 182 // listeners will received the following properties: 183 // 184 // - `addonId`: the add-on ID that redirected the request, if any. 185 // - `firstUrl`: the first monitored URL of the request that has 186 // been redirected. 187 // - `lastUrl`: the last URL loaded for the request, after one or 188 // more redirects. 189 onRedirected: new ExtensionCommon.EventManager({ 190 context, 191 name: "addonsSearchDetection.onRedirected", 192 register: (fire, filter) => { 193 const stopListener = event => { 194 if (event.type != "stop") { 195 return; 196 } 197 198 const wrapper = event.currentTarget; 199 const { channel, id: requestId } = wrapper; 200 201 // When we detected a redirect, we read the request property, 202 // hoping to find an add-on ID corresponding to the add-on that 203 // initiated the redirect. It might not return anything when the 204 // redirect is a search server-side redirect but it can also be 205 // caused by an error. 206 let addonId; 207 try { 208 addonId = channel 209 ?.QueryInterface(Ci.nsIPropertyBag) 210 ?.getProperty("redirectedByExtension"); 211 } catch (err) { 212 console.error(err); 213 } 214 215 const firstUrl = this.firstMatchedUrls[requestId]; 216 // We don't need this entry anymore. 217 delete this.firstMatchedUrls[requestId]; 218 219 const lastUrl = wrapper.finalURL; 220 221 if (!firstUrl || !lastUrl) { 222 // Something went wrong but there is nothing we can do at this 223 // point. 224 return; 225 } 226 227 fire.sync({ addonId, firstUrl, lastUrl }); 228 }; 229 230 const remoteTab = context.xulBrowser.frameLoader.remoteTab; 231 232 const listener = ({ requestId, url, originUrl }) => { 233 // We exclude requests not originating from the location bar, 234 // bookmarks and other "system-ish" requests. 235 if (originUrl !== undefined) { 236 return; 237 } 238 239 // Keep the first monitored URL that was redirected for the 240 // request until the request has stopped. 241 if (!this.firstMatchedUrls[requestId]) { 242 this.firstMatchedUrls[requestId] = url; 243 244 const wrapper = ChannelWrapper.getRegisteredChannel( 245 requestId, 246 context.extension.policy, 247 remoteTab 248 ); 249 250 wrapper.addEventListener("stop", stopListener); 251 } 252 }; 253 254 const ensureRegisterChannel = data => { 255 // onRedirected depends on ChannelWrapper.getRegisteredChannel, 256 // which in turn depends on registerTraceableChannel to have been 257 // called. When a blocking webRequest listener is present, the 258 // parent/ext-webRequest.js implementation already calls that. 259 // 260 // A downside to a blocking webRequest listener is that it delays 261 // the network request until a roundtrip to the listener in the 262 // extension process has happened. Since we don't need to handle 263 // the onBeforeRequest event, avoid the overhead by handling the 264 // event and registration here, in the parent process. 265 data.registerTraceableChannel(extension.policy, remoteTab); 266 }; 267 268 const parsedFilter = { 269 types: ["main_frame"], 270 urls: ExtensionUtils.parseMatchPatterns(filter.urls), 271 }; 272 273 WebRequest.onBeforeRequest.addListener( 274 ensureRegisterChannel, 275 parsedFilter, 276 // blocking is needed to unlock data.registerTraceableChannel. 277 ["blocking"], 278 { 279 addonId: extension.id, 280 policy: extension.policy, 281 blockingAllowed: true, 282 } 283 ); 284 285 WebRequest.onBeforeRedirect.addListener( 286 listener, 287 parsedFilter, 288 // info 289 [], 290 // listener details 291 { 292 addonId: extension.id, 293 policy: extension.policy, 294 blockingAllowed: false, 295 } 296 ); 297 298 return () => { 299 WebRequest.onBeforeRequest.removeListener(ensureRegisterChannel); 300 WebRequest.onBeforeRedirect.removeListener(listener); 301 }; 302 }, 303 }).api(), 304 }, 305 }; 306 } 307 };