tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 18c91fcc4f00dd5ed460ece22fa9e212c7d5868b
parent c4e71c1f46c5f3f49c4af06f930358637e69c2a7
Author: Nathan Barrett <nbarrett@mozilla.com>
Date:   Wed,  5 Nov 2025 15:55:35 +0000

Bug 1992020 - Split out DAPSender into its own module separate from telemetry r=gleonard,mconley

Differential Revision: https://phabricator.services.mozilla.com/D270897

Diffstat:
Atoolkit/components/dap/DAPSender.sys.mjs | 327+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/dap/DAPTelemetrySender.sys.mjs | 263+++----------------------------------------------------------------------------
Mtoolkit/components/dap/moz.build | 1+
Mtoolkit/components/dap/tests/xpcshell/test_dap.js | 28++++++++++------------------
Atoolkit/components/dap/tests/xpcshell/test_dap_telemetry.js | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/dap/tests/xpcshell/xpcshell.toml | 3+++
6 files changed, 448 insertions(+), 271 deletions(-)

diff --git a/toolkit/components/dap/DAPSender.sys.mjs b/toolkit/components/dap/DAPSender.sys.mjs @@ -0,0 +1,327 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { HPKEConfigManager } from "resource://gre/modules/HPKEConfigManager.sys.mjs"; + +let lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "logConsole", function () { + return console.createInstance({ + prefix: "DAPSender", + maxLogLevelPref: "toolkit.telemetry.dap.logLevel", + }); +}); +ChromeUtils.defineESModuleGetters(lazy, { + setTimeout: "resource://gre/modules/Timer.sys.mjs", + ObliviousHTTP: "resource://gre/modules/ObliviousHTTP.sys.mjs", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gDapEndpoint", + "toolkit.telemetry.dap.leader.url" +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gLeaderHpke", + "toolkit.telemetry.dap.leader.hpke" +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gHelperHpke", + "toolkit.telemetry.dap.helper.hpke" +); + +/** + * The purpose of this singleton is to handle the core DAP (Distributed Aggregation Protocol) functionality. + * The current DAP draft standard is available here: + * https://github.com/ietf-wg-ppm/draft-ietf-ppm-dap + * + * This module provides the low-level DAP report generation and sending capabilities, + * independent of telemetry preferences. It can be used by any feature that needs + * privacy-preserving aggregation. + * + * NOTE: Do not, under any circumstances, use this mechanism directly + * to send telemetry. Use DAPTelemetrySender for that instead. + */ + +export const DAPSender = new (class { + /** + * @typedef { 'sum' | 'sumvec' | 'histogram' } VDAF + */ + + /** + * Task configuration must match a configured task on the DAP server. + * + * @typedef {object} Task + * @property {string} id - The task ID in urlsafe_base64 encoding. + * @property {VDAF} vdaf - The VDAF used by the task. + * @property {number} [bits] - The bit-width of integers in sum/sumvec measurements. + * @property {number} [length] - The number of vector/histogram elements. + * @property {number} time_precision - The rounding granularity in seconds + * that is applied to timestamps attached + * to the report. + */ + + /** + * Internal testing function to verify the DAP aggregator keys match current + * values advertised by servers. + */ + async checkHpkeKeys() { + async function check_key(url, expected) { + let response = await fetch(url + "/hpke_config"); + let body = await response.arrayBuffer(); + let actual = ChromeUtils.base64URLEncode(body, { pad: false }); + if (actual != expected) { + throw new Error(`HPKE for ${url} does not match`); + } + } + await Promise.allSettled([ + await check_key( + Services.prefs.getStringPref("toolkit.telemetry.dap.leader.url"), + Services.prefs.getStringPref("toolkit.telemetry.dap.leader.hpke") + ), + await check_key( + Services.prefs.getStringPref("toolkit.telemetry.dap.helper.url"), + Services.prefs.getStringPref("toolkit.telemetry.dap.helper.hpke") + ), + ]); + } + + /** + * Sends test reports with randomly generated measurements for testing the DAP infrastructure. + * + * @param {Array<Task>} tasks + * Array of task definitions to send test reports for. + * @param {object} options + * Options to pass to sendDAPMeasurement. + */ + async sendTestReports(tasks, options = {}) { + for (let task of tasks) { + let measurement; + if (task.vdaf == "sum") { + measurement = 3; + } else if (task.vdaf == "sumvec") { + measurement = new Array(20).fill(0); + let r = Math.floor(Math.random() * 10); + measurement[r] += 1; + measurement[19] += 1; + } else if (task.vdaf == "histogram") { + measurement = Math.floor(Math.random() * 15); + } else { + throw new Error(`Unknown VDAF ${task.vdaf}`); + } + + await this.sendDAPMeasurement(task, measurement, options); + } + } + + /** + * Periodically sends test reports on a timer. + * + * @param {Array<Task>} tasks + * Array of task definitions to send test reports for. + */ + async timedSendTestReports(tasks) { + lazy.logConsole.debug("Sending on timer."); + await this.sendTestReports(tasks); + lazy.setTimeout( + () => this.timedSendTestReports(tasks), + this.timeout_value() + ); + } + + /** + * @returns {number} A random timeout value between 9-11 minutes in milliseconds. + */ + timeout_value() { + const MINUTE = 60 * 1000; + return MINUTE * (9 + Math.random() * 2); // 9 - 11 minutes + } + + /** + * Creates a DAP report for a specific task from a measurement and sends it. + * + * @param {Task} task + * Definition of the task for which the measurement was taken. + * @param {number|Array<number>} measurement + * The measured value for which a report is generated. + * @param {object} options + * @param {number} options.timeout + * The timeout for request in milliseconds. Defaults to 30s. + * @param {string} options.reason + * A string to indicate the reason for triggering a submission. This is + * currently ignored and not recorded. + * @param {string} options.ohttp_relay + * @param {Uint8Array} options.ohttp_hpke + * If an OHTTP relay is specified, the reports are uploaded over OHTTP. + */ + async sendDAPMeasurement(task, measurement, options = {}) { + try { + const controller = new AbortController(); + lazy.setTimeout(() => controller.abort(), options.timeout ?? 30_000); + + let keys = { + leader_hpke: HPKEConfigManager.decodeKey(lazy.gLeaderHpke), + helper_hpke: HPKEConfigManager.decodeKey(lazy.gHelperHpke), + }; + + let report = this.generateReport(task, measurement, keys); + + await this.sendReport( + lazy.gDapEndpoint, + task.id, + report, + controller.signal, + options + ); + } catch (e) { + if (e.name === "AbortError") { + lazy.logConsole.error("Aborted DAP report generation: ", e); + } else { + lazy.logConsole.error("DAP report generation failed: " + e); + } + + throw e; + } + } + + /** + * @typedef {object} AggregatorKeys + * @property {Uint8Array} leader_hpke - The leader's DAP HPKE key. + * @property {Uint8Array} helper_hpke - The helper's DAP HPKE key. + */ + + /** + * Generates the encrypted DAP report. + * + * @param {Task} task + * Definition of the task for which the measurement was taken. + * @param {number|Array<number>} measurement + * The measured value for which a report is generated. + * @param {AggregatorKeys} keys + * The DAP encryption keys for each aggregator. + * + * @returns {ArrayBuffer} The generated binary report data. + */ + generateReport(task, measurement, keys) { + let task_id = new Uint8Array( + ChromeUtils.base64URLDecode(task.id, { padding: "ignore" }) + ); + + let reportOut = {}; + + if (task.vdaf === "sum") { + Services.DAPTelemetry.GetReportPrioSum( + keys.leader_hpke, + keys.helper_hpke, + measurement, + task_id, + task.bits, + task.time_precision, + reportOut + ); + } else if (task.vdaf === "sumvec") { + if (measurement.length != task.length) { + throw new Error( + "Measurement vector length doesn't match task configuration" + ); + } + Services.DAPTelemetry.GetReportPrioSumVec( + keys.leader_hpke, + keys.helper_hpke, + measurement, + task_id, + task.bits, + task.time_precision, + reportOut + ); + } else if (task.vdaf === "histogram") { + Services.DAPTelemetry.GetReportPrioHistogram( + keys.leader_hpke, + keys.helper_hpke, + measurement, + task_id, + task.length, + task.time_precision, + reportOut + ); + } else { + throw new Error( + `Unknown measurement type for task ${task.id}: ${task.vdaf} ${task.bits}` + ); + } + + return new Uint8Array(reportOut.value).buffer; + } + + /** + * Sends a report to the leader. + * + * @param {string} leader_endpoint + * The URL for the leader. + * @param {string} task_id + * Base64 encoded task_id as it appears in the upload path. + * @param {ArrayBuffer} report + * Raw bytes of the TLS encoded report. + * @param {AbortSignal} abortSignal + * Can be used to cancel network requests. Does not cancel computation. + * @param {object} options + * @param {string} options.ohttp_relay + * @param {Uint8Array} options.ohttp_hpke + * If an OHTTP relay is specified, the reports are uploaded over OHTTP. In + * this case, the OHTTP and DAP keys must be provided and this code will not + * attempt to fetch them. + * + * @returns {Promise<undefined>} Once the attempt to send the report completes, whether or not it was successful. + */ + async sendReport(leader_endpoint, task_id, report, abortSignal, options) { + const upload_path = leader_endpoint + "/tasks/" + task_id + "/reports"; + try { + let requestOptions = { + method: "PUT", + headers: { "Content-Type": "application/dap-report" }, + body: report, + signal: abortSignal, + }; + let response; + if (options.ohttp_relay) { + response = await lazy.ObliviousHTTP.ohttpRequest( + options.ohttp_relay, + options.ohttp_hpke, + upload_path, + requestOptions + ); + } else { + response = await fetch(upload_path, requestOptions); + } + + if (response.status != 200) { + const content_type = response.headers.get("content-type"); + if (content_type && content_type === "application/json") { + let error = await response.json(); + throw new Error( + `Sending failed. HTTP response: ${response.status} ${response.statusText}. Error: ${error.type} ${error.title}` + ); + } else { + let error = await response.text(); + throw new Error( + `Sending failed. HTTP response: ${response.status} ${response.statusText}. Error: ${error}` + ); + } + } else { + lazy.logConsole.debug("DAP report sent"); + } + } catch (err) { + if (err.name === "AbortError") { + lazy.logConsole.error("Aborted DAP report sending: ", err); + } else { + lazy.logConsole.error("Failed to send report: ", err); + } + + throw err; + } + } +})(); diff --git a/toolkit/components/dap/DAPTelemetrySender.sys.mjs b/toolkit/components/dap/DAPTelemetrySender.sys.mjs @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; -import { HPKEConfigManager } from "resource://gre/modules/HPKEConfigManager.sys.mjs"; +import { DAPSender } from "resource://gre/modules/DAPSender.sys.mjs"; let lazy = {}; @@ -13,11 +13,11 @@ ChromeUtils.defineLazyGetter(lazy, "logConsole", function () { maxLogLevelPref: "toolkit.telemetry.dap.logLevel", }); }); + ChromeUtils.defineESModuleGetters(lazy, { AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", - ObliviousHTTP: "resource://gre/modules/ObliviousHTTP.sys.mjs", }); XPCOMUtils.defineLazyPreferenceGetter( @@ -27,28 +27,10 @@ XPCOMUtils.defineLazyPreferenceGetter( false ); -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "gDapEndpoint", - "toolkit.telemetry.dap.leader.url" -); -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "gLeaderHpke", - "toolkit.telemetry.dap.leader.hpke" -); -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "gHelperHpke", - "toolkit.telemetry.dap.helper.hpke" -); - /** - * The purpose of this singleton is to handle sending of DAP telemetry data. - * The current DAP draft standard is available here: - * https://github.com/ietf-wg-ppm/draft-ietf-ppm-dap * - * The specific purpose of this singleton is to make the necessary calls to fetch to do networking. + * This class wraps DAPSender and adds telemetry-specific logic, ensuring that + * DAP reports are only sent when telemetry is enabled. */ export const DAPTelemetrySender = new (class { @@ -106,13 +88,13 @@ export const DAPTelemetrySender = new (class { tasks.push(task); lazy.setTimeout( - () => this.timedSendTestReports(tasks), - this.timeout_value() + () => DAPSender.timedSendTestReports(tasks), + DAPSender.timeout_value() ); lazy.NimbusFeatures.dapTelemetry.onUpdate(async () => { if (typeof this.counters !== "undefined") { - await this.sendTestReports(tasks, { reason: "nimbus-update" }); + await DAPSender.sendTestReports(tasks, { reason: "nimbus-update" }); } }); } @@ -120,7 +102,7 @@ export const DAPTelemetrySender = new (class { this._asyncShutdownBlocker = async () => { lazy.logConsole.debug(`Sending on shutdown.`); // Shorter timeout to prevent crashing due to blocking shutdown - await this.sendTestReports(tasks, { + await DAPSender.sendTestReports(tasks, { timeout: 2_000, reason: "shutdown", }); @@ -133,67 +115,8 @@ export const DAPTelemetrySender = new (class { } } - async sendTestReports(tasks, options = {}) { - for (let task of tasks) { - let measurement; - if (task.vdaf == "sum") { - measurement = 3; - } else if (task.vdaf == "sumvec") { - measurement = new Array(20).fill(0); - let r = Math.floor(Math.random() * 10); - measurement[r] += 1; - measurement[19] += 1; - } else if (task.vdaf == "histogram") { - measurement = Math.floor(Math.random() * 15); - } else { - throw new Error(`Unknown VDAF ${task.vdaf}`); - } - - await this.sendDAPMeasurement(task, measurement, options); - } - } - - async timedSendTestReports(tasks) { - lazy.logConsole.debug("Sending on timer."); - await this.sendTestReports(tasks); - lazy.setTimeout( - () => this.timedSendTestReports(tasks), - this.timeout_value() - ); - } - - timeout_value() { - const MINUTE = 60 * 1000; - return MINUTE * (9 + Math.random() * 2); // 9 - 11 minutes - } - /** - * Internal testing function to verify the DAP aggregator keys match current - * values advertised by servers. - */ - async checkHpkeKeys() { - async function check_key(url, expected) { - let response = await fetch(url + "/hpke_config"); - let body = await response.arrayBuffer(); - let actual = ChromeUtils.base64URLEncode(body, { pad: false }); - if (actual != expected) { - throw new Error(`HPKE for ${url} does not match`); - } - } - await Promise.allSettled([ - await check_key( - Services.prefs.getStringPref("toolkit.telemetry.dap.leader.url"), - Services.prefs.getStringPref("toolkit.telemetry.dap.leader.hpke") - ), - await check_key( - Services.prefs.getStringPref("toolkit.telemetry.dap.helper.url"), - Services.prefs.getStringPref("toolkit.telemetry.dap.helper.hpke") - ), - ]); - } - - /** - * Creates a DAP report for a specific task from a measurement and sends it. + * Creates a DAP report for a specific task from a measurement and sends it if telemetry is enabled. * * @param {Task} task * Definition of the task for which the measurement was taken. @@ -210,176 +133,10 @@ export const DAPTelemetrySender = new (class { * If an OHTTP relay is specified, the reports are uploaded over OHTTP. */ async sendDAPMeasurement(task, measurement, options = {}) { - try { - const controller = new AbortController(); - lazy.setTimeout(() => controller.abort(), options.timeout ?? 30_000); - - let keys = { - leader_hpke: HPKEConfigManager.decodeKey(lazy.gLeaderHpke), - helper_hpke: HPKEConfigManager.decodeKey(lazy.gHelperHpke), - }; - - let report = this.generateReport(task, measurement, keys); - - await this.sendReport( - lazy.gDapEndpoint, - task.id, - report, - controller.signal, - options - ); - } catch (e) { - if (e.name === "AbortError") { - lazy.logConsole.error("Aborted DAP report generation: ", e); - } else { - lazy.logConsole.error("DAP report generation failed: " + e); - } - - throw e; - } - } - - /** - * @typedef {object} AggregatorKeys - * @property {Uint8Array} leader_hpke - The leader's DAP HPKE key. - * @property {Uint8Array} helper_hpke - The helper's DAP HPKE key. - */ - - /** - * Generates the encrypted DAP report. - * - * @param {Task} task - * Definition of the task for which the measurement was taken. - * @param {number|Array<number>} measurement - * The measured value for which a report is generated. - * @param {AggregatorKeys} keys - * The DAP encryption keys for each aggregator. - * - * @returns {ArrayBuffer} The generated binary report data. - */ - generateReport(task, measurement, keys) { - let task_id = new Uint8Array( - ChromeUtils.base64URLDecode(task.id, { padding: "ignore" }) - ); - - let reportOut = {}; - - if (task.vdaf === "sum") { - Services.DAPTelemetry.GetReportPrioSum( - keys.leader_hpke, - keys.helper_hpke, - measurement, - task_id, - task.bits, - task.time_precision, - reportOut - ); - } else if (task.vdaf === "sumvec") { - if (measurement.length != task.length) { - throw new Error( - "Measurement vector length doesn't match task configuration" - ); - } - Services.DAPTelemetry.GetReportPrioSumVec( - keys.leader_hpke, - keys.helper_hpke, - measurement, - task_id, - task.bits, - task.time_precision, - reportOut - ); - } else if (task.vdaf === "histogram") { - Services.DAPTelemetry.GetReportPrioHistogram( - keys.leader_hpke, - keys.helper_hpke, - measurement, - task_id, - task.length, - task.time_precision, - reportOut - ); - } else { - throw new Error( - `Unknown measurement type for task ${task.id}: ${task.vdaf} ${task.bits}` - ); - } - - return new Uint8Array(reportOut.value).buffer; - } - - /** - * Sends a report to the leader. - * - * @param {string} leader_endpoint - * The URL for the leader. - * @param {string} task_id - * Base64 encoded task_id as it appears in the upload path. - * @param {ArrayBuffer} report - * Raw bytes of the TLS encoded report. - * @param {AbortSignal} abortSignal - * Can be used to cancel network requests. Does not cancel computation. - * @param {object} options - * @param {string} options.ohttp_relay - * @param {Uint8Array} options.ohttp_hpke - * If an OHTTP relay is specified, the reports are uploaded over OHTTP. In - * this case, the OHTTP and DAP keys must be provided and this code will not - * attempt to fetch them. - * - * @returns {Promise<undefined>} Once the attempt to send the report completes, whether or not it was successful. - */ - async sendReport(leader_endpoint, task_id, report, abortSignal, options) { - // If telemetry disabled, don't upload DAP reports either. if (!lazy.gTelemetryEnabled) { return; } - const upload_path = leader_endpoint + "/tasks/" + task_id + "/reports"; - try { - let requestOptions = { - method: "PUT", - headers: { "Content-Type": "application/dap-report" }, - body: report, - signal: abortSignal, - }; - let response; - if (options.ohttp_relay) { - response = await lazy.ObliviousHTTP.ohttpRequest( - options.ohttp_relay, - options.ohttp_hpke, - upload_path, - requestOptions - ); - } else { - response = await fetch(upload_path, requestOptions); - } - - if (response.status != 200) { - const content_type = response.headers.get("content-type"); - if (content_type && content_type === "application/json") { - // A JSON error from the DAP server. - let error = await response.json(); - throw new Error( - `Sending failed. HTTP response: ${response.status} ${response.statusText}. Error: ${error.type} ${error.title}` - ); - } else { - // A different error, e.g. from a load-balancer. - let error = await response.text(); - throw new Error( - `Sending failed. HTTP response: ${response.status} ${response.statusText}. Error: ${error}` - ); - } - } else { - lazy.logConsole.debug("DAP report sent"); - } - } catch (err) { - if (err.name === "AbortError") { - lazy.logConsole.error("Aborted DAP report sending: ", err); - } else { - lazy.logConsole.error("Failed to send report: ", err); - } - - throw err; - } + await DAPSender.sendDAPMeasurement(task, measurement, options); } })(); diff --git a/toolkit/components/dap/moz.build b/toolkit/components/dap/moz.build @@ -39,6 +39,7 @@ UNIFIED_SOURCES += [ EXTRA_JS_MODULES += [ "DAPIncrementality.sys.mjs", "DAPReportController.sys.mjs", + "DAPSender.sys.mjs", "DAPTelemetrySender.sys.mjs", "DAPVisitCounter.sys.mjs", ] diff --git a/toolkit/components/dap/tests/xpcshell/test_dap.js b/toolkit/components/dap/tests/xpcshell/test_dap.js @@ -11,7 +11,7 @@ const { HttpServer } = ChromeUtils.importESModule( const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - DAPTelemetrySender: "resource://gre/modules/DAPTelemetrySender.sys.mjs", + DAPSender: "resource://gre/modules/DAPSender.sys.mjs", }); const BinaryInputStream = Components.Constructor( @@ -22,7 +22,6 @@ const BinaryInputStream = Components.Constructor( const PREF_LEADER = "toolkit.telemetry.dap.leader.url"; const PREF_HELPER = "toolkit.telemetry.dap.helper.url"; - const PREF_DATAUPLOAD = "datareporting.healthreport.uploadEnabled"; let server; @@ -109,7 +108,7 @@ add_setup(async function () { add_task(async function testVerificationTask() { server_requests = []; - await lazy.DAPTelemetrySender.sendTestReports(tasks, { timeout: 5000 }); + await lazy.DAPSender.sendTestReports(tasks, { timeout: 5000 }); Assert.deepEqual( server_requests, task_report_sizes, @@ -125,7 +124,7 @@ add_task(async function testNetworkError() { let thrownErr; try { - await lazy.DAPTelemetrySender.sendTestReports(tasks, { timeout: 5000 }); + await lazy.DAPSender.sendTestReports(tasks, { timeout: 5000 }); } catch (e) { thrownErr = e; } @@ -136,21 +135,14 @@ add_task(async function testNetworkError() { Services.prefs.setStringPref(PREF_LEADER, test_leader); }); -add_task(async function testTelemetryToggle() { - // Normal - server_requests = []; - await lazy.DAPTelemetrySender.sendTestReports(tasks, { timeout: 5000 }); - Assert.deepEqual(server_requests, task_report_sizes); - - // Telemetry off +add_task(async function testTelemetryDisabled() { server_requests = []; Services.prefs.setBoolPref(PREF_DATAUPLOAD, false); - await lazy.DAPTelemetrySender.sendTestReports(tasks, { timeout: 5000 }); - Assert.deepEqual(server_requests, []); - - // Normal - server_requests = []; + await lazy.DAPSender.sendTestReports(tasks, { timeout: 5000 }); + Assert.deepEqual( + server_requests, + task_report_sizes, + "Report upload successful even with telemetry disabled." + ); Services.prefs.clearUserPref(PREF_DATAUPLOAD); - await lazy.DAPTelemetrySender.sendTestReports(tasks, { timeout: 5000 }); - Assert.deepEqual(server_requests, task_report_sizes); }); diff --git a/toolkit/components/dap/tests/xpcshell/test_dap_telemetry.js b/toolkit/components/dap/tests/xpcshell/test_dap_telemetry.js @@ -0,0 +1,97 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + DAPTelemetrySender: "resource://gre/modules/DAPTelemetrySender.sys.mjs", +}); + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +const PREF_LEADER = "toolkit.telemetry.dap.leader.url"; +const PREF_HELPER = "toolkit.telemetry.dap.helper.url"; +const PREF_DATAUPLOAD = "datareporting.healthreport.uploadEnabled"; + +let server; +let server_addr; + +let server_requests = []; + +const task = { + id: "QjMD4n8l_MHBoLrbCfLTFi8hC264fC59SKHPviPF0q8", + vdaf: "sum", + bits: 8, + time_precision: 300, +}; + +const task_report_size = 886; + +function uploadHandler(request, response) { + Assert.equal( + request.getHeader("Content-Type"), + "application/dap-report", + "Wrong Content-Type header." + ); + + let body = new BinaryInputStream(request.bodyInputStream); + server_requests.push(body.available()); + + response.setStatusLine(request.httpVersion, 200); +} + +add_setup(async function () { + do_get_profile(); + + server = new HttpServer(); + server.registerPrefixHandler("/leader_endpoint/tasks/", uploadHandler); + server.start(-1); + const i = server.identity; + server_addr = i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort; + + Services.prefs.setStringPref(PREF_LEADER, server_addr + "/leader_endpoint"); + Services.prefs.setStringPref(PREF_HELPER, server_addr + "/helper_endpoint"); + registerCleanupFunction(() => { + Services.prefs.clearUserPref(PREF_LEADER); + Services.prefs.clearUserPref(PREF_HELPER); + + return new Promise(resolve => { + server.stop(resolve); + }); + }); +}); + +add_task(async function testTelemetryToggle() { + server_requests = []; + await lazy.DAPTelemetrySender.sendDAPMeasurement(task, 3, { timeout: 5000 }); + Assert.deepEqual( + server_requests, + [task_report_size], + "Telemetry enabled works." + ); + + server_requests = []; + Services.prefs.setBoolPref(PREF_DATAUPLOAD, false); + await lazy.DAPTelemetrySender.sendDAPMeasurement(task, 3, { timeout: 5000 }); + Assert.deepEqual(server_requests, [], "Telemetry disabled blocks sending."); + + server_requests = []; + Services.prefs.clearUserPref(PREF_DATAUPLOAD); + await lazy.DAPTelemetrySender.sendDAPMeasurement(task, 3, { timeout: 5000 }); + Assert.deepEqual( + server_requests, + [task_report_size], + "Telemetry re-enabled works." + ); +}); diff --git a/toolkit/components/dap/tests/xpcshell/xpcshell.toml b/toolkit/components/dap/tests/xpcshell/xpcshell.toml @@ -10,5 +10,8 @@ skip-if = ["os == 'android'"] # DAP is not supported on Android ["test_dap_report_controller.js"] skip-if = ["os == 'android'"] # DAP is not supported on Android +["test_dap_telemetry.js"] +skip-if = ["os == 'android'"] # DAP is not supported on Android + ["test_dap_visit_counter.js"] skip-if = ["os == 'android'"] # DAP is not supported on Android