/* -*- 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 #include #include #include #include #include "common.h" #include "Cache.h" #include "EventLog.h" #include "Notification.h" #include "Policy.h" #include "UtfConvert.h" #include "Registry.h" #include "json/json.h" #include "mozilla/ArrayUtils.h" #include "mozilla/CmdLineAndEnvUtils.h" #include "mozilla/HelperMacros.h" #include "mozilla/UniquePtr.h" #include "mozilla/Unused.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_DOCTYPE \ "/" TELEMETRY_PING_VERSION "/" // We only want to send one ping per day. However, this is slightly less than 24 // hours so that we have a little bit of wiggle room on our task, which is also // supposed to run every 24 hours. #define MINIMUM_PING_PERIOD_SEC ((23 * 60 * 60) + (45 * 60)) #define PREV_NOTIFICATION_ACTION_REG_NAME L"PrevNotificationAction" #if !defined(RRF_SUBKEY_WOW6464KEY) # define RRF_SUBKEY_WOW6464KEY 0x00010000 #endif // !defined(RRF_SUBKEY_WOW6464KEY) using TelemetryFieldResult = mozilla::WindowsErrorResult; using BoolResult = mozilla::WindowsErrorResult; // This function was copied from the implementation of // nsITelemetry::isOfficialTelemetry, currently found in the file // toolkit/components/telemetry/core/Telemetry.cpp. static bool IsOfficialTelemetry() { #if defined(MOZILLA_OFFICIAL) && defined(MOZ_TELEMETRY_REPORTING) && \ !defined(DEBUG) return true; #else return false; #endif } static TelemetryFieldResult GetOSVersion() { OSVERSIONINFOEXW osv = {sizeof(osv)}; if (::GetVersionExW(reinterpret_cast(&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. Utf16ToUtf8Result narrowLocaleName = Utf16ToUtf8(localeName); return narrowLocaleName.unwrapOr(""); } 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 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 mozilla::WindowsError SendPing( const std::string defaultBrowser, const std::string previousDefaultBrowser, const std::string defaultPdf, const std::string osVersion, const std::string osLocale, const std::string notificationType, const std::string notificationShown, const std::string notificationAction, const std::string prevNotificationAction) { // Fill in the ping JSON object. Json::Value ping; ping["build_channel"] = MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL); ping["build_version"] = MOZILLA_VERSION; ping["default_browser"] = defaultBrowser; ping["previous_default_browser"] = previousDefaultBrowser; ping["default_pdf_viewer_raw"] = defaultPdf; ping["os_version"] = osVersion; ping["os_locale"] = osLocale; ping["notification_type"] = notificationType; ping["notification_shown"] = notificationShown; ping["notification_action"] = notificationAction; ping["previous_notification_action"] = prevNotificationAction; // 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 = GetRelativeBinaryPath(L"pingsender.exe"); 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 pingsenderCmdLine( mozilla::MakeCommandLine(mozilla::ArrayLength(pingsenderArgs), const_cast(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(); } // This function checks if a ping has already been sent today. If one has not, // it assumes that we are about to send one and sets a registry entry that will // cause this function to return true for the next day. // This function uses unprefixed registry entries, so a RegistryMutex should be // held before calling. static BoolResult GetPingAlreadySentToday() { const wchar_t* valueName = L"LastPingSentAt"; MaybeQwordResult readResult = RegistryGetValueQword(IsPrefixed::Unprefixed, valueName); if (readResult.isErr()) { HRESULT hr = readResult.unwrapErr().AsHResult(); LOG_ERROR_MESSAGE(L"Unable to read registry: %#X", hr); return BoolResult(mozilla::WindowsError::FromHResult(hr)); } mozilla::Maybe maybeValue = readResult.unwrap(); ULONGLONG now = GetCurrentTimestamp(); if (maybeValue.isSome()) { ULONGLONG lastPingTime = maybeValue.value(); if (SecondsPassedSince(lastPingTime, now) < MINIMUM_PING_PERIOD_SEC) { return true; } } mozilla::WindowsErrorResult writeResult = RegistrySetValueQword(IsPrefixed::Unprefixed, valueName, now); if (writeResult.isErr()) { HRESULT hr = readResult.unwrapErr().AsHResult(); LOG_ERROR_MESSAGE(L"Unable to write registry: %#X", hr); return BoolResult(mozilla::WindowsError::FromHResult(hr)); } return false; } // This both retrieves a value from the registry and writes new data // (currentDefault) to the same value. If there is no value stored, the value // passed for prevDefault will be converted to a string and returned instead. // // Although we already store and retrieve a cached previous default browser // value elsewhere, it may be updated when we don't send a ping. The value we // retrieve here will only be updated when we are sending a ping to ensure // that pings don't miss a default browser transition. static TelemetryFieldResult GetAndUpdatePreviousDefaultBrowser( const std::string& currentDefault, Browser prevDefault) { const wchar_t* registryValueName = L"PingCurrentDefault"; MaybeStringResult readResult = RegistryGetValueString(IsPrefixed::Unprefixed, registryValueName); if (readResult.isErr()) { HRESULT hr = readResult.unwrapErr().AsHResult(); LOG_ERROR_MESSAGE(L"Unable to read registry: %#X", hr); return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr)); } mozilla::Maybe maybeValue = readResult.unwrap(); std::string oldCurrentDefault; if (maybeValue.isSome()) { oldCurrentDefault = maybeValue.value(); } else { oldCurrentDefault = GetStringForBrowser(prevDefault); } mozilla::WindowsErrorResult writeResult = RegistrySetValueString( IsPrefixed::Unprefixed, registryValueName, currentDefault.c_str()); if (writeResult.isErr()) { HRESULT hr = writeResult.unwrapErr().AsHResult(); LOG_ERROR_MESSAGE(L"Unable to write registry: %#X", hr); return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr)); } return oldCurrentDefault; } // If notifications actions occurred, we want to make sure a ping gets sent for // them. If we aren't sending a ping right now, we want to cache the ping values // for the next time the ping is sent. // The values passed will only be cached if actions were actually taken // (i.e. not when notificationShown == "not-shown") HRESULT MaybeCache(Cache& cache, const std::string& notificationType, const std::string& notificationShown, const std::string& notificationAction, const std::string& prevNotificationAction) { std::string notShown = GetStringForNotificationShown(NotificationShown::NotShown); if (notificationShown == notShown) { return S_OK; } Cache::Entry entry{ .notificationType = notificationType, .notificationShown = notificationShown, .notificationAction = notificationAction, .prevNotificationAction = prevNotificationAction, }; VoidResult result = cache.Enqueue(entry); if (result.isErr()) { return result.unwrapErr().AsHResult(); } return S_OK; } // This function retrieves values cached by MaybeCache. If any values were // loaded from the cache, the values passed in to this function are passed to // MaybeCache so that they are not lost. If there are no values in the cache, // the values passed will not be changed. // Values retrieved from the cache will also be removed from it. HRESULT MaybeSwapForCached(Cache& cache, std::string& notificationType, std::string& notificationShown, std::string& notificationAction, std::string& prevNotificationAction) { Cache::MaybeEntryResult result = cache.Dequeue(); if (result.isErr()) { HRESULT hr = result.unwrapErr().AsHResult(); LOG_ERROR_MESSAGE(L"Failed to read cache: %#X", hr); return hr; } Cache::MaybeEntry maybeEntry = result.unwrap(); if (maybeEntry.isNothing()) { return S_OK; } MaybeCache(cache, notificationType, notificationShown, notificationAction, prevNotificationAction); notificationType = maybeEntry.value().notificationType; notificationShown = maybeEntry.value().notificationShown; notificationAction = maybeEntry.value().notificationAction; if (maybeEntry.value().prevNotificationAction.isSome()) { prevNotificationAction = maybeEntry.value().prevNotificationAction.value(); } else { prevNotificationAction = GetStringForNotificationAction(NotificationAction::NoAction); } return S_OK; } HRESULT ReadPreviousNotificationAction(std::string& prevAction) { MaybeStringResult maybePrevActionResult = RegistryGetValueString( IsPrefixed::Unprefixed, PREV_NOTIFICATION_ACTION_REG_NAME); if (maybePrevActionResult.isErr()) { HRESULT hr = maybePrevActionResult.unwrapErr().AsHResult(); LOG_ERROR_MESSAGE(L"Unable to read prev action from registry: %#X", hr); return hr; } mozilla::Maybe maybePrevAction = maybePrevActionResult.unwrap(); if (maybePrevAction.isNothing()) { prevAction = GetStringForNotificationAction(NotificationAction::NoAction); } else { prevAction = maybePrevAction.value(); // There's no good reason why there should be an invalid value stored here. // But it's also not worth aborting the whole ping over. This function will // silently change it to "no-action" if the value isn't valid to prevent us // from sending unexpected telemetry values. EnsureValidNotificationAction(prevAction); } return S_OK; } // Writes the previous notification action to the registry, but only if a // notification was shown. HRESULT MaybeWritePreviousNotificationAction( const NotificationActivities& activitiesPerformed) { if (activitiesPerformed.shown != NotificationShown::Shown) { return S_OK; } std::string notificationAction = GetStringForNotificationAction(activitiesPerformed.action); mozilla::WindowsErrorResult result = RegistrySetValueString( IsPrefixed::Unprefixed, PREV_NOTIFICATION_ACTION_REG_NAME, notificationAction.c_str()); if (result.isErr()) { HRESULT hr = result.unwrapErr().AsHResult(); LOG_ERROR_MESSAGE(L"Unable to write prev action to registry: %#X", hr); return hr; } return S_OK; } HRESULT SendDefaultBrowserPing( const DefaultBrowserInfo& browserInfo, const DefaultPdfInfo& pdfInfo, const NotificationActivities& activitiesPerformed) { std::string currentDefaultBrowser = GetStringForBrowser(browserInfo.currentDefaultBrowser); std::string currentDefaultPdf = pdfInfo.currentDefaultPdf; std::string notificationType = GetStringForNotificationType(activitiesPerformed.type); std::string notificationShown = GetStringForNotificationShown(activitiesPerformed.shown); std::string notificationAction = GetStringForNotificationAction(activitiesPerformed.action); 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(); std::string prevNotificationAction; HRESULT hr = ReadPreviousNotificationAction(prevNotificationAction); if (FAILED(hr)) { return hr; } // Intentionally discard the result of this write. There's no real reason // to abort sending the ping in the error case and it already wrote an error // message. So there isn't really anything to do at this point. MaybeWritePreviousNotificationAction(activitiesPerformed); Cache cache; // Do not send the ping if we are not an official telemetry-enabled build; // don't even generate the ping in fact, because if we write the file out // then some other build might find it later and decide to submit it. if (!IsOfficialTelemetry() || IsTelemetryDisabled()) { return MaybeCache(cache, notificationType, notificationShown, notificationAction, prevNotificationAction); } // Pings are limited to one per day (across all installations), so check if we // already sent one today. // This will also set a registry entry indicating that the last ping was // just sent, to prevent another one from being sent today. We'll do this // now even though we haven't sent the ping yet. After this check, we send // a ping unconditionally. The only exception is for errors, and any error // that we get now will probably be hit every time. // Because unsent pings attempted with pingsender can get automatically // re-sent later, we don't even want to try again on transient network // failures. BoolResult pingAlreadySentResult = GetPingAlreadySentToday(); if (pingAlreadySentResult.isErr()) { return pingAlreadySentResult.unwrapErr().AsHResult(); } bool pingAlreadySent = pingAlreadySentResult.unwrap(); if (pingAlreadySent) { return MaybeCache(cache, notificationType, notificationShown, notificationAction, prevNotificationAction); } hr = MaybeSwapForCached(cache, notificationType, notificationShown, notificationAction, prevNotificationAction); if (FAILED(hr)) { return hr; } // Don't update the registry's default browser data until we are sure we // want to send a ping. Otherwise it could be updated to reflect a ping we // never sent. TelemetryFieldResult previousDefaultBrowserResult = GetAndUpdatePreviousDefaultBrowser(currentDefaultBrowser, browserInfo.previousDefaultBrowser); if (previousDefaultBrowserResult.isErr()) { return previousDefaultBrowserResult.unwrapErr().AsHResult(); } std::string previousDefaultBrowser = previousDefaultBrowserResult.unwrap(); return SendPing(currentDefaultBrowser, previousDefaultBrowser, currentDefaultPdf, osVersion, osLocale, notificationType, notificationShown, notificationAction, prevNotificationAction) .AsHResult(); }