Bug 1243603 - Telemetry for when Windows invokes Firefox to handle a registered file type or protocol. r=Gijs

The `-osint` flag is used as the signal that Windows is invoking
Firefox to handle a file type or protocol.  The `-osint` flag was
introduced in order to mitigate security breaches due to poor argument
quoting (by consumers invoking Firefox); to use it for this new
purpose, it must be preserved for downstream consumers to react to.
Alternately, some marker of the flag could be maintained.  Since the
flag needs to transit through the launcher process, I've elected to
simply not strip it as we validate command lines, and to accommodate
it further downstream.  (It looks like Thunderbird already
accommodates `-osint`: see
https://searchfox.org/comm-central/rev/3e8f926de9ea09945b237177eb6d489c70318f0e/mail/components/MessengerContentHandler.jsm#568.)

The telemetry in this patch achieves two purposes.  The first is to
count the number of times Firefox is invoked to handle a registered
file type or protocol: for this, a new keyed uint scalar was added.
File types start with a ".", just like on Windows; protocols
(equivalently, the schemes used to identify them) do not start with a
".".

The second is to identify times when Firefox is launched (i.e., it was
not already running) to handle a registered file type or protocol.

This generalizes the existing `os.environment.launch_method`,
introducing `os.environment.launched_to_handle` and
`os.environment.invoked_to_handle` string scalars, which record the
file type or protocol.

The command line state `STATE_INITIAL_LAUNCH` is used to discriminate
launching from invoking.

Differential Revision: https://phabricator.services.mozilla.com/D132288
This commit is contained in:
Nick Alexander 2021-12-11 00:00:55 +00:00
Родитель 7957290efd
Коммит 969b4ce42e
7 изменённых файлов: 335 добавлений и 4 удалений

Просмотреть файл

@ -913,6 +913,53 @@ function handURIToExistingBrowser(
);
}
/**
* If given URI is a file type or a protocol, record telemetry that
* Firefox was invoked or launched (if `isLaunch` is truth-y). If the
* file type or protocol is not registered by default, record it as
* ".<other extension>" or "<other protocol>".
*
* @param uri
* The URI Firefox was asked to handle.
* @param isLaunch
* truth-y if Firefox was launched/started rather than running and invoked.
*/
function maybeRecordToHandleTelemetry(uri, isLaunch) {
let scalar = isLaunch
? "os.environment.launched_to_handle"
: "os.environment.invoked_to_handle";
if (uri instanceof Ci.nsIFileURL) {
let extension = "." + uri.fileExtension.toLowerCase();
// Keep synchronized with https://searchfox.org/mozilla-central/source/browser/installer/windows/nsis/shared.nsh
// and https://searchfox.org/mozilla-central/source/browser/installer/windows/msix/AppxManifest.xml.in.
let registeredExtensions = new Set([
".avif",
".htm",
".html",
".pdf",
".shtml",
".xht",
".xhtml",
".svg",
".webp",
]);
if (registeredExtensions.has(extension)) {
Services.telemetry.keyedScalarAdd(scalar, extension, 1);
} else {
Services.telemetry.keyedScalarAdd(scalar, ".<other extension>", 1);
}
} else if (uri) {
let scheme = uri.scheme.toLowerCase();
let registeredSchemes = new Set(["about", "http", "https", "mailto"]);
if (registeredSchemes.has(scheme)) {
Services.telemetry.keyedScalarAdd(scalar, scheme, 1);
} else {
Services.telemetry.keyedScalarAdd(scalar, "<other protocol>", 1);
}
}
}
function nsDefaultCommandLineHandler() {}
nsDefaultCommandLineHandler.prototype = {
@ -960,11 +1007,32 @@ nsDefaultCommandLineHandler.prototype = {
}
}
// `-osint` and handling registered file types and protocols is Windows-only.
let launchedWithArg_osint =
AppConstants.platform == "win" && cmdLine.findFlag("osint", false) == 0;
if (launchedWithArg_osint) {
cmdLine.handleFlag("osint", false);
}
try {
var ar;
while ((ar = cmdLine.handleFlagWithParam("url", false))) {
var uri = resolveURIInternal(cmdLine, ar);
urilist.push(uri);
if (launchedWithArg_osint) {
launchedWithArg_osint = false;
// We use the resolved URI here, even though it can produce
// surprising results where-by `-osint -url test.pdf` resolves to
// a query with search parameter "test.pdf". But that shouldn't
// happen when Firefox is launched by Windows itself: files should
// exist and be resolved to file URLs.
const isLaunch =
cmdLine && cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH;
maybeRecordToHandleTelemetry(uri, isLaunch);
}
}
} catch (e) {
Cu.reportError(e);

Просмотреть файл

@ -1,6 +1,7 @@
[DEFAULT]
support-files =
head.js
../../../../dom/security/test/csp/dummy.pdf
[browser_browserGlue_telemetry.js]
[browser_browserGlue_upgradeDialog.js]
@ -13,6 +14,8 @@ reason = test depends on update channel
[browser_default_browser_prompt.js]
[browser_initial_tab_remoteType.js]
https_first_disabled = true
[browser_to_handle_telemetry.js]
run-if = os == 'win'
[browser_quit_multiple_tabs.js]
[browser_startup_homepage.js]
[browser_quit_disabled.js]

Просмотреть файл

@ -0,0 +1,199 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
async function handleCommandLine(args, state) {
let newWinPromise;
let target = args[args.length - 1];
const EXISTING_FILE = Cc["@mozilla.org/file/local;1"].createInstance(
Ci.nsIFile
);
EXISTING_FILE.initWithPath(getTestFilePath("dummy.pdf"));
if (!target.includes("://")) {
// For simplicity, we handle only absolute paths. We could resolve relative
// paths, but that would itself require the functionality of the
// `nsICommandLine` instance we produce using this input.
const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
file.initWithPath(target);
target = Services.io.newFileURI(file).spec;
}
if (state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
newWinPromise = BrowserTestUtils.waitForNewWindow({
url: target, // N.b.: trailing slashes matter when matching.
});
}
let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService(
Ci.nsICommandLineHandler
);
let fakeCmdLine = Cu.createCommandLine(args, EXISTING_FILE.parent, state);
cmdLineHandler.handle(fakeCmdLine);
if (newWinPromise) {
let newWin = await newWinPromise;
await BrowserTestUtils.closeWindow(newWin);
} else {
BrowserTestUtils.removeTab(gBrowser.selectedTab);
}
}
function assertToHandleTelemetry(assertions) {
const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
const { invoked, launched, ...unknown } = assertions;
if (Object.keys(unknown).length) {
throw Error(
`Unknown keys given to assertToHandleTelemetry: ${JSON.stringify(
unknown
)}`
);
}
if (invoked === undefined && launched === undefined) {
throw Error("No known keys given to assertToHandleTelemetry");
}
for (let scalar of ["invoked", "launched"]) {
if (scalar in assertions) {
const { handled, not_handled } = assertions[scalar] || {};
if (handled) {
TelemetryTestUtils.assertKeyedScalar(
scalars,
`os.environment.${scalar}_to_handle`,
handled,
1,
`${scalar} to handle '${handled}' 1 times`
);
// Intentionally nested.
if (not_handled) {
Assert.equal(
not_handled in scalars[`os.environment.${scalar}_to_handle`],
false,
`${scalar} to handle '${not_handled}' 0 times`
);
}
} else {
TelemetryTestUtils.assertScalarUnset(
scalars,
`os.environment.${scalar}_to_handle`
);
if (not_handled) {
throw new Error(
`In ${scalar}, 'not_handled' is only valid with 'handled'`
);
}
}
}
}
}
add_task(async function test_invoked_to_handle_registered_file_type() {
await handleCommandLine(
[
"-osint",
"-url",
getTestFilePath("../../../../dom/security/test/csp/dummy.pdf"),
],
Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
);
assertToHandleTelemetry({
invoked: { handled: ".pdf", not_handled: ".html" },
launched: null,
});
});
add_task(async function test_invoked_to_handle_unregistered_file_type() {
await handleCommandLine(
["-osint", "-url", getTestFilePath("browser.ini")],
Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
);
assertToHandleTelemetry({
invoked: { handled: ".<other extension>", not_handled: ".ini" },
launched: null,
});
});
add_task(async function test_invoked_to_handle_registered_protocol() {
await handleCommandLine(
["-osint", "-url", "https://example.com/"],
Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
);
assertToHandleTelemetry({
invoked: { handled: "https", not_handled: "mailto" },
launched: null,
});
});
add_task(async function test_invoked_to_handle_unregistered_protocol() {
// Truly unknown protocols get "URI fixed up" to search provider queries.
// `ftp` does not get fixed up.
await handleCommandLine(
["-osint", "-url", "ftp://example.com/"],
Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
);
assertToHandleTelemetry({
invoked: { handled: "<other protocol>", not_handled: "ftp" },
launched: null,
});
});
add_task(async function test_launched_to_handle_registered_protocol() {
await handleCommandLine(
["-osint", "-url", "https://example.com/"],
Ci.nsICommandLine.STATE_INITIAL_LAUNCH
);
assertToHandleTelemetry({
invoked: null,
launched: { handled: "https", not_handled: "mailto" },
});
});
add_task(async function test_launched_to_handle_registered_file_type() {
await handleCommandLine(
[
"-osint",
"-url",
getTestFilePath("../../../../dom/security/test/csp/dummy.pdf"),
],
Ci.nsICommandLine.STATE_INITIAL_LAUNCH
);
assertToHandleTelemetry({
invoked: null,
launched: { handled: ".pdf", not_handled: ".html" },
});
});
add_task(async function test_invoked_no_osint() {
await handleCommandLine(
["-url", "https://example.com/"],
Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
);
assertToHandleTelemetry({
invoked: null,
launched: null,
});
});
add_task(async function test_launched_no_osint() {
await handleCommandLine(
["-url", "https://example.com/"],
Ci.nsICommandLine.STATE_INITIAL_LAUNCH
);
assertToHandleTelemetry({
invoked: null,
launched: null,
});
});

Просмотреть файл

@ -32,6 +32,10 @@
<uap3:Extension Category="windows.fileTypeAssociation">
<uap3:FileTypeAssociation Name="htm">
<uap:SupportedFileTypes>
<!-- Keep synchronized with
https://searchfox.org/mozilla-central/source/browser/installer/windows/nsis/shared.nsh
and `os.environment.launched_to_handle` and `os.environment.invoked_to_handle` telemetry in
https://searchfox.org/mozilla-central/source/browser/components/BrowserContentHandler.jsm. -->
<uap:FileType>.avif</uap:FileType>
<uap:FileType>.htm</uap:FileType>
<uap:FileType>.html</uap:FileType>

Просмотреть файл

@ -505,6 +505,10 @@ ${RemoveDefaultBrowserAgentShortcut}
${EndIf}
; Keep this list synchronized with
; https://searchfox.org/mozilla-central/source/browser/installer/windows/msix/AppxManifest.xml.in.
; and `os.environment.launched_to_handle` and `os.environment.invoked_to_handle` telemetry in
; https://searchfox.org/mozilla-central/source/browser/components/BrowserContentHandler.jsm.
${AddAssociationIfNoneExist} ".pdf" "FirefoxHTML$5"
${AddAssociationIfNoneExist} ".oga" "FirefoxHTML$5"
${AddAssociationIfNoneExist} ".ogg" "FirefoxHTML$5"

Просмотреть файл

@ -1867,6 +1867,61 @@ os.environment:
operating_systems:
- windows
launched_to_handle:
bug_numbers:
- 1243603
description: >
Records counts for when Firefox was launched afresh (i.e., was not already
running) to handle a file type or protocol with `-osint -url ...`. The
result is split into keys which represent the file extension: currently,
the set of file types Firefox registers to handle, namely ".avif", ".htm",
".html", ".pdf", ".shtml", ".xht", ".xhtml", ".svg", ".webp", and the set
of protocol schemes that Firefox registers to handle, namely "about",
"http", "https", "mailto". If Firefox was launched to handle a file type
or protocol it does not register to handle by default, the count is
recorded as ".<other extension>" or "<other protocol>", respectively
(neither of which are valid extension or protocol identifiers).
keyed: true
expires: "106"
kind: uint
notification_emails:
- application-update-telemetry-alerts@mozilla.com
release_channel_collection: opt-out
products:
- firefox
record_in_processes:
- main
operating_systems:
- windows
invoked_to_handle:
bug_numbers:
- 1243603
description: >
Records counts for when Firefox was invoked (i.e., was already running and
was not launched) to handle a file type or protocol with `-osint -url
...`. The result is split into keys which represent the file extension:
currently, the set of file types Firefox registers to handle, namely
".avif", ".htm", ".html", ".pdf", ".shtml", ".xht", ".xhtml", ".svg",
".webp", and the set of protocol schemes that Firefox registers to handle,
namely "about", "http", "https", "mailto". If Firefox was invoked to
handle a file type or protocol it does not register to handle by default,
the count is recorded as ".<other extension>" or "<other protocol>",
respectively (neither of which are valid extension or protocol
identifiers).
keyed: true
expires: "106"
kind: uint
notification_emails:
- application-update-telemetry-alerts@mozilla.com
release_channel_collection: opt-out
products:
- firefox
record_in_processes:
- main
operating_systems:
- windows
is_kept_in_dock:
bug_numbers:
- 1715348

Просмотреть файл

@ -228,11 +228,9 @@ inline void EnsureCommandlineSafe(int& aArgc, CharT** aArgv,
if (!strimatch(osintLit, arg)) {
exit(127);
}
// Strip it:
RemoveArg(aArgc, aArgv + 1);
// Now only an acceptable argument and a parameter for it should be left:
arg = aArgv[1];
arg = aArgv[2];
if (*arg != '-'
#ifdef XP_WIN
&& *arg != '/'
@ -257,7 +255,7 @@ inline void EnsureCommandlineSafe(int& aArgc, CharT** aArgv,
exit(127);
}
// The param that is passed afterwards shouldn't be another switch:
arg = aArgv[2];
arg = aArgv[3];
if (*arg == '-'
#ifdef XP_WIN
|| *arg == '/'