1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "Telemetry.h"
8 
9 #include <shlobj.h>
10 #include <shlwapi.h>
11 
12 #include <fstream>
13 #include <string>
14 #include <unordered_map>
15 
16 #include "common.h"
17 #include "EventLog.h"
18 
19 #include "json/json.h"
20 #include "mozilla/ArrayUtils.h"
21 #include "mozilla/CmdLineAndEnvUtils.h"
22 #include "mozilla/HelperMacros.h"
23 #include "mozilla/RefPtr.h"
24 #include "mozilla/UniquePtr.h"
25 #include "mozilla/WinHeaderOnlyUtils.h"
26 
27 #define TELEMETRY_BASE_URL "https://incoming.telemetry.mozilla.org/submit"
28 #define TELEMETRY_NAMESPACE "default-browser-agent"
29 #define TELEMETRY_PING_VERSION "1"
30 #define TELEMETRY_PING_DOCTYPE "default-browser"
31 
32 // This is almost the complete URL, just needs a UUID appended.
33 #define TELEMETRY_PING_URL                                              \
34   TELEMETRY_BASE_URL "/" TELEMETRY_NAMESPACE "/" TELEMETRY_PING_DOCTYPE \
35                      "/" TELEMETRY_PING_VERSION "/"
36 
37 #if !defined(RRF_SUBKEY_WOW6464KEY)
38 #  define RRF_SUBKEY_WOW6464KEY 0x00010000
39 #endif  // !defined(RRF_SUBKEY_WOW6464KEY)
40 
41 using TelemetryFieldResult = mozilla::WindowsErrorResult<std::string>;
42 using FilePathResult = mozilla::WindowsErrorResult<std::wstring>;
43 
44 // This function was copied from the implementation of
45 // nsITelemetry::isOfficialTelemetry, currently found in the file
46 // toolkit/components/telemetry/core/Telemetry.cpp.
IsOfficialTelemetry()47 static bool IsOfficialTelemetry() {
48 #if defined(MOZILLA_OFFICIAL) && defined(MOZ_TELEMETRY_REPORTING) && \
49     !defined(DEBUG)
50   return true;
51 #else
52   return false;
53 #endif
54 }
55 
GetDefaultBrowser()56 static TelemetryFieldResult GetDefaultBrowser() {
57   RefPtr<IApplicationAssociationRegistration> pAAR;
58   HRESULT hr = CoCreateInstance(
59       CLSID_ApplicationAssociationRegistration, nullptr, CLSCTX_INPROC,
60       IID_IApplicationAssociationRegistration, getter_AddRefs(pAAR));
61   if (FAILED(hr)) {
62     LOG_ERROR(hr);
63     return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
64   }
65 
66   // Whatever is handling the HTTP protocol is effectively the default browser.
67   wchar_t* rawRegisteredApp;
68   hr = pAAR->QueryCurrentDefault(L"http", AT_URLPROTOCOL, AL_EFFECTIVE,
69                                  &rawRegisteredApp);
70   if (FAILED(hr)) {
71     LOG_ERROR(hr);
72     return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
73   }
74   mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter> registeredApp(
75       rawRegisteredApp);
76 
77   // This maps a prefix of the AppID string used to register each browser's HTTP
78   // handler to a custom string that we'll use to identify that browser in our
79   // telemetry ping (which is this function's return value).
80   // We're assuming that any UWP app set as the default browser must be Edge.
81   const std::unordered_map<std::wstring, std::string> AppIDPrefixes = {
82       {L"Firefox", "firefox"},       {L"Chrome", "chrome"}, {L"AppX", "edge"},
83       {L"MSEdgeHTM", "edge-chrome"}, {L"IE.", "ie"},        {L"Opera", "opera"},
84       {L"Brave", "brave"},
85   };
86 
87   for (const auto& prefix : AppIDPrefixes) {
88     if (!wcsnicmp(registeredApp.get(), prefix.first.c_str(),
89                   prefix.first.length())) {
90       return prefix.second;
91     }
92   }
93 
94   // The default browser is one that we don't know about.
95   return std::string("");
96 }
97 
GetPreviousDefaultBrowser(std::string & currentDefault)98 static TelemetryFieldResult GetPreviousDefaultBrowser(
99     std::string& currentDefault) {
100   // This function uses a registry value which stores the current default
101   // browser. It returns the data stored in that registry value and replaces the
102   // stored string with the current default browser string that was passed in.
103 
104   // We'll need the currentDefault string in UTF-16 so that we can use it
105   // in and around the registry.
106   int currentDefaultLen =
107       MultiByteToWideChar(CP_UTF8, 0, currentDefault.c_str(), -1, nullptr, 0);
108   mozilla::UniquePtr<wchar_t[]> wCurrentDefault =
109       mozilla::MakeUnique<wchar_t[]>(currentDefaultLen);
110   MultiByteToWideChar(CP_UTF8, 0, currentDefault.c_str(), -1,
111                       wCurrentDefault.get(), currentDefaultLen);
112 
113   // We don't really need to store this value using a name that includes the
114   // install path, because the default browser is a system (per-user) setting,
115   // but we're doing it anyway as a means of avoiding concurrency issues if
116   // multiple instances of the task are running at once.
117   mozilla::UniquePtr<wchar_t[]> installPath = mozilla::GetFullBinaryPath();
118   if (!PathRemoveFileSpecW(installPath.get())) {
119     LOG_ERROR(HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER));
120     return std::string("");
121   }
122   std::wstring currentDefaultRegistryValueName(installPath.get());
123   currentDefaultRegistryValueName.append(L"|CurrentDefault");
124 
125   // First, read the "current default" value that is already stored in the
126   // registry.
127   wchar_t oldCurrentDefault[MAX_PATH + 1] = L"";
128   DWORD regStrLen = MAX_PATH + 1;
129   LSTATUS ls =
130       RegGetValueW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME,
131                    currentDefaultRegistryValueName.c_str(), RRF_RT_REG_SZ,
132                    nullptr, &oldCurrentDefault, &regStrLen);
133   if (ls != ERROR_SUCCESS) {
134     wcsncpy_s(oldCurrentDefault, MAX_PATH, wCurrentDefault.get(), _TRUNCATE);
135   }
136 
137   // Next, store the string passed in into the registry value
138   RegSetKeyValueW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME,
139                   currentDefaultRegistryValueName.c_str(), REG_SZ,
140                   wCurrentDefault.get(), currentDefaultLen * sizeof(wchar_t));
141 
142   // We need the previous default string in UTF-8 so we can submit it.
143   int previousDefaultLen = WideCharToMultiByte(
144       CP_UTF8, 0, oldCurrentDefault, -1, nullptr, 0, nullptr, nullptr);
145   mozilla::UniquePtr<char[]> narrowPreviousDefault =
146       mozilla::MakeUnique<char[]>(previousDefaultLen);
147   WideCharToMultiByte(CP_UTF8, 0, oldCurrentDefault, -1,
148                       narrowPreviousDefault.get(), previousDefaultLen, nullptr,
149                       nullptr);
150   return std::string(narrowPreviousDefault.get());
151 }
152 
GetOSVersion()153 static TelemetryFieldResult GetOSVersion() {
154   OSVERSIONINFOEXW osv = {sizeof(osv)};
155   if (::GetVersionExW(reinterpret_cast<OSVERSIONINFOW*>(&osv))) {
156     std::ostringstream oss;
157     oss << osv.dwMajorVersion << "." << osv.dwMinorVersion << "."
158         << osv.dwBuildNumber;
159 
160     if (osv.dwMajorVersion == 10 && osv.dwMinorVersion == 0) {
161       // Get the "Update Build Revision" (UBR) value
162       DWORD ubrValue;
163       DWORD ubrValueLen = sizeof(ubrValue);
164       LSTATUS ubrOk =
165           ::RegGetValueW(HKEY_LOCAL_MACHINE,
166                          L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
167                          L"UBR", RRF_RT_DWORD | RRF_SUBKEY_WOW6464KEY, nullptr,
168                          &ubrValue, &ubrValueLen);
169       if (ubrOk == ERROR_SUCCESS) {
170         oss << "." << ubrValue;
171       }
172     }
173 
174     return oss.str();
175   }
176 
177   HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
178   LOG_ERROR(hr);
179   return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
180 }
181 
GetOSLocale()182 static TelemetryFieldResult GetOSLocale() {
183   wchar_t localeName[LOCALE_NAME_MAX_LENGTH] = L"";
184   if (!GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH)) {
185     HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
186     LOG_ERROR(hr);
187     return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
188   }
189 
190   // We'll need the locale string in UTF-8 to be able to submit it.
191   int bufLen = WideCharToMultiByte(CP_UTF8, 0, localeName, -1, nullptr, 0,
192                                    nullptr, nullptr);
193   mozilla::UniquePtr<char[]> narrowLocaleName =
194       mozilla::MakeUnique<char[]>(bufLen);
195   if (!narrowLocaleName) {
196     HRESULT hr = HRESULT_FROM_WIN32(ERROR_OUTOFMEMORY);
197     LOG_ERROR(hr);
198     return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
199   }
200   WideCharToMultiByte(CP_UTF8, 0, localeName, -1, narrowLocaleName.get(),
201                       bufLen, nullptr, nullptr);
202 
203   return std::string(narrowLocaleName.get());
204 }
205 
GenerateUUIDStr()206 static FilePathResult GenerateUUIDStr() {
207   UUID uuid;
208   RPC_STATUS status = UuidCreate(&uuid);
209   if (status != RPC_S_OK) {
210     HRESULT hr = MAKE_HRESULT(1, FACILITY_RPC, status);
211     LOG_ERROR(hr);
212     return FilePathResult(mozilla::WindowsError::FromHResult(hr));
213   }
214 
215   // 39 == length of a UUID string including braces and NUL.
216   wchar_t guidBuf[39] = {};
217   if (StringFromGUID2(uuid, guidBuf, 39) != 39) {
218     LOG_ERROR(HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER));
219     return FilePathResult(
220         mozilla::WindowsError::FromWin32Error(ERROR_INSUFFICIENT_BUFFER));
221   }
222 
223   // Remove the curly braces.
224   return std::wstring(guidBuf + 1, guidBuf + 37);
225 }
226 
GetPingFilePath(std::wstring & uuid)227 static FilePathResult GetPingFilePath(std::wstring& uuid) {
228   wchar_t* rawAppDataPath;
229   HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr,
230                                     &rawAppDataPath);
231   if (FAILED(hr)) {
232     LOG_ERROR(hr);
233     return FilePathResult(mozilla::WindowsError::FromHResult(hr));
234   }
235   mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter> appDataPath(
236       rawAppDataPath);
237 
238   // The Path* functions don't set LastError, but this is the only thing that
239   // can really cause them to fail, so if they ever do we assume this is why.
240   hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
241 
242   wchar_t pingFilePath[MAX_PATH] = L"";
243   if (!PathCombineW(pingFilePath, appDataPath.get(), L"" MOZ_APP_VENDOR)) {
244     LOG_ERROR(hr);
245     return FilePathResult(mozilla::WindowsError::FromHResult(hr));
246   }
247 
248   if (!PathAppendW(pingFilePath, L"" MOZ_APP_BASENAME)) {
249     LOG_ERROR(hr);
250     return FilePathResult(mozilla::WindowsError::FromHResult(hr));
251   }
252 
253   if (!PathAppendW(pingFilePath, L"Pending Pings")) {
254     LOG_ERROR(hr);
255     return FilePathResult(mozilla::WindowsError::FromHResult(hr));
256   }
257 
258   if (!PathAppendW(pingFilePath, uuid.c_str())) {
259     LOG_ERROR(hr);
260     return FilePathResult(mozilla::WindowsError::FromHResult(hr));
261   }
262 
263   return std::wstring(pingFilePath);
264 }
265 
GetPingsenderPath()266 static FilePathResult GetPingsenderPath() {
267   // The Path* functions don't set LastError, but this is the only thing that
268   // can really cause them to fail, so if they ever do we assume this is why.
269   HRESULT hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
270 
271   mozilla::UniquePtr<wchar_t[]> thisBinaryPath = mozilla::GetFullBinaryPath();
272   if (!PathRemoveFileSpecW(thisBinaryPath.get())) {
273     LOG_ERROR(hr);
274     return FilePathResult(mozilla::WindowsError::FromHResult(hr));
275   }
276 
277   wchar_t pingsenderPath[MAX_PATH] = L"";
278 
279   if (!PathCombineW(pingsenderPath, thisBinaryPath.get(), L"pingsender.exe")) {
280     LOG_ERROR(hr);
281     return FilePathResult(mozilla::WindowsError::FromHResult(hr));
282   }
283 
284   return std::wstring(pingsenderPath);
285 }
286 
SendPing(std::string defaultBrowser,std::string previousDefaultBrowser,std::string osVersion,std::string osLocale)287 static mozilla::WindowsError SendPing(std::string defaultBrowser,
288                                       std::string previousDefaultBrowser,
289                                       std::string osVersion,
290                                       std::string osLocale) {
291   // Fill in the ping JSON object.
292   Json::Value ping;
293   ping["build_channel"] = MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL);
294   ping["build_version"] = MOZILLA_VERSION;
295   ping["default_browser"] = defaultBrowser;
296   ping["previous_default_browser"] = previousDefaultBrowser;
297   ping["os_version"] = osVersion;
298   ping["os_locale"] = osLocale;
299 
300   // Stringify the JSON.
301   Json::StreamWriterBuilder jsonStream;
302   jsonStream["indentation"] = "";
303   std::string pingStr = Json::writeString(jsonStream, ping);
304 
305   // Generate a UUID for the ping.
306   FilePathResult uuidResult = GenerateUUIDStr();
307   if (uuidResult.isErr()) {
308     return uuidResult.unwrapErr();
309   }
310   std::wstring uuid = uuidResult.unwrap();
311 
312   // Write the JSON string to a file. Use the UUID in the file name so that if
313   // multiple instances of this task are running they'll have their own files.
314   FilePathResult pingFilePathResult = GetPingFilePath(uuid);
315   if (pingFilePathResult.isErr()) {
316     return pingFilePathResult.unwrapErr();
317   }
318   std::wstring pingFilePath = pingFilePathResult.unwrap();
319 
320   {
321     std::ofstream outFile(pingFilePath);
322     outFile << pingStr;
323     if (outFile.fail()) {
324       // We have no way to get a specific error code out of a file stream
325       // other than to catch an exception, so substitute a generic error code.
326       HRESULT hr = HRESULT_FROM_WIN32(ERROR_IO_DEVICE);
327       LOG_ERROR(hr);
328       return mozilla::WindowsError::FromHResult(hr);
329     }
330   }
331 
332   // Hand the file off to pingsender to submit.
333   FilePathResult pingsenderPathResult = GetPingsenderPath();
334   if (pingsenderPathResult.isErr()) {
335     return pingsenderPathResult.unwrapErr();
336   }
337   std::wstring pingsenderPath = pingsenderPathResult.unwrap();
338 
339   std::wstring url(L"" TELEMETRY_PING_URL);
340   url.append(uuid);
341 
342   const wchar_t* pingsenderArgs[] = {pingsenderPath.c_str(), url.c_str(),
343                                      pingFilePath.c_str()};
344   mozilla::UniquePtr<wchar_t[]> pingsenderCmdLine(
345       mozilla::MakeCommandLine(mozilla::ArrayLength(pingsenderArgs),
346                                const_cast<wchar_t**>(pingsenderArgs)));
347 
348   PROCESS_INFORMATION pi;
349   STARTUPINFOW si = {sizeof(si)};
350   si.dwFlags = STARTF_USESHOWWINDOW;
351   si.wShowWindow = SW_HIDE;
352   if (!::CreateProcessW(pingsenderPath.c_str(), pingsenderCmdLine.get(),
353                         nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si,
354                         &pi)) {
355     HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
356     LOG_ERROR(hr);
357     return mozilla::WindowsError::FromHResult(hr);
358   }
359 
360   CloseHandle(pi.hThread);
361   CloseHandle(pi.hProcess);
362 
363   return mozilla::WindowsError::CreateSuccess();
364 }
365 
SendDefaultBrowserPing()366 HRESULT SendDefaultBrowserPing() {
367   TelemetryFieldResult defaultBrowserResult = GetDefaultBrowser();
368   if (defaultBrowserResult.isErr()) {
369     return defaultBrowserResult.unwrapErr().AsHResult();
370   }
371   std::string defaultBrowser = defaultBrowserResult.unwrap();
372 
373   TelemetryFieldResult previousDefaultBrowserResult =
374       GetPreviousDefaultBrowser(defaultBrowser);
375   if (previousDefaultBrowserResult.isErr()) {
376     return previousDefaultBrowserResult.unwrapErr().AsHResult();
377   }
378   std::string previousDefaultBrowser = previousDefaultBrowserResult.unwrap();
379 
380   TelemetryFieldResult osVersionResult = GetOSVersion();
381   if (osVersionResult.isErr()) {
382     return osVersionResult.unwrapErr().AsHResult();
383   }
384   std::string osVersion = osVersionResult.unwrap();
385 
386   TelemetryFieldResult osLocaleResult = GetOSLocale();
387   if (osLocaleResult.isErr()) {
388     return osLocaleResult.unwrapErr().AsHResult();
389   }
390   std::string osLocale = osLocaleResult.unwrap();
391 
392   // Do not send the ping if we are not an official telemetry-enabled build;
393   // don't even generate the ping in fact, because if we write the file out
394   // then some other build might find it later and decide to submit it.
395   if (!IsOfficialTelemetry()) {
396     return S_OK;
397   }
398 
399   return SendPing(defaultBrowser, previousDefaultBrowser, osVersion, osLocale)
400       .AsHResult();
401 }
402