download_firefox.cpp (11364B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 #include "download_firefox.h" 6 #include <iostream> 7 #include <optional> 8 #include <string> 9 #include <wchar.h> 10 #include <windows.h> 11 #include <winhttp.h> 12 #include "data_sink.h" 13 14 static const wchar_t* user_agent = L"FirefoxDesktopLauncher/0.1.0"; 15 16 // This is how long we allow users to wait before giving up on the download. 17 static const DWORD timeout_ms = 5000; 18 static const int BUFFER_SIZE = 1 << 16; 19 static char buffer[BUFFER_SIZE]; 20 21 struct DownloadContext { 22 HINTERNET hsession; 23 HINTERNET hconnection; 24 HINTERNET hrequest; 25 DataSink* dataSink; 26 std::wstring contentType; 27 HANDLE eventHandle; 28 ErrCode asyncStatus; 29 }; 30 31 std::optional<std::wstring> get_architecture() { 32 SYSTEM_INFO sysinfo; 33 GetSystemInfo(&sysinfo); 34 switch (sysinfo.wProcessorArchitecture) { 35 case PROCESSOR_ARCHITECTURE_AMD64: 36 return L"win64"; 37 case PROCESSOR_ARCHITECTURE_INTEL: 38 return L"win"; 39 case PROCESSOR_ARCHITECTURE_ARM64: 40 return L"win64-aarch64"; 41 default: 42 return std::nullopt; 43 } 44 } 45 46 /** 47 * Generate the path and query parameters needed for the request 48 * to download the Firefox stub installer. The object name 49 * includes the user's locale, which indicates which language/locale 50 * version of Firefox to download. 51 * @return The path and query params for including in the HTTP request 52 */ 53 std::optional<std::wstring> get_object_name() { 54 wchar_t locale_name[LOCALE_NAME_MAX_LENGTH]{}; 55 int ct = GetUserDefaultLocaleName(locale_name, sizeof locale_name); 56 if (ct == 0) { 57 return std::nullopt; 58 } 59 std::wstring lang = locale_name; 60 std::optional<std::wstring> arch = get_architecture(); 61 if (!arch.has_value()) { 62 return std::nullopt; 63 } 64 65 #if defined(MOZ_BRANDING_IS_OFFICIAL) 66 // Common case: download stub installer for release. Note that ESR releases 67 // will (eventually) also go this route, and the stub installer is responsible 68 // for installing the supported release for the new machine. 69 std::wstring product = L"firefox-stub"; 70 #elif defined(MOZ_BRANDING_IS_NIGHTLY) 71 // Nightly build: download the latest Firefox Nightly installer 72 std::wstring product = L"firefox-nightly-stub"; 73 #elif defined(MOZ_BRANDING_IS_BETA) 74 // Beta edition build: download the latest Firefox Beta installer 75 std::wstring product = L"firefox-beta-stub"; 76 #elif defined(MOZ_BRANDING_IS_DEVEDITION) 77 // Dev edition build: download the latest Firefox Devevloper Edition installer 78 std::wstring product = L"firefox-devedition-stub"; 79 #elif defined(MOZ_BRANDING_IS_UNOFFICIAL) 80 // For unofficial/local builds, download the nightly version. The advantage of 81 // this, over the release version, is that it uses the full installer, which 82 // gives the user the chance to cancel installation. 83 std::wstring product = L"firefox-nightly-stub"; 84 #else 85 // Unexpected case. Fail the build here so we can figure out the missing case. 86 static_assert(false); 87 #endif 88 89 return L"?os=" + arch.value() + L"&lang=" + lang + L"&product=" + product; 90 } 91 92 /** 93 * To exit from the WinHttp callback, you remove the callback from the 94 * request object. Additionally, since the download_firefox function is 95 * blocked waiting for completion, we need to signal the event that it is 96 * waiting on. 97 */ 98 static void exitCallback(DownloadContext* context, ErrCode exitStatus) { 99 context->asyncStatus = exitStatus; 100 WinHttpSetStatusCallback(context->hrequest, nullptr, 101 WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, NULL); 102 SetEvent(context->eventHandle); 103 } 104 105 /** 106 * Async event handler for the WinHttp request, satisfying type 107 * WINHTTP_STATUS_CALLBACK 108 */ 109 static void CALLBACK AsyncHttpStatusCallback(HINTERNET hInternet, 110 DWORD_PTR dwContext, 111 DWORD dwInternetStatus, 112 void* lpvStatusInformation, 113 DWORD dwStatusInformationLength) { 114 DownloadContext* context = (DownloadContext*)dwContext; 115 DWORD dwCount = 0; 116 DWORD dwResponseStatus; 117 wchar_t contentTypeBuf[256]{}; 118 switch (dwInternetStatus) { 119 case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE: 120 // We have completed sending the request. Now tell the API to receive a 121 // response. 122 if (!WinHttpReceiveResponse(context->hrequest, nullptr)) { 123 exitCallback(context, ErrCode::ERR_RECEIVE); 124 } 125 break; 126 case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE: { 127 dwCount = sizeof(DWORD); 128 if (!WinHttpQueryHeaders( 129 context->hrequest, 130 WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, 131 WINHTTP_HEADER_NAME_BY_INDEX, &dwResponseStatus, &dwCount, 132 WINHTTP_NO_HEADER_INDEX)) { 133 exitCallback(context, ErrCode::ERR_HEADER); 134 return; 135 } 136 if (dwResponseStatus != 200) { 137 if (dwResponseStatus == 404) { 138 exitCallback(context, ErrCode::ERR_FILE_NOT_FOUND); 139 } else if (dwResponseStatus >= 400 && dwResponseStatus < 500) { 140 exitCallback(context, ErrCode::ERR_CLIENT_REQUEST); 141 } else { 142 exitCallback(context, ErrCode::ERR_SERVER); 143 } 144 return; 145 } 146 dwCount = sizeof contentTypeBuf; 147 if (!WinHttpQueryHeaders(context->hrequest, WINHTTP_QUERY_CONTENT_TYPE, 148 WINHTTP_HEADER_NAME_BY_INDEX, contentTypeBuf, 149 &dwCount, WINHTTP_NO_HEADER_INDEX)) { 150 exitCallback(context, ErrCode::ERR_HEADER); 151 return; 152 } 153 // We have received the headers. Call query data to start the reading loop 154 if (!WinHttpQueryDataAvailable(context->hrequest, nullptr)) { 155 exitCallback(context, ErrCode::ERR_QUERY_DATA); 156 } 157 } break; 158 case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE: 159 // We have data available. Tell the API to put it in our buffer 160 dwCount = *((DWORD*)lpvStatusInformation); 161 if (dwCount == 0) { 162 // nothing available. We must be done. 163 exitCallback(context, ErrCode::OK); 164 return; 165 } 166 if (dwCount > sizeof buffer) { 167 dwCount = sizeof buffer; 168 } 169 ZeroMemory(buffer, sizeof buffer); 170 if (!WinHttpReadData(context->hrequest, buffer, dwCount, nullptr)) { 171 exitCallback(context, ErrCode::ERR_READ_DATA); 172 } 173 break; 174 175 case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: 176 dwCount = dwStatusInformationLength; 177 if (!context->dataSink->accept((char*)lpvStatusInformation, dwCount)) { 178 exitCallback(context, ErrCode::ERR_FILE); 179 } 180 // Is there more? 181 else if (!WinHttpQueryDataAvailable(context->hrequest, nullptr)) { 182 exitCallback(context, ErrCode::ERR_QUERY_DATA); 183 } 184 break; 185 186 case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: 187 exitCallback(context, ErrCode::ERR_REQUEST_INVALID); 188 break; 189 } 190 } 191 192 /** 193 * Attempt to download a file from an HTTP service, sinking its data to the 194 * specified DataSink object 195 * @param dataSink A DataSink object that will accept downloaded data 196 * @param server_name The DNS name of the HTTP server 197 * @param server_port The port number for the HTTP service 198 * @param is_https Whether this is a secure HTTPS service or not 199 * @param object_name the file path and query parameters to include in the HTTP 200 * request 201 * @return An ErrCode enum that indicates the success or failure of the download 202 * attempt 203 */ 204 ErrCode download_file(DataSink* dataSink, std::wstring server_name, 205 int server_port, bool is_https, std::wstring object_name, 206 std::wstring contentType) { 207 DownloadContext context{}; 208 context.asyncStatus = ErrCode::UNKNOWN; 209 context.dataSink = dataSink; 210 context.contentType = contentType; 211 const wchar_t* accept_types[] = {contentType.c_str(), nullptr}; 212 // Create an event to be used in signaling between our WinHttp callback, which 213 // runs asynchronously, and this function. 214 context.eventHandle = CreateEventW(nullptr, false, false, nullptr); 215 if (!context.eventHandle) { 216 return ErrCode::ERR_EVENT; 217 } 218 // Initiate a WinHttp session. 219 // Note: The WINHTTP_FLAG_SECURE_DEFAULTS flag instructs WinHttp to use secure 220 // settings, such as disabling fallback to old versions of TLS, but it has the 221 // side-effect of also forcing the session to be in async mode. 222 context.hsession = 223 WinHttpOpen(user_agent, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, 224 WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 225 is_https ? WINHTTP_FLAG_SECURE_DEFAULTS : WINHTTP_FLAG_ASYNC); 226 if (context.hsession == nullptr) { 227 return ErrCode::ERR_OPEN; 228 } 229 // Create a (disconnected) connection by specifying the server and port 230 context.hconnection = 231 WinHttpConnect(context.hsession, server_name.c_str(), server_port, 0); 232 if (!context.hconnection) { 233 return ErrCode::ERR_CONNECT; 234 } 235 // Create an HTTP request object by specifying the verb (GET) and name 236 // (path/params) for the URL, as well as some other properties. 237 context.hrequest = WinHttpOpenRequest( 238 context.hconnection, L"GET", object_name.c_str(), nullptr, 239 WINHTTP_NO_REFERER, accept_types, is_https ? WINHTTP_FLAG_SECURE : 0); 240 if (!context.hrequest) { 241 return ErrCode::ERR_OPEN_REQ; 242 } 243 WINHTTP_STATUS_CALLBACK statusCallback = AsyncHttpStatusCallback; 244 // Register the async callback to be used in handling the request 245 if (WinHttpSetStatusCallback(context.hrequest, statusCallback, 246 WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, 247 NULL) == WINHTTP_INVALID_STATUS_CALLBACK) { 248 return ErrCode::ERR_SET_CALLBACK; 249 } 250 // Actually send the request 251 if (!WinHttpSendRequest(context.hrequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, 252 WINHTTP_NO_REQUEST_DATA, 0, 0, (DWORD_PTR)&context)) { 253 return ErrCode::ERR_SEND; 254 } 255 256 // Wait for the async request to complete 257 ErrCode result; 258 if (WaitForSingleObject(context.eventHandle, timeout_ms) == WAIT_OBJECT_0) { 259 // async request completed 260 result = context.asyncStatus; 261 } else { 262 // Timed out waiting for the async request to complete 263 result = ErrCode::ERR_TIMEOUT; 264 } 265 return result; 266 } 267 268 static const wchar_t* server_name = L"download.mozilla.org"; 269 static const wchar_t* installer_content_type = L"application/x-msdos-program"; 270 static const int standard_server_port = INTERNET_DEFAULT_HTTPS_PORT; 271 static const bool standard_is_https = true; 272 273 /** 274 * Attempt to download the Firefox stub installer, sinking its data to the 275 * specified DataSink object 276 * @param dataSink A DataSink object that will accept downloaded data 277 * @return An ErrCode enum that indicates the success or failure of the download 278 * attempt 279 */ 280 ErrCode download_firefox(DataSink* dataSink) { 281 std::optional<std::wstring> object_name_opt = get_object_name(); 282 std::wstring object_name; 283 if (object_name_opt.has_value()) { 284 object_name = object_name_opt.value(); 285 } else { 286 return ErrCode::ERR_ENVIRON; 287 } 288 289 return download_file(dataSink, server_name, standard_server_port, 290 standard_is_https, object_name, installer_content_type); 291 }