gecko-dev/toolkit/components/telemetry/dap/DAPTelemetrySender.sys.mjs

199 lines
6.6 KiB
JavaScript

/* 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";
let lazy = {};
XPCOMUtils.defineLazyGetter(lazy, "logConsole", function() {
return console.createInstance({
prefix: "DAPTelemetrySender",
maxLogLevelPref: "toolkit.telemetry.dap.logLevel",
});
});
XPCOMUtils.defineLazyModuleGetters(lazy, {
NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm",
});
const PREF_LEADER = "toolkit.telemetry.dap_leader";
const PREF_HELPER = "toolkit.telemetry.dap_helper";
XPCOMUtils.defineLazyPreferenceGetter(lazy, "LEADER", PREF_LEADER, undefined);
XPCOMUtils.defineLazyPreferenceGetter(lazy, "HELPER", PREF_HELPER, undefined);
/**
* 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.
*/
export const DAPTelemetrySender = new (class {
startup() {
lazy.logConsole.info("Performing DAP startup");
if (lazy.NimbusFeatures.dapTelemetry.getVariable("task1Enabled")) {
// For now we are sending a constant value because it simplifies verification.
let measurement = 3;
this.sendVerificationTaskReport(measurement);
}
}
/**
* For testing: sends a hard coded report for a hard coded task.
*
* @param {number} measurement
*/
async sendVerificationTaskReport(measurement) {
lazy.logConsole.info("Trying to send verification task.");
// For now there is only a single task which is hardcoded here.
/**
* @typedef {object} Task
* @property {string} id_hexstring - The task's ID hex encoded.
* @property {string} id_base64 - The same ID base 64 encoded.
* @property {string} leader_endpoint - Base URL for the leader.
* @property {string} helper_endpoint - Base URL for the helper.
* @property {number} time_precision - Timestamps (in s) are rounded to the nearest multiple of this.
*/
const task = {
// Note that this does not exactly match the task definition from the standard linked-to above.
id_hexstring:
"4ad95d3b67332ff89a505da296315b88b88d4f1c5535d3c780fbae1162c79ec9",
id_base64: "StldO2czL_iaUF2iljFbiLiNTxxVNdPHgPuuEWLHnsk",
leader_endpoint: null,
helper_endpoint: null,
time_precision: 600,
};
task.leader_endpoint = lazy.LEADER;
if (!task.leader_endpoint) {
lazy.logConsole.error('Preference "' + PREF_LEADER + '" not set');
return;
}
task.helper_endpoint = lazy.HELPER;
if (!task.helper_endpoint) {
lazy.logConsole.error('Preference "' + PREF_HELPER + '" not set');
return;
}
try {
let report = await this.generateReport(task, measurement);
Glean.dap.reportGenerationStatus.success.add(1);
await this.sendReport(task.leader_endpoint, report);
} catch (e) {
Glean.dap.reportGenerationStatus.failure.add(1);
lazy.logConsole.error("DAP report generation failed: " + e.message);
}
}
/**
* Downloads HPKE configs for endpoints and generates report.
*
* @param {Task} task
* Definition of the task for which the measurement was taken.
* @param {number} measurement
* The measured value for which a report is generated.
* @returns Promise
* @resolves {Uint8Array} The generated binary report data.
* @rejects {Error} If an exception is thrown while generating the report.
*/
async generateReport(task, measurement) {
let [leader_config_bytes, helper_config_bytes] = await Promise.all([
this.getHpkeConfig(
task.leader_endpoint + "/hpke_config?task_id=" + task.id_base64
),
this.getHpkeConfig(
task.helper_endpoint + "/hpke_config?task_id=" + task.id_base64
),
]);
let task_id = hexString2Binary(task.id_hexstring);
let report = {};
Services.DAPTelemetry.GetReport(
leader_config_bytes,
helper_config_bytes,
measurement,
task_id,
task.time_precision,
report
);
let reportData = new Uint8Array(report.value);
return reportData;
}
/**
* Fetches TLS encoded HPKE config from a URL.
*
* @param {string} endpoint
* The URL from where to get the data.
* @returns Promise
* @resolves {Uint8Array} The binary representation of the endpoint configuration.
* @rejects {Error} If an exception is thrown while fetching the configuration.
*/
async getHpkeConfig(endpoint) {
let response = await fetch(endpoint);
if (!response.ok) {
throw new Error(
`Failed to retrieve HPKE config for DAP from: ${endpoint}. Response: ${
response.status
}: ${await response.text()}.`
);
}
let buffer = await response.arrayBuffer();
let hpke_config_bytes = new Uint8Array(buffer);
return hpke_config_bytes;
}
/**
* Sends a report to the leader.
*
* @param {string} leader_endpoint
* The URL for the leader.
* @param {Uint8Array} report
* Raw bytes of the TLS encoded report.
* @returns Promise
* @resolves {undefined} Once the attempt to send the report completes, whether or not it was successful.
*/
async sendReport(leader_endpoint, report) {
const upload_path = leader_endpoint + "/upload";
try {
let response = await fetch(upload_path, {
method: "POST",
headers: { "Content-Type": "application/dap-report" },
body: report,
});
if (response.status != 200) {
let error = await response.json();
lazy.logConsole.error(
`Sending failed. HTTP response: ${response.status} ${response.statusText}. Error: ${error.type} ${error.title}`
);
Glean.dap.uploadStatus.failure.add(1);
} else {
lazy.logConsole.info("DAP report sent");
Glean.dap.uploadStatus.success.add(1);
}
} catch (err) {
lazy.logConsole.error("Failed to send report. fetch failed", err);
Glean.dap.uploadStatus.failure.add(1);
}
}
})();
/**
* Converts a hex representation of a byte string into an array
* @param {string} hexstring - a list of bytes represented as a hex string two characters per bytes
* @return {Uint8Array} - the input byte list as an array
*/
function hexString2Binary(hexstring) {
const binlen = hexstring.length / 2;
let binary = new Uint8Array(binlen);
for (var i = 0; i < binlen; i++) {
binary[i] = parseInt(hexstring.substring(2 * i, 2 * (i + 1)), 16);
}
return binary;
}