forked from mirrors/gecko-dev
Add a function that sets scalars to convey what kind of data is present in the :Zone.Identifier Alternate Data Stream. Also adds testing that the scalars get set correctly. Does not actually call the function that sends the telemetry. This will be done in the next patch since we need to call it in some slightly different places than we usually would since we want to be sure it is included in the `new-profile` ping and in the ping with the `installation.first_seen` event. Differential Revision: https://phabricator.services.mozilla.com/D171818
517 lines
20 KiB
JavaScript
517 lines
20 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
|
|
const lazy = {};
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AttributionIOUtils: "resource:///modules/AttributionCode.sys.mjs",
|
|
});
|
|
|
|
let gReadZoneIdPromise = null;
|
|
let gTelemetryPromise = null;
|
|
|
|
export var ProvenanceData = {
|
|
/**
|
|
* Clears cached code/Promises. For testing only.
|
|
*/
|
|
_clearCache() {
|
|
if (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) {
|
|
gReadZoneIdPromise = null;
|
|
gTelemetryPromise = null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns an nsIFile for the file containing the zone identifier-based
|
|
* provenance data. This currently only exists on Windows. On other platforms,
|
|
* this will return null.
|
|
*/
|
|
get zoneIdProvenanceFile() {
|
|
if (AppConstants.platform == "win") {
|
|
let file = Services.dirsvc.get("GreD", Ci.nsIFile);
|
|
file.append("zoneIdProvenanceData");
|
|
return file;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Loads the provenance data that the installer copied (along with some
|
|
* metadata) from the Firefox installer used to create the current
|
|
* installation.
|
|
*
|
|
* If it doesn't already exist, creates a global Promise that loads the data
|
|
* from the file and caches it. Subsequent calls get the same Promise. There
|
|
* is no support for re-reading the file from the disk because there is no
|
|
* good reason that the contents of the file should change.
|
|
*
|
|
* Only expected contents will be pulled out of the file. This does not
|
|
* extract arbitrary, unexpected data. Data will be validated, to the extent
|
|
* possible. Most possible data returned has a potential key indicating that
|
|
* we read an unexpected value out of the file.
|
|
*
|
|
* @returns `null` on unsupported OSs. Otherwise, an object with these
|
|
* possible keys:
|
|
* `readProvenanceError`
|
|
* Will be present if there was a problem when Firefox tried to read the
|
|
* file that ought to have been written by the installer. Possible values
|
|
* are:
|
|
* `noSuchFile`, `readError`, `parseError`
|
|
* If this key is present, no other keys will be present.
|
|
* `fileSystem`
|
|
* What filesystem the installer was on. If available, this will be a
|
|
* string returned from `GetVolumeInformationByHandleW` via its
|
|
* `lpFileSystemNameBuffer` parameter. Possible values are:
|
|
* `NTFS`, `FAT32`, `other`, `missing`, `readIniError`
|
|
* `other` will be used if the value read does not match one of the
|
|
* expected file systems.
|
|
* `missing` will be used if the file didn't contain `fileSystem` or
|
|
* `readFsError` information.
|
|
* `readIniError` will be used if an error is encountered when reading
|
|
* the key from the file in the installation directory.
|
|
* `readFsError`
|
|
* The reason why the installer file system could not be determined. Will
|
|
* be present if `readProvenanceError` and `fileSystem` are not. Possible
|
|
* values are:
|
|
* `openFile`, `getVolInfo`, `fsUnterminated`, `getBufferSize`,
|
|
* `convertString`, `unexpected`, `readIniError`
|
|
* `unexpected` will be used if the value read from the file didn't match
|
|
* any of the expected values, for some reason.
|
|
* `missing` will be used if the file didn't contain `fileSystem` or
|
|
* `readFsError` information.
|
|
* `readIniError` will be used if an error is encountered when reading
|
|
* the key from the file in the installation directory.
|
|
* `readFsErrorCode`
|
|
* An integer returned by `GetLastError()` indicating, in more detail,
|
|
* why we failed to obtain the file system. This key may exist if
|
|
* `readFsError` exists.
|
|
* `readZoneIdError`
|
|
* The reason why the installer was unable to read its zone identifier
|
|
* ADS. Possible values are:
|
|
* `openFile`, `readFile`, `unexpected`, `readIniError`
|
|
* `unexpected` will be used if the value read from the file didn't match
|
|
* any of the expected values, for some reason.
|
|
* `readIniError` will be used if an error is encountered when reading
|
|
* the key from the file in the installation directory.
|
|
* `readZoneIdErrorCode`
|
|
* An integer returned by `GetLastError()` indicating, in more detail,
|
|
* why we failed to read the zone identifier ADS. This key may exist if
|
|
* `readZoneIdError` exists.
|
|
* `zoneIdFileSize`
|
|
* This key should exist if Firefox successfully read the file in the
|
|
* installation directory and the installer successfully opened the ADS.
|
|
* If the installer failed to get the size of the ADS prior to reading
|
|
* it, this will be `unknown`. If the installer was able to get the ADS
|
|
* size, this will be an integer describing how many bytes long it was.
|
|
* If this value in installation directory's file isn't `unknown` or an
|
|
* integer, this will be `unexpected`. If an error is encountered when
|
|
* reading the key from the file in the installation directory, this will
|
|
* be `readIniError`.
|
|
* `zoneIdBufferLargeEnough`
|
|
* This key should exist if Firefox successfully read the file in the
|
|
* installation directory and the installer successfully opened the ADS.
|
|
* Indicates whether the zone identifier ADS size was bigger than the
|
|
* maximum size that the installer will read from it. If we failed to
|
|
* determine the ADS size, this will be `unknown`. If the installation
|
|
* directory's file contains an invalid value, this will be `unexpected`.
|
|
* If an error is encountered when reading the key from the file in the
|
|
* installation directory, this will be `readIniError`.
|
|
* Otherwise, this will be a boolean indicating whether or not the buffer
|
|
* was large enough to fit the ADS data into.
|
|
* `zoneIdTruncated`
|
|
* This key should exist if Firefox successfully read the file in the
|
|
* installation directory and the installer successfully read the ADS.
|
|
* Indicates whether or not we read through the end of the ADS data when
|
|
* we copied it. If the installer failed to determine this, this value
|
|
* will be `unknown`. If the installation directory's file contains an
|
|
* invalid value, this will be `unexpected`. If an error is encountered
|
|
* when reading the key from the file in the installation directory, this
|
|
* will be `readIniError`. Otherwise, this will be a boolean value
|
|
* indicating whether or not the data that we copied was truncated.
|
|
* `zoneId`
|
|
* The Security Zone that the Zone Identifier data indicates that
|
|
* installer was downloaded from. See this documentation:
|
|
* https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/ms537183(v=vs.85)
|
|
* This key will be present if `readProvenanceError` and
|
|
* `readZoneIdError` are not. It will either be a valid zone ID (an
|
|
* integer between 0 and 4, inclusive), or else it will be `unexpected`,
|
|
* `missing`, or `readIniError`.
|
|
* `referrerUrl`
|
|
* The URL of the download referrer. This key will be present if
|
|
* `readProvenanceError` and `readZoneIdError` are not. It will either be
|
|
* a `URL` object, or else it will be `unexpected`, `missing`, or
|
|
* `readIniError`.
|
|
* `referrerUrlIsMozilla`
|
|
* This key will be present if `ReferrerUrl` is a `URL` object. It will
|
|
* be `true`` if the download referrer appears to be a Mozilla URL.
|
|
* Otherwise it will be `false`.
|
|
* `hostUrl`
|
|
* The URL of the download source. This key will be present if
|
|
* `readProvenanceError` and `readZoneIdError` are not. It will either be
|
|
* a `URL` object, or else it will be `unexpected`, `missing`, or
|
|
* `readIniError`
|
|
* `hostUrlIsMozilla`
|
|
* This key will be present if `HostUrl` is a `URL` object. It will
|
|
* be `true`` if the download source appears to be a Mozilla URL.
|
|
* Otherwise it will be `false`.
|
|
*/
|
|
async readZoneIdProvenanceFile() {
|
|
if (gReadZoneIdPromise) {
|
|
return gReadZoneIdPromise;
|
|
}
|
|
gReadZoneIdPromise = (async () => {
|
|
let file = this.zoneIdProvenanceFile;
|
|
if (!file) {
|
|
return null;
|
|
}
|
|
let iniData;
|
|
try {
|
|
iniData = await lazy.AttributionIOUtils.readUTF8(file.path);
|
|
} catch (ex) {
|
|
if (DOMException.isInstance(ex) && ex.name == "NotFoundError") {
|
|
return { readProvenanceError: "noSuchFile" };
|
|
}
|
|
return { readProvenanceError: "readError" };
|
|
}
|
|
|
|
let ini;
|
|
try {
|
|
// We would rather use asynchronous I/O, so we are going to read the
|
|
// file with IOUtils and then pass the result into the INI parser
|
|
// rather than just giving the INI parser factory the file.
|
|
ini = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]
|
|
.getService(Ci.nsIINIParserFactory)
|
|
.createINIParser(null);
|
|
ini.initFromString(iniData);
|
|
} catch (ex) {
|
|
return { readProvenanceError: "parseError" };
|
|
}
|
|
|
|
const unexpectedValueError = "unexpected";
|
|
const missingKeyError = "missing";
|
|
const readIniError = "readIniError";
|
|
const possibleIniErrors = [missingKeyError, readIniError];
|
|
|
|
// Format is {"IniSectionName": {"IniKeyName": "IniValue"}}
|
|
let iniValues = {
|
|
Mozilla: {
|
|
fileSystem: null,
|
|
readFsError: null,
|
|
readFsErrorCode: null,
|
|
readZoneIdError: null,
|
|
readZoneIdErrorCode: null,
|
|
zoneIdFileSize: null,
|
|
zoneIdBufferLargeEnough: null,
|
|
zoneIdTruncated: null,
|
|
},
|
|
ZoneTransfer: {
|
|
ZoneId: null,
|
|
ReferrerUrl: null,
|
|
HostUrl: null,
|
|
},
|
|
};
|
|
|
|
// The ini reader interface is a little weird in that if we just try to
|
|
// read a value from a known section/key and the section/key doesn't
|
|
// exist, we just get a generic error rather than an indication that
|
|
// the section/key doesn't exist. To distinguish missing keys from any
|
|
// other potential errors, we are going to enumerate the sections and
|
|
// keys use that to determine if we should try to read from them.
|
|
let existingSections;
|
|
try {
|
|
existingSections = Array.from(ini.getSections());
|
|
} catch (ex) {
|
|
return { readProvenanceError: "parseError" };
|
|
}
|
|
for (const section in iniValues) {
|
|
if (!existingSections.includes(section)) {
|
|
for (const key in iniValues[section]) {
|
|
iniValues[section][key] = missingKeyError;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let existingKeys;
|
|
try {
|
|
existingKeys = Array.from(ini.getKeys(section));
|
|
} catch (ex) {
|
|
for (const key in iniValues[section]) {
|
|
iniValues[section][key] = readIniError;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
for (const key in iniValues[section]) {
|
|
if (!existingKeys.includes(key)) {
|
|
iniValues[section][key] = missingKeyError;
|
|
continue;
|
|
}
|
|
|
|
let value;
|
|
try {
|
|
value = ini.getString(section, key).trim();
|
|
} catch (ex) {
|
|
value = readIniError;
|
|
}
|
|
iniValues[section][key] = value;
|
|
}
|
|
}
|
|
|
|
// This helps with how verbose the validation gets.
|
|
const fileSystem = iniValues.Mozilla.fileSystem;
|
|
const readFsError = iniValues.Mozilla.readFsError;
|
|
const readFsErrorCode = iniValues.Mozilla.readFsErrorCode;
|
|
const readZoneIdError = iniValues.Mozilla.readZoneIdError;
|
|
const readZoneIdErrorCode = iniValues.Mozilla.readZoneIdErrorCode;
|
|
const zoneIdFileSize = iniValues.Mozilla.zoneIdFileSize;
|
|
const zoneIdBufferLargeEnough = iniValues.Mozilla.zoneIdBufferLargeEnough;
|
|
const zoneIdTruncated = iniValues.Mozilla.zoneIdTruncated;
|
|
const zoneId = iniValues.ZoneTransfer.ZoneId;
|
|
const referrerUrl = iniValues.ZoneTransfer.ReferrerUrl;
|
|
const hostUrl = iniValues.ZoneTransfer.HostUrl;
|
|
|
|
let returnObject = {};
|
|
|
|
// readFsError, readFsErrorCode, fileSystem
|
|
const validReadFsErrors = [
|
|
"openFile",
|
|
"getVolInfo",
|
|
"fsUnterminated",
|
|
"getBufferSize",
|
|
"convertString",
|
|
];
|
|
// These must be upper case
|
|
const validFileSystemValues = ["NTFS", "FAT32"];
|
|
if (fileSystem == missingKeyError && readFsError != missingKeyError) {
|
|
if (
|
|
possibleIniErrors.includes(readFsError) ||
|
|
validReadFsErrors.includes(readFsError)
|
|
) {
|
|
returnObject.readFsError = readFsError;
|
|
} else {
|
|
returnObject.readFsError = unexpectedValueError;
|
|
}
|
|
if (readFsErrorCode != missingKeyError) {
|
|
let code = parseInt(readFsErrorCode, 10);
|
|
if (!isNaN(code)) {
|
|
returnObject.readFsErrorCode = code;
|
|
}
|
|
}
|
|
} else if (possibleIniErrors.includes(fileSystem)) {
|
|
returnObject.fileSystem = fileSystem;
|
|
} else if (validFileSystemValues.includes(fileSystem.toUpperCase())) {
|
|
returnObject.fileSystem = fileSystem.toUpperCase();
|
|
} else {
|
|
returnObject.fileSystem = "other";
|
|
}
|
|
|
|
// zoneIdFileSize
|
|
if (zoneIdFileSize == missingKeyError) {
|
|
// We don't include this one if it's missing.
|
|
} else if (
|
|
zoneIdFileSize == readIniError ||
|
|
zoneIdFileSize == "unknown"
|
|
) {
|
|
returnObject.zoneIdFileSize = zoneIdFileSize;
|
|
} else {
|
|
let size = parseInt(zoneIdFileSize, 10);
|
|
if (isNaN(size)) {
|
|
returnObject.zoneIdFileSize = unexpectedValueError;
|
|
} else {
|
|
returnObject.zoneIdFileSize = size;
|
|
}
|
|
}
|
|
|
|
// zoneIdBufferLargeEnough
|
|
if (zoneIdBufferLargeEnough == missingKeyError) {
|
|
// We don't include this one if it's missing.
|
|
} else if (
|
|
zoneIdBufferLargeEnough == readIniError ||
|
|
zoneIdBufferLargeEnough == "unknown"
|
|
) {
|
|
returnObject.zoneIdBufferLargeEnough = zoneIdBufferLargeEnough;
|
|
} else if (zoneIdBufferLargeEnough.toLowerCase() == "true") {
|
|
returnObject.zoneIdBufferLargeEnough = true;
|
|
} else if (zoneIdBufferLargeEnough.toLowerCase() == "false") {
|
|
returnObject.zoneIdBufferLargeEnough = false;
|
|
} else {
|
|
returnObject.zoneIdBufferLargeEnough = unexpectedValueError;
|
|
}
|
|
|
|
// zoneIdTruncated
|
|
if (zoneIdTruncated == missingKeyError) {
|
|
// We don't include this one if it's missing.
|
|
} else if (
|
|
zoneIdTruncated == readIniError ||
|
|
zoneIdTruncated == "unknown"
|
|
) {
|
|
returnObject.zoneIdTruncated = zoneIdTruncated;
|
|
} else if (zoneIdTruncated.toLowerCase() == "true") {
|
|
returnObject.zoneIdTruncated = true;
|
|
} else if (zoneIdTruncated.toLowerCase() == "false") {
|
|
returnObject.zoneIdTruncated = false;
|
|
} else {
|
|
returnObject.zoneIdTruncated = unexpectedValueError;
|
|
}
|
|
|
|
// readZoneIdError, readZoneIdErrorCode, zoneId, referrerUrl, hostUrl,
|
|
// referrerUrlIsMozilla, hostUrlIsMozilla
|
|
const validReadZoneIdErrors = ["openFile", "readFile"];
|
|
if (
|
|
readZoneIdError != missingKeyError &&
|
|
zoneId == missingKeyError &&
|
|
referrerUrl == missingKeyError &&
|
|
hostUrl == missingKeyError
|
|
) {
|
|
if (
|
|
possibleIniErrors.includes(readZoneIdError) ||
|
|
validReadZoneIdErrors.includes(readZoneIdError)
|
|
) {
|
|
returnObject.readZoneIdError = readZoneIdError;
|
|
} else {
|
|
returnObject.readZoneIdError = unexpectedValueError;
|
|
}
|
|
if (readZoneIdErrorCode != missingKeyError) {
|
|
let code = parseInt(readZoneIdErrorCode, 10);
|
|
if (!isNaN(code)) {
|
|
returnObject.readZoneIdErrorCode = code;
|
|
}
|
|
}
|
|
} else {
|
|
if (possibleIniErrors.includes(zoneId)) {
|
|
returnObject.zoneId = zoneId;
|
|
} else {
|
|
let id = parseInt(zoneId, 10);
|
|
if (isNaN(id) || id < 0 || id > 4) {
|
|
returnObject.zoneId = unexpectedValueError;
|
|
} else {
|
|
returnObject.zoneId = id;
|
|
}
|
|
}
|
|
|
|
let isMozillaURL = url => {
|
|
const mozillaDomains = ["mozilla.com", "mozilla.net", "mozilla.org"];
|
|
for (const domain of mozillaDomains) {
|
|
if (url.hostname == domain) {
|
|
return true;
|
|
}
|
|
if (url.hostname.endsWith("." + domain)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
if (possibleIniErrors.includes(referrerUrl)) {
|
|
returnObject.referrerUrl = referrerUrl;
|
|
} else {
|
|
try {
|
|
returnObject.referrerUrl = new URL(referrerUrl);
|
|
} catch (ex) {
|
|
returnObject.referrerUrl = unexpectedValueError;
|
|
}
|
|
if (URL.isInstance(returnObject.referrerUrl)) {
|
|
returnObject.referrerUrlIsMozilla = isMozillaURL(
|
|
returnObject.referrerUrl
|
|
);
|
|
}
|
|
}
|
|
|
|
if (possibleIniErrors.includes(hostUrl)) {
|
|
returnObject.hostUrl = hostUrl;
|
|
} else {
|
|
try {
|
|
returnObject.hostUrl = new URL(hostUrl);
|
|
} catch (ex) {
|
|
returnObject.hostUrl = unexpectedValueError;
|
|
}
|
|
if (URL.isInstance(returnObject.hostUrl)) {
|
|
returnObject.hostUrlIsMozilla = isMozillaURL(returnObject.hostUrl);
|
|
}
|
|
}
|
|
}
|
|
|
|
return returnObject;
|
|
})();
|
|
return gReadZoneIdPromise;
|
|
},
|
|
|
|
/**
|
|
* Only submits telemetry once, no matter how many times it is called.
|
|
* Has no effect on OSs where provenance data is not supported.
|
|
*/
|
|
async submitProvenanceTelemetry() {
|
|
if (gTelemetryPromise) {
|
|
return gTelemetryPromise;
|
|
}
|
|
gTelemetryPromise = (async () => {
|
|
const errorValue = "error";
|
|
let provenance = await this.readZoneIdProvenanceFile();
|
|
if (!provenance) {
|
|
return;
|
|
}
|
|
|
|
Services.telemetry.scalarSet(
|
|
"attribution.provenance.data_exists",
|
|
!provenance.readProvenanceError
|
|
);
|
|
if (provenance.readProvenanceError) {
|
|
return;
|
|
}
|
|
|
|
Services.telemetry.scalarSet(
|
|
"attribution.provenance.file_system",
|
|
provenance.fileSystem ?? errorValue
|
|
);
|
|
|
|
// https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-#ERROR_FILE_NOT_FOUND
|
|
const ERROR_FILE_NOT_FOUND = 2;
|
|
|
|
let ads_exists =
|
|
!provenance.readProvenanceError &&
|
|
!(
|
|
provenance.readZoneIdError == "openFile" &&
|
|
provenance.readZoneIdErrorCode == ERROR_FILE_NOT_FOUND
|
|
);
|
|
Services.telemetry.scalarSet(
|
|
"attribution.provenance.ads_exists",
|
|
ads_exists
|
|
);
|
|
if (!ads_exists) {
|
|
return;
|
|
}
|
|
|
|
Services.telemetry.scalarSet(
|
|
"attribution.provenance.security_zone",
|
|
"zoneId" in provenance ? provenance.zoneId.toString() : errorValue
|
|
);
|
|
|
|
let haveReferrerUrl = URL.isInstance(provenance.referrerUrl);
|
|
Services.telemetry.scalarSet(
|
|
"attribution.provenance.referrer_url_exists",
|
|
haveReferrerUrl
|
|
);
|
|
if (haveReferrerUrl) {
|
|
Services.telemetry.scalarSet(
|
|
"attribution.provenance.referrer_url_is_mozilla",
|
|
provenance.referrerUrlIsMozilla
|
|
);
|
|
}
|
|
|
|
let haveHostUrl = URL.isInstance(provenance.hostUrl);
|
|
Services.telemetry.scalarSet(
|
|
"attribution.provenance.host_url_exists",
|
|
haveHostUrl
|
|
);
|
|
if (haveHostUrl) {
|
|
Services.telemetry.scalarSet(
|
|
"attribution.provenance.host_url_is_mozilla",
|
|
provenance.hostUrlIsMozilla
|
|
);
|
|
}
|
|
})();
|
|
return gTelemetryPromise;
|
|
},
|
|
};
|