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, ®StrLen);
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