Bug 1602463 Part 3 - Windows default browser agent. r=agashlin,bytesized,nalexander

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Molly Howell 2020-03-16 15:11:17 +00:00
Родитель 0ad0bf91cd
Коммит 3964971fc6
20 изменённых файлов: 1351 добавлений и 0 удалений

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

@ -13,6 +13,7 @@ This is the nascent documentation of the Firefox front-end code.
extensions/formautofill/docs/index
components/newtab/docs/index
installer/windows/installer/index
/toolkit/mozapps/defaultagent/default-browser-agent/index
components/newtab/content-src/asrouter/docs/index
base/sslerrorreport/index
base/tabbrowser/index

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

@ -487,3 +487,7 @@ i686/gmp-clearkey/0.1/clearkey.dll
; build, which, practically speaking, is the case.
@BINPATH@/gmp-clearkey/0.1/manifest.json
#endif
#if defined(XP_WIN)
@BINPATH@/default-browser-agent@BIN_SUFFIX@
#endif

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

@ -0,0 +1,57 @@
======================
"default-browser" ping
======================
This opt-out ping is sent from the Default Browser Agent, which is a Windows-only program that registers itself during Firefox installation with the Windows scheduled tasks system so that it runs automatically every 24 hours, whether Firefox is running or not. The scheduled task gathers the data for this ping and then sends it by handing it off to :doc:`../internals/pingsender`.
Even though this ping is generated by a binary separate from Firefox itself, opting out of telemetry does disable it; the pref value is copied to the registry so that the default browser agent can read it without needing to work with profiles. Relevant policies are consulted as well. The agent also has its own pref, ``default-agent.enabled``, which if set to false disables all agent functionality, including generating this ping.
Each installation of Firefox has its own copy of the agent and its own scheduled task, so one ping will be sent every day for each installation on a given machine. This is needed because the default browser setting is per-user, and different installations may have been created by different users. If multiple operating system-level users are all using one copy of Firefox, only one scheduled task will have been created and only one ping will be sent, even though the users might have different default browser settings.
The namespace this ping is in is called ``default-browser-agent``.
For more information about the default browser agent itself, see :doc:`its documentation </toolkit/mozapps/defaultagent/default-browser-agent/index>`.
Structure
=========
Since this ping is sent from an external binary, it's structured as its own ping document type and not in the standard Firefox telemetry format. It's also missing lots of data that would normally be present; for instance, there is no ``clientId``, because the agent does not load any profile and so has no way to find any, and no environment block because the agent doesn't contain the telemetry library code to build it.
Here's the format of the ping data, with example values for each property:
.. code-block:: js
{
build_channel: <string>, // ex. "nightly", or "beta", or "release"
version: <string>, // ex. "72.0.2"
os_version: <string>, // ex. 10.0.18363.592
os_locale: <string>, // ex. en-US
default_browser: <string>, // ex. "firefox"
previous_default_browser: <string> // ex. "edge"
}
``build_channel``
-----------------
The Firefox channel.
``version``
-----------
The Firefox version.
``os_version``
--------------
The Windows version number. Below Windows 10, this is in the format ``[major].[minor].[build]``; for Windows 10, the format is ``10.0.[build].[UBR]``.
``os_locale``
-------------
The locale that the user has selected for the operating system (NOT for Firefox).
``default_browser``
-------------------
Which browser is currently set as the system default web browser. This is simply a string with the name of the browser; the possible values include "firefox", "chrome", "edge", "edge-chrome", "ie", "opera", and "brave".
``previous_default_browser``
----------------------------
Which browser was set as the system default before it was changed to the current setting. The possible values are the same as for ``default_browser``.
The OS does not keep track of previous default settings, so the agent records this information itself. That means that it will be inaccurate until the first time the default is changed after the agent task begins running. Before then, the value of ``previous_default_browser`` will be the same as ``default_browser``.

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

@ -26,6 +26,9 @@ DIRS += [
'themes',
]
if CONFIG['OS_ARCH'] == 'WINNT' and CONFIG['MOZ_DEFAULT_BROWSER_AGENT']:
DIRS += ['mozapps/defaultagent']
if CONFIG['MOZ_UPDATER'] and CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
DIRS += ['mozapps/update']

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

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="DefaultBrowserAgent"
type="win32"
/>
<description>Default Browser Agent</description>
<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
<ms_asmv3:security>
<ms_asmv3:requestedPrivileges>
<ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" />
</ms_asmv3:requestedPrivileges>
</ms_asmv3:security>
</ms_asmv3:trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
</application>
</compatibility>
<ms_asmv3:application xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
<ms_asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>True/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
</ms_asmv3:windowsSettings>
</ms_asmv3:application>
</assembly>

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

@ -0,0 +1,6 @@
/* 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/. */
#include "winresrc.h"
1 RT_MANIFEST "DefaultBrowserAgent.manifest"

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

@ -0,0 +1,30 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */
#include "EventLog.h"
#include <stdio.h>
void WriteEventLogError(HRESULT hr, const char* sourceFile, int sourceLine) {
HANDLE source = RegisterEventSourceW(
nullptr, L"" MOZ_APP_DISPLAYNAME " Default Browser Agent");
if (!source) {
// Not much we can do about this.
return;
}
// The size of this buffer is arbitrary, but it should easily be enough
// unless we come up with a really excessively long function name.
wchar_t errorStr[MAX_PATH + 1] = L"";
_snwprintf_s(errorStr, MAX_PATH + 1, _TRUNCATE, L"0x%X in %S:%d", hr,
sourceFile, sourceLine);
const wchar_t* stringsArray[] = {errorStr};
ReportEventW(source, EVENTLOG_ERROR_TYPE, 0, hr, nullptr, 1, 0, stringsArray,
nullptr);
DeregisterEventSource(source);
}

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

@ -0,0 +1,16 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */
#ifndef __DEFAULT_BROWSER_AGENT_EVENT_LOG_H__
#define __DEFAULT_BROWSER_AGENT_EVENT_LOG_H__
#include <windows.h>
void WriteEventLogError(HRESULT hr, const char* sourceFile, int sourceLine);
#define LOG_ERROR(hr) WriteEventLogError(hr, __FUNCTION__, __LINE__)
#endif // __DEFAULT_BROWSER_AGENT_EVENT_LOG_H__

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

@ -0,0 +1,13 @@
# 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/.
# This binary should never open a console window in release builds, because
# it's going to run in the background when the user may not expect it, and
# we don't want a console window to just appear out of nowhere on them.
# For debug builds though, it's okay to use the existing MOZ_WINCONSOLE value.
ifndef MOZ_DEBUG
MOZ_WINCONSOLE = 0
endif
include $(topsrcdir)/config/rules.mk

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

@ -0,0 +1,171 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */
#include "Policy.h"
#include <windows.h>
#include <shlwapi.h>
#include <fstream>
#include "common.h"
#include "json/json.h"
#include "mozilla/HelperMacros.h"
#include "mozilla/Maybe.h"
#include "mozilla/WinHeaderOnlyUtils.h"
// There is little logging or error handling in this file, because the file and
// registry values we are reading here are normally absent, so never finding
// anything that we look for at all would not be an error worth generating an
// event log for.
#define AGENT_POLICY_NAME "DisableDefaultBrowserAgent"
#define TELEMETRY_POLICY_NAME "DisableTelemetry"
// The Firefox policy engine hardcodes the string "Mozilla" in its registry
// key accesses rather than using the configured vendor name, so we should do
// the same here to be sure we're compatible with it.
#define POLICY_REGKEY_NAME L"SOFTWARE\\Policies\\Mozilla\\" MOZ_APP_BASENAME
// This enum is the return type for the functions that check policy values.
enum class PolicyState {
Enabled, // There is a policy explicitly set to enabled
Disabled, // There is a policy explicitly set to disabled
NoPolicy, // This policy isn't configured
};
static PolicyState FindPolicyInRegistry(HKEY rootKey,
const wchar_t* policyName) {
HKEY rawRegKey = nullptr;
RegOpenKeyExW(rootKey, POLICY_REGKEY_NAME, 0, KEY_READ, &rawRegKey);
nsAutoRegKey regKey(rawRegKey);
if (!regKey) {
return PolicyState::NoPolicy;
}
// If this key is empty and doesn't have any actual policies in it,
// treat that the same as the key not existing and return no result.
DWORD numSubKeys = 0, numValues = 0;
LSTATUS ls = RegQueryInfoKeyW(regKey.get(), nullptr, nullptr, nullptr,
&numSubKeys, nullptr, nullptr, &numValues,
nullptr, nullptr, nullptr, nullptr);
if (ls != ERROR_SUCCESS) {
return PolicyState::NoPolicy;
}
DWORD policyValue = UINT32_MAX;
DWORD policyValueSize = sizeof(policyValue);
ls = RegGetValueW(regKey.get(), nullptr, policyName, RRF_RT_REG_DWORD,
nullptr, &policyValue, &policyValueSize);
if (ls != ERROR_SUCCESS) {
return PolicyState::NoPolicy;
}
return policyValue == 0 ? PolicyState::Disabled : PolicyState::Enabled;
}
static PolicyState FindPolicyInFile(const char* policyName) {
mozilla::UniquePtr<wchar_t[]> thisBinaryPath = mozilla::GetFullBinaryPath();
if (!PathRemoveFileSpecW(thisBinaryPath.get())) {
return PolicyState::NoPolicy;
}
wchar_t policiesFilePath[MAX_PATH] = L"";
if (!PathCombineW(policiesFilePath, thisBinaryPath.get(), L"distribution")) {
return PolicyState::NoPolicy;
}
if (!PathAppendW(policiesFilePath, L"policies.json")) {
return PolicyState::NoPolicy;
}
// We need a narrow string-based std::ifstream because that's all jsoncpp can
// use; that means we need to supply it the file path as a narrow string.
int policiesFilePathLen = WideCharToMultiByte(
CP_UTF8, 0, policiesFilePath, -1, nullptr, 0, nullptr, nullptr);
if (policiesFilePathLen == 0) {
return PolicyState::NoPolicy;
}
mozilla::UniquePtr<char[]> policiesFilePathA =
mozilla::MakeUnique<char[]>(policiesFilePathLen);
policiesFilePathLen = WideCharToMultiByte(
CP_UTF8, 0, policiesFilePath, -1, policiesFilePathA.get(),
policiesFilePathLen, nullptr, nullptr);
if (policiesFilePathLen == 0) {
return PolicyState::NoPolicy;
}
Json::Value jsonRoot;
std::ifstream stream(policiesFilePathA.get());
Json::Reader().parse(stream, jsonRoot);
if (jsonRoot.isObject() && jsonRoot.isMember("Policies") &&
jsonRoot["Policies"].isObject()) {
if (jsonRoot["Policies"].isMember(policyName) &&
jsonRoot["Policies"][policyName].isBool()) {
return jsonRoot["Policies"][policyName].asBool() ? PolicyState::Enabled
: PolicyState::Disabled;
} else {
return PolicyState::NoPolicy;
}
}
return PolicyState::NoPolicy;
}
static PolicyState IsDisabledByPref(const wchar_t* prefRegValue) {
mozilla::UniquePtr<wchar_t[]> installPath = mozilla::GetFullBinaryPath();
if (!PathRemoveFileSpecW(installPath.get())) {
return PolicyState::NoPolicy;
}
std::wstring telemetryRegistryValueName(installPath.get());
telemetryRegistryValueName.append(L"|");
telemetryRegistryValueName.append(prefRegValue);
DWORD prefValue = 0, dataLen = sizeof(DWORD);
LSTATUS ls = RegGetValueW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME,
telemetryRegistryValueName.c_str(),
RRF_RT_REG_DWORD, nullptr, &prefValue, &dataLen);
if (ls == ERROR_SUCCESS) {
return prefValue == 0 ? PolicyState::Disabled : PolicyState::Enabled;
}
return PolicyState::NoPolicy;
}
// Everything we call from this function wants wide strings, except for jsoncpp,
// which cannot work with them at all, so at some point we need both formats.
// It's awkward to take both formats as individual arguments, but it would be
// more awkward to take one and runtime convert it to the other, or to turn
// this function into a macro so that the preprocessor can trigger the
// conversion for us, so this is what we've got.
static bool IsThingDisabled(const char* thing, const wchar_t* wideThing) {
// The logic here is intended to be the same as that used by Firefox's policy
// engine implementation; they should be kept in sync. We have added the pref
// check at the end though, since that's our own custom mechanism.
PolicyState state = FindPolicyInRegistry(HKEY_LOCAL_MACHINE, wideThing);
if (state == PolicyState::NoPolicy) {
state = FindPolicyInRegistry(HKEY_CURRENT_USER, wideThing);
}
if (state == PolicyState::NoPolicy) {
state = FindPolicyInFile(thing);
}
if (state == PolicyState::NoPolicy) {
state = IsDisabledByPref(wideThing);
}
return state == PolicyState::Enabled ? true : false;
}
bool IsAgentDisabled() {
return IsThingDisabled(AGENT_POLICY_NAME, L"" AGENT_POLICY_NAME);
}
bool IsTelemetryDisabled() {
return IsThingDisabled(TELEMETRY_POLICY_NAME, L"" TELEMETRY_POLICY_NAME);
}

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

@ -0,0 +1,13 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */
#ifndef __DEFAULT_BROWSER_AGENT_POLICY_H__
#define __DEFAULT_BROWSER_AGENT_POLICY_H__
bool IsAgentDisabled();
bool IsTelemetryDisabled();
#endif // __DEFAULT_BROWSER_AGENT_POLICY_H__

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

@ -0,0 +1,291 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */
#include "ScheduledTask.h"
#include <string>
#include <time.h>
#include <taskschd.h>
#include "mozilla/RefPtr.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/WinHeaderOnlyUtils.h"
const wchar_t* kTaskVendor = L"" MOZ_APP_VENDOR;
// kTaskName should have the unique token appended before being used.
const wchar_t* kTaskName = L"" MOZ_APP_DISPLAYNAME " Default Browser Agent ";
// The task scheduler requires its time values to come in the form of a string
// in the format YYYY-MM-DDTHH:MM:SSZ. This format string is used to get that
// out of the C library wcsftime function.
const wchar_t* kTimeFormat = L"%Y-%m-%dT%H:%M:%SZ";
// The expanded time string should always be this length, for example:
// 2020-02-12T16:59:32Z
const size_t kTimeStrMaxLen = 20;
#define ENSURE(x) \
if (FAILED(hr = (x))) { \
LOG_ERROR(hr); \
return hr; \
}
struct SysFreeStringDeleter {
void operator()(BSTR aPtr) { ::SysFreeString(aPtr); }
};
using BStrPtr = mozilla::UniquePtr<OLECHAR, SysFreeStringDeleter>;
HRESULT RegisterTask(const wchar_t* uniqueToken,
BSTR startTime /* = nullptr */) {
// Make sure we don't try to register a task that already exists.
RemoveTask(uniqueToken);
HRESULT hr = S_OK;
RefPtr<ITaskService> scheduler;
ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
IID_ITaskService, getter_AddRefs(scheduler)));
ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
RefPtr<ITaskFolder> rootFolder;
BStrPtr rootFolderBStr = BStrPtr(SysAllocString(L"\\"));
ENSURE(
scheduler->GetFolder(rootFolderBStr.get(), getter_AddRefs(rootFolder)));
RefPtr<ITaskFolder> taskFolder;
BStrPtr vendorBStr = BStrPtr(SysAllocString(kTaskVendor));
if (FAILED(rootFolder->GetFolder(vendorBStr.get(),
getter_AddRefs(taskFolder)))) {
hr = rootFolder->CreateFolder(vendorBStr.get(), VARIANT{},
getter_AddRefs(taskFolder));
// The folder already existing isn't an error.
if (FAILED(hr) && hr != HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) {
LOG_ERROR(hr);
return hr;
}
}
RefPtr<ITaskDefinition> newTask;
ENSURE(scheduler->NewTask(0, getter_AddRefs(newTask)));
RefPtr<ITaskSettings> taskSettings;
ENSURE(newTask->get_Settings(getter_AddRefs(taskSettings)));
ENSURE(taskSettings->put_DisallowStartIfOnBatteries(VARIANT_FALSE));
ENSURE(taskSettings->put_MultipleInstances(TASK_INSTANCES_IGNORE_NEW));
ENSURE(taskSettings->put_StartWhenAvailable(VARIANT_TRUE));
ENSURE(taskSettings->put_StopIfGoingOnBatteries(VARIANT_FALSE));
// This cryptic string means "5 minutes". So, if the task runs for longer
// than that, the process will be killed, because that should never happen.
BStrPtr execTimeLimitBStr = BStrPtr(SysAllocString(L"PT5M"));
ENSURE(taskSettings->put_ExecutionTimeLimit(execTimeLimitBStr.get()));
RefPtr<IRegistrationInfo> regInfo;
ENSURE(newTask->get_RegistrationInfo(getter_AddRefs(regInfo)));
ENSURE(regInfo->put_Author(vendorBStr.get()));
RefPtr<ITriggerCollection> triggers;
ENSURE(newTask->get_Triggers(getter_AddRefs(triggers)));
RefPtr<ITrigger> newTrigger;
ENSURE(triggers->Create(TASK_TRIGGER_DAILY, getter_AddRefs(newTrigger)));
RefPtr<IDailyTrigger> dailyTrigger;
ENSURE(newTrigger->QueryInterface(IID_IDailyTrigger,
getter_AddRefs(dailyTrigger)));
if (startTime) {
ENSURE(dailyTrigger->put_StartBoundary(startTime));
} else {
// The time that the task is scheduled to run at every day is taken from the
// time in the trigger's StartBoundary property. We'll set this to the
// current time, on the theory that the time at which we're being installed
// is a time that the computer is likely to be on other days. If our
// theory is wrong and the computer is offline at the scheduled time, then
// because we've set StartWhenAvailable above, the task will run whenever
// it wakes up. Since our task is entirely in the background and doesn't use
// a lot of resources, we're not concerned about it bothering the user if it
// runs while they're actively using this computer.
time_t now_t = time(nullptr);
// Subtract a minute from the current time, to avoid "winning" a potential
// race with the scheduler that might have it start the task immediately
// after we register it, if we finish doing that and then it evaluates the
// trigger during the same second. We haven't seen this happen in practice,
// but there's no documented guarantee that it won't, so let's be sure.
now_t -= 60;
tm now_tm;
errno_t errno_rv = gmtime_s(&now_tm, &now_t);
if (errno_rv != 0) {
// The C runtime has a (private) function to convert Win32 error codes to
// errno values, but there's nothing that goes the other way, and it
// isn't worth including one here for something that's this unlikely to
// fail anyway. So just return a generic error.
hr = HRESULT_FROM_WIN32(ERROR_INVALID_TIME);
LOG_ERROR(hr);
return hr;
}
mozilla::UniquePtr<wchar_t[]> timeStr =
mozilla::MakeUnique<wchar_t[]>(kTimeStrMaxLen + 1);
if (!timeStr) {
return E_OUTOFMEMORY;
}
if (wcsftime(timeStr.get(), kTimeStrMaxLen + 1, kTimeFormat, &now_tm) ==
0) {
hr = E_NOT_SUFFICIENT_BUFFER;
LOG_ERROR(hr);
return hr;
}
BStrPtr startTimeBStr = BStrPtr(SysAllocString(timeStr.get()));
ENSURE(dailyTrigger->put_StartBoundary(startTimeBStr.get()));
}
ENSURE(dailyTrigger->put_DaysInterval(1));
RefPtr<IActionCollection> actions;
ENSURE(newTask->get_Actions(getter_AddRefs(actions)));
RefPtr<IAction> action;
ENSURE(actions->Create(TASK_ACTION_EXEC, getter_AddRefs(action)));
RefPtr<IExecAction> execAction;
ENSURE(action->QueryInterface(IID_IExecAction, getter_AddRefs(execAction)));
BStrPtr binaryPathBStr =
BStrPtr(SysAllocString(mozilla::GetFullBinaryPath().get()));
ENSURE(execAction->put_Path(binaryPathBStr.get()));
BStrPtr argsBStr = BStrPtr(SysAllocString(L"do-task"));
ENSURE(execAction->put_Arguments(argsBStr.get()));
std::wstring taskName(kTaskName);
taskName += uniqueToken;
BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str()));
RefPtr<IRegisteredTask> registeredTask;
ENSURE(taskFolder->RegisterTaskDefinition(
taskNameBStr.get(), newTask, TASK_CREATE_OR_UPDATE, VARIANT{}, VARIANT{},
TASK_LOGON_INTERACTIVE_TOKEN, VARIANT{}, getter_AddRefs(registeredTask)));
return hr;
}
HRESULT UpdateTask(const wchar_t* uniqueToken) {
RefPtr<ITaskService> scheduler;
HRESULT hr = S_OK;
ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
IID_ITaskService, getter_AddRefs(scheduler)));
ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
RefPtr<ITaskFolder> taskFolder;
BStrPtr folderBStr = BStrPtr(SysAllocString(kTaskVendor));
if (FAILED(
scheduler->GetFolder(folderBStr.get(), getter_AddRefs(taskFolder)))) {
// If our folder doesn't exist, create it and the task.
return RegisterTask(uniqueToken);
}
std::wstring taskName(kTaskName);
taskName += uniqueToken;
BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str()));
RefPtr<IRegisteredTask> task;
if (FAILED(taskFolder->GetTask(taskNameBStr.get(), getter_AddRefs(task)))) {
// If our task doesn't exist at all, just create one.
return RegisterTask(uniqueToken);
}
// If we have a task registered already, we need to recreate it because
// something might have changed that we need to update. But we don't
// want to restart the schedule from now, because that might mean the
// task never runs at all for e.g. Nightly. So create a new task, but
// first get and preserve the existing trigger.
RefPtr<ITaskDefinition> definition;
if (FAILED(task->get_Definition(getter_AddRefs(definition)))) {
// This task is broken, make a new one.
return RegisterTask(uniqueToken);
}
RefPtr<ITriggerCollection> triggerList;
if (FAILED(definition->get_Triggers(getter_AddRefs(triggerList)))) {
// This task is broken, make a new one.
return RegisterTask(uniqueToken);
}
RefPtr<ITrigger> trigger;
if (FAILED(triggerList->get_Item(1, getter_AddRefs(trigger)))) {
// This task is broken, make a new one.
return RegisterTask(uniqueToken);
}
BSTR startTimeBstr;
if (FAILED(trigger->get_StartBoundary(&startTimeBstr))) {
// This task is broken, make a new one.
return RegisterTask(uniqueToken);
}
BStrPtr startTime(startTimeBstr);
return RegisterTask(uniqueToken, startTime.get());
}
HRESULT RemoveTask(const wchar_t* uniqueToken) {
RefPtr<ITaskService> scheduler;
HRESULT hr = S_OK;
ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
IID_ITaskService, getter_AddRefs(scheduler)));
ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
RefPtr<ITaskFolder> taskFolder;
BStrPtr folderBStr = BStrPtr(SysAllocString(kTaskVendor));
hr = scheduler->GetFolder(folderBStr.get(), getter_AddRefs(taskFolder));
if (FAILED(hr)) {
if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) {
// Don't return an error code if our folder doesn't exist,
// because that just means it's been removed already.
return S_OK;
} else {
return hr;
}
}
std::wstring taskName(kTaskName);
taskName += uniqueToken;
BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str()));
hr = taskFolder->DeleteTask(taskNameBStr.get(), 0);
if (FAILED(hr)) {
if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) {
// Failing to delete the task because it didn't exist also isn't fatal.
return S_OK;
} else {
return hr;
}
}
// See if there are any tasks left in our folder, and delete it if not.
RefPtr<IRegisteredTaskCollection> tasksInFolder;
ENSURE(taskFolder->GetTasks(TASK_ENUM_HIDDEN, getter_AddRefs(tasksInFolder)));
LONG numTasks = 0;
ENSURE(tasksInFolder->get_Count(&numTasks));
if (numTasks <= 0) {
RefPtr<ITaskFolder> rootFolder;
BStrPtr rootFolderBStr = BStrPtr(SysAllocString(L"\\"));
ENSURE(
scheduler->GetFolder(rootFolderBStr.get(), getter_AddRefs(rootFolder)));
ENSURE(rootFolder->DeleteFolder(folderBStr.get(), 0));
}
return hr;
}

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

@ -0,0 +1,19 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */
#ifndef __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_H__
#define __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_H__
#include <windows.h>
// uniqueToken should be a string unique to the installation, so that a
// separate task can be created for each installation. Typically this will be
// the install hash string.
HRESULT RegisterTask(const wchar_t* uniqueToken, BSTR startTime = nullptr);
HRESULT UpdateTask(const wchar_t* uniqueToken);
HRESULT RemoveTask(const wchar_t* uniqueToken);
#endif // __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_H__

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

@ -0,0 +1,413 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */
#include "Telemetry.h"
#include <shlobj.h>
#include <shlwapi.h>
#include <fstream>
#include <string>
#include <unordered_map>
#include "common.h"
#include "EventLog.h"
#include "json/json.h"
#include "mozilla/ArrayUtils.h"
#include "mozilla/CmdLineAndEnvUtils.h"
#include "mozilla/HelperMacros.h"
#include "mozilla/RefPtr.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/WinHeaderOnlyUtils.h"
#define TELEMETRY_BASE_URL "https://incoming.telemetry.mozilla.org/submit"
#define TELEMETRY_NAMESPACE "default-browser-agent"
#define TELEMETRY_PING_VERSION "1"
#define TELEMETRY_PING_DOCTYPE "default-browser"
// This is almost the complete URL, just needs a UUID appended.
#define TELEMETRY_PING_URL \
TELEMETRY_BASE_URL "/" TELEMETRY_NAMESPACE "/" TELEMETRY_PING_VERSION \
"/" TELEMETRY_PING_DOCTYPE "/"
#if !defined(RRF_SUBKEY_WOW6464KEY)
# define RRF_SUBKEY_WOW6464KEY 0x00010000
#endif // !defined(RRF_SUBKEY_WOW6464KEY)
using TelemetryFieldResult = mozilla::WindowsErrorResult<std::string>;
using FilePathResult = mozilla::WindowsErrorResult<std::wstring>;
static TelemetryFieldResult GetDefaultBrowser() {
RefPtr<IApplicationAssociationRegistration> pAAR;
HRESULT hr = CoCreateInstance(
CLSID_ApplicationAssociationRegistration, nullptr, CLSCTX_INPROC,
IID_IApplicationAssociationRegistration, getter_AddRefs(pAAR));
if (FAILED(hr)) {
LOG_ERROR(hr);
return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
}
// Whatever is handling the HTTP protocol is effectively the default browser.
wchar_t* rawRegisteredApp;
hr = pAAR->QueryCurrentDefault(L"http", AT_URLPROTOCOL, AL_EFFECTIVE,
&rawRegisteredApp);
if (FAILED(hr)) {
LOG_ERROR(hr);
return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
}
mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter> registeredApp(
rawRegisteredApp);
// This maps a prefix of the AppID string used to register each browser's HTTP
// handler to a custom string that we'll use to identify that browser in our
// telemetry ping (which is this function's return value).
// We're assuming that any UWP app set as the default browser must be Edge.
const std::unordered_map<std::wstring, std::string> AppIDPrefixes = {
{L"Firefox", "firefox"}, {L"Chrome", "chrome"}, {L"AppX", "edge"},
{L"MSEdgeHTM", "edge-chrome"}, {L"IE.", "ie"}, {L"Opera", "opera"},
{L"Brave", "brave"},
};
for (const auto& prefix : AppIDPrefixes) {
if (!wcsnicmp(registeredApp.get(), prefix.first.c_str(),
prefix.first.length())) {
return prefix.second;
}
}
// The default browser is one that we don't know about.
return std::string("");
}
static TelemetryFieldResult GetPreviousDefaultBrowser(
std::string& currentDefault) {
// This function uses two registry values which store the current and the
// previous default browser. If the actual current default browser is
// different from the one we have stored, both values will be updated and the
// new previous default value reported. Otherwise, we'll just report the
// existing previous default value.
// We'll need the currentDefault string in UTF-16 so that we can use it
// in and around the registry.
int currentDefaultLen =
MultiByteToWideChar(CP_UTF8, 0, currentDefault.c_str(), -1, nullptr, 0);
mozilla::UniquePtr<wchar_t[]> wCurrentDefault =
mozilla::MakeUnique<wchar_t[]>(currentDefaultLen);
MultiByteToWideChar(CP_UTF8, 0, currentDefault.c_str(), -1,
wCurrentDefault.get(), currentDefaultLen);
// We don't really need to store these values using names that include the
// install path, because the default browser is a system (per-user) setting,
// but we're doing it anyway as a means of avoiding concurrency issues if
// multiple instances of the task are running at once.
mozilla::UniquePtr<wchar_t[]> installPath = mozilla::GetFullBinaryPath();
if (!PathRemoveFileSpecW(installPath.get())) {
LOG_ERROR(HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER));
return std::string("");
}
std::wstring currentDefaultRegistryValueName(installPath.get());
currentDefaultRegistryValueName.append(L"|CurrentDefault");
std::wstring previousDefaultRegistryValueName(installPath.get());
previousDefaultRegistryValueName.append(L"|PreviousDefault");
// First, read the "current default" value that is already stored in the
// registry, or write a value there if there isn't one.
wchar_t oldCurrentDefault[MAX_PATH + 1] = L"";
DWORD regStrLen = MAX_PATH + 1;
LSTATUS ls =
RegGetValueW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME,
currentDefaultRegistryValueName.c_str(), RRF_RT_REG_SZ,
nullptr, &oldCurrentDefault, &regStrLen);
if (ls != ERROR_SUCCESS) {
RegSetKeyValueW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME,
currentDefaultRegistryValueName.c_str(), REG_SZ,
wCurrentDefault.get(), currentDefaultLen * sizeof(wchar_t));
wcsncpy_s(oldCurrentDefault, MAX_PATH, wCurrentDefault.get(), _TRUNCATE);
}
// Repeat the above for the "previous default" value.
wchar_t oldPreviousDefault[MAX_PATH + 1] = L"";
regStrLen = MAX_PATH + 1;
ls = RegGetValueW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME,
previousDefaultRegistryValueName.c_str(), RRF_RT_REG_SZ,
nullptr, &oldPreviousDefault, &regStrLen);
if (ls != ERROR_SUCCESS) {
RegSetKeyValueW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME,
previousDefaultRegistryValueName.c_str(), REG_SZ,
wCurrentDefault.get(), currentDefaultLen * sizeof(wchar_t));
wcsncpy_s(oldPreviousDefault, MAX_PATH, wCurrentDefault.get(), _TRUNCATE);
}
// Now, see if the two registry values need to be updated because the actual
// default browser setting has changed since we last ran.
std::wstring previousDefault(oldPreviousDefault);
if (wcsnicmp(oldCurrentDefault, wCurrentDefault.get(), currentDefaultLen)) {
previousDefault = oldCurrentDefault;
RegSetKeyValueW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME,
previousDefaultRegistryValueName.c_str(), REG_SZ,
oldCurrentDefault,
(wcslen(oldCurrentDefault) + 1) * sizeof(wchar_t));
RegSetKeyValueW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME,
currentDefaultRegistryValueName.c_str(), REG_SZ,
wCurrentDefault.get(), currentDefaultLen * sizeof(wchar_t));
}
// We need the previous default string in UTF-8 so we can submit it.
int previousDefaultLen = WideCharToMultiByte(
CP_UTF8, 0, previousDefault.c_str(), -1, nullptr, 0, nullptr, nullptr);
mozilla::UniquePtr<char[]> narrowPreviousDefault =
mozilla::MakeUnique<char[]>(previousDefaultLen);
WideCharToMultiByte(CP_UTF8, 0, previousDefault.c_str(), -1,
narrowPreviousDefault.get(), previousDefaultLen, nullptr,
nullptr);
return std::string(narrowPreviousDefault.get());
}
static TelemetryFieldResult GetOSVersion() {
OSVERSIONINFOEXW osv = {sizeof(osv)};
if (::GetVersionExW(reinterpret_cast<OSVERSIONINFOW*>(&osv))) {
std::ostringstream oss;
oss << osv.dwMajorVersion << "." << osv.dwMinorVersion << "."
<< osv.dwBuildNumber;
if (osv.dwMajorVersion == 10 && osv.dwMinorVersion == 0) {
// Get the "Update Build Revision" (UBR) value
DWORD ubrValue;
DWORD ubrValueLen = sizeof(ubrValue);
LSTATUS ubrOk =
::RegGetValueW(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
L"UBR", RRF_RT_DWORD | RRF_SUBKEY_WOW6464KEY, nullptr,
&ubrValue, &ubrValueLen);
if (ubrOk == ERROR_SUCCESS) {
oss << "." << ubrValue;
}
}
return oss.str();
}
HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
LOG_ERROR(hr);
return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
}
static TelemetryFieldResult GetOSLocale() {
wchar_t localeName[LOCALE_NAME_MAX_LENGTH] = L"";
if (!GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH)) {
HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
LOG_ERROR(hr);
return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
}
// We'll need the locale string in UTF-8 to be able to submit it.
int bufLen = WideCharToMultiByte(CP_UTF8, 0, localeName, -1, nullptr, 0,
nullptr, nullptr);
mozilla::UniquePtr<char[]> narrowLocaleName =
mozilla::MakeUnique<char[]>(bufLen);
if (!narrowLocaleName) {
HRESULT hr = HRESULT_FROM_WIN32(ERROR_OUTOFMEMORY);
LOG_ERROR(hr);
return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
}
WideCharToMultiByte(CP_UTF8, 0, localeName, -1, narrowLocaleName.get(),
bufLen, nullptr, nullptr);
return std::string(narrowLocaleName.get());
}
static FilePathResult GenerateUUIDStr() {
UUID uuid;
RPC_STATUS status = UuidCreate(&uuid);
if (status != RPC_S_OK) {
HRESULT hr = MAKE_HRESULT(1, FACILITY_RPC, status);
LOG_ERROR(hr);
return FilePathResult(mozilla::WindowsError::FromHResult(hr));
}
// 39 == length of a UUID string including braces and NUL.
wchar_t guidBuf[39] = {};
if (StringFromGUID2(uuid, guidBuf, 39) != 39) {
LOG_ERROR(HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER));
return FilePathResult(
mozilla::WindowsError::FromWin32Error(ERROR_INSUFFICIENT_BUFFER));
}
// Remove the curly braces.
return std::wstring(guidBuf + 1, guidBuf + 37);
}
static FilePathResult GetPingFilePath(std::wstring& uuid) {
wchar_t* rawAppDataPath;
HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr,
&rawAppDataPath);
if (FAILED(hr)) {
LOG_ERROR(hr);
return FilePathResult(mozilla::WindowsError::FromHResult(hr));
}
mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter> appDataPath(
rawAppDataPath);
// The Path* functions don't set LastError, but this is the only thing that
// can really cause them to fail, so if they ever do we assume this is why.
hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
wchar_t pingFilePath[MAX_PATH] = L"";
if (!PathCombineW(pingFilePath, appDataPath.get(), L"" MOZ_APP_VENDOR)) {
LOG_ERROR(hr);
return FilePathResult(mozilla::WindowsError::FromHResult(hr));
}
if (!PathAppendW(pingFilePath, L"" MOZ_APP_BASENAME)) {
LOG_ERROR(hr);
return FilePathResult(mozilla::WindowsError::FromHResult(hr));
}
if (!PathAppendW(pingFilePath, L"Pending Pings")) {
LOG_ERROR(hr);
return FilePathResult(mozilla::WindowsError::FromHResult(hr));
}
if (!PathAppendW(pingFilePath, uuid.c_str())) {
LOG_ERROR(hr);
return FilePathResult(mozilla::WindowsError::FromHResult(hr));
}
return std::wstring(pingFilePath);
}
static FilePathResult GetPingsenderPath() {
// The Path* functions don't set LastError, but this is the only thing that
// can really cause them to fail, so if they ever do we assume this is why.
HRESULT hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
mozilla::UniquePtr<wchar_t[]> thisBinaryPath = mozilla::GetFullBinaryPath();
if (!PathRemoveFileSpecW(thisBinaryPath.get())) {
LOG_ERROR(hr);
return FilePathResult(mozilla::WindowsError::FromHResult(hr));
}
wchar_t pingsenderPath[MAX_PATH] = L"";
if (!PathCombineW(pingsenderPath, thisBinaryPath.get(), L"pingsender.exe")) {
LOG_ERROR(hr);
return FilePathResult(mozilla::WindowsError::FromHResult(hr));
}
return std::wstring(pingsenderPath);
}
static mozilla::WindowsError SendPing(std::string defaultBrowser,
std::string previousDefaultBrowser,
std::string osVersion,
std::string osLocale) {
// Fill in the ping JSON object.
Json::Value ping;
ping["build_channel"] = MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL);
ping["version"] = MOZILLA_VERSION;
ping["default_browser"] = defaultBrowser;
ping["previous_default_browser"] = previousDefaultBrowser;
ping["os_version"] = osVersion;
ping["os_locale"] = osLocale;
// Stringify the JSON.
Json::StreamWriterBuilder jsonStream;
jsonStream["indentation"] = "";
std::string pingStr = Json::writeString(jsonStream, ping);
// Generate a UUID for the ping.
FilePathResult uuidResult = GenerateUUIDStr();
if (uuidResult.isErr()) {
return uuidResult.unwrapErr();
}
std::wstring uuid = uuidResult.unwrap();
// Write the JSON string to a file. Use the UUID in the file name so that if
// multiple instances of this task are running they'll have their own files.
FilePathResult pingFilePathResult = GetPingFilePath(uuid);
if (pingFilePathResult.isErr()) {
return pingFilePathResult.unwrapErr();
}
std::wstring pingFilePath = pingFilePathResult.unwrap();
{
std::ofstream outFile(pingFilePath);
outFile << pingStr;
if (outFile.fail()) {
// We have no way to get a specific error code out of a file stream
// other than to catch an exception, so substitute a generic error code.
HRESULT hr = HRESULT_FROM_WIN32(ERROR_IO_DEVICE);
LOG_ERROR(hr);
return mozilla::WindowsError::FromHResult(hr);
}
}
// Hand the file off to pingsender to submit.
FilePathResult pingsenderPathResult = GetPingsenderPath();
if (pingsenderPathResult.isErr()) {
return pingsenderPathResult.unwrapErr();
}
std::wstring pingsenderPath = pingsenderPathResult.unwrap();
std::wstring url(L"" TELEMETRY_PING_URL);
url.append(uuid);
const wchar_t* pingsenderArgs[] = {pingsenderPath.c_str(), url.c_str(),
pingFilePath.c_str()};
mozilla::UniquePtr<wchar_t[]> pingsenderCmdLine(mozilla::MakeCommandLine(
mozilla::ArrayLength(pingsenderArgs),
const_cast<wchar_t**>(pingsenderArgs)));
PROCESS_INFORMATION pi;
STARTUPINFOW si = {sizeof(si)};
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
if (!::CreateProcessW(pingsenderPath.c_str(), pingsenderCmdLine.get(),
nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si,
&pi)) {
HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
LOG_ERROR(hr);
return mozilla::WindowsError::FromHResult(hr);
}
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return mozilla::WindowsError::CreateSuccess();
}
HRESULT SendDefaultBrowserPing() {
TelemetryFieldResult defaultBrowserResult = GetDefaultBrowser();
if (defaultBrowserResult.isErr()) {
return defaultBrowserResult.unwrapErr().AsHResult();
}
std::string defaultBrowser = defaultBrowserResult.unwrap();
TelemetryFieldResult previousDefaultBrowserResult =
GetPreviousDefaultBrowser(defaultBrowser);
if (previousDefaultBrowserResult.isErr()) {
return previousDefaultBrowserResult.unwrapErr().AsHResult();
}
std::string previousDefaultBrowser = previousDefaultBrowserResult.unwrap();
TelemetryFieldResult osVersionResult = GetOSVersion();
if (osVersionResult.isErr()) {
return osVersionResult.unwrapErr().AsHResult();
}
std::string osVersion = osVersionResult.unwrap();
TelemetryFieldResult osLocaleResult = GetOSLocale();
if (osLocaleResult.isErr()) {
return osLocaleResult.unwrapErr().AsHResult();
}
std::string osLocale = osLocaleResult.unwrap();
return
SendPing(defaultBrowser, previousDefaultBrowser, osVersion, osLocale)
.AsHResult();
}

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

@ -0,0 +1,14 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */
#ifndef __DEFAULT_BROWSER_TELEMETRY_H__
#define __DEFAULT_BROWSER_TELEMETRY_H__
#include <windows.h>
HRESULT SendDefaultBrowserPing();
#endif // __DEFAULT_BROWSER_TELEMETRY_H__

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

@ -0,0 +1,14 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */
#ifndef __DEFAULT_BROWSER_AGENT_COMMON_H__
#define __DEFAULT_BROWSER_AGENT_COMMON_H__
#define AGENT_REGKEY_NAME \
L"SOFTWARE\\" MOZ_APP_VENDOR \
"\\" MOZ_APP_BASENAME "\\Default Browser Agent"
#endif // __DEFAULT_BROWSER_AGENT_COMMON_H__

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

@ -0,0 +1,39 @@
=====================
Default Browser Agent
=====================
The Default Browser Agent is a Windows-only scheduled task which runs in the background to collect and submit data about the browser that the user has set as their OS default (that is, the browser that will be invoked by the operating system to open web links that the user clicks on in other programs). Its purpose is to help Mozilla understand user's default browser choices and, in the future, to engage with users at a time when they may not be actively running Firefox.
For information about the specific data that the agent sends, see :doc:`the ping documentation </toolkit/components/telemetry/data/default-browser-ping>`.
Scheduled Task
==============
The agent runs as a `Windows scheduled task <https://docs.microsoft.com/en-us/windows/win32/taskschd/about-the-task-scheduler>`_. The scheduled task executes all of the agent's primary functions; all of its other functions relate to managing the task. The Windows installer is responsible for creating (and the uninstaller for removing) the agent's task entry, but the code for actually doing this resides in the agent itself, and the installers simply call it using dedicated command line parameters (``register-task`` and ``unregister-task``). The :doc:`PostUpdate </browser/installer/windows/installer/Helper>` code also calls the agent with ``update-task`` to update any properties of an existing task registration that need to be updated, or to create one during an application update if none exists.
The tasks are normal entries in the Windows Task Scheduler, managed using `its Win32 API <https://docs.microsoft.com/en-us/windows/win32/api/_taskschd/>`_. They're created in a tasks folder called "Mozilla" (or whatever the application's vendor name is), and there's one for each installation of Firefox (or other Mozilla application). The task is set to run automatically every 24 hours starting at the time it's registered (with the first run being 24 hours after that), or the nearest time after that the computer is awake. The task is configured with one action, which is to run the agent binary with the command line parameter ``do-task``, the command that invokes the actual agent functionality.
The default browser agent needs to run as some OS-level user, as opposed to, say, ``LOCAL SERVICE``, in order to read the user's default browser setting. Therefore, the default browser agent runs as the user that ran the Firefox installer (although always without elevation, whether the installer had it or not).
Data Management
===============
The default browser agent has to be able to work with settings at several different levels: a Firefox profile, an OS user, a Firefox installation, and the entire system. This need creates an information architecture mismatch between all of those things, mostly because no Firefox profile is available to the agent while it's running; it's not really feasible to either directly use or to clone Firefox's profile selection functionality, and even if we could select a profile, whatever code we might use to actually work with it would have the same problems. So, in order to allow for controlling the agent from Firefox, certain settings are mirrored from Firefox to a location where the agent can read them. Since the agent operates in the context only of an OS-level user, that means that in this situation a single OS-level user who uses multiple Firefox profiles may be able to observe the agent's settings changing as the different profiles race to be the active mirror, without them knowingly taking any action.
Pref Reflection
---------------
The agent needs to be able to read (but not set) values that have their canonical representation in the form of Firefox prefs. This means those pref values have to be copied out to a place where the agent can read them. The Windows registry was chosen as that place; it's easier to use than a file, and we already have keys there which are reserved by Firefox. Specifically, the subkey used for these prefs is ``HKEY_CURRENT_USER\Software\[app vendor name]\[app name]\Default Browser Agent\``. During Firefox startup, the values of the prefs that control the agent are reflected to this key, and those values are updated whenever the prefs change after that.
The list of reflected prefs includes the global telemetry opt-out pref ``datareporting.healthreport.uploadEnabled`` and a pref called ``default-browser-agent.enabled``, which can enable or disable the entire agent. The agent checks these registry-reflected pref values when its scheduled task runs, they do not actually prevent the scheduled task from running.
Enterprise policies also exist to perform the same functions as these prefs. These work the same way as all other Firefox policies and `the documentation for those <https://github.com/mozilla/policy-templates/blob/master/README.md>`_ explains how to use them.
Default Browser Setting
-----------------------
The agent is responsible for reporting both the user's current default browser and their previous default browser. Nothing in the operating system records past associations, so the agent must do this for itself. First, it gets the current default browser by calling `IApplicationAssociationRegistration::QueryCurrentDefault <https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-iapplicationassociationregistration-querycurrentdefault>`_ for the ``http`` protocol. It then checks that against a value stored in its own registry key and, if those are different, it knows that the default browser has changed, and records the new and old defaults.

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

@ -0,0 +1,156 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */
#include <windows.h>
#include <shlwapi.h>
#include <objbase.h>
#include <string.h>
#include "nsAutoRef.h"
#include "nsWindowsHelpers.h"
#include "mozilla/WinHeaderOnlyUtils.h"
#include "ScheduledTask.h"
#include "Policy.h"
#include "Telemetry.h"
static void RemoveAllRegistryEntries() {
mozilla::UniquePtr<wchar_t[]> installPath = mozilla::GetFullBinaryPath();
if (!PathRemoveFileSpecW(installPath.get())) {
return;
}
const wchar_t* regKeyName = L"SOFTWARE\\" MOZ_APP_VENDOR "\\" MOZ_APP_BASENAME
"\\Default Browser Agent";
HKEY rawRegKey = nullptr;
if (ERROR_SUCCESS !=
RegOpenKeyExW(HKEY_CURRENT_USER, regKeyName, 0,
KEY_WRITE | KEY_QUERY_VALUE | KEY_WOW64_64KEY,
&rawRegKey)) {
return;
}
nsAutoRegKey regKey(rawRegKey);
DWORD maxValueNameLen = 0;
if (ERROR_SUCCESS != RegQueryInfoKeyW(regKey.get(), nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
&maxValueNameLen, nullptr, nullptr,
nullptr)) {
return;
}
// The length that RegQueryInfoKeyW returns is without a terminator.
maxValueNameLen += 1;
mozilla::UniquePtr<wchar_t[]> valueName =
mozilla::MakeUnique<wchar_t[]>(maxValueNameLen);
DWORD valueIndex = 0;
while (true) {
DWORD valueNameLen = maxValueNameLen;
LSTATUS ls =
RegEnumValueW(regKey.get(), valueIndex, valueName.get(), &valueNameLen,
nullptr, nullptr, nullptr, nullptr);
if (ls != ERROR_SUCCESS) {
break;
}
if (!wcsnicmp(valueName.get(), installPath.get(),
wcslen(installPath.get()))) {
RegDeleteValue(regKey.get(), valueName.get());
// Only increment the index if we did not delete this value, because if
// we did then the indexes of all the values after that one just got
// decremented, meaning the index we already have now refers to a value
// that we haven't looked at yet.
} else {
valueIndex++;
}
}
// If we just deleted every value, then also delete the key.
if (ERROR_SUCCESS != RegQueryInfoKeyW(regKey.get(), nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, &valueIndex,
nullptr, nullptr, nullptr, nullptr)) {
return;
}
regKey.reset();
if (valueIndex == 0) {
RegDeleteKeyW(HKEY_CURRENT_USER, regKeyName);
}
}
// We expect to be given a command string in argv[1], perhaps followed by other
// arguments depending on the command. The valid commands are:
// register-task [unique-token]
// Create a Windows scheduled task that will launch this binary with the
// do-task command every 24 hours, starting from 24 hours after register-task
// is run. unique-token is required and should be some string that uniquely
// identifies this installation of the product; typically this will be the
// install path hash that's used for the update directory, the AppUserModelID,
// and other related purposes.
// update-task [unique-token]
// Update an existing task registration, without changing its schedule. This
// should be called during updates of the application, in case this program
// has been updated and any of the task parameters have changed. The unique
// token argument is required and should be the same one that was passed in
// when the task was registered.
// unregister-task [unique-token]
// Removes the previously created task along with any registry entries that
// running the task may have created. The unique token argument is required
// and should be the same one that was passed in when the task was registered.
// do-task
// Actually performs the default agent task, which currently means generating
// and sending our telemetry ping.
int wmain(int argc, wchar_t** argv) {
if (argc < 2 || !argv[1]) {
return E_INVALIDARG;
}
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(hr)) {
return hr;
}
const struct ComUninitializer {
~ComUninitializer() { CoUninitialize(); }
} kCUi;
// The remove-task command is allowed even if the policy disabling the task
// is set, mainly so that the uninstaller will work.
if (!wcscmp(argv[1], L"unregister-task")) {
if (argc < 3 || !argv[2]) {
return E_INVALIDARG;
}
RemoveAllRegistryEntries();
return RemoveTask(argv[2]);
}
if (IsAgentDisabled()) {
return HRESULT_FROM_WIN32(ERROR_ACCESS_DISABLED_BY_POLICY);
}
if (!wcscmp(argv[1], L"register-task")) {
if (argc < 3 || !argv[2]) {
return E_INVALIDARG;
}
return RegisterTask(argv[2]);
} else if (!wcscmp(argv[1], L"update-task")) {
if (argc < 3 || !argv[2]) {
return E_INVALIDARG;
}
return UpdateTask(argv[2]);
} else if (!wcscmp(argv[1], L"do-task")) {
if (!IsTelemetryDisabled()) {
return SendDefaultBrowserPing();
}
return S_OK;
} else {
return E_INVALIDARG;
}
}

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

@ -0,0 +1 @@
WIN32_MODULE_DESCRIPTION=@MOZ_APP_DISPLAYNAME@ Default Browser Agent

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

@ -0,0 +1,59 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
Program("default-browser-agent")
SPHINX_TREES['default-browser-agent'] = "docs"
UNIFIED_SOURCES += [
"EventLog.cpp",
"main.cpp",
"Policy.cpp",
"ScheduledTask.cpp",
"Telemetry.cpp",
]
USE_LIBS += [
"jsoncpp",
]
LOCAL_INCLUDES += [
'/toolkit/components/jsoncpp/include',
'/xpcom/build',
]
OS_LIBS += [
"ole32",
"oleaut32",
"rpcrt4",
"shell32",
"shlwapi",
"taskschd",
]
DEFINES["NS_NO_XPCOM"] = True
DEFINES['UNICODE'] = True
DEFINES['_UNICODE'] = True
for var in ("MOZ_APP_BASENAME", "MOZ_APP_DISPLAYNAME", "MOZ_APP_VENDOR"):
DEFINES[var] = '"%s"' % CONFIG[var]
RCINCLUDE = "DefaultBrowserAgent.rc"
# We need STL headers that aren't allowed when wrapping is on (at least
# <filesystem>, and possibly others).
DisableStlWrapping()
# We need this to be able to use wmain as the entry point on MinGW;
# otherwise it will try to use WinMain.
if CONFIG['CC_TYPE'] == 'clang-cl':
WIN32_EXE_LDFLAGS += ['-ENTRY:wmainCRTStartup']
else:
WIN32_EXE_LDFLAGS += ['-municode']
with Files("**"):
BUG_COMPONENT = ("Firefox", "Installer")