diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/mozapps/defaultagent/main.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | toolkit/mozapps/defaultagent/main.cpp | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/toolkit/mozapps/defaultagent/main.cpp b/toolkit/mozapps/defaultagent/main.cpp new file mode 100644 index 0000000000..42eabb8d77 --- /dev/null +++ b/toolkit/mozapps/defaultagent/main.cpp @@ -0,0 +1,428 @@ +/* -*- 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 <iostream> + +#include "nsAutoRef.h" +#include "nsWindowsHelpers.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +#include "common.h" +#include "Policy.h" +#include "DefaultBrowser.h" +#include "DefaultPDF.h" +#include "EventLog.h" +#include "Notification.h" +#include "Registry.h" +#include "RemoteSettings.h" +#include "ScheduledTask.h" +#include "SetDefaultBrowser.h" +#include "Telemetry.h" + +// The AGENT_REGKEY_NAME is dependent on MOZ_APP_VENDOR and MOZ_APP_BASENAME, +// so using those values in the mutex name prevents waiting on processes that +// are using completely different data. +#define REGISTRY_MUTEX_NAME \ + L"" MOZ_APP_VENDOR MOZ_APP_BASENAME L"DefaultBrowserAgentRegistryMutex" +// How long to wait on the registry mutex before giving up on it. This should +// be short. Although the WDBA runs in the background, uninstallation happens +// synchronously in the foreground. +#define REGISTRY_MUTEX_TIMEOUT_MS (3 * 1000) + +// Returns true if the registry value name given is one of the +// install-directory-prefixed values used by the Windows Default Browser Agent. +// ex: "C:\Program Files\Mozilla Firefox|PreviousDefault" +// Returns true +// ex: "InitialNotificationShown" +// Returns false +static bool IsPrefixedValueName(const wchar_t* valueName) { + // Prefixed value names use '|' as a delimiter. None of the + // non-install-directory-prefixed value names contain one. + return wcschr(valueName, L'|') != nullptr; +} + +static void RemoveAllRegistryEntries() { + mozilla::UniquePtr<wchar_t[]> installPath = mozilla::GetFullBinaryPath(); + if (!PathRemoveFileSpecW(installPath.get())) { + return; + } + + HKEY rawRegKey = nullptr; + if (ERROR_SUCCESS != + RegOpenKeyExW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME, 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; + // Set this to true if we encounter values in this key that are prefixed with + // different install directories, indicating that this key is still in use + // by other installs. + bool keyStillInUse = false; + + 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 (IsPrefixedValueName(valueName.get())) { + // If this is not one of the unprefixed value names, it must be one of + // the install-directory prefixed values. + keyStillInUse = true; + } + } + } + + regKey.reset(); + + // If no other installs are using this key, remove it now. + if (!keyStillInUse) { + // Use RegDeleteTreeW to remove the cache as well, which is in subkey. + RegDeleteTreeW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME); + } +} + +// This function adds a registry value with this format: +// <install-dir>|Installed=1 +// RemoveAllRegistryEntries() determines whether the registry key is in use +// by other installations by checking for install-directory-prefixed value +// names. Although Firefox mirrors some preferences into install-directory- +// prefixed values, the WDBA no longer uses any prefixed values. Adding this one +// makes uninstallation work as expected slightly more reliably. +static void WriteInstallationRegistryEntry() { + mozilla::WindowsErrorResult<mozilla::Ok> result = + RegistrySetValueBool(IsPrefixed::Prefixed, L"Installed", true); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Failed to write installation registry entry: %#X", + result.unwrapErr().AsHResult()); + } +} + +// This class is designed to prevent concurrency problems when accessing the +// registry. It should be acquired before any usage of unprefixed registry +// entries. +class RegistryMutex { + private: + nsAutoHandle mMutex; + bool mLocked; + + public: + RegistryMutex() : mMutex(nullptr), mLocked(false) {} + ~RegistryMutex() { + Release(); + // nsAutoHandle will take care of closing the mutex's handle. + } + + // Returns true on success, false on failure. + bool Acquire() { + if (mLocked) { + return true; + } + + if (mMutex.get() == nullptr) { + // It seems like we would want to set the second parameter (bInitialOwner) + // to TRUE, but the documentation for CreateMutexW suggests that, because + // we aren't sure that the mutex doesn't already exist, we can't be sure + // whether we got ownership via this mechanism. + mMutex.own(CreateMutexW(nullptr, FALSE, REGISTRY_MUTEX_NAME)); + if (mMutex.get() == nullptr) { + LOG_ERROR_MESSAGE(L"Couldn't open registry mutex: %#X", GetLastError()); + return false; + } + } + + DWORD mutexStatus = + WaitForSingleObject(mMutex.get(), REGISTRY_MUTEX_TIMEOUT_MS); + if (mutexStatus == WAIT_OBJECT_0) { + mLocked = true; + } else if (mutexStatus == WAIT_TIMEOUT) { + LOG_ERROR_MESSAGE(L"Timed out waiting for registry mutex"); + } else if (mutexStatus == WAIT_ABANDONED) { + // This isn't really an error for us. No one else is using the registry. + // This status code means that we are supposed to check our data for + // consistency, but there isn't really anything we can fix here. + // This is an indication that an agent crashed though, which is clearly an + // error, so log an error message. + LOG_ERROR_MESSAGE(L"Found abandoned registry mutex. Continuing..."); + mLocked = true; + } else { + // The only other documented status code is WAIT_FAILED. In the case that + // we somehow get some other code, that is also an error. + LOG_ERROR_MESSAGE(L"Failed to wait on registry mutex: %#X", + GetLastError()); + } + return mLocked; + } + + bool IsLocked() { return mLocked; } + + void Release() { + if (mLocked) { + if (mMutex.get() == nullptr) { + LOG_ERROR_MESSAGE(L"Unexpectedly missing registry mutex"); + return; + } + BOOL success = ReleaseMutex(mMutex.get()); + if (!success) { + LOG_ERROR_MESSAGE(L"Failed to release registry mutex"); + } + mLocked = false; + } + } +}; + +// Returns false (without setting aResult) if reading last run time failed. +static bool CheckIfAppRanRecently(bool* aResult) { + const ULONGLONG kTaskExpirationDays = 90; + const ULONGLONG kTaskExpirationSeconds = kTaskExpirationDays * 24 * 60 * 60; + + MaybeQwordResult lastRunTimeResult = + RegistryGetValueQword(IsPrefixed::Prefixed, L"AppLastRunTime"); + if (lastRunTimeResult.isErr()) { + return false; + } + mozilla::Maybe<ULONGLONG> lastRunTimeMaybe = lastRunTimeResult.unwrap(); + if (!lastRunTimeMaybe.isSome()) { + return false; + } + + ULONGLONG secondsSinceLastRunTime = + SecondsPassedSince(lastRunTimeMaybe.value()); + + *aResult = secondsSinceLastRunTime < kTaskExpirationSeconds; + return true; +} + +// 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. The unique token argument is required +// and should be the same one that was passed in when the task was registered. +// uninstall [unique-token] +// Removes the previously created task, and also removes all registry entries +// 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 [app-user-model-id] +// Actually performs the default agent task, which currently means generating +// and sending our telemetry ping and possibly showing a notification to the +// user if their browser has switched from Firefox to Edge with Blink. +// set-default-browser-user-choice [app-user-model-id] [[.file1 ProgIDRoot1] +// ...] +// Set the default browser via the UserChoice registry keys. Additional +// optional file extensions to register can be specified as additional +// argument pairs: the first element is the file extension, the second element +// is the root of a ProgID, which will be suffixed with `-$AUMI`. +int wmain(int argc, wchar_t** argv) { + if (argc < 2 || !argv[1]) { + return E_INVALIDARG; + } + + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (FAILED(hr)) { + return hr; + } + + const struct ComUninitializer { + ~ComUninitializer() { CoUninitialize(); } + } kCUi; + + RegistryMutex regMutex; + + // The uninstall and unregister commands are allowed even if the policy + // disabling the task is set, so that uninstalls and updates always work. + // Similarly, debug commands are always allowed. + if (!wcscmp(argv[1], L"uninstall")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + + // We aren't actually going to check whether we got the mutex here. + // Ideally we would acquire it since we are about to access the registry, + // so we would like to block simultaneous users of our registry key. + // But there are two reasons that it is preferable to ignore a mutex + // wait timeout here: + // 1. If we fail to uninstall our prefixed registry entries, the + // registry key containing them will never be removed, even when the + // last installation is uninstalled. + // 2. If we timed out waiting on the mutex, it implies that there are + // other installations. If there are other installations, there will + // be other prefixed registry entries. If there are other prefixed + // registry entries, we won't remove the whole key or touch the + // unprefixed entries during uninstallation. Therefore, we should + // be able to safely uninstall without stepping on anyone's toes. + regMutex.Acquire(); + + RemoveAllRegistryEntries(); + return RemoveTasks(argv[2], WhichTasks::AllTasksForInstallation); + } else if (!wcscmp(argv[1], L"unregister-task")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + + return RemoveTasks(argv[2], WhichTasks::WdbaTaskOnly); + } else if (!wcscmp(argv[1], L"debug-remote-disabled")) { + int disabled = IsAgentRemoteDisabled(); + std::cerr << "default-browser-agent: IsAgentRemoteDisabled: " << disabled + << std::endl; + return S_OK; + } + + // We check for disablement by policy because that's assumed to be static. + // But we don't check for disablement by remote settings so that + // `register-task` and `update-task` can proceed as part of the update + // cycle, waiting for remote (re-)enablement. + 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; + } + // We aren't actually going to check whether we got the mutex here. + // Ideally we would acquire it since registration might migrate registry + // entries. But it is preferable to ignore a mutex wait timeout here + // because: + // 1. Otherwise the task doesn't get registered at all + // 2. If another installation's agent is holding the mutex, it either + // is far enough out of date that it doesn't yet use the migrated + // values, or it already did the migration for us. + regMutex.Acquire(); + + WriteInstallationRegistryEntry(); + + return RegisterTask(argv[2]); + } else if (!wcscmp(argv[1], L"update-task")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + // Not checking if we got the mutex for the same reason we didn't in + // register-task + regMutex.Acquire(); + + WriteInstallationRegistryEntry(); + + return UpdateTask(argv[2]); + } else if (!wcscmp(argv[1], L"do-task")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + + bool force = (argc > 3) && ((0 == wcscmp(argv[3], L"--force")) || + (0 == wcscmp(argv[3], L"-force"))); + + // Acquire() has a short timeout. Since this runs in the background, we + // could use a longer timeout in this situation. However, if another + // installation's agent is already running, it will update CurrentDefault, + // possibly send a ping, and possibly show a notification. + // Once all that has happened, there is no real reason to do it again. We + // only send one ping per day, so we aren't going to do that again. And + // the only time we ever show a second notification is 7 days after the + // first one, so we aren't going to do that again either. + // If the other process didn't take those actions, there is no reason that + // this process would take them. + // If the other process fails, this one will most likely fail for the same + // reason. + // So we'll just bail if we can't get the mutex quickly. + if (!regMutex.Acquire()) { + return HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION); + } + + // Check that Firefox ran recently, if not then stop here. + // Also stop if no timestamp was found, which most likely indicates + // that Firefox was not yet run. + bool ranRecently = false; + if (!force && (!CheckIfAppRanRecently(&ranRecently) || !ranRecently)) { + return SCHED_E_TASK_ATTEMPTED; + } + + // Check for remote disable and (re-)enable before (potentially) + // updating registry entries and showing notifications. + if (IsAgentRemoteDisabled()) { + return S_OK; + } + + DefaultBrowserResult defaultBrowserResult = GetDefaultBrowserInfo(); + if (defaultBrowserResult.isErr()) { + return defaultBrowserResult.unwrapErr().AsHResult(); + } + DefaultBrowserInfo browserInfo = defaultBrowserResult.unwrap(); + + DefaultPdfResult defaultPdfResult = GetDefaultPdfInfo(); + if (defaultPdfResult.isErr()) { + return defaultPdfResult.unwrapErr().AsHResult(); + } + DefaultPdfInfo pdfInfo = defaultPdfResult.unwrap(); + + NotificationActivities activitiesPerformed = + MaybeShowNotification(browserInfo, argv[2], force); + + return SendDefaultBrowserPing(browserInfo, pdfInfo, activitiesPerformed); + } else if (!wcscmp(argv[1], L"set-default-browser-user-choice")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + + // `argv` is itself null-terminated, so we can safely pass the tail of the + // array here. + return SetDefaultBrowserUserChoice(argv[2], &argv[3]); + } else if (!wcscmp(argv[1], L"set-default-extension-handlers-user-choice")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + + // `argv` is itself null-terminated, so we can safely pass the tail of the + // array here. + return SetDefaultExtensionHandlersUserChoice(argv[2], &argv[3]); + } else { + return E_INVALIDARG; + } +} |