test_detected_requests_telemetry.js (10158B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ 3 */ 4 5 const { AddonManager } = ChromeUtils.importESModule( 6 "resource://gre/modules/AddonManager.sys.mjs" 7 ); 8 const { AddonTestUtils } = ChromeUtils.importESModule( 9 "resource://testing-common/AddonTestUtils.sys.mjs" 10 ); 11 const { ExtensionTestUtils } = ChromeUtils.importESModule( 12 "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" 13 ); 14 15 ChromeUtils.defineESModuleGetters(this, { 16 RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", 17 }); 18 19 AddonTestUtils.init(this); 20 AddonTestUtils.overrideCertDB(); 21 AddonTestUtils.createAppInfo( 22 "xpcshell@tests.mozilla.org", 23 "XPCShell", 24 "139", 25 "139" 26 ); 27 ExtensionTestUtils.init(this); 28 29 const BUILTIN_ADDON_ID = "data-leak-blocker@mozilla.com"; 30 const RS_COLLECTION = "addons-data-leak-blocker-domains"; 31 const GLEAN_REGISTERED_PING = "dataLeakBlocker"; 32 const GLEAN_REGISTERED_CATEGORY = "dataLeakBlocker"; 33 const GLEAN_REGISTERED_METRIC = "reportV1"; 34 35 const IS_GLEAN_DATA_LEAK_BLOCKER_BUILTIN = GLEAN_REGISTERED_CATEGORY in Glean; 36 37 const server = AddonTestUtils.createHttpServer({ 38 hosts: ["expected.example.org", "unexpected.example.org"], 39 }); 40 server.registerPathHandler("/test", (req, res) => { 41 info(`Test HTTP server for domain "${req.host}" got ${req.method} request\n`); 42 res.setStatusLine(req.httpVersion, 200, "OK"); 43 // Needed by the MV3 extension (due to the MV3 content script being inherited 44 // from the webpage instead of being overridden by a fetch instance that belongs 45 // to the extension ExpandedPrincipal as for MV2 content scripts). 46 res.setHeader("Access-Control-Allow-Origin", "http://unexpected.example.org"); 47 res.write("OK"); 48 }); 49 50 add_setup(async () => { 51 do_get_profile(); 52 53 // Disable loading artifacts build jogfile for this test 54 // (workaround Bug 1983674). 55 Services.prefs.setBoolPref("telemetry.fog.artifact_build", false); 56 57 Services.fog.initializeFOG(); 58 59 // Enable scope application and override the built_in_addons.json 60 // resource to ensure the add-on built-in into the Firefox Desktop 61 // omni jar is going to be loaded. 62 Services.prefs.setIntPref( 63 "extensions.enabledScopes", 64 AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION 65 ); 66 67 // Enable more verbose logging for the logs emitted by this extension. 68 Services.prefs.setBoolPref(`extensions.${BUILTIN_ADDON_ID}.testing`, true); 69 70 // Reduce timeout on the add-on's DeferredTask that submits the custom 71 // ping. 72 Services.prefs.setIntPref( 73 `extensions.${BUILTIN_ADDON_ID}.gleanSubmitTaskTimeout`, 74 500 75 ); 76 77 const builtinsConfig = await fetch( 78 "chrome://browser/content/built_in_addons.json" 79 ).then(res => res.json()); 80 81 await AddonTestUtils.overrideBuiltIns({ 82 system: [], 83 builtins: builtinsConfig.builtins.filter( 84 entry => entry.addon_id === BUILTIN_ADDON_ID 85 ), 86 }); 87 await AddonTestUtils.promiseStartupManager({ 88 lateStartup: false, 89 }); 90 91 // Sanity check. 92 const builtinAddon = await AddonManager.getAddonByID(BUILTIN_ADDON_ID); 93 ok(builtinAddon, "Got AddonWrapper instance for the builtin add-on"); 94 ok(builtinAddon.isActive, "Expect builtin add-on to be active"); 95 96 if (!IS_GLEAN_DATA_LEAK_BLOCKER_BUILTIN) { 97 Assert.ok( 98 !(GLEAN_REGISTERED_PING in GleanPings), 99 "Expect runtime registered ping to be registered" 100 ); 101 Assert.ok( 102 !( 103 GLEAN_REGISTERED_CATEGORY in Glean && 104 GLEAN_REGISTERED_METRIC in Glean[GLEAN_REGISTERED_CATEGORY] 105 ), 106 "Expect runtime registered metric to be registered" 107 ); 108 } 109 110 await AddonTestUtils.notifyLateStartup(); 111 112 Assert.ok( 113 GLEAN_REGISTERED_PING in GleanPings, 114 "Expect runtime registered ping to be registered" 115 ); 116 Assert.ok( 117 GLEAN_REGISTERED_CATEGORY in Glean && 118 GLEAN_REGISTERED_METRIC in Glean[GLEAN_REGISTERED_CATEGORY], 119 "Expect runtime registered metric to be registered" 120 ); 121 }); 122 123 async function test_extension_fetch({ 124 addon_id, 125 manifest_version = 2, 126 expectedGleanEvents, 127 }) { 128 const extension = ExtensionTestUtils.loadExtension({ 129 useAddonManager: "permanent", 130 manifest: { 131 manifest_version, 132 browser_specific_settings: { 133 gecko: { id: addon_id }, 134 }, 135 content_scripts: [ 136 { 137 matches: ["*://*.example.org/*"], 138 js: ["content_script.js"], 139 }, 140 ], 141 ...(manifest_version >= 3 142 ? { 143 host_permissions: ["<all_urls>"], 144 // Prevent http request to be auto-upgraded to https 145 content_security_policy: { 146 extension_pages: "script-src 'self';", 147 }, 148 } 149 : { permissions: ["<all_urls>"] }), 150 }, 151 background() { 152 const { browser } = this; 153 browser.test.onMessage.addListener(async (msg, ...args) => { 154 if (msg === "extpage-call-fetch") { 155 const [{ url, fetchOptions }] = args; 156 let fetchError; 157 await fetch(url, fetchOptions).catch(err => (fetchError = err)); 158 browser.test.sendMessage(`${msg}:done`, String(fetchError)); 159 } 160 }); 161 }, 162 files: { 163 "content_script.js": function () { 164 const { browser } = this; 165 browser.test.onMessage.addListener(async (msg, ...args) => { 166 if (msg === "contentscript-call-fetch") { 167 const [{ url, fetchOptions }] = args; 168 let fetchError; 169 await fetch(url, fetchOptions).catch(err => (fetchError = err)); 170 browser.test.sendMessage(`${msg}:done`, String(fetchError)); 171 } 172 }); 173 browser.test.sendMessage("contentscript:ready"); 174 }, 175 }, 176 }); 177 178 await extension.startup(); 179 180 const page = await ExtensionTestUtils.loadContentPage( 181 "http://unexpected.example.org/test" 182 ); 183 const pingSubmitDeferred = Promise.withResolvers(); 184 const allGleanEvents = []; 185 const pingSubmitCallback = () => { 186 info("dataAbuseDetection Glean ping submit callback called"); 187 const gleanEvents = Glean[GLEAN_REGISTERED_CATEGORY][ 188 GLEAN_REGISTERED_METRIC 189 ].testGetValue()?.map(event => event.extra); 190 allGleanEvents.push(...(gleanEvents ?? [])); 191 if (allGleanEvents.length < expectedGleanEvents.length) { 192 info( 193 `Wait for next Glean ping submit for the missing events (${JSON.stringify( 194 { 195 expected: expectedGleanEvents.length, 196 actual: allGleanEvents.length, 197 } 198 )})` 199 ); 200 GleanPings[GLEAN_REGISTERED_PING].testBeforeNextSubmit( 201 pingSubmitCallback 202 ); 203 return; 204 } 205 pingSubmitDeferred.resolve(); 206 }; 207 GleanPings[GLEAN_REGISTERED_PING].testBeforeNextSubmit(pingSubmitCallback); 208 209 const rsClient = RemoteSettings(RS_COLLECTION); 210 const rsData = [ 211 { 212 id: "monitored-domains-set1", 213 domains: ["expected.example.org"], 214 }, 215 ]; 216 await rsClient.db.importChanges({}, Date.now(), rsData, { 217 clear: true, 218 }); 219 await rsClient.emit("sync", { data: {} }); 220 221 extension.sendMessage("extpage-call-fetch", { 222 url: "http://unexpected.example.org/test#extpage", 223 }); 224 Assert.equal( 225 await extension.awaitMessage("extpage-call-fetch:done"), 226 "undefined", 227 "Expect non monitored domains to not be blocked" 228 ); 229 extension.sendMessage("extpage-call-fetch", { 230 url: "http://expected.example.org/test#extpage", 231 }); 232 Assert.equal( 233 await extension.awaitMessage("extpage-call-fetch:done"), 234 "TypeError: NetworkError when attempting to fetch resource.", 235 "Expect monitored domains to be blocked" 236 ); 237 238 // Expect mv2 content script request to be attributed to the 239 // addon (and blocked), while mv3 content script are expected 240 // to not be attributed to addons (and not blocked). 241 const expectContentScriptRequestBlocked = manifest_version < 3; 242 243 await extension.awaitMessage("contentscript:ready"); 244 245 extension.sendMessage("contentscript-call-fetch", { 246 url: "http://unexpected.example.org/test#extcs", 247 fetchOptions: { method: "POST" }, 248 }); 249 Assert.equal( 250 await extension.awaitMessage("contentscript-call-fetch:done"), 251 "undefined", 252 "Expect non monitored domains to not be blocked" 253 ); 254 extension.sendMessage("contentscript-call-fetch", { 255 url: "http://expected.example.org/test#extcs", 256 fetchOptions: { method: "POST" }, 257 }); 258 Assert.equal( 259 await extension.awaitMessage("contentscript-call-fetch:done"), 260 expectContentScriptRequestBlocked 261 ? "TypeError: NetworkError when attempting to fetch resource." 262 : "undefined", 263 expectContentScriptRequestBlocked 264 ? "Expect monitored domains to be blocked on mv2 fetch request" 265 : "Expect monitored domains to not be blocked on mv3 fetch request or exempted add-ons" 266 ); 267 268 await extension.unload(); 269 await page.close(); 270 271 info("Wait for the Glean ping to be submitted"); 272 273 await pingSubmitDeferred.promise; 274 275 Assert.deepEqual( 276 allGleanEvents, 277 expectedGleanEvents, 278 "Got the expected telemetry events" 279 ); 280 } 281 282 add_task(async function test_mv2_extension() { 283 const addon_id = "ext-mv2@test"; 284 await test_extension_fetch({ 285 addon_id, 286 manifest_version: 2, 287 expectedGleanEvents: [ 288 { 289 addon_id, 290 method: "GET", 291 blocked: "true", 292 content_policy_type: `${Ci.nsIContentPolicy.TYPE_FETCH}`, 293 is_addon_triggering: "true", 294 is_addon_loading: "false", 295 is_content_script: "false", 296 }, 297 { 298 addon_id, 299 blocked: "true", 300 method: "POST", 301 content_policy_type: `${Ci.nsIContentPolicy.TYPE_FETCH}`, 302 is_addon_triggering: "true", 303 is_addon_loading: "false", 304 is_content_script: "true", 305 }, 306 ], 307 }); 308 }); 309 310 add_task(async function test_mv3_extension() { 311 const addon_id = "ext-mv3@test"; 312 await test_extension_fetch({ 313 addon_id, 314 manifest_version: 3, 315 expectedGleanEvents: [ 316 { 317 method: "GET", 318 blocked: "true", 319 content_policy_type: `${Ci.nsIContentPolicy.TYPE_FETCH}`, 320 is_addon_triggering: "true", 321 is_addon_loading: "false", 322 is_content_script: "false", 323 addon_id, 324 }, 325 ], 326 }); 327 });