background.js (4908B)
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 browser */ 8 9 // Telemetry values 10 const TELEMETRY_VALUE_EXTENSION = "extension"; 11 const TELEMETRY_VALUE_SERVER = "server"; 12 13 class AddonsSearchDetection { 14 constructor() { 15 /** @type {{baseUrl: string, addonId?: string, paramName?: string}[]} */ 16 this.engines = []; 17 18 this.onRedirectedListener = this.onRedirectedListener.bind(this); 19 } 20 21 async getEngines() { 22 try { 23 this.engines = await browser.addonsSearchDetection.getEngines(); 24 } catch (err) { 25 console.error(`failed to retrieve the list of URL patterns: ${err}`); 26 this.engines = []; 27 } 28 29 return this.engines; 30 } 31 32 // When the search service changes the set of engines that are enabled, we 33 // update our pattern matching in the webrequest listeners (go to the bottom 34 // of this file for the search service events we listen to). 35 async monitor() { 36 // If there is already a listener, remove it so that we can re-add one 37 // after. This is because we're using the same listener with different URL 38 // patterns (when the list of search engines changes). 39 if ( 40 browser.addonsSearchDetection.onRedirected.hasListener( 41 this.onRedirectedListener 42 ) 43 ) { 44 browser.addonsSearchDetection.onRedirected.removeListener( 45 this.onRedirectedListener 46 ); 47 } 48 49 // Retrieve the list of URL patterns to monitor with our listener. 50 // 51 // Note: search suggestions are system principal requests, so webRequest 52 // cannot intercept them. 53 const engines = await this.getEngines(); 54 const patterns = new Set(engines.map(e => e.baseUrl + "*")); 55 56 if (patterns.size === 0) { 57 return; 58 } 59 60 browser.addonsSearchDetection.onRedirected.addListener( 61 this.onRedirectedListener, 62 { urls: [...patterns] } 63 ); 64 } 65 66 async onRedirectedListener({ addonId, firstUrl, lastUrl }) { 67 // When we do not have an add-on ID (in the request property bag), we 68 // likely detected a search server-side redirect. 69 const maybeServerSideRedirect = !addonId; 70 71 // All engines that match the initial url. 72 let engines = this.getEnginesForUrl(firstUrl); 73 74 let addonIds = []; 75 // Search server-side redirects are possible because an extension has 76 // registered a search engine, which is why we can (hopefully) retrieve the 77 // add-on ID. 78 if (maybeServerSideRedirect) { 79 addonIds = engines.filter(e => e.addonId).map(e => e.addonId); 80 } else if (addonId) { 81 addonIds = [addonId]; 82 } 83 84 if (addonIds.length === 0) { 85 // No add-on ID means there is nothing we can report. 86 return; 87 } 88 89 // This is the monitored URL that was first redirected. 90 const from = await browser.addonsSearchDetection.getPublicSuffix(firstUrl); 91 // This is the final URL after redirect(s). 92 const to = await browser.addonsSearchDetection.getPublicSuffix(lastUrl); 93 94 let sameSite = from === to; 95 let paramChanged = false; 96 if (sameSite) { 97 // We report redirects within the same site separately. 98 99 // Known limitation: if a redirect chain starts and ends with the same 100 // public suffix, it will still get reported as a same_site_redirect, 101 // even if the chain contains different public suffixes in between. 102 103 // Need special logic to detect changes to the query param named in `paramName`. 104 let firstParams = new URLSearchParams(new URL(firstUrl).search); 105 let lastParams = new URLSearchParams(new URL(lastUrl).search); 106 for (let { paramName } of engines.filter(e => e.paramName)) { 107 if (firstParams.get(paramName) !== lastParams.get(paramName)) { 108 paramChanged = true; 109 break; 110 } 111 } 112 } 113 114 for (const id of addonIds) { 115 const addonVersion = 116 await browser.addonsSearchDetection.getAddonVersion(id); 117 const extra = { 118 addonId: id, 119 addonVersion, 120 from, 121 to, 122 value: maybeServerSideRedirect 123 ? TELEMETRY_VALUE_SERVER 124 : TELEMETRY_VALUE_EXTENSION, 125 }; 126 if (sameSite) { 127 browser.addonsSearchDetection.reportSameSiteRedirect({ 128 addonId: id, 129 addonVersion, 130 origin: from, 131 paramChanged, 132 }); 133 } else if (maybeServerSideRedirect) { 134 browser.addonsSearchDetection.reportETLDChangeOther(extra); 135 } else { 136 browser.addonsSearchDetection.reportETLDChangeWebrequest(extra); 137 } 138 } 139 } 140 141 getEnginesForUrl(url) { 142 return this.engines.filter(e => url.startsWith(e.baseUrl)); 143 } 144 } 145 146 const exp = new AddonsSearchDetection(); 147 exp.monitor(); 148 149 browser.addonsSearchDetection.onSearchEngineModified.addListener(async () => { 150 await exp.monitor(); 151 });