/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /* eslint-disable mozilla/no-arbitrary-setTimeout */ // This tests submitting a ping using the stand-alone pingsender program. "use strict"; const { TelemetrySend } = ChromeUtils.importESModule( "resource://gre/modules/TelemetrySend.sys.mjs" ); const { TelemetryStorage } = ChromeUtils.importESModule( "resource://gre/modules/TelemetryStorage.sys.mjs" ); const { TelemetryUtils } = ChromeUtils.importESModule( "resource://gre/modules/TelemetryUtils.sys.mjs" ); const { setTimeout } = ChromeUtils.importESModule( "resource://gre/modules/Timer.sys.mjs" ); function generateTestPingData() { return { type: "test-pingsender-type", id: TelemetryUtils.generateUUID(), creationDate: new Date().toISOString(), version: 4, payload: { dummy: "stuff", }, }; } function testSendingPings(pingPaths) { const url = "http://localhost:" + PingServer.port + "/submit/telemetry/"; const pings = pingPaths.map(path => { return { url, path, }; }); TelemetrySend.testRunPingSender(pings, (_, topic, __) => { switch (topic) { case "process-finished": // finished indicates an exit code of 0 Assert.ok(true, "Pingsender should be able to post to localhost"); break; case "process-failed": // failed indicates an exit code != 0 Assert.ok(false, "Pingsender should be able to post to localhost"); break; } }); } /** * Wait for a ping file to be deleted from the pending pings directory. */ function waitForPingDeletion(pingId) { const path = PathUtils.join(TelemetryStorage.pingDirectoryPath, pingId); let checkFn = (resolve, reject) => setTimeout(() => { IOUtils.exists(path).then(exists => { if (!exists) { Assert.ok(true, `${pingId} was deleted`); resolve(); } else { checkFn(resolve, reject); } }, reject); }, 250); return new Promise((resolve, reject) => checkFn(resolve, reject)); } add_task(async function setup() { // Init the profile. do_get_profile(true); Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true); // Start the ping server and let Telemetry know about it. PingServer.start(); }); async function test_pingSender(version = "1.0") { // Generate a new ping and save it among the pending pings. const data = generateTestPingData(); await TelemetryStorage.savePing(data, true); // Get the local path of the saved ping. const pingPath = PathUtils.join(TelemetryStorage.pingDirectoryPath, data.id); // Spawn an HTTP server that returns an error. We will be running the // PingSender twice, trying to send the ping to this server. After the // second time, we will resolve |deferred404Hit|. let failingServer = new HttpServer(); let deferred404Hit = Promise.withResolvers(); let hitCount = 0; failingServer.registerPathHandler("/lookup_fail", (metadata, response) => { response.setStatusLine("1.1", 404, "Not Found"); hitCount++; if (hitCount >= 2) { // Resolve the promise on the next tick. Services.tm.dispatchToMainThread(() => deferred404Hit.resolve()); } }); failingServer.start(-1); // Try to send the ping twice using the pingsender (we expect 404 both times). const errorUrl = "http://localhost:" + failingServer.identity.primaryPort + "/lookup_fail"; TelemetrySend.testRunPingSender([{ url: errorUrl, path: pingPath }]); TelemetrySend.testRunPingSender([{ url: errorUrl, path: pingPath }]); // Wait until we hit the 404 server twice. After that, make sure that the ping // still exists locally. await deferred404Hit.promise; Assert.ok( await IOUtils.exists(pingPath), "The pending ping must not be deleted if we fail to send using the PingSender" ); // Try to send it using the pingsender. testSendingPings([pingPath]); let req = await PingServer.promiseNextRequest(); let ping = decodeRequestPayload(req); Assert.equal( req.getHeader("User-Agent"), `pingsender/${version}`, "Should have received the correct user agent string." ); Assert.equal( req.getHeader("X-PingSender-Version"), version, "Should have received the correct PingSender version string." ); Assert.equal( req.getHeader("Content-Encoding"), "gzip", "Should have a gzip encoded ping." ); Assert.ok(req.getHeader("Date"), "Should have received a Date header."); Assert.equal(ping.id, data.id, "Should have received the correct ping id."); Assert.equal( ping.type, data.type, "Should have received the correct ping type." ); Assert.deepEqual( ping.payload, data.payload, "Should have received the correct payload." ); // Check that the PingSender removed the pending ping. await waitForPingDeletion(data.id); // Shut down the failing server. await new Promise(r => failingServer.stop(r)); } add_task(async function test_pingsender1() { let orig = Services.prefs.getBoolPref( "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled", false ); try { Services.prefs.setBoolPref( "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled", false ); await test_pingSender("1.0"); } finally { Services.prefs.setBoolPref( "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled", orig ); } }); add_task(async function test_pingsender2() { let orig = Services.prefs.getBoolPref( "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled", false ); try { Services.prefs.setBoolPref( "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled", true ); await test_pingSender("2.0"); } finally { Services.prefs.setBoolPref( "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled", orig ); } }); add_task(async function test_bannedDomains() { // Generate a new ping and save it among the pending pings. const data = generateTestPingData(); await TelemetryStorage.savePing(data, true); // Get the local path of the saved ping. const pingPath = PathUtils.join(TelemetryStorage.pingDirectoryPath, data.id); // Confirm we can't send a ping to another destination url let bannedUris = [ "https://example.com", "http://localhost.com", "http://localHOST.com", "http://localhost@example.com", "http://localhost:bob@example.com", "http://localhost:localhost@localhost.example.com", ]; for (let url of bannedUris) { let result = await new Promise(resolve => TelemetrySend.testRunPingSender( [{ url, path: pingPath }], (_, topic, __) => { switch (topic) { case "process-finished": // finished indicates an exit code of 0 case "process-failed": // failed indicates an exit code != 0 resolve(topic); } } ) ); Assert.equal( result, "process-failed", `Pingsender should not be able to post to ${url}` ); } }); add_task(async function test_pingSender_multiple_pings() { // Generate two new pings and save them among the pending pings. const data = [generateTestPingData(), generateTestPingData()]; for (const d of data) { await TelemetryStorage.savePing(d, true); } // Get the local path of the saved pings. const pingPaths = data.map(d => PathUtils.join(TelemetryStorage.pingDirectoryPath, d.id) ); // Try to send them using the pingsender. testSendingPings(pingPaths); // Check the pings. We don't have an ordering guarantee, so we move the // elements to a new array when we find them. let data2 = []; while (data.length) { let req = await PingServer.promiseNextRequest(); let ping = decodeRequestPayload(req); let idx = data.findIndex(d => d.id == ping.id); Assert.ok( idx >= 0, `Should have received the correct ping id: ${data[idx].id}` ); data2.push(data[idx]); data.splice(idx, 1); } // Check that the PingSender removed the pending pings. for (const d of data2) { await waitForPingDeletion(d.id); } }); add_task(async function cleanup() { await PingServer.stop(); });